Sviluppare l'audio del gioco con l'API Web Audio

Boris Smus
Boris Smus

Introduzione

L'audio è una parte fondamentale di ciò che rende le esperienze multimediali così accattivanti. Se ti è già capitato di provare a guardare un film con l'audio disattivato, probabilmente l'avrai notato.

I giochi non fanno eccezione. I miei ricordi più cari ai videogiochi sono la musica e gli effetti sonori. In molti casi, quasi due decenni dopo aver giocato ai miei preferiti, non riesco ancora a togliermi dalla testa le composizioni Zelda di Koji Kondo e la colonna sonora atmosferica Diablo di Matt Uelmen. Lo stesso effetto accattivante vale per gli effetti sonori, come le risposte al clic dell'unità immediatamente riconoscibili in Warcraft e i Sample dei classici Nintendo.

L'audio del gioco presenta alcune sfide interessanti. Per creare musica di gioco convincente, i designer devono adattarsi allo stato del gioco potenzialmente imprevedibile in cui si trovano i giocatori. In pratica, alcune parti del gioco possono protrarsi per una durata sconosciuta, i suoni possono interagire con l'ambiente e mixare in modi complessi, ad esempio con gli effetti della stanza e il relativo posizionamento sonoro. Infine, ci può essere un gran numero di suoni riprodotti contemporaneamente, tutti devono avere un audio di qualità insieme ed essere visualizzato senza introdurre conseguenze negative in termini di prestazioni.

Game Audio sul web

Per i giochi semplici, potrebbe essere sufficiente utilizzare il tag <audio>. Tuttavia, molti browser forniscono implementazioni di scarsa qualità, che causano glitch audio ed elevata latenza. Si spera che si tratti di un problema temporaneo, dal momento che i fornitori stanno lavorando sodo per migliorare le rispettive implementazioni. Per una panoramica dello stato del tag <audio>, è disponibile una suite di test all'indirizzo areweplayingyet.org.

Analizzando in dettaglio la specifica del tag <audio>, tuttavia, diventa chiaro che ci sono molte cose che non si possono fare, il che non sorprende, dal momento che è stato progettato per la riproduzione di contenuti multimediali. Alcune limitazioni includono:

  • Nessuna possibilità di applicare filtri al segnale sonoro
  • Nessun modo per accedere ai dati PCM non elaborati
  • Nessun concetto di posizione e direzione delle fonti e degli ascoltatori
  • Nessuna tempistica granulare.

Nel resto dell'articolo, affronteremo alcuni di questi argomenti nel contesto dell'audio di un gioco scritto con l'API Web Audio. Per una breve introduzione a questa API, dai un'occhiata al tutorial per iniziare.

Musica di sottofondo

I giochi prevedono spesso la riproduzione di musica di sottofondo in loop.

Può diventare molto fastidioso se il loop è breve e prevedibile. Se un player è bloccato in un'area o in un livello e lo stesso campione viene riprodotto continuamente in background, può essere utile rimuovere gradualmente la traccia per evitare ulteriore frustrazione. Un'altra strategia consiste nell'avere combinazioni di varie intensità che si dissolveno gradualmente l'una nell'altra, a seconda del contesto del gioco.

Ad esempio, se il tuo giocatore si trova in una zona in cui combatte un'epica battaglia contro i boss, potresti avere diversi mix con diversi livelli emotivi, da quelli suggestivi a quelli più prepotenti o intensi. Un software di sintesi musicale consente spesso di esportare diversi mix (della stessa lunghezza) in base a un brano, scegliendo l'insieme di tracce da utilizzare nell'esportazione. In questo modo avrai una certa coerenza interna ed eviterai transizioni fastidiose mentre passi da una traccia all'altra.

Banda di garage

Quindi, utilizzando l'API Web Audio, puoi importare tutti questi esempi utilizzando qualcosa come la classe BufferLoader tramite XHR (questa procedura è descritta in dettaglio nell'articolo introduttivo sull'API Web Audio). Il caricamento delle tracce audio richiede tempo, quindi le risorse utilizzate nel gioco devono essere caricate al caricamento della pagina, all'inizio del livello o magari in modo incrementale durante il gioco.

Successivamente, creerai un'origine per ogni nodo e un nodo di guadagno per ogni origine e collegherai il grafico.

Dopodiché puoi riprodurre tutte queste origini contemporaneamente in loop e, poiché sono tutte della stessa durata, l'API Web Audio garantirà che rimarranno allineate. Man mano che il personaggio si avvicina o si allontana dalla battaglia finale con i boss, il gioco può variare i valori di guadagno per ciascuno dei rispettivi nodi della catena, utilizzando un algoritmo relativo alla quantità di guadagno come il seguente:

// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
    gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
    // If there is, adjust its gain.
    gains[leftNode + 1].gain.value = gain2;
}

Nell'approccio descritto in precedenza, vengono giocate due origini contemporaneamente e viene applicata la dissolvenza incrociata utilizzando curve di potenza uguali (come descritto nell'introduzione).

Oggi molti sviluppatori di giochi utilizzano il tag <audio> per la musica di sottofondo, poiché è ideale per lo streaming di contenuti. Ora puoi importare i contenuti del tag <audio> in un contesto audio web.

Questa tecnica può essere utile poiché il tag <audio> può funzionare con i contenuti in streaming, il che ti consente di riprodurre immediatamente la musica di sottofondo invece di dover aspettare il download di tutta la musica. Caricando lo stream nell'API Web Audio, puoi manipolarlo o analizzarlo. L'esempio seguente applica un filtro passa basso alla musica riprodotta tramite il tag <audio>:

var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);

Per una discussione più completa sull'integrazione del tag <audio> con l'API Web Audio, consulta questo breve articolo.

Effetti sonori

I giochi spesso riproducono effetti sonori in risposta all'input dell'utente o a cambiamenti nello stato del gioco. Come per la musica di sottofondo, però, gli effetti sonori possono diventare molto fastidiosi. Per evitare che ciò accada, spesso è utile avere un pool di suoni simili ma diversi da riprodurre. Questo può variare da variazioni lievi dei campioni di passi a variazioni drastiche, come mostrato nella serie Warcraft in risposta ai clic sulle unità.

Un'altra caratteristica chiave degli effetti sonori nei giochi è che possono essere diversi contemporaneamente. Immagina di essere nel bel mezzo di uno scontro a fuoco con più attori che sparano mitragliatrici. Ogni mitragliatrice si accende molte volte al secondo, generando contemporaneamente la riproduzione di decine di effetti sonori. La riproduzione di audio da più fonti con timestamp preciso è un punto di forza dell'API Web Audio.

L'esempio seguente crea un round di mitragliatrice a partire da più singoli campioni di proiettili creando più sorgenti sonore la cui riproduzione viene scaglionata nel tempo.

var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
    var source = this.makeSource(this.buffers[M4A1]);
    source.noteOn(time + i - interval);
}

Se tutte le mitragliatrici nel tuo gioco suonassero esattamente così, sarebbe piuttosto noioso. Ovviamente varierebbero per suono in base alla distanza dal target e alla posizione relativa (ulteriori informazioni più avanti), ma anche questo potrebbe non essere sufficiente. Fortunatamente l'API Web Audio offre un modo per modificare facilmente l'esempio sopra riportato in due modi:

  1. Con una leggera variazione temporale tra l'attivazione dei proiettili
  2. Modificando la velocità di riproduzione di ogni campione (cambiando anche il tono) per simulare meglio la casualità del mondo reale.

Per un esempio reale di queste tecniche in azione, dai un'occhiata alla demo del tavolo da biliardo, che utilizza un campionamento casuale e varia la percentuale di riproduzione per un suono più interessante della collisione della palla.

Suono di posizione 3D

I giochi sono spesso ambientati in un mondo con alcune proprietà geometriche, in 2D o in 3D. In questo caso, l'audio in stereo può aumentare molto l'immersività dell'esperienza. Fortunatamente, l'API Web Audio comprende funzionalità audio posizionali integrate con accelerazione hardware e molto semplici da usare. A proposito, devi assicurarti di disporre di altoparlanti stereo (preferibilmente cuffie) affinché il seguente esempio abbia senso.

Nell'esempio precedente, è presente un listener (icona persona) al centro della tela e il mouse influisce sulla posizione dell'origine (icona dell'altoparlante). Quanto riportato sopra è un semplice esempio di utilizzo di AudioPannerNode per ottenere questo tipo di effetto. L'idea alla base dell'esempio precedente è rispondere al movimento del mouse impostando la posizione della sorgente audio come segue:

PositionSample.prototype.changePosition = function(position) {
    // Position coordinates are in normalized canvas coordinates
    // with -0.5 < x, y < 0.5
    if (position) {
    if (!this.isPlaying) {
        this.play();
    }
    var mul = 2;
    var x = position.x / this.size.width;
    var y = -position.y / this.size.height;
    this.panner.setPosition(x - mul, y - mul, -0.5);
    } else {
    this.stop();
    }
};

Informazioni importanti sul trattamento della spazializzazione di Web Audio:

  • Per impostazione predefinita, il listener si trova all'origine (0, 0, 0).
  • Le API di posizionamento audio web sono senza unità, quindi ho introdotto un moltiplicatore per migliorare il suono della demo.
  • Web Audio utilizza le coordinate cartesiane con y-is-up (il contrario della maggior parte dei sistemi grafici per computer). Ecco perché nello snippet qui sopra sto scambiando l'asse y

Funzionalità avanzate: coni sonori

Il modello posizionale è molto potente e piuttosto avanzato, in gran parte basato su OpenAL. Per ulteriori dettagli, consulta le sezioni 3 e 4 delle specifiche collegate sopra.

Modello posizione

Esiste un singolo AudioListener collegato al contesto dell'API Web Audio che può essere configurato nello spazio tramite posizione e orientamento. Ogni origine può essere trasmessa attraverso un AudioPannerNode, che spazializza l'audio di input. Il nodo riquadro ha posizione e orientamento, nonché un modello di distanza e direzionale.

Il modello di distanza specifica la quantità di guadagno in base alla vicinanza alla sorgente, mentre il modello direzionale può essere configurato specificando un cono interno ed esterno, che determinano la quantità di guadagno (solitamente negativo) se il listener si trova all'interno del cono interno, tra il cono interno ed esterno oppure l'esterno del cono.

var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;

Anche se il mio esempio è in 2D, questo modello si estende facilmente alla terza dimensione. Per un esempio di suono spazializzato in 3D, consulta questo campione relativo alla posizione. Oltre alla posizione, il modello audio Web Audio include facoltativamente anche la velocità per le variazioni doppler. Questo esempio mostra l'effetto doppler in maggiore dettaglio.

Per ulteriori informazioni su questo argomento, leggi questo tutorial dettagliato sul [mixing di audio posizionale e WebGL][webgl].

Filtri e effetti per le stanze

In realtà, la percezione del suono dipende molto dalla stanza in cui viene percepito. Lo stesso rumore della porta suonerà molto diverso in uno seminterrato rispetto a un ampio corridoio aperto. I giochi con un elevato valore di produzione tendono a imitare questi effetti, poiché creare un set separato di campioni per ogni ambiente è costoso in modo proibitivo e comporterebbe ancora più asset e una maggiore quantità di dati di gioco.

In parole povere, il termine audio che indica la differenza tra il suono non elaborato e il modo in cui suona nella realtà è risposta impulsiva. Queste risposte di impulso possono essere faticosamente registrate; infatti, esistono siti che ospitano molti di questi file di risposta agli impulsi preregistrati (memorizzati come audio) per comodità.

Per molte altre informazioni su come è possibile creare le risposte agli impulsi da un determinato ambiente, leggi la sezione "Configurazione della registrazione" nella parte Convolution delle specifiche dell'API Web Audio.

Aspetto ancora più importante per i nostri scopi, l'API Web Audio offre un modo semplice per applicare queste risposte di impulso ai nostri suoni utilizzando il ConvolverNode.

// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);

Inoltre, guarda questa demo degli effetti stanza nella pagina delle specifiche dell'API Web Audio, oltre a questo esempio che ti offre il controllo sulla combinazione di un eccellente standard jazz, a umido (raw) e a umido (elaborato tramite convolver).

Il conto alla rovescia finale

Hai creato un gioco, configurato l'audio posizionale e ora hai un gran numero di AudioNode nel grafico, tutti riprodotti contemporaneamente. Ottimo, ma c'è ancora un'altra cosa da considerare:

Dato che i suoni vengono sovrapposti uno sopra l'altro senza normalizzazione, potresti trovarti in una situazione in cui stai superando la soglia di capacità dell'altoparlante. Come le immagini che superano i limiti del canvas, i suoni possono essere ritagliati anche se la forma d'onda supera la soglia massima, producendo una netta distorsione. La forma d'onda sarà simile alla seguente:

Sutura

Ecco un esempio reale di ritaglio. La forma d'onda sembra pessima:

Sutura

È importante ascoltare distorsioni decise come quella riportata sopra o, al contrario, mix troppo sottotiti che costringono gli ascoltatori ad alzare il volume. Se ti trovi in questa situazione, devi davvero risolverlo.

Rileva ritaglio

Da un punto di vista tecnico, il ritaglio si verifica quando il valore dell'indicatore in qualsiasi canale supera l'intervallo valido, ovvero tra -1 e 1. Una volta rilevato, è utile fornire un feedback visivo per informarti che si sta verificando. Per farlo in modo affidabile, inserisci un JavaScriptAudioNode nel grafico. Il grafico audio viene configurato come segue:

// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);

Inoltre, il ritaglio potrebbe essere rilevato nel seguente gestore processAudio:

function processAudio(e) {
    var buffer = e.inputBuffer.getChannelData(0);

    var isClipping = false;
    // Iterate through buffer to check if any of the |values| exceeds 1.
    for (var i = 0; i < buffer.length; i++) {
    var absValue = Math.abs(buffer[i]);
    if (absValue >= 1) {
        isClipping = true;
        break;
    }
    }
}

In generale, fai attenzione a non fare un uso eccessivo di JavaScriptAudioNode per motivi legati alle prestazioni. In questo caso, un'implementazione alternativa del monitoraggio potrebbe eseguire il polling di RealtimeAnalyserNode nel grafico audio per getByteFrequencyData, al momento del rendering, come determinato da requestAnimationFrame. Questo approccio è più efficiente, ma non perde la maggior parte del segnale (inclusi i punti in cui potrebbe essere clip), poiché il rendering avviene al massimo 60 volte al secondo, mentre il segnale audio cambia molto più rapidamente.

Data la grande importanza del rilevamento dei clip, è probabile che in futuro vedremo un nodo integrato dell'API Web Audio MeterNode.

Impedisci ritaglio

Regolando il guadagno dell'AudioGainNode principale, puoi abbassare il tuo mix a un livello che impedisce il clipping. Tuttavia, in pratica, poiché i suoni riprodotti nel gioco possono dipendere da un'enorme varietà di fattori, potrebbe essere difficile stabilire il valore di guadagno principale che impedisce il ritaglio per tutti gli stati. In generale, dovresti modificare i guadagni per prevedere il caso peggiore, ma questa è più un'arte che una scienza.

Aggiungi un po' di zucchero

I compressori vengono comunemente utilizzati nella produzione di musica e giochi per compensare i picchi di segnale e di controllo del segnale complessivo. Questa funzionalità è disponibile nell'ambito dell'audio sul web tramite DynamicsCompressorNode, che può essere inserita nel grafico audio per ottenere un suono più forte, pieno e ricco, nonché per realizzare clip. Citando direttamente la specifica, questo nodo

In genere è consigliabile utilizzare la compressione dinamica, soprattutto nell'ambientazione di un gioco in cui, come spiegato in precedenza, non sai esattamente quali suoni verranno riprodotti e quando. Plink dei lab DinahMoe ne è un ottimo esempio, dato che i suoni che vengono riprodotti dipendono completamente da te e dagli altri partecipanti. Il compressore è utile nella maggior parte dei casi, tranne in alcuni rari, in cui hai a che fare con tracce scrupolosamente masterizzate e che sono già state messe a punto per suonare perfettamente.

L'implementazione di questa soluzione consiste semplicemente nell'includere un DynamicsCompressorNode nel grafico audio, in genere come ultimo nodo prima della destinazione.

// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);

Per ulteriori dettagli sulla compressione delle dinamiche, questo articolo di Wikipedia è molto informativo.

Riassumendo, ascolta attentamente i clip e evitali inserendo un nodo master di guadagno. Quindi stringere l'intero mix utilizzando un nodo compressore dinamico. Il grafico audio potrebbe avere il seguente aspetto:

Risultato finale

Conclusione

Abbiamo definito gli aspetti più importanti dello sviluppo dell'audio dei giochi con l'API Web Audio. Con queste tecniche, puoi creare esperienze audio davvero avvincenti direttamente nel tuo browser. Prima di disconnettermi, ti lascio un suggerimento specifico per il browser: assicurati di mettere in pausa l'audio se la tua scheda passa in background utilizzando l'API di visibilità della pagina, altrimenti creerai un'esperienza potenzialmente frustrante per il tuo utente.

Per ulteriori informazioni su Web Audio, dai un'occhiata all'articolo introduttivo più introduttivo e, in caso di domande, controlla se hai già trovato una risposta nelle Domande frequenti sull'audio web. Infine, se hai altre domande, puoi porle su Stack Overflow utilizzando il tag web-audio.

Prima di concludere, vorrei lasciarti fare alcuni straordinari utilizzi dell'API Web Audio nei giochi reali di oggi: