Format WAV : créez des ondes sonores en C

GNU/Linux Magazine n° 190 | février 2016 | Vincent Magnin
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Vous voulez revivre les balbutiements de la musique électronique et les émois acoustiques des pionniers des années 50-60 ? Je vous propose en une série de deux articles d'autopsier le vénérable format WAV et de revenir aux bases de la synthèse sonore à l'aide d'un court programme en C.
Note

Un fichier WAV est un fichier binaire composé d'un en-tête suivi de signaux sonores qui, la plupart du temps, sont simplement codés en Pulse Coded Modulation (PCM). À chaque pas d'échantillonnage, l'amplitude de chaque canal sonore est codée par un entier signé sur deux octets, écrit en little endian. Il est donc très simple d'écrire un petit programme en C pour créer des ondes sonores. C'est ce que nous allons faire dans ce premier article avec comme modeste objectif d'obtenir un son sinusoïdal pur, brique de base qui nous permettra de générer des sons beaucoup plus complexes dans un second article.

Bien que l’on puisse faire remonter ses origines aux débuts de l’électricité, la première grande période d’exploration de la musique électronique se situe dans les années 50. Les pionniers se nomment Herbert Eimert (Klangstudie II, 1952), Karlheinz Stockhausen (Elektronische Studie I, 1953), Louis et Bebe Barron (B.O.F. de Forbidden Planet, 1956), etc. Ils synthétisent et manipulent des sons inouïs, c’est-à-dire que personne n’avait jamais entendu. Vous trouverez facilement sur YouTube leur travail exploratoire ainsi que les premières musiques populaires entièrement électroniques à la fin des années 60 et durant les années 70 (Gershon Kingsley, Kraftwerk, Tangerine Dream, Jean-Michel Jarre, etc.).

Soixante ans après ces pionniers, nous allons nous aussi nous initier à la synthèse sonore, en partant comme Stockhausen de la sinusoïde pure, à l’aide d’un court programme en C qui permet de créer des fichiers au format WAV. Nous allons donc explorer simultanément ce programme et la structure d'un fichier WAV classique, et terminer ce premier article par la synthèse d'un son sinusoïdal pur.

1. Le programme

Vous pouvez récupérer le fichier synthe.c sur le dépôt GitHub du magazine. Le code source est écrit en C, dans un style impératif simple et évitant autant que possible les syntaxes idiomatiques du C afin qu'il soit aisément compréhensible et traduisible dans tout autre langage. À vous de prendre possession du code, de l'améliorer ou de le réécrire au fil de vos explorations. Si vos souvenirs du C sont lointains, replongez-vous dans les hors-séries de GLMF consacrés au C [1, 2].

1.1 Programme principal

Rappelons que le son se propage dans l’air sous forme d’une onde de pression longitudinale, qui peut provenir des vibrations d’un corps matériel ou des mouvements de la membrane d’un haut-parleur. Un son peut donc être décrit par son amplitude en fonction du temps au niveau de sa source. Cette amplitude sera stockée dans des tableaux de réels : un pour le canal gauche et un pour le droit (ligne 10). La taille de ces tableaux sera calculée en fonction du taux d'échantillonnage et de la durée de la piste sonore. Ces variables sont globales :

01: #include <stdio.h>
02: #include <math.h>
03: #include <stdlib.h>
04: 
05: #define PI 3.14159265358979323846264338327
06: #define AMPLITUDE_MAXI 32767
07: 
08: unsigned int taux ;
09: unsigned long int taille ;
10: double *gauche, *droite ;
11: double duree ;

Le programme principal est simplement chargé de créer le fichier synthe.wav qui est un fichier binaire (paramètre "wb" ligne 108), d'y écrire l'en-tête du fichier WAV, de remplir les tableaux gauche et droite avec des signaux sonores ligne 111 (ici de la seconde 0 à la seconde 5 avec une fréquence de 440 Hz et une amplitude unitaire), qui sont ensuite normalisés selon AMPLITUDE_MAXI et écrits sous forme d'entiers signés dans le fichier (ligne 114). Avant de rendre la main, la mémoire allouée aux tableaux est libérée (lignes 117 et 118). Quant à la procédure generateur_enveloppe(), elle sera présentée dans l'article suivant consacré aux méthodes de synthèse sonore.

107: int main(void) {
108:  FILE *fichier_wav = fopen("synthe.wav", "wb") ;
109:  ecrire_en_tete_WAV(fichier_wav) ;
110: 
111:  mon_signal(0.0, 5.0, 440.0, 1.0) ;
112:  //generateur_enveloppe(0.0, 5.0, 30.0, 20.0, 80.0, 30.0) ;
113: 
114:  ecrire_donnees_normalisees_WAV(fichier_wav) ;
115:  fclose(fichier_wav) ;
116: 
117:  free(gauche) ;

1.2 Petit-boutiste

Dans un fichier WAV, tous les entiers sont stockés en little-endian, c'est-à-dire que l'on commence par écrire les octets de poids faible et que l'on termine par l'octet de poids fort [3], contrairement à l'écriture habituelle. C'est le rôle de la procédure ecrire_little_endian() qui utilise un masque binaire & 0x000000FF pour récupérer l'octet de poids le plus faible (ligne 17), qui l'écrit dans le fichier WAV ligne 18 puis qui décale de huit positions vers la droite les bits restants (>> 8) ligne 19, jusqu'à ce que tous les octets composant l'entier aient été écrits :

13: void ecrire_little_endian(unsigned int octets, int taille, FILE *fichier) {
14:  unsigned char faible ;
15: 
16:  while(taille > 0) {
17:  faible = octets & 0x000000FF ;
18:  fwrite(&faible, 1, 1, fichier) ;
19:  octets = octets >> 8 ;
20:  taille = taille - 1 ;

1.3 En-tête du fichier WAV

Avant que nous ne continuions à parcourir le code source, vous pouvez compiler et exécuter le programme, sans oublier l'option -lm qui est nécessaire pour lier le programme à la librairie mathématique du système :

$ gcc synthe.c -lm

Vous pouvez obtenir des informations sur le format du fichier de sortie synthe.wav avec la commande suivante :

$ file synthe.wav
synthe.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, stereo 44100 Hz

La commande hexdump permet de visualiser facilement le contenu de ce fichier binaire :

$ hexdump synthe_a.wav -C | head      
00000000 52 49 46 46 e4 7f a1 00 57 41 56 45 66 6d 74 20 |RIFF....WAVEfmt |

00000010 10 00 00 00 01 00 02 00 44 ac 00 00 10 b1 02 00 |........D.......|

00000020 04 00 10 00 64 61 74 61 c0 7f a1 00 00 00 00 00 |....data........|

00000030 04 08 04 08 01 10 01 10 ee 17 ee 17 c2 1f c2 1f |................|

00000040 77 27 77 27 04 2f 04 2f 61 36 61 36 88 3d 88 3d |w'w'././a6a6.=.=|

00000050 71 44 71 44 16 4b 16 4b 6e 51 6e 51 75 57 75 57 |qDqD.K.KnQnQuWuW|

00000060 24 5d 24 5d 75 62 75 62 63 67 63 67 ea 6b ea 6b |$]$]ububcgcg.k.k|

00000070 03 70 03 70 ac 73 ac 73 e1 76 e1 76 9e 79 9e 79 |.p.p.s.s.v.v.y.y|

00000080 e1 7b e1 7b a7 7d a7 7d ee 7e ee 7e b7 7f b7 7f |.{.{.}.}.~.~....|

00000090 fe 7f fe 7f c5 7f c5 7f 0c 7f 0c 7f d2 7d d2 7d |.............}.}|

L'option -C (pour canonique) permet d'afficher les adresses (ou offsets) en hexadécimal à gauche, les octets en hexadécimal au milieu et les caractères correspondants à droite. Le tout est envoyé à la commande head à l'aide d'une pipe, afin de n'afficher que les dix premières lignes. Les 44 octets de l'en-tête précédant les données sont ici mis en couleur.

Nous découvrons à l'aide de ces commandes qu'un fichier WAV utilise le format RIFF (Ressource Interchange File Format) publié par Microsoft et IBM en 1991 [4]. Ce format peut en fait contenir des données audio ou vidéo sous différents formats. Une de ses caractéristiques est d'être organisé en chunks (terme que je traduirai par blocs) et sub-chunks. Chaque bloc commence par un tag de quatre caractères, suivi d'un entier sur quatre octets codant la taille en octets des données contenues à sa suite dans ce bloc.

Après avoir défini ou calculé les paramètres du fichier WAV, notre procédure ecrire_en_tete_WAV() va ainsi commencer ligne 35 l'écriture du fichier par le tag RIFF. On écrit ensuite sur quatre octets la taille de ce qui va suivre, c'est-à-dire les 36 octets terminant l'en-tête plus le nombre d'octets de données.

24: void ecrire_en_tete_WAV(FILE *fichier) {
25:  unsigned short int nb_canaux = 2 ;
26:  unsigned short int nb_bits = 16 ;
27:  taux = 44100 ; // En Hz
28:  duree = 60 ; // En secondes
29:  taille = taux * duree ;
30:  unsigned long int nb_octets_donnees = (nb_bits / 8) * nb_canaux * taille ;
31: 
32:  gauche = calloc(taille, sizeof(double)) ;
33:  droite = calloc(taille, sizeof(double)) ;
34: 
35:  fwrite("RIFF", 4, 1, fichier) ;
36:  ecrire_little_endian(36 + nb_octets_donnees, 4, fichier) ;

On a également alloué, lignes 32 et 33, la mémoire nécessaire pour stocker les échantillons sonores dans les tableaux de réels double précision gauche et droite. La fonction calloc() initialise également le contenu de ces tableaux avec des octets nuls. En toute rigueur, la norme du C ne garantit pas que cela corresponde à la représentation du réel zéro, mais c'est le cas pour tous les microprocesseurs respectant la norme IEEE 754.

Ligne 37, on écrit ensuite le tag WAVE qui indique que le fichier va contenir des données audio au format WAVE (Waveform audio file format), plus communément appelé WAV d'après son extension [5, 6]. Suit un sous-bloc commençant par le tag fmt (avec comme quatrième caractère un espace), suivi du nombre d'octets terminant ce sous-bloc (16 octets, 0x10 en hexadécimal). Le format audio proprement dit est ensuite codé ligne 41 sur deux octets : le WAV est en fait un conteneur pouvant contenir des données audio codées dans différents formats, éventuellement même en MP3. Nous utilisons ici le codage 1 (le plus courant) : le LPCM (Linear Pulse-Code Modulation), appelé plus simplement PCM,qui est également utilisé dans les CD audio et que nous décrirons plus loin.

37:  fwrite("WAVE", 4, 1, fichier) ;
38: 
39:  fwrite("fmt ", 4, 1, fichier) ;
40:  ecrire_little_endian(16, 4, fichier) ;
41:  ecrire_little_endian(1, 2, fichier) ;

Viennent ensuite :

- le nombre de canaux, ici deux pour la stéréo (ligne 42) ;

- le taux (ou fréquence) d'échantillonnage, ici 44100 échantillons par seconde (0x0002B110 en hexadécimal, ce qui donne 10 b1 02 00 en little endian), comme dans un CD audio ;

- le nombre d'octets par seconde (ligne 44), valeur proportionnelle au taux d'échantillonnage, au nombre de canaux et au nombre de bits utilisés pour stocker chaque échantillon, ici 16 bits (0x10 en hexadécimal) comme dans un CD audio ;

- le nombre d'octets par échantillon (ligne 45), calculé à partir du nombre de canaux et du nombre de bits ;

- le nombre de bits par échantillon.

42:  ecrire_little_endian(nb_canaux, 2, fichier) ;
43:  ecrire_little_endian(taux, 4, fichier) ;
44:  ecrire_little_endian(taux * nb_canaux * (nb_bits / 8), 4, fichier) ;
45:  ecrire_little_endian(nb_canaux * (nb_bits / 8), 2, fichier) ;
46:  ecrire_little_endian(nb_bits, 2, fichier) ;

On remarquera au passage qu'il y a une certaine redondance dans ces données, certaines pouvant être calculées à partir d'autres (lignes 44 et 45).

1.4 Les données

Nous arrivons enfin au sous-bloc contenant les données et qui commence donc par le tag data, suivi par la taille des données qui vont suivre (ligne 49). Cette taille étant codée par un entier sur quatre octets sera donc limitée à 4 Gio.

48:  fwrite("data", 4, 1, fichier) ;
49:  ecrire_little_endian(taille * nb_canaux * (nb_bits / 8), 4, fichier) ;

La numérisation du son s’opère par échantillonnage (figure 1) : son amplitude est mesurée ou définie à des intervalles réguliers et codée sous forme d’un entier signé proportionnel à cette amplitude [7, 8]. C'est le format PCM déjà évoqué. On utilise ici deux octets (16 bits) pour le canal gauche suivis de deux octets pour le canal droit, et ce 44100 fois par seconde. Dans notre programme, la procédure ecrire_donnees_normalisees_WAV() s’occupe de normaliser le volume sonore et de le numériser sous forme d’un entier signé sur 2 octets, donc compris au maximum entre -32768 à +32767 (−215 et 215 − 1). La constante AMPLITUDE_MAXI est déclarée ligne 6 et vaut 32767. Les lignes 56 à 59 sont chargées de trouver le niveau sonore maximum sur l'ensemble des deux canaux. L'écriture des entiers dans le fichier WAV se fait en little-endian lignes 62 et 63 :

52: void ecrire_donnees_normalisees_WAV(FILE *fichier) {
53:  unsigned int i ;
54:  double maxi = 1e-16 ;
55: 
56:  for (i = 0 ; i < taille ; i=i+1) {
57:  if (fabs(gauche[i]) > maxi) maxi = fabs(gauche[i]) ;
58:  if (fabs(droite[i]) > maxi) maxi = fabs(droite[i]) ;
59:  }
60: 
61:  for (i = 0 ; i < taille ; i=i+1) {
62:  ecrire_little_endian((int)(gauche[i]/maxi*AMPLITUDE_MAXI), 2, fichier) ;
63:  ecrire_little_endian((int)(droite[i]/maxi*AMPLITUDE_MAXI), 2, fichier) ;

L’espace disque utilisé par un fichier WAV stéréo à 44100 Hz et 16 bits (qualité d'un CD) sera donc de 44100*2*(16/8)=176400 octets par seconde, soit 10,6 Mio/min. Vous pouvez compresser vos fichiers WAV en MP3 (ou en Ogg Vorbis) avec la commande avconv ou ffmpeg :

$ avconv −i monfichier.wav monfichier.mp3

Fig. 1 : Échantillonnage d'un signal.

2. Synthèse d'un son sinusoïdal

Après écriture de l'en-tête du fichier WAV, le programme principal appelle ligne 111 la procédure mon_signal() qui va remplir les tableaux de réels gauche[] et droite[] avec de simples signaux sinusoïdaux Amp * sin(omega*t + phi), avec ici une phase phi nulle, commençant à l'instant t1 et se terminant à l'instant t2, ces deux tableaux constituant en fait une piste dont la durée est indiquée dans la variable globale duree. Vous pouvez ainsi disposer d'autant de sons que vous voulez sur cette piste et même les superposer, car la procédure mon_signal() ajoute en fait le signal aux données déjà présentes dans les tableaux.

67: void mon_signal(double t1, double t2, double f, double Amp) {
68:  unsigned int i;
69:  double omega = 2 * PI * f ;
70:  double t = 0 ;
71:  double dt = 1.0 / taux ;
72:  double phi = 0 ;
73: 
74:  for (i=(unsigned int)(t1*taux) ; i<(unsigned int)(t2*taux) ; i=i+1) {
75:  gauche[i] = gauche[i] + Amp * sin(omega*t + phi) ;
76:  droite[i] = droite[i] + Amp * sin(omega*t + phi) ;
77:  t = t + dt ;

Rappelons que omega=2*PI*f est la pulsation, exprimée en radians par seconde.

Remarquez dans le résultat de la commande hexdump présentée plus haut, que les quatre premiers octets de données, juste après l'en-tête du fichier, valent zéro puisque notre programme commence la sinusoïde en un nœud. Et les octets suivants se répètent deux par deux puisqu'ici nous générons deux canaux stéréo identiques.

Vous pouvez écouter le fichier résultat synthe.wav avec votre lecteur habituel ou en utilisant la commande play après installation du paquet de SoX :

$ play synthe.wav

synthe.wav:
File Size: 10.6M     Bit Rate: 1.41M
 Encoding: Signed PCM     
 Channels: 2 @ 16-bit    
Samplerate: 44100Hz       
Replaygain: off          
 Duration: 00:01:00.00   
In:1.55% 00:00:00.93 [00:00:59.07] Out:41.0k [-=====|=====-] Hd:5.0 Clip:0

Vous obtenez un son très pur et vous pourrez visualiser la sinusoïde en zoomant dans Audacity. Rien de très excitant pour l'instant, mais sachez que la sinusoïde est un peu à la musique ce que l'alphabet est à la littérature !

Conclusion

Nous avons fait le tour du programme, à part la procédure generateur_enveloppe() que nous aborderons la prochaine fois, l'enveloppe étant un concept important de la synthèse sonore.

Nous avons découvert comment étaient codés les fichiers WAV les plus courants, mais sachez qu'ils peuvent également contenir plusieurs pistes ainsi que des métadonnées. Ils sont même utilisés par certains logiciels d'électronique pour stocker des ondes échantillonnées non acoustiques. Bien que le standard WAV présente quelques défauts tels que des ambiguïtés ou une certaine redondance des données dans l'en-tête, il reste très utilisé dans les logiciels de manipulation du son et il est assez facile d'écrire un programme écrivant ou lisant ce format.

Les pionniers de la musique électronique des années 50 travaillaient bien sûr avec des circuits analogiques. Quant à nous, avec un court programme en C nous pouvons partir sur leurs traces et générer des sons numériques dont nous maîtrisons parfaitement chaque paramètre. Et tel était l'objectif de Stockhausen : pouvoir travailler avec des sons parfaitement maîtrisés, puisque synthétisés [9]. Nous verrons dans un prochain article que la synthèse sonore permet effectivement de mieux comprendre ce qu'est un harmonique, un timbre, un accord, etc. Et que l'on peut créer des sons étonnants avec finalement une grande économie de moyens !

Références

[1] BROSSEAU F., « Langage C, le guide pour mieux développer en C sous Linux », GNU/Linux Magazine HS n°70.

[2] CHAZALLET S., « Langage C, le guide pour apprendre à programmer en C en 5 jours », GNU/Linux Magazine HS n°80.

[3] Codage Little Endian : https://fr.wikipedia.org/wiki/Endianness

[4] Le format RIFF : https://en.wikipedia.org/wiki/Resource_Interchange_File_Format

[5] Le format WAV : https://en.wikipedia.org/wiki/WAV

[6] Spécifications du format WAV : http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html

[7] Collectif, « Le son numérique », Linux Pratique HS n°29, p. 8 à 15.

[8] Codage PCM : https://en.wikipedia.org/wiki/Pulse-code_modulation

[9] Laurent Fichet,Les théories scientifiques de la musique aux XIXe et XXe siècles,Vrin, 1996, ISBN 978-2-7116-4284-7.

Pour aller plus loin

Si vous n'avez pas la patience d'attendre le second article, vous pouvez consulter les documents suivants :

BENSON D., « Music: A Mathematical Offering », 2008, livre téléchargeable gratuitement en PDF sur son site : http://homepages.abdn.ac.uk/mth192/pages/html/maths-music.html

RISSET J.-C., « COMPUTER MUSIC: WHY ? » : https://www.utexas.edu/cola/france-ut/_files/pdf/resources/risset_2.pdf

Tags : Audio/Vidéo, C, WAV