Un petit synthetiseur à tables d'ondes avec un arduino



Encore un petit projet à base d'arduino. Il s'agit d'un mini synthetiseur à tables ondes (monophonique dans un premier temps) tournant sur une base arduino uno (ou un montage quelconque du moment qu'il s'agit d'un ATMEGA328P). Cela dit il sera très facile de porter le programme sur un atmega 2560 afin de disposer notamment de plus de mémoire. J'insiste sur le nom "MINI synthetiseur", car celui-ci est bien loin d'avoir tous les raffinements d'un synthé à table d'ondes classique...



Fonctionnalités



Voici la liste des fonctionnalités qui m'intéressent dans ce synthétiseur... Voici donc les fonctionnalités de base. A la date du 12 octobre 2013, une partie des fonctions sont actives. Je peux utiliser une quinzaine de forme d'ondes sur chaque oscillateur, le déclenchement des notes via midi est OK, le réglage des paramètres de base via les encodeurs et le LCD sont OK. Le pilotage du DAC principal via un DAC secondaire fonctionne aussi ainsi que l'enveloppe. Le mixage grossier des amplitudes des 2 oscillateurs est fonctionnel ainsi que le décalage en demi-tons.



Le principe DDS



Le principe mis en oeuvre est appelé synthèse DDS (digital direct synthesis).

Chaque table d'onde contient un cycle d'une forme d'onde. Les données sont stockées en 8 bits et nous avons 256 échantillons par table. Donc, pour stocker un cycle complet d'une forme d'onde sinusoidale nous avons 256 octets de données. Les tables d'ondes de bases sont stockées en flash via la primitive PROGMEM afin de stocker les données non pas en RAM mais en mémoire programme.

Pour les tables d'ondes qui seront ajoutées par l'utilisateur, elles seront copiées via des programmes additionnels dans l'EEPROM, le chargement se fera de la même facon que d'habitude en manoeuvrant les encodeurs, les 20 premières formes d'ondes seront copiés depuis la mémoire flash, les suivantes depuis l'EEPROM.

Lorsque nous sélectionnons une forme d'onde (peu importe la provenance, FLASH ou EEPROM) elle est recopiée dans un tableau de travail en RAM.

Exemple de table d'onde pour du sinus :

prog_uchar PROGMEM sinus[] = { // Sinus wave 128,131,134,137,140,143,146,149,152,156,159,162,165,168,171,174, 176,179,182,185,188,191,193,196,199,201,204,206,209,211,213,216, 218,220,222,224,226,228,230,232,234,236,237,239,240,242,243,245, 246,247,248,249,250,251,252,252,253,254,254,255,255,255,255,255, 255,255,255,255,255,255,254,254,253,252,252,251,250,249,248,247, 246,245,243,242,240,239,237,236,234,232,230,228,226,224,222,220, 218,216,213,211,209,206,204,201,199,196,193,191,188,185,182,179, 176,174,171,168,165,162,159,156,152,149,146,143,140,137,134,131, 128,124,121,118,115,112,109,106,103,99, 96, 93, 90, 87, 84, 81, 79, 76, 73, 70, 67, 64, 62, 59, 56, 54, 51, 49, 46, 44, 42, 39, 37, 35, 33, 31, 29, 27, 25, 23, 21, 19, 18, 16, 15, 13, 12, 10, 9, 8, 7, 6, 5, 4, 3, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 16, 18, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 42, 44, 46, 49, 51, 54, 56, 59, 62, 64, 67, 70, 73, 76, 79, 81, 84, 87, 90, 93, 96, 99, 103,106,109,112,115,118,121,124 };

Pour générer la forme d'onde à la sortie du synthétiseur, il faut lire les données et les recopier dans le DAC. Les DAC utilisés sont des MCP4921. Ce sont des DAC 12 bits qui se programment via le bus SPI, la recopie des données est donc très rapide. Nous verrons plus loin comment copier les données dans les DAC.

Dans un synthétiseur, la fréquence du signal doit forcément varier en fonction de la note appuyée sur le clavier, ca parait logique. Nous avons 2 possibilités :
- soit recopier plus ou moins rapidement des données dans le DAC, plus on recopie rapidement, plus la fréquence du signal de sortie augmente. On peut le faire en executant une procédure de copie de données via une interruption déclenchée par un timer dont on fait varier les registres de comparaison en fonction de la note jouée mais c'est un peu compliqué car il faudrait aussi jouer sur les prescaler.
- soit utiliser le principe de DDS (Direct Digital Synthesis - Synthese Numerique Directe)

Dans le principe de la méthode DDS (je vous invite à regarder de près ce qu'il en est sur Internet, car l'explication que je donnerai ici est plus que vulgarisée...), la fréquence à laquelle nous recopions les données dans le DAC est fixe (Quelque soit la fréquence du signal à la sortie...) Nous allons donc fixer une fréquence d'échantillonnage de l'ordre de 16khz (15625 Hz exactement). Avec ce système, les données sont donc recopiées régulièrement à une fréquence fixe dans le DAC, ce qui nous simplifie la tâche. Afin de recopier les données régulièrement nous allons utiliser une interruption déclenchée par un compteur et nous utiliserons le timer 2.

Pour fixer la fréquence de 15625 Hz, nous utiliserons un prescaler de 8, ce qui permet une incrementation du compteur à 16mhz / 8 soit 2Mhz. Pour arriver à la fréquence de 15625 Hz, il suffit de programmer une interruption sur dépassement d'un registre de comparaison que nous allons fixer à 127, donc nous redivisons en fait par 255-127=128 et donc, 2 000 000 / 128 = 15625Khz. Ce qui donne le code suivant pour la partie timer 2

// Autoriser comparaison TIMSK2 = (1 << OCIE2A); // chargement registre de comparaison OCR2A = 127; // CTC Mode TCCR2A = 1<<WGM21 | 0<<WGM20; // Prescaler a 8 TCCR2B = 0<<CS22 | 1<<CS21 | 0<<CS20;
Une fois que ces données sont en place, nous aurons une interruption déclenchée régulièrement à 15625Khz et une execution automatique d'une procédure nommée "ISR(TIMER2_COMPA_vect)".

C'est dans cette interruption que nous calculerons l'échantillon et que nous le recopierons dans le DAC.

Pour mettre en place la DDS nous utilisons un registre de phase et une variable permettant le tuning des données, c'est le registre de saut de phase. Le principe ne consiste plus à lire les données octet par octet sucessifs dans la table, l'index sera calculé. Il variera suivant la fréquence.

Pour trouver l'index voulu à un instant 't', il nous faut d'une part un accumulateur et un registre de saut de phase.

On ajoute dans l'accumulateur une constante qui dépend de la fréquence du signal que vous voulons générer, donc de la fréquence de la note que nous voulons jouer, c'est le registre de saut de phase, (nous verrons comment le calculer, le contenu de registre dépend donc de la fréquence de la note à jouer. Si vous jouez 10 fois la même note, le registre de saut de phase prendra 10 fois cette même valeur), cet accumulateur s'incrémente donc. Ici c'est un accumulateur 16 bits, dés qu'il dépasse la valeur de 65535 il repart à zero automatiquement.

Nous prenons ensuite l'octet de poids fort de cet accumulateur et il nous servira d'index pour lire les données dans la table d'onde. Comme cet index est une valeur de 8 bits, il prendra donc forcément des valeurs de 0 à 255, c'est pour cela que chacune de nos tables ondes contient 256 valeurs. On lit donc la valeur dans la table et on la recopie dans le DAC via le bus SPI.

Résumons :

Nous voyons donc que plus la fréquence augmente, plus l'écart entre les index varie, ce qui va détériorer nécessairement le signal puisqu'il sera de moins en moins complet. Un filtre passe-bas est donc nécessaire à la sortie afin de ne pas trop entendre les imperfections du son (Aliasing dans notre cas)

Nous avons 2 oscillateurs. Que cela ne tienne, il suffit d'avoir 2 accumulateurs séparés, de parcourir 2 tables, d'extraire 2 données. Comment les mixer ? Il suffit de les ajouter. Si nous voulions 4 oscillateurs, il suffirait d'avoir 4 accumulateurs, etc, etc. Nous pouvons aussi faire facilement de la polyphonie. Par exemple pour 4 notes, il suffirait d'avoir 4 accumulateurs séparés et de lire dans la (ou les) table(s) d'ondes. Il suffit ensuite d'ajouter les données afin de constituer un echantillon à envoyer dans le DAC. Bien sur il existe des limites :
Si nous prenons par exemple 2 tables ondes avec une polyphonie de 4 notes, nous aurons au minimum 4 accumulateurs (en supposant que nous utilisons le même accumulateur pour chaque note et comme index équivalent dans les 2 tables, sinon nous aurons besoin de 8 accumulateurs...). Ensuite il faudra chercher les données 4 fois dans les 2 tables, soit ramener 8 octets de données qu'il faudra ajouter dans une variable afin de constituer l'échantillon, ce qui représente 8 additions. Il faut ensuite recopier les données en SPI dans le DAC. Et tout cela doit être effectué 15625 fois par seconde, autant dire que cette routine d'interruption doit être optimisée. Et je n'ai pas parlé de l'enveloppe qui elle aussi consomme quelques cycles puisqu'elle nécessite la recopie de donnée dans un autre DAC.



Le registre de saut de phase



Nous avons vu plus haut que ajoutons les données du registre de saut de phase (qui dépend de la fréquence de signal à obtenir) à l'accumulateur.

Le registre de saut de phase, contient donc une valeur qui dépend directement de la note jouée sur le clavier. Nous pourrions la calculer en temps réel, mais cela consommerait du temps machine, d'autant plus que pour une note donnée, la valeur à ajouter est une constante.

Voici la formule de calcul :
increment = freq * (2^16 / 15625) En clair, le registre de saut de phase (ou la constante d'incrementation) est égal a : la frequence de la note à jouer, multipliée par la valeur maxi de l'accumulateur , donc 2 élevé à la puissance du nombre de bits de l'accumulateur (16 bits donc) le tout divisé par la frequence d'échantillonnage (15625Khz)
On voit que 2ˆ16 est une valeur fixe, ainsi que 15625. La seule variable est la fréquence. Etant donné que nous allons jouer sur un clavier midi, nous pouvons connaitre les fréquences exacte de chaque note en fonction de son numéro de note midi. Donc, il est facile de pré-calculer un tableau qui pour chaque numero de note midi nous donne la valeur du registre de saut phase. Cela prend un peu de place en mémoire mais évite de faire des calculs.

La valeur du registre de phase n'aura besoin d'etre extraite qu'a chaque appui sur une nouvelle note, ou à chaque changement de fréquence par le pitch-bend par exemple.

Voici donc le tableau en question déja calculé :
prog_uint16_t PROGMEM MidiIncr[] = { 34, 36, 38, 41, 43, 46, 48, 51, 54, 58, 61, 65, 69, 73, 77, 82, 86, 92, 97, 103, 109, 115, 122, 129, 137, 145, 154, 163, 173, 183, 194, 206, 218, 231, 244, 259, 274, 291, 308, 326, 346, 366, 388, 411, 435, 461, 489, 518, 549, 581, 616, 652, 691, 732, 776, 822, 871, 923, 978, 1036, 1097, 1163, 1232, 1305, 1383, 1465, 1552, 1644, 1742, 1845, 1955, 2071, 2195, 2325, 2463, 2610, 2765, 2929, 3104, 3288, 3484, 3691, 3910, 4143, 4389, 4650, 4927, 5220, 5530, 5859, 6207, 6576, 6968, 7382, 7821, 8286, 8779, 9301, 9854, 10440, 11060, 11718, 12415, 13153, 13935, 14764, 15642, 16572, 17557, 18601, 19707, 20879, 22121, 23436, 24830, 26306, 27870, 29527, 31283, 33143, 35114, 37202, 39414, 41758, 44241, 46872, 49659, 52612 };
Mettons que nous jouions la note C5 (note numéro 48 en midi), les tableaux de correspondances que nous trouvons facilement sur internet nous donne la fréquence de 130.812hz environ. Si on applique la formule, on aura un saut de phase de :

saut_phase = freq * 65536 /15625 soit saut_phase=548,665 on arrondit à 549. Plutot que de faire les calculs, on utilise le tableau MidiIncr et on recherche la valeur à l'index 48. Si vous observez le tableau ci-dessus, vous verrez que :
MidiIncr[48]=549
Ca tombe bien et c'est fait pour, le tableau est précalculé pour les 127 notes midi. Maintenant, rien ne vous empêche de faire du micro-tuning et d'accorder votre clavier comme bon vous semble en mettant d'autres valeurs.

Nous avons donc notre tableau MidiIncr[] le contenu des valeurs de registre de saut de phase en fonction de la note à jouer. Il suffit dextraire la bonne valeur dés qu'une note est appuyée. Cela va se faire dans les handler midi qui sont automatiquement déclenchés quand une note change ou quand le pitch-bend varie.

Voici un exemple de calcul dans la partie Note-On midi. Le tableau incr[] contient les valeurs des registres de saut de phase pour les oscillateurs 1 et 2.
incr[0] = pgm_read_word_near( MidiIncr + pitch + semi[0] - 12); incr[1] = pgm_read_word_near( MidiIncr + pitch + semi[1] - 12);
pgm_read_word_near permet de lire un mot de 16 bits dans un tableau en mémoire flash et prend en parametre l'adresse du mot à lire. Cette fonction renvoie un mot de 16 bits. Le paramètre passé à cette fonction est l'adresse du mot à lire. Qu'elle est cette adresse ?

C'est d'abord l'adresse de notre tableau MidiIncr
à laquelle on ajoute un déplacement correspondant au numéro de la note jouée (Pitch)
auquel on ajoute un déplacement de -12 à +12, en fonction du réglage de demi-ton de l'oscillateur. Comme le réglage de demi-ton va de 0 à 24, on retranche 12 pour avoir des valeur de -12 à +12.

Ainsi pour la note numero 36, l'index 36 du tableau renvoie 274. Ce qui veut dire qu'a chaque note numero 36 qui sera jouée, le registre de saut de phase vaudra systématiquement 274.

Cette valeur doit être recalculée si nous changeons de note ou si le pitch-bend fait varier la fréquence de l'oscillateur.

Voici la forme d'onde de base sur 256 poins, si elle était lu avec un index qui s'incrémente de 1

sine1.png

Et la même forme d'onde, toujours avec 256 points, mais maintenant lue en fonction de la note C7, avec un incrément de 2195 dans l'accumulateur. La courbe n'a pas l'air si mal comme ca, mais nécessairement, elle comporte des valeurs qui sont arrondies puisque qu'on affiche plus de 8 cycles avec toujours 256 points.

sine2.png



Calcul de l'échantillon



Nous avons identifié la note jouée par le handler midi. Nous récupérons le numéro de la note. Ce numéro sert d'index pour trouver la valeur du registre de saut de phase.

Il reste maintenant à ajouter cette valeur dans un accumulateur au niveau de l'interruption déclenchée à 15625hz. Une fois ajoutée, on extrait la partie haute de cet accumulateur et le nombre obtenu nous donne l'index à lire dans la table d'onde. Il suffit de lire la valeur dans le tableau wavetable[] pointé par cette index, nous obtenons la valeur de l'échantillon, il suffit de recopier cette valeur dans le DAC.

Nos accumulateurs ( 1 par oscillateur) sont des accumulateurs 16 bits. Comme je prévois de mixer 2 oscillateurs (on a 2 table ondes) et 2 autres oscillateurs en plus pour le mode unison, cela porte à 4 le nombre d'oscillateurs, il me faut donc 4 accumulateurs. J'ai fait un tableau d'accumulateur qui se nomme pos[].

Pos[0] est l'accumulateur de l'oscillateur 1
Pos[1] est l'accumulateur de l'oscillateur 2
Pos[2] est l'accumulateur de l'oscillateur 1 bis en mode unison
Pos[3] est l'accumulateur de l'oscillateur 2 bis en mode unison

Voici le principe de calcul des accumulateurs, nous nous concentrons ici uniquement sur les 2 oscillateurs de base 1 et 2.

En entete de programme déclaration du tableau volatile uint16_t pos[] = {0,0,0,0}; Il est déclaré en volatile parce qu'il est modifié dans l'ISR et il pourrait être modifié aussi en dehors. Une programmation de base comme celle qui suit pourrait suffire pos[0] += incr[0]; // Increment wavetable 1 pos[1] += incr[1]; // Increment wavetable 2 P0=highByte(pos[0]); // On garde l'octet de poids fort pour l'index de position dans la table onde * ampli oscillateur P1=highByte(pos[1]); // On garde l'octet de poids fort pour l'index de position dans la table onde * ampli oscillateur Dans PO et dans P1 on a nos 2 index pour lire des données dans nos tables d'ondes. Il faut ensuite aller chercher les données et fabriquer un échantillon. samplet = wavetables[P0]; samplet1 = wavetables[P1+256]; samplet est un échantillon contenant la valeur de l'échantillon lu sans la premier table d'ondes en mémoire. samplet1 est la même chose mais sur la deuxieme table, comme les 2 tables se suivent il suffit de décaler de 256 octet. On va ensuite mixer les 2 valeurs, simplement en incrementant sampleT avec la valeur de samplet1 samplet += samplet1; samplet contient maintenant la valeur à recopier dans le DAC. Je rapelle que nous avons un DAC 12 bits, donc celui ci peut prendre en entrée des valeurs entre 0 et 4095. Ici, nos tables d'ondes sont en 8 bits, donc en ajoutant 2 échantillons 8 bits à leur valeur maximum (255) on obtiendra que 510. On peut multiplier cette valeur par 8 pour profiter des 12 bits du DAC, pour cela il suffit de faire un décalage de 3 bits. samplet = samplet >>3;
Voici donc le principe expliqué pour 2 oscillateurs, avec des fonctionnalités de base. Dans mon programme, c'est un peu plus compliqué, car non seulement je peux utiliser 4 oscillateurs en mode unison, mais je fait aussi des calculs pour le pitch bend et je tiens compte d'une modulation de pitch éventuelle par un lfo. Je passe aussi sur le fait que l'amplitide des oscillateurs peuvent être réglé grossièrement à 100¨%, 50% et 25% par un simple décalage de bits.

Donc pour les oscillateurs 1 et 2 je me retrouve avec ca :
pos[0] += incr[0]+bendOsc1+pitchOsc1; pos[1] += incr[1]+bendOsc2+pitchOsc2;
et pour le mode unison s'il est utilisé nous avons cela :
pos[2] += incr[0]-decunison1+bendOsc1+pitchOsc1; pos[3] += incr[1]+decunison2+bendOsc2+pitchOsc2;
C'est un peu différent sur la présentation mais le principe est le même.

Pour ce qui est de l'amplitude grossière de chaque oscillateur, elle est calculée par un décalage de bit. L'amplitude de l'oscillateur 1 est réglé dans la variable VCA_OSC1, celle de l'oscillateur 2 dans VCA_OSC2. La valeur est en pourcentage et seule 3 valeurs sont possibles 25, 50 et 100, ce qui correspond à 25%, 50% et 100%

A 100% je ne fais rien, à 50% je divise la valeur par 2 en décalant d'un bit et à 25% je divise la valeur par 4 en décalant de 2 bits. Ce qui donne un test comme il suit, par exemple pour l'oscillateur 1.
switch(VCA_OSC1) { case 100: samplet = wavetables[P0]; break; case 50: samplet = wavetables[P0] >> 1; break; case 25: samplet = wavetables[P0] >> 2; break; }
Nous avons donc notre échantillon final calculé, il reste à le recopier dans le DAC



Recopie des données dans le DAC



Le DAC est du type MCP4921. C'est un DAC alimenté en 5V. Il a besoin d'une tension de référence pour générer une tension de sortie en fonction du contenu de son registre interne. Si on fixe en dur cette tension de référence à +5V, une valeur de 0 écrite dans le DAC, générera une tension de sortie de 0V. Une écriture de la valeur 4095, générera en sortie une tension de 5V et toute les valeurs intermédiaires entre 0 et 4095, généreront une tension proportionnelle entre 0 et 5V. C'est tout.

J'ai utilisé 2 DAC de type MCP4921, vous pouvez prendre un modèle MCP4922 qui aurait l'avantage de contenir 2 DAC dans un seule boitier...

Voici le brochage du DAC

DAC4921-1.png

Pour utiliser un composant SPI il faut d'abord le sélectionner en mettant sa pin de sélection (CS) à l'état bas (0V). Ensuite on écrit les données. Le registre interne du DAC est un registre 16 bits, mais seuls 12 bits sont utilisés pour les données du convertisseur, il reste 4 bits utilisables pour d'autres fonctionnalités du DAC, comme par exemple le fait de multiplier la tension de sortie par 2, d'utiliser le buffer interne ou pas,etc.

Pour le DAC principal (celui qui me sert à générer l'audio), la pin de sélection est branchée sur la pin 9 de l'arduino

Voici la répartition des pins et surtout des ports sur un ATMega328P

pinout-arduino.png

La pin 9 (au sens pin 9 d'entrée/sortie) correspond à PB1, donc le bits 1 (le deuxième en fait vu que le premier est le 0) du PORTB. Afin d'accélérer l'écriture d'une donnée sur juste une pin, on évite d'utiliser la procédure digitalWrite et on préférera l'écriture directe dans le registre qui ne prend qu'un cycle machine. Pour rappel la pin 9 utilisée en programmation correspond à la pin 15 physique du micro-contrôleur ATmega328P.

Pour écrire une donnée dans un composant SPI, on met sa pin CS (chip select) à l'état bas (donc on écrit 0 sur la pin de sortie correspondante sur l'arduino), en clair, on écrit 0 dans le bits 1 du PORTB sans toucher les autres bits de PORTB qui peuvent servir. Il suffit de faire un "ET" logique avec une négation pour mettre le bit à 0 et un "OU" logique pour le mettre à 1.

Comme indiqué plus haut, nous allons nous servir que de 12 bits de données, mais les 4 bits supplémentaires peuvent servir à setter certains fonction, ces 4 bits sont ceux de poids fort du premier octet envoyé, les 3 premiers sont à mettre à 1, il suffira de faire un OU avec la valeur 0x70 pour mettre ces 3 bits à 1. Voici le bout de code qui permet donc de copier l'échantillon dans le DAC.

En SPI, l'octet à copier doit se trouver dans le registre SPDR (SPI Data Register). Une fois ce registre renseigné, les données vont être envoyées sur le bus SPI. Il faut ensuite attendre la fin de la copie. Pour cela, il suffit d'inspecter le registre SPSR (SPI Status Register) et de regarder l'état du bit SPIF (le bit 7 du registre). SPIF est le bit SPI Interrupt Flag. Ce bit est en read only et passe a 1 quand un transfert série est réussi. Si le bit SPIF ou le registre SPSR est lu, ce bit repasse automatiquement à zero, donc pas besoin de le ressetter, il est prêt à l'emploi pour le prochain transfert.

Donc une fois que l'octet de poids fort de nos données est transféré, nous nous mettons en attente du passage à 1 du bit SPIF. Cela se fait en une ligne de code :
while (!(SPSR & (1<<SPIF)));

Il suffit de recopier maintenant l'octet de poids faible de notre échantillon, d'attendre la recopie et de repasser la pin de CS à l'état haut.

Nous avons parlé plus haut de quelques bits supplémentaire sur le DAC. Voici une description du registre d'écriture des DAC MCP4921.

DAC4921-2.png

Les bits 12 à 15 sont les 4 bits qui permettent de changer quelques fonctionnalités sur le DAC. Nous voulons rendre Enable la sortie, donc bit 12 à 1, une gain normal de 1, donc bit 13 à 1, Utiliser Vref, donc bit 14 à 1 et ensuite utiliser le DAC A (il y en a qu'un seul dans nos boitiers) donc bit 15 à 0.

En partant de la gauche nous aurons les données suivantes (x sont les bits de l'échantillons à copier)

Octet poids fort 0111 xxxx
Octet de poids faible xxxxxxxx

Pour setter les 4 premier bits à 0111, il nous faudra un masque de 0x70 en hexadécimal.
Voici le code complet pour la recopie de l'échantillon contenu dans "sample" sur notre DAC.
// Chip select PORTB &= ~(1<<1); // Copie MSB + 4 bits programmation du DAC (buffered, 1x gain et active mode) SPDR = highByte(sample) | 0x70; // Attente de la copie while (!(SPSR & (1<<SPIF))); // Copie du LSB SPDR = lowByte(sample); // Attente de la copie while (!(SPSR & (1<<SPIF))); // Liberation du chip PORTB |= (1<<1);
Le même principe de copie de donnée sera utilisé pour le second DAC. Ce second DAC à une fonction particulière, il va transformer le premier DAC en VCA. C'est maintenant l'occasion de parler de l'enveloppe...



L'enveloppe



L'enveloppe, présente sur la plupart des synthétiseurs un peu évolués, permet au minimum de piloter un VCA (Voltage Control Amplifier). Ce VCA est une sorte de potentiomètre qui serait commandé en tension. Commande à zéro, le potentiomètre est à zero, on a pas de son à la sortie. Commande en tension à fond (par exemple 5V) , le potentiomètre est à fond, le son sort avec un niveau maximum. Ce VCA est piloté par une enveloppe qui comporte 4 segments : Attaque, Decay, Sustain et Release. c'est pour cela qu'on parle d'enveloppe de type ADSR. L'enveloppe est un système capable de générer une tension variable en fonction du temps. Cette enveloppe est déclenchée lors de l'appui sur une note et les segments sont parcouru séquentiellement, d'abord l'attaque, puis le decay, le sustain et au relachement de la note le release. Voici la signification des 4 paramètres.

A la base, aucune touche est appuyée, il n'y a aucun son. Si on appui sur une note, l'enveloppe est déclenchée (Triggée) et on passe à la phase dite "Attaque"
- ATTAQUE : c'est le temps que va mettre le son pour passer du niveau 0 au niveau maximum. Si l'attaque vaut 0, le son passe immédiatement à son maximum. Pour les autres valeurs, tout dépend des graduations. Sur notre synthé le réglage se fait en milli-secondes. Si nous avons 100, le son va passer d'un niveau minimum à un niveau maximum en 100ms, si nous mettons 4 secondes, il prendra 4 secondes pour passer du silence au niveau maximum, etc...
- DECAY : La phase de decay commence lorsque l'attaque est terminée. C'est le temps que va mettre le son pour passer du niveau maximum atteint à la fin de l'attaque, pour redescendre à un niveau fixé par le troisième parametre qui est le SUSTAIN
- SUSTAIN : c'est le niveau sonore de l'oscillateur, lorsque le DECAY est terminée et tant que la note est maintenue appuyée. Si la note reste appuyée pendant 10 secondes, le niveau sonore de l'oscillateur sera celui fixé par la valeur du SUSTAIN pendant 10 secondes.
- RELEASE : Lorsque la note est relachée, la phase de SUSTAIN s'arrête et nous démarrons la phase de release. Le réglage de RELEASE correspond au temps que mettra le synthétiseur pour passer du dernier niveau sonore connu (celui du SUSTAIN) au silence absolu. Si nous mettons par exemple 1 secondes, dés le relachement de la note, le son passera du niveau sonore du SUSTAIN au niveau 0 en une seconde.

Sur un synthétiseur, nous avons en général au moins une enveloppe qui pilote le VCA, mais cette enveloppe peut piloter en parallèle tout autre équipement du synthétiseur qui se commande avec une tension (une ouverture filtre, une fréquence d'oscillateur, voir même un segment d'une autre enveloppe).

Ici, notre enveloppe ADSR se contentera de gèrer le niveau sonore de nos oscillateur. Nous devrions normalement installer un VCA, un composant capable de régler le niveau sonore en fonction de l'enveloppe, mais nous avons décidé de construire quelque chose de simple. L'idée est de nous servir d'une pin spécifique de notre DAC qui gèrer les échantillons. Il s'agit de la pin VRef des MCP4921. Sur cette pin soit se trouver une référence de tension que le DAC utilise pour générer la tension de sortie.

On imagine le DAC alimenté à la base en 5V. Si Vref vaut aussi 5V, lorsque nous écrirons 0 dans le registre du DAC, la sortie sera à 0V, si nous écrivons 4095 (valeur maximum en 12bits) nous aurons 5V à la sortie.

Imaginons que nous continuons à alimenter le DAC en 5V, mais que la pin VREF est à 2V. Si nous écrivons 0 dans le DAC, la sortie sera à 0V, mais si nous écrivons la valeur maimum de 4095, la sortie sera à 2V maximum, nous voyons donc que la tension de sortie dépend de la valeur appliquée sur Vref. Il suffirait d'avoir un dispositif capable de générer une tension qui suit l'enveloppe et d'envoyer cette tension sur VRef pour piloter le niveau de sortie de ce DAC. Quoi de plus simple que d'utiliser un autre DAC pour cette fonction. C'est ce que nous utilions dans ce synthétiser. Nous avons donc un dac chargé de convertir les données numérique en analogique, et l'autre DAC pilote la pin VRef du premier DAC pour gèrer son niveau de sortie.

Comme l'enveloppe nécessite quelques calculs, nous la calculons séparemment dans le programme principal quand celui à du temps. Ce calcul nous donne une valeur qui varie en fonction du temps et bien sur des réglages de l'enveloppe. Cette valeur doit être recopiée dans le second DAC afin de générer une tension et la sortie de ce DAC sera branché directement via une résistance sur la pin VRef de notre convertisseur Digital/Analogique en sortie de synthé.

Pour recopier les données, nous utilisons le même type de procédure que pour le premier DAC. Les 2 DAC sont sur le même bus SPI, seules leur pin CS différent et permet d'adresser l'un ou l'autre des DAC. Le DAC de l'enveloppe est sur la pin 8, celle-ci correspond au bit 0 du PORTB. Le code est donc le même que pour l'autre DAC si ce n'est la partie "Chip select"

// Chip select PORTB &= ~(1<<0); // Copie MSB + 4 bits programmation du DAC (buffered, 1x gain et active mode) SPDR = highByte(sample) | 0x70; // Attente de la copie while (!(SPSR & (1<<SPIF))); // Copie du LSB SPDR = lowByte(sample); // Attente de la copie while (!(SPSR & (1<<SPIF))); // Liberation du chip PORTB |= (1<<0);



Le mode unison



Afin d'épaissir un peu le son, j'ai bidouillé un mode unison. Le mode unison, dans les synthétiseurs analogiques, permettait d'empiler plusieurs oscillateurs afin d'épaissir le son. Comme les oscillateurs étaient libres et manquaient un peu de précision, ils n'étaient jamais vraiment accordé sur la même fréquence et n'était jamais en phase. Cela permettait d'obtenir des sons de basses assez monstrueux. Sur un polysix Korg par exemple, nous avions le mode polyphonique 6 notes permettant de jouer 6 notes en même temps et chacune sur un oscillateur, l'autre mode était le mode Unison qui était monophonique mais les 6 oscillateurs jouait en même temps la même note.

Pour tenter de recréer ce mode, j'ai créé 2 oscillateurs supplémentaires, prenant les données des premiers mais légèrement décalées. Le décalage est calculé dans la boucle principale, (quand elle a le temps et ce n'est pas plus mal car cela rajoute un peu d'incertitude), et à base d'une fonction random, ce qui permet d'obtenir artificiellement un peu d'imprécision. L'oscillateur 3 prend la valeur de l'oscillateur 1 plus le décalage 1, et l'oscillateur 4 prend la valeur de l'oscillateur 2 - le décalage 2.

J'ai ajouté un réglage de profondeur du décalage qui ne fait que déplacer les marges de la fonction random dans la boucle principale.

if (unison) { decunison1=random(1,(incr[0]>>7)*depth_unison); // Calcul du décalage 1 decunison2=random(1,(incr[1]>>7)*depth_unison); // Calcul du décalage 2 }



Les lfos



Nous avons 2 LFO disponibles, ils tournent en free-running indépendamment l'un de l'autre avec chacun leur vitesse.

Il n'y a pour l'instant qu'une seule forme d'onde disponible qui est un signal triangulaire variant en interne de 0 à 127.

La valeur des 2 LFO est calculée dans l'ISR principale.

Notez bien que dans le principe, la mémoire est remplie de table d'ondes, on pourrait les utiliser pour la forme d'onde principale du LFO. A voir plus tard...

Les paramètres qui sont modifiés par les LFO sont calculés dans la boucle principale, s'il y a le temps, cette tâche n'est pas prioritaire en fait...

Lorque la vitesse est de 0, le LFO est Off. La profondeur de LFO se choisi de 1 à 255 et s'applique sur les paramètres suivants :

Pour le LFO 1 :

Pour le LFO 2 (même destination mais sur oscillateur 2:


Particularité, le LFO2 n'a pas le VCA en destination
1) Pour la modulation PWM, la table d'onde est recalculée à chaque changement, on pourrait surement optimiser ca, mais c'était pour ne pas modifier l'ISR et la laisser lire une table d'onde standard en mémoire. 2) Pour la modulation du pitch, une valeur est ajoutée à l'accumulateur de phase. 3) Pour la modulation de phase, l'index lu dans la table est plus ou moins décalé. 4) Pour la modulation du VCA, une valeur variant de 0 à 4096 est soustraite de la valeur normale du VCA calculée par l'ADSR (VCA_12Bits). La valeur finale (VCA) est ensuite recopiée dans le DAC 1 pour fournir une tension qui pilotera l'entrée VRef du deuxième DAC.



Les menus



Voici la liste des menus disponibles à ce jour sur ce synthetiseur. Ces menus sont réduits à leur plus simple expression.

La sélection d'un paramètre se fait généralement avec l'encodeur de gauche. Si une validation est nécessaire, elle se fait en appuyant sur l'encodeur droit, si un choix est nécessaire (modification d'un des paramètres du menu en cours par exemple), ce choix se fait avec l'encodeur droit.

Au fur et à mesure, on descend dans l'arborescence des menus, pour remonter il suffit d'appuyer sur le bouton "Select Menu" et on remonte d'un cran.

Par exemple, pour éditer l'ADSR, on revient sur le menu principal avec le bouton "Menu Select", ensuite on choisit "EDIT" avec l'encodeur gauche, on valide avec l'encodeur droit. On fait défiler les menus avec l'encodeur gauche jusqu'au menu ADSR, on valide avec l'encodeur droit. Arrivé ici, les 4 paramètres de l'ADSR sont accessibles, on les choisit avec l'encodeur gauche, on les change avec l'encodeur droit. Ce principe est à appliquer pour tous les paramètres du synthé. Une fois vos paramètres modifiés vous pourrez les sauvegarder dans un preset. Si vous ne le faites pas, les données seront perdues.

Voici la liste des menus :

MENU PRINCIPAL LOAD



Selection du preset



MENU PRINCIPAL SAVE



Sélection du numero de preset à sauvegarder



Changement du nom du preset si besoin



MENU PRINCIPAL EDIT



Formes d'ondes



20 formes ondes en FLASH, 96 possibles en EEPROM



Volumes coarse des 2 oscillateurs



Réglage 100%, 50%, 25% pour chaque oscillateur



Réglage en demi-tons de chaque oscillateurs



Réglage de -12 à +12 demi-tons.



Réglage de la largeur du carré (si oscillateur square bien sur)



Réglage de 8 à 250 pour la largeur.



Enveloppe ADSR



Premier paramètre : attaque
Deuxième paramètre : decay
Troisième paramètre : sustain
Quatrième paramètre : release




Mode Unison



Choix On/Off et profondeur



Réglage LFO 1 (s'applique sur OSC 1, sauf VCA qui est global)



Premier paramètre la vitesse (de Off à 255)
Deuxième paramètre la profondeur
Troisième paramètre la destination













Réglage LFO 2 (les mêmes que LFO1, mais sans VCA et sur oscillateur 2)



Destination PWM2, PHASE 2 et Pitch 2 (pas de VCA) Ici exemple sur tune2




Le montage



Voici maintenant le montage proprement dit. Cliquez sur le dessin suivant pour voir le schema dans un autre onglet.



Le montage est architecturé autour d'un microcontrolleur ATMEGA328P de chez Atmel. Vous pouvez réaliser ce montage sur une plateforme type Arduino Uno (ou compatible), ou bien faire comme moi un montage sur perfboard y compris le micro-controleur (dans ce cas, il vous faudra transfèrer le programme par une interface type USB2SERIAL via un connecteur ISCP).

Le schéma est assez simple. Nous avons une partie alimentation, qui redresse une tension de 9V alternatif (de 7 à 10V c'est très bien), la régulation est faite à partir d'un regulateur standard type 7805.

L'entrée midi est isolée du micro-controlleur via un optocoupleur. Ici est représenté un 4N25, sur mon montage j'ai mis un 6N138., les 2 sont très bien. Si vous utilisez un 6N138 il faudra le monter comme il suit.
Enfin, du temps s'est écoulé depuis la création de ce montage et compte tenu de la variation de qualité des 4N25 qui de toutes facons était un modèle d'optocoupleur vraiment d'entrée de gamme, je vous conseille "fortement" d'utiliser un 6N138.

midi-6N138.png

Pour le reste j'utilise des encodeurs avec interrupteurs tout ce qu'il y a de plus classique ainsi qu'un interrupteur seul pour la sélection des menus.

La mémoire EEPROM est une 24C256, donc 256Kbits (ou 32Ko).

Les convertisseurs sont des DAC de type MCP4921 qui ont le mérite de fonctionner en SPI. Ce sont des DAC 12 bits qui peuvent être programmés très rapidement.

L'écran LCD est un 2x16 caractères sur BUS I2C. ce bus n'est pas de ce qu'il y a de plus rapide mais il a le mérite de n'utiliser que 2 fils, c'est plus simple à brancher et ca mange moins de ports d'entrée/sortie sur l'arduino.

Vou remarquerez que la sortie d'un DAC (le DAC2 sur le schéma) pilote l'entrée VRef du DAC1. En effet, comme expliqué plus haut, le DAC2 sert à générer une tension d'enveloppe qui pilote l'entrée de tension de référence du DAC1 et cela permet de faire fonctionner le DAC1 en une sorte de VCA.

On note un petit filtrage sur la sortie pour atténuer un peu l'aliasing. Il serait mieux de mettre des inductances à la place des résistances, ca serait meilleur et plus élégant, mais je n'en avais pas sous la main, libre à vous de modifier le filtrage en sortie, cela ne change rien au principe.

Une sortie GATE est présente. La tension sur cette sortie est de 5V maximum, mais elle permet de déclencher n'importe quelle module de mon synthé modulaire, cela parait suffisant.

Petite bricole non présente sur le schéma ci-dessus, j'ai bidouiller un clone de pédale de distortion de type MXR+. Ce petit circuit est construit autour d'un µA741. Cet ampli-op est vraiment cheap, mais justement, au niveau distortion il est vraiment très bien. Comme en plus il ne coute rien et qu'il suffit de rajouter une poignée de composant autour, c'est facile.... Il y a plein de schéma disponibles sur le net, tapez "MXR+ veroboard" ou "741 distortion veroboard" dans google et vous ramenerez plein de schéma.

Voici un exemple possible ci-dessous :



Dans mon synthé, j'ai fixé la distortion, j'ai remplacé le potentiomètre de gain (ou de drive si vous préférez) par une résistance fixe de 150K environ. Le potard de sortie est remplacé lui aussi par une résistance fixe de 10K.

Avec un double inverseur, je "bypasse" ou pas la distortion.

Schéma du double inverseur :





Quelques photos du montage.



Le circuit de distortion





Le module principal avec l'ATMEGA, les DAC, l'EEPROM et l'optocoupleur





Vue arrière de la facade, avec le LCD, le circuit des encodeurs, jack, interrupteur...





L'ensemble des modules avec à gauche les connexions alimentation et MIDI





Voici une vu de la facade. On sent que ce n'est pas un travail d'artiste, il est vrai que j'ai fait ca rapidement et que la dremel a un peut dérapée :)



En haut à gauche le bouton menu select. permet de revenir progressivement à la racine des menus.

Sous l'écran LCD les 2 encodeurs munis chacun d'un interrupteur. L'encodeur 1 permet de sélectionner une fonction ou un sous-menu l'interrupteur de l'encodeur 2 permet de valider. Lorsqu'une page présente plusieurs paramètresn l'encodeur 1 permet de naviguer (de choisir) le parametre et l'encodeur 2 change la valeur de ce paramètre.

En haut à droite la sortie son du synthé, avec en dessous l'interrupteur qui met en/hors service le drive.

A gauche, la sortie Gate qui est déclenchée à chaque appui sur une nouvelle note. La led au dessus s'allume en conséquence.



On pourrait rajouter (ou modifier) pas mal de bricoles sur ce synthé.

- Ajouter une alimentation symetrique de -12V à +12V afin de mieux piloter des éléments externes
- Ensuite bufferiser la sortie GATE
- Mettre un ampli-op sur la sortie du DAC2 avec un gain de 2.5 afin d'avoir une sortie enveloppe de 0 à 15V. ON pourrait même envisager un ampli-op supplémentaire pour avoir une enveloppe inverse et avoir ainsi 2 sorties enveloppes.
- Rajouter un filtre analogique et pouvoir le piloter avec ces enveloppes.

Dans le cadre de l'utilisation dans un synthétiseur modulaire, on pourrait ne garder que les parties oscillateurs et cela aurait les avantages suivants :
- Suppression de la gestion de l'enveloppe et du LFO. L'arduino se contenterait de calculer les échantillons, donc en étant plus rapide, je pense qu'on pourrait monter un peu la fréquence d'échantillonnage.
- nous aurions un DAC dédié par oscillateur, donc mixage inutile en interne, on gagne encore en temps machine.




Le programme



J'allais oublier l'essentiel... Le programme évidemment.

Vous trouvez tout ce qu'il faut dans l'archive ci-dessous, n'hésitez pas à faire des modifications, c'est cadeau...

Archive du programme


A la racine de l'archive (enfin dans le répertoire wavefa), vous avez les procédures principales du programmes regrouper dans différents fichiers. Le principal se nomme "wavefa.ino".

Les autres sous-répertoires concernent des parties additionnelles.

wavefamemwrite sert à formater l'EEPROM pour construire la table des presets.
wavefamemread permet de voir si l'écriture s'est bien passée.
Les sous-répertoire writetable1 à writetable4, sont des petits programmes qui permettent d'écrire des tables d'ondes supplémentaire en mémoire.

Donc, dans l'ordre voici ce qu'il faut faire :

1) construitre le montage et l'alimenter.
2) compiler et transférer wavefamemwrite pour créer la table des presets (eventuellement utiliser wavefamemread pour vérifier)
3) compiler et transférer writetable1 pour générer des tables ondes supplémentaires
4) compiler et transférer writetable2 pour générer des tables ondes supplémentaires
5) compiler et transférer writetable3 pour générer des tables ondes supplémentaires
6) compiler et transférer writetable4 pour générer des tables ondes supplémentaires
7) enfin finir par compiler wavefa et le transfèrer dans l'arduino, c'est le programme principal.



Liens divers



Quelques liens sympathiques qui m'ont bien aidé dans cette réalisation.

new-noises-from-midivox.html

narbotic.com/projects/midivox

arduino-sound-part-1




Exemples de sons


Voici quelques exemples de sons. Pour les jouer, vous devez avoir flash playser d'installé sinon, vous pouvez télécharger les mp3 plus loin.

Sons directs
(même séquence jouée avec différents presets)



Mode Unison
(2 x sans, 2 x avec et ensuite variation profondeur)



Lfo1 sur PWM1
(LFO1 sur largeur de carré oscillateur 1, l'oscillateur 2 est sans LFO)



Lfo sur Pitch1
(LFO1 sur frequence oscillateur 1, l'oscillateur 2 ne change pas de fréquence)



Lfo sur Phase1
(LFO1 sur phase oscillateur 1, l'oscillateur 2 ne change pas de phase)



Lfo sur VCA
(LFO1 sur VCA global, modulation amplitude qui génère un trémolo)



Avec module de distortion
(différents sons sans drive puis avec drive)



Avec filtre externe à base de SSM2044
(La sortie du synthé est envoyé dans un module filtre à base de SSM2044
filtre équivalent à celui d'un Korg Polysix)




Liste des mp3 pour les navigateurs non compatibles HTML5 ou flasplayer.

Sons directs

Mode unison

LFO1 sur PWM oscillateur 1(pas de changement sur OSC2)

LFO1 sur pitch oscillateur 1(pas de changement sur OSC2)

LFO1 sur phase oscillateur 1(pas de changement sur OSC2)

LFO1 sur VCA global (trémolo)

Module de distortion

Synthé branché sur un filtre SSM2044 (genre Polysix Korg)



Liste des tables d'ondes



Voici la liste des tables d'ondes disponibles actuellement. Elles sont représentées dans l'ordre où vous pouvez les sélectionner dans l'interface du synthétiseur.

Une petite exception concerne la forme d'onde carré (Square). Elle est configurée à la base à 50% comme ci-dessous, mais on peut modifier la largeur du carré et faire de la PWM (Pulse Width Modulation).

w00_square.png

Les autres formes d'ondes sont utilisées par contre, telles quelles.

w01_saw.png

w02_triangle.png

w03_sine.png

w04_DigiWave1.png

w05_DigiWave2.png

w06_DigiWave6.png

w07_DigiWave4.png

w08_DigiWave5.png

w09_DigiWave6.png

w10_TanWave1.png

w11_TanWave2.png

w12_TanWave3.png

w13_TanWave4.png

w14_StairExp.png

w15_ParticularHard.png

w16_ParticularSoft.png

w17_FuzzSquare.png

w18_SpikeFred.png

w19_SquareSteps.png

w20_waveprom01.png

w21_waveprom02.png

w22_waveprom03.png

w23_waveprom04.png

w24_waveprom05.png

w25_waveprom06.png

w26_waveprom07.png

w27_waveprom08.png

w28_waveprom09.png

w29_waveprom10.png

w30_waveprom11.png

w31_waveprom12.png

w32_waveprom13.png

w33_waveprom14.png

w34_waveprom15.png

w35_waveprom16.png

w36_waveprom17.png

w37_waveprom18.png

w38_waveprom19.png

w39_waveprom20.png

w40_waveprom21.png

w41_waveprom22.png

w42_waveprom23.png

w43_waveprom24.png


Voila pour la description. Je vous souhaite une bonne construction, vous pourrez aisement utiliser ce montage dans un modulaire et en rajoutant un filtre à sa sortie ca sera vraiment très bien. Vous disposer aussi d'une sortie gate sur le montage qui permettra de déclencher une enveloppe supplémentaire sur le modulaire...