Traitement du signal : applications aux effets audio

Ce tutoriel présente quelques effets numériques audio et leur implémentation en C sur PC.

Les différents effets présentés sont illustrés par des exemples audio : une voix chantée est utilisée comme signal d’origine.


Introduction

1) Panoramique

2) Retard

3) Profondeur stéréo

4) Flanger-chorus

5) Echo

6) Réverbération
7) Combinaison d'effets





Introduction

En général, dans les applications de traitement de signal, on cherche à effectuer les traitements en temps réel. Le système de traitement numérique, à chaque instant discret nT, où T est la période d’échantillonnage, possède une entrée x(nT) et fournit une sortie y(nT). L’échantillon résultant du traitement y(nT) doit être disponible avant que le prochain échantillon à traiter x(nT) arrive. La durée entre l’arrivée de deux échantillons successifs est la période d’échantillonnage du signal. Pour simplifier l’écriture dans tout ce qui suit, on peut omettre la variable T.


Ce traitement en temps réel est possible dans le cas d’un traitement réalisé par un processeur spécialisé (DSP), puisque ce système est complètement programmable et contrôlable. Par contre, dans le cas d’un traitement réalisé par ordinateur, le signal doit être traité blocs par blocs, parce que l’ordinateur effectue la plupart du temps plusieurs tâches en parallèle sans que l’utilisateur ait le contrôle de celles-ci.

Dans le cas d’un traitement par ordinateur, on peut donc définir l’algorithme de traitement général d’un bloc audio par :

Répéter indéfiniment

            Acquisition d’un bloc audio

            Traitement de ce bloc d’entrée, génération d’un bloc de sortie

            Envoi du bloc généré en sortie audio


L’algorithme de traitement d’un bloc pourrait prendre la forme ci-dessous. Dans une implémentation en C, les arguments d’une fonction de traitement « traite_ech » seraient :

·   buf_e : le bloc à traiter (bloc d’entrée)

·   buf_mem : le buffer mémoire du module de traitement ; ses éléments doivent être initialisés à 0

·   ind : un indice de pointage dans le tampon mémoire du module de traitement 

for(i=0 ; i<taille_bloc ; i++)

            buf_s[i]=traite_ech(buf_e[i], buf_mem, &ind);

On caractérise souvent un filtre numérique par sa réponse implusionnelle. La réponse impulsionnelle est la réponse à une suite de nombre dont seul le premier est différent de 0. Par convention ce premier échantillon est égal à 1. 

La réponse impulsionnelle permet de comprendre l’effet du système sur un signal d’entrée plus complexe, comme un son par exemple : ce son serait répété à intervalles réguliers, tout en étant atténué.



1) Panoramique

L’effet de panoramique est un effet stéréo qui permet de simuler un positionnement d'une source sonore par rapport à l'auditeur. Cet effet est obtenu simplement en appliquant un volume différents aux deux voies gauche et droite.

Pour obtenir la sensation d'une source sonore se déplaçant de gauche à droite ou de gauche à droite, il suffit de moduler les amplitudes, la somme des volumes des deux voies restant constante. Par exemple, si ag est le volume de la voie gauche, le volume de la voie droite sera égal à :
ad=1-ag



Exemple audio

Dans cet exemple, le volume d’un des deux canaux stéréo est modulé par une sinusoïde de fréquence de l’ordre du Hertz. Le volume de l'autre canal est obtenu de la manière décrite ci-dessus.

voix_sanseffet.wav                             voix_panor.wav



2) Retard

Un des traitements les plus simples que l’on peut appliquer à un signal est un retard temporel. Un retard constant appliqué à un signal n’est pas perceptible si le signal non-retardé n'est pas utilisé également (et donc n’a aucune utilité ). Par contre, en modulant ce retard en fonction du temps, il devient perceptible. 

La relation entrée/sortie est définie par :

y(n)=x(n-d)

où d est le décalage temporel (un retard) en nombre d’échantillons. La relation entre le décalage en échantillons de et le décalage en secondes ds est :

de = ds  × fe

où fe est la fréquence d’échantillonnage.

Qu'il s'agisse d'une implémentation logicielle ou matérielle, le bloc de retard doit comporter de la mémoire : en effet, au moment de la génération de l’échantillon y(n), seul l’échantillon x(n) est présenté en entrée du bloc. Or y(n) dépend d’une valeur passée de l’entrée : x(n-d). Cette valeur doit donc être mémorisée à l’intérieur du bloc. La taille de ce tampon doit au moins être égale au délai d.



Algorithme


L’algorithme pour générer l’échantillon y(n) en fonction de x(n) nécessite les paramètres d’entrée suivants :

·   x : échantillon d’entrée courant

·   buf[ ] : tampon mémoire pour les échantillons d’entrée (x) passés nécessaires

·   d : la taille de ce buffer

·   i : indice de pointage dans le buffer ; cet indice doit rester compris entre 0 et d-1

L’algorithme génère l’échantillon de sortie y.

y=buf[i]

buf[i]=x

if(++i>=d)

       i=0


Pour le tampon mémoire, on parle de « tampon circulaire » pour qualifier le fait que le pointeur revienne en début de tampon après avoir atteint la fin.



Exemple avec d=2

Dans les tableaux ci-dessous, l’élément mis à jour dans le tampon à chaque itération est mis en gras.

Itérations


1

2

3

4

5

6

7

8

9

x


1

0

0

0

0

0

0

0

0

buffer

0

0

0

1

0

0

1

0

0

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

y


0

0

1

0

0

0

0

0

0


Première implémentation en C


L'implémentation en C de l'algorithme défini ci-dessus, est directe :

//   Fonction de délai, appliquée à un échantillon

//   Entrées :

//       x : échantillon d’entrée

//       buf : tampon mémoire (initialisé à 0)

//       d : retard de y par rapport à x  (et taille du tampon)

//       i : indice de pointage dans le tampon

//  Sortie :

//       y : échantillon de sortie

float

delai_ech(float x, int d, float *buf, int *i)

{

            float      y;

            y=*(buf+*i);

            *(buf+* i)=x;

            if(++(*i)>=d)

                        *i=0;

            return y ;

}

Cette première implémentation est simple mais limitée aux délais fixes. Si l’on souhaite un délai variable, pour programmer les effets chorus ou flanger par exemple, il est nécessaire d'utiliser un tampon mémoire de taille au moins égale à la valeur maximale du délai variable, et d'utiliser 2 pointeurs : un pour la mise à jour du tampon et l’autre pour obtenir l'échantillon de sortie. Une deuxième implémentation en C, plus complète donc, est donnée ci-dessous.



Deuxième implémentation en C

Dans l’exemple ci-dessous, on choisit de décrémenter le pointeur du tampon, contrairement à la première implémentation, pour éviter d’avoir à utiliser un pointeur supplémentaire pour l’échantillon utilisé.

//   Fonction de délai, appliquée à un échantillon

//   Entrées :

//       x : échantillon d’entrée

//       buf : tampon mémoire (initialisé à 0)

//       d : retard de y par rapport à x

//       D : taille du tampon

//       i : indice de pointage dans le tampon

//  Sortie :

//       y : échantillon de sortie

float

delai_ech(float x, int d, float *buf, int *i)
[

          float      y;

          y=*(buf+(*i+d)%D);

          *(buf+*i)=x;

          if(--(*i)<0)
                 (*i)+=D;

          return y ;

}



Exemple d’exécution de la deuxième implémentation, avec d=2 et D=5


L’élément mis à jour dans le tampon à chaque itération est mis en gras. L’élément du tampon utilisé comme échantillon de sortie est souligné. Contrairement à la première implémentation, ces 2 éléments sont différents.

Itérations

 

1

2

3

4

5

6

7

8

9

x

 

1

0

0

0

0

0

0

0

0

buffer

0

0

0

0

0

1

0

0

0

0

1

0

0

0

0

1

0

0

0

0

1

0

0

0

0

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

y

 

0

0

1

0

0

0

0

0

0

Exemple audio

Le délai appliqué au signal original varie en cours du temps de manière sinusoïdale (fréquence de l’ordre du Hertz).

voix_sanseffet.wav                             voix_delai.wav



3) Profondeur stéréo

L'effet de panoramique décrit plus haut était un premier effet stéréo. Pour accroître la sensation de positionnement d'une source sonore dans l'espace situé autour de l'auditeur, un deuxième effet consiste à appliquer un léger décalage temporel (retard) sur l'une des deux voies. Le son d'une source située sur la gauche ou la droite de l'auditeur, parvient en effet à l'oreille opposée avec un volume un peu inférieur à celui qui parvient à l'autre, mais aussi avec un très léger retard temporel.


L'algorithme de décalage temporel est celui décrit plus haut.



Exemple audio


Dans cet exemple, un décalage temporel est appliqué sur le canal de droite.


voix_sanseffet.wav                             voix_stereo.wav



4) Flanger-Chorus


Les effets flanger et chorus consistent à ajouter à chaque échantillon d'un son un échantillon passé, comme dans l'écho unique, mais avec les différences suivantes :

- le retard est beaucoup plus réduit
- ce retard varie dans le temps
Selon la façon dont ce retard varie, on parle plutôt de flanger ou de chorus. Dans le cas du flanger, il varie régulièrement, par exemple de manière sinusoïdale. Dans le cas du chorus, il varie de manière aléatoire, simulant l'effet de "choeur", c'est à dire de plusieurs instruments jouant en même temps de même son, mais avec des petits décalages temporels aléatoires.



Exemple audio


Dans cet exemple, le retard appliqué au son varie de manière sinusoïdale.

voix_sanseffet.wav                             voix_flanger.wav



5) Echo


5.1) Echo unique


Le traitement de type écho consiste à ajouter à chaque échantillon d'un signal, un échantillon passé. Le retard doit être de l’ordre de quelques dizaines à quelques centaines de millisecondes.

La relation entrée/sortie est définie par :

y(n)=x(n)+a.x(n-d)

Là encore le buffer mémoire doit avoir une taille au moins égale au retard d.



Implémentation en C

/*

Fonction d’écho, pour un échantillon

  Paramètres :

x : échantillon d’entrée

y : échantillon de sortie

buf : buffer mémoire

d : retard de y par rapport à x (et taille du buffer)

i : indice de pointage dans le buffer (initialisé à 0)

*/

float

echo_ech(float x, int d, float *buf, int *i)          

{

         float      y;

         y=x+a**(buf+*i);

         *(buf+i)=x;

         if(++(*i)>=d)

                     *i=0;

         return y ;
}



Exemple avec d=3 et a=0,5


L’élément mis à jour dans le buffer à chaque itération est mis en gras.

Itérations


1

2

3

4

5

6

7

8

9

x

 

1

0

0

0

0

0

0

0

0

buffer

0

0

0

1

0

0

1

0

0

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

y

 

1

0

0

0,5

0

0

0

0

0



5.2) Echos multiples

Dans le cas d'échos multiples, on utilise la structure de filtre dite en peigne (en référence à sa réponse fréquentielle, qui ne laisse passer que certaines fréquences, régulièrement espacées). Contrairement au filtre utilisé pour l'écho simple, ici l'échantillon de sortie y(n) dépend d'échantillons passés de la sortie. Cette structure est celle d'un filtre à réponse impulsionnelle infinie (RIF).

Filtre en peigne

Filtre en peigne


Chaque échantillon de sortie est obtenu en effectuant la somme de l’échantillon d’entrée x(n) et d’une portion d’un échantillon de sortie précédent :

y(n)=x(n)+a.y(n-d)

avec a<1, sinon on aurait une divergence des valeurs de y(n), c’est à dire que la valeur des échantillons de sortie augmenterait indéfiniment. 'a' peut être vu comme un taux de ré-injection. d est un retard temporel, en nombre d’échantillons.


L’implémentation de ce bloc nécessite un tampon mémoire de 'd' échantillons. On pourrait penser initialement qu’il suffit de mémoriser 2 échantillons : x(n) et y(n-d), mais ça n’est pas suffisant. En effet, quand on calcul par exemple l’échantillon y(n), il dépend de l’échantillon passé y(n-d), qui doit avoir été mémorisé. L’échantillon suivant y(n+1) dépend quant à lui de l’échantillon y(n-d+1) qui doit donc lui aussi avoir été mémorisé.



Implémentation en C du filtre en peigne

float

peigne_ech(float x, int d, int D, float *buf, int *i)

{

         float      y;

         y=x+a**(buf+(*i+d)%D);
         *(buf+*i)=y;
         if(--(*i)<0)
                     (*i)+=D;

         return y;

}


Exemple de réponse impulsionnelle avec D=5 et d=3 :


x

1

0

0

0

0

0

y

1

0

0,5

0

0,25

0


Itérations
         
1
2
3
4
5
6
7
8
9

x

1
0
0
0
0
0
0
0
0

buffer

0

0

0

0

0

1

0

0

0

0

1

0

0

0

0

1

0

0

0

0

1

0

0,5

0

0

1

0

0,5

0

0

0

0

0,5

0

0

0

0

0,5

0

0,25

0

0

0,5

0

0,25

0

0

0,5

0

0,25


y
 
1
0
0
0,5
0
0
0,25
0
0



Exemple audio

voix_sanseffet.wav                             voix_echo.wav





6) Réverbération

Une structure de réverbérateur très connue est celle de Schroëder.

Le réverbérateur de Schroëder est composé de filtres en peigne en parallèle, suivis de filtres passe-tout en série.

Structure du réverbérateur de Schroëder


Le nombre de filtres élémentaires est variable.


Filtre passe-tout

Le nom de ce filtre provient des propriétés de l’opération qu’il réalise sur les échantillons d’entrée : l’amplitude du signal n’est pas affectée, quelle que soit sa fréquence. Sa réponse en fréquence est plate. Par contre la phase du signal d’entrée est modifiée.

Le schéma-bloc d’un filtre passe-tout est défini par :

La relation entrée/sortie est définie par :

y(n) = -a.x(n)+x(n-d)+a.y(n-d)

En général, le signal de sortie est un mélange du signal direct et du signal réverbéré, les amplitudes de ces deux composantes pouvant être ajustées séparément.

Réverbérateur complet

Le bloc réverbérateur complet



Exemple audio


voix_sanseffet.wav
                            voix_reverb.wav


7) Combinaison d’effets

Exemple audio

voix_sanseffet.wav                             voix_flanger+reverb+stereo.wav