Filtre SVF Numérique
Dans cette section, nous allons nous focaliser dans l'implémentation du filtre SVF numérique.
Signal d'Entrée
Pour pouvoir analyser le comportement du filtre, il est essentiel d'utiliser un signal approprié qui permette de mettre en évidence les caractéristiques du filtre. Un choix courant est d'utiliser un générateur de bruit blanc. Le bruit blanc est un signal aléatoire qui contient toutes les fréquences avec une amplitude constante sur toute la bande passante. En utilisant ce signal en entrée, nous pouvons observer comment le filtre réagit à différentes fréquences et comment il modifie le spectre du signal.
Génération de bruit blanc
Pour générer du bruit blanc, nous allons mettre en place un générateur linéaire congruentiel (LCG). Ce type de générateur est simple à implémenter et a une faible complexité calculatoire, ce qui le rend adapté aux applications en temps réel comme le traitement audio.
Pour implémenter le générateur LCG, nous allons ajouter plusieurs lignes de code au fichier src/audio_processor.c
.
...
float randomGenerator() {
static uint32_t seed = 123;
seed = (1664525 * seed + 1013904223) % 4294967296;
return 2.0*((float)seed / 4294967296) - 1.0;
}
void render_audio(int16_t *buf, int16_t length) {
for (uint16_t i = 0; i < length; i = i+2) {
//generate random number
float y = randomGenerator();
int16_t value = ((int16_t) ((32767.0f) * y)); // conversion float -> int
buf[i] = value; // left channel sample
buf[i+1] = value; // right channel sample
}
}
Implémentation du filtre
Nous souhaitons implémenter le SVF optimisé pour lequel les équations d'état sont données par
où:
et:
Nous allons tout d'abord implémenter un filtre ayant une fréquence de coupure de 500 Hz et un facteur Q de 0.95 de sorte à obtenir des résultats sonores significatifs. Le squelette de la fonction render_audio
est présenté ci-dessous.
void render_audio(int16_t *buf, int16_t length) {
static float x_n_1 = 0;
static float y1_n_1 = 0;
static float y2_n_1 = 0;
// <- compute g and k from fc and Q here
float g = ...
float k = ...
// <- compute the filter coefficents here (c_21, c_22, c_23, c_31, c_32, c_33, d_2, d_3)
...
for (uint16_t i = 0; i < length; i = i+2) {
//generate random number
float x = randomGenerator();
// <- compute the new filter state here
float x_n = ...;
float y1_n = ...;
float y2_n = ...;
// update the filter state
y1_n_1 = y1_n;
y2_n_1 = y2_n;
x_n_1 = x_n;
// keep the LP output
float y = y2_n;
int16_t value = ((int16_t) ((32767.0f) * y)); // conversion float -> int
buf[i] = value; // left channel sample
buf[i+1] = value; // right channel sample
}
}
Manipulation 1
A partir des équations d'état du filtre SVF, complétez la fonction render_audio
. Visualisez le signal filtré à l'oscilloscope.
Utilisation d'une structure
Nous allons utiliser une structure pour centraliser les paramètres relatifs à la gestion du filtre.
Définition de la structure
typedef struct {
float gain; // gain
float cutoff; // cutoff frequency
float Q; // Q factor
float c_21; // filter coef
float c_22; // filter coef
float c_23; // filter coef
float c_31; // filter coef
float c_32; // filter coef
float c_33; // filter coef
float d_2; // filter coef
float d_3; // filter coef
float x_n_1; // filter state value
float y1_n_1; // filter state value
float y2_n_1; // filter state value
} SVFFilter;
Fonctions associées
Pour interagir avec la structure SVFFilter, il est possible de définir plusieurs fonctions pour gérer l'initialisation, la mise à jour des paramètres et le calcul des échantillons en sortie du filtre.
void svfFilterInit(SVFFilter* filter, float gain); //Initialisation du contenu de la structure (tous les parametres à 0 sauf gain)
void svfFilterSetCutOff(SVFFilter* filter, float cutoff); // changement de la frequence de coupure
void svfFilterSetQ(SVFFilter* filter, float Q); // changement de la valeur de Q
void svfFilterUpdateCoef(SVFFilter* filter); // mise à jour des coefficients à partir des valeurs internes de fc et de q
float svfFilterNextSample(SVFFilter* filter, float x); // calcul de la sortie du filtre
Manipulation 2
Implémentez le code de la structure SVFFilter
, puis les fonctions associées svfFilterInit
, svfFilterSetCutOff
, svfFilterSetQ
, svfFilterUpdateCoef
et svfFilterNextSample
.
Implémentation d'un synthétiseur soustractif
Architecture de base
Pour créer un synthétiseur monophonique soustractif, nous allons mettre en place une architecture simple mais efficace, à la base de nombreux synthés comme la TB303. Le diagramme ci-dessous illustre cette architecture :
- Oscillateur :
- Oscillateur par wavetable,
- Wavetable de type dent de scie ou carré,
- pitch et vélocité controlés en MIDI.
- Filtre:
- Filtre SVF numérique de type passe-bas,
- Fréquence de coupure de 500Hz et facteur Q de 0.9.
Manipulation 3
Implémentez le synthétiseur monophonique soustractif proposé.
Contrôle des paramètres du filtre en MIDI
Pour rendre le synthétiseur plus expressif, nous souhaitons permettre au musicien de modifier la fréquence de coupure (cutoff) et le facteur Q du filtre via des messages MIDI. Une méthode courante pour gérer cette interaction est d'utiliser les potentiomètres d'un contrôleur MIDI. Les potentiomètres d'un contrôleur MIDI génèrent des messages de type Control Change (CC) allant de 0 à 127.
Pour gérer les messages de type CC, il est recommandé de définir une nouvelle fonction dans le fichier src/midi_processor.c
void monoSynthChangeParam(MonoSynth* synth, uint8_t cc, uint8_t value); // Reception d'un message CC
Manipulation 4
En vous inspirant du contrôle du pitch et de la vélocité en MIDI, modifiez votre programme pour permettre au musicien de contrôler la fréquence de coupure et le facteur Q du filtre en utilisant la transmission de messages CC.
Manipulation 5
En vous inspirant du contrôle de la vélocité en MIDI, modifiez votre programme pour permettre de contrôler la fréquence de coupure en utilisant un mapping non-linéaire de la valeur du message CC. Un réglagle classique consiste à prendre