giovedì 22 dicembre 2011

Windows: riprodurre un'onda sonora tramite l'interfaccia "waveOut"

Per continuare l'argomento intrapreso due settimane fa, ovvero Un semplice programma in C per leggere i file RIFF WAVE, passiamo adesso alla riproduzione di un'onda PCM. Intanto, a differenza del precedente post, dimentichiamoci della struttura del file WAV e di come aprirlo e suonarlo (rimanderò ancora più avanti l'argomento), ma preoccupiamoci di generare un'onda qualsiasi (per semplicità genereremo un'onda sinusoidale), e comunicare in qualche modo con la scheda audio per inviarle dei dati grezzi e quindi, sentire un suono dalle casse del proprio computer.

La piattaforma in cui faremo ciò è Windows, e useremo alcune sue API di basso livello per comunicare con la scheda audio (come al solito, rimanderò più avanti nel caso di Linux). Sebbene utilizzeremo le API di Win32, scordatevi comunque di vedere una finestra bella colorata con tanti tasti e caselle di input: quel che vedremo è una scarna finestra di console che si chiude non appena finisce di riprodurre il suono.

Il linguaggio è sempre il C.
Prima cosa essenziale: creiamoci una bella enumerazione che ci consente di manipolare le caratteristiche dell'onda senza andare a cercare fra il codice:

enum {
TIME = 5, // Durata della traccia (secondi)
CHANNELS = 1,         // Numero dei canali
SAMPLE_RATE = 44100, // Frequenza di campionamento (Hz)
BITS_PER_SAMPLE = 8, // Bits per campione
BLOCK_ALIGN = CHANNELS * BITS_PER_SAMPLE / 8, // Allineamento del blocco
BYTES_PER_SECOND  SAMPLE_RATE * BLOCK_ALIGN, // Frequenza di campionamento in Hz
SAMPLES = CHANNELS * SAMPLE_RATE * TIME, // Numero di campioni richiesti per la durata scelta
};
E' necessario che abbiate letto almeno il precedente post  per capire cosa sia per esempio la frequenza di campionamento o le altre cose (se non lo sapete già).

PASSO 1: APRIRE IL DEVICE

Cominciamo a scrivere la nostra main e a creare le variabili necessarie per aprire il device.



int main() {    
    HWAVEOUT hWaveOut; /* device handle */
    WAVEFORMATEX wfx; /* una struttura che riassume le caratteristiche dell'audio */
    MMRESULT result;/* per il valore di ritorno di waveOut */
    char* block;/* il puntatore ai nostri campioni */
    /*
     * inizializzo la struttura
     */
    wfx.nSamplesPerSec = SAMPLE_RATE;
    wfx.wBitsPerSample = BITS_PER_SAMPLE;
    wfx.nChannels = CHANNELS;
    wfx.nBlockAlign = BLOCK_ALIGN;
    wfx.nAvgBytesPerSec = BYTES_PER_SECOND;
    wfx.cbSize = 0; /* lunghezza delle informazioni extra */
    wfx.wFormatTag = WAVE_FORMAT_PCM  /* onda PCM; */

    if(waveOutOpen(&hWaveOut, WAVE_MAPPER, &wfx, 0,0,CALLBACK_NULL) != MMSYSERR_NOERROR) {
        fprintf(stderr, "Impossibile aprire il device.\n");
        return 1;
    }
    else printf("Il device è stato aperto correttamente.\n");
    waveOutClose(hWaveOut); 
}


Questo programma per ora non fa niente, si limita ad aprire il device e a richiuderlo, ma comunque introduce molte caratteristiche interessanti. La funzione-chiave è waveOutOpen che accetta sei parametri:

LPHWAVEOUT phwo : in questo parametro passato per indirizzo, verrà "depositato" l'handle del dispositivo ((hWaveOut)
UINT uDeviceID : il  valore viene impostato a "WAVE_MAPPER" in modo che la funzione scelga il dispositivo adatto a riprodurre il formato specificato
LPWAVEFORMATEX pwfx : il puntatore alla struttura, ovvero &wfx
DWORD dwCallback : indirizzo dell'eventuale funzione CALLBACK, che in questo contesto non ci serve (più avanti si). Lo impostiamo a 0
DWORD dwInstance : specifica l'istanza del processo. Lo impostiamo a 0
DWORD fdwOpen : specifica alcuni comportamenti della funzione (callback, flags) che per ora ignoreremo. Lo impostiamo a CALLBACK_NULL(nessuna chiamata di callback)

Benissimo, adesso che abbiamo aperto il device, dobbiamo trovare un modo per  comunicare con lui. Ci arriveremo al passo 3, per ora creiamo l'onda.

PASSO 2 : CREIAMO UN'ONDA SINUSOIDALE



    int tone = 440;
    float volume = 10.0f;
    float pulsazione = ( 2.0f * 3.14f / (float) SAMPLE_RATE / CHANNELS ) * (float) tone;
    int i;
    if ((block = (char*)malloc(SAMPLES))==NULL)
       return 1;
    for (i=0 ; i < SAMPLES; i++ ) 
        block[i] = (char) ( cos( (float) i * oscillation ) * volume + 128.0f );

Questo pezzetto di codice alloca la memoria necessaria per memorizzare i campioni e la inizializza con i valori dell'onda.
Sappiamo che questa è l'equazione di un'onda sinusoidale di ampiezza A, pulsazione w e fase f. Possiamo dire lecitamente che l'ampiezza non è altro che il volume(intensità) dell'onda, la fase è nulla (non è altro che una traslazione orizzontale dell'onda  che non ci interessa) e la pulsazione è una funzione della frequenza secondo questa equazione:
dove f questa volta è la frequenza dell'onda. Non ho studiato teoria dei segnali, ma secondo il teorema del campionamento di Nyquist c'è una condizione: la frequenza di campionamento deve essere almeno il doppio della massima frequenza dello spettro dell'onda. Lo spettro acustico che un essere umano percepisce va da 16 Hz a 20000 Hz più o meno, quindi come frequenza di campionamento per memorizzare i campioni viene scelta 44100 Hz, che è diciamo uno standard, utilizzata negli ormai comuni CD Audio. Sempre secondo il teorema del campionamento,  l'equazione esatta dell'onda è questa:
dove n, un numero intero, rappresenta l'indice del campione (da 0 a SAMPLES nel codice sopra), Tc è il tempo che intercorre fra un campione e un altro, ossia l'inverso della frequenza di campionamento, fc è la frequenza di campionamento e f0 è la frequenza del segnale analogico.
In base a tutto ciò si spiega facilmente il codice sopra:  float pulsazione = ( 2.0f * 3.14f / (float) SAMPLE_RATE / CHANNELS ) * (float) tone; definisce la pulsazione costante mentre il ciclo for memorizza campione per campione tramite l'espressione block[i] = (char) ( cos( (float) i * oscillation ) * volume + 128.0f ); .

Da notare che il codice che utilizzo utilizza il puntatore a char* per memorizzare i campioni. char è un byte sostanzialmente, quindi se modifico BITS_PER_SAMPLE nell'enumerazione e metto 16, per esempio come valore, si avrà uno stravolgimento totale. Meglio non modificarlo! In C++ potremmo creare una funzione che memorizza i dati passandogli un tipo tramite un template, in modo da ovviare alla cosa. Ma non preoccupiamoci, almeno per ora.

PASSO 3 : INVIAMO I DATI AL DEVICE



void writeAudioBlock(HWAVEOUT hWaveOut, LPSTR block, DWORD size)
{
    WAVEHDR header;
    /*
     * Inizializza l'intestazione con la lunghezza dei dati
     * e naturalmente il puntatore.
     */
    ZeroMemory(&header, sizeof(WAVEHDR));
    header.dwBufferLength = size;
    header.lpData = block;
    /*
     * crea l'intestazione
     */
    waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
    /*
     * scrive i dati in maniera asincrona
     * (il controllo passerà subito al programma)
     */
    waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR));
    /*
     * Aspetta un po'
     */
    Sleep(500);
    while(waveOutUnprepareHeader(hWaveOut, &header, sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
      Sleep(100);
}

Una bella funzione. Il codice è tutto commentato quindi non ci dovrebbero essere problemi. In soldoni: preparo l'header, cancello tutta la memoria occupata dall'header, inizializzo l'header, creo l'intestazione, scrivo i dati, faccio un ciclo fin quando la riproduzione finisce, e chiudo.
Quindi, in definitiva, ecco il codice completo:

#include <windows.h>
#include <mmsystem.h>
#include <stdio.h>
#include <math.h>
#include <stdint.h>
enum {

        TIME                = 5,         // Durata della traccia (secondi)
        FMT_SIZE            = 16,        // Dimensione dell'Fmt Chunk
        CHANNELS            = 1,         // Numero dei canali
        SAMPLE_RATE            = 44100,   // Frequenza di campionamento (Hz)
        BITS_PER_SAMPLE        = 8,      // Bits per campione

        BLOCK_ALIGN            = CHANNELS * BITS_PER_SAMPLE / 8,    // Allineamento del blocco
        BYTES_PER_SECOND    = SAMPLE_RATE * BLOCK_ALIGN,            // Frequenza di campionamento in Hz

        SAMPLES = CHANNELS * SAMPLE_RATE * TIME,  // Numero di campioni richiesti per la durata scelta
    };
    
    

void writeAudioBlock(HWAVEOUT hWaveOut, LPSTR block, DWORD size)
{
    WAVEHDR header;
    /*
     * Inizializza l'intestazione con la lunghezza dei dati
     * e naturalmente il puntatore.
     */
    ZeroMemory(&header, sizeof(WAVEHDR));
    header.dwBufferLength = size;
    header.lpData = block;
    /*
     * crea l'intestazione
     */
    waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
    /*
     * scrive i dati in maniera asincrona
     * (il controllo passerà subito al programma)
     */
    waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR));
    /*
     * Aspetta un po'
     */
    Sleep(500);
    while(waveOutUnprepareHeader(hWaveOut, &header, sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
      Sleep(100);
}





int main() {
    HWAVEOUT hWaveOut; /* device handle */
    WAVEFORMATEX wfx; /* una struttura che riassume le caratteristiche dell'audio */
    MMRESULT result;/* per il valore di ritorno di waveOut */
    char* block;/* il puntatore ai nostri campioni */
    /*
     * inizializzo la struttura
     */
    wfx.nSamplesPerSec = SAMPLE_RATE;
    wfx.wBitsPerSample = BITS_PER_SAMPLE;
    wfx.nChannels = CHANNELS;
    wfx.nBlockAlign = BLOCK_ALIGN;
    wfx.nAvgBytesPerSec = BYTES_PER_SECOND;
    wfx.cbSize = 0; /* lunghezza delle informazioni extra */
    wfx.wFormatTag = WAVE_FORMAT_PCM  /* onda PCM; */

    if(waveOutOpen(&hWaveOut, WAVE_MAPPER, &wfx, 0,0,CALLBACK_NULL) != MMSYSERR_NOERROR) {
        fprintf(stderr, "Impossibile aprire il device.\n");
        return 1;
    }
    else printf("Il device è stato aperto correttamente.\n");


    int tone = 440;  //"La" centrale
    float volume = 10.0f;
    float pulsazione = ( 2.0f * 3.14f / (float) SAMPLE_RATE / CHANNELS ) * (float) tone;
    int i;
    if ((block = (char*)malloc(SAMPLES))==NULL)
       return 1;
    for (i=0 ; i < SAMPLES; i++ ) 
        block[i] = (char) ( cos( (float) i * oscillation ) * volume + 128.0f );
    }
    writeAudioBlock(hWaveOut, block, SAMPLES); 
    waveOutClose(hWaveOut);
}


Non dimentichiamoci di linkare al compilatore la libreria winmm  altrimenti il linker ci da errore.

Per dubbi, problemi e chiarimenti commentate pure.
Alberto


Siti da cui ho preso informazioni:

Nessun commento:

Posta un commento