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
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:
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: