Case study - The Sounds of Racer

Introduzione

Racer è un esperimento di Chrome multi-giocatore e multi-dispositivo. Un gioco di auto da corsa in stile retrò da giocare su più schermi. Su smartphone o tablet, Android o iOS. Chiunque può partecipare. Nessuna app. Nessun download. Solo sul web mobile.

Plan8, insieme ai nostri amici di 14islands, ha creato un'esperienza musicale e sonora dinamica basata su una composizione originale di Giorgio Moroder. Racer include suoni del motore sensibili, effetti sonori di corse, ma soprattutto un mix musicale dinamico che si distribuisce su più dispositivi man mano che i piloti si uniscono. Si tratta di un'installazione multi-altoparlante composta da smartphone.

La connessione di più dispositivi era un'idea che stavamo prendendo in considerazione da un po' di tempo. Avevamo fatto esperimenti musicali in cui l'audio veniva suddiviso su dispositivi diversi o passava da un dispositivo all'altro, quindi non vedevamo l'ora di applicare queste idee a Racer.

Nello specifico, volevamo verificare se era possibile creare la traccia audio su tutti i dispositivi man mano che sempre più persone si univano al gioco, iniziando con la batteria e il basso, poi aggiungendo la chitarra e i sintetizzatori e così via. Abbiamo fatto alcune demo musicali e ci siamo immersi nel codice. L'effetto multi speaker è stato davvero gratificante. A quel punto la sincronizzazione non era ancora perfetta, ma quando abbiamo sentito i livelli di audio distribuiti sui dispositivi, abbiamo capito che eravamo sulla buona strada.

Creazione dei suoni

Google Creative Lab aveva delineato una direzione creativa per l'audio e la musica. Volevamo utilizzare sintetizzatori analogici per creare gli effetti sonori anziché registrare i suoni reali o ricorrere a librerie audio. Sapevamo inoltre che, nella maggior parte dei casi, lo speaker di output sarebbe stato un piccolo altoparlante di smartphone o tablet, quindi i suoni dovevano essere limitati nello spettro di frequenza per evitare che gli speaker si distorcessero. Questa è stata una sfida piuttosto impegnativa. Quando abbiamo ricevuto le prime bozze musicali di Giorgio, è stato un sollievo perché la sua composizione andava perfettamente a braccetto con i suoni che avevamo creato.

Suono del motore

La sfida più grande nella programmazione dei suoni è stata trovare il suono migliore del motore e modellare il suo comportamento. Il circuito di gara assomigliava a quello di Formula 1 o NASCAR, quindi le auto dovevano sembrare veloci ed esplosive. Allo stesso tempo, le auto erano molto piccole, quindi un suono del motore troppo alto non avrebbe collegato il suono alle immagini. Non potevamo far sentire un motore ruggente nello speaker del cellulare, quindi abbiamo dovuto trovare un'altra soluzione.

Per trovare ispirazione, abbiamo collegato alcuni dei sintetizzatori modulari del nostro amico Jon Ekstrand e abbiamo iniziato a fare esperimenti. Ci è piaciuto quello che abbiamo sentito. Ecco come suona con due oscillatori, alcuni bei filtri e LFO.

Le apparecchiature analogiche sono state rimodellate con grande successo utilizzando l'API Web Audio, quindi avevamo grandi speranze e abbiamo iniziato a creare un semplice sintetizzatore in Web Audio. Un suono generato sarebbe il più reattivo, ma metterebbe a dura prova la potenza di elaborazione del dispositivo. Per far sì che le immagini funzionassero senza problemi, dovevamo essere estremamente efficienti per risparmiare tutte le risorse possibili. Perciò abbiamo cambiato tecnica in favore della riproduzione di sample audio.

Sintetizzatore modulare per ispirazione per i suoni del motore

Esistono diverse tecniche che possono essere utilizzate per creare il suono di un motore a partire da sample. L'approccio più comune per i giochi su console è avere un livello di più suoni (più ce ne sono, meglio è) del motore a diverse RPM (con carico) e poi eseguire il crossfade e il crosspitch tra di loro. Aggiungi poi un livello di più suoni del motore che gira (senza carico) alla stessa velocità in RPM e applica il crossfade e il crosspitch tra i due. Se eseguita correttamente, la transizione tra questi livelli durante il cambio di marcia avrà un suono molto realistico, ma solo se hai una grande quantità di file audio. Il crosspitching non può essere troppo ampio, altrimenti il suono risulterà molto sintetico. Dato che dovevamo evitare tempi di caricamento lunghi, questa opzione non era adatta a noi. Abbiamo provato con cinque o sei file audio per ogni livello, ma il risultato non ci ha soddisfatto. Dovevamo trovare un modo per utilizzare meno file.

La soluzione più efficace si è rivelata la seguente:

  • Un file audio con accelerazione e cambio marcia sincronizzati con l'accelerazione visiva dell'auto che termina in un loop programmato alla frequenza / rpm più elevata. L'API Web Audio è molto brava a creare loop precisi, quindi abbiamo potuto farlo senza glitch o scoppiettii.
  • Un file audio con decelerazione / rallentamento del motore.
  • Infine, un file audio che riproduce l'audio di attesa / inattività in loop.

Come questa

Grafica dell'audio del motore

Per il primo evento tocco / accelerazione, riproduciamo il primo file dall'inizio e, se il giocatore rilascia l'acceleratore, calcoliamo il tempo dal punto in cui ci trovavamo nel file audio al momento del rilascio in modo che, quando l'acceleratore si riaccende, salti al punto giusto nel file di accelerazione dopo la riproduzione del secondo file (riduzione del numero di giri).

function throttleOn(throttle) {
    //Calculate the start position depending 
    //on the current amount of throttle.
    //By multiplying throttle we get a start position 
    //between 0 and 3 seconds.
    var startPosition = throttle * 3;

    var audio = context.createBufferSource();
    audio.buffer = loadedBuffers["accelerate_and_loop"];

    //Sets the loop positions for the buffer source.
    audio.loopStart = 5;
    audio.loopEnd = 9;

    //Starts the buffer source at the current time
    //with the calculated offset.
    audio.start(context.currentTime, startPosition);
}

Provaci

Avvia il motore e premi il pulsante "Acceleratore".

<input type="button" id="playstop" value = "Start/Stop Engine" onclick='playStop()'>
<input type="button" id="throttle" value = "Throttle" onmousedown='throttleOn()' onmouseup='throttleOff()'>

Così, con solo tre piccoli file audio e un motore audio di buona qualità, abbiamo deciso di passare alla sfida successiva.

Sincronizzazione

Insieme a David Lindkvist di 14islands, abbiamo iniziato a esaminare più da vicino la possibilità di riprodurre i dispositivi in perfetta sincronia. La teoria di base è semplice. Il dispositivo chiede al server l'ora, tiene conto della latenza della rete e poi calcola l'offset dell'orologio locale.

syncOffset = localTime - serverTime - networkLatency

Con questo offset, ogni dispositivo connesso condivide lo stesso concetto di tempo. Facile, no? (Ancora una volta, in teoria).

Calcolo della latenza di rete

Possiamo assumere che la latenza sia la metà del tempo necessario per richiedere e ricevere una risposta dal server:

networkLatency = (receivedTime - sentTime) × 0.5

Il problema di questo presupposto è che il viaggio di andata e ritorno al server non è sempre simmetrico, ovvero la richiesta potrebbe richiedere più tempo della risposta o viceversa. Maggiore è la latenza della rete, maggiore sarà l'impatto di questa asimmetria, causando ritardi e riproduzione non sincronizzata dei suoni con gli altri dispositivi.

Fortunatamente, il nostro cervello è programmato per non notare se i suoni sono leggermente in ritardo. Alcuni studi hanno dimostrato che è necessario un ritardo di 20-30 millisecondi (ms) prima che il nostro cervello percepisca i suoni come distinti. Tuttavia, a partire da circa 12-15 ms, inizierai a "sentire" gli effetti di un segnale in ritardo anche se non riesci a "percepirlo" completamente. Abbiamo esaminato un paio di protocolli di sincronizzazione dell'ora consolidati, alternative più semplici, e abbiamo provato a implementarne alcune nella pratica. Alla fine, grazie all'infrastruttura a bassa latenza di Google, abbiamo potuto semplicemente campionare un picco di richieste e utilizzare il campione con la latenza più bassa come riferimento.

Evitare la deriva dell'orologio

Ha funzionato. Abbiamo avuto più di 5 dispositivi che riproducevano un impulso in perfetta sincronia, ma solo per un po' di tempo. Dopo aver riprodotto l'audio per un paio di minuti, i dispositivi si allontanavano anche se avevamo pianificato l'audio utilizzando il tempo di contesto dell'API Web Audio, estremamente preciso. Il ritardo si accumulava lentamente, solo di un paio di millisecondi alla volta e non era rilevabile all'inizio, ma dopo aver ascoltato la traccia per periodi di tempo più lunghi, i livelli musicali erano totalmente fuori sincrono. Ciao, sfasamento dell\'orologio.

La soluzione è stata sincronizzare di nuovo ogni pochi secondi, calcolare un nuovo offset dell'orologio e inserirlo senza problemi nell'organizzatore audio. Per ridurre il rischio di modifiche significative nella musica a causa del ritardo della rete, abbiamo deciso di attenuare la variazione mantenendo una cronologia degli ultimi offset di sincronizzazione e calcolando una media.

Pianificazione dei brani e cambio di arrangiamento

Se crei un'esperienza sonora interattiva, non hai più il controllo sul momento in cui verranno riprodotte le parti del brano, poiché devi fare affidamento sulle azioni dell'utente per modificare lo stato corrente. Dovevamo assicurarci di poter passare da un arrangiamento all'altro nel brano in modo tempestivo, il che significa che il nostro programmatore doveva essere in grado di calcolare quanto rimane della barra attualmente in riproduzione prima di passare all'arrangiamento successivo. Il nostro algoritmo ha avuto il seguente aspetto:

  • Client(1) avvia il brano.
  • Client(n) chiede al primo client quando è stato avviato il brano.
  • Client(n) calcola un punto di riferimento per il momento in cui è stato avviato il brano utilizzando il relativo contesto Web Audio, tenendo conto di syncOffset e del tempo trascorso dalla creazione del contesto audio.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) calcola il tempo di riproduzione del brano utilizzando playDelta. Il programmatore dei brani lo utilizza per sapere quale barra dell'arrangiamento corrente deve essere riprodotta di seguito.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

Per praticità, abbiamo limitato i nostri arrangiamenti a essere sempre lunghi otto battute e avere lo stesso tempo (battute al minuto).

Guarda avanti

È sempre importante pianificare in anticipo quando si utilizzano setTimeout o setInterval in JavaScript. Questo perché l'orologio JavaScript non è molto preciso e i richiami pianificati possono essere facilmente alterati di decine di millisecondi o più da layout, rendering, raccolta dei rifiuti e XMLHTTPRequest. Nel nostro caso, abbiamo dovuto anche tenere conto del tempo necessario a tutti i client per ricevere lo stesso evento sulla rete.

Sprite audio

Combinare i suoni in un unico file è un ottimo modo per ridurre le richieste HTTP, sia per HTML Audio che per l'API Web Audio. Inoltre, è il modo migliore per riprodurre i suoni in modo reattivo utilizzando l'oggetto Audio, poiché non è necessario caricare un nuovo oggetto audio prima della riproduzione. Esistono già alcune buone implementazioni che abbiamo utilizzato come punto di partenza. Abbiamo esteso il nostro sprite in modo che funzioni in modo affidabile sia su iOS che su Android, nonché per gestire alcuni casi insoliti in cui i dispositivi vanno in sospensione.

Su Android, gli elementi audio continuano a essere riprodotti anche se metti il dispositivo in modalità di sospensione. In modalità sospensione, l'esecuzione di JavaScript è limitata per risparmiare batteria e non puoi fare affidamento su requestAnimationFrame, setInterval o setTimeout per attivare i callback. Questo è un problema perché gli sprite audio si basano su JavaScript per continuare a controllare se la riproduzione deve essere interrotta. A peggiorare le cose, in alcuni casi currentTime dell'elemento Audio non si aggiorna anche se l'audio continua a essere riprodotto.

Dai un'occhiata all'implementazione di AudioSprite che abbiamo utilizzato in Chrome Racer come alternativa a Web Audio.

Elemento audio

Quando abbiamo iniziato a lavorare a Racer, Chrome per Android non supportava ancora l'API Web Audio. La logica di utilizzo di HTML Audio per alcuni dispositivi e dell'API Web Audio per altri, combinata con l'output audio avanzato che volevamo ottenere, ha creato alcune sfide interessanti. Fortunatamente, ora è tutto passato. L'API Web Audio è implementata in Android M28 beta.

  • Problemi di ritardo/tempi. L'elemento Audio non viene sempre riprodotto esattamente quando glielo chiedi. Poiché JavaScript è a thread singolo, il browser potrebbe essere occupato, causando ritardi nella riproduzione fino a due secondi.
  • I ritardi nella riproduzione fanno sì che non sia sempre possibile eseguire un loop fluido. Sul computer puoi utilizzare il doppio buffering per ottenere loop quasi senza interruzioni, ma sui dispositivi mobili questa opzione non è disponibile perché:
    • La maggior parte dei dispositivi mobili non riproduce più di un elemento Audio alla volta.
    • Volume fisso. Né Android né iOS ti consentono di modificare il volume di un oggetto Audio.
  • Nessun precaricamento. Sui dispositivi mobili, l'elemento Audio non inizierà a caricare la relativa origine a meno che la riproduzione non venga avviata in un gestore touchStart.
  • Ricerca di problemi. L'ottenimento di duration o l'impostazione di currentTime non andrà a buon fine a meno che il server non supporti l'intervallo di byte HTTP. Fai attenzione a questo problema se stai creando uno sprite audio come abbiamo fatto noi.
  • L'autenticazione di base su MP3 non va a buon fine. Su alcuni dispositivi non è possibile caricare i file MP3 protetti dall'autenticazione di base, indipendentemente dal browser utilizzato.

Conclusioni

Abbiamo fatto molta strada da quando il pulsante di disattivazione dell'audio era l'opzione migliore per gestire l'audio per il web, ma questo è solo l'inizio e l'audio sul web sta per diventare un'esperienza eccezionale. Abbiamo solo toccato la superficie di ciò che è possibile fare in termini di sincronizzazione di più dispositivi. Gli smartphone e i tablet non avevano la potenza di elaborazione necessaria per gestire l'elaborazione del segnale e gli effetti (come il riverbero), ma con l'aumento delle prestazioni dei dispositivi, anche i giochi basati sul web potranno sfruttare queste funzionalità. È un momento entusiasmante per continuare a esplorare le possibilità del suono.