Une fois décompressé, ce repertoire contient les fichiers sources, un fichier README, ainsi qu'un Makefile pour compiler et installer automatiquement les bibliotheques.
Karim Barkati
Maîtrise d'Informatique
Septembre 2000
Programmation de bibliothèques
C compatibles C++ pour la gestion du son sous Linux
Directeur de recherche : M. Daniel
Goossens
Université Paris 8 / Vincennes
- Saint-Denis
Je remercie les enseignants du département d'Informatique qui ont façonné mon approche de l'informatique : Harald WERTZ, Jean MEHAT, Roger TANGUY, Jean-François DEGREMONT, ALI CHERIF, Vincent BOYER, Daniel HECKER, Eric DAUBRESSE, et plus particulièrement Vincent LESBROS.
Je souhaite aussi remercier les enseignants du département de Musicologie qui m'ont donné le goût de l'informatique musicale, dans l'ordre chronologique : Ricardo JACOBSON, Martin LALIBERTE, Mario MARY, José-Manuel LOPEZ-LOPEZ, Makis SOLOMOS, Anne SEDES, et Horacio VAGGIONE.
Je remercie mes camarades Alain ZHU, Hervé FERDINAND, Benjamin DRIEU, et Arnauld MICHELIZZA, administrateur du " bocal ", pour leurs conseils techniques et leurs relectures éclairées, ainsi qu'Emmanuelle MEUNIER pour ses corrections patientes.
Je remercie enfin Daniel GOOSSENS, mon directeur de recherche, pour sa générosité et sa perspicacité.
Ce travail d'une année tente de combler un manque dans notre département d'Informatique : l'absence d'outils facilitant l'utilisation du son dans les programmes en langages C ou C++ sous Linux. Il a abouti à la création des deux bibliothèques kwav et kfft, et du logiciel kediteur. La bibliothèque kwav permet de manipuler du son de manière relativement simple, en gérant automatiquement le pilote de la carte son et le format wav, tandis que kfft propose des fonctions pour l'analyse et la resynthèse du son d'après la transformée de Fourier. Le programme kediteur permet une édition graphique des fichiers sons au format wav, avec des opérations de lecture, d'enregistrement, de sauvegarde, de découpage, d'affichage des paramètres audionumériques, et d'analyse fréquentielle graphique. Ces outils sont en service à l'université depuis un semestre, et sont documentés par un manuel en ligne.
Ce mémoire présente d'une part l'utilisation de ces outils, illustrée par des programmes simples, et d'autre part leur développement, avec les notions et les conventions importantes qui concernent les bibliothèques en langage C et l'audionumérique.
I. Introduction *
II. Développement de bibliothèques *
B. La bibliothèque kwav *
1. Principe *
2. Utilisation *
C. La bibliothèque kfft *
1. Présentation de la FFT *
2. Utilisation *
D. Le programme kediteur *
1. Visualisation du son *
E. Pour aller plus loin *
1. Principes de développement d'une bibliothèque
sous Linux *
Le son reste encore le parent pauvre de l'informatique,
comparé au graphisme notamment. En général, les ordinateurs
sont mal ou non équipés pour le son. Les outils logiciels
aussi font défaut, en particulier pour la programmation, puisqu'il
existe de nombreuses bibliothèques graphiques, comme X11, QT, ou
TK, mais une seule bibliothèque sonore à ma connaissance
sous Unix. Elle se nomme AudioFile (AUDIOFILE, 1993), et repose sur une
architecture complexe client / serveur, semblable à celle de X11.
Or, plusieurs domaines de recherche en informatique nécessitent
des outils sonores simples pour s'épanouir, comme la synthèse
sonore, la reconnaissance vocale, ou l'informatique musicale.
Le département d'informatique de l'université Paris 8 ne possède pas de bibliothèque sonore. L'un des objectifs de ce travail est de proposer aux étudiants de ce département des outils qui leur permettent de gérer et d'analyser du son simplement dans leurs programmes en langage C ou C++. Matériellement, nos ordinateurs alpha du " bocal " (notre salle d'informatique) sont déjà aptes à gérer du son, puisqu'ils sont tous équipés d'une carte son avec une entrée et une sortie audio, d'un petit haut-parleur et des périphériques systèmes nécessaires (TRANTER 1999). Cependant, lorsque l'on souhaite programmer du son sur ces machines, deux difficultés surgissent : la première réside dans l'appel de primitives assez absconses pour la configuration la carte son, et la seconde dans l'absence de format standard pour les fichiers audionumériques.
Afin de parer à ces difficultés, cette maîtrise comprend deux bibliothèques, kwav et kfft, un éditeur graphique de fichiers audio, kediteur, ainsi que ce doument qui détaille l'utilisation et le fonctionnement de ces programmes. L'éditeur de sons permet à la fois de démontrer les potentialités de ces bibliothèques, et de témoigner de leur bon fonctionnement lors des opérations de maintenance. Les bibliothèques sont en libre-service à l'université depuis le mois de février 2000. Elles sont documentées par un manuel en ligne (taper man kwav sous shell), et les administrateurs possèdent une version complète pour réinstaller, en cas de besoin, tout ou partie des bibliothèques. Quelques étudiants les ont d'ailleurs utilisées ce semestre pour leurs projets de licence.
Comme l'expliquent Brian KERNIGHAN et Denis RITCHIE,
les principaux créateurs du C, " les fonctions partitionnent les
gros traitements en tâches plus petites, et elles permettent de construire
des programmes à partir de briques déjà écrites,
au lieu de tout recommencer à zéro. Les fonctions bien conçues
cachent les détails de leur fonctionnement aux parties du programme
qui n'ont pas besoin de les connaître, ce qui clarifie l'ensemble
et facilite les modifications ultérieures. " (KERNIGHAN et RITCHIE
1997, p. 67). Dans ce sens, le développement d'une bibliothèque
constitue un exercice difficile, car il faut dépasser ses propres
réflexes de programmation pour imaginer les besoins des autres programmeurs.
Une attention particulière a donc été portée
à la conception de l'ensemble : traitements à regrouper,
noms des fonctions et des variables, pertinence des arguments, des valeurs
de retour, et de leurs types.
Le terme " bibliothèque " est employé
ici au sens du langage C : il s'agit d'un fichier regroupant un ensemble
de fonctions et de données utilisables par d'autres programmes.
" Bibliothèque " est la traduction correcte de l'anglais " library
",
même si l'on emploie souvent à tort le terme " librairie ".
On utilise souvent les bibliothèques standards de la norme ANSI
du langage C, comme stdio
pour la célèbre fonction printf().
En pratique, les bibliothèques comportent plusieurs fichiers : les
fichiers d'en-tête, les archives, et les fichiers partagés.
Ces fichiers sont placés dans des répertoires accessibles
à tous les utilisateurs, ici /usr/local/lib
et /usr/local/include.
Les fichiers dont le nom se termine par .h
sont appelés " fichiers d'en-tête ", " header " en
anglais. Ils doivent être inclus au début des programmes,
par une simple ligne :
#include "/usr/local/include/kwav.h"
Cette ligne sera remplacée par le contenu du fichier d'en-tête lui-même (KERNIGHAN et RITCHIE 1997). Ces fichiers contiennent tout ce dont le programme aura potentiellement besoin de connaître, comme le nom des fonctions externes, les structures, les macros, et les constantes et variables globales.
La directive d'inclusion #include peut prendre deux formes différentes. Le nom du fichier d'en-tête est entouré par les caractères < > :
#include <stdio.h>
Cette forme est utilisée pour les fichiers d'en-tête standard, où la recherche du fichier se fait dans un emplacement défini par l'implémentation (généralement /usr/include sous Unix).
Le nom du fichier d'en-tête est entouré par les caractères " " :
#include "/usr/local/include/kwav.h"
Cette forme est plutôt réservée
aux fichiers d'en-tête non standards, et leur recherche se fait par
défaut dans le répertoire du fichier source que l'on est
en train de compiler. Pour l'utilisation de kwav
ou kfft, il est
préférable d'indiquer le chemin absolu du fichier d'en-tête,
comme dans l'exemple précédent, sauf si l'on a copié
les fichiers d'en-tête dans son propre répertoire.
Les fichiers dont le nom se termine par .a
sont appelés " archives statiques ", et contiennent les fonctions
elles-mêmes, sauf la fonction main().
Ils s'utilisent comme des fichiers .c
ordinaires lors de la compilation, c'est-à-dire qu'il suffit de
les ajouter à la suite du nom du programme principal :
gcc prog.c /usr/local/lib/kwav.a /usr/local/lib/kfft.a -lm
Ces fichiers sont dits " statiques " car le code
complet de l'archive est inclus dans le fichier exécutable produit,
ce qui peut présenter un inconvénient au niveau de la mémoire
si l'archive est importante. En revanche, le fichier exécutable
ainsi produit fonctionnera même si la bibliothèque est absente
ou endommagée. L'option -lm
concerne la bibliothèque mathématique standard, utilisée
par kwav et kfft.
Les fichiers dont le nom se termine par .so
sont appelés " bibliothèques partagées ", " shared
object " en anglais. Leur contenu est identique aux archives, mais
leur forme précompilée permet une utilisation " dynamique
", ce qui évite le problème de mémoire posé
par les archives. Lors d'une compilation dynamique, le code nécessaire
n'est pas ajouté au fichier exécutable, il ne sera recherché
qu'au moment de son exécution effective. L'aspect dynamique offre
un autre avantage : les programmes n'ont pas besoin d'être recompilés
pour bénéficier d'une amélioration ou d'une correction.
Il suffit que la bibliothèque récemment modifiée porte
le même nom et se trouve au même endroit. Par contre, il est
indispensable que la bibliothèque soit présente dans son
répertoire lors de l'exécution, ce qui n'est pas le cas pour
une compilation statique.
La ligne de commande pour la compilation est moins triviale que pour les archives, puisqu'il faut indiquer deux options : -L, suivie du répertoire où se trouve la bibliothèque, puis -l,suivie du nom de la bibliothèque.
gcc mon_programme.c -L/usr/local/lib -lkwav
kwav est une
bibliothèque C compatible C++. Elle permet de gérer la carte
son et les fichiers audionumériques au format wav. Elle comprend
cinq fichiers :
Cependant, kwav
n'est pas qu'une simple collection de fonctions, mais plutôt un environnement
de programmation sonore. En effet, en utilisant cette librairie, le programmeur
dipose de structures spécifiques comme kwav_struct
et kwav_header,
et peut manipuler des variables globales que nous appelons globales
d'environnement. Celles-ci représentent les paramètres
pertinents du signal audionumérique, comme la fréquence d'échantillonnage,
le taux d'échantillonnage, ou le nombre de voies. Cet environnement
est préconfiguré avec les valeurs par défaut indiquées
dans le manuel en ligne, mais le programmeur peut le reconfigurer, ou laisser
la possibilité à l'utilisateur de choisir les valeurs qu'il
souhaite. La lecture d'un fichier dont les valeurs propres sont différentes
des globales d'environnement se déroule correctement, sans perturber
l'environnement. Nous détaillerons les moyens employés pour
arriver à ce résultat plus loin. La compréhension
de ces mécanismes de paramètrage de l'environnement audio
n'est pas nécessaire pour la plupart des programmes.
La bibliothèque kwav permet de lire des fichiers wav, d'enregistrer du son dans des fichiers wav, et de manipuler le son lui-même, en se déplaçant dans une suite d'échantillons. La plupart des programmes peuvent être ainsi implémentés, simplement en ouvrant le périphérique de la carte son, en lisant ou en écrivant en continu, puis en refermant le périphérique (TRANTER, 2000).
L'utilisation d'un programme de mixage comme xmix
peut être nécessaire avant de lire ou d'enregistrer du son,
pour régler les niveau sonores (CHUNG, 1998).
Le petit programme jouerwav.c lit un fichier wav en utilisant la bibliothèque kwav :
#include <stdio.h>
#include "/usr/local/include/kwav.h"
main (int argc, char *argv[])
{
kwav_struct sonwav;
char nom[256];
kwav_Init(argc, argv);
printf("Nom du fichier a lire : ");
scanf("%s", nom);
kwav_Load(&sonwav, nom);
kwav_Play(sonwav);
kwav_Release();
}
Ce programme témoigne d'une volonté de simplicité explicite. En effet, il suffit d'inclure le fichier d'en-tête kwav.h, de déclarer une structure kwav_struct, et d'appeler quatre fonctions. La première, kwav_Init(argc, argv), initialise l'environnement en ouvrant la carte son, et en positionnant les variables globales en fonction de la ligne de commande (cf. Gestion de la ligne de commande). La dernière, kwav_Release(), referme la carte son. Les deux fonctions intermédiaires effectuent le travail central. kwav_Load(&sonwav, nom) ouvre le fichier demandé en lecture, alloue une structure kwav_struct en mémoire vive, puis la remplit à partir du fichier. Enfin, kwav_Play(sonwav) configure la carte son d'après l'en-tête de la structure, puis envoie les échantillons à la carte son.
Pour la compilation, on peut choisir indifféremment les deux possibilités évoquées précédemment, mais les autres exemples seront fournis en compilation statique :
alpha6[/home/barkati/SON/KWAV/MYLIB6]: gcc jouerwav.c -L/usr/local/lib -lkwav
alpha6[/home/barkati/SON/KWAV/MYLIB6]: gcc jouerwav.c /usr/local/lib/kwav.a -lm
Lors de l'exécution, il faut taper le nom du fichier que l'on souhaite entendre, pour qu'il soit joué aussitôt que l'on valide ce nom :
alpha6[/home/barkati/SON/KWAV/MYLIB6]: a.out
Nom du fichier a lire : son.wav
alpha6[/home/barkati/SON/KWAV/MYLIB6]:
Si l'on passe l'option -v lors de l'exécution, ou --verbose pour " verbeux ", on peut alors observer l'évolution de ces quatres étapes et des valeurs importantes :
alpha5:~/temp/PACK_LIB# ./jouerwav -v
Option(s) demandee(s): -v
Valeurs par defaut :
stereo = 1 voie
freq = 22050 Hertz
bits = 16 bits
duree = 1000 ms
nom = son.wav
kwav_Init_dsp :
stereophonie = 1 voie(s)
frequence d'echantillonnage = 22094 hertz
taille d'echantillon = 16 bits
taille de bloc = 4096 octets
Nom du fichier a lire : son.wav
lecture du fichier "son.wav":
lecture header...
{ main_chunk = 'RIFF'
{ length = 44144 octets
{ chunk_type = 'WAVE'
{ sub_chunk = 'fmt'
{ length_chunk = 16 octets
{ format = 1 (PCM-code)
{ modus = 1 (mono)
{ sample_fq = 22050 echantillons/s
{ byte_p_sec = 44100 octets/s
{ byte_p_spl = 2 octets/echantillon
{ bit_p_spl = 16 bits/echantillon
{ data_chunk = 'data'
{ data_length = 44100 octets
lecture data...
allouer...
->fin allouer
->fin lecture "son.wav"
kwav_Reconfig_dsp :
stereophonie = 1 voie(s)
frequence d'echantillonnage = 22094 Hertz
taille d'echantillon = 16 bits
jouer...
->fin jouer
alpha5:~/temp/PACK_LIB#
Voici le programme enregistrerwav.c qui enregistre du son dans un fichier wav. Ce programme ressemble fort au précédent :
#include "/usr/local/include/kwav.h"
#include <stdio.h>
main (int argc, char *argv[])
{
kwav_struct son;
char nom[256];
int duree;
kwav_Init(argc, argv);
printf("Nom du fichier a enregistrer : ");
scanf("%s", nom);
printf("duree en millisecondes : ");
scanf("%d", &duree);
kwav_Record(&son, kwav_ms2oct(duree));
kwav_Play(son);
kwav_Save(son, nom);
kwav_Release();
}
Ce programme demande à l'utilisateur le temps d'enregistrement qu'il souhaite en millisecondes, et le stocke dans la variable duree. Celle-ci est convertie en octets par la fonction kwav_ms2oct. L'enregistrement est réalisé par la fonction kwav_Record. Afin que la taille du futur fichier ne dépende pas d'un message d'arrêt, et risque ainsi de saturer la mémoire, la fonction kwav_Record demande en argument cette taille, exprimée en octets. kwav_Record alloue une zone de la mémoire en conséquence, puis y écrit les données qui proviennent de la carte son. A cette étape de l'exécution, le son est seulement enregistré en mémoire vive, dans une structure de type kwav_struct. L'enregistrement sur le disque dur nécessite l'appel de la fonction kwav_Save, qui formate automatiquement le fichier en wav.
La compilation, puis l'exécution, peuvent se faire de la manière suivante :
alpha5[/home/barkati/SON/KWAV/PACK_LIB]: gcc -L/usr/local/lib -lkwav
enregistrerwav.c
alpha5[/home/barkati/SON/KWAV/PACK_LIB]: a.out
Nom du fichier a enregistrer : test
duree en millisecondes : 2000
alpha5[/home/barkati/SON/KWAV/PACK_LIB]: ls -l
-rw------- 1 barkati users 88244 Aug 5 20:45 test.wav
On remarque que l'enregistrement commence dès que la durée est validée, que les permissions sont attribuées à l'utilisateur en lecture et écriture, et que l'extension .wav est ajouté automatiquement au nom du fichier.
L'option -v permet de suivre là encore les étapes importantes de l'exécution :
alpha5[/home/barkati/SON/KWAV/PACK_LIB]: a.out -v
Option(s) demandee(s): -v
Valeurs par defaut :
stereo = 1 voie
freq = 22050 Hertz
bits = 16 bits
duree = 1000 ms
nom = son.wav
Init_dsp :
stereophonie = 1 voie(s)
frequence d'echantillonnage = 22094 Hertz
taille d'echantillon = 16 bits
taille de bloc = 4096 octets
Nom du fichier a enregistrer : test2
duree en millisecondes : 1000
allouer...
->fin allouer
enregistrer...
->fin enregistrer
Reconfig_dsp :
stereophonie = 1 voie(s)
frequence d'echantillonnage = 22094 Hertz
taille d'echantillon = 16 bits
jouer...
->fin jouer
ecriture du fichier "test2.wav"...
header... data...
->fin d'ecriture du fichier "test2.wav"
L'astuce employée fréquement pour enregistrer
du son sans microphone consiste à utiliser l'oreillette d'un casque
audio. En effet, les deux prises sont au même format mini-jack, et
l'oreillette possède un mécanisme transducteur semblable
à un microphone. Sa membrane peut convertir elle aussi les variations
de pression acoustique en courant éléctrique. Bien sûr,
la qualité de l'enregistrement sera médiocre, néanmoins
reconnaissable.
Voici un programme concis (amplifier.c) qui amplifie un son lu depuis le disque dur, puis joue ce son amplifié :
#include <stdio.h>
#include "/usr/local/include/kwav.h"
main (int argc, char *argv[])
{
kwav_struct sonwav;
char nom[256];
int nb_ech;
int coeff;
int i;
kwav_Init(argc, argv);
printf("Nom du fichier a lire : ");
scanf("%s", nom);
kwav_Load(&sonwav, nom);
printf("Valeur du coefficient : ");
scanf("%d", &coeff);
nb_ech = (sonwav.head.data_length) / (sonwav.head.byte_p_spl);
for(i=0; i<nb_ech; i++)
sonwav.data[i] *= coeff;
kwav_Play(sonwav);
kwav_Release();
}
Le principe consiste à calculer le nombre d'échantillons à traiter d'après les informations contenues dans le champs head de la structure, puis de parcourir et modifier échantillon par échantillon la suite d'échantillons pointée par le champs data :
nb_ech = (sonwav.head.data_length) / (sonwav.head.byte_p_spl);
for(i=0; i<nb_ech; i++)
sonwav.data[i] *= coeff;
De nombreuses opérations de traitement du signal peuvent être programmées à partir de ce modèle, notamment certains filtrages, avec des opérations un peu plus complexes. La modification est ici une simple multiplication de l'amplitude, échantillon par échantillon. Le coefficient est demandé à l'utilisateur, puis une boucle remplace chaque échantillon par son ancienne valeur multipliée par le coefficient, du premier jusqu'au dernier. Le nombre d'échantillons est déduit des informations contenues dans l'en-tête du fichier wav, recopiées dans la structure de type kwav_struct.
La compilation et l'éxecution restent aussi simples que pour les programmes précédents :
alpha6[/home/barkati/SON/KWAV/PACK_LIB]: gcc -L/usr/local/lib -lkwav
amplifier.c -o amplifier
alpha6[/home/barkati/SON/KWAV/PACK_LIB]: amplifier
Nom du fichier a lire : son.wav
Valeur du coefficient : 2
alpha6[/home/barkati/SON/KWAV/PACK_LIB]:
#include <stdio.h>
#include <string.h>
#include "/usr/local/include/kwav.h"
main (int argc, char *argv[])
{
kwav_struct sonwav;
char nom[256];
kwav_Init(argc, argv);
strcpy(nom, kwav_get_gfilename());
kwav_Load(&sonwav, nom);
kwav_Play(sonwav);
kwav_Release();
}
Ainsi, ce programme jouerwav2.c
ne propose plus comme jouerwav.c
d'entrer le nom du fichier à lire. Il lira le fichier désigné
par kwav_get_gfilename
: soit le fichier son.wav
par défaut ou suite à une saisie incorrecte ou absente, soit,
suite à une saisie correcte, le nom de fichier passé dans
la ligne de commande. Pour que ce nom soit accepté, il faut qu'il
contienne une extension .wav,
sauf si l'on précise l'option -n.
Un argument incompris par kwav_Init
sera tout simplement ignoré, ce qui a pour effet dans le cas présent
de lire le son par défaut.
La cohérence du choix des arguments et des valeurs de retour demande aussi une grande rigueur (KOENIG 1991). Le passage par adresse est utilisé au minimum, uniquement quand une fonction doit modifier une variable de la fonction appelante (cf. Gestion de l'écriture et de la lecture sur le disque dur). Les risques posés par les variables globales (DELANNOY 1992) sont contournés par implémentation d'accesseurs et de modificateurs spécifiques, comme kwav_get_gfreq ou kwav_set_gfreq.
En cas de problème, un mécanisme peut
activer l'affichage des étapes et des valeurs en cours, car un affichage
systématique est encombrant lorsque tout se déroule correctement.
C'est le rôle de la variable globale d'environnement gverbose,
qui autorise, lorsqu'elle vaut _KWAV_OUI
ou
1, l'exécution des fonctions printf
disséminées dans le code.
Vincent LESBROS propose une définition intuitive des sons échantillonnés (LEBROS 1995) : " Les sons échantillonnés (ou signaux sonores échantillonnés), ou sons numériques, sont conservés sous la forme amplitude / temps. Un son est alors représenté par une suite de valeurs numériques décrivant les variations de pression de l'air par rapport à la pression normale. Une façon intuitive de la présenter est de dire que les valeurs indiquent la position de la membrane du haut-parleur (ou du micro, ou encore du tympan) à chaque instant. ". Nous appelons " échantillon " une seule de ces mesures de pression acoustique, codée numériquement sur un nombre entier plus ou moins grand selon la résolution, et " flux audionumérique " le déplacement d'une suite d'échantillons, c'est-à-dire leur lecture ou leur écriture, selon le sens du déplacement. La fréquence d'échantillonnage indique le nombre de mesures effectuées par seconde, c'est-à-dire le débit du flux audionumérique par voie, exprimé en hertz. Les sons sont monophoniques s'ils ne contiennent qu'une voie, et stéréophoniques s'ils en contiennent deux, une voie à droite, et l'autre à gauche.
Puisque le son numérique est une suite d'échantillons, une façon évidente de les représenter en langage C consiste à définir un type pour les échantillons, KWAV_ECH, puis d'utiliser un tableau d'échantillons. En fait, nous utilisons un pointeur data sur des échantillons, pour faciliter les allocations dynamiques :
typedef struct {
kwav_header head;
KWAV_ECH *data;
} kwav_struct;
Les échantillons d'un signal sonore numérique peuvent être codés sur 8, 12, 16, 24, ou 32 bits. Parmi ces résolutions, le type KWAV_ECH ne peut prendre qu'une seule valeur, que nous avons fixé à 16 bits par défaut, car c'est la valeur la plus répandue. Cependant, les sons codés sur 8 bits sont aussi très répandus, et le typage rigide du langage C ne permet pas à un type de changer de taille aisément. Les unions ne résolvent pas le problème, puisqu'elles réservent l'espace de leur membre le plus grand, ne gérant donc pas les plus petites tailles. Les conversions de type proposée par le C avec des " casts " aboutissent à des syntaxes très complexes, comportant beaucoup d'étoiles et de parenthèses. Nous proposons une solution plus élégante grâce à la définition de plusieurs types, qui produit cependant une surcharge de code :
typedef signed short KWAV_ECH16; /* 16 bits par echantillon */
typedef unsigned char KWAV_ECH8; /* 8 bits par echantillon */
typedef KWAV_ECH16 KWAV_ECH;
Nous définissons donc deux types, KWAV_ECH16 et KWAV_ECH8, car les autres résolutions sont beaucoup moins répandues, puis nous positionnons par défaut notre type standard KWAV_ECH sur KWAV_ECH16. Celui-ci occupe deux octets, ce qui correspond à un type signed short (nombre entier court signé) sur nos machines, et le KWAV_ECH8 occupe un seul octet, soit un type unsignedchar (caractère non signé). " Les variables de types unsigned char peuvent prendre les valeurs de 0 à 255, tandis que les signed char vont de -128 à 127 (sur une machine fonctionnant en complément à deux). " (KERNIGHAN et RITCHIE 1997, p.36). Notre carte son requièrt des short signés mais des char non signés.
Finalement, un signal sonore est représenté par une suite d'échantillons codés sur 16 bits par défaut, soit 2 octets, dans un tableau alloué dynamiquement, le champs data de la structure kwav_struct contenant l'adresse de ce tableau.
Pour illustrer l'utilisation de cette représentation, prenons un exemple: augmenter ou diminuer l'amplitude d'un son. Ce code est extrait de la fonction Normalize de la bibliothèque. Le traitement de signal audionumérique suivant s'effectue en parcourant simplement les échantillons un à un, et en modifiant leur valeur de façon systématique :
for(i=0; i<nb_ech; i++)
wavptr->data[i] *= coeff;
Cet algorithme fontionne avec des sons codés sur 16 bits, en incrémentant l'adresse du pointeur de 2 octets à chaque boucle. Il multiplie simplement la valeur qui se trouve à cette adresse par le coefficient. Pour que cet algorithme fonctionne aussi avec les sons codés sur 8 bits, il faut que l'adresse soit incrémentée de seulement 1 octet par boucle, en utilisant un pointeur sur des KWAV_ECH8. Il suffit de déclarer un pointeur de type KWAV_ECH8, de lui affecter l'adresse du début du son après conversion de type, et d'orienter l'exécution vers le bon algorithme, en fonction de l'information de résolution du son courant, contenue dans le champs header.bit_p_spl (nombre de bits par échantillon) :
KWAV_ECH8* ptr8;
ptr8 = (KWAV_ECH8*)wavptr->data;
for(i=0; i<nb_ech; i++)
{
if(wavptr->header.bit_p_spl==16)
wavptr->ptrson[i] *= coeff;
else
ptr8[i] *= coeff;
}
Ces structures de données permettent de représenter
informatiquement des flux audionumériques, et donc de faire de la
synthèse et du traitement du signal au sens donné par Curtis
ROADS : " La synthèse est le procédé de génération
de flux d'échantillons grâce à des outils algorithmiques.
[...] Le traitement du signal transforme les flux d'échantillons.
" (ROADS 1998, p.48). Les applications les plus typiques de ces domaines
sont variées : citons les synthèses additive, granulaire,
soustractive, formantique, par modulation, par tables d'ondes, par modèles
physiques, et différents traitements tels que l'amplification, le
mixage, les filtrage et égalisation, les retards, la projection
spaciale, la réduction de bruit, ou la conversion de taux d'échantillonnage.
Les traitements comme l'analyse / resynthèse ou la compression /
expansion temporelle seront facilités par les outils proposés
dans la bibliothèque kfft.
/* ouvre un fichier dsp en lecture et en ecriture */
if ((kwav_fdsp = open("/dev/dsp", O_RDWR))<0){
perror("Ouverture du fichier /dev/dsp");
return;}
La configuration de la carte son se fait par l'appel de fonctions ioctl, de " input/output control " en anglais, " contrôle des entrées / sorties " en français, qui proviennent de la bibliothèque spécifique à Linux sys/ioctl.h.
ioctl(kwav_fdsp, SNDCTL_DSP_STEREO, &en_stereo);
ioctl(kwav_fdsp, SNDCTL_DSP_SPEED, &freq_ech);
ioctl(kwav_fdsp, SNDCTL_DSP_SAMPLESIZE, &bits_par_ech);
ioctl(kwav_fdsp, SNDCTL_DSP_GETBLKSIZE, >aillebloc);
Ces fonctions permettent de contrôler des périphériques, plus exactement les paramètres sous-jacents des fichiers spéciaux. Elles ne bénéficient pas d'une standardisation unique, car elles ont été conçues comme un " fourre-tout " pour les opérations qui ne correspondent pas au modèle Unix des flux d'entrées/sorties, à partir de la version 7 d'Unix AT&T (IOCTL MAN PAGE, 1993). La syntaxe est la suivante :
int ioctl(int d, int request, ...)
Le premier argument d doit être un descripteur de fichier ouvert, et request un numéro de requête prédéfini, que l'on peut trouver dans le manuel ioctl_list. Le troisième argument est variable.
Voici le détail des requêtes que nous utilisons :
ioctl(kwav_fdsp, SNDCTL_DSP_SYNC, 0);
Celle-ci se distingue des précédentes, car elle ne paramètre pas le périphérique, ni ne récupère une valeur, mais valide le paramétrage et demande au périphérique de lire l'intégralité des données sans s'interrompre. Elle fonctionne comme un verrou en bloquant l'accès pendant les opérations read ou write sur le périphérique. Ce mécanisme a l'avantage de réduire les risques d'interruption de lecture ou d'écriture si désagréables pour le son, mais possède l'inconvénient de limiter à un seul le nombre de fichiers écoutables simultanément.
Des conseils d'utilisation de ces fonctions ioctl
et de programmation d'applications pour le son sur Unix sont disponibles
sur le site d'Opensound, aux adesses www.se.opensound.com/pguide/audio.html
et www.se.opensound.com/pguide/audio2.html.
Le format wav se distingue d'un fichier audio brut par l'ajout d'un en-tête qui contient des informations essentielles à la manipulation des échantillons. La bibliothèque kwav propose donc deux structures, kwav_struct et kwav_header, ainsi qu'un type KWAV_ECH qui définit la taille des échantillons :
typedef signed short KWAV_ECH16; /* 16 bits par echantillon */
typedef unsigned char KWAV_ECH8; /* 8 bits par echantillon */
typedef KWAV_ECH16 KWAV_ECH;
typedef struct {
kwav_header head;
KWAV_ECH *data;
} kwav_struct ;
La structure kwav_header représente un en-tête wav standard de 44 octets. Elle contient quelques marqueurs d'identification du format, la taille des données en octets, et les informations audionumériques suivantes : le mode, monophonique ou stéréophonique, la fréquence d'échantillonnage en hertz, et le taux d'échantillonnage en nombre de bits par échantillon. Sans ces informations, la lecture d'un son se ferait rarement correctement, et poserait entre autres des problèmes de vitesse de relecture.
typedef struct { /* header for WAV-Files */
char main_chunk[4]; /* 'RIFF' */
int length; /* length of file */
char chunk_type[4]; /* 'WAVE' */
char sub_chunk[4]; /* 'fmt' */
int length_chunk; /* length sub_chunk, always 16 bytes */
short format; /* always 1 = PCM-Code */
short modus; /* 1 = Mono, 2 = Stereo */
int sample_fq; /* Sample Freq */
int byte_p_sec; /* Data per sec */
short byte_p_spl; /* bytes per sample,
1=8 bit, 2=16 bit (mono)
2=8 bit, 4=16 bit (stereo) */
short bit_p_spl; /* bits per sample, 8, 12, 16 */
char data_chunk[4]; /* 'data' */
int data_length; /* length of data */
} kwav_header;
Les programmes utilisant kwav manipulent des structures kwav_struct plutôt que directement du son brut, afin que les fonctions connaissent toutes les caractéristiques du signal courant. Toutes ces informations sont contenues dans la structure kwav_header et sont vraiment indispensables. Typiquement, on a besoin de connaître le nombre d'échantillons d'un son, pour le parcourir du début à la fin :
nb_ech = (wavptr->head.data_length) / (wavptr->head.byte_p_spl);
for(i=0; i<nb_ech; i++)
wavptr->data[i] *= coeff;
Il existe quelques versions différentes du format wav, notamment avec des en-têtes plus importants. Pour l'instant, ceux-ci ne sont pas pris en compte par notre bibliothèque. Des informations complémentaires sur l'implémentation et l'utilisation du format wav sont disponibles aux adresses suivantes :
http://www.lightlink.com/tjweber/StripWav/WAVE.html
http://www.geocities.com/~vmushinskiy/fformats/files/wave.htm
La fonction kwav_Save gère l'écriture d'une structure kwav_struct dans un fichier au format wav, tandis que la fonction kwav_Load lit ce type de fichier sur le disque dur pour remplir une telle structure en mémoire vive. La bibliothèque kwav manipule localement un seul descripteur de fichier pour ces opérations, nommé fdwav. Pour kwav_Save comme pour kwav_Load, ce descripteur est ouvert puis refermé dans la même fonction. Ces deux fonctions utilisent open et close plutôt que fopen et fclose, afin que les permissions ne dépendent pas de l'environnement.
Voici le code complet de la fonction kwav_Save :
void kwav_Save (kwav_struct sonwav, char nom_fichier[])
{
int i;
int fdwav;
char wav[] = ".wav";
/* recherche de l'extension, sinon ajout */
for(i=0; i<4; i++)
if(nom_fichier[strlen(nom_fichier)-4+i] != wav[i])
strcat(nom_fichier, wav);
/* ecrit un fichier son.wav sur le disque et le relit */
if((fdwav = open(nom_fichier, O_WRONLY | O_CREAT, S_IWRITE | S_IREAD)) == -1){
perror("kwav: Ouverture du fichier wav");
return;}
if(gverbose == _KWAV_OUI){
printf("ecriture du fichier \"%s\"... \n", nom_fichier);
printf(" header...");}
write(fdwav, &sonwav.head, sizeof(sonwav.head));
if(gverbose == _KWAV_OUI)
printf(" data...\n");
write(fdwav, sonwav.data, sonwav.head.data_length);
if(gverbose == _KWAV_OUI)
printf("->fin d'ecriture du fichier \"%s\"\n", nom_fichier);
if((close(fdwav)) == -1){
perror("kwav: Fermeture du fichier wav");
return;}
}
On peut résumer cette fonction à quatre instructions :
fdwav = open(nom_fichier, O_WRONLY | O_CREAT, S_IWRITE | S_IREAD);
write(fdwav, &sonwav.header, sizeof(sonwav.header));
write(fdwav, sonwav.ptrson, sonwav.header.data_length);
close(fdwav);
La première ouvre le fichier demandé en écriture s'il existe, le créé sinon, avec les permissions adéquates. La constante S_IRUSR (ou S_IREAD) donne le droit de lecture du fichier à l'utilisateur, et S_IWUSR (ou S_IWRITE) celui d'écriture. La deuxième fonction écrit dans ce fichier le contenu de l'en-tête, et la troisième le son proprement dit, octets par octets. La dernière, enfin, ferme le descripteur de ce fichier.
La lecture d'un fichier wav repose sur un modèle presque identique à l'écriture. La fonction kwav_Load ouvre le fichier demandé en lecture, lit l'en-tête champs par champs, lit le son, puis referme le fichier :
void kwav_Load (kwav_struct* psonwav, char nom_fichier[])
{
int fdwav;
/* ouverture du fichier */
if((fdwav = open(nom_fichier, O_RDONLY)) == -1){
perror("kwav: Ouverture du fichier en lecture");
return;}
if(gverbose == _KWAV_OUI){
printf("lecture du fichier \"%s\": \n", nom_fichier);
printf(" header...\n");}
read (fdwav, &psonwav->head.main_chunk, 4*sizeof(char) ); /* 'RIFF' */
read (fdwav, &psonwav->head.length, sizeof(int) ); /* length of file */
read (fdwav, &psonwav->head.chunk_type, 4*sizeof(char) ); /* 'WAVE' */
read (fdwav, &psonwav->head.sub_chunk, 4*sizeof(char) ); /* 'fmt' */
read (fdwav, &psonwav->head.length_chunk, sizeof(int) ); /* 16 bytes */
read (fdwav, &psonwav->head.format, sizeof(short)); /* 1=PCM-Code */
read (fdwav, &psonwav->head.modus, sizeof(short)); /* 1=Mono, 2=St. */
read (fdwav, &psonwav->head.sample_fq, sizeof(int) ); /* Sample Freq */
read (fdwav, &psonwav->head.byte_p_sec, sizeof(int) ); /* Data per sec */
read (fdwav, &psonwav->head.byte_p_spl, sizeof(short)); /* 1, 2, 3, 4 */
read (fdwav, &psonwav->head.bit_p_spl, sizeof(short)); /* 8, 12, 16 */
read (fdwav, &psonwav->head.data_chunk, 4*sizeof(char) ); /* 'data' */
read (fdwav, &psonwav->head.data_length, sizeof(int) ); /* length of data */
if(gverbose == _KWAV_OUI){
kwav_Print_header (psonwav->head);
printf(" data...\n");}
psonwav->data = kwav_Alloc (psonwav->data, psonwav->head.data_length);
read (fdwav, psonwav->data, psonwav->head.data_length); /* chargement du son lui-meme */
if(gverbose == _KWAV_OUI)
printf("->fin lecture \"%s\"\n", nom_fichier);
if((close(fdwav)) == -1){
perror("kwav: Fermeture du fichier wav");
return;}
}
Il y a cependant quelques différences. La
première tient bien sûr dans l'utilisation de la fonction
read
à la place de write.
La deuxième tient dans l'appel de la fonction kwav_Alloc,
car la structure kwav_struct
manipulée contient un pointeur KWAV_ECH*
data qui n'est pas censé avoir déjà
été alloué. En conséquence, si la même
structure est utilisée plusieurs fois, il est préférable
de libérer ce pointeur avant d'appeler kwav_Load.
La troisième différence concerne les arguments de kwav_Load
: le passage de la structure kwav_struct
se
fait par adresse. " Comme le langage C passe les arguments des fonctions
par valeur, la fonction appelée n'a aucun moyen direct de modifier
une variable de la fonction appelante. [...] Les arguments de type pointeur
permettent à une fonction d'accéder aux objets de la fonction
appelante et de les modifier. " (KERNIGHAN et RITCHIE 1997, p. 93) Ainsi,
les opérations ont bien lieu sur la strucure kwav_struct
d'origine,
et non pas sur sa copie, afin d'allouer puis d'écrire les informations
et le son directement à partir de son adresse en mémoire
vive.
La taille des fichiers audio peut varier beaucoup, et être très importante si le fichier dure plusieurs minutes, surtout au format wav non compressé, tel que nous l'utilisons. C'est pourquoi les structures kwav_struct contiennent un pointeur vers une zone mémoire que l'on peut allouer et désallouer :
KWAV_ECH *kwav_Alloc (KWAV_ECH* pson, int nboctets)
{
if(gverbose == _KWAV_OUI)
printf("allouer... \n");
pson = malloc (nboctets); /* allocation en octets */
if (pson==NULL){
perror("kwav: Allocation du pointeur");
return pson;}
if(gverbose == _KWAV_OUI)
printf("->fin allouer\n");
return pson;
}
void kwav_Free (KWAV_ECH* pson)
{
free(pson);
}
Ces fonctions utilisent malloc
et free de la
bibliothèque standard stdlib.h.
La fonction kwav_Alloc
renvoie l'adresse du début de la zone mémoire allouée
afin qu'elle soit affectée au pointeur, conformément à
l'écriture d'un malloc.
Elle vérifie s'il y a une erreur en testant si la valeur retournée
par malloc vaut
NULL,
ce que ne peut pas faire la fonction kwav_Free
puisque free
ne retourne pas de valeur.
N'importe quel programme utilisant kwav et transmettant ces arguments peut recevoir l'option d'aide -h ou --help, qui liste alors toutes les options possibles :
alpha5:~/temp/PACK_LIB# ./jouerwav -h
Option(s) demandee(s): -h
<fichier.wav>
-b <resolution en bits par echantillon>, 8 ou 16
-d <duree en millisecondes>
-f <frequence d'echantillonnage>, 11025 ou 22050 ou 44100
-n <nom du fichier>
-s <stereophonie>, 1 ou 2
-v ou --verbose
-h ou --help
alpha5:~/temp/PACK_LIB#
On discerne bien ici les trois types d'utilisations : le passage de nom de fichier, le paramètrage, et l'aide, qui peut se faire avec ou sans l'option -n. Le paramètrage positionne les variables d'environnement suivantes, telles qu'elles sont décrites dans le fichier d'en-tête kwavlib.h :
int gstereo; /* stereo globale */
int gfreq; /* frequence globale */
int gbits; /* resolution globale */
int gtaille; /* taille globale en octet */
char gnom_fichier[256]; /* nom global */
int gverbose; /* drapeau global pour les printf */
Si la ligne de commande ne précise pas certaines options, la fonction kwav_Init positionne ces variables aux valeurs par défaut décrites dans le même fichier :
#define _KWAV_DEFAULT_STEREO 1
#define _KWAV_DEFAULT_FREQ_ECH 22050
#define _KWAV_DEFAULT_BITS (8 * sizeof(KWAV_ECH))
#define _KWAV_DEFAULT_TAILLE (_KWAV_DEFAULT_FREQ_ECH * sizeof(KWAV_ECH) * _KWAV_DEFAULT_STEREO) /* 1 sec de son en oct */
#define _KWAV_DEFAULT_FILE_NAME "son.wav"
Voici une illustration typique de l'utilisation de la ligne de commande. Cette ligne permet de positionner des variables d'environnement à d'autres valeurs que les valeurs par défaut, simplement :
alpha6:~/temp/PACK_LIB# ./enregistrerwav -b 8 -f 44100 -s 2
Option(s) demandee(s): -b 8 -f 44100 -s 2
Nom du fichier a enregistrer : options
duree en millisecondes : 2000
alpha6:~/temp/PACK_LIB# ls -l options.wav
-rw------- 1 root root 176444 Aug 10 00:55 options.wav
L'option -v ou --verbose fait partie des options de paramètrage en tant qu'interrupteur, puisqu'elle positionne la variable d'environnement gverbose à 1. Elle signifie " verbeux ", pour suggérer que le programme va afficher des commentaires sur la sortie standard pendant son exécution. Cependant, elle s'utilise différemment des options -b, -f, -s, ou -d, puisqu'elle ne prend pas de valeur, et qu'elle possède deux noms. Son utilisation sur l'exemple précédent montre que la ligne de commande opère une substitution des valeurs par défaut, qui sont : mono 22050 Hz, 16 bits.
alpha6:~/temp/PACK_LIB# ./enregistrerwav -b 8 -f 44100 -s 2 -v
Option(s) demandee(s): -b 8 -f 44100 -s 2 -v
Valeurs par defaut :
stereo = 2 voie
freq = 44100 Hertz
bits = 8 bits
duree = 500 ms
nom = son.wav
Init_dsp :
stereophonie = 2 voie(s)
frequence d'echantillonnage = 44188 hertz
taille d'echantillon = 8 bits
taille de bloc = 4096 octets
Nom du fichier a enregistrer : options2
duree en millisecondes : 1000
allouer...
->fin allouer
enregistrer...
->fin enregistrer
Reconfig_dsp :
stereophonie = 2 voie(s)
frequence d'echantillonnage = 44188 Hertz
taille d'echantillon = 8 bits
jouer...
->fin jouer
ecriture du fichier "options2.wav"...
header... data...
->fin d'ecriture du fichier "options2.wav"
alpha6:~/temp/PACK_LIB#
Ces mécanismes de paramétrage sont décrits dans la fonction void kwav_Init (int argc, char *argv[]), qui récupère dans ces arguments la ligne de commande complète. " Quand on appelle main, deux arguments lui sont passés. Le premier (baptisé conventionnellement argc signifiant nombre d'arguments - argument count) représente le nombre d'arguments de la ligne de commande qui a appelé le programme ; le second (argv, signifiant vecteur d'arguments - argument vector) est un pointeur sur un tableau de chaînes de caractères qui contiennent les arguments, à raison de un par chaîne. [...] Par convention, argv[0] est le nom par lequel le programme a été appelé, argc vaut au moins 1." (KERNIGHAN et RITCHIE 1997, p. 112). La fonction kwav_Init positionne d'abord les variables d'environnement aux valeurs par défaut, puis, si la ligne de commande contient autre chose que le nom du programme appelé, teste si elle reconnaît une option parmi les chaînes de caractères contenues dans argv pour changer la valeur de la variable qui lui correspond. Elle appelle enfin la fonction kwav_Init_dsp avec certaines de ces valeurs pour configurer le périphérique de la carte son, et rend la main à la fonction appelante.
void kwav_Init (int argc, char *argv[])
{
int i;
char wav[] = ".wav";
char ext[4];
/* Parametrage par defaut */
kwav_set_gfreq (_KWAV_DEFAULT_FREQ_ECH);
kwav_set_gbits (8 * sizeof(KWAV_ECH));
kwav_set_gstereo (_KWAV_DEFAULT_STEREO);
kwav_set_gsize (_KWAV_DEFAULT_TAILLE);
kwav_set_gverbose (KWAV_FALSE);
kwav_set_gfilename (_KWAV_DEFAULT_FILE_NAME);
/* Verification des options */
if(argc > 1 && argv[1] != '\0')
{
for(i=1; i<argc; i++)
if(strcmp(argv[i], "-v")==0 || strcmp(argv[i], "--verbose")==0)
kwav_set_gverbose (KWAV_TRUE);
if(gverbose == KWAV_TRUE)
printf("\tOption(s) demandee(s): ");
for(i=1; i<argc; i++)
{
if(gverbose == KWAV_TRUE)
printf("%s ", argv[i]); /* simple echo */
if(strcmp(argv[i], "-s")==0)
kwav_set_gstereo (atoi(argv[i+1])); /* m=1 et s=2 */
if(strcmp(argv[i], "-f")==0)
kwav_set_gfreq (atoi(argv[i+1])); /* en hertz */
if(strcmp(argv[i], "-b")==0){
kwav_set_gbits (atoi(argv[i+1]));
if (gbits != 8) gbits = 16;} /* seulement 8 ou 16 */
if(strcmp(argv[i], "-d")==0)
kwav_set_gsize (kwav_ms2oct(atoi(argv[i+1]))); /* en ms */
if(strcmp(argv[i], "-n")==0)
kwav_set_gfilename (argv[i+1]);
if(strcmp(argv[i], "-h")==0 || strcmp(argv[i],"--help")==0)
{
printf("\n"
"<fichier.wav>\n"
"-b <resolution en bits par echantillon>, 8 ou 16\n"
"-d <duree en millisecondes>\n"
"-f <frequence d'echantillonnage>, 11025 ou 22050 ou 44100\n"
"-n <nom du fichier>\n"
"-s <stereophonie>, 1 ou 2\n"
"-v ou --verbose\n"
"-h ou --help\n");
exit(0);
}
/* recherche de l'extension */
strcpy(ext, argv[i]+strlen(argv[i])-4);
if(strcmp(ext, wav)==0)
strcpy(gnom_fichier, argv[i]);
}
if(gverbose == KWAV_TRUE)
printf("\n");
}
if(gverbose == KWAV_TRUE) {
printf("\tValeurs par defaut :\n");
printf("stereo = %d voie\n", gstereo);
printf("freq = %d Hertz\n", gfreq);
printf("bits = %d bits\n", gbits);
printf("duree = %d ms\n", kwav_oct2ms(gtaille));
printf("nom = %s\n", gnom_fichier);
}
/* Parametrage du peripherique */
kwav_Init_dsp(gfreq, gbits, gstereo);
}
Afin de désactiver ce système de gestion de la ligne de commande, pour créer son propre système par exemple, il suffit de ne pas transmettre ces arguments à la fonction kwav_Init, en l'appelant de la façon suivante :
kwav_Init(0, NULL);
Cette bibliothèque propose des fonctions d'analyse et de resynthèse du son basées sur un algorithme de Transformée Rapide de Fourier, TFR ou FFT (Fast Fourier Transform) en anglais, à partir d'un code source proposé par Daniel GOOSSENS dans son cours sur l'intelligence artificielle (GOOSSENS 1999). Nous ne détaillerons donc pas ici le fonctionnement du programme, déjà largement décrit dans le document précité.
kfft est une bibliothèque C compatible C++. Elle permet d'analyser et de resynthétiser du signal audionumérique. Elle nécessite la bibliothèque kwav pour fonctionner, et comprend cinq fichiers :
" Le musicien créatif ne sera-t-il pas un maître plus puissant s'il est également informé de la science pure des méthodes et des matériaux de son art ? Ne sera-t-il pas capable de mélanger les couleurs sonores avec une plus grande habileté s'il comprend la nature des ingrédients et des effets qu'ils produisent ? " (MILLER 1916).
L'analyse spectrale constitue un outil essentiel pour les acousticiens, psychoacousticiens, musicologues, et compositeurs, en révélant la microstructure des sons. C'est toujours un domaine en évolution, car elle contient des problèmes intrinsèques irrésolus à ce jour, au point que Curtis ROADS parle " d'estimation spectrale " (ROADS 1998). Beaucoup de méthodes ont été mises au point, dont les analyses par banques de filtres à Q constant, par ondelettes, par distribution de Wigner, ou par autorégression. L'analyse de Fourier est elle-même une famille de techniques différentes qui continuent d'évoluer.
Les méthodes basées sur l'analyse de
Fourier décomposent le son fourni en entrée sous forme d'une
somme de sinusoïdes également espacées entre 0 Hertz
et la fréquence d'échantillonnage. Comme nous traitons de
sons numériques, notre méthode isole une courte portion de
son ou fenêtre, en général de 512 ou 1024 échantillons.
La FFT est une simple accélération de l'algorithme de la
transformée discrète de Fourier ou DFT, de Discrete Fourier
Transform en anglais (REINHARD 1997). Elle demande que la taille de
la fenêtre soit une puissance de 2.
Le nombre de fonctions est très limité pour l'utilisateur :
En informatique, la visualisation et autres GUI (Graphic
User Interface) se sont imposés ces dernières années.
En musique, les représentations du son amplitude / temps et fréquence
/ temps ont changé certaines habitudes de pensée, sortant
de la traditionnelle partition. La manipulation informatique des sons gagne
une précision inégalée grâce à la visualisation
de ceux-ci. Nous proposons deux algorithmes de visualisation, écrits
à l'aide de la bibliothèque graphique X11 (MEHAT, 1991),
kwav,
et kfft.
Voici le résultat graphique d'un algorithme basé sur un dessin par lignes. Celui-ci reste le plus répandu pour les représentations amplitude / temps, c'est pourquoi nous l'utilisons ici, bien qu'il soit trompeur. En effet, à cette échelle temporelle d'environ une seconde, il y a beaucoup plus d'échantillons que de points en largeur d'écran.
Dans cet extrait du programme kediteur.c, le cas des sons codés sur 8 bits est traité en sus du cas normal de ceux codés sur 16 bits (cf. Gestion du flux audionumérique). En revanche, le cas des sons stéréophoniques n'est pas traité, car cela demanderait deux fenêtres au lieu d'une. On suppose que le fichier wav est déjà chargé, ainsi que la bibliothèque kwav.
void Dessine_wav(Window win, kwav_struct sonwav)
{
int i;
int nb_ech;
int zero;
double pas;
unsigned int h;
unsigned int l;
double zoomy;
KWAV_ECH8* ptr8;
h = getHeight(win);
l = getWidth(win);
nb_ech = (sonwav.head.data_length) / (sonwav.head.byte_p_spl);
pas = nb_ech / (double)l;
zoomy = h / pow(2, sonwav.head.bit_p_spl);
ptr8 = (KWAV_ECH8*)sonwav.data;
XSetForeground(dpy, gc, rouge); /* horizontale */
XDrawLine(dpy, win, gc, 0, h/2, l, h/2);
XSetForeground(dpy, gc, jaune); /* signal */
if(sonwav.head.bit_p_spl != 8)
{
zero = (h/2);
for(i=0; (i+1)*pas<nb_ech; i++)
XDrawLine(dpy, win, gc,
i, sonwav.data[(int)(i*pas)] * zoomy + zero,
i+1, sonwav.data[(int)((i+1)*pas)] * zoomy + zero);
}
else
{
zero = 0;
for(i=0; (i+1)*pas<nb_ech; i++)
XDrawLine(dpy, win, gc,
i, ptr8[(int)(i*pas)] * zoomy + zero,
i+1, ptr8[(int)((i+1)*pas)] * zoomy + zero);
}
XFlush(dpy);
}
Cet algorithme commence par récupérer la taille de la fenêtre dans laquelle il doit dessiner, pour en déduire le pas qu'il devra faire dans le son pour aller d'un échantillon au suivant. Cette variable est de type double pour pouvoir afficher des sons qui seraient plus courts que la largeur de la fenêtre, une sorte d'échelle microscopique. Le zoomy est nivelé par la résolution maximum du son.
Après avoir tracé une ligne horizontale en rouge, le programme commence à tracer les lignes jaunes qui représentent le signal lui-même, lorsqu'il n'est pas codé sur 8 bits. La boucle trace des lignes qui relient le pixel courant à son voisin horizontal de droite, dont les ordonnées sont relevées dans le son à l'échantillon correspondant, puis nivelées par le zoomy et le zero. Cette boucle s'arrête dès que le numéro de l'hypothétique échantillon qui correspond au pixel suivant dépasse le nombre d'échantillons de ce son.
L'algorithme des sons 8 bits est identique, à
ceci près que zero
vaut 0 au lieu
de h/2, à
cause du caractère non signé du type KWAV_ECH8
qui vaut un unsigned char
au lieu d'un signed short
(cf. Gestion du flux audionumérique).
La fonction suivante trace un sonogramme du son passé en argument. La fonction kfft_Init est supposée avoir déjà été appelée, et les bibliothèques kwav et kfft inclues. Cet algorithme est donc basé sur la transformée de Fourier (cf. La bibliothèque kfft).
void Dessine_sono(Window win, kwav_struct sonwav)
{
int i, j, amp, inc, nbfreqs;
unsigned int h;
unsigned int l;
h = getHeight(win);
l = getWidth (win);
nbfreqs = kfft_get_nbfreqs();
XClearWindow (dpy, win);
inc = selwav.head.data_length / sonwav.head.byte_p_spl / l;
for(i=0; i<l; i++)
{
kfft_Analyse(sonwav.data + i*inc);
for(j=0; j<h; j++)
{
amp = kfft_modules_reels[j*nbfreqs/h/2];
XSetForeground(dpy, gc, conversion((amp>512)?255:0, (amp>256)?255:amp, 0));
XDrawPoint(dpy, win, gc, i, h-j);
XFlush(dpy);
}
}
}
Comme le programme précédent, celui-ci récupère les dimensions de la fenêtre graphique, et calcule un incrément inc pour la correspondance entre le nombre de pixels en abscisse et le nombre d'échantillons. Il récupère aussi le nombre de fréquences qui seront renvoyées par l'analyse, pour la correspondance en ordonnée. La boucle principale se déplace de gauche à droite, pixel par pixel. Pour chacune de ces tranches verticales d'un pixel d'épaisseur, elle lance une analyse du son à partir de l'échantillon qui correspond au pixel courant :
kfft_Analyse(sonwav.data + i*inc);
La boucle secondaire balaye les ordonnées de ces tranches verticales de bas en haut. L'énergie amp de la bande de fréquences qui correspond à l'ordonnée courante est récupérée, sachant que seule la première moitié du tableau des amplitudes est pertinente :
amp = kfft_modules_reels[j*nbfreqs/h/2];
Cette énergie est traduite en couleurs du jaune au vert, grâce à la fonction de conversion du codage en rouge, vert, bleu en code couleur, puis affichée :
XSetForeground(dpy, gc, conversion((amp>512)?255:0, (amp>256)?255:amp, 0));
XDrawPoint(dpy, win, gc, i, h-j);
D'abord, il faut ne pas oublier que l'on développe
pour d'autres programmeurs, d'où la nécessité d'une
pensée rigueureuse et cohérente (cf. Ergonomie générale).
Pour les bibliothèques partagées, le nom doit commencer par lib pour library, même si ce préfixe disparaît lorsqu'on appelle une bibliothèque, et contenir l'extension .so pour " shared object ", qui disparaît aussi :
libkwav.so
gcc mon_programme.c -L/usr/local/ -lkwav
Ceci n'est pas vrai pour les archives, qui ne prennent pas de préfixe, et dont l'extension .a pour archive reste lors de l'appel à la compilation :
kwav.a
gcc mon_programme.c kwav.a
Pour générer une bibliothèque partagée, le compilateur gcc prend l'option -shared " partagé " en français, et l'option -o suivie du nom du fichier ainsi compilé :
gcc -shared -Wall -lm libkwav3.c -o libkwav.so
Les options -Wall et -lm concernent respectivement le signalement d'avertissements supplémentaires, et l'appel de la bibliothèque contenant les fonctions mathématiques standards.
La génération d'une archive s'effectue en deux temps. Le premier compile le fichier source libkwav.c en un fichier objet libkwav.o, à l'aide de l'option -c de gcc :
gcc -c libkwav.c
Le deuxième créé l'archive à partir de ce fichier objet :
ar -cr kwav.a libkwav.o
Le programme ar
permet de créer, modifier, et extraire des archives. L'option -c
créé une archive, et -d
insère les fichiers dans l'archive, en les remplaçant s'ils
existent déjà.
IOCTL_LIST(2) Linux Programmer's Manual IOCTL_LIST(2)
// <include/linux/soundcard.h>
0x00005100 SNDCTL_SEQ_RESET void
0x00005101 SNDCTL_SEQ_SYNC void
0xC08C5102 SNDCTL_SYNTH_INFO struct synth_info * // I-O
0xC0045103 SNDCTL_SEQ_CTRLRATE int * // I-O
0x80045104 SNDCTL_SEQ_GETOUTCOUNT int *
0x80045105 SNDCTL_SEQ_GETINCOUNT int *
0x40045106 SNDCTL_SEQ_PERCMODE void
0x40285107 SNDCTL_FM_LOAD_INSTR const struct sbi_instrument *
0x40045108 SNDCTL_SEQ_TESTMIDI const int *
0x40045109 SNDCTL_SEQ_RESETSAMPLES const int *
0x8004510A SNDCTL_SEQ_NRSYNTHS int *
0x8004510B SNDCTL_SEQ_NRMIDIS int *
0xC074510C SNDCTL_MIDI_INFO struct midi_info * // I-O
0x4004510D SNDCTL_SEQ_THRESHOLD const int *
0xC004510E SNDCTL_SYNTH_MEMAVL int * // I-O
0x4004510F SNDCTL_FM_4OP_ENABLE const int *
0xCFB85110 SNDCTL_PMGR_ACCESS struct patmgr_info * // I-O
0x00005111 SNDCTL_SEQ_PANIC void
0x40085112 SNDCTL_SEQ_OUTOFBAND const struct seq_event_rec *
0xC0045401 SNDCTL_TMR_TIMEBASE int * // I-O
0x00005402 SNDCTL_TMR_START void
0x00005403 SNDCTL_TMR_STOP void
0x00005404 SNDCTL_TMR_CONTINUE void
0xC0045405 SNDCTL_TMR_TEMPO int * // I-O
0xC0045406 SNDCTL_TMR_SOURCE int * // I-O
0x40045407 SNDCTL_TMR_METRONOME const int *
0x40045408 SNDCTL_TMR_SELECT int * // I-O
0xCFB85001 SNDCTL_PMGR_IFACE struct patmgr_info * // I-O
0xC0046D00 SNDCTL_MIDI_PRETIME int * // I-O
0xC0046D01 SNDCTL_MIDI_MPUMODE const int *
0xC0216D02 SNDCTL_MIDI_MPUCMD struct mpu_command_rec * // I-O
0x00005000 SNDCTL_DSP_RESET void
0x00005001 SNDCTL_DSP_SYNC void
0xC0045002 SNDCTL_DSP_SPEED int * // I-O
0xC0045003 SNDCTL_DSP_STEREO int * // I-O
0xC0045004 SNDCTL_DSP_GETBLKSIZE int * // I-O
0xC0045006 SOUND_PCM_WRITE_CHANNELS int * // I-O
0xC0045007 SOUND_PCM_WRITE_FILTER int * // I-O
0x00005008 SNDCTL_DSP_POST void
0xC0045009 SNDCTL_DSP_SUBDIVIDE int * // I-O
0xC004500A SNDCTL_DSP_SETFRAGMENT int * // I-O
0x8004500B SNDCTL_DSP_GETFMTS int *
0xC0045005 SNDCTL_DSP_SETFMT int * // I-O
0x800C500C SNDCTL_DSP_GETOSPACE struct audio_buf_info *
0x800C500D SNDCTL_DSP_GETISPACE struct audio_buf_info *
0x0000500E SNDCTL_DSP_NONBLOCK void
0x80045002 SOUND_PCM_READ_RATE int *
0x80045006 SOUND_PCM_READ_CHANNELS int *
0x80045005 SOUND_PCM_READ_BITS int *
0x80045007 SOUND_PCM_READ_FILTER int *
0x00004300 SNDCTL_COPR_RESET void
0xCFB04301 SNDCTL_COPR_LOAD const struct copr_buffer *
0xC0144302 SNDCTL_COPR_RDATA struct copr_debug_buf * // I-O
0xC0144303 SNDCTL_COPR_RCODE struct copr_debug_buf * // I-O
0x40144304 SNDCTL_COPR_WDATA const struct copr_debug_buf *
0x40144305 SNDCTL_COPR_WCODE const struct copr_debug_buf *
0xC0144306 SNDCTL_COPR_RUN struct copr_debug_buf * // I-O
0xC0144307 SNDCTL_COPR_HALT struct copr_debug_buf * // I-O
0x4FA44308 SNDCTL_COPR_SENDMSG const struct copr_msg *
0x8FA44309 SNDCTL_COPR_RCVMSG struct copr_msg *
0x80044D00 SOUND_MIXER_READ_VOLUME int *
0x80044D01 SOUND_MIXER_READ_BASS int *
0x80044D02 SOUND_MIXER_READ_TREBLE int *
0x80044D03 SOUND_MIXER_READ_SYNTH int *
0x80044D04 SOUND_MIXER_READ_PCM int *
0x80044D05 SOUND_MIXER_READ_SPEAKER int *
0x80044D06 SOUND_MIXER_READ_LINE int *
0x80044D07 SOUND_MIXER_READ_MIC int *
0x80044D08 SOUND_MIXER_READ_CD int *
0x80044D09 SOUND_MIXER_READ_IMIX int *
0x80044D0A SOUND_MIXER_READ_ALTPCM int *
0x80044D0B SOUND_MIXER_READ_RECLEV int *
0x80044D0C SOUND_MIXER_READ_IGAIN int *
0x80044D0D SOUND_MIXER_READ_OGAIN int *
0x80044D0E SOUND_MIXER_READ_LINE1 int *
0x80044D0F SOUND_MIXER_READ_LINE2 int *
0x80044D10 SOUND_MIXER_READ_LINE3 int *
0x80044D1C SOUND_MIXER_READ_MUTE int *
0x80044D1D SOUND_MIXER_READ_ENHANCE int *
0x80044D1E SOUND_MIXER_READ_LOUD int *
0x80044DFF SOUND_MIXER_READ_RECSRC int *
0x80044DFE SOUND_MIXER_READ_DEVMASK int *
0x80044DFD SOUND_MIXER_READ_RECMASK int *
0x80044DFB SOUND_MIXER_READ_STEREODEVS int *
0x80044DFC SOUND_MIXER_READ_CAPS int *
0xC0044D00 SOUND_MIXER_WRITE_VOLUME int * // I-O
0xC0044D01 SOUND_MIXER_WRITE_BASS int * // I-O
0xC0044D02 SOUND_MIXER_WRITE_TREBLE int * // I-O
0xC0044D03 SOUND_MIXER_WRITE_SYNTH int * // I-O
0xC0044D04 SOUND_MIXER_WRITE_PCM int * // I-O
0xC0044D05 SOUND_MIXER_WRITE_SPEAKER int * // I-O
0xC0044D06 SOUND_MIXER_WRITE_LINE int * // I-O
0xC0044D07 SOUND_MIXER_WRITE_MIC int * // I-O
0xC0044D08 SOUND_MIXER_WRITE_CD int * // I-O
0xC0044D09 SOUND_MIXER_WRITE_IMIX int * // I-O
0xC0044D0A SOUND_MIXER_WRITE_ALTPCM int * // I-O
0xC0044D0B SOUND_MIXER_WRITE_RECLEV int * // I-O
0xC0044D0C SOUND_MIXER_WRITE_IGAIN int * // I-O
0xC0044D0D SOUND_MIXER_WRITE_OGAIN int * // I-O
0xC0044D0E SOUND_MIXER_WRITE_LINE1 int * // I-O
0xC0044D0F SOUND_MIXER_WRITE_LINE2 int * // I-O
0xC0044D10 SOUND_MIXER_WRITE_LINE3 int * // I-O
0xC0044D1C SOUND_MIXER_WRITE_MUTE int * // I-O
0xC0044D1D SOUND_MIXER_WRITE_ENHANCE int * // I-O
0xC0044D1E SOUND_MIXER_WRITE_LOUD int * // I-O
0xC0044DFF SOUND_MIXER_WRITE_RECSRC
int * // I-O
Dès les débuts de l'informatique, la musique a cherché à mettre l'ordinateur à son service, notamment avec des programmes de synthèse sonore (POPE 1993). Certains ordinateurs ont ensuite marqué l'informatique musicale pour le grand public, comme l'Atari, ou les Macintoshs, qui possèdent des API (Application Programming Interface) de haut niveau, qui facilitent le développement d'applications audionumériques. Les systèmes Unix sont capables de gèrer du son, mais il faut utiliser des fonctions de plutôt bas niveau comme les ioctl. Les deux bibliothèques développées pour ce travail de maîtrise proposent des fonctions d'un niveau d'abstraction supérieur, de manière à simplifier la programmation de logiciels audionumériques sur ce système d'exploitation, que nous utilisons à l'université.
Ce mémoire présente le travail d'une année, et porte sur la gestion du son sous Linux. Il contient la bibliothèque kwav, qui gère le son d'une manière robuste en ne manipulant qu'un son à la fois, grâce à un mécanisme de verrouillage, et en utilisant le format wav. La bibliothèque kfft est un complément de kwav, et permet d'analyser un son en fréquence, de le resynthétiser, et d'avoir accès à la taille de la fenêtre d'analyse. L'application kediteur permet de lire, d'enregistrer, et de visualiser du son, et démontre donc les fonctionnalités des deux bibliothèques. Ce projet comprend enfin un manuel en ligne, et quelques programmes simples à vocation pédagogique.
Ce travail pourrait être complété par une démonstration des fonctions de resynthèse et de la manipulation des fichiers stéréophoniques, et par un perfectionnement de l'éditeur, comme la saisie des chaînes de caractères directement dans l'interface graphique ; ce serait utile pour les noms de fichiers, et le reparamètrage de l'environnement audio de kwav. Pour que l'internationalisation du projet soit totale, il faudrait traduire les commentaires, les noms de certaines variables, les messages d'erreurs, ainsi que le manuel en anglais. D'ailleurs, les outils GNU permettent de traduire automatiquement beaucoup de messages de l'anglais vers la langue souhaitée, grâce à la fonction gettext et à la variable d'environnement LANG.
Malgré ces possibilités de développement
de ce projet, il s'agit d'un outil fonctionnel et prometteur. Des améliorations
par d'autres programmeurs sont envisageables, car le code source est disponible
sur le réseau internet depuis le serveur de l'université,
à l'adresse http://inferno.cs.univ-paris8.fr/~barkati. Ces programmes
pourraient constituer le point de départ d'une bibliothèque
de plus grande portée, l'écriture essayant de répondre
aux critères répandus au sein de la communauté informatique
(GOURDIN 1991). Nous souhaitons que la diffusion de tels travaux favorise
le développement des applications sonores, à l'université
et peut-être ailleurs.
Références bibliographiques
Ci-joint une impression du manuel en ligne de la bibliothèque kwav (taper man kwav sous shell), ainsi que le code source des programmes suivants :