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.
#include <stdlib.h>
#include <SDL/SDL.h>
#include <SDL/SDL_mixer.h>
void handleKey(SDL_KeyboardEvent key);
Mix_Music *music;
Mix_Chunk *sample1, *sample2, *sample3;
int main(int argc, char **argv)
{
SDL_Surface *screen;
SDL_Event event;
Uint8 *keys;
int quit = 0;
int frequency = 44100;
Uint16 format = MIX_DEFAULT_FORMAT; /* Corrisponde a AUDIO_S16SYS */
int channels = MIX_DEFAULT_CHANNELS; /* Stereo */
int buffers = 2048;
Uint8 left = 127;
int volume = MIX_MAX_VOLUME;
char *music_file, *sample1_file, *sample2_file, *sample3_file;
/* Operiamo il parsing dei parametri dati da shell */
if (argc == 1) {
music_file = "music.ogg";
sample1_file = "sample1.ogg";
sample2_file = "sample2.ogg";
sample3_file = "sample3.ogg";
}
else if (argc == 5) {
music_file = argv[1];
sample1_file = argv[2];
sample2_file = argv[3];
sample3_file = argv[4];
}
else {
printf("La sintassi corretta è: %s musica campione1 campione2 campione3\n"
, argv[0]);
exit(-1);
}
/* Inizializzazione del sottosistema audio e video */
if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) < 0) {
printf("Errore init SDL: %s\n", SDL_GetError());
exit (-1);
}
/* All'uscita del programma viene eseguito SDL_Quit per risistemare le cose
*/
atexit(SDL_Quit);
/* Apriamo una finestra per catturare gli eventi dell'utente */
if ((screen = SDL_SetVideoMode(320, 240, 0, 0)) == NULL) {
printf("Errore apertura video: %s\n", SDL_GetError());
exit (-1);
}
/* Apriamo il dispositivo audio */
if (Mix_OpenAudio(frequency, format, channels, buffers) == -1) {
printf("Errore apertura dispositivo audio: %s\n", Mix_GetError());
exit(-1);
}
/* Ridimensioniamo il numero di canali allocati, ce ne bastano solo 4 */
Mix_AllocateChannels(4);
/* Analizziamo cosa ci ha effettivamente dato a disposizione il sistema */
int numtimesopened;
numtimesopened = Mix_QuerySpec(&frequency, &format, &channels);
if (!numtimesopened)
printf("Errore con la Mix_QuerySpec(): %s\n", Mix_GetError());
else {
char *format_str="Sconosciuto";
switch (format) {
case AUDIO_U8:
format_str="U8";
break;
case AUDIO_S8:
format_str="S8";
break;
case AUDIO_U16LSB:
format_str="U16LSB";
break;
case AUDIO_S16LSB:
format_str="S16LSB";
break;
case AUDIO_U16MSB:
format_str="U16MSB";
break;
case AUDIO_S16MSB:
format_str="S16MSB";
break;
}
printf("Il dispositivo è stato aperto %d volte\n", numtimesopened);
printf("Specifiche audio ottenute dal sistema:\n");
printf("Frequenza = %d\n", frequency);
printf("Formato = %s\n", format_str);
printf("Canali = %d\n\n", channels);
}
/* Carichiamo la musica */
if ((music = Mix_LoadMUS(music_file)) == NULL)
printf("Errore caricamento musica : %s\n", Mix_GetError());
/* Carichiamo i campioni audio */
if ((sample1 = Mix_LoadWAV(sample1_file)) == NULL)
printf("Errore caricamento campione 1: %s\n", Mix_GetError());
if ((sample2 = Mix_LoadWAV(sample2_file)) == NULL)
printf("Errore caricamento campione 2: %s\n", Mix_GetError());
if ((sample3 = Mix_LoadWAV(sample3_file)) == NULL)
printf("Errore caricamento campione 3: %s\n", Mix_GetError());
/* Registriamo un effetto di panning centrale */
Mix_SetPanning(MIX_CHANNEL_POST, 127, 127);
/* Ciclo di gestione degli eventi dell'utente */
while (!quit) {
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
quit = 1;
break;
case SDL_KEYDOWN:
handleKey(event.key);
break;
}
if (event.key.keysym.sym == SDLK_ESCAPE || event.key.keysym.sym ==
SDLK_q)
quit = 1;
}
keys = SDL_GetKeyState(NULL);
if (keys[SDLK_UP]) {
if (volume < MIX_MAX_VOLUME) {
volume += 4;
Mix_Volume(-1, volume);
Mix_VolumeMusic(volume);
printf("Volume = %d\n", volume);
}
}
if (keys[SDLK_DOWN]) {
if (volume > 0) {
volume -= 4;
Mix_Volume(-1, volume);
Mix_VolumeMusic(volume);
printf("Volume = %d\n", volume);
}
}
if (keys[SDLK_LEFT]) {
if (left < 250) {
left += 4;
Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left);
printf("Panning = L:%d, R:%d\n", left, 254-left);
}
}
if (keys[SDLK_RIGHT]) {
if (left > 4) {
left -= 4;
Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left);
printf("Panning = L:%d, R:%d\n", left, 254-left);
}
}
/* Facciamo respirare la CPU tra un poll ed un altro */
SDL_Delay(50);
}
/* Liberiamo la memoria allocata per i campioni e la musica e chiudiamo il
dispositivo audio */
Mix_FreeChunk(sample3);
Mix_FreeChunk(sample2);
Mix_FreeChunk(sample1);
Mix_FreeMusic(music);
Mix_CloseAudio();
SDL_Quit();
return 0;
}
/* Gestisce la struttura SDL_KeyboardEvent per rispondere all'input da
tastiera */
void handleKey(SDL_KeyboardEvent key)
{
switch(key.keysym.sym) {
case SDLK_c:
Mix_SetPanning(MIX_CHANNEL_POST, 127, 127);
printf("Panning = Centrale\n");
break;
case SDLK_f:
if (Mix_PlayingMusic())
Mix_FadeOutMusic(3000);
else
Mix_FadeInMusic(music, -1, 3000);
break;
case SDLK_m:
if (Mix_PlayingMusic())
Mix_HaltMusic();
else
Mix_PlayMusic(music, -1);
break;
case SDLK_p:
if (Mix_PausedMusic())
Mix_ResumeMusic();
else
Mix_PauseMusic();
break;
case SDLK_1:
case SDLK_KP1:
Mix_PlayChannel(1, sample1, 0);
break;
case SDLK_2:
case SDLK_KP2:
Mix_PlayChannel(2, sample2, 0);
break;
case SDLK_3:
case SDLK_KP3:
Mix_PlayChannel(3, sample3, 0);
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.
#include <stdlib.h>
#include <SDL/SDL.h>
#include <SDL/SDL_mixer.h>
void handleKey(SDL_KeyboardEvent key);
Mix_Music *music;
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.
int main(int argc, char **argv)
{
SDL_Surface *screen;
SDL_Event event;
Uint8 *keys;
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.
int frequency = 44100;
Uint16 format = MIX_DEFAULT_FORMAT; /* Corrisponde a AUDIO_S16SYS */
int channels = MIX_DEFAULT_CHANNELS; /* Stereo */
int buffers = 2048;
Uint8 left = 127;
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.
char *music_file, *sample1_file, *sample2_file, *sample3_file;
/* Operiamo il parsing dei parametri dati da shell */
if (argc == 1) {
music_file = "music.ogg";
sample1_file = "sample1.ogg";
sample2_file = "sample2.ogg";
sample3_file = "sample3.ogg";
}
else if (argc == 5) {
music_file = argv[1];
sample1_file = argv[2];
sample2_file = argv[3];
sample3_file = argv[4];
}
else {
printf("La sintassi corretta è: %s musica campione1 campione2 campione3\n"
, argv[0]);
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.
/* Inizializzazione del sottosistema audio e video */
if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) < 0) {
printf("Errore init SDL: %s\n", SDL_GetError());
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.
/* All'uscita del programma viene eseguito SDL_Quit per risistemare le cose
*/
atexit(SDL_Quit);
Affidiamo alla SDL_Quit() il ripristino delle condizioni iniziali in caso di uscita con
una exit().
/* Apriamo una finestra per catturare gli eventi dell'utente */
if ((screen = SDL_SetVideoMode(320, 240, 0, 0)) == NULL) {
printf("Errore apertura video: %s\n", SDL_GetError());
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.
/* Apriamo il dispositivo audio */
if (Mix_OpenAudio(frequency, format, channels, buffers) == -1) {
printf("Errore apertura dispositivo audio: %s\n", Mix_GetError());
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.
/* Ridimensioniamo il numero di canali allocati, ce ne bastano solo 4 */
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.
/* Analizziamo cosa ci ha effettivamente dato a disposizione il sistema */
int numtimesopened;
numtimesopened = Mix_QuerySpec(&frequency, &format, &channels);
if (!numtimesopened)
printf("Errore con la Mix_QuerySpec(): %s\n", Mix_GetError());
else {
char *format_str="Sconosciuto";
switch (format) {
case AUDIO_U8:
format_str="U8";
break;
case AUDIO_S8:
format_str="S8";
break;
case AUDIO_U16LSB:
format_str="U16LSB";
break;
case AUDIO_S16LSB:
format_str="S16LSB";
break;
case AUDIO_U16MSB:
format_str="U16MSB";
break;
case AUDIO_S16MSB:
format_str="S16MSB";
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.
printf("Il dispositivo è stato aperto %d volte\n", numtimesopened);
printf("Specifiche audio ottenute dal sistema:\n");
printf("Frequenza = %d\n", frequency);
printf("Formato = %s\n", format_str);
printf("Canali = %d\n\n", channels);
}
Ottenuti i dati che volevamo è tempo di stamparli per renderli disponibili all'utente del programma.
/* Carichiamo la musica */
if ((music = Mix_LoadMUS(music_file)) == NULL)
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à.
/* Carichiamo i campioni audio */
if ((sample1 = Mix_LoadWAV(sample1_file)) == NULL)
printf("Errore caricamento campione 1: %s\n", Mix_GetError());
if ((sample2 = Mix_LoadWAV(sample2_file)) == NULL)
printf("Errore caricamento campione 2: %s\n", Mix_GetError());
if ((sample3 = Mix_LoadWAV(sample3_file)) == NULL)
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.
/* Registriamo un effetto di panning centrale */
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().
/* Liberiamo la memoria allocata per i campioni e la musica e chiudiamo il
dispositivo audio */
Mix_FreeChunk(sample3);
Mix_FreeChunk(sample2);
Mix_FreeChunk(sample1);
Mix_FreeMusic(music);
Mix_CloseAudio();
SDL_Quit();
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
/* Ciclo di gestione degli eventi dell'utente */
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.
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
quit = 1;
break;
case SDL_KEYDOWN:
handleKey(event.key);
break;
}
if (event.key.keysym.sym == SDLK_ESCAPE || event.key.keysym.sym ==
SDLK_q)
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).
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.
if (keys[SDLK_UP]) {
if (volume < MIX_MAX_VOLUME) {
volume += 4;
Mix_Volume(-1, volume);
Mix_VolumeMusic(volume);
printf("Volume = %d\n", volume);
}
}
if (keys[SDLK_DOWN]) {
if (volume > 0) {
volume -= 4;
Mix_Volume(-1, volume);
Mix_VolumeMusic(volume);
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).
if (keys[SDLK_LEFT]) {
if (left < 250) {
left += 4;
Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left);
printf("Panning = L:%d, R:%d\n", left, 254-left);
}
}
if (keys[SDLK_RIGHT]) {
if (left > 4) {
left -= 4;
Mix_SetPanning(MIX_CHANNEL_POST, left, 254-left);
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.
/* Facciamo respirare la CPU tra un poll ed un altro */
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)
/* Gestisce la struttura SDL_KeyboardEvent per rispondere all'input da
tastiera */
void handleKey(SDL_KeyboardEvent key)
{
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.
case SDLK_c:
Mix_SetPanning(MIX_CHANNEL_POST, 127, 127);
printf("Panning = Centrale\n");
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).
case SDLK_f:
if (Mix_PlayingMusic())
Mix_FadeOutMusic(3000);
else
Mix_FadeInMusic(music, -1, 3000);
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.
case SDLK_m:
if (Mix_PlayingMusic())
Mix_HaltMusic();
else
Mix_PlayMusic(music, -1);
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.
case SDLK_p:
if (Mix_PausedMusic())
Mix_ResumeMusic();
else
Mix_PauseMusic();
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.
case SDLK_1:
case SDLK_KP1:
Mix_PlayChannel(1, sample1, 0);
break;
case SDLK_2:
case SDLK_KP2:
Mix_PlayChannel(2, sample2, 0);
break;
case SDLK_3:
case SDLK_KP3:
Mix_PlayChannel(3, sample3, 0);
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.