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 limiti significativi per l'implementazione di giochi sofisticati e applicazioni interattive.

L'API Web Audio è un'API JavaScript di alto livello per l'elaborazione e la sintesi dell'audio nelle applicazioni web. Lo scopo di questa API è includere le funzionalità presenti nei moderni motori audio di gioco e alcune delle attività di mixing, elaborazione e filtraggio disponibili nelle moderne applicazioni di produzione audio per computer. Di seguito è riportata un'introduzione graduale all'utilizzo di questa potente API.

Iniziare a utilizzare AudioContext

Un AudioContext serve per gestire e riprodurre tutti i suoni. Per produrre un suono utilizzando l'API Web Audio, crea una o più sorgenti sonore e connettile 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 in maggiore dettaglio nella specifica di Web Audio.

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

Il seguente snippet 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 per webkitAudioContext.

Molte delle interessanti funzionalità dell'API Web Audio, come la creazione AudioNode 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 breve e media durata. L'approccio di base consiste nell'utilizzare XMLHttpRequest per recuperare i file audio.

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

Lo snippet seguente mostra il caricamento di un sample 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 responseType della richiesta su 'arraybuffer'. Per ulteriori informazioni su ArrayBuffers, consulta questo articolo su XHR2.

Una volta ricevuti i dati del file audio (non decodificati), possono essere conservati per la decodifica successiva o decodificati immediatamente utilizzando il metodo decodeAudioData() AudioContext. Questo metodo prende il ArrayBuffer dei dati dei file audio archiviati in request.response e li decodifica in modo asincrono (non bloccando il thread di esecuzione JavaScript principale).

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

Riproduzione di suoni

Un grafico audio semplice
Un semplice grafico audio

Una volta caricati uno o più AudioBuffers, possiamo riprodurre i suoni. Supponiamo di aver appena caricato un AudioBuffer con l'audio di un cane che abbaia e che il caricamento sia terminato. Poi possiamo 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) consente di programmare facilmente la riproduzione di audio preciso per giochi e altre applicazioni con tempistiche critiche. Tuttavia, per far sì che questa programmazione funzioni correttamente, assicurati che i buffer audio siano precaricati.

Astrazione dell'API Web Audio

Ovviamente, sarebbe meglio creare un sistema di caricamento più generale che non sia hardcoded per il caricamento di questo suono specifico. Esistono molti approcci per gestire i numerosi suoni di breve e media durata che un'applicazione audio o un gioco userebbero. Ecco un modo per utilizzare BufferLoader (non incluso nello standard web).

Di seguito è riportato un esempio di come utilizzare la classe BufferLoader. Creiamo due AudioBuffers e, non appena vengono caricati, riproduciamoli 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);
}

Gestire il tempo: riprodurre suoni con ritmo

L'API Web Audio consente agli sviluppatori di pianificare con precisione la riproduzione. Per dimostrare questo concetto, creiamo una traccia ritmica semplice. Probabilmente il pattern di drumkit più noto è il seguente:

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

in cui un hi-hat viene suonato ogni ottavo e la cassa e la cassa risonante vengono suonate in alternanza ogni quarto, in tempo 4/4.

Supponendo di aver caricato i buffer kick, snare e hihat, il codice per farlo è 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 anziché il loop illimitato che vediamo nel foglio di musica. 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 potresti voler eseguire su un audio è modificarne il volume. Utilizzando l'API Web Audio, possiamo instradare la sorgente alla destinazione tramite un AudioGainNode per manipolare il volume:

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

Questa configurazione della connessione può essere eseguita nel seguente modo:

// 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 in modo programmatico 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 riproduciamo più suoni, ma vogliamo eseguire la transizione tra di essi. Si tratta di un caso comune in un'applicazione come quella di un DJ, in cui abbiamo due giradischi e vogliamo poter passare da un'origine audio all'altra.

Questo può essere fatto con il seguente grafico audio:

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

Per configurare questa opzione, basta creare due AudioGainNodes e collegare ogni sorgente tramite i nodi, utilizzando una funzione simile a questa:

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 di transizione lineare ingenuo presenta un calo del volume durante il passaggio tra i sample.

Una dissolvenza incrociata lineare
Transizione graduale lineare

Per risolvere il problema, utilizziamo una curva di potenza uguale, in cui le curve di guadagno corrispondenti sono non lineari e si intersecano a un'ampiezza superiore. In questo modo, i cali di volume tra le regioni audio vengono ridotti al minimo, con un risultato migliore per il crossfade tra regioni che potrebbero avere un livello leggermente diverso.

Una dissolvenza incrociata di uguale potenza.
Transizione con dissolvenza incrociata di potenza uguale

Dissolvenza incrociata della playlist

Un'altra applicazione comune del crossfader è per un'applicazione di lettore musicale. Quando cambia un brano, vogliamo attenuare quello corrente e aumentare gradualmente il volume del nuovo brano per evitare una transizione brusca. Per farlo, pianifica un crossfade in futuro. Sebbene potremmo utilizzare setTimeout per eseguire questa programmazione, non è precisa. Con l'API Web Audio, possiamo utilizzare l'interfaccia AudioParam per pianificare valori futuri per parametri come il valore di guadagno di un AudioGainNode.

Di conseguenza, data una playlist, possiamo passare da una traccia all'altra programmando una diminuzione del guadagno per la traccia attualmente in riproduzione e un aumento del guadagno sulla traccia successiva, entrambi leggermente prima del termine della riproduzione della traccia corrente:

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 fornisce un pratico 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 lineari e esponenziali predefinite (come sopra), puoi anche specificare la tua curva di valore tramite un array di valori utilizzando la funzione setValueCurveAtTime.

Applicazione di un semplice effetto filtro a un audio

Un grafico audio con un nodo BiquadFilter
Un grafo audio con un nodo BiquadFilterNode

L'API Web Audio ti consente di incanalare l'audio da un nodo audio a un altro, creando una catena potenzialmente complessa di processori per aggiungere effetti complessi ai tuoi soundform.

Un modo per farlo è posizionare BiquadFilterNode tra la sorgente e la destinazione audio. Questo tipo di nodo audio può applicare una varietà di filtri di ordine ridotto che possono essere utilizzati per creare equalizzatori grafici ed effetti anche più complessi, principalmente relativi alla selezione delle parti dello spettro di frequenza di un suono da enfatizzare e quali assorbire.

I tipi di filtri supportati includono:

  • Filtro passa basso
  • Filtro superamento pass
  • Filtro passa banda
  • Filtro barra delle app in basso
  • Filtro scaffale alto
  • Filtro picco
  • Filtro Notch
  • Filtro Tutti i pass

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 elimina le frequenze elevate. Il punto di interruzione è determinato dal valore di frequenza, mentre il fattore Q è senza unità di misura e determina la forma del grafico. Il guadagno interessa solo alcuni filtri, come i filtri Low-Sheet e Peaking, e non questo filtro passa basso.

Impostiamo un semplice filtro passa basso per estrarre solo le basi da un sample audio:

// 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 di frequenza devono essere modificati per funzionare su una scala logaritmica, poiché l'udito umano funziona sullo stesso principio (ovvero A4 è 440 Hz e A5 è 880 Hz). Per maggiori dettagli, consulta la funzione FilterSample.changeFrequency nel link al codice sorgente riportato sopra.

Infine, tieni presente che il codice di esempio ti consente di collegare e scollegare il filtro, modificando dinamicamente il grafico AudioContext. Possiamo scollegare AudioNodes dal grafico chiamando node.disconnect(outputNumber). Ad esempio, per reindirizzare il grafico da un filtro a un collegamento diretto, possiamo procedere nel seguente modo:

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

Maggiore ascolto

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

Se sei in cerca di ispirazione, molti sviluppatori hanno già creato un ottimo lavoro utilizzando l'API Web Audio. Ecco alcuni dei miei preferiti:

  • AudioJedit, uno strumento di splicing audio in-browser che utilizza i permalink di SoundCloud.
  • ToneCraft, un sequenziatore audio in cui i suoni vengono creati impilando blocchi 3D.
  • Plink, un gioco di creazione musicale collaborativo che utilizza Web Audio e WebSocket.