Introduzione all'API Web Audio

Boris Smus
Boris Smus

Prima dell'elemento <audio> HTML5, era necessario Flash o un altro plug-in per rompere il silenzio del web. Sebbene l'audio sul web non richieda più un plug-in, il tag audio presenta limitazioni significative per l'implementazione di giochi sofisticati e applicazioni interattive.

L'API Web Audio è un'API JavaScript di alto livello per l'elaborazione e la sintetizzazione dell'audio nelle applicazioni web. L'obiettivo di questa API è includere le funzionalità presenti nei moderni motori di gioco audio e alcune attività di missaggio, elaborazione e filtro presenti nelle moderne applicazioni di produzione audio per desktop. Di seguito trovi un'introduzione gentile all'uso di questa potente API.

Introduzione ad AudioContext

Un AudioContext consente di gestire e riprodurre tutti i suoni. Per produrre un suono utilizzando l'API Web Audio, crea una o più sorgenti audio e collegale alla destinazione audio fornita dall'istanza AudioContext. Questa connessione non deve essere diretta e può passare attraverso un numero qualsiasi di AudioNodes intermedi che fungono da moduli di elaborazione per il segnale audio. Questo routing è descritto più dettagliatamente nella specifica dell'audio web.

Una singola istanza di AudioContext può supportare più ingressi audio e grafici audio complessi, quindi ne avremo bisogno solo per ogni applicazione audio creata.

Lo snippet seguente crea un AudioContext:

var context;
window.addEventListener('load', init, false);
function init() {
    try {
    context = new AudioContext();
    }
    catch(e) {
    alert('Web Audio API is not supported in this browser');
    }
}

Per i browser basati su WebKit meno recenti, utilizza il prefisso webkit, come webkitAudioContext.

Molte delle interessanti funzionalità dell'API Web Audio, come la creazione di AudioNodes e la decodifica dei dati dei file audio, sono metodi di AudioContext.

Caricamento suoni in corso...

L'API Web Audio utilizza un AudioBuffer per i suoni di durata breve e media. L'approccio di base consiste nell'utilizzare XMLHttpRequest per il recupero dei file audio.

L'API supporta il caricamento di dati di file audio in più formati, ad esempio WAV, MP3, AAC, OGG e altri. Il supporto dei browser per diversi formati audio varia.

Lo snippet seguente mostra il caricamento di un esempio audio:

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    // Decode asynchronously
    request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer = buffer;
    }, onError);
    }
    request.send();
}

I dati del file audio sono binari (non di testo), quindi impostiamo il responseType della richiesta su 'arraybuffer'. Per maggiori informazioni su ArrayBuffers, consulta questo articolo su XHR2.

Una volta ricevuti, i dati del file audio (non decodificati) possono essere tenuti a disposizione per una successiva decodifica oppure possono essere decodificati immediatamente utilizzando il metodo decodeAudioData() AudioContext. Questo metodo prende l'ArrayBuffer di dati dei file audio archiviati in request.response e li decodifica in modo asincrono (senza bloccare il thread di esecuzione JavaScript principale).

Al termine, decodeAudioData() chiama una funzione di callback che fornisce i dati audio PCM decodificati come AudioBuffer.

Riproduzione di suoni in corso...

Un semplice grafico audio
Un semplice grafico audio

Una volta caricati uno o più AudioBuffers, potrai iniziare a riprodurre i suoni. Supponiamo di aver appena caricato un AudioBuffer con il suono di un cane che abbaia e che il caricamento sia terminato. Possiamo quindi riprodurre questo buffer con il seguente codice.

var context = new AudioContext();

function playSound(buffer) {
    var source = context.createBufferSource(); // creates a sound source
    source.buffer = buffer;                    // tell the source which sound to play
    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source.noteOn(0);                          // play the source now
}

Questa funzione playSound() può essere chiamata ogni volta che qualcuno preme un tasto o fa clic su qualcosa con il mouse.

La funzione noteOn(time) semplifica la pianificazione di una riproduzione audio precisa per giochi e altre applicazioni critiche. Tuttavia, per far sì che questa pianificazione funzioni correttamente, assicurati che i buffer audio siano precaricati.

Astrattismo dell'API Web Audio

Ovviamente sarebbe meglio creare un sistema di caricamento più generale, che non sia hardcoded in modo da caricare l'audio in questione. Esistono diversi approcci per gestire i numerosi suoni di breve e media durata che un'applicazione o un gioco audio potrebbe utilizzare. Ecco un modo per utilizzare BufferLoader (non fa parte dello standard web).

Di seguito è riportato un esempio di come puoi utilizzare la classe BufferLoader. Creiamo due AudioBuffers e, appena vengono caricati, rieseguiamo la riproduzione contemporaneamente.

window.onload = init;
var context;
var bufferLoader;

function init() {
    context = new AudioContext();

    bufferLoader = new BufferLoader(
    context,
    [
        '../sounds/hyper-reality/br-jam-loop.wav',
        '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

    bufferLoader.load();
}

function finishedLoading(bufferList) {
    // Create two sources and play them both together.
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource();
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];

    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.noteOn(0);
    source2.noteOn(0);
}

Affrontare il tempo: riprodurre i suoni a ritmo

L'API Web Audio consente agli sviluppatori di programmare con precisione la riproduzione. Per dimostrarlo, impostiamo una semplice traccia ritmica. Probabilmente il pattern di batteria più noto è il seguente:

Un semplice pattern di batteria rock
Un semplice pattern di batteria rock

in cui si suona un hihat ogni ottava, mentre calcio e rullante vengono suonati alternandosi ogni quarto, in un tempo di 4/4.

Supponendo di aver caricato i buffer kick, snare e hihat, il codice per eseguire questa operazione è semplice:

for (var bar = 0; bar < 2; bar++) {
    var time = startTime + bar * 8 * eighthNoteTime;
    // Play the bass (kick) drum on beats 1, 5
    playSound(kick, time);
    playSound(kick, time + 4 * eighthNoteTime);

    // Play the snare drum on beats 3, 7
    playSound(snare, time + 2 * eighthNoteTime);
    playSound(snare, time + 6 * eighthNoteTime);

    // Play the hi-hat every eighth note.
    for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
    }
}

Qui facciamo una sola ripetizione invece del loop illimitato che si vede nello spartito. La funzione playSound è un metodo che riproduce un buffer in un momento specificato, come segue:

function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.noteOn(time);
}

Regolare il volume di un suono

Una delle operazioni di base che si potrebbe eseguire per un suono è regolarne il volume. Utilizzando l'API Web Audio, possiamo instradare la sorgente alla sua destinazione tramite un AudioGainNode, in modo da manipolare il volume:

Grafico audio con un nodo di guadagno
Grafico audio con un nodo di guadagno

Questa configurazione della connessione può essere effettuata come segue:

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

Dopo aver configurato il grafico, puoi modificare il volume a livello di programmazione manipolando gainNode.gain.value come segue:

// Reduce the volume.
gainNode.gain.value = 0.5;

Dissolvenza incrociata tra due suoni

Ora, supponiamo di avere uno scenario leggermente più complesso, in cui eproduciamo più suoni, ma vogliamo applicare una dissolvenza incrociata. Si tratta di un caso comune in un'applicazione simile a DJ, in cui abbiamo due giradischi e vogliamo essere in grado di eseguire la panoramica da una sorgente all'altra.

Per farlo, usa il seguente grafico audio:

Grafico audio con due origini collegate tramite nodi di guadagno
Grafico audio con due origini collegate tramite nodi di guadagno

Per configurarlo, creiamo semplicemente due AudioGainNodes e connettiamo ogni origine tramite i nodi utilizzando una funzione simile alla seguente:

function createSource(buffer) {
    var source = context.createBufferSource();
    // Create a gain node.
    var gainNode = context.createGainNode();
    source.buffer = buffer;
    // Turn on looping.
    source.loop = true;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
    source: source,
    gainNode: gainNode
    };
}

Dissolvenza incrociata a potenza uguale

Un approccio ingenuo alla dissolvenza incrociata lineare mostra un calo di volume durante la panoramica tra i campioni.

Una dissolvenza incrociata lineare
Una dissolvenza incrociata lineare

Per risolvere questo problema, utilizziamo una curva di potenza uguale, in cui le curve di guadagno corrispondenti non sono lineari e si intersecano con un'ampiezza maggiore. Questo riduce al minimo i cali di volume tra le regioni audio, con una conseguente dissolvenza incrociata più uniforme tra le regioni il cui livello potrebbe essere leggermente diverso.

Una dissolvenza incrociata di uguale potenza.
Una dissolvenza incrociata di uguale potenza

Dissolvenza incrociata delle playlist

Un'altra applicazione crossfader comune è quella di un lettore musicale. Quando un brano cambia, vogliamo eliminare la traccia corrente e aggiungere una dissolvenza a quella nuova in entrata, per evitare una transizione fastidiosa. Per farlo, pianifica una dissolvenza incrociata futura. Anche se potremmo utilizzare setTimeout per questa pianificazione, questa programmazione non è precisa. Con l'API Web Audio, possiamo utilizzare l'interfaccia AudioParam per pianificare valori futuri di parametri come il valore di guadagno di AudioGainNode.

Di conseguenza, data una playlist, possiamo passare da una traccia all'altra programmando una diminuzione della traccia attualmente in riproduzione e un aumento del guadagno su quella successiva, entrambi leggermente prima della fine della riproduzione della traccia attuale:

function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
    // Fade the playNow track in.
    gainNode.gain.linearRampToValueAtTime(0, currTime);
    gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
    // Play the playNow track.
    source.noteOn(0);
    // At the end of the track, fade it out.
    gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
    // Schedule a recursive track change with the tracks swapped.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) - 1000);
}

L'API Web Audio offre un comodo insieme di metodi RampToValue per modificare gradualmente il valore di un parametro, ad esempio linearRampToValueAtTime e exponentialRampToValueAtTime.

Sebbene la funzione di temporizzazione della transizione possa essere scelta tra quelle integrate lineari ed esponenziali (come sopra), puoi anche specificare la tua curva di valore tramite un array di valori utilizzando la funzione setValueCurveAtTime.

Applicare un semplice effetto di filtro a un suono

Un grafico audio con un BiquadFilterNode
Un grafico audio con un BiquadFilterNode

L'API Web Audio consente di trasferire il suono da un nodo audio a un altro, creando una catena potenzialmente complessa di processori per aggiungere effetti complessi alle forme sonore.

Un modo per farlo è posizionare BiquadFilterNode tra l'origine e la destinazione audio. Questo tipo di nodo audio può disporre di una varietà di filtri di ordine basso che possono essere utilizzati per creare equalizzatori grafici ed effetti ancora più complessi, principalmente per selezionare le parti dello spettro di frequenza di un suono da enfatizzare e quali abbassare.

I tipi di filtri supportati includono:

  • Filtro passa basso
  • Filtro passa alto
  • Filtro Passa banda
  • Filtro scaffale basso
  • Filtro scaffale alto
  • Filtro Peaking
  • Filtro tacca
  • Filtro di tutte le tessere

Inoltre, tutti i filtri includono parametri per specificare una certa quantità di guadagno, la frequenza con cui applicare il filtro e un fattore di qualità. Il filtro passa-basso mantiene l'intervallo di frequenza più basso, ma scarta le frequenze alte. Il punto di interruzione è determinato dal valore della frequenza e il fattore Q è senza unità e determina la forma del grafico. Il guadagno interessa solo alcuni filtri, come quelli di fascia bassa e di picco, e non su questo filtro passa-basso.

Configuriamo un semplice filtro passa-basso per estrarre solo le basi da un campione sonoro:

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);

In generale, i controlli della frequenza devono essere regolati per funzionare su una scala logaritmica, poiché l'udito umano stesso funziona sullo stesso principio (ovvero, A4 è 440 Hz e A5 è 880 Hz). Per maggiori dettagli, consulta la funzione FilterSample.changeFrequency nel link del codice sorgente sopra.

Infine, tieni presente che il codice campione consente di collegare e scollegare il filtro, modificando in modo dinamico il grafico AudioContext. Possiamo scollegare AudioNodes dal grafico chiamando node.disconnect(outputNumber). Ad esempio, per reindirizzare il grafico da un filtro a una connessione diretta, possiamo fare quanto segue:

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

Ascolto ulteriore

Abbiamo esaminato le nozioni di base dell'API, inclusi il caricamento e la riproduzione di esempi audio. Abbiamo creato grafici audio con nodi e filtri di guadagno, suoni programmati e modifiche ai parametri audio per abilitare alcuni effetti sonori comuni. A questo punto, sei pronto per creare delle dolci applicazioni audio web.

Se cerchi ispirazione, molti sviluppatori hanno già realizzato ottimo lavoro utilizzando l'API Web Audio. Ecco alcuni dei miei preferiti:

  • AudioJedit, uno strumento di giunzione del suono integrato nel browser che utilizza i permalink di SoundCloud.
  • ToneCraft, un sequenziatore di suoni in cui i suoni vengono creati impilando blocchi 3D.
  • Plink, un gioco collaborativo per la creazione di musica che utilizza Web Audio e Web Socket.