Skip to content

Synthèse par Wavetable

Pour générer des oscillateurs, une méthode efficace est la synthèse par wavetable. Cette technique consiste à précalculer les échantillons et à les stocker dans une table. L'oscillateur peut ensuite être créé en parcourant simplement cette table à une vitesse variable, ce qui permet de contrôler la fréquence de l'oscillateur. La synthèse par wavetable offre ainsi une flexibilité remarquable pour la création de timbres variés et complexes, tout en optimisant l'utilisation des ressources processeur. Cette technique est à la base de synthétiseurs comme le Massive de Native Instruments ou Serum de Xfer.

Création de la wavetable

La synthèse par wavetable repose sur la construction d'une table de L échantillons décrivant une période d'un signal périodique (sinusoidal ou autre). Cette table peut être précalculée avant le lancement du moteur audio.

A titre d'exemple, le modèle suivant décrit une table d'échantillons décrivant une période d'un signal sinusoidal :

x[l]=sin(2πl/L)
  • L : nombre d'échantillons dans la table d'onde
  • l: entier variant de 0 à L1

La figure suivante présente le contenu de la table pour L=128 échantillons.

Step1

Manipulation 1

Dans le fichier src/audio_processor.c, précalculez une table d'échantillons nommée sine_wavetable de L=512 échantillons contenant les échantillons décrivant exactement une période d'un signal sinusoidal.

Génération du signal

Pour générer un oscillateur, une approche possible consiste à lire les éléments de la table avec un pas de Δ échantillons

l[n]=(l[n1]+Δ)mod(L)y[n]=x[l[n]]
  • l[n]: pointeur dans la table,
  • mod: opérateur modulo,
  • Δ: incrément de lecture.

Lorsque la table contient exactement une période et lorsque l'incrément de lecture est fixé à Δ=1 échantillon, la fréquence fondamentale du signal générée, noté f0, est égale à :

f0=fs/L  [Hz]

Manipulation 2

Générez une sinusoïde avec un incrément de lecture Δ=1 échantillon et une table d'onde de L=512 échantillons. Mesurez la fréquence fondamentale du signal à l'oscilloscope.

Modification de la fréquence fondamentale

Un synthétiseur doit pouvoir générer des oscillateurs de fréquence variable. Pour générer un oscillateur de fréquence f0 Hz, l'incrément de lecture doit être fixé à :

Δ=Lf0fs [sample]

Dans le cas général, l'incrément de lecture Δ et donc le pointeur dans la table l[n] ne sont pas toujours des entiers. Ce phénomène est illustré dans la figure ci-dessus.

Step1

Pour approximer la valeur de l[n] puis celle de x[l[n]], plusieurs stratégies sont possibles. L'utilisation d'une stratégie particulière est motivée par la recherche du meilleur compromis entre la complexité calculatoire et la minimisation de l'erreur d'approximation ϵ=|y[n]x[l[n]]|y[n] désigne la valeur approximée.

Interpolation d'ordre zéro

Une technique simple consiste à arrondir la valeur de k à l'entier inférieur le plus proche (bloqueur d'ordre 0). Cette approche présente une très faible complexité calculatoire mais est souvent associée à une erreur d'approximation importante. Notons . l'opérateur permettant d'extraire l'entier inférieur le plus proche. Par exemple, 3.21=3 et 5.98=5. En utilisant cet opérateur, il est possible de générer un oscillateur de fréquence f0 de la manière suivante :

l[n]=(l[n1]+Δ)mod(L)l0=l[n]y[n]=x[l0]

Manipulation 3

A partir d'une table d'onde de L=512 échantillons, générez une sinusoïde avec une fréquence de 110Hz en utilisant une interpolation d'ordre 0. Vérifiez la fréquence fondamentale du signal à l'oscilloscope.

Interpolation d'ordre 1

Pour réduire l'erreur d'approximation, une seconde technique consiste à appliquer une interpolation linéaire (ordre 1) entre les échantillons x[l0] et x[l0+1]. En utilisant cette stratégie, il est possible de générer un oscillateur de fréquence f0 de la manière suivante :

l[n]=(l[n1]+Δ)mod(L)l0=l[n]l1={l0+1si l0<L10ailleursα=l[n]l0y[n]=α(x[l1]x[l0])+x[l0]

où :

  • α correspond au coefficient de pondération entre les échantillons x[l0] et x[l1]. Notons que y[n]x[l0] lorsque α0 et y[n]x[l1] lorsque α1.

Manipulation 4

A partir d'une table d'onde de L=512 échantillons, générez une sinusoïde avec une fréquence de 110Hz en utilisant une interpolation d'ordre 1. Vérifiez la fréquence fondamentale du signal généré à l'oscilloscope.

INFO

Le calcul de l_1 peut se faire efficacement en utilisant l'opérateur ternaire du C.

Création de wavetable standards

Pour obtenir un oscillateur au contenu sonore intéressant, une technique possible consiste à utiliser des signaux périodiques spectralement riches. La plupart des synthétiseurs analogiques et numériques permettent de générer des signaux de type carré, dent de scie ou triangulaire.

Manipulation 5

Créez 3 nouvelles tables d'onde de L=512 échantillons permettant de générer :

  • un signal carré,
  • un signal en dent de scie,
  • un signal triangulaire.

Observez ensuite ces signaux à l'oscilloscope.

Pour gagner en souplesse dans l'utilisation des wavetable (changement à chaud de la table), il est recommandé de stocker les 4 wavetable (sinusoïde, carré, dent de scie et triangle) dans un tableau à 2 dimensions.

Structure WavetableOsc

Définition de la structure

Pour centraliser les paramètres relatifs à la gestion des wavetable, nous allons définir une structure stockant la valeur de l'amplitude du signal, de l'incrément de lecture Delta, de l'index de position l[n], et de l'index de la wavetable (0: sinusoide, 1: carré, ...), et un booléen permettant de spécifier le type d'interpolation utilisé.

c
typedef struct {
	float amp;              // Amplitude du signal
    float delta;            //increment de lecture
    float positionIndex;    //index de lecture dans la table 
	uint8_t wavetableIndex; //choix de la forme d'onde 
    bool isInterpolated;   //gestion de l'interpolation (false: ordre 0, true: ordre 1)
} WavetableOsc;

Manipulation 6

Ajoutez la définition de la structure WavetableOsc dans votre programme.

Fonctions associées

Pour améliorer l'organisation du code, une approche possible consiste à développer des fonctions permettant de manipuler la structure WavetableOsc.

c
void wavetableOscInit(WavetableOsc* osc, float amp, uint8_t wavetableIndex, bool isInterpolated); //Initialisation du contenu de la structure (positionIndex=0, delta=0)
void wavetableOscSetDelta(WavetableOsc* osc, float frequency); // Changement de l'incrément de lecture à partir de la fréquence
void wavetableOscSetAmp(WavetableOsc* osc, float amp); // Changement de l'amplitude
float wavetableOscNextSample(WavetableOsc* osc); // Calcul du prochain index de position, puis calcul et renvoie du prochain échantillon.

Manipulation 7

Ajouter les définitions puis implémentez les fonctions wavetableOscInit, wavetableOscSetDelta, wavetableOscSetAmp et wavetableOscNextSample.

References