Un petit synthétiseur avec le chips SN76489 Texas Instrument
et un arduino...






montage01.jpg



Le chips sn76489 est un CSG (complex sound generator) conçu dans les années 80 par Texas Instrument.

Ce circuit équipait de nombreuses consoles de jeu comme La Sega Master System, la Game Gear ou la Mega drive et certains ordinateurs personnels de l'époque comme le Ti/99 ou le BBC Micro pour ne citer qu'eux. Ce circuit dispose de 4 voies , il peut donc générer 4 sons simultanément (3 voies pour la musique et une voie plus rudimentaire pour faire du bruit, pour les effets spéciaux dans les jeux par exemple). Chaque canal dispose d'un attenuateur et les 4 canaux sont ensuite mixés en interne et une pin est disponible pour la sortie audio sur le chip.

Le principe de génération de son est basé sur l'utilisation de compteurs, il y en a un pour chaque voie musique (donc 3 compteurs), associés à des registres de comparaison dont le contenu est fixé par l'utilisateur.

Le fonctionnement est simple. Les 3 compteurs sont incrémentés à la fréquence d'une horloge externe (en fait à la fréquence d'une horloge externe divisée par 8 ou 32 suivant les chips). Lorsqu'un compteur atteint la valeur de son registre de comparaison, le compteur est remis à zéro et l'état de la sortie associée s'inverse (si l'état était à 0 il passe à 1 et vice versa).
Les sorties ne pouvant prendre que 2 états les signaux générés sont donc carrés.

L'utilisateur communique avec le chip par l'intermédiaire de registres. On programme ces registres via un bus de données 8 bits. La communication est "write-only", on ne peut qu'écrire sur le bus, en clair, impossible de lire le contenu actuel des registres, c'est à l'utilisateur de mémoriser les états.

L'utilisateur dispose de 8 registres pour communiquer avec le chip :
L'horloge externe doit avoir une fréquence comprise entre 500khz et 4 mhz.

Un arduino va être utilisé (arduino nano dans ma construction mais n'importe quel modèle conviendra) pour communiquer en MIDI avec le chip.


Fonctionnalités prévues



Interface
Implémentation midi




Fonctionnement des registres



Brochage du SN76489.

SN76489-pinout.png


Répartition des pins sur le sn76489

SN76489-pinout.png


Séquencement pour écrire dans les registres.

Si on a qu'un seul chip, on peut négligler la partie /CS. Ce qui revient à executer la séquence suivante.

Le bus ne permet de transférer qu'un octet à la fois, donc 8 bits maximum...

- Certaines données peuvent être transférées en une seule fois, comme par exemple la mise à jour d'un registre atténuateur. - D'autres nécessitent un transfert en 2 parties. - Dans les 2 cas, les données sont d'abord écrites dans une sorte de buffer, le registre de latch ou "Latch Register" (une sorte de registre de travail) et lorsque que le bit "Latch" de ce registre est à 1 on copie l'intégralité des données de ce registre de travail dans le registre de destination.

1 bit permet de d'informer le chips que le transfert est terminé. Lorsque le bit est à un, le registre de transfert est copié dans le registre de destination. Ce bit s'appelle le bit de LATCH.

- Si dans un octet, ce bit est à 1, le flush s'effectue en interne dans le SN76489 (LATCH/DATA).
- Si ce bit est à 0, le SN76489 s'attend à un second transfert (DATA)

On peut donc considérer 2 type d'octets. Nous allons les nommer DATA et LATCH/DATA

Si un atténuateur ou un registre noise doit être mis à jour on utilise 1 octet LATCH/DATA

Si un tone register (canaux 0 à 2) doit être mis à jour, on utilise d'abord 1 octet DATA puis 1 octet LATCH/DATA

latch-data-byte.png


latch-data-byte-desc.png


data-byte.png


data-byte-desc.png


Questions existentielles :
Commnent alors le chip détermine qu'il faut 2 octets ?
- D'abord parce que le premier contient le bit "Latch" à 0 ce qui implique la transmission d'un deuxième octet.
- Et si le deuxième octet a le bit "Type" à 1 et le canal entre 0 et 2, il s'agit forcément d'un tone register et il faut donc 2 octets.

A l'inverse comment le chip détermine qu'il ne faut qu'un octet ?
- Si le bit "Type" est à 1 on a une information de volume, donc 1 seul octet.
- Si le bit "Type" est à 0 mais que le canal est 3, on a une information de bruit, donc la aussi 1 seul octet.

Que se passe t'il si le permier octet n'est pas transféré ou est perdu ?
- Tant pis on va dire... Seule la partie haute sera flushée et la partie basse sera le contenu précédent du registre de "Latch". D'ailleurs cette particularité de fonctionnement est utilisée dans certains cas.
Remarque : Le registre de latch n'est jamais effacé entre 2 mise à jour. Donc, si une information nécessite 2 octets et qu'il n'y en a qu'un seul, vous aurez que la moitié à jour dans les "tone registers" (il n'y a que les tone registers qui utilisent 2 octets)

Voila. Nous avons vu comment écrire les registres dans le SN76489. Alors comment jouer un son ????

Il faut déterminer plusieurs paramètres
On a pas la possibilité d'arrêter un son, les compteurs sont en "free-running" ils tournent tout le temps. La seule possibilité d'arrêter le son sur un canal, est de mettre son niveau à 0. Le niveau se règle de 0 à 15. Evidemment, pour faire simple, 0 est le volume maximum et 15 le volume minimum (pas de son...). Il faut donc mettre un atténuateur à 15 pôur couper le son sur un canal.





Génération du son



Structure interne du SN76489


sn76489-block.png


Sur la gauche le bus de données, avec au dessus l'entrée horloge. L'horloge a une fréquence située entre 500Khz et 4Mhz. Le datasheet spécifie un maximum de 4 mhz. En général on utilisait les horloge des systèmes video des appareils dans lesquel le sn76489 était implémenté, soit 3579545Hz pour des systèmes NTSC et 3546893Hz pour du PAL/SECAM.

On voit que cette horloge est quoiqu'il arrive divisée par 16.
le diviseur est variable suivant les versions de SN76489, mais le principe est la. Certaines versions de sn76489 utilisent une horloge de 500khz et le diviseur en question n'est que de 2

Ensuite, après la division par 16, les 4 compteurs (un par voie musique et un pour le noise) sont décrémentées. Ils finissent à un moment par arriver à zero. Voici ce qui se passe suivant les canaux.

Génération de son sur Tone channel

Une fois arrivé à zero le compteur reprend la valeur fixée par le registre que nous avons mis à jour via le bus de données.
La sortie du canal associé (rappel : 3 voies musiques donc 3 compteurs, 3 registres pour sauvegarder la valeurs, 3 sorties), va s'inverser. Si la sortie était au niveau LOW (ou 0) elle passe HIGH (ou 1) et vice-versa. Ce qui va généré un signal carré. Sa fréquence sera de :

sample-tone-formula.png


Une fois la sortie inversée, celle-ci est envoyée au mixer. Le mixer mélange les 4 canaux (les 3 tone-channels et le noise-channel).
Voici quelques exemples :
Le registre en en 10 bits, donc il ne peut prendre que des valeurs de 0 à 1023.


Si le registre vaut 0xff en hexadécimal, soit 254 en décimal nous aurons la fréquence suivante.

sample-tone-calc1.png


La fréquence minimum est atteinte lorsque le registre vaut 0x3ff soit 1023.

sample-tone-calc3.png


La fréquence maximum audible est atteintre lorsque le registe vaut 6.

sample-tone-calc2.png


La limite théorique serait lorsque le registre vaut 1.

sample-tone-calc4.png


Génération de son sur noise channel

Le compteur est resetté en fonction de la valeur des 2 premiers bits du registre de bruit.

noise-register-2bits.png


Comme pour les "tone-channels" le bit de sortie s'inverse. Mais, il n'est pas ensuite envoyé directement dans le mixer. Au changement d'état, un bit va être envoié dans le LFSR (Linear Feedback Shift Register - ou - Registre à décalage à rétroaction linéaire).

On imagine que les bits se décale vers la droite le bit 15 vient à la place du bit 14, le 14 dans le 13, etc... le 1 dans le 0 et le 0 est perdu. On va entrer un nouveau bit dans l'entrée du LFSR (le bit 15) et celui-ci sera le résultat d'une opération XOR dont les 2 entrées seront choisie parmi les 16 bits disponibles.

Ce genre de montage est utilisé pour générer des valeurs aléatoires. On s'en servait dans bien des applications (comme le chiffrement, des générateurs pseudo-aléatoires) et musicalement parlant, ce genre de système est utilisée dans certaines boites à rythme ou synthé pour fabriquer du bruit.
Dans les DR-110 le son des Hi-Hats est fabriqué de cette façon

Le LFSR est tableau de 15 ou 16 bits (suivants les versions du chip).

LFSR2.png


Le bit en entrée est calculé par un reseau XOR. Il y a 2 réseaux, un externe où les porte XOR sont externe au registre à décalage, et un réseau interne, où le calcul XOR se fait entre les bits du tableau.

Je vous invite à consulter de la documentation sur internet pour avoir plus d'information sur le sujet car nous pourrions en dire des dizaines de pages. Ce qu'il faut retenir c'est que le principe du LFSR est utilisé pour générer du bruit dans le sn76489 et que nous pouvons agir un peu sur le fonctionnement de ce LFSR.

Quelques liens sur le sujet :

Registre a decalage a retroaction lineaire.html

Linear-feedback_shift_register.html

lfsr.html

Bruit blanc

Pour générer du bruit blanc le bits 2 du Noise register doit être à 1.

Comme vu plus haut nous prélevons 2 bits qui serviront d'entrée à une bascule XOR et la sortie de la bascule va dans l'entrée du registre à décalage, qui décale donc tous ses bits de 1 dans la chaine.

Suivant les matériels, les 2 bits en question sont différents. a titre d'exemple voici quelques matériels avec les bits concernés et le positionnement du bouclage.

tapped-bits-noise.png


Exemple pour Genesis :

On effectue un XOR des bits 0 et 3 et on envoie le résultat sur le bit 15.

noise-register-xor-1.png


Ce qui donne le resultat suivant après le shift.

noise-register-xor-2.png


Bruit périodique

Pour générer du bruit périodique le bits 2 du Noise register doit être à 0.

Dans tous les montages, on récupère le bit 0 et on le réinjecte dans l'entrée.
Quand le registre est écrit, un reset du registre est effectué. Tous les bits sont à zero sauf le plus haut. Cela produit un bruit périodique avec une largeur de 1/16ème ou 1/15ème, cette modification affecte aussi le son du bruit blanc.

Pour un registre de 16 bits et une horloge à 3759545Hz cela permet d'obtenir un range de bruit périodique entre 6.8hz et 6991hz quand on utilise le canal2 comme driver (avec le registre du canal 2 ayant pour valeur 0x3ff et 0x001).

periodic-noise-1.png


periodic-noise-2.png



Atténuateurs



Le volume sonores de chaque voie (pour rappel : 3 voies musiques, une voie noise) se fait par l'intermédiaire d'atténuateurs. Une fois que les 4 sons sont fabriqués, ils doivent être mixés. Le mixer multiplie chaque canal de sortie par le volume correspondant et additionne le tout. Le resultat est ensuite envoyé sur un amplificateur avant d'atteindre la sortie (pin OUT du circuit).

Le SN76489 attenue le volume de 2dB pour chaque pas du registre de volume.

Nous ne pouvons agir que sur 4 bits sur les registres d'atténuations, ce qui revient à dire que les valeurs s'échelonneront de 0 à 15. On parle d'atténuateur, donc une valeur de 0 n'atténue rien, une valeur de 15 atténue au maximum. La valeur de 15 devrait tout de même présenté un signal atténué mais en fait elle correspond à un signal nul sur la sortie.

Voici quelques exemples de réglages de volumes.
Rappel : Pour chaque mise à jour de volume nous devons préciser - bit latch à 1 - numero de canal : 0,1 ou 2 pour canaux musique et 3 pour canal noise - bit Type à 1 (1 correspond au réglage de volume) - 4 bits pour le volume : 0000 (0) correspond au maximum de volume, 1111 (15) correspond au minimum

volume-samples.png


Voici la fin de ce chapitre sur les registre. Nous disposons de tout le nécessaire pour definir la fréquence d'un canal musique, le type et la fréquence du canal bruit ainsi que le volume des 4 canaux. Il nous reste à décrire le montage employé qui nous permettra de commander ce chip sn76489 en midi en interfacant ce dernier avec un arduino nano.



Description du montage



Nous allons interfacer le SN76489 avec un arduino afin de pouvoir le piloter en midi. Nous allons aussi rajouter quelques potentiomètres pour avoir un minimum d'interface utilisateur. Le cahier des charge a déja été décrit plus haut mais voici un résumé.
On utilise le montage habituel, avec un 6N138 comme optocoupleur relié sur le l'entrée série de l'arduino. Je n'ai pas mis de sortie midi, ni de midi-thru sur ce montage. Un interrupteur permet d'isoler le midi pour progammer l'arduino nano (les ports TX et RX sont utilisés pour programmer le micro-controleur, il faut pouvoir isoler le midi pour reprogrammer l'arduino)

Le nano est alimenté en 9V, on utilise le régulateur interne du nano pour alimenter en 5V le reste du montage : la chaine de potentiomètres, le chip SN76489, l'optocoupleur.

Les potentiomètres sont des 100K linéaires. Les curseurs des potentiomètres sont reliés sur les entrées analogiques de l'arduino nano. Les 4 potentiomètres sont en parallèles sur l'alimentation 5V. On recueille donc une tension réglable (entre 0V et 5V) sur chacun des curseurs.

Un interrupteur permet de sélectionner les modes monophonique, polyphonique et unison. Un pont de résistances commutées par ce double inverseur permet d'avoir 3 tensions différentes qui seront envoyées sur une entrée analogique afin de tester la position de ce commutateur. Le commutateur à un position centrale. Au centre rien n'est connecté, on envoie 5V sur l'entrée analogique. Commutation coté unison, l'entrée analogique est reliée à la masse, on a donc 0V. Coté polyphonique on a 2.5V puisqu'on recueille une tension entre 2 résistances identiques branchées aux extrémités sur 5V.

Le filtrage de la sortie est effectué par 2 résistances et un condensateur. Ce n'est pas très évolué mais c'est suffisant.

schematic-sn76489.png


Donc un montage ultra-simple. J'ai utilisé un veroboard que j'ai gliussé dans un boitier Monacor type AH-103 mais vous pouvez prendre ce que vous voulez.

monacor-ah103.png


Cablage pas optimisé du tout, mais j'ai pas le temps et j'en fait qu'un alors voila :) . Dans la vraie vie, l'optocoupleur est un 6N138. La j'avais mis un 4N25 mais il me causait des soucis.

sn08.jpg


J'ai imprimé la facade sur une feuille A4 que j'ai recouverte d'un film transparent autocollant.

sn04.jpg


sn05.jpg


Ci-dessous, et de haut en bas sur le coté du boitier
sn01.jpg


sn02.jpg


sn03.jpg



Le programme



Rien de bien compliqué dans ce programme.

Pour faire fonctionner le chip il faut le relier à l'arduino, on réserve donc 8 pins pour cela. Je n'ai pas pris les ports D0 et D1 car ils sont réservés pour le midi. C'est dommage, parce que le port D contient justement 8 bits. Il faudra utiliser plusieurs ports différents, mais ce n'est pas bien grave. La pin 9 est utilisé pour générer l'horloge interne. Il faut aussi réserver les ports pour les potentiomètres et le bouton de mode.

#define PIN_D0_SN 2 // Pin D0 SN76489 #define PIN_D1_SN 3 // Pin D1 SN76489 #define PIN_D2_SN 4 // Pin D2 SN76489 #define PIN_D3_SN 5 // Pin D3 SN76489 #define PIN_D4_SN 6 // Pin D4 SN76489 #define PIN_D5_SN 7 // Pin D5 SN76489 #define PIN_D6_SN A0 // Pin D6 SN76489 #define PIN_D7_SN A1 // Pin D7 SN76489 #define PIN_CLOCK 9 // Pin clock SN76489 #define PIN_CS1 A2 // Chips select SN76489 #define PIN_MODE A3 // Selection mode 3 voies, Poly et Unison #define PIN_LFOSPEED A4 // Reglage frequence LFO #define PIN_LFODEPTH A5 // Reglage profondeur LFO #define PIN_RANGE A6 // Reglage range (vitesse horloge 4 à 1 mhz) #define PIN_RELEASE A7 // Reglage Release

On doit ensuite pouvoir génerer une horloge entre 500khz et 4 Mhz (500 Khz, 1 Mhz, 2 Mhz et 4 Mhz)

// Generation d'une clock sur la pin 9 // Reset TCCR1A TCCR1A &= ~((1<<COM1A1) | (1<<COM1A0) | (1<<WGM11) | (1<<WGM10)); // Reset TCCR1B TCCR1B &= ~((1<<WGM13) | (1<<WGM12) | (1<<CS12) | (1<<CS11) | (1<<CS10)); // Toggle OC1A/OC1B on Compare Match TCCR1A |= (0<<COM1A1) | (1<<COM1A0); TCCR1A |= (0<<WGM11) | (0<<WGM10); // CTC Mode TCCR1B |= (0<<WGM13) | (1<<WGM12); // No prescaling TCCR1B |= (0<<CS12) | (0<<CS11) | (1<<CS10); // Clockgen dans OCR1A : 1=4 Mhz, 3=2Mhz, 7=1Mhz OCR1A = 1;

Le démarrage du midi et les handlers sont définis dans le setup
MIDI.begin(MIDI_CHANNEL_OMNI); // demarre le midi en mode OMNI // Initialisation Handler Midi //---------------------------- MIDI.setHandleNoteOn(MyHandleNoteOn); // handler note on MIDI.setHandleNoteOff(MyHandleNoteOff); // handler note off MIDI.setHandleControlChange(MyHandleCtrl); // handler CC MIDI.setHandlePitchBend (MyHandlePitchBend); // handler pitch-bend

A chaque itération, la boucle principale execute la lecture du midi, la mise à jour de l'unison, la lecture des boutons et potentiomètres. On met aussi à jour les informations qui concernent l'enveloppe et les LFO. Comme je ne suis pas à cheval pour le temps réel, j'ai tout mis la. On a une variable pour la lecture des boutons et potentiomètres, a chaque exécution de read_buttons_and_pots() une seul port analogique est lu afin de gagner du temps.
void loop() { MIDI.read(); update_unison(); read_buttons_and_pots(); update_enveloppe(); update_lfo(); }

UpdateSN76489() est la procédure qui permet d'écrire un octet dans le SN76489. C'est la procédure de base pour toutes les mises à jour.
//--------------------------------------------- // Procedure mise à jour d'un octet sur SN76489 //--------------------------------------------- void UpdateSN76489(uint8_t dataSN, uint8_t nochips ) { uint8_t CS_PIN_SN; uint8_t bufdataSN; uint8_t inPORT; CS_PIN_SN = nochips + PIN_CS1; bufdataSN=dataSN<<2; // Recuperation 6 bits de poids faible des datas inPORT=PORTD&3; // Masquage pour lire le PORTD sur le 2 premiers bits uniquement PORTD=bufdataSN+inPORT; // reecrire des 2 premiers bits et 6 bits de poids faibles data inPORT=PORTC&252; // recuperation de PORTC sauf 2 premiers bits remis a zero bufdataSN=dataSN>>6; // recuperation des 2 derniers bits des datas en decalant de 6 PORTC=inPORT+bufdataSN; // reecrire les 2 premiers bits de PORTC sans changer le reste delayMicroseconds(DELAY_WRITE_SN); digitalWrite(CS_PIN_SN, LOW); delayMicroseconds(DELAY_WRITE_SN); digitalWrite(CS_PIN_SN, HIGH); delayMicroseconds(DELAY_WRITE_SN); }

Nous avons ensuite 3 procédures permettant de mettre à jour l'ampliture (les atténuateurs), la fréquence des oscillateurs (pour les 3 canaux musique) et le canal bruit.
UpdateAmplitude() permet de mettre à jour les atténuateurs qui font plus ou moins office de VCA sur les 4 canaux. La réduction est assez violente compte tenu du fait que nous n'avons que 16 pas en tout. Comme il s'agit d'atténuateurs, 0 est le volume maximum, 15 le volume mini. A chaque changement de valeur dans les atténuateurs, il y a un petit claquement sympathique...

//----------------------------------------------- // Mise a jour des attenuateurs // channel : de 0 à 2 pour musique, 3 pour noise // amplitude : Attenuation 0 à 127 (en midi) transformée de 0 a 15 (0 = son au maxi, 15=mini) // chipNumber : no de chip SN76489 (si plusieurs...) //----------------------------------------------- void UpdateAmplitude(uint8_t channel, uint8_t amplitude, uint8_t chipNumber ) { uint8_t n_channel,n_amplitude,DataOut; // Si derniere MAJ du chip identique pour le canal // alors on sort if (last_dataSN_Amplitude[channel] == amplitude ) return; last_dataSN_Amplitude[channel] = amplitude; n_channel = channel << 5; // conversion banque format SN n_amplitude = 15 -(amplitud e >> 3); // conversion velocity midi en format SN DataOut = B10010000 + n_channel + n_amplitude; // calcul octet a ecrit pour MAJ amplitude UpdateSN76489(DataOut,chipNumber); // maj SN76489 }

UpdateFrequency() permet de mettre à jour la fréquence d'un canal sur le SN76489. le parametre dataSN contient la valeur pré-calculée permettant de mettre à jour les compteurs dans le SN76489.

//---------------------------------- // Update de la frequence d'un canal //---------------------------------- void UpdateFrequency(uint8_t channel, uint16_t dataSN, byte chipNumber ) { uint8_t n_channel,n_amplitude,DataOut; int16_t n_dataSN; // Si derniere MAJ du chip identique pour le canal // alors on sort if (last_dataSN_Frequency[channel] == dataSN ) return; last_dataSN_Frequency[channel] = dataSN; n_channel = channel << 5; // conversion canal format SN if (dataSN < 0 ) dataSN = 0; if (dataSN > 1023 ) dataSN = 1023; n_dataSN = dataSN % 16; // garder que les 4 premiers bits DataOut = B10000000 | n_channel | n_dataSN; // calcul 1er octet pour MAJ frequency UpdateSN76489(DataOut,chipNumber); // ecrire cet octet dans SN76489 DataOut = (dataSN >> 4) & B00111111 ; // calcul 2eme octet pour MAJ frequency UpdateSN76489(DataOut,chipNumber); // ecrire ce deuxieme octet dans SN76489 }

UpdateNoise() permet de mettre à jour le canal de bruit avec les différents type de bruits disponibles (voir plus haut dans le chapitre la description à ce sujet.
//----------------------------------------------- // Mise a jour du canal noise // Type noise contient le genre de bruit. 8 possibles // nochips : no de chip SN76489 (si plusieurs...) //----------------------------------------------- void UpdateNoise(uint8_t typenoise, uint8_t chipNumber ) { uint8_t DataOut; DataOut = B11100000+typenoise; // calcul octet a ecrit pour MAJ amplitude UpdateSN76489(DataOut,chipNumber); // maj SN76489 }

Les messages de note-on midi, permettent de connaitre le channel, la note (donc on peut calculer la fréquence) et la velocité (on peut donc mettre à jour l'atténuateur). Donc, avec ce type de messages on peut jouer n'importe qu'elle note (dés l'instant qu'elle est dans le spectre audio reproductible par le SN76489), sur un des canal avec un niveau de sortie réglable.

Cela doit de faire d'abord par une mise çà jour de la fréquence du canal concerné et un réglage de l'atténuateur en fonction de la velocité de la note. Lors du note-off, on met l'atténuateur sur 15 pour fermer le canal brusquement (ou alors on fait jouer le release de l'enveloppe qui fermera progressivement l'atténuateur du canal).
La partie la plus important consiste à aller chercher la valeur à mettre à jour dans le SN76489 pour lui faire jouer une fréquence précise. Ce tableau se nomme MidiToReg[ ] et contient les fréquences pré-calculées pour toutes les notes midi.

Lorsqu'une note est jouée, le handler midi du note-on est activé.

Dans différents tableaux indicés par numéro de canal on va stocker des valeurs.
On exécute ensuite la procédure PlayNote() qui se charge de tout mettre à jour dans le chips (fréquence et atténuateurs sur le bon canal).
v_decrement[i] = (float)velocity / (float)v_release; // Valeur a decrementer pour le release a chaque decrementation v_velocityInit[i] = (float)velocity; // Velocity en float stateRelease[i] = RELEASE_INIT; // Prevoir futur Release pour ce canal MidinoteNumber[i] = mynote; // memorisation de la note MidinoteVelocity[i] = velocity; // memorisation de la velocite MidinotePlay[i] = pgm_read_word_near( MidiToReg+mynote); // memorisation de la frequence PlayNote(i); // on joue la note sur le canal defini par i

Le handler de note-off permet de déclencher la phase de release. En fonction de la quantité de release réglée sur la facade via le potentiomètre "Release", un compteur de décremente pour arriver à zero. Ce compteur met plus ou moins de temps pour arriver à zero, c'est ce qui défini le release de chaque note, au fur et à mesure de la décrémentation de ce compteur, l'atténuateur correspondant au canal sera augmenté afin de baisser le niveau sonore.

Pour le handler note-off, c'est simple on active un flag stateRelease[channel] et on le positionne à RELEASE_IN_PROGRESS.
// Si note-off sur derniere note jouee // alors on mute le canal concerné if (MidinoteNumber[SN_channel]==mynote) { if (stateRelease[SN_channel]) // On verifie que l'enveloppe est bien declenchee stateRelease[SN_channel]=RELEASE_IN_PROGRESS; // On initialise la MAJ du Release

Ensuite, à chaque itération de la boucle principale, la procédure update_enveloppe() est exécutée. C'est elle qui va se charger de baisser progressivement chaque canal si c'est nécessaire.

On a le tableau v_velocityInit[] qui a été initialisé à la valeur de vélocité initiale. Ensuite, le tableau v_decrement[] qui contient une faible valeur a décrémenter dans le tableau v_velocityInit[]. Une fois que ce tableau est à jour on met à jour l'amplitude du canal via UpdateAmplitude(). Cette procédure UpdateAmplitude() est donc exécutée fréquemment mais si la valeur de velocité ne change pas de plus de 1/16 des valeurs possible, cette procédure envoie un return, ce n'est donc pas trop consommateur en ressource.

Voici la partie intéressante de la mise à jour de l'enveloppe
if (stateRelease[i]==RELEASE_IN_PROGRESS ) { // si release en cours pour le canaux selectionne v_velocityInit[i]=v_velocityInit[i]-v_decrement[i]; // On decremente sa velocite if (v_velocityInit[i] > 0) { // Si velocite positive UpdateAmplitude(i,(uint8_t)v_velocityInit[i],0); // on l'ecrit dans le chip


Mode de jeu


J'ai prévu 3 modes de jeu : La sélection du mode se fait avec interrupteur 3 positions qui mets plus ou moins à la masse un réseau de résistance, ce qui permet de générer 3 tensions différentes qui seront lues sur une entrée analogique. La variable ModeMidi définie le mode de jeu en cours.
m=analogRead(PIN_MODE); if (m> 900) { ModeMidi=MONO; } else if (m>450) { ModeMidi=POLY; } else { ModeMidi=UNISON; }


Le lfo


On règle la vitesse et la profondeur du lfo avec 2 potentiomètres ("Speed" et "Release").

Lors du jeu via le midi, si les données de pitch sont rafraichies, la valeur calculée du lfo est ajoutée au pitch ce qui permet de faire varier le pitch au rythme du LFO. Cette valeur qui va "détuner" les oscillateurs est mise à jour lors de l'utilisation de la modulation wheel. On a donc déja une lecture du controleur 1 qui soit de faire sur les 3 canaux musiques. Un tableau lfo[ ], indicé par le numéro de canal est mimse à jour si le controleur 1 est supérieur à 64. SI le lfo est actif pour un canal il vaut 1 sinon il faut 9.

if (value >=64) // Si molette active lfo[channel-1]=1; // on toggle lfo else lfo[channel-1]=9; // sinon on prepare sa remise a zero en le mettant a 9 break;

On positionne le lfo à 9 pour être sur de bien pouvoir le remettre à zero ensuite et rafraichir les fréquences des 3 canaux.

Donc la fréquence d'un oscillateur tient compte :
if (MidinoteBend[SN_channel]>=0) vfreq=MidinotePlay[SN_channel]+vunison+((float)MidinoteBend[SN_channel]/512.0*MidinotePlay[SN_channel]+vlfo[SN_channel]); else vfreq=MidinotePlay[SN_channel]+vunison+((float)MidinotePlay[SN_channel]/1024.0*MidinoteBend[SN_channel]+vlfo[SN_channel]);

La valeur à ajouter au pitch est pour le channel vlfo[channel].

On lit d'abord les 2 potentiomètres du lfo. La fréquence du lfo se trouve dans lfofreq. La profondeur dans lfodepth.
On calcule ensuite une variable appelée lfodelta, puis la valeur du lfo lfoval réajustée à la moitié de la profondeur. A chaque rafraichissement de la valeur du lfo une variable lfotimer est incrémentée. Si lfotimer est supérieure à la moitié de la valeur de lfofreq on attaque la pente descendante du lfo (sinon c'est la pente montante). Enfin, lorsque lfotimer est supérieur à lfofreq on le remet à zéro. Ce qui donne approximativement le code suivant.
lfodelta = (lfofreq) ? (lfodepth*256U) / lfofreq : 0; lfoval = (lfotimer*lfodelta) >> 8; if (lfotimer >= lfofreq/2) // la profondeur totale est utilisee pour la montee et la descente cumulee lfoval = lfodepth - lfoval; // donc les valeurs de lfo seront egale à la moitie de la profondeur vlfo[i]=lfoval-(lfodepth/4); // on update la valeur du lfo et on transforme en symetrique

Cliquez sur le lien ci-dessous pour récupèrer l'archive du code source.
Le premier lien vous concerne si vous avez un ancien environnement de dev arduino, genre 1.0.x et une vieille librairie midi. le pitchbend est géré différement avec le vieil environnement. Si vous avez un environnement récent, prenez la deuxième archive, elle contient la correction du pitch-bend et aussi la ligne supplémentaire pour initialiser le MIDI.

Code source ancienne lib MIDI et ancien environnement



Code source lib MIDI 5.0.2 et env >1.6.5



Voila, vous savez tout. Voila comment utiliser un SN76489 avec un arduino. Il est ensuite facile de faire la même chose avec 2 chips, soit pour avoir de la stéréro, soit pour avoir 6 canaux musiques et 2 canaux noise, ou alors de la polyphonie sur 6 voies.

Bonne construction.