Case study - The Sounds of Racer

Introduzione

Racer è un esperimento Chrome multi-player e multi-dispositivo. Una slot car in stile rétro che può essere giocata su più schermi. Su smartphone o tablet, Android o iOS. Chiunque può partecipare. Nessuna app. Niente più download. Solo il web mobile.

Plan8, insieme ai nostri amici di 14islands, ha creato l'esperienza sonora e musicale dinamica basata su una composizione originale di Giorgio Moroder. Il pilota dispone di suoni reattivi del motore, effetti sonori da gara, ma soprattutto un mix musicale dinamico che si distribuisce su vari dispositivi a mano a mano che i piloti si uniscono. Si tratta di un'installazione multi-altoparlante composta da smartphone.

Collegare più dispositivi era una cosa che avevamo già preso in giro da un po' di tempo. Avevamo fatto esperimenti musicali in cui l'audio si divideva su diversi dispositivi o passava da un dispositivo all'altro, quindi eravamo impazienti di applicare queste idee a Racer.

Nello specifico, volevamo testare se fosse possibile creare la traccia musicale su tutti i dispositivi man mano che un numero sempre maggiore di persone si unisce al gioco, iniziando con la batteria e il basso, aggiungendo chitarra e sintetizzatori e così via. Abbiamo fatto delle demo musicali e ci siamo dedicati alla programmazione. L'effetto multi-altoparlante è stato davvero gratificante. A quel punto non avevamo tutta la sincronizzazione, ma quando abbiamo sentito che gli strati di suono si diffondevano sui dispositivi, abbiamo capito che stavamo cercando qualcosa di buono.

Creare le tracce audio

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 raccolte di suoni. Sapevamo anche che l'altoparlante in uscita sarebbe stato, nella maggior parte dei casi, un piccolo altoparlante di smartphone o tablet, quindi i suoni dovevano essere limitati nello spettro di frequenza per evitare che gli altoparlanti si distorcessero. Questo approccio si è rivelato una sfida impegnativa. Quando abbiamo ricevuto le prime bozze di musica di Giorgio, è stato un sollievo perché la sua composizione era perfettamente compatibile con le sonorità che avevamo creato.

Rumore motore

La più grande sfida per la programmazione dei suoni è stata quella di trovare il miglior suono del motore e plasmare il suo comportamento. La pista somigliava a una pista di F1 o Nascar, quindi le auto dovevano essere veloci ed esplosive. Allo stesso tempo le auto erano molto piccole, quindi un grande suono del motore non poteva davvero collegare il suono alle immagini. Non potevamo comunque avere un motore energizzante che risuonava nell'altoparlante mobile, quindi abbiamo dovuto capire qualcos'altro.

Per trovare l'ispirazione, abbiamo creato una raccolta di sintetizzatori modulari del nostro amico Jon Ekstrand e abbiamo iniziato a scherzare. Ci è piaciuto quello che abbiamo sentito. Questo è quello che sembrava con due oscillatori, alcuni bei filtri e LFO.

In passato, le apparecchiature analogiche sono state rimodellate con grande successo utilizzando l'API Web Audio, quindi nutrivamo grandi speranze e abbiamo iniziato a creare un semplice sintetizzatore in Web Audio. Un suono generato sarebbe quello più reattivo, ma inciderebbe sulla potenza di elaborazione del dispositivo. Dovevamo essere estremamente esigenti per risparmiare tutte le risorse possibili affinché le immagini funzionassero senza problemi. Abbiamo quindi cambiato tecnica a favore della riproduzione di campioni audio.

Sintetizzatore modulare per l'ispirazione del motore

Esistono diverse tecniche che si possono usare per produrre il suono di un motore a partire dai campioni. L'approccio più comune per i giochi per console sarebbe quello di avere un livello di più suoni (più è, meglio è) del motore a diversi RPM (con carico) e quindi di dissolvenza incrociata e intersezione tra loro. Poi aggiungi un livello di più suoni del motore che gira (senza carico) allo stesso giri al minuto, con dissolvenza incrociata e intersezione tra i due. La dissolvenza incrociata tra questi livelli quando si cambia marcia, se eseguita correttamente, risulterà molto realistica, ma solo se si dispone di una grande quantità di file audio. Il crosspitit non può essere troppo ampio o risulterà molto sintetico. Poiché dovevamo evitare lunghi tempi di caricamento, questa opzione non ci è stata utile. Abbiamo provato con cinque o sei file audio per ogni livello, ma l'audio è stato deludente. Dovevamo trovare un modo con meno file.

La soluzione più efficace si è dimostrata la seguente:

  • Un file audio con accelerazione e cambio di marcia sincronizzati con l'accelerazione visiva dell'auto che termina con un loop programmato al massimo tono / RPM. L'API Web Audio è ottima nel creare loop con precisione, quindi possiamo farlo senza glitch o gli scoppiettii.
  • Un file audio con decelerazione / il motore si abbassa.
  • Infine, un file audio che riproduce il suono fermo / inattivo in loop.

Si presenta come questa

Immagine dell'audio del motore

Per l'evento di primo tocco o l'accelerazione, riprodurremmo il primo file dall'inizio e, se il player rilasciava l'acceleratore, calcoleremo il tempo da dove ci trovavamo nel file audio al momento del rilascio, in modo che, quando viene riattivata la limitazione, passi al punto giusto nel file di accelerazione dopo che è stato riprodotto il secondo file (abbassando il ritmo).

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);
}

Prova

Avviare il motore e premere il pulsante "Accelerazione".

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

Quindi, con solo tre piccoli file audio e un buon motore dal suono, abbiamo deciso di passare alla sfida successiva.

Eseguire la sincronizzazione

Insieme a David Lindkvist di 14islands, abbiamo iniziato a esaminare più a fondo la riproduzione in perfetta sincronizzazione dei dispositivi. La teoria di base è semplice. Il dispositivo chiede al server l'ora, tiene conto della latenza di rete, quindi calcola lo scarto dell'orologio locale.

syncOffset = localTime - serverTime - networkLatency

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

Calcolo della latenza di rete

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

networkLatency = (receivedTime - sentTime) × 0.5

Il problema con questo presupposto è che il round trip al server non è sempre simmetrico, ossia la richiesta potrebbe richiedere più tempo della risposta o viceversa. Maggiore è la latenza di rete, maggiore sarà l'impatto di questa asimmetria, causando un ritardo dei suoni e una riproduzione non sincronizzata con altri dispositivi.

Fortunatamente il nostro cervello non riesce a accorgersi se i suoni subiscono un leggero ritardo. Studi hanno dimostrato che occorre un ritardo di 20-30 millisecondi (ms) prima che il nostro cervello percepisca i suoni come separati. Tuttavia, entro 12-15 ms, inizierai a "sentire" gli effetti di un segnale in ritardo anche se non sei in grado di "percepirlo" completamente. Abbiamo esaminato un paio di protocolli di sincronizzazione dell'ora consolidati, alternative più semplici e abbiamo provato a implementarne alcuni nella pratica. Alla fine, grazie all'infrastruttura a bassa latenza di Google, siamo riusciti a campionare semplicemente una serie di richieste e a utilizzare come riferimento il campione con la latenza più bassa.

Lotta alla deriva dell'orologio

Ha funzionato! Avevamo più di 5 dispositivi che riproducevano un impulso in perfetta sincronizzazione, ma solo per un po'. Dopo aver giocato per un paio di minuti, i dispositivi si allontanavano anche se avevamo programmato l'audio utilizzando il tempo di contesto dell'API Web Audio altamente preciso. Il ritardo si accumulava lentamente, solo di un paio di millisecondi alla volta e all'inizio non era rilevabile, ma i livelli musicali erano totalmente fuori sincronia dopo la riproduzione per periodi di tempo più lunghi. Salve, deviazione dell'orologio.

La soluzione era ripetere la sincronizzazione a intervalli di pochi secondi, calcolare un nuovo offset dell'orologio e trasmetterlo senza problemi al scheduler audio. Per ridurre il rischio di cambiamenti significativi nella musica a causa del ritardo di rete, abbiamo deciso di apporre il cambiamento mantenendo una cronologia degli ultimi offset di sincronizzazione e calcolare una media.

Programmazione del brano e cambio di arrangiamento

Se crei un'esperienza sonora interattiva, non hai più il controllo su quando verranno riprodotte parti del brano, perché la modifica dello stato corrente dipende dalle azioni degli utenti. Dovevamo assicurarci di poter passare da un arrangiamento del brano all'altro in modo tempestivo, il che significa che il nostro scheduler doveva essere in grado di calcolare quanto rimane della barra attualmente in riproduzione prima di passare al successivo arrangiamento. Il nostro algoritmo ha ottenuto un aspetto simile al seguente:

  • Client(1) avvia il brano.
  • Client(n) chiede al primo client quando è stato avviato il brano.
  • Client(n) calcola un punto di riferimento a quando il brano è stato avviato utilizzando il relativo contesto audio web, tenendo conto di syncOffset e il tempo trascorso dalla creazione del relativo contesto audio.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) calcola la durata della riproduzione del brano utilizzando il PlayDelta. Il programma di pianificazione dei brani utilizza questo valore per sapere quale battuta deve essere riprodotta successivamente nell'arrangiamento corrente.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

Per motivi di buon umore, abbiamo limitato i nostri arrangiamenti a una lunghezza sempre di otto battute e ad avere lo stesso tempo (battiti al minuto).

Guarda avanti

È sempre importante pianificare in anticipo quando utilizzi setTimeout o setInterval in JavaScript. Questo perché l'orologio JavaScript non è molto preciso e i callback pianificati possono essere facilmente distorti di decine di millisecondi o più per layout, rendering, garbage collection e XMLHTTPRequests. Nel nostro caso, abbiamo dovuto anche tenere conto del tempo necessario a tutti i clienti per ricevere lo stesso evento sulla rete.

Sprite audio

Combinare l'audio in un unico file è un ottimo modo per ridurre le richieste HTTP, sia per l'audio HTML che per l'API Web Audio. Rappresenta inoltre il modo migliore per riprodurre i suoni in modo reattivo utilizzando l'oggetto Audio, dal momento che non è necessario caricare un nuovo oggetto audio prima di riprodurlo. Esistono già buoni 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, oltre a gestire alcuni strani casi in cui i dispositivi si addormentano.

Su Android, la riproduzione degli elementi audio continua anche se attivi la modalità di sospensione sul dispositivo. In modalità di 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, poiché gli sprite audio si basano su JavaScript per continuare a controllare se la riproduzione deve essere interrotta. A peggiorare le cose, in alcuni casi il valore currentTime dell'elemento audio non si aggiorna anche se l'audio è ancora in riproduzione.

Dai un'occhiata all'implementazione di AudioSprite che abbiamo usato in Chrome Racer come elemento di riserva non Web Audio.

Elemento audio

Quando abbiamo iniziato a lavorare su Racer, Chrome per Android non supportava ancora l'API Web Audio. La logica di utilizzo dell'audio HTML su alcuni dispositivi, l'API Web Audio per altri, combinata con l'output audio avanzato che volevamo realizzare ha rappresentato alcune sfide interessanti. Per fortuna, ora è tutta la storia. L'API Web Audio è implementata in Android M28 beta.

  • Ritardi/problemi relativi ai tempi. L'elemento audio non viene sempre riprodotto esattamente quando gli chiedi di riprodurlo. Poiché JavaScript è a thread unico, il browser potrebbe essere occupato, causando ritardi di riproduzione fino a due secondi.
  • I ritardi di riproduzione indicano che non è sempre possibile una riproduzione in loop fluida. Sui computer puoi utilizzare il doppio buffering per ottenere loop alquanto senza interruzioni, ma sui dispositivi mobili non è possibile farlo 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 inizia a caricare la sua origine a meno che la riproduzione non venga avviata in un gestore touchStart.
  • Ricerca di problemi. Il recupero di duration o l'impostazione di currentTime avrà esito negativo, a meno che il tuo server non supporti HTTP Byte-Range. Fai attenzione a questo se stai creando uno sprite audio come abbiamo fatto noi.
  • Autorizzazione di base su MP3 non riuscita. Alcuni dispositivi non riescono a caricare file MP3 protetti da Basic Auth, a prescindere dal browser utilizzato.

Conclusioni

Abbiamo fatto molta strada da quando premere il tasto di disattivazione audio come opzione migliore per gestire l'audio per il web, ma questo è solo l'inizio e l'audio sul web sta per diventare forte. Abbiamo toccato solo l'aspetto di ciò che si può fare quando si tratta di sincronizzazione di più dispositivi. Non avevamo la potenza di elaborazione degli smartphone e dei tablet per immergerci nell'elaborazione dei segnali e negli effetti (come il riverbero), ma con l'aumento delle prestazioni dei dispositivi, anche i giochi basati sul web trarranno vantaggio da queste funzionalità. Sono tempi entusiasmanti per continuare a spingere le possibilità del suono.