Introducción
Racer es un experimento de Chrome multidispositivo y multijugador. Un juego de autos tragamonedas de estilo retro que se juega en diferentes pantallas. En teléfonos o tablets, Android o iOS. Cualquier persona puede unirse. No hay aplicaciones. No hay descargas. Solo en la Web móvil
Plan8, junto con nuestros amigos de 14islands, crearon la experiencia musical y de sonido dinámica basada en una composición original de Giorgio Moroder. Racer incluye sonidos de motor responsivos, efectos de sonido de carreras y, lo que es más importante, una combinación de música dinámica que se distribuye en varios dispositivos a medida que se unen los corredores. Es una instalación de varias bocinas compuesta por smartphones.
Conectar varios dispositivos entre sí era algo que habíamos estado probando durante algún tiempo. Habíamos hecho experimentos musicales en los que el sonido se dividía en diferentes dispositivos o saltaba entre ellos, por lo que teníamos muchas ganas de aplicar esas ideas a Racer.
Más específicamente, queríamos probar si podíamos crear la pista de música en todos los dispositivos a medida que más y más personas se unían al juego, comenzando con la batería y el bajo, y luego agregando guitarra y sintetizadores, etcétera. Hicimos algunas demostraciones musicales y nos sumergimos en la programación. El efecto de varias bocinas fue muy gratificante. En ese momento, no teníamos toda la sincronización en orden, pero cuando escuchamos las capas de sonido que se extendían por los dispositivos, supimos que estábamos en el camino correcto.
Crea los sonidos
Google Creative Lab había delineado una dirección creativa para el sonido y la música. Queríamos usar sintetizadores analógicos para crear los efectos de sonido en lugar de grabar los sonidos reales o recurrir a bibliotecas de sonido. También sabíamos que, en la mayoría de los casos, la bocina de salida sería una bocina pequeña de teléfono o tablet, por lo que los sonidos debían limitarse en el espectro de frecuencia para evitar que se distorsionen. Esto resultó ser un gran desafío. Cuando recibimos los primeros borradores musicales de Giorgio, fue un alivio porque su composición funcionaba perfectamente con los sonidos que habíamos creado.
Sonido del motor
El mayor desafío a la hora de programar los sonidos fue encontrar el mejor sonido del motor y definir su comportamiento. El circuito de carreras se parecía a uno de Fórmula 1 o Nascar, por lo que los autos debían sentirse rápidos y explosivos. Al mismo tiempo, los autos eran muy pequeños, por lo que un sonido de motor grande no conectaría el sonido con las imágenes. De todos modos, no podíamos reproducir un motor rugiente en la bocina del dispositivo móvil, así que tuvimos que idear otra solución.
Para inspirarnos, conectamos algunos de los sintetizadores modulares de nuestro amigo Jon Ekstrand y comenzamos a experimentar. Nos gustó lo que escuchamos. Así sonaba con dos osciladores, algunos filtros agradables y LFO.
El equipo analógico se remodeló con gran éxito con la API de Web Audio antes, así que teníamos grandes esperanzas y comenzamos a crear un sintetizador simple en Web Audio. Un sonido generado sería el más responsivo, pero sobrecargaría la potencia de procesamiento del dispositivo. Tuvimos que ser muy eficientes para ahorrar todos los recursos que pudimos para que las imágenes se ejecutaran sin problemas. Por lo tanto, cambiamos de técnica y preferimos reproducir muestras de audio.

Existen varias técnicas que se pueden usar para crear un sonido de motor a partir de muestras. El enfoque más común para los juegos de consola sería tener una capa de varios sonidos (cuantos más, mejor) del motor a diferentes RPM (con carga) y, luego, hacer una transición entre ellos. Luego, agrega una capa de varios sonidos del motor que simplemente acelera (sin carga) a la misma RPM y realiza una transición entre ellos. Si se hace correctamente, la transición entre esas capas cuando cambias de marcha sonará muy realista, pero solo si tienes una gran cantidad de archivos de sonido. El tono cruzado no puede ser demasiado amplio, o bien sonará muy sintético. Como debíamos evitar tiempos de carga largos, esta opción no era adecuada para nosotros. Intentamos con cinco o seis archivos de sonido para cada capa, pero el sonido fue decepcionante. Tuvimos que encontrar una forma con menos archivos.
La solución más eficaz resultó ser la siguiente:
- Un archivo de sonido con aceleración y cambio de marcha sincronizados con la aceleración visual del automóvil que finaliza en un bucle programado con el tono o las RPM más altos La API de Web Audio es muy buena para hacer bucles con precisión, por lo que podemos hacerlo sin fallas ni interrupciones.
- Un archivo de sonido con desaceleración o reducción de las revoluciones del motor.
- Por último, un archivo de sonido que reproduce el sonido inactivo en un bucle.
Se ve de la siguiente manera:

Para el primer evento de aceleración o toque, reproduciríamos el primer archivo desde el principio y, si el usuario soltaba el acelerador, calcularíamos el tiempo desde el que estábamos en el archivo de sonido en el momento del lanzamiento para que, cuando el acelerador se volviera a activar, salte al lugar correcto en el archivo de aceleración después de que se reproduzca el segundo archivo (reducción de revoluciones).
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);
}
Pruébala
Enciende el motor y presiona el botón “Acelerador”.
<input type="button" id="playstop" value = "Start/Stop Engine" onclick='playStop()'>
<input type="button" id="throttle" value = "Throttle" onmousedown='throttleOn()' onmouseup='throttleOff()'>
Así que, con solo tres archivos de sonido pequeños y un motor de sonido de buena calidad, decidimos pasar al siguiente desafío.
Cómo obtener la sincronización
Junto con David Lindkvist de 14islands, comenzamos a analizar en detalle cómo hacer que los dispositivos se reproduzcan en perfecta sincronización. La teoría básica es simple. El dispositivo le solicita la hora al servidor, considera la latencia de la red y, luego, calcula el desplazamiento del reloj local.
syncOffset = localTime - serverTime - networkLatency
Con este desfase, cada dispositivo conectado comparte el mismo concepto de tiempo. Fácil, ¿verdad? (Una vez más, en teoría).
Cómo calcular la latencia de red
Supongamos que la latencia es la mitad del tiempo que tarda en solicitarse y recibirse una respuesta del servidor:
networkLatency = (receivedTime - sentTime) × 0.5
El problema con esta suposición es que el recorrido de ida y vuelta al servidor no siempre es simétrico, es decir, la solicitud puede tardar más que la respuesta o viceversa. Cuanto mayor sea la latencia de la red, mayor será el impacto de esta asimetría, lo que provocará que los sonidos se retrasen y se reproduzcan fuera de sincronización con otros dispositivos.
Por suerte, nuestro cerebro está diseñado para no notar si los sonidos se retrasan un poco. Los estudios han demostrado que se necesita un retraso de 20 a 30 milisegundos (ms) para que nuestro cerebro perciba los sonidos como separados. Sin embargo, entre 12 y 15 ms, comenzarás a “sentir” los efectos de una señal retrasada, incluso si no puedes “percibirla” por completo. Investigamos algunos protocolos de sincronización de hora establecidos, alternativas más simples y tratamos de implementar algunos de ellos en la práctica. Al final, gracias a la infraestructura de baja latencia de Google, pudimos simplemente tomar una muestra de una ráfaga de solicitudes y usar la muestra con la latencia más baja como referencia.
Cómo combatir el desvío del reloj
¡Funcionó! Teníamos más de 5 dispositivos reproduciendo un pulso en perfecta sincronización, pero solo por un tiempo. Después de reproducir durante un par de minutos, los dispositivos se separaban, a pesar de que programamos el sonido con el tiempo de contexto de la API de Web Audio altamente preciso. La latencia se acumulaba lentamente, solo un par de milisegundos a la vez y no se podía detectar al principio, pero generaba capas musicales totalmente desincronizadas después de jugar durante períodos más largos. Hola, desviación del reloj.
La solución fue volver a sincronizar cada pocos segundos, calcular un nuevo desfase de reloj y enviarlo sin problemas al programador de audio. Para reducir el riesgo de cambios notables en la música debido a la latencia de la red, decidimos suavizar el cambio manteniendo un historial de los desfases de sincronización más recientes y calculando un promedio.
Programa canciones y cambia de pista
Crear una experiencia de sonido interactiva significa que ya no tienes el control de cuándo se reproducirán partes de la canción, ya que dependes de las acciones del usuario para cambiar el estado actual. Tuvimos que asegurarnos de poder cambiar entre los arreglos de la canción de forma oportuna, lo que significa que nuestro programador debía poder calcular cuánto queda de la barra que se está reproduciendo antes de cambiar al siguiente arreglo. Nuestro algoritmo terminó pareciendose a lo siguiente:
Client(1)
inicia la canción.Client(n)
le pregunta al primer cliente cuándo comenzó la canción.Client(n)
calcula un punto de referencia para el momento en que se inició la canción con su contexto de Web Audio, teniendo en cuenta syncOffset y el tiempo transcurrido desde que se creó su contexto de audio.playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
Client(n)
calcula cuánto tiempo se está reproduciendo la canción con playDelta. El programador de canciones usa esto para saber qué barra del arreglo actual se debe reproducir a continuación.playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars
Para que no te vuelvas loco, limitamos nuestros arreglos para que siempre tengan ocho barras y el mismo tempo (pulsaciones por minuto).
Mira hacia adelante
Siempre es importante programar con anticipación cuando se usa setTimeout
o setInterval
en JavaScript. Esto se debe a que el reloj de JavaScript no es muy preciso y las devoluciones de llamadas programadas pueden desviarse fácilmente por decenas de milisegundos o más debido al diseño, la renderización, la recolección de elementos no utilizados y las XMLHTTPRequests. En nuestro caso, también tuvimos que tener en cuenta el tiempo que tardan todos los clientes en recibir el mismo evento a través de la red.
Sprites de audio
Combinar sonidos en un archivo es una excelente manera de reducir las solicitudes HTTP, tanto para HTML Audio como para la API de Web Audio. También es la mejor manera de reproducir sonidos de manera responsiva con el objeto Audio, ya que no tiene que cargar un objeto de audio nuevo antes de reproducirlo. Ya hay algunas implementaciones buenas que usamos como punto de partida. Ampliamos nuestro sprite para que funcione de forma confiable en iOS y Android, y también para controlar algunos casos inusuales en los que los dispositivos se suspenden.
En Android, los elementos de audio siguen reproduciéndose incluso si pones el dispositivo en modo de suspensión. En el modo de suspensión, la ejecución de JavaScript se limita para preservar la batería y no puedes usar requestAnimationFrame
, setInterval
ni setTimeout
para activar devoluciones de llamada. Esto es un problema, ya que los sprites de audio dependen de JavaScript para seguir verificando si se debe detener la reproducción. Para empeorar las cosas, en algunos casos, el currentTime
del elemento Audio no se actualiza, aunque el audio sigue reproduciéndose.
Consulta la implementación de AudioSprite que usamos en Chrome Racer como resguardo que no es de Audio Web.
Elemento de audio
Cuando comenzamos a trabajar en Racer, Chrome para Android aún no era compatible con la API de Web Audio. La lógica de usar HTML Audio para algunos dispositivos, la API de Web Audio para otros, combinada con la salida de audio avanzada que queríamos lograr, generó algunos desafíos interesantes. Por suerte, eso ya es historia. La API de Web Audio se implementa en Android M28 beta.
- Retrasos o problemas de tiempo El elemento Audio no siempre se reproduce exactamente cuando le indicas que lo haga. Dado que JavaScript es de un solo subproceso, es posible que el navegador esté ocupado, lo que causa demoras de reproducción de hasta dos segundos.
- Las demoras en la reproducción significan que no siempre es posible realizar un bucle fluido. En computadoras, puedes usar el almacenamiento en búfer doble para lograr bucles sin interrupciones, pero en dispositivos móviles no es una opción, por los siguientes motivos:
- La mayoría de los dispositivos móviles no reproducen más de un elemento de audio a la vez.
- Volumen fijo. Ni Android ni iOS te permiten cambiar el volumen de un objeto de audio.
- Sin precarga. En dispositivos móviles, el elemento Audio no comenzará a cargar su fuente, a menos que se inicie la reproducción en un controlador
touchStart
. - Buscar problemas Obtener
duration
o configurarcurrentTime
fallará, a menos que tu servidor admita el rango de bytes HTTP. Ten en cuenta esto si estás compilando un sprite de audio como lo hicimos nosotros. - La autenticación básica en MP3 falla. Algunos dispositivos no cargan archivos MP3 protegidos por la autenticación básica, independientemente del navegador que uses.
Conclusiones
Hemos recorrido un largo camino desde que presionamos el botón de silenciar como la mejor opción para controlar el sonido en la Web, pero esto es solo el comienzo, y el audio web está a punto de ser increíble. Solo hemos arañado la superficie de lo que se puede hacer en cuanto a la sincronización de varios dispositivos. No teníamos la potencia de procesamiento en los teléfonos y las tablets para profundizar en el procesamiento de señales y los efectos (como la reverberación), pero a medida que aumenta el rendimiento de los dispositivos, los juegos basados en la Web también aprovecharán esas funciones. Estos son tiempos emocionantes para seguir explorando las posibilidades del sonido.