A(cco)rduino

Le projet en bref

L'autre jour je voulais accorder une guitare électrique. Je m'arme donc d'un accordeur électronique qui traînait dans un coin, branche mon jack, puis, Horreur; les piles étaient plates. Je suis donc allé au magasin en acheter des neuves. J'ai donc opter pour la seule solution envisageable afin de résoudre ce problème de taille: designer mon propre accordeur avec un microControleur.

screen

Sommaire

  1. Un peu de théorie
    1. La fondamentale et les harmoniques
    2. Le monde magique de Fourier
  2. Le Hardware
    1. La source de tension 2.5v
    2. Amplification du signal
    3. Filtrage du signal
    4. La SRAM
    5. L'affichage
  3. Le Software
    1. Les unions
    2. Les interruptions
    3. La Fast Fourier Transform
    4. Calcul de la tonalité
  4. Downloads

Un peu de théorie

La fondamentale et les harmoniques

Une corde de guitare (bien accordée) vibre avant tout à la fréquence de sa fondamentale :

Fréquences guitare
Corde Note Fréquence
1 Mi 82.4 Hz
2 La 110.0 Hz
3 146.8 Hz
4 Sol 196.0 Hz
5 Si 246.9 Hz
6 Mi 329.6 Hz

Fréquences guitare basse
Corde Note Fréquence
1 Mi 41.2 Hz
2 La 55.0 Hz
3 73.4 Hz
4 Sol 98.0 Hz

Mais également aux fréquences de ses harmoniques. Les harmoniques sont dites d'un certain ordre. Une harmonique du deuxième ordre à une fréquence double à celle de la fondamentale, une du troisième ordre triple, du quatrième quadruple etc.. Le nombre d'harmoniques présentes dans le signal sonore est difficile à prédire. Elles dépendent essentiellement du type ainsi que de la qualité de l'instrument. Pour la petite histoire, les premiers synthétiseurs furent réalisés avec des signaux sinusoïdaux pure où l'on superposait un certain nombre d'harmoniques à la fondamentale, ce qui donnait des sons métallisés et peu agréables à l'écoute.

Une analyse spectrale nous permet de mettre en évidence ces harmoniques :

Mi_spectrum

On remarque deux propriétés intéressantes: (1) L'amplitude de la fondamentale n'est pas forcément la plus grande dans notre signal. Ici c'est l'harmonique du deuxième ordre qui a l'amplitude la plus importante. (2) Les harmoniques ne sont pas des multiples fréquentiels exactes de la fondamentale, ce qui montre que notre système physique n'est pas parfaitement descriptible par un modèle mathématique simple.

Fréquences guitare basse - Mi
harmonique Fréquence
1 41.99 Hz
2 81.83 Hz
3 122.7 Hz
4 163.7 Hz
5 203.5 Hz
6 244.4 Hz

Une résolution mathématique afin de trouver le mode vibratoire de notre corde est possible. La solution est directement, comme beaucoup d'autres phénomènes physiques, liée à l'équation des ondes. Ici on ne donnera que le résultat du développement ainsi que quelques explications:

ondes

La fonction y(x,t) est la position de la corde perpendiculairement à x au temps t (x allant de 0 à L, L étant la longueure de la corde). Il s'agit d'une somme infinie des n harmoniques, Bn est l'amplitude de la nème harmonique. Le terme en sinus donne la forme de vibration (le nombre "d'arc" formé) et le terme en cosinus la fréquence de vibration. v est la vitesse de propagation de l'onde dans la corde. Elle est dépendante du type de matériaux ainsi que de la tension de la corde.

vib_corde1
trois premières harmoniques d'une corde avec le nombre "d'arcs" formés
vib_corde2
fondamentale, fondamentale avec une harmonique, fondamentale avec deux harmoniques

Le monde magique de Fourier

Quoi !?
Tu veux faire ton accordeur avec une transformation de Fourier !
Casse-toi pas la tête et utilise un convertisseur fréquence - tension.

Effectivement un convertisseur fréquence - tension pourrait amplement suffire à notre projet. D'ailleurs ça serait un choix judicieux vu la facilité de mise en oeuvre.

MAIS :

  • Les circuit intégré de transformation fréquence - tension pour notre gamme de fréquence sont difficiles à trouver.
  • Leurs résolutions sont limitées. D'une part intrinsèquement, d'autre part à cause du microControleur.
  • Les nombreuses harmoniques des instruments musicaux fausseront nos mesures.
  • Ils ne peuvent suivre qu'une seule fréquence en même temps.
  • La transformation de Fourier, c'est bien plus cool à implémenter qu'un convertisseur fréquence - tension.

---------------

La transformation de Fourier est un outil mathématique de base du couteau Suisse en traitement du signal. Elle nous permet de représenter un signal temporel selon les fréquences sinusoïdales qui le compose.
Pour comprendre plus facilement comment elle fonctionne, on illustrera le cas du signal de la corde Mi d'une guitare basse (La plus grosse).

Lorsque l'on joue notre Mi, la corde vibre d'une certaine façon périodique. Nos micros, constitués d'aimants ainsi que d'une bobine de fil, sont capables de retranscrire cette vibration en une tension électrique.

MI_signal signal électrique

On remarque que notre note jouée est composée de plusieurs signaux sinusoïdaux d'intensités différentes :

MI_freq Légende : fréquences et amplitudes des sinusoïdes constitutives du Mi MI_recon

Si l'on regarde ces sinusoïdes du Mi d'un point de vue vectoriel, on peut réaliser des projections sur d'autre sinusoides. De la même manière que l'on peut projeter un vecteur bidimensionnel sur un autre :

vector

C'est de cette manière que fonctionne la transformation de Fourier. Elle projette un signal quelconque sur des "vecteur sinusoïdaux". Si notre signal est uniquement composé d'une sinusoïde pure de fréquence 42Hz, il sera décomposé dans le domaine de Fourier sur le vecteur correspondant à une sinusoïde pure de 42Hz.

La base de Fourier est composée de toutes les sinusoïdes pures possible et imaginable (1Hz, 5.6Hz, 135.28Hz, 1'865'254Hz...). Il nous faudra donc limiter cette base afin de pouvoir calculer de manière numérique la transformation. C'est la transformation de Fourier discrète. On choisit un nombre de "vecteur sinusoïdaux" sur lesquels on projettera notre signal. Dans notre cas il s'agit de limiter ces vecteurs jusqu'à celui correspondant à 330Hz qui est la fréquence de la corde la plus aiguë d'une guitare.

Pour réaliser cette limitation, on utilise une fréquence d'échantillonage double à celle de notre fréquence maximale, c'est-à-dire 660Hz. Ainsi, d'après le théorème d'échantillonage, on aura la garantie que nos fréquences jusqu'à 330Hz seront correctement projetée.

Afin de pouvoir réaliser notre accordage de manière continue, il nous faudra également limiter notre nombre d'échantillons. Si l'on veut que notre A(cco)rduino effectue une nouvelle mesure de l'accordage toutes les deux secondes, on sera limité à 1320 échantillons (660Hz*2sec).
Ceci aura un impact important sur notre transformation. La résolution spectrale sera limitée par Fs/N(1). Au lieu d'obtenir une transformation composée de toutes les fréquences comprises entre 0 et 330Hz, on aura une transformation composée par les fréquences entre 0 et 330Hz par palier de 0.5Hz (0Hz, 0.5Hz, 1Hz, ..., 329.5Hz, 330Hz).
Avec cette résolution, un signal sinusoïdale de 1.25Hz sera décomposé sur les "vecteurs sinusoïdaux" de 1Hz et de 1.5Hz de manière égale. On peut donc mettre à notre avantage cette propriété pour la détection du signal de 1.25Hz.

(1) Fs : fréquence d'échantillonage, N : nombre d'échantillons

Le Hardware

sketch

La source de tension 2.5v

Une tension de 2.5v nous permet d'ajouter un offset au signal électrique retransmit par les vibrations des cordes. Cet offset est indispensable car le convertisseur analogique-numérique du microControleur possède un full-scale de 0v à 5v. Ainsi, sans cet offset, on serait dans l'incapacité de convertir la moitié du signal reçu :

offset_expl

Notre source est réalisée grâce à un pont diviseur de deux résistances et d'un amplificateur opérationnel fonctionnant en suiveur de tension. L'impédance de sortie de l'ampli-op étant très faible, la tension délivrée est (presque) indépendante de ce que l'on branche en aval. Ce qui n'est pas le cas d'un pont diviseur de deux résistances.

sketch01

Amplification du signal

Le signal électrique restitué par notre instrument ne dépassant guère la dizaine de millivolt (pour une guitare basse), il nous faudra obligatoirement l'amplifier afin de pouvoir le détecter par L'Arduino. Pour ce faire, un ampli-op est utilisé de la manière suivante :

sketch02 sketch02formula

L'inductance "Guitar Inductor" symbolise le circuit intérieur à notre instrument. En réalité il est légèrement plus complexe qu'une simple inductance, mais pour un schéma de principe cette représentation est suffisante. Lorsque notre corde est statique, la tension sur la borne "1" du MCP6002 est de 2.5V. En faisant varier la position de la corde, cette tension change légèrement. C'est cette différence de potentiel qui est amplifiée par notre ampli-op. On notera que l'impédance d'entrée de l'ampli-op est beaucoup plus grande (plusieurs ordres de grandeur) que celle de notre instrument. Ainsi, ce montage est sans danger vis-à-vis des courants circulant dans notre instrument.

Filtrage du signal

En fin de circuit on ajoute un filtre passe-bas afin de couper les fréquences supérieures à notre fréquence d'intérêt qui est de 329.6Hz.

sketch03

La fonction de transfert "H(jw)" du filtre (fonction reliant la tension d'entrée avec la tension de sortie) est donnée par :

sketch03_formula
"j" est le nombre complexe : j2 = -1
"w" est la pulsation (w = 2*pi*fréquence)

développement complet 
sketch03_formulabis
Où V(jw) est la fonction v(t) dans le domaine de Fourier. On notera que la fonction d'intégration est un multiplication par 1/jw dans le domaine de Fourier.

Le graphique suivant nous montre l'amplitude du signal de sortie en fonction de la fréquence par rapport à l'amplitude du signal d'entrée. Il s'agit simplement de la norme de la fonction de transfert à différente fréquence. Dans les basses fréquences* la totalité du signal est restitué (0dB), alors qu'en hautes fréquences* le signal est coupé. On notera la fréquence de coupure à -3dB (fréquence à laquelle la puissance de sortie est deux fois moindre que celle d'entrée) d'environ 400Hz.

*par rapport à la fréquence de coupure

low_pass

La SRAM

L'une des limitations principales de notre microControleur est la RAM à disposition. 2048 octets en tout et pour tout, ce qui nous permet de stocker 512 float et donc 256 échantillons (la transformation de fourrier nécessite deux float par échantillon). Heureusement des ICs SRAM existes et sont facilement exploitables. Ici on utilisera un 23LC1024. Ce circuit fonctionne à une fréquence maximale de 20MHz (L'arduino UNO à une fréquence de clock de 16MHz). On peut y stocker jusqu'à 128k octets (donc 16000 échantillons). La communication entre notre SRAM et l'Arduino s'effectue par SPI. Pour ceux qui ne sont pas familiers de ce mode de communication, un bref coup d'oeil à la Datasheet du composant s'impose :

rw_sequences

CS : Chip Select, pin de selection de l'IC avec lequel le microControleur communique.
SCK : Serial Clock, signal de clock du microControleur
SI : Serial Input (communication microControleur -> IC)
SO : Serial Output (communication IC -> microControleur)

Premièrement, il nous faudra ramener le pin CS à un niveau logique bas :

digitalWrite(sramCSPin, LOW);

Puis, le microControleur envoie l'octet d'instruction (0x02 pour l'écriture, 0x03 pour la lecture) :

SPI.transfer(instruction);

Ensuite vient l'adresse de l'octet auquel on souhaite accéder :
*address est un unsigned int sur 4 octets

SPI.transfer(address >> 16);
SPI.transfer(address >> 8);
SPI.transfer(address);

L'octet que l'on souhaite lire/écrire :

readByte = SPI.transfer(0xFF);
SPI.transfer(writeByte);

Finalement, on ramène le pin CS à un niveau logique haut :

digitalWrite(sramCSPin, HIGH);

On notera que durant ces communications il est impératif de désactiver les interruptions (si on en utilise). Divers modes de lecture/écriture existent (Byte/Page/Stream). De plus, il nous faudra configurer notre objet SPI dans le code de l'Arduino avant de démarrer toute communication.

SPI.begin();
digitalWrite(sramCSPin, HIGH);
pinMode(sramCSPin, OUTPUT);
SPI.beginTransaction(SPISettings(unoFreq, MSBFIRST, SPI_MODE0));

Pour changer le mode de lecture/écriture, il nous faut modifier le Mode Register (commande 0x01). Dans le cadre de notre projet on utilise le Stream mode (commande 0x41) :

digitalWrite(sramCSPin, LOW);
SPI.transfer(0x01);
SPI.transfer(0x41);
digitalWrite(sramCSPin, HIGH);

Un dernier point important : la communication est très sensible. Assurez vous d'utiliser des câbles de bonne qualité entre l'Arduino et l'IC. J'avais utilisé des vieux câbles chinois scandaleusement bon marché, ce qui m'a valu de nombreuse heures d'incompréhension sur le pourquoi du comment la moitié de mes communications se soldaient par un échec de lecture/écriture.

L'affichage

Pour l'affichage, l'écran LCD que la plupart des kits de démarrage Arduino fournissent fera amplement l'affaire. La particularité est que l'on utilise un shift register 74HC595N afin de le piloter. Ceci nous permet de diminuer le nombre d'entrées/sorties utilisées sur l'Arduino. Dans le cadre de ce projet il n'est pas nécessaire d'employer cette technique particulière, mais c'est une habitude que j'ai prise. Référez-vous à cette page pour en savoir plus.

Software

Les unions

Les plus attentifs auront remarqué que l'on peut uniquement lire/écrire byte par byte dans notre SRAM. Le fait que l'on utilise des float (formatage sur quatre bytes) pour la représentation des échantillons pourrait poser certains problèmes si les unions n'existaient pas dans notre langage de programmation. Les unions nous permettent de représenter la même zone de mémoire sous plusieurs formes différentes. Ici, on utilisera la représentation float de la mémoire lorsque l'on veut exécuter des calculs et la représentation byte[4] lorsque l'on effectue les cycles de lecture/écriture.

union MyUnion{
float flt;
byte byteArray[4];
};

union MyUnion var;
var.flt = 5.25*2;
byte firstByte = var.byteArray[0]; // 01000001
byte secByte = var.byteArray[1]; // 00101000
byte thirdByte = var.byteArray[2]; // 00000000
byte fourthByte = var.byteArray[3]; // 00000000

Les interruptions

Les interruptions nous permettent de sortir de notre programme séquentiel principal et d'effectuer un bout de code spécifique à une fréquence régulière. Ici on utilisera donc une sous-routine afin d'acquérir le signal à une fréquence d'environ 800Hz. Pour en savoir plus, référez-vous à cet article très détaillé sur ce sujet.

La Fast Fourier Transform

La FFT est un algorithme pour le calcul de la transformation de Fourier discrète (avec des échantillons donc). Ici on ne va pas détailler précisément comment l'algorithme fonctionne, mais plutôt faire quelques considérations quant à son coût calcul.
L'algorithme utilisé est celui de Cooley–Tukey. Il est effectué avec une complexité O(N*log2(N)). Où N est notre nombre d'échantillons. Sa limitation est que N doit être une puissance de 2. Ainsi, par exemple, pour 1024 échantillons, il nous faudra 1024*log2(1024) = 10240 tours de boucle pour effectuer notre transformation. En regardant uniquement les accès mémoire SRAM, cela correspondra à :

t_access
A chaque tour de boucle, on effectue deux accès de lecture et deux accès d'écriture. Tdata vaut 8*8*Tck, car pour chaque échantillon, il y a deux float (donc 64bits en tout).


Ainsi, après nos 10240 tours de boucle, les accès mémoires nous auront pris :

t_access1

A ces temps d'accès mémoires, il faudra évidemment ajouter les temps nécéssaire aux opérations de base réalisées. Empiriquement, notre FFT est effectuée en :

t_fft

De plus, on effectue l'échantillonage de la FFT suivante en même temps que le calcul de la FFT précédente. Ainsi, les temps de calculs sont légèrement plus élevés (car l'algorithme est souvent interrompu par la sous-routine d'échantillonage) :

t_fft2

Calcul de la tonalité

Et voilà, une fois que l'on a filtré puis acquis nos N échantillons, que l'on a effectué notre FFT sur autant d'échantillons grâce à notre SRAM et que l'on a détecté la fréquence dominante du signal, il ne nous reste plus qu'à faire de légers calculs pour retranscrire cette fréquence en une note et afficher le résultat. En début d'article, on a discuté brièvement des fréquences des cordes d'une guitare. Ci-dessous sont affichés les fréquences de toutes les notes.

tone1

On remarquera que la fréquence d'une note à une autre change par un facteur 2^(1/12). Ainsi, il est possible de rapporter n'importe quelle note à un multiple entier d'une note de base via une relation logarithmique. Par exemple, nous utiliserons ici la fréquence comprise entre le do(0) et le Si(-1) comme fréquence de base, c'est-à-dire 31.772Hz. En effectuant le logarithme de base 2^(1/12) de notre fréquence résultante de la FFT par cette fréquence de base, on obtiendra un nombre compris entre 0 (lorsque la fréquence vaut 31.772Hz) et 48 (lorsque la fréquence vaut 380.84 Hz).

float noteFloat = MajorPeak(); // fréquence dominante
noteFloat = log(noteFloat/31.77219916)/0.057762265; // 0.057762265 = log(2^(1/12))

*pour effectuer le logarithme de base x de A, on effectue le logarithme népérien (naturel) de A divisé par le logarithme népérien de x.

Ensuite il nous faut séparer la partie entière de la partie décimale. On effectura un modulo de 12 (reste de la division entière) de la partie entière afin d'obtenir un nombre entre 0 et 11. Ce nombre définira notre note (de Do à Si). La partie décimale nous donne l'information sur la justesse de l'accordage; lorsque cette partie décimale est comprise entre 0 et 0.5 la note est trop basse, lorsqu'elle vaut 0.5 la note est exacte et lorsqu'elle est comprise entre 0.5 et 1 la note est trop haute.

uint8_t noteInt = (uint8_t)noteFloat; // partie entière
noteFloat -= (float)noteInt; // partie décimale
noteInt %= 12; //modulo par 12 de la partie entière

screen2
On modifie l'alignement en fonction de la justesse de l'accordage.

Downloads

L'archive du sketch Arduino