Différences entre versions de « P2E-THEREMIN »
(2 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 36 : | Ligne 36 : | ||
{{rbox-orange| Toutes les photo-résistances étant différentes comme les conditions d'illumination, de fait, vous ne disposerez peut-être que de quelques notes réparties sur quelques centimètres. }} | {{rbox-orange| Toutes les photo-résistances étant différentes comme les conditions d'illumination, de fait, vous ne disposerez peut-être que de quelques notes réparties sur quelques centimètres. }} | ||
− | Comparez | + | Comparez la hauteur affichées dans la sortie REPL avec la réalité, si celle-cis diffèrent de trop alors il faudra envisager le calcul de vos propres valeurs C et D. |
== A propos de la Photo-résistance == | == A propos de la Photo-résistance == | ||
Ligne 214 : | Ligne 214 : | ||
Cette version du Théremin divise l'espace des 30cm au dessus de la photo résistance en une progression linéaire de la fréquence entre 523 Hz (DO4) à 1046 Hz (DO5). | Cette version du Théremin divise l'espace des 30cm au dessus de la photo résistance en une progression linéaire de la fréquence entre 523 Hz (DO4) à 1046 Hz (DO5). | ||
+ | |||
+ | {{rbox-orange|Ne pas oublier de presser sur le bouton pour démarrer la génération du son!}} | ||
<syntaxhighlight lang="python" line> | <syntaxhighlight lang="python" line> |
Version actuelle datée du 10 avril 2025 à 21:30
Theremin
Le thérémine est un des plus anciens instruments de musique électronique datant des années 1920.
Initialement équipé de deux antennes, l'instrument produit des sons sans être touché par l’instrumentiste. Une main commande la hauteur de la note en faisant varier sa distance à l’antenne verticale tandis que l’antenne horizontale utilisée avec l'autre main contrôle le volume.
Leon Thérémine et son instrument.
La vidéo "The theremin - A short introduction to a unique instrument" (YouTube) est une excellente introduction moderne au Thérémine.
Application au Pico-2-Explorer
Le Pico-2-Explorer ne dispose pas d'antennes, il est cependant possible d'utiliser la photo-résistance pour évaluer grossièrement la hauteur de la main (au dessus de celle-ci).
Nous savons:
- d'une part, que la résistance de la photo-résistance change en fonction de quantité de lumière qui l'atteint.
- d'autre part, la quantité de lumière qui atteint la photo-résistance dépend de la proximité de la main.
Reste à identifier la relation qui pourrait exister entre la hauteur de la main (H) et la valeur de la photo-résistance (R).
Des expériences passées, nous savons aussi qu'il existe une relation proportionnelle entre la résistance et tension de sortie (voir les tutoriels sur le potentiomètre et sur la photo-résistance).
Il est donc tout à fait raisonnable d'éliminer calculs intermédiaires en essayant d'identifier une relation directe entre H (hauteur de la main) et la donnée obtenue sur l'entrée analogique (valeur obtenue à l'aide de ADC.read_u16().
Spoiler
Ce projet Theremin fait intervenir deux paramètres C et D identifier par expérience.
Ce développement est quelque peu technique et permet d'identifier les valeurs de C et D permettant de calculer la hauteur de la main entre 1 et 30 centimètre.
Comparez la hauteur affichées dans la sortie REPL avec la réalité, si celle-cis diffèrent de trop alors il faudra envisager le calcul de vos propres valeurs C et D.
A propos de la Photo-résistance
Une même surface masquant la photorésistance, il est instinctivement possible de se rendre compte que luminosité captée va croître/décroître avec le carré de la distance (puisque l'ombre se propage dans les deux directions de l'espace (X et Y).
Cette relation au carré de la distance, même si pas encore démontrée, existe aussi pour la puissance sonore (qui diminue avec le carré de la distance).
C'est pour cette raison que les potentiomètres de réglage de volumes n'ont pas de fonction linéaire mais une fonction logarithmique. Cela donne l' impression à l'oreille d'avoir un réglage du volume qui augmente linéairement avec la rotation du potentiomètre.
Linéaire ou pas, quel importance?
Le but du Theremin est de faire varier le son de façon proportionnelle avec la position de la main au dessus de la photorésistance.
Il est donc nécessaire d'avoir une relation linéaire entre la position de la main et la fréquence du son produit.
Savoir si la relation lumière/résistance de la photo-résistance est linéaire ou pas a donc de toute importance!
Si, comme supposé, la relation lumière/résistance est logarithmique cela signifie que la majorité de la variation de résistance (donc de fréquence du son) s'opère a proximité de la photo-résistance. Dans pareil cas, il faudra faire en sorti de reconvertir la réponse logarithmique vers une réponse linéaire.
Mesures sur la photo-résistance
![]() |
Ce point et le suivant ne sont utiles que si vous avez besoin de calculer vos propres valeurs de C et D. Ce qui pourrait être utile étant donné que toutes les photo-résistances ont des caractéristiques différentes. |
Il est très facile de se faire une idée de la réponse lumière/résistance en utilisant des conditions d'illumination constante et en pratiquant une série de mesure en positionnant la main à une hauteur donnée puis en mesurant la réponse sur le convertisseur ADC.
En utilisant le câblage du Theremin, le petit script read.py (présent dans le projet Theremin) permet d'effectuer les relevés nécessaires "sans main" (la valeur 99999) puis à 30cm, 25cm, 20cm, 15cm, 10cm, 5cm et enfin 1cm simplement en pressant le bouton lorsque la main est positionnée.
Les valeurs obtenues sont encodées sur 10 bits significatifs (donc de 0 à 1023) pour rester au dessus du bruit (voir tutoriel sur l'entrée analogique).
import micropython, time
from machine import Pin, ADC
micropython.alloc_emergency_exception_buf(100)
counter = 0
def bouton_cb( obj ):
global counter
counter += 1
# https://docs.micropython.org/en/latest/library/machine.Pin.html
# https://docs.micropython.org/en/latest/reference/isr_rules.html
btn = Pin( Pin.board.GP16, Pin.IN, Pin.PULL_UP )
btn.irq( handler=bouton_cb, trigger=Pin.IRQ_FALLING )
a1 = ADC( Pin( Pin.board.GP27 ) )
read_for_cm = [99999,30,25,20,15,10,5,1]
index = 0
print( "read for %i cm" % read_for_cm[index] )
while True:
if counter>0: # Button pressed?
val = 0
# Acquire analog on 10 bits
val = 0
for i in range( 10 ):
val += (a1.read_u16()>>6)
# calculer la moyenne
val = val/10
print( " %i of 1024" % vald )
index += 1
if index>=len( read_for_cm ):
index = 0
# next value
print( "read for %i cm" % read_for_cm[index] )
# Reset button detection
time.sleep_ms( 500 )
counter=0
else:
time.sleep_ms( 200 )
Ce qui permet de faire un relevé comme celui-ci dessous.
read for 99999 cm 793 of 1024 read for 30 cm 770 of 1024 read for 25 cm 758 of 1024 read for 20 cm 755 of 1024 read for 15 cm 742 of 1024 read for 10 cm 708 of 1024 read for 5 cm 634 of 1024 read for 1 cm 499 of 1024
A noter également le minima de 500 et maxima de 793.
En reportant les valeurs dans un tableur, il est clairement possible de voir la progression logarithmique avec le plafonnement vers le maxima.
remarque: la distance 99999 est ici remplacée par 50 pour faciliter le rendu du graphique.
Non seulement cela ressemble à une progression logarithmique mais il serait aussi possible de faire coller un logarithme népérien (fonction ln().
La fonction s'exprime, en première approximation, sous la forme:
y = ln(x)*C+D
avec:
- D = valeur à 1cm car x=1 => ln(1)=0 .
- C = (valeur_à_30cm - D) / ln(30) .
Avec les relevés ci-avant, cela donne:
- D = 499
- C = (770-499)/ln(30) = 79.6778221
La fonction d'approximation s'exprimerait donc:
y = ln(x) * 79.6778221 + 499
La graphique ci-dessous reprend la fonction g(x)=ln(x)*79.6778221+499 (en rouge) et la compare avec les données expérimentales (en bleu).
Ce n'est pas une parfaite correspondance mais c'est suffisamment proche pour passer au point suivant.
Fonction réciproque
Maintenant que nous avons identifié la fonction y = f(x) sous la forme:
y = ln(x) * C + D y = ln(x) * 79.6778221 + 499
Avec:
- y la valeur du convertisseur ADC (sur 10 bits)
- x la position de la main au dessus du capteur en cm.
Ce qui serait intéressant, c'est de pouvoir calculer la fonction réciproque x = f-1(y) .
La hauteur de la main étant linéaire, la fonction réciproque permettra aussi produire une variation linéaire de la fréquence du son avec la hauteur de la main.
La réciproque de ln() est la fonction exp() .
x = exp( (y-D)/C ) x = exp( (y-499)/79.6778221 )
Par exemple, si le convertisseur analogique (ADC) retourne la valeur 10bits de 680 (y=680), la main est située à 9.69 cm comme l'indique le calcul ci-dessous:
x = exp( (680-499)/79.6778221 ) x = 9.69 cm
Brancher
Pico | Pico-2-Explorer |
GP16 | Btn3 |
GP27 | Photo-résistance |
GP13 | Buzzer (cavalier place) |
Le bouton permet d'activer/désactiver la génération du signal sonore. Ne pas oublier de presser le bouton après avoir démarré le script.
Code : fréquence linéaire
Le script theremin.py est disponible dans le dépôt dédié au Pico-2-Explorer.
Cette version du Théremin divise l'espace des 30cm au dessus de la photo résistance en une progression linéaire de la fréquence entre 523 Hz (DO4) à 1046 Hz (DO5).
1 from machine import Pin, ADC, PWM
2 from math import exp
3 from maps import map
4 import micropython, time
5 micropython.alloc_emergency_exception_buf(100)
6
7 C = 79.6778221
8 D = 499
9
10 DO4 = 523
11 DO5 = 1046
12
13 Pin( 23, Pin.OUT, value=True )
14
15 counter = 0
16 def bouton_cb( obj ):
17 global counter
18 counter += 1
19
20 btn = Pin( Pin.board.GP16, Pin.IN, Pin.PULL_UP )
21 btn.irq( handler=bouton_cb, trigger=Pin.IRQ_FALLING )
22
23 buzzer = PWM( Pin( Pin.board.GP13 ) )
24 buzzer.freq( 500 )
25 buzzer.duty_u16( 0 ) # silence
26
27 a1 = ADC( Pin( Pin.board.GP27 ) )
28 while True:
29 if counter>0:
30 if buzzer.duty_u16() == 0:
31 buzzer.duty_u16( 65535//3 )
32 else:
33 buzzer.duty_u16( 0 )
34 time.sleep_ms(300)
35 counter=0
36 continue
37 # lecture analogique sur 10 bits
38 val = 0
39 for i in range( 10 ):
40 val += (a1.read_u16()>>6)
41 val = val/10 # Moyenne
42
43 cm = exp( (val-D)/C )
44
45 freq = int( map( cm, 1, 30, DO4, DO5 ) )
46 buzzer.freq( freq )
47 print( 'adc_10bits=%5i , cm=%2.2f, freq=%i Hz' % (val,cm,freq) )
48 time.sleep_ms( 20 )
Voici quelques informations concernant ce script.
- Ligne 1 : importation des classes permettant de contrôler les broches (Pin), une entrée analogique (ADC) et la génération d'un signal PWM.
- Ligne 2 : importation de la fonction exp() permettant de faire le calcul mathématique de l'exponentielle.
- Ligne 3 : importation de la fonction map() utilisée pour réaliser une interpolation linéaire.
- Ligne 5: : étant donné que cet exemple utilisera le mécanisme d'interruption, il est nécessaire d'allouer un espace mémoire permettant la transmission des messages d'erreurs.
- Lignes 7 et 8 : paramètres de la fonction réciproque permettant de transformer la valeur du convertisseur analogique (valeur ADC) en une distance exprimée en centimètres.
- Ligne 13 : désactivation du mode d'économie d'énergie du régulateur. Cela permet de réduire le bruit sur l'étage d'alimentation (bruit que l'on retrouverait sur l'entrée analogique).
- lignes 15 à 21 : mise en place de la détection du bouton sur GP16 (voir le projet sonnette).
- Lignes 23 à 25 : définition de l'objet buzzer sur GP13 et utilisé pour produire du son. La fréquence est fixée à 500 hertz et le cycle utile du signal PWM à 0%. Le buzzer est donc silencieux.
- Ligne 27 : définition de l'objet a1, entrée analogique (classe ADC) associée à la broche GP27.
- Lignes 28 à 48 : boucle infinie exécutant les tâche suivantes:
- activation/désactivation du buzzer lorsque le bouton est pressé.
- lecture analogique de la photorésistance sur 10 bits (au lieu de 16 bits afin de réduire l'impact du bruit).
- calcul de la distance en cm.
- conversion de la distance en fréquence
- modification de la fréquence du buzzer
- Ligne 29 : exécution des lignes 30 à 36 si le bouton a été pressé.
- Lignes 30 et 31 : si le buzzer ne produit pas de son alors le cycle utile du signal PWM est à zéro. Dans pareil cas, le cycle utile du buzzer est configuré sur 30% et il commence à produire du son à la fréquence PWM actuelle configurée. Remarque: l'opération '//' permet de réaliser une division entière (sans partie décimale).
- Ligne 32 et 33 : le buzzer produisait du son, il faut donc arrêter le générateur PWM en fixant le cycle utile sur 0.
- Ligne 34 à 36 : attendre 300ms pour éviter tous les problèmes de rebonds et laisser le temps à l'utilisateur de retirer le doigt du bouton. Ensuite le counter est replacé à zéro et l'instruction continue redémarre un nouveau tour de la boucle while.
- Lignes 38 à 41 : lecture analogique d'une valeur sur 10bits (0 à 1023) stockage de cette valeur dans la variable val. Application du principe des moyennes sur 10 échantillons. Cette méthode est expliquée dans le tutoriel Entrée analogique .
- Ligne 43 : calcul de la distance de la main (en centimètre) à partir de la valeur obtenue sur le convertisseur analogique val. La variable cm ainsi obtenue contient , en principe, une valeur située entre 1.0 et 30.0 (avec décimales).
En conséquence, le contenu de cm peut s'écarter significativement des valeurs 0 à 30. Elle pourrait allègrement atteindre 100cm comme ne jamais pouvoir atteindre 30cm.
Si tel était le cas, n'hésitez pas à calculer les valeurs de C et D correspondant à vos conditions d'utilisation (cfr: début de cette page).- Ligne 45 : utilisation de la fonction map() pour convertir la distance cm en une fréquence située entre les fréquences DO4 = 523 Hertz (DO4) et 1046 (DO5).
- Ligne 46 : modification de la fréquence du signal PWM. Si le buzzer est actif (cycle utile > 0) alors cette modification est immédiatement audible. Dans le cas contraire la fréquence sera déjà fixée en attente de la prochaine réactivation du buzzer.
- Ligne 47 : affiche toutes les informations pertinentes sur la sortie REPL. Cela permet, en autre, d'identifier les gamme de valeur de la photo-résistance sur du convertisseur ADC ainsi que l'amplitude des distances mesurées.
- Ligne 48 : petite pause de 20ms afin de ne pas surcharger le microcontrôleur.
Code : transformation en notes
Le script précédents permet d'explorer toute la gamme de fréquence. Cela peut être très amusant mais d'un intérêt musical plutôt limité.
Cette seconde version du script Théremin divise l'espace des 30cm au dessus de la photo résistance en 12 espacements identiques correspondant au 13 notes présentes entre DO4 et DO5.
C'est qu'en fonction de la position de la main, le script détermine la note de musique inférieure la plus proches et produit celle-ci sur le buzzer. La note ne changera que lorsque la position de la main aura atteint une nouvelle position permettant passer sur une autre note de musique.
Voici le script theremin2.py, également disponible dans le dépôt dédié au Pico-2-Explorer.
1 from machine import Pin, ADC, PWM
2 from math import exp
3 from maps import ranking
4 import micropython, time
5 micropython.alloc_emergency_exception_buf(100)
6
7 C = 79.6778221
8 D = 499
9
10 DO4 = 523
11 DO4diese = 554
12 RE4 = 587
13 RE4diese = 622
14 MI4 = 659
15 FA4 = 698
16 FA4diese = 740
17 SOL4 = 784
18 SOL4diese = 831
19 LA4 = 880
20 LA4diese = 932
21 SI4 = 988
22 DO5 = 1046
23
24 NOTES = [ DO4, DO4diese, RE4, RE4diese, MI4, FA4, \
25 FA4diese, SOL4, SOL4diese, LA4, LA4diese, SI4, DO5 ]
26
27 # CM between notes (ranges_of_cm / intervalles)
28 cm_spacing = (30-1) / (len(NOTES)-1)
29 distances = []
30 for i in range( len(NOTES) ):
31 distances.append( 1+i*cm_spacing )
32
33 Pin( 23, Pin.OUT, value=True )
34
35 counter = 0
36 def bouton_cb( obj ):
37 global counter
38 counter += 1
39
40 btn = Pin( Pin.board.GP16, Pin.IN, Pin.PULL_UP )
41 btn.irq( handler=bouton_cb, trigger=Pin.IRQ_FALLING )
42
43 buzzer = PWM( Pin( Pin.board.GP13 ) )
44 buzzer.freq( 500 )
45 buzzer.duty_u16( 0 ) # No sound
46
47 a1 = ADC( Pin( Pin.board.GP27 ) )
48 while True:
49 # turn on/off the buzzer
50 if counter>0:
51 if buzzer.duty_u16() == 0:
52 buzzer.duty_u16( 65535//3 )
53 else:
54 buzzer.duty_u16( 0 )
55 time.sleep_ms(300)
56 counter=0
57 continue
58
59 val = 0
60 for i in range( 10 ):
61 val += (a1.read_u16()>>6)
62 val = val/10
63
64 cm = exp( (val-D)/C )
65
66 freq = ranking( cm, distances, NOTES ) # Notes are int.
67 if freq!=None:
68 buzzer.freq( freq )
69 print( 'adc_10bits=%5i , cm=%2.2f, freq=%i Hz' % (val,cm,freq) )
70 time.sleep_ms( 20 )
Le corps du code est très similaire à la précédente version. La principale différence réside dans l'utilisation de la fonction ranking() au lieu de la fonction map() .
A propos de ranking()
La fonction ranking() a été ajoutée à la bibliothèque LIBRARIAN/maps.py le 6 avril 2025 (la première génération de carte Pico-2-Explorer devra faire une mise-à-jour du fichier lib/maps.py ).
La fonction ranking() permet de catégoriser facilement une valeur d'entrée parmi une liste de valeurs précisées dans `ranges`. La fonction permet également de transposer la valeur catégorisée avec une des valeurs de sortie mentionnées dans `transposed` (si celui-ci est défini).
def ranking( value, ranges, transposed=None )
Paramètres:
- value: valeur comparée aux différents intervalles. Lorsqu'une valeur tombe dans un intervalle, la limite inférieure de celui-ci est retourné (ou valeur transposée correspondante). None est retourné si value est en dessous du premier intervalle.
- ranges : liste des valeurs croissantes définissant les n intervalles.
- transposed : (optionnel) liste des valeurs transposées correspondant à aux intervalles. Si définit c'est la valeur transposée qui est retournée n lieu et place de la borne inférieure de l'intervalle.
Retour:
La borne inférieure de l'intervalle dans lequel la valeur value tombe ou None. Si une liste de valeurs transposée existe alors le résultat (borne inférieure) est extrait de la liste transposed avant d'être retourné.
ranking() par l'exemple
Les deux exemples ci-dessous permettent d'appréhender le fonctionnement de ranking() avec un usage simple et un usage avec donnée transposée.
result = ranking( val, [10,20,30,40] )
- retourne None si val < 10
- retourne 10 si val>= 10 et val < 20
- retourne 20 si val>= 20 et val < 30
- retourne 30 si val>= 30 et val < 40
- retourne 40 si val>= 40
result = ranking( val, [10,20,30,40], ["A", "B", "C", "D"] )
- retourne None si val < 10
- retourne "A" si val>= 10 et val < 20
- retourne "B" si val>= 20 et val < 30
- retourne "C" si val>= 30 et val < 40
- retourne "D" si val>= 40
A propos du code
Les deux script Theremin et Theremin2 étant fort semblables, y compris dans leur fonctionnement, ces explications ci-dessous vont uniquement se concentrer sur les éléments distinctifs.
- Ligne 3 : importation de la fonction ranking en lieu et place de la fonction maps.
- Lignes 10 à 22 : définition des constantes avec les fréquences des différentes notes sur un octave
- Ligne 24 : création de la constante NOTES reprenant les différentes fréquences. Son évaluation produit la liste suivante [523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1046] .
- Ligne 34 : diviser l'espace disponible (entre 1 et 30cm) en 12 intervalles identiques (avec 13 notes, il faut 12 intervalles). Cet espace cm_spacing est évalué à 2.41cm entre chaque note. Pour rappel, l'évaluation de la position de la main se fait en centimètre.
- Lignes 35 à 37 : calcul de la liste distances correspondant à chacune des notes. L'évaluation de distances produit la liste suivante [1.0, 3.41, 5.83, 8.25, 10.66, 13.08, 15.5, 17.91, 20.33, 22.75, 25.16, 27.58, 30.0] .
La main se trouvant entre 1.00 et 3.41cm produira la note DO4 (523 hertz), tandis que la main positionnée entre 5.83 et 8.25cm produira la note RE4 (587 hertz). - Ligne 75 : l'utilisation de la fonction ranking() permet de sélectionner la distance immédiatement inférieure et comme la liste NOTES est fournie au paramètre transposed alors ranking() retourne la note correspondante (la fréquence de la note pour être plus préçis).
- Ligne 76 : étant donné qu'il n'est pas exclus de recevoir une valeur None, il est préférable de ne pas essayer de l'assignée comme fréquence PWM.
Maintenant, le Thérémin joue uniquement des notes de musiques.
Ressources
- Bibliothèque LIBRARIAN et son fichier readme.
- Machine.Pin : cette ressource de MicroPython reprend, entre autre, la documentation sur les interruption et les broches.
- Développement avec interruption : cette ressource de MicroPython reprend de nombreuses recommandations concernant le développement de routines d'interruption avec MicroPython.
Traduction augmentée réalisée par Meurisse. D pour shop.MCHobby.be - Licence CC-BY-SA.