Paru dans Linux Mag 70, mars 2005
Durant les prochains mois, nous allons découvrir une facette de SDL encore inconnue pour nous: la gestion de l'audio. L'article de ce mois-ci posera les bases qui nous permettront d'aller plus loin. Commençons donc par comprendre comment nos machines produisent de si jolis sons...
L'élément de base du son numérique est l'échantillon. Il correspond à une prise de mesure de la pression sur l'air générée par la source sonore à un instant donné. Cette dépression ou surpression sera restituée lors de la lecture par une vibration de la membrane des enceintes. L'échantillon est au son ce que le pixel est à l'image: comme pour un pixel, un échantillon possède un format, et comme pour un pixel celui-ci conditionne le nombre de nuances qu'il est possible d'obtenir. SDL gère les formats sonores 8 et 16 bits, les formats supérieurs étant réservés aux cartes sonores haut de gamme. Pour plus d'informations sur le son numérique: http://www.commentcamarche.net/audio/son.php3
Tout comme une image est le résultat de l'assemblage de nombreux pixels, un son se compose d'un certain nombre d'échantillons qui sont joués à une certaine fréquence. Cette fréquence s'exprime en hertz, une fréquence de 11025 Hz correspondant à 11025 échantillons joués en une seconde. La fréquence sonore est en quelque sorte l'équivalent de la résolution d'une image: plus une image comporte de pixels, plus elle est nette. De la même manière, une fréquence sonore importante garantit la pureté du son. Dans les deux cas, une résolution ou une fréquence plus élevée est également synonyme de consommation mémoire plus importante pour le stockage des médias.
Un autre paramètre est à prendre en compte: le nombre de canaux, autrement dit le nombre d'échantillons pris en même temps à partir de sources différentes. Un son mono ne dispose que d'un canal, le son stéréo offre un canal par oreille, le son 5.1 6 canaux. En imagerie, l'équivalent du son sur deux canaux correspondrait aux lunettes se branchant sur la sortie moniteur d'une carte vidéo. Ces périphériques d'affichage coûteux, aujourd'hui peu utilisées en dehors de la recherche en imagerie, offrent une image différente à chaque oeil pour donner une impression de relief.
À titre d'information, un CD audio a une fréquence d'échantillonage de 44100 Hz, un format d'échantillonnage de 16 bits et deux canaux. Le terme « qualité radio » s'applique quant à lui souvent à une fréquence de 22050 Hz.
Vous le voyez donc, nous allons vite retrouver les marques que nous avons acquises lorsque nous avons appris à manipuler des images.
Ce mois-ci, nous allons simplement étudier l'architecture sonore de SDL et la tester par un programme très basique. Comme pour la vidéo, SDL propose des fonctions de très bas niveau, et il nous faudra les maîtriser avant d'envisager de réaliser des programmes vraiment utiles...
L'utilisation des fonctions audio de SDL passe, comme à l'habitude, par le passage du flag correspondant (ici, SDL_INIT_AUDIO) à la fonction SDL_Init. Puis nous allons définir les paramètres du périphérique audio avec la fonction SDL_OpenAudio:
int SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained);
Elle prend en paramètre deux pointeurs: l'un pointant vers la description du format sonore désiré, le deuxième étant utilisé pour renseigner le programmeur sur le format sonore effectivement obtenu, pour le cas où le matériel ne supporterait pas le format demandé. Si ce pointeur vaut NULL, et que le format demandé n'est pas supporté par le matériel, SDL met alors en place un mécanisme d'émulation. Le format visible pour le programmeur sera bien le format qu'il a demandé, mais SDL fera une conversion transparente vers le format effectivement supporté avant de l'envoyer à la carte son. La valeur de retour sera 0 en cas de succès, ou -1 en cas d'échec.
La structure SDL_AudioSpec comporte les champs nécessaires à la description du format sonore tel que nous l'avons décrit plus haut:
D'autres champs, non moins utiles, sont disponibles:
Les trois derniers champs méritent quelques explications supplémentaires. Contrairement à l'affichage, pour lequel nous avions un contrôle total sur la fréquence des images affichées (grâce à SDL_Flip, notamment), l'audio requiert de la régularité dans l'envoi des données sonores au matériel. De plus, la fréquence du média sonore est bien plus élevée que pour le média visuel (jusqu'à 44100 Hz pour l'audio, en général 70 Hz pour l'affichage). Il est donc difficilement envisageable d'envoyer manuellement chaque échantillon vers le matériel. À la place, nous allons remplir un tampon audio dans lequel le matériel ira continuellement lire les données sonores. Il est par conséquent très important de remplir ce tampon de manière régulière: s'il n'est plus rempli, le son va « boucler » jusqu'à ce que de nouvelles données y soient placées. Vous avez probablement déjà remarqué ce comportement dans certains jeux, notamment lors de plantages.
La régularité du remplissage du tampon est assurée par la fonction de rappel. Celle-ci est appelée automatiquement par SDL, via un thread dédié, lorsque cela devient nécessaire. Elle reçoit en paramètres le pointeur userdata, un pointeur vers le buffer audio, et le nombre d'octets à y placer. Son rôle, bien souvent, est de mixer les différentes source sonores et de copier le résultat dans le tampon audio.
La fréquence d'appel de la fonction de rappel est directement liée à la taille du tampon audio. Il y a un compromis à trouver ici: plus le tampon audio est petit, plus la fonction de rappel devra être appelée souvent, et plus il y aura de chances de remplir le tampon audio en retard et donc d'obtenir un son haché. Au contraire, un grand tampon garantit un son mixé en temps et en heure – par contre, la latence sera plus importante. Une trop grande latence se traduira par un décalage perceptible entre une action faite par l'utilisateur (par exemple, l'appui d'une touche au clavier) et l'effet sonore qui lui est associé. Cet effet, très désagréable, est intolérable dans un jeu.
Fort heureusement, des tailles de tampon faibles restent exploitables en pratique, et il est ainsi très rare de rater une échéance. Nous pourrons utiliser un taille de tampon de 512 échantillons sans problèmes.
La taille réelle du tampon sonore sera égale au champ samples multiplié par la taille d'un échantillon, fois le nombre de canaux.
Ces précisions devraient nous permettre de remplir correctement la structure SDL_AudioSpec passée à SDL_OpenAudio. Une fois le périphérique audio ouvert, la fonction de rappel n'est pas immédiatement appelée. Le son est mis en « pause » par défaut pour permettre au programmeur d'initialiser ses propres structures sonores. Un appel à SDL_PauseAudio avec un paramètre de 0 suffira pour commencer le mixage sonore.
Le chargement d'un son, via un fichier .wav, est assez similaire à l'ouverture d'une image .bmp. Dans un premier temps, le son est chargé par la fonction SDL_LoadWAV:
SDL_AudioSpec *SDL_LoadWAV(const char *file, SDL_AudioSpec *spec,
Uint8 **audio_buf, Uint32 *audio_len);
Le premier paramètre contient le nom du fichier .wav à ouvrir. Le second est un pointeur vers un SDL_AudioSpec qui sera rempli avec le format du fichier chargé. audio_buf est un pointeur qui sera alloué par SDL_LoadWAV et rempli avec les données audio. Enfin, l'entier indiqué par le pointeur audio_len contiendra, à l'issue de l'appel, la taille de audio_buf en octets.
Si le fichier a pu être chargé, SDL_LoadWAV retourne le même pointeur que spec, ou NULL si le chargement a échoué. Les sons ainsi chargés peuvent être libérés par SDL_FreeWAV.
Le format des sons chargés ne sera vraisemblablement pas le même que celui du périphérique audio. Comme pour une image, il faudra passer par une phase de conversion avant d'envoyer nos données au matériel. C'est le rôle de la fonction SDL_ConvertAudio:
int SDL_ConvertAudio(SDL_AudioCVT *cvt);
La conversion sonore demandant un grand nombre de paramètres, SDL passe par une structure dédiée pour cette opération. La structure SDL_AudioCVT doit être préalablement remplie par un appel à SDL_BuildAudioCvt:
int SDL_BuildAudioCVT(SDL_AudioCVT *cvt, Uint16 src_format, Uint8 src_channels, int src_rate,
Uint16 dst_format, Uint8 dst_channels, int dst_rate);
Les paramètres à passer sont la structure à initialiser, suivie des formats, nombre de canaux et fréquences de la source et de la destination. Une fois cette opération réalisée, il reste à copier les données audio à convertir dans le champ buf de SDL_AudioCVT, qu'il faudra au préalable allouer nous-mêmes. La taille à allouer sera égale à la taille du tampon source en octets, multiplié par le champ len_mult de la structure SDL_AudioCVT remplie. Ce facteur multiplicatif permet d'assurer suffisamment de place à SDL pour effectuer la conversion.
Une fois toutes ces étapes accomplies, l'appel à SDL_ConvertAudio transformera les données contenues dans buf du format source vers le format destination. La longueur du nouveau buffer sera égale au champ len_cvt de SDL_AudioCVT.
À présent que notre son est dans le bon format, comment le jouer? Tout simplement en attendant que notre fonction de rappel soit appelée, et en y copiant les données dans le tampon audio. Si notre son est plus long que le tampon audio (ce qui sera vraisemblablement le cas), il faudra procéder en plusieurs fois, chaque appel de la fonction de rappel correspondant à la copie d'un bout du son de la même taille que le tampon audio.
Le programme du mois illustre tous ces principes, en jouant un fichier sonore .wav avant de quitter:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "SDL.h"
/* Les données du fichier son chargé */
Uint8 * sounddata;
/* La taille du fichier son chargé, en octets */
Uint32 soundlength;
/* Position courante de lecture dans le fichier son */
Uint32 soundpos;
/* Fonction de rappel qui copie les données sonores dans le tampon audio */
void mixaudio(void * userdata, Uint8 * stream, int len)
{
/* Attention à ne pas déborder lors de la copie */
Uint32 tocopy = soundlength - soundpos > len ? len : soundlength - soundpos;
/* Copie des données sonores dans le tampon audio... */
memcpy(stream, sounddata + soundpos, tocopy);
/* Mise à jour de la position de lecture */
soundpos += tocopy;
}
int main(int argc, char * argv[])
{
SDL_AudioSpec desired, obtained, soundfile;
SDL_AudioCVT cvt;
if (SDL_Init(SDL_INIT_AUDIO) == -1) {
printf("Erreur lors de l'initialisation de SDL!\n");
return 1;
}
/* Son 16 bits stéréo à 44100 Hz */
desired.freq = 44100;
desired.format = AUDIO_U16SYS;
desired.channels = 2;
/* Le tampon audio contiendra 512 échantillons */
desired.samples = 512;
/* Mise en place de la fonction de rappel et des données utilisateur */
desired.callback = &mixaudio;
desired.userdata = NULL;
if (SDL_OpenAudio(&desired, &obtained) != 0) {
printf("Erreur lors de l'ouverture du périphérique audio: %s\n", SDL_GetError());
return 1;
}
printf("Paramètres audio obtenus: d canaux, fréquence d échantillons.\n",
obtained.format & 0xff, obtained.channels, obtained.freq, obtained.samples);
/* Chargement du fichier .wav */
if (SDL_LoadWAV("KDE_Startup_new.wav", &soundfile, &sounddata, &soundlength) == NULL) {
printf("Erreur lors du chargement du fichier son: %s\n", SDL_GetError());
return 1;
}
printf("Propriétés du fichier audio: d canaux, fréquence d octets.\n",
soundfile.format & 0xff, soundfile.channels, soundfile.freq, soundlength);
/* Conversion vers le format du tampon audio */
if (SDL_BuildAudioCVT(&cvt, soundfile.format, soundfile.channels, soundfile.freq,
obtained.format, obtained.channels, obtained.freq) < 0) {
printf("Impossible de construire le convertisseur audio!\n");
return 1;
}
/* Création du tampon utilisé pour la conversion */
cvt.buf = malloc(soundlength * cvt.len_mult);
cvt.len = soundlength;
memcpy(cvt.buf, sounddata, soundlength);
/* Conversion... */
if (SDL_ConvertAudio(&cvt) != 0) {
printf("Erreur lors de la conversion du fichier audio: %s\n", SDL_GetError());
return 1;
}
/* Libération de l'ancien tampon, création du nouveau,
copie des données converties, effacement du tampon de conversion */
SDL_FreeWAV(sounddata);
sounddata = malloc(cvt.len_cvt);
memcpy(sounddata, cvt.buf, cvt.len_cvt);
free(cvt.buf);
soundlength = cvt.len_cvt;
printf("Taille du son converti: %d octets\n", soundlength);
soundpos = 0;
/* La fonction de rappel commence à être appelée à partir de maintenant. */
printf("Démarrage de la lecture...\n");
SDL_PauseAudio(0);
/* On attend que l'autre thread ait fini la lecture du son... */
while (soundpos < soundlength);
/* On cesse d'appeler la fonction de rappel */
SDL_PauseAudio(1);
/* Fermer le périphérique audio */
SDL_CloseAudio();
SDL_Quit();
return 0;
}
Comme d'habitude, vous pourrez trouver de programme sur le CD du magazine ou sur http://www.gnurou.org/documents/linuxmag/SDL/SDL-5.tar.gz.
Attention: pour vos tests, il est recommandé de désactiver les démons sonores tels que arts ou esd. SDL sait les utiliser, et ainsi mixer ses sa sortie sonore avec d'autres programmes, mais les tailles de tampon utilisées sont en général très grandes augmentent ainsi grandement la latence, faussant le résultat.
Vous pouvez essayer de changer les valeurs de la structure desired et observer les effets qu'ont la fréquence, la nombre de canaux et le format sur la qualité du son joué et sur la taille des échantillons sonores. Vous ne pourrez pas vous permettre n'importe quelle valeur, notamment pour la fréquence ; mais les classiques 44100, 22050 et 11025 Hz devraient fonctionner correctement.
Ce qui nous attend par la suite: mixage de différentes sources sonores, moteur de son plus sophistiqué et surtout plus simple à utiliser, et effets sur les sons. D'ici là, joyeux code!