Questo tutorial spiega come integrare e usare le librerie SDL (Simple Direct media Layer) audio con CodeBlocks in ambiente Windows XP/2000/Vista.

INSTALLAZIONE

Scarichiamo l'ultima versione delle librerie da http://www.libsdl.org/ e scompattiamo in una cartella qualunque,ad esempio

C:\SDL

Specifichiamo questo percorso all'atto della creazione del nuovo progetto C++ SDL.

IL PRIMO PROGRAMMA

In questo mini-tutorial vedremo come suonare un file audio WAV con e senza l'uso della tecnica del buffering.

Prima di tutto dobbiamo eliminare il file risorsa cb.bmp,che CodeBlocks crea per default per visualizzare una bitmap usando le API 2D di SDL;poi cancelliamo il contenuto del metodo main nel file main.cpp.

La prima cosa da fare è includere la libreria per i visualizzare i messaggi su std output

#include <iostream>
using namespace std;
fatto questo possiamo creare lo scheletro della nostra applicazione,dapprima inizializziamo il sottosistema audio
if(SDL_Init(SDL_INIT_AUDIO))
{
    cout << "Errore di inizializzazione" << endl;
    return -1;
}
    cout << "Inizializzazione avvenuta con successo" << endl;
Compiliamo ed eseguiamo,se compare il messaggio "Inizializzazione avvenuta con successo" possiamo proseguire.

RIPRODUZIONE DI UN FILE AUDIO WAV

Per riprodurre un file audio WAV dobbiamo definire una variabile di tipo SDL_AudioSpec,che altro non è che un descrittore del file WAV che andremo a riprodurre
SDL_AudioSpec fileWav;
Definiamo poi due variabili globali (mi raccomando è importante che siano fuori dalla funzione main,poi capiremo perchè)
Uint8*  samples;
Uint32  samplesLength;
A questo punto chiamiamo la funzione SDL_LoadWAV,passandole il percorso completo del file, il puntatore al descrittore del file,il puntatore al buffer e il puntatore alla lunghezza del buffer stesso.Per esempio se nella cartella del programma vogliamo riprodurre il file test.wav avremo
if( SDL_LoadWAV("test.wav", &fileWav, &samples, &samplesLength) == NULL )
{
    cout << "Errore nel caricamento del file" << endl;
    return -2;
}
    cout << "Caricamento del file completato con successo" << endl;
Compiliamo ed eseguiamo,se compare il messaggio "Caricamento del file completato con successo" significa che il programma ha riconosciuto il formato del file e ha inizializzato correttamente le tre variabili che gli abbiamo passato.

Dobbiamo ancora definire una cosa:la funzione di callback che verrà chiamata per riprodurre il file.Possiamo chiamare questa funzione come vogliamo,purchè rispetti la signature
void SDLCALL /*nome_funz*/(void*, Uint8*, int);
La funzione riceve come secondo parametro il buffer che conterrà i campioni da riprodurre e come terzo la lunghezza del buffer da riprodurre.
Supponendo di chiamarla PlayWav,specifichiamo che quella sarà la funzione da chiamare per riprodurre il nostro file
fileWav.callback = PlayWav;
A questo punto dobbiamo predisporre la nostra scheda audio per riprodurre il file desiderato,lo facciamo passandole il puntatore al descrittore del file (in questo tutorial di base tralasciamo il secondo parametro e impostiamolo a NULL)
if( SDL_OpenAudio(&fileWav,NULL) )
{
    cout << "Errore di inizializzazione della periferica audio" << endl;
    return -3;
}
    cout << "Inizializzazione della periferica audio avvenuta con successo" << endl;
Compiliamo ed eseguiamo,se compare il messaggio "Inizializzazione della periferica audio avvenuta con successo" tutto è andato per il verso giusto e non c'e' più niente che ci possa impedire di riprodurre il nostro file audio! ^^

LA FUNZIONE DI CALLBACK

Prima di continuare a scrivere codice è utile fare un passo indietro per capire come un PC riproduce dati multimediali.

La riproduzione di un flusso multimediale è un processo che viene eseguito in background, vale a dire che durante la riproduzione il sistema deve essere lasciato "libero" di eseguire altre operazioni importanti (ad esempio la scansione della tastiera,del mouse, il refresh dell'orologio di sistema ecc...).

Se per i suoni che durano pochi istanti la cosa può non sembrare così importante,pensiamo a cosa succederebbe se il sistema rimanesse totalmente bloccato per 3-4 minuti perchè si sta facendo streaming di una canzone o di un video musicale...

Dopo aver caricato il file in memoria,il sistema lo divide in un certo numero di "pezzi"; la cpu passa l'indirizzo di memoria e la lunghezza del primo pezzo ad uno speciale dispositivo detto controllore di DMA.

Il controllore di DMA copia la prima porzione del file in una speciale area di memoria che può essere processata dal dispositivo di output;nel frattempo la cpu può continuare ad eseguire altre operazioni senza rischiare di essere saturata.

Terminata la riproduzione del primo pezzo di file,il controllore di DMA invia un segnale di notifica alla cpu;la cpu passa al controllore di DMA il puntatore al pezzo successivo,e così via fino a che non è stato riprodotto l'intero file.

Nel nostro codice,ogni volta che viene chiamata la funzione di callback significa che è appena terminata la riproduzione di una certa porzione del file e che il controllore di DMA è pronto a ricevere il blocco successivo.

In base a quanto ci siamo detti,per riprodurre il file dobbiamo sapere:

1) dove si trova il file caricato in memoria
2) dove comincia il blocco da dare in pasto al controllore di DMA
3) quanto è lungo il blocco da dare in pasto al controllore di DMA
4) dove si trova l'area di memoria in cui scrive il controllore di DMA
5) come passare tutte le informazioni precedenti al controllore di DMA

Se avete seguito con attenzione,il file WAV in memoria si trova all'indirizzo puntato da samples;ovviamente il primo blocco viene caricato dall'offset 0,pertanto possiamo introdurre una variabile statica che alla prima chiamata punti all'indirizzo di partenza del file
static Uint8* pos = samples;
il terzo parametro ricevuto dalla funzione di callback indica quanto è lungo il blocco da passare al controllore di DMA,mentre il secondo parametro indica dove il controllore di DMA andrà a copiare il blocco.Allora,per consentire al controllore di copiare il blocco basterà chiamare la funzione
SDL_memcpy(toDma,pos,buffLen);
supposti toDma e buffLen il nome del secondo e del terzo parametro della funzione PlayWav.

Per determinare l'indirizzo di partenza del blocco n+1 (n>0) basta sommare all'indirizzo del blocco n la lunghezza del blocco,in altre parole basta incrementare il puntatore al blocco n di buffLen posizioni
pos += buffLen;
Manca ancora qualcosa,perchè il programma deve aver modo di sapere dove termina il file; è molto semplice,basta sottrarre alla lunghezza del file il numero di bytes già processati
samplesLength -= buffLen;
e nella funzione main possiamo introdurre un ciclo while di controllo che termina non appena il file è stato riprodotto completamente
while(samplesLength>0)
SDL_Delay(300);
La funzione SDL_Delay consente alla cpu di eseguire altri processi durante la riproduzione del file.

Finito???Non proprio

L'ultimo blocco da riprodurre potrebbe contenere meno dati del valore buffLen,quindi dovremmo gestire anche questo caso.

Ci sono molti modi di gestire questo caso,il più semplice e meno elegante è modificare il codice nella funzione PlayWav in questo modo:
static Uint8* pos = samples;

if(samplesLength>buffLen)
{
    SDL_memcpy(toDma,pos,buffLen);
    samplesLength -= buffLen;
    pos += buffLen;
}
else
{
    SDL_memcpy(toDma,pos,samplesLength);
    samplesLength = 0;
}
Niente di speciale:se mancano meno dati di quanti ce ne ha richiesti il controllore,gli facciamo copiare solo quelli rimasti.

Adesso possiamo finalmente testare il nostro semplice riproduttore di file WAV! Compiliamo,eseguiamo e...NO,non suona

Ma certo,che sbadati:non abbiamo attivato la periferica audio...aggiungiamo prima del ciclo while questa istruzione
SDL_PauseAudio(0);
E il gioco è fatto

CARICAMENTO BUFFERIZZATO (BUFFERING)

Quando si trattano file multimediali bisogna cercare di ridurre al minimo le latenze e questo vale anche durante la fase di caricamento di un file in memoria.

Questo è particolarmente vero quando si ha a che fare con file molto lunghi (decine o addirittura centinaia di megabyte) o che devono essere riprodotti istantaneamente, ad esempio file collegati agli effetti sonori di un gioco.

In una situazione del genere è impensabile attendere il caricamento del file in memoria prima di iniziare la riproduzione,allora quel che si fa è ricorrere alla tecnica del buffering,o caricamento bufferizzato.

L'idea è molto semplice:si carica una piccola porzione di file alla volta,esattamente come fa il controllore di DMA per riprodurre il file in background.

Un modo semplice per implementare questa tecnica consiste nel passare al controllore di DMA il blocco,attendere che il controllore lo copi e quindi caricare il blocco successivo.

Bisogna fare attenzione alla dimensione del buffer da passare al controllore;se è troppo esigua,la cpu rischia di dover leggere troppo di frequente dal file e il sistema potrebbe subire rallentamenti.
D'altro canto,non deve essere nemmeno troppo grande,altrimenti la latenza iniziale rischia di essere troppo alta,in altre parole la riproduzione del file parte "troppo tardi".

Siccome non esiste una funzione SDL che consenta il buffering dobbiamo ingegnarci noi con quello che abbiamo a disposizione.

Creiamo un nuovo progetto SDL,cancelliamo il file risorsa e il codice della funzione main e copiamo dal progetto precedente queste righe di codice
if(SDL_Init(SDL_INIT_AUDIO))
{
    cout << "Errore di inizializzazione" << endl;
    return -1;
}
    cout << "Inizializzazione avvenuta con successo" << endl;

SDL_AudioSpec fileWav;
Notate che non possiamo più utilizzare SDL_LoadWAV perchè questa funzione carica tutto il file in una sola volta,cosa che noi NON vogliamo assolutamente.

Per poter riprodurre correttamente il file,la nostra scheda audio necessita di cinque parametri:

1) numero di canali codificati nel file
2) formato dei campioni
3) frequenza di campionamento
4) numero di campioni da passare al controllore di DMA
5) funzione di callback

L'ultimo parametro possiamo sceglierlo noi,gli altri quattro dobbiamo determinarli leggendo il file WAV (in fondo,è proprio questo quello che fa la funzione SDL_LoadWAV .

Per prima cosa includiamo la librerie e ,necessarie per gestire il file e i messaggi d'errore
#include <iostream>
#include <cstdio>
using namespace std;

#define BUFFER_SIZE = 8192; //è la dimensione del buffer che contiene i campioni
apriamo il file
FILE* wavFile = fopen("test.wav","rb");
Definiamo globalmente il buffer che andrà passato al controllore di DMA e leggiamo l'intestazione del file (per un file PCM sono sempre i primi 44 bytes)
unsigned char tempBuff[BUFFER_SIZE]; //va definito all'esterno della funzione main
fread( tempBuff, 1, 44, wavFile );
PICCOLA NOTA:abbiamo scelto come dimensione del buffer 8192 bytes ma ovviamente può essere variata secondo le proprie esigenze,purchè sia pari a una potenza di 2 (in questo tutorial, il motivo non ci interessa).

Per semplicità verifichiamo che si tratti di un file PCM,in altre parole non compresso;in caso contrario restituiamo un messaggio d'errore
if( tempBuff[20] != 1 || tempBuff[21] != 0 )
{
    cout << "Errore,formato del file non supportato." << endl;
    return -2;
}
Leggiamo anche il numero di canali e la frequenza di campionamento del file
fileWav.channels = tempBuff[22] | (tempBuff[23] << 8);
fileWav.freq     = tempBuff[24] |
                  (tempBuff[25] << 8) |
                  (tempBuff[26] << 16) |
                  (tempBuff[27] << 24);
Il formato dei campioni indica come verranno convertiti dal DAC del dispositivo audio, serve un piccolo check perchè ne possiamo avere di diversi tipi secondo il numero di canali e il numero di byte per campione
if(fileVaw.channels == 2)
    fileWav.format = AUDIO_S16SYS;
else
{
    if(tempBuff[35] == 8)
        fileWav.format = AUDIO_U8;
    else
        fileWav.format = AUDIO_S16SYS;
}
Il numero di campioni da passare al controllore di DMA sarà uguale alla dimensione del buffer diviso il numero di canali diviso il numero di byte per campione
int Bps;

if(fileWav.format == AUDIO_S16SYS)
    Bps = 2;
else
    Bps = 1;

fileWav.samples = BUFFER_SIZE/fileWav.channels/Bps;
Ottimo,adesso possiamo definire la funzione di callback e inizializzare il dispositivo audio con le informazioni raccolte,copiamo paro paro il codice del progetto precedente
fileWav.callback = PlayWav;

if( SDL_OpenAudio(&fileWav,NULL) )
{
    cout << "Errore di inizializzazione della periferica audio" << endl;
    return -3;
}
    cout << "Inizializzazione della periferica audio avvenuta con successo" << endl;

SDL_PauseAudio(0);
Ora arriva la parte "difficile":sincronizzare la cpu e il controllore.

Per poter caricare in memoria il blocco successivo,la cpu deve sapere quando il controllore di DMA ha terminato di copiare un blocco.Possiamo definire una variabile globale che funga da semaforo
bool done = true;
quando la variabile vale true,la cpu può caricare in memoria un nuovo blocco;quando vale false il controllore di DMA può copiare il blocco caricato dalla cpu.

La cpu non dovrà far altro che attendere che il controllore di DMA copi il blocco caricato; fatto questo,potrà caricare il blocco successivo
do{

    load = fread( tempBuff, 1, BUFFER_SIZE, wavFile );

    done = false;

    while(!done)
        SDL_Delay(100);

}while(load == BUFFER_SIZE);
La variabile load è definita a livello globale e permette alla funzione main di capire

1) quando è stato caricato l'ultimo blocco del file
2) quanto è lungo il blocco da passare al controllore

Stavolta la funzione di callback è molto più semplice di prima perchè non deve far altro che passare al controllore il puntatore al blocco (sempre tempBuff) e la lunghezza del blocco (sempre il valore load).
SDL_memcpy( toDma, tempBuff, load );
Una volta copiato il blocco,lo facciamo presente al programma principale
done = true;
E' tutto,possiamo compilare ed eseguire

Per renderci conto della differenza con o senza buffering,proviamo a riprodurre un file WAV un pò lungo,per esempio una canzone di qualche minuto;usando il primo programma si noterà una certa latenza iniziale,dovuta al tempo necessario per caricare l'intero file in memoria.Usando il secondo programma,invece,il file viene riprodotto in maniera pressochè istantanea appunto perchè la cpu passa al controllore di DMA le porzioni del file man mano che le legge dal disco.

Potrebbe essere necessario fare un pò di esperimenti prima di trovare l'assetto ideale.Se la riproduzione procede a scatti (magari perchè la bitrate del file è troppo alta) provate ad aumentare la dimensione del buffer o a diminuire il delay della cpu,magari adattandoli a runtime.

Enjoy!!!

Indietro