Synthétiseur audio numérique (soustractif)
Synthétiseur soustractif monophonique sur STM32F746 Discovery : oscillateur wavetable, filtre à variables d'état, enveloppe ADSR et LFO calculés à 44,1 kHz dans l'interruption du codec et joués depuis un clavier USB MIDI.
Projet académique (ENIB, 2023).
- STM32F746
- C
- USB MIDI host
- CMSIS-DSP
- WM8994
- SVF
- ADSR
- Wavetable
- Python/SciPy
L'objectif et le point de départ
L'objectif était un petit synthétiseur jouable : un clavier USB MIDI branché à une carte STM32F746 Discovery, et le son qui sort du codec audio de la carte. Le son est construit de façon soustractive : un oscillateur produit une forme d'onde brillante, un filtre en retire une partie et une enveloppe façonne son attaque et son extinction.
Nous sommes partis d'un projet de départ fourni par le cours. Il contenait déjà l'initialisation de la carte et du codec (issue du kit éducatif DSP d'ARM), une classe USB MIDI hôte de Xavier Halgand, un oscillateur basique et une ébauche d'enveloppe presque vide. J'ai écrit tout ce qui en fait un synthétiseur : les fonctions d'oscillateur, le filtre à variables d'état, l'enveloppe ADSR, le LFO, la correspondance MIDI vers son et l'intégration de l'ensemble dans l'interruption audio.
Architecture
Chaque échantillon audio est calculé dans l'interruption du codec WM8994 à 44,1 kHz sur un STM32F746 cadencé à 200 MHz. Les paquets USB MIDI hôte (note on/off, control change, program change) sont analysés dans la boucle principale. Chaîne : oscillateur sinus (wavetable de 1024 points), dent de scie ou carré ; un LFO (environ 1 à 20 Hz) ajouté à la fréquence de coupure du filtre ; un filtre à variables d'état dont la coupure et la résonance sont bornées dans sa limite de stabilité ; et une enveloppe ADSR linéaire ou exponentielle. Les sorties gauche et droite portent le même échantillon mono.
Comment il a été construit
L'historique git du projet montre l'ordre dans lequel il a pris forme :
- D'abord les fonctions d'oscillateur, réécrites, et la première gestion MIDI (numéro de note vers fréquence).
- Ensuite le filtre à variables d'état, d'abord sous forme de nouveaux fichiers de filtres puis fonctionnel.
- Ensuite l'enveloppe ADSR. Le filtre fonctionnait avant l'enveloppe, à cause de la façon dont celle-ci suivait le temps ; l'enveloppe a été terminée ensuite.
- Enfin le LFO, la correspondance des control changes qui transforme les potentiomètres du clavier en paramètres, les program changes et la normalisation du niveau des oscillateurs.
Prototype en Python, puis C
Les signaux ont d'abord été mis au point sur PC avec Python et SciPy. Les wavetables (sinus, dent de scie, carré) ont été générées dans un notebook et collées en C sous forme de tableaux. Les oscillateurs ont été écrits en C, compilés en bibliothèque partagée et appelés depuis Python avec ctypes pour les comparer aux formes d'onde de référence de SciPy. Le filtre et l'enveloppe ont aussi été esquissés en Python ; pour le filtre, j'ai calculé sa réponse en fréquence avec SciPy et déterminé sa limite de stabilité, devenue une borne sur la résonance dans le code C. Puis tout a été réécrit pour la carte, où toute la chaîne doit se terminer dans l'interruption audio.
Les figures de cette page viennent de la compilation des propres fichiers C du projet sur un PC et du tracé de leur sortie : elles montrent ce que calcule le code du firmware, pas ce que la carte a joué.

Le filtre
Le filtre est un filtre à variables d'état avec des sorties passe-bas, passe-bande et passe-haut ; le synthétiseur utilise le passe-bas. Il a deux coefficients : l'un issu de la fréquence de coupure et l'autre de la résonance (Q). Le filtre devient instable si la résonance est trop basse pour la coupure ; le code recalcule donc à chaque fois un Q minimal et ne laisse jamais Q descendre en dessous. Depuis le clavier, la coupure va de 50 Hz à 10 kHz sur une échelle exponentielle et Q monte jusqu'à 10.

L'enveloppe et la commande MIDI
L'ADSR est une petite machine à états mise à jour à chaque échantillon audio : repos, attaque, decay, sustain et release. Les pentes ne sont recalculées que lorsqu'un paramètre change ; l'interruption audio n'ajoute donc qu'un nombre par échantillon. Relâcher une touche ne déclenche le release que s'il s'agit de la note en cours, de sorte que relâcher une ancienne touche ne coupe pas la nouvelle note.
Les notes MIDI deviennent des fréquences en tempérament égal (La4 = 440 Hz). Les control changes règlent l'attaque (5 ms à 15 s), le decay (200 à 500 ms), le niveau de sustain (0,001 à 1), le release (50 ms à 2 s), la coupure du filtre, la résonance, la profondeur du LFO et sa vitesse (1 à 20 Hz). Les program changes choisissent la forme d'onde, le mode de l'enveloppe et l'activation du LFO.

Problèmes connus et périmètre
Un prototype fonctionnel, pas un instrument fini :
- Il est monophonique : une note à la fois.
- Le mode d'enveloppe exponentiel existe dans le code mais ne fonctionne pas correctement : en exécutant le code sur PC, l'enveloppe saute au niveau maximal au lieu de monter progressivement. Seul le mode linéaire se comporte comme prévu.
- Le LFO ajoute sa sortie à la coupure du filtre à chaque échantillon : la coupure vient donc buter sur ses limites (50 Hz et 13 kHz) au lieu d'osciller autour de la valeur réglée. Mes propres notes dans le code signalent que la coupure sonne bizarrement avec le LFO actif, et du bruit avec le sinus et le LFO ensemble.
- Le glide entre les notes était prévu mais n'a pas été fait.
- Le code audio est compilé sans optimisation.
- La configuration FreeRTOS, le code LCD, les aides FIR et biquad et une variante audio DMA sont dans l'arborescence du projet mais ne font pas partie du chemin qui produit le son.