Articoli Manifesto Tools Links Canali Libri Contatti ?
Giochi / Programmazione / SDL

SDL_mixer: gestire musica e campioni audio con SDL

Abstract
Nella presente guida viene mostrata la gestione, in contemporanea, di una traccia musicale e di diversi campioni audio. Sfruttando la libreria SDL_mixer, la musica ed i campioni verranno miscelati tra loro senza particolari difficoltà, in più si analizzeranno alcune delle funzioni più significative, quali quelle per la gestione dei volumi, del panning e del fade.
Data di stesura: 09/09/2004
Data di pubblicazione: 02/11/2004
Ultima modifica: 04/04/2006
di Angelo Theodorou Discuti sul forum   Stampa

Premesse

Questa guida richiede la conoscenza del linguaggio C e dei concetti base della libSDL. Consiglio, a riguardo, la lettura di un buon libro su tale linguaggio e delle prime guide di M3xican [2][3] su tale libreria, dal momento che non verranno fatti approfondimenti particolari su questi due argomenti.

Segnalo, a titolo informativo, che la presente guida, ed il programma allegato, sono stati realizzati su piattaforma Linux (MDK 9.2, con GCC 3.3.1 e libSDL 1.2.5).

Si segnala che l'articolo è stato realizzato durante lo sviluppo di Mars, Land of No Mercy, un gioco strategico a turni opensource.

SDL_mixer

Qui vale lo stesso discorso fatto per la SDL_image nella guida [4] di M3xican.
Nativamente le SDL permettono la riproduzione sonora unicamente di campioni sonori nel formato WAV, questo, come accade per il BMP, è molto facile da gestire per gli sviluppatori delle SDL, ma molto scomodo da usare per l'utente di tale libreria, dal momento che si tratta di un formato che occupa molto spazio e che non usa alcun tipo di compressione.

La SDL_mixer [5] viene incontro a questo problema, ma anche a quello della difficoltà di utilizzo dell'audio tramite una libSDL "liscia", la quale, come per la sottosezione video, si pone ad un livello molto basso, dove è alquanto difficile anche solo far suonare un campione.
SDL_mixer permette di gestire dati audio in formato AIFF, RIFF, VOC, MP3, OGG, MOD, MIDI, oltre naturalmente al WAV. In più permette il missaggio di diversi canali audio, alcuni effetti, un canale dedicato alla musica e la gestione di "gruppi" di canali.

Consiglio vivamente la lettura della guida online [6] della SDL_mixer.

Sorgente

Ecco di seguito il sorgente del programma che andremo poi ad analizzare con calma nella sezione 5, dategli un primo sguardo.
  1. #include <stdlib.h> 
  2. #include <SDL/SDL.h> 
  3. #include <SDL/SDL_mixer.h> 
  4.  
  5. void handleKey(SDL_KeyboardEvent key); 
  6.  
  7.  
  8. Mix_Music *music; 
  9. Mix_Chunk *sample1, *sample2, *sample3; 
  10.  
  11. int main(int argc, char **argv) 
  12.   SDL_Surface *screen; 
  13.   SDL_Event event; 
  14.   Uint8 *keys; 
  15.   int quit = 0; 
  16.  
  17.   int frequency = 44100; 
  18.   Uint16 format = MIX_DEFAULT_FORMAT; /* Corrisponde a AUDIO_S16SYS */ 
  19.   int channels = MIX_DEFAULT_CHANNELS; /* Stereo */ 
  20.   int buffers = 2048; 
  21.  
  22.   Uint8 left = 127; 
  23.   int volume = MIX_MAX_VOLUME; 
  24.  
  25.   char *music_file, *sample1_file, *sample2_file, *sample3_file; 
  26.  
  27.   /* Operiamo il parsing dei parametri dati da shell */ 
  28.   if (argc == 1) { 
  29.     music_file = "music.ogg"; 
  30.     sample1_file = "sample1.ogg"; 
  31.     sample2_file = "sample2.ogg"; 
  32.     sample3_file = "sample3.ogg"; 
  33.   else if (argc == 5) { 
  34.     music_file = argv[1]; 
  35.     sample1_file = argv[2]; 
  36.     sample2_file = argv[3]; 
  37.     sample3_file = argv[4]; 
  38.   else { 
  39.     printf("La sintassi corretta è: %s musica campione1 campione2 campione3\n"

        , argv[0]); 
  40.     exit(-1); 
  41.  
  42.   /* Inizializzazione del sottosistema audio e video */ 
  43.   if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) < 0) { 
  44.     printf("Errore init SDL: %s\n", SDL_GetError()); 
  45.     exit (-1); 
  46.  
  47.   /* All'uscita del programma viene eseguito SDL_Quit per risistemare le cose 

      */ 
  48.         atexit(SDL_Quit); 
  49.  
  50.   /* Apriamo una finestra per catturare gli eventi dell'utente */ 
  51.   if ((screen = SDL_SetVideoMode(320, 240, 0, 0)) == NULL) { 
  52.     printf("Errore apertura video: %s\n", SDL_GetError()); 
  53.     exit (-1); 
  54.  
  55.   /* Apriamo il dispositivo audio */ 
  56.   if (Mix_OpenAudio(frequency, format, channels, buffers) == -1) { 
  57.     printf("Errore apertura dispositivo audio: %s\n", Mix_GetError()); 
  58.     exit(-1); 
  59.  
  60.   /* Ridimensioniamo il numero di canali allocati, ce ne bastano solo 4 */ 
  61.   Mix_AllocateChannels(4); 
  62.  
  63.   /* Analizziamo cosa ci ha effettivamente dato a disposizione il sistema */ 
  64.   int numtimesopened; 
  65.   numtimesopened = Mix_QuerySpec(&frequency, &format, &channels); 
  66.   if (!numtimesopened) 
  67.     printf("Errore con la Mix_QuerySpec(): %s\n", Mix_GetError()); 
  68.   else { 
  69.     char *format_str="Sconosciuto"; 
  70.     switch (format) { 
  71.       case AUDIO_U8: 
  72.         format_str="U8"; 
  73.         break; 
  74.       case AUDIO_S8: 
  75.         format_str="S8"; 
  76.         break; 
  77.       case AUDIO_U16LSB: 
  78.         format_str="U16LSB"; 
  79.         break; 
  80.       case AUDIO_S16LSB: 
  81.         format_str="S16LSB"; 
  82.         break; 
  83.       case AUDIO_U16MSB: 
  84.         format_str="U16MSB"; 
  85.         break; 
  86.       case AUDIO_S16MSB: 
  87.         format_str="S16MSB"; 
  88.         break; 
  89.  
  90.   printf("Il dispositivo è stato aperto %d volte\n", numtimesopened); 
  91.   printf("Specifiche audio ottenute dal sistema:\n"); 
  92.   printf("Frequenza = %d\n", frequency); 
  93.   printf("Formato = %s\n", format_str); 
  94.   printf("Canali = %d\n\n", channels); 
  95.  
  96.   /* Carichiamo la musica */ 
  97.   if ((music = Mix_LoadMUS(music_file)) == NULL) 
  98.     printf("Errore caricamento musica : %s\n", Mix_GetError()); 
  99.  
  100.   /* Carichiamo i campioni audio */ 
  101.   if ((sample1 = Mix_LoadWAV(sample1_file)) == NULL) 
  102.     printf("Errore caricamento campione 1: %s\n", Mix_GetError()); 
  103.   if ((sample2 = Mix_LoadWAV(sample2_file)) == NULL) 
  104.     printf("Errore caricamento campione 2: %s\n", Mix_GetError()); 
  105.   if ((sample3 = Mix_LoadWAV(sample3_file)) == NULL) 
  106.     printf("Errore caricamento campione 3: %s\n", Mix_GetError()); 
  107.  
  108.   /* Registriamo un effetto di panning centrale */ 
  109.   Mix_SetPanning(MIX_CHANNEL_POST, 127, 127); 
  110.  
  111.   /* Ciclo di gestione degli eventi dell'utente */ 
  112.   while (!quit) { 
  113.     while (SDL_PollEvent(&event)) { 
  114.       switch (event.type) { 
  115.       case SDL_QUIT: 
  116.         quit = 1; 
  117.         break; 
  118.       case SDL_KEYDOWN: 
  119.         handleKey(event.key); 
  120.         break; 
  121.       if (event.key.keysym.sym == SDLK_ESCAPE || event.key.keysym.sym == 

          SDLK_q) 
  122.         quit = 1; 
  123.  
  124.     keys = SDL_GetKeyState(NULL); 
  125.  
  126.     if (keys[SDLK_UP]) { 
  127.       if (volume < MIX_MAX_VOLUME) { 
  128.         volume += 4; 
  129.         Mix_Volume(-1, volume); 
  130.         Mix_VolumeMusic(volume); 
  131.         printf("Volume = %d\n", volume); 
  132.  
  133.     if (keys[SDLK_DOWN]) { 
  134.       if (volume > 0) { 
  135.         volume -= 4; 
  136.         Mix_Volume(-1, volume); 
  137.         Mix_VolumeMusic(volume); 
  138.         printf("Volume = %d\n", volume); 
  139.  
  140.     if (keys[SDLK_LEFT]) { 
  141.       if (left < 250) { 
  142.         left += 4; 
  143.         Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left); 
  144.         printf("Panning = L:%d, R:%d\n", left, 254-left); 
  145.  
  146.     if (keys[SDLK_RIGHT]) { 
  147.       if (left > 4) { 
  148.         left -= 4; 
  149.         Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left); 
  150.         printf("Panning = L:%d, R:%d\n", left, 254-left); 
  151.  
  152.     /* Facciamo respirare la CPU tra un poll ed un altro */ 
  153.     SDL_Delay(50); 
  154.  
  155.   /* Liberiamo la memoria allocata per i campioni e la musica e chiudiamo il 

      dispositivo audio */ 
  156.   Mix_FreeChunk(sample3); 
  157.   Mix_FreeChunk(sample2); 
  158.   Mix_FreeChunk(sample1); 
  159.   Mix_FreeMusic(music); 
  160.   Mix_CloseAudio(); 
  161.   SDL_Quit(); 
  162.  
  163.   return 0; 
  164.  
  165. /* Gestisce la struttura SDL_KeyboardEvent per rispondere all'input da 

    tastiera */ 
  166. void handleKey(SDL_KeyboardEvent key) 
  167.   switch(key.keysym.sym) { 
  168.   case SDLK_c: 
  169.     Mix_SetPanning(MIX_CHANNEL_POST, 127, 127); 
  170.     printf("Panning = Centrale\n"); 
  171.     break; 
  172.   case SDLK_f: 
  173.     if (Mix_PlayingMusic()) 
  174.       Mix_FadeOutMusic(3000); 
  175.     else 
  176.       Mix_FadeInMusic(music, -1, 3000); 
  177.     break; 
  178.   case SDLK_m: 
  179.     if (Mix_PlayingMusic()) 
  180.       Mix_HaltMusic(); 
  181.     else 
  182.       Mix_PlayMusic(music, -1); 
  183.     break; 
  184.   case SDLK_p: 
  185.     if (Mix_PausedMusic()) 
  186.       Mix_ResumeMusic(); 
  187.     else 
  188.       Mix_PauseMusic(); 
  189.     break; 
  190.   case SDLK_1: 
  191.   case SDLK_KP1: 
  192.     Mix_PlayChannel(1, sample1, 0); 
  193.     break; 
  194.   case SDLK_2: 
  195.   case SDLK_KP2: 
  196.     Mix_PlayChannel(2, sample2, 0); 
  197.     break; 
  198.   case SDLK_3: 
  199.   case SDLK_KP3: 
  200.     Mix_PlayChannel(3, sample3, 0); 
  201.     break; 

Spiegazione del sorgente

main

In questa sezione verrà spiegato in dettaglio il sorgente che vi è stato presentato poco sopra, con particolare attenzione ai concetti nuovi introdotti dalla SDL_mixer.
  1. #include <stdlib.h> 
  2. #include <SDL/SDL.h> 
  3. #include <SDL/SDL_mixer.h> 
  4.  
  5. void handleKey(SDL_KeyboardEvent key); 
  6.  
  7.  
  8. Mix_Music *music; 
  9. Mix_Chunk *sample1, *sample2, *sample3; 
La prima riga è l'include dell'header per la exit() e la at_exit() usate in questo programma, mentre ormai sarete abituati alla seconda, che include l'header per utilizzare le libSDL, la terza invece, come avrete intuito, permette l'utilizzo della SDL_mixer.
Alla riga 5 dichiariamo una funzione per la gestione degli eventi che verrà spiegata più avanti.

Le righe 8 e 9 introducono le prime vere novità, qui vengono dichiarati 4 puntatori che verranno successivamente fatti puntare a delle particolari strutture della SDL_mixer per la gestione della musica e dei campioni audio.

  1. int main(int argc, char **argv) 
  2.   SDL_Surface *screen; 
  3.   SDL_Event event; 
  4.   Uint8 *keys; 
  5.   int quit = 0; 
Anche in queste righe nulla di nuovo. Notate solamente la definizione del main, che in questo programma riceverà parametri dall shell.
Le righe 13-16 dichiarano un puntatore per la superficie dello schermo, una struttura per gli eventi, un puntatore ad una struttura con la mappa dei tasti premuti in quel momento ed una variabile intera che fungerà da flag per il ciclo degli eventi.
  1.   int frequency = 44100; 
  2.   Uint16 format = MIX_DEFAULT_FORMAT; /* Corrisponde a AUDIO_S16SYS */ 
  3.   int channels = MIX_DEFAULT_CHANNELS; /* Stereo */ 
  4.   int buffers = 2048; 
  5.  
  6.   Uint8 left = 127; 
  7.   int volume = MIX_MAX_VOLUME; 
Le righe 18-21 verranno spiegate successivamente, quando andremo ad usare le variabili ivi dichiarate come parametri per la Mix_OpenAudio().
Anche la 23 e la 24 verranno meglio approfondite quando saranno effettivamente utilizzate.
  1.   char *music_file, *sample1_file, *sample2_file, *sample3_file; 
  2.  
  3.   /* Operiamo il parsing dei parametri dati da shell */ 
  4.   if (argc == 1) { 
  5.     music_file = "music.ogg"; 
  6.     sample1_file = "sample1.ogg"; 
  7.     sample2_file = "sample2.ogg"; 
  8.     sample3_file = "sample3.ogg"; 
  9.   else if (argc == 5) { 
  10.     music_file = argv[1]; 
  11.     sample1_file = argv[2]; 
  12.     sample2_file = argv[3]; 
  13.     sample3_file = argv[4]; 
  14.   else { 
  15.     printf("La sintassi corretta è: %s musica campione1 campione2 campione3\n"

        , argv[0]); 
  16.     exit(-1); 
I nomi dei file che andremo a caricare sono puntati dalle variabili puntatore dichiarate nella riga 26.
Se l'utente fa partire il programma senza parametri, le righe 29-34 assegneranno dei valori di default come nome dei file da caricare, altrimenti, se l'utente intende specificare i 4 file da utilizzare, può farlo quando invoca il programma (righe 35-40).
Le righe 41-44 fanno sì che il programma esca stampando un messaggio d'errore se l'utente non adopera la sintassi corretta.
  1.   /* Inizializzazione del sottosistema audio e video */ 
  2.   if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) < 0) { 
  3.     printf("Errore init SDL: %s\n", SDL_GetError()); 
  4.     exit (-1); 
Verificato che il programma sia stato invocato correttamente, è tempo di inizializzare le SDL chiamando la SDL_Init(), notate che vengono inizializzati sia il sottosistema audio che quello video.
  1.   /* All'uscita del programma viene eseguito SDL_Quit per risistemare le cose 

      */ 
  2.         atexit(SDL_Quit); 
Affidiamo alla SDL_Quit() il ripristino delle condizioni iniziali in caso di uscita con una exit().
  1.   /* Apriamo una finestra per catturare gli eventi dell'utente */ 
  2.   if ((screen = SDL_SetVideoMode(320, 240, 0, 0)) == NULL) { 
  3.     printf("Errore apertura video: %s\n", SDL_GetError()); 
  4.     exit (-1); 
Apriamo una piccola finestra con la nota funzione SDL_SetVideoMode().
Anche se non ci servirà per disegnarci dentro, è fondamentale avere una finestra qualsiasi aperta per catturare gli eventi dell'utente quando quest'ultima ha il focus, ovvero risulta selezionata.
  1.   /* Apriamo il dispositivo audio */ 
  2.   if (Mix_OpenAudio(frequency, format, channels, buffers) == -1) { 
  3.     printf("Errore apertura dispositivo audio: %s\n", Mix_GetError()); 
  4.     exit(-1); 
Analogamente a ciò che facciamo nel caso volessimo disegnare qualcosa, ovvero l'apertura dello schermo, per la SDL_mixer, prima di tentar di fare qualsiasi cosa con il suono, è necessario aprire il dispositivo audio, ovvero farsi riservare dal sistema delle risorse sonore in base alle nostre richieste.

La Mix_OpenAudio() fa proprio questo, riceve quattro parametri con le specifiche scelte e ritorna un intero ad indicare se l'inizializzazione ha avuto successo oppure no.

Ecco la sintassi dal manuale [6] della SDL_mixer:

int Mix OpenAudio(int frequency, Uint16 format, int channels, int chunksize)
frequency è un intero che indica la frequenza di campionamento, ovvero quanti campioni al secondo compongono l'approssimazione digitale dell'onda sonora. Non è un cattiva idea usare il define MIX_DEFAULT_FREQUENCY come argomento per l'utilizzo nei giochi, esso è pari a 22khz, un valore che non appesantisce troppo le CPU più vecchie ma che garantisce comunque un buon compromesso con la qualità.

format è un intero che definisce, insieme a frequency, la tipologia di campioni utilizzati per l'audio. Infatti, anche caricando suoni con caratteristiche sonore diverse, questi verranno tutti convertiti internamente nel formato indicato all'apertura del dispositivo audio.
I valori che format può assumere sono i seguenti:

AUDIO_U8
Campioni a 8-bit senza segno
AUDIO_S8
Campioni a 8-bit con segno
AUDIO_U16LSB
Campioni a 16-bit senza segno, con ordine di byte little-endian
AUDIO_S16LSB
Campioni a 16-bit con segno, con ordine di byte little-endian
AUDIO_U16MSB
Campioni a 16-bit senza segno, con ordine di byte big-endian
AUDIO_S16MSB
Campioni a 16-bit con segno, con ordine di byte big-endian
AUDIO_U16
lo stesso di AUDIO_U16LSB (probabilmente per compatibilità all'indietro)
AUDIO_S16
lo stesso di AUDIO_S16LSB (probabilmente per compatibilità all'indietro)
AUDIO_U16SYS
Campioni a 16-bit senza segno, con ordine di byte dettato dal sistema
AUDIO_S16SYS
Campioni a 16-bit con segno, con ordine di byte dettato dal sistema
Nel caso che tutte queste differenti opzioni vi creino solo confusione, potete usare senza problemi, come è stato fatto in questo programma, MIX_DEFAULT_FORMAT, che corrisponde ad AUDIO_S16SYS.

channels è un intero che può assumere solo due valori, 1, nel caso vogliate che il flusso audio sia mono, e 2 se invece volete utilizzare un output stereo.
Ancora una volta è possibile usare un define di default, MIX_DEFAULT_CHANNLES, che equivale a esprimere la volontà di utilizzare campioni stereofonici.

chunksize indica la dimensione in byte del buffer audio. Quando ordinate alla SDL_mixer di suonare un qualsiasi campione, oppure una musica, questa si preoccupa di copiare piccoli pezzi, della dimensione di chunksize, in un buffer particolare. Quest'ultimo viene utilizzato dal thread audio per emettere suoni senza interferire minimamente con il flusso del programma principale. Appena il buffer è stato suonato completamente, il thread audio avverte la SDL_mixer, la quale riempie il buffer di nuovi dati, in modo che il suono non si interrompa.
Questo funzionamento è del tutto trasparente all'utente (mentre, se avessimo usato le SDL senza SDL_mixer, il procedimento non sarebbe stato così automatico), che però si deve preoccupare di indicare una dimensione per questo importantissimo buffer. Infatti valori troppo piccoli possono rendere il suono non fluido su una macchina lenta, la quale non riuscirà a stare dietro alla continua necessità di aggiornare il buffer con dati freschi, mentre uno troppo grande renderebbe il suono poco responsivo, infatti ogni cambiamento nel flusso audio (ad esempio l'esplosione di un'astronave in un gioco ambientato nello spazio) si manifesterebbe solo al caricamento del prossimo buffer.
Nel caso stiate suonando solo musica potete fissare la dimensione a 4kb (4096 byte) o più, ma se è presente molta azione, allora è il caso di considerare valori minori.
Ad ogni modo, fate prove con valori differenti per trovare quello che meglio soddisfa le vostre necessità.

In caso di errore, al rigo 63, viene utilizzata la Mix_GetError() che, analogamente alla SDL_GetError(), ritorna un puntatore ad una stringa con le ragioni per le quali si è verificato tale errore.

  1.   /* Ridimensioniamo il numero di canali allocati, ce ne bastano solo 4 */ 
  2.   Mix_AllocateChannels(4); 
Di default, con l'apertura del dispositivo audio, vengono allocati 8 canali audio indipendenti, questi verranno missati insieme per determinare effettivamente l'audio riprodotto dall'applicazione.
Nel nostro caso ci basta avere anche solo 4 canali, uno per la musica e tre per i campioni.
Possiamo chiamare la Mix_AllocateChannels() anche successivamente, in qualsiasi punto del sorgente, per cambiare dinamicamente il numero di canali da utilizzare. La funzione ritorna il numero di canali allocati e generalmente non dovrebbe mai fallire, a meno che non specifichiate un numero grandissimo di canali che potrebbe saturare tutta la memoria e causare un errore di segmentazione, ma sono casi più unici che rari.

N.B. Non c'è alcuna relazione tra i canali per il mixing e quelli definiti con la Mix_OpenAudio(), che invece servono solo a distinguere tra la modalità monofonica e quella stereofonica.

  1.   /* Analizziamo cosa ci ha effettivamente dato a disposizione il sistema */ 
  2.   int numtimesopened; 
  3.   numtimesopened = Mix_QuerySpec(&frequency, &format, &channels); 
  4.   if (!numtimesopened) 
  5.     printf("Errore con la Mix_QuerySpec(): %s\n", Mix_GetError()); 
  6.   else { 
  7.     char *format_str="Sconosciuto"; 
  8.     switch (format) { 
  9.       case AUDIO_U8: 
  10.         format_str="U8"; 
  11.         break; 
  12.       case AUDIO_S8: 
  13.         format_str="S8"; 
  14.         break; 
  15.       case AUDIO_U16LSB: 
  16.         format_str="U16LSB"; 
  17.         break; 
  18.       case AUDIO_S16LSB: 
  19.         format_str="S16LSB"; 
  20.         break; 
  21.       case AUDIO_U16MSB: 
  22.         format_str="U16MSB"; 
  23.         break; 
  24.       case AUDIO_S16MSB: 
  25.         format_str="S16MSB"; 
  26.         break; 
Una volta utilizzata la Mix_OpenAudio() per indicare le nostre preferenze per quanto riguarda il dispositivo audio, questo non vuol dire che poi siano quelle che effettivamente otteniamo dal sistema, che potrebbe non avere la possibilità di rendere disponibili alcune modalità.
Per richiedere informazioni sulle specifiche fornite effettivamente dal sistema utilizziamo la Mix_QuerySpec(), fornendo come parametri, rispettivamente, un puntatore ad una variabile di tipo int dove noi vogliamo che la funzione ci ritorni la frequenza effettivamente in uso, un puntatore ad un Uint16 per il formato ed un altro di tipo intero per il numero di canali allocati (mono o stereo).
Nel sorgente ho "riciclato" le variabili usate per la Mix_OpenAudio(), dal momento che non ci servono più e che sono del tipo adatto ad essere usate con la Mix_QuerySpec(), in più ho dichiarato un intero numtimesopened pronto a raccogliere il valore di ritorno della funzione, equivalente al numero di volte che il dispositivo audio è stato aperto dall'inizio del programma.
Ovviamente, se tale variabile è uguale a zero, allora qualcosa non è andato per il verso giusto (righe 73-74).

Le righe 76-96 servono ad identificare e tradurre in stringa i diversi formati, che invece vengono indicati e restituiti dalla funzione come interi.

  1.   printf("Il dispositivo è stato aperto %d volte\n", numtimesopened); 
  2.   printf("Specifiche audio ottenute dal sistema:\n"); 
  3.   printf("Frequenza = %d\n", frequency); 
  4.   printf("Formato = %s\n", format_str); 
  5.   printf("Canali = %d\n\n", channels); 
Ottenuti i dati che volevamo è tempo di stamparli per renderli disponibili all'utente del programma.
  1.   /* Carichiamo la musica */ 
  2.   if ((music = Mix_LoadMUS(music_file)) == NULL) 
  3.     printf("Errore caricamento musica : %s\n", Mix_GetError()); 
Ora che siamo sicuri di avere a disposizione le risorse necessarie a riprodurre del suono, è tempo di caricare i dati, iniziando dalla musica.
Benché alla fin fine la musica è generalmente nient'altro che un campione sonoro molto lungo, la SDL_mixer mette comunque a disposizione un canale dedicato e delle funzioni specifiche per la gestione della stessa.

Il caricamento avviene chiamando la funzione Mix_LoadMUS() e fornendogli come parametro una stringa con il nome del file. Nel nostro caso la stringa è music_file, in realtà è un puntatore ad un parametro ricevuto da shell o ad una costante stringa di default, questo dipende da come è stato invocato il programma (righe 26-44).

Il valore di ritorno è un puntatore di tipo Mix_Music, nel nostro caso questo si chiama music ed è stato dichiarato globalmente alla riga 8. Se tale valore è pari a NULL, allora la musica non è stata caricata e verrà stampato un errore con la solita Mix_GetError().
Notate che, anche se la musica non riesce ad essere caricata correttamente, il programma non viene interrotto, semplicemente questa non si sentirà.

  1.   /* Carichiamo i campioni audio */ 
  2.   if ((sample1 = Mix_LoadWAV(sample1_file)) == NULL) 
  3.     printf("Errore caricamento campione 1: %s\n", Mix_GetError()); 
  4.   if ((sample2 = Mix_LoadWAV(sample2_file)) == NULL) 
  5.     printf("Errore caricamento campione 2: %s\n", Mix_GetError()); 
  6.   if ((sample3 = Mix_LoadWAV(sample3_file)) == NULL) 
  7.     printf("Errore caricamento campione 3: %s\n", Mix_GetError()); 
In queste righe carichiamo i tre campioni sonori da utilizzare nel nostro programma, il codice è molto simile a quello usato per la musica.
Le uniche differenze sono nell'uso di puntatori di tipo Mix_Chunk (dichiarati alla riga 9) e della funzione Mix_LoadWAV() in luogo della Mix_LoadMUS(), la quale si comporta esattamente come quest'ultima per quanto riguarda il suo unico parametro ed il valore di ritorno.
  1.   /* Registriamo un effetto di panning centrale */ 
  2.   Mix_SetPanning(MIX_CHANNEL_POST, 127, 127); 
Questa è la prima delle funzioni di manipolazione dell'audio che incontriamo.
La Mix_SetPanning() serve ad impostare, come indica il suo nome, il panning dei canali audio, ovvero la distribuzione della riproduzione audio per quanto riguarda il canale destro e quello sinistro, ed ovviamente funziona solamente se abbiamo a disposizione un dispositivo audio stereofonico.

Prima di analizzare i parametri ed il valore di ritorno della funzione, mi sembra opportuno fare una piccola digressione sulla "registrazione" degli effetti con la SDL_mixer.
Quando viene chiamata la Mix_SetPanning() quello che si fa in realtà è indicare, al thread che suona l'audio, una funzione aggiuntiva da richiamare ogni volta che si sta per riprodurre il buffer audio.
Questa manipolerà i dati audio in maniera tale da ricavare un certo "effetto", quale il panning (Mix_SetPanning()), la distanza dall'ascoltatore (Mix_SetDistance()), la posizione relativa della sorgente sonora (Mix_SetPosition()) o l'inversione dei canali destro/sinistro (Set_ReverseStereo()).
Gli effetti utilizzabili non si fermano qui, perché con la Mix_RegisterEffect() è possibile adoperare una propria funzione per processare i dati audio, ma questo è un tema avanzato che richiede conoscenze fisiche sulle onde sonore e sulla teoria dei segnali oltre che un maggior approfondimento del manuale [6] della SDL_mixer.

Torniamo a parlare della Mix_SetPanning(), come si vede dalla riga 118 questa accetta 3 parametri, il primo è un intero che indica il canale che sarà affetto dal panning, nel sorgente è usata la costante MIX_CHANNEL_POST, ciò sta ad indicare che questo è applicato su tutti i canali, o meglio, sul canale finale che ospita il missaggio di tutti i singoli canali.
Gli altri due parametri sono due interi (compresi tra 0 e 255) che indicano invece il volume per ciascuno dei due canali destro/sinistro indipendentemente, e quindi sono i veri artefici dell'effetto di panning.

Notate bene che non ho usato 255, il valore massimo, per definire un panning centrale, ma 127, dimezzando quindi il volume globale. In questo modo, quando successivamente verranno fatti variare i volumi tra i due canali, non ci saranno salti di volume, il volume totale, ovvero la somma tra i volumi dei singoli canali, sarà sempre uguale a 254, l'intero pari più grande nel raggio possibile dei valori.
Questo vuole anche dire che se non si ha in mente di usare il panning nelle proprie applicazioni è inutile registrare tale effetto e dimezzare il volume totale come fatto alla riga 118.

Le righe dalla 120 alla 173 verranno approfondite meglio nella prossima sottosezione, così come la funzione ausiliaria handleKey().

  1.   /* Liberiamo la memoria allocata per i campioni e la musica e chiudiamo il 

      dispositivo audio */ 
  2.   Mix_FreeChunk(sample3); 
  3.   Mix_FreeChunk(sample2); 
  4.   Mix_FreeChunk(sample1); 
  5.   Mix_FreeMusic(music); 
  6.   Mix_CloseAudio(); 
  7.   SDL_Quit(); 
  8.  
  9.   return 0; 
Arriviamo così alla fine del programma, dove le risorse allocate precedentemente vengono rilasciate.
Viene liberata la memoria per i 3 campioni audio (righe 176-178) e quella per la musica (riga 179), viene chiuso il dispositivo audio (riga 180) e viene terminata ogni funzione relativa alle SDL (riga 181).
Inoltre il main() ritorna al sistema l'intero 0 per indicare che non ci sono stati problemi nell'esecuzione del programma (riga 183).

Il ciclo degli eventi

  1.   /* Ciclo di gestione degli eventi dell'utente */ 
  2.   while (!quit) { 
Alla riga 121 inizia l'ormai famoso ciclo degli eventi del programma. La flag che verrà settata per l'uscita dal while più esterno è l'intero di nome quit.
  1.     while (SDL_PollEvent(&event)) { 
  2.       switch (event.type) { 
  3.       case SDL_QUIT: 
  4.         quit = 1; 
  5.         break; 
  6.       case SDL_KEYDOWN: 
  7.         handleKey(event.key); 
  8.         break; 
  9.       if (event.key.keysym.sym == SDLK_ESCAPE || event.key.keysym.sym == 

          SDLK_q) 
  10.         quit = 1; 
La SDL_PollEvent() viene usata come al solito per riempire una struttura di eventi, la quale viene analizzata per mezzo di un costrutto switch ed una serie di case.
Da notare, rispetto alle precedenti guide [2] di M3xican, che ho preferito demandare ad una funzione apposita (riga 128) la gestione dell'evento SDL_KEYDOWN (la pressione di un tasto) per non appesantire troppo il sorgente.
Un'altra piccola differenza è il tasto per l'uscita dal programma, oltre all'Escape ho preso in considerazione anche la lettera "q" (riga 131).
  1.     keys = SDL_GetKeyState(NULL); 
Chiuso il while della SDL_PollEvent(), sensibile ad eventi quali la pressione o il rilascio di un tasto, è necessario usare la SDL_GetKeyState() per ricavare una mappa dei tasti correntemente premuti, in questo modo è possibile reagire anche ad eventi quale la pressione continua di uno stesso tasto.
La funzione viene chiamata con un puntatore nullo come parametro e ritorna il puntatore ad un dato di tipo Uint8 contenente lo stato di pressione dei tasti in quel dato momento.
  1.     if (keys[SDLK_UP]) { 
  2.       if (volume < MIX_MAX_VOLUME) { 
  3.         volume += 4; 
  4.         Mix_Volume(-1, volume); 
  5.         Mix_VolumeMusic(volume); 
  6.         printf("Volume = %d\n", volume); 
  7.  
  8.     if (keys[SDLK_DOWN]) { 
  9.       if (volume > 0) { 
  10.         volume -= 4; 
  11.         Mix_Volume(-1, volume); 
  12.         Mix_VolumeMusic(volume); 
  13.         printf("Volume = %d\n", volume); 
Nel caso l'utente stia tenendo premuto la freccia verso l'alto (righe 137-144) o quella verso il basso (righe 146-153) il programma risponde di conseguenza, modificando il volume della riproduzione.
Per prima cosa controlla se non si è già raggiunto il limite massimo o minimo (righe 138 e 147), dopodiché modifica la variabile volume (definita alla riga 24 ed inizializzata con la costante MIX_MAX_VOLUME, ovvero il massimo valore possibile) di conseguenza (righe 139 e 148).
Il prossimo passo è quello di impostare effettivamente il volume per la riproduzione, le funzioni da usare sono due, una per la musica ed una per gli altri canali audio.

La Mix_VolumeMusic() accetta un solo parametro che è semplicemente l'intero (da 0 a 128) che definisce il volume per la musica, mentre la Mix_Volume() ha in più un altro parametro, il primo, che serve invece ad indicare il canale per il quale deve valere la modifica al volume. Nel programma è stato usato l'intero -1, ciò significa che la modifica varrà per tutti i canali audio, eccetto ovviamente quello della musica, che può essere manipolato solo mediante la Mix_VolumeMusic().

Modificato il volume di tutti i canali in accordo con l'input dell'utente (righe 140-141 e 149-150) notifichiamo quest'ultimo dell'avvenuto cambiamento stampando il valore corrente. (righe 142 e 151).

  1.     if (keys[SDLK_LEFT]) { 
  2.       if (left < 250) { 
  3.         left += 4; 
  4.         Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left); 
  5.         printf("Panning = L:%d, R:%d\n", left, 254-left); 
  6.  
  7.     if (keys[SDLK_RIGHT]) { 
  8.       if (left > 4) { 
  9.         left -= 4; 
  10.         Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left); 
  11.         printf("Panning = L:%d, R:%d\n", left, 254-left); 
In maniera molto simile a quello che abbiamo visto prima col volume, premendo i tasti freccia sinistra (riga 155-161) o freccia destra (riga 163-169) viene modificato il panning della riproduzione audio.
Alle righe 156 e 164 c'è il controllo per il limite massimo e minimo, alla 157 ed alla 165 c'è l'aggiornamento della variabile che memorizza il valore corrente, alla 158 ed alla 166 ritroviamo nuovamente la Mix_SetPanning() per l'impostazione dell'effetto ed alla 158 e 166 la notifica del cambiamento avvenuto.

Notate come avviene il controllo dei limiti (righe 156 e 164), non vengono controllati rispetto al valore minimo di 0 e quello massimo di 255 ma con quello di 4 e di 250, questo è dovuto all'incremento usato (pari a 4).
Altra cosa è l'utilizzo della Mix_SetPanning() usando come valori per i volumi una variabile indipendente "left" ed una dipendente da questa "254-left", questo ci assicura che la somma tra i volumi dei due canali sia sempre 254, cosa, questa, che garantisce un panning fluido ed omogeneo.

  1.     /* Facciamo respirare la CPU tra un poll ed un altro */ 
  2.     SDL_Delay(50); 
Il ciclo si conclude con una chiamata alla SDL_Delay() con un valore di 50ms. Questo significa che tra un poll degli eventi ed un altro passano sempre minimo 5 centesimi di secondo, dando così la possibilità alla cpu di schedulare altri processi e quindi di andare avanti con altri programmi.
Il problema dell'occupazione del processore causato dal ciclo degli eventi non si esaurisce qui, è un fenomeno non banale, che necessiterebbe, per una sua completa risoluzione, dell'utilizzo della programmazione multithreading (supportata dalle SDL), ma il tema è molto articolato e difficile, ritengo che l'utilizzo della SDL_Delay() sia un compromesso più che sufficiente per il programma analizzato in questa sede.

handleKey() (1 parametro)

  1. /* Gestisce la struttura SDL_KeyboardEvent per rispondere all'input da 

    tastiera */ 
  2. void handleKey(SDL_KeyboardEvent key) 
  3.   switch(key.keysym.sym) { 
La funzione handleKey() riceve come parametro una struttura di tipo SDL_KeyboardEvent e non restituisce alcun valore. Il suo scopo è quello di discriminare l'avvenuta pressione dei diversi tasti e di reagire di conseguenza, tutto ciò avviene mediante il solito costrutto switch ed i singoli case in corrispondenza dei vari tasti premuti.
  1.   case SDLK_c: 
  2.     Mix_SetPanning(MIX_CHANNEL_POST, 127, 127); 
  3.     printf("Panning = Centrale\n"); 
  4.     break; 
Con la pressione del tasto "c" viene ripristinato il panning centrale (riga 191), che poteva essere stato precedentemente disequilibrato dall'utente con i tasti freccia, e viene stampato un messaggio di notifica (riga 192).
  1.   case SDLK_f: 
  2.     if (Mix_PlayingMusic()) 
  3.       Mix_FadeOutMusic(3000); 
  4.     else 
  5.       Mix_FadeInMusic(music, -1, 3000); 
  6.     break; 
In questo pezzo di codice incontriamo nuove funzioni messeci a disposizione dalla SDL_mixer, in particolare quelle relative al fade (dissolvenza) della musica.
Appena viene premuto il tasto "f" da parte dell'utente, il programma controlla se la musica stia effettivamente suonando in quel momento, tramite la Mix_PlayingMusic(), la quale ritorna zero se la musica non sta suonando e uno nel caso opposto.

Se la musica sta suonando, la condizione alla riga 195 è positiva e l'if passa il controllo all'istruzione successiva, la quale esegue un fadeout sulla musica della durata di 3000ms, ovvero l'argomento della funzione.

Nel caso invece che la musica in quel momento non stia suonando, alla riga 198 si riprende a suonarla, indicando alla Mix_FadeInMusic() il puntatore ad una struttura Mix_Music, ottenuta al caricamento della musica, come primo argomento.
Il secondo argomento, invece, è un intero che indica il numero di ripetizioni da eseguire durante la riproduzione, ciò vuol dire che tale numero dev'essere uguale al numero di volte che noi vogliamo che la musica sia riprodotta, meno uno. Se indichiamo il numero 0, è perché vogliamo che la musica sia riprodotta soltanto una volta, e quindi con zero ripetizioni, se invece vogliamo che la riproduzione non finisca mai, indichiamo come argomento il numero -1.
L'ultimo argomento è un intero che specifica il numero di millisecondi di durata del fadein.

  1.   case SDLK_m: 
  2.     if (Mix_PlayingMusic()) 
  3.       Mix_HaltMusic(); 
  4.     else 
  5.       Mix_PlayMusic(music, -1); 
  6.     break; 
Il programma inizialmente non emetterà alcun suono, per far partire la musica è necessario premere "m", e premerlo nuovamente quando invece si vuole fermarla.
Tutto ciò viene ottenuto molto semplicemente utilizzando 3 funzioni, la Mix_PlayingMusic() l'abbiamo già incontrata precedentemente, ed anche in questo caso ci permette di stabilire se la musica sta suonando o meno, in caso affermativo questa viene fermata (riga 202) chiamando la Mix_HaltMusic(), altrimenti (riga 204) la Mix_PlayMusic() la farà partire dall'inizio, passandogli come argomenti il puntatore alla struttura Mix_Music ed il numero di ripetizioni.
  1.   case SDLK_p: 
  2.     if (Mix_PausedMusic()) 
  3.       Mix_ResumeMusic(); 
  4.     else 
  5.       Mix_PauseMusic(); 
  6.     break; 
Premendo il tasto "p" mettiamo in pausa la musica, anche in questo caso, il codice è molto semplice. Controlliamo che la musica non sia già in pausa (riga 207), in caso affermativo riprendiamo a suonare da dove eravamo rimasti (riga 208), altrimenti la mettiamo in pausa (riga 210).

Analizzate alcune delle varie funzioni dedicate alla musica, è tempo di suonare qualche campione, ma prima voglio farvi notare che le funzioni di controllo, di fade, di play, di halt, di pausa e di resume sono disponibili anche per i canali dedicati ai campioni sonori generici, hanno nomi differenti ma vengono utilizzate in maniera molto simile.
Vi consiglio, a riguardo, la lettura del manuale [6] della libreria.

  1.   case SDLK_1: 
  2.   case SDLK_KP1: 
  3.     Mix_PlayChannel(1, sample1, 0); 
  4.     break; 
  5.   case SDLK_2: 
  6.   case SDLK_KP2: 
  7.     Mix_PlayChannel(2, sample2, 0); 
  8.     break; 
  9.   case SDLK_3: 
  10.   case SDLK_KP3: 
  11.     Mix_PlayChannel(3, sample3, 0); 
  12.     break; 
Le ultime righe del codice della handleKey() sono dedicate, come abbiamo detto poco sopra, alla riproduzione dei tre campioni audio caricati nel main(). L'utente potrà scegliere quale dei tre suonare premendo, sia sulla tastiera che sul tastierino numerico, un numero da 1 a 3.
Alla pressione di uno di questi tasti il programma invocherà la Mix_PlayChannel(), la quale ordinerà al thread audio di suonare uno dei tre campioni, dopodiché il controllo sarà passato di nuovo al nostro programma, il quale potrà andare avanti e suonare magari un altro campione (oltre che la musica), mentre la riproduzione del primo scorrerà indipendente.
Il primo argomento della Mix_PlayChannel() è il canale sul quale vogliamo che sia suonato il campione, ho scelto un canale diverso per ognuno di loro, così che tutti possano essere miscelati senza problemi e suonati in contemporanea.
Come secondo argomento indichiamo il puntatore alla struttura Mix_Chunk ottenuta al caricamento dei dati, e come terzo argomento, come succedeva con la Mix_FadeInMusic() e la Mix_PlayMusic(), il numero di loop.

La disamina del programma finisce qui, ma le potenzialità della SDL_mixer continuano, per cui vi consiglio, ancora una volta, di dare uno sguardo al manuale [6] della libreria ed approfondire i concetti riguardanti i campioni audio, gli effetti, la musica, i canali, e di leggere la parte, non trattata in alcun modo qui, sui gruppi di canali.

Compilazione

Per compilare il nostro sorgente è necessario utilizzare come segue il gcc:
gcc -s -o mix mix.c `sdl-config --cflags --libs` -lSDL_mixer
L'opzione -s effettuerà lo strip sull'eseguibile, rimuovendone le parti non utilizzate e rendendolo quindi più piccolo.

L'opzione -o imposterà il nome dell'eseguibile in mix.

`sdl-config --cflags --libs` ci fornirà tutte le opzioni necessarie per includere ed utilizzare la libreria SDL.

-lSDL_mixer linkerà il nostro programma con la libreria SDL_mixer.

Informazioni sull'autore

Angelo Theodorou, studente di Scienze Informatiche presso la Federico II di Napoli. Si occupa principalmente di programmazione ma da sfogo alla sua creatività anche nel campo della grafica 3d. Utente Linux e fautore del software libero, è anche profondo amante della piattaforma Amiga (di cui è stato assiduo utente per 10 anni) e della sua scena demo.
Il sito dove tiene traccia dei suoi progetti: http://autistici.org/encelo/

È possibile consultare l'elenco degli articoli scritti da Angelo Theodorou.

Altri articoli sul tema Giochi / Programmazione / SDL.

Risorse

  1. Sorgenti dell'esempio.
    http://www.siforge.org/articles/2004/11/sdl_mixer/sdl_mixer.zip (16Kb)
  2. Introduzione alla programmazione con SDL.
    http://www.siforge.org/articles/2004/03/01-sdl-intro.html
  3. Seconda introduzione alla programmazione con SDL.
    http://www.siforge.org/articles/2004/03/22-sdl-intro-2.html
  4. Gestire immagini con SDL: SDL_Image.
    http://www.siforge.org/articles/2004/04/19-sdl_image.html
  5. Sito ufficiale del progetto SDL_mixer.
    http://www.libsdl.org/projects/SDL_mixer/
  6. Documentazione di SDL_mixer.
    http://jcatki.no-ip.org/SDL_mixer/
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.