Desarrollo de audio de juegos con la API de Web Audio

Introducción

El audio es una parte importante de lo que hace que las experiencias multimedia sean tan atractivas. Si alguna vez intentaste mirar una película con el sonido desactivado, probablemente lo hayas notado.

Los juegos no son una excepción. Mis mejores recuerdos son la música y los efectos de sonido. Ahora, en muchos casos, casi dos décadas después de jugar a mis favoritos, todavía no puedo quitarme de la cabeza las composiciones de Zelda de Koji Kondo y la banda sonora atmosférica de Diablo de Matt Uelmen. Lo mismo ocurre con los efectos de sonido, como las respuestas de clic de unidad reconocibles al instante de Warcraft y las muestras de los clásicos de Nintendo.

El audio de los juegos presenta algunos desafíos interesantes. Para crear música de juego convincente, los diseñadores deben adaptarse al estado de juego potencialmente impredecible en el que se encuentra un jugador. En la práctica, algunas partes del juego pueden continuar durante una duración desconocida, los sonidos pueden interactuar con el entorno y mezclarse de maneras complejas, como efectos de habitación y posicionamiento del sonido relativo. Por último, puede haber una gran cantidad de sonidos reproduciéndose a la vez, los cuales deben sonar bien juntos y renderizarse sin introducir penalizaciones de rendimiento.

Audio de juegos en la Web

Para juegos simples, puede ser suficiente usar la etiqueta <audio>. Sin embargo, muchos navegadores proporcionan implementaciones deficientes, lo que genera fallas de audio y latencia alta. Se trata de un problema temporal, ya que los proveedores están trabajando arduamente para mejorar sus respectivas implementaciones. Para darte una idea del estado de la etiqueta <audio>, puedes encontrar un buen paquete de pruebas en areweplayingyet.org.

Sin embargo, si analizamos en más detalle la especificación de la etiqueta <audio>, queda claro que hay muchas cosas que simplemente no se pueden hacer con ella, lo que no es sorprendente, ya que se diseñó para la reproducción de contenido multimedia. Estas son algunas de las limitaciones:

  • No se puede aplicar filtros a la señal de sonido.
  • No hay forma de acceder a los datos PCM sin procesar.
  • Sin concepto de posición y dirección de fuentes y objetos de escucha
  • No hay tiempos detallados.

En el resto del artículo, analizaré algunos de estos temas en el contexto del audio de juegos escrito con la API de Web Audio. Para obtener una breve introducción a esta API, consulta el instructivo de introducción.

Música en segundo plano

Los juegos suelen tener música de fondo que se reproduce en un bucle.

Puede ser muy molesto si el bucle es corto y predecible. Si un jugador está atascado en un área o nivel, y el mismo sample se reproduce de forma continua en segundo plano, puede ser conveniente atenuar gradualmente la pista para evitar más frustraciones. Otra estrategia es tener combinaciones de variadas intensidades que se mezclen gradualmente entre sí, según el contexto del juego.

Por ejemplo, si el jugador se encuentra en una zona con una batalla épica contra el jefe, es posible que tengas varias mezclas que varíen en rango emocional, desde algo atmosférico hasta uno previo o intenso. El software de síntesis de música a menudo te permite exportar varias mezclas (de la misma duración) según una pieza eligiendo el conjunto de pistas que se usarán en la exportación. De esta manera, tendrás cierta coherencia interna y evitarás tener transiciones discordantes cuando realices la transición de un segmento a otro.

GarageBand

Luego, con la API de Web Audio, puedes importar todos estos samples con algo como la clase BufferLoader a través de XHR (esto se explica en detalle en el artículo introductorio de la API de Web Audio). La carga de sonidos lleva tiempo, por lo que los recursos que se usan en el juego deben cargarse cuando se carga la página, al comienzo del nivel o, tal vez, de forma incremental mientras el jugador juega.

A continuación, creas una fuente para cada nodo y un nodo de ganancia para cada fuente, y conectas el gráfico.

Después de hacerlo, puedes reproducir todas estas fuentes de forma simultánea en un bucle y, como todas tienen la misma duración, la API de Web Audio garantizará que permanezcan alineadas. A medida que el personaje se acerca o se aleja de la batalla final contra el jefe, el juego puede variar los valores de ganancia para cada uno de los nodos respectivos de la cadena, con un algoritmo de cantidad de ganancias como el siguiente:

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

En el enfoque anterior, se reproducen dos fuentes a la vez y se realiza una transición entre ellas con curvas de potencia iguales (como se describe en la introducción).

Actualmente, muchos desarrolladores de juegos usan la etiqueta <audio> para su música de fondo, ya que es adecuada para transmitir contenido. Ahora puedes incorporar contenido de la etiqueta <audio> a un contexto de Web Audio.

Esta técnica puede ser útil, ya que la etiqueta <audio> puede funcionar con contenido de transmisión, lo que te permite reproducir la música de fondo de inmediato en lugar de tener que esperar a que se descargue todo. Cuando llevas el flujo a la API de Web Audio, puedes manipularlo o analizarlo. En el siguiente ejemplo, se aplica un filtro de paso bajo a la música que se reproduce a través de la etiqueta <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);

Para obtener una explicación más completa sobre la integración de la etiqueta <audio> con la API de Web Audio, consulta este artículo breve.

Efectos de sonido

Los juegos suelen reproducir efectos de sonido en respuesta a las entradas del usuario o a los cambios en el estado del juego. Sin embargo, al igual que la música de fondo, los efectos de sonido pueden ser molestos con rapidez. Para evitar esto, a menudo es útil tener un grupo de sonidos similares, pero diferentes, para reproducir. Esto puede variar desde variaciones leves de muestras de pasos hasta variaciones drásticas, como se ve en la serie de Warcraft cuando se hace clic en las unidades.

Otra característica clave de los efectos de sonido en los juegos es que puede haber muchos de ellos a la vez. Imagina que estás en medio de un tiroteo con varios actores que disparan ametralladoras. Cada ametralladora dispara muchas veces por segundo, lo que hace que se reproduzcan decenas de efectos de sonido al mismo tiempo. La reproducción de sonido de varias fuentes con una sincronización precisa de forma simultánea es un área en la que la API de Web Audio realmente se destaca.

En el siguiente ejemplo, se crea una ronda de ametralladora a partir de varios muestras de bala individuales creando varias fuentes de sonido cuya reproducción se escalona en el tiempo.

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

Si todas las ametralladoras de tu juego sonarían así, sería bastante aburrido. Por supuesto, variarían según el sonido en función de la distancia del objetivo y la posición relativa (hablaremos más sobre esto más adelante), pero incluso eso podría no ser suficiente. Afortunadamente, la API de Web Audio proporciona una forma de modificar con facilidad el ejemplo anterior de dos maneras:

  1. Con un cambio sutil en el tiempo entre el disparo de las balas
  2. A través de la alteración de playbackRate de cada muestra (también se cambia el tono) para simular mejor la aleatoriedad del mundo real.

Para ver un ejemplo más realista de estas técnicas en acción, consulta la demostración de la mesa de billar, que usa el muestreo aleatorio y varía playbackRate para obtener un sonido de colisión de bolas más interesante.

Sonido posicional 3D

Los juegos suelen desarrollarse en un mundo con algunas propiedades geométricas, ya sea en 2D o en 3D. Si este es el caso, el audio posicionado en estéreo puede aumentar considerablemente la inmersión de la experiencia. Por suerte, la API de Web Audio cuenta con funciones de audio posicional aceleradas por hardware integradas que son bastante sencillas de usar. Por cierto, debes asegurarte de tener bocinas estéreo (preferentemente auriculares) para que el siguiente ejemplo tenga sentido.

En el ejemplo anterior, hay un objeto de escucha (ícono de persona) en el medio del lienzo, y el mouse afecta la posición de la fuente (ícono de bocina). Lo anterior es un ejemplo sencillo del uso de AudioPannerNode para lograr este tipo de efecto. La idea básica del ejemplo anterior es responder al movimiento del mouse configurando la posición de la fuente de audio, de la siguiente manera:

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

Información que debes saber sobre el tratamiento de la espacialización de Web Audio:

  • El objeto de escucha se encuentra en el origen (0, 0, 0) de forma predeterminada.
  • Las APIs de posición de Web Audio no tienen unidades, por lo que introduje un multiplicador para que la demo suene mejor.
  • Web Audio usa las coordenadas cartesianas en las que el eje Y está hacia arriba (lo opuesto a la mayoría de los sistemas de gráficos por computadora). Por eso, estoy cambiando el eje Y en el fragmento anterior.

Avanzado: conos de sonido

El modelo posicional es muy potente y bastante avanzado, y se basa principalmente en OpenAL. Para obtener más detalles, consulta las secciones 3 y 4 de la especificación del vínculo anterior.

Modelo de posición

Hay un solo AudioListener conectado al contexto de la API de Web Audio que se puede configurar en el espacio a través de la posición y la orientación. Cada fuente se puede pasar a través de un AudioPannerNode, que espacializa el audio de entrada. El nodo panorámico tiene posición y orientación, además de un modelo direccional y de distancia.

El modelo de distancia especifica la cantidad de ganancia según la proximidad al origen, mientras que el modelo direccional se puede configurar especificando un cono interno y externo, que determinan la cantidad de ganancia (generalmente negativa) si el objeto de escucha está dentro del cono interno, entre el cono interno y externo, o fuera del cono externo.

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

Aunque mi ejemplo es en 2D, este modelo se generaliza fácilmente a la tercera dimensión. Para ver un ejemplo de sonido espacializado en 3D, consulta este ejemplo de posición. Además de la posición, el modelo de sonido del audio web también incluye de manera opcional la velocidad para los cambios doppler. En este ejemplo, se muestra el efecto Doppler con más detalle.

Para obtener más información sobre este tema, lee este instructivo detallado sobre [cómo mezclar audio posicional y WebGL][webgl].

Efectos y filtros de habitaciones

En realidad, la forma en que se percibe el sonido depende en gran medida de la habitación en la que se escucha. La misma puerta chirriante sonará muy diferente en un sótano en comparación con un gran vestíbulo abierto. Los juegos con un alto valor de producción querrán imitar estos efectos, ya que crear un conjunto de muestras independiente para cada entorno es prohibitivamente costoso y generaría aún más recursos y una mayor cantidad de datos de juegos.

En términos generales, el término de audio para la diferencia entre el sonido sin procesar y la forma en que suena en la realidad es la respuesta al impulso. Estas respuestas de impulso se pueden grabar con mucho cuidado y, de hecho, hay sitios que alojan muchos de estos archivos de respuesta de impulso pregrabados (almacenados como audio) para tu comodidad.

Para obtener más información sobre cómo se pueden crear respuestas por impulso a partir de un entorno determinado, lee la sección "Configuración de grabación" en la sección Convolución de las especificaciones de la API de Web Audio.

Más importante aún para nuestros fines, la API de Web Audio proporciona una manera fácil de aplicar estas respuestas de impulso a nuestros sonidos con 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);

Consulta también esta demostración de efectos de ambiente en la página de especificaciones de la API de Web Audio, así como este ejemplo, que te permite controlar la mezcla seca (sin procesar) y húmeda (procesada mediante convolver) de un gran estándar de jazz.

La cuenta regresiva final

Así que creaste un juego, configuraste tu audio posicional y ahora tienes una gran cantidad de AudioNodes en tu gráfico, todos reproduciéndose de forma simultánea. Genial, pero aún hay un aspecto más que considerar:

Dado que varios sonidos se apilan uno encima del otro sin normalización, es posible que te encuentres en una situación en la que superes el umbral de la capacidad de la bocina. Al igual que las imágenes que superan los límites del lienzo, los sonidos también pueden cortarse si la forma de onda supera su umbral máximo, lo que produce una distorsión distinta. La forma de onda se ve de la siguiente manera:

Recorte

Este es un ejemplo real de recorte en acción. La forma de onda se ve mal:

Recorte

Es importante escuchar distorsiones fuertes como la anterior o, al contrario, mezclas demasiado atenuadas que obliguen a los usuarios a subir el volumen. Si te encuentras en esta situación, debes solucionarla.

Detecta el recorte

Desde una perspectiva técnica, la limitación se produce cuando el valor de la señal en cualquier canal supera el rango válido, es decir, entre -1 y 1. Una vez que se detecta esto, es útil proporcionar comentarios visuales de que esto está sucediendo. Para hacerlo de forma confiable, coloca un JavaScriptAudioNode en tu gráfico. El gráfico de audio se configuraría de la siguiente manera:

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

Además, se podría detectar el recorte en el siguiente controlador 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;
    }
    }
}

En general, ten cuidado de no usar en exceso JavaScriptAudioNode por motivos de rendimiento. En este caso, una implementación alternativa de la medición podría sondear un RealtimeAnalyserNode en el gráfico de audio para getByteFrequencyData, en el momento de la renderización, según lo determine requestAnimationFrame. Este enfoque es más eficiente, pero pierde la mayor parte de la señal (incluidos los lugares en los que podría cortarse), ya que la renderización se produce como máximo 60 veces por segundo, mientras que la señal de audio cambia mucho más rápido.

Debido a que la detección de clips es muy importante, es probable que veamos un nodo integrado de la API de Web Audio MeterNode en el futuro.

Evita el recorte

Si ajustas la ganancia en el AudioGainNode principal, puedes atenuar la mezcla a un nivel que evite el recorte. Sin embargo, en la práctica, como los sonidos que se reproducen en el juego pueden depender de una gran variedad de factores, puede ser difícil decidir el valor de ganancia maestra que evita el recorte para todos los estados. En general, debes ajustar las ganancias para anticipar el peor de los casos, pero esto es más un arte que una ciencia.

Agrega un poco de azúcar.

Los compresores se usan comúnmente en la producción de música y juegos para suavizar el sonido y controlar los picos en el sonido general. Esta funcionalidad está disponible en el mundo de Web Audio a través de DynamicsCompressorNode, que se puede insertar en tu gráfico de audio para brindar un sonido más alto, más rico y más completo, y también ayudar con el recorte. Citando directamente la especificación, este nodo

Por lo general, se recomienda usar la compresión dinámica, en especial en un entorno de juego en el que, como se mencionó antes, no se sabe exactamente qué sonidos se reproducirán ni cuándo. Plink de DinahMoe Labs es un buen ejemplo de esto, ya que los sonidos que se reproducen dependen completamente de ti y de los demás participantes. Un compresor es útil en la mayoría de los casos, excepto en algunos casos excepcionales, en los que se trata de pistas masterizadas con mucho cuidado que ya se ajustaron para sonar “justo bien”.

Para implementar esto, solo debes incluir un DynamicsCompressorNode en tu gráfico de audio, generalmente como el último nodo antes del destino.:

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

Para obtener más detalles sobre la compresión dinámica, este artículo de Wikipedia es muy informativo.

En resumen, escucha con atención si hay recortes y, luego, insértalo para evitarlos. un nodo de ganancia principal. Luego, ajustar toda la combinación con un nodo compresor dinámico. Tu gráfico de audio podría ser similar al siguiente:

Resultado final

Conclusión

Eso abarca lo que creo que son los aspectos más importantes del desarrollo de audio de juegos con la API de Web Audio. Con estas técnicas, puedes crear experiencias de audio realmente atractivas directamente en tu navegador. Antes de despedirme, te dejo una sugerencia específica del navegador: asegúrate de pausar el sonido si la pestaña pasa a segundo plano con la API de visibilidad de la página. De lo contrario, crearás una experiencia potencialmente frustrante para el usuario.

Para obtener más información sobre Web Audio, consulta el artículo de introducción y, si tienes alguna pregunta, comprueba si ya se respondió en las Preguntas frecuentes sobre audio web. Por último, si tienes más preguntas, hazlas en Stack Overflow con la etiqueta web-audio.

Antes de irme, te dejo algunos usos increíbles de la API de Web Audio en juegos reales: