Desarrollo de audio de juegos con la API de Web Audio

Introducción

El audio es una parte fundamental de lo que hace que las experiencias multimedia sean tan atractivas. Si alguna vez intentaste ver una película con el sonido desactivado, es probable que te hayas dado cuenta.

Los juegos no son la excepción. Mis mejores recuerdos de videojuegos son de la música y los efectos de sonido. En muchos casos, casi dos décadas después de jugar a mis favoritos, todavía no me salen de la cabeza las composiciones de Zelda de Koji Kondo ni la banda sonora Diablo de Matt Uelmen. El mismo atractivo se aplica a 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 un juego presenta algunos desafíos interesantes. Para crear música de juego convincente, los diseñadores deben adaptarse a estados de juego potencialmente impredecibles en el que se encuentra un jugador. En la práctica, algunas partes del juego pueden continuar durante un período desconocido, y los sonidos pueden interactuar con el entorno y mezclarse de maneras complejas, como con efectos de habitación y posicionamiento de sonido relativo. Por último, puede haber una gran cantidad de sonidos que se reproducen a la vez, los cuales deben sonar bien juntos y renderizarse sin generar penalizaciones de rendimiento.

Audio de juegos en la Web

Para juegos simples, usar la etiqueta <audio> puede ser suficiente. Sin embargo, muchos navegadores proporcionan implementaciones deficientes, que generan fallas de audio y una latencia alta. Esperamos que este sea un problema temporal, ya que los proveedores están trabajando arduamente para mejorar sus respectivas implementaciones. Para obtener una idea del estado de la etiqueta <audio>, hay un buen conjunto de pruebas en areweplayingyet.org.

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

  • No se pueden 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
  • Sin tiempos precisos.

En el resto del artículo, analizaremos 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 reproducir música de fondo indefinidamente.

Puede volverse muy molesto si el bucle es corto y predecible. Si un jugador está atascado en un área o nivel, y la misma muestra se reproduce de forma continua en segundo plano, puede valer la pena atenuar gradualmente la pista para evitar una mayor frustración. Otra estrategia es hacer combinaciones de varias intensidades que se encadenan gradualmente entre sí, según el contexto del juego.

Por ejemplo, si el jugador se encuentra en una zona en la que se emocionó una batalla épica contra jefes, es posible que tengas varias mezclas con diferentes niveles de emociones, desde atmosféricos hasta previsibles o intensos. El software de síntesis de música a menudo te permite exportar varias mezclas (de la misma duración) basadas en una pieza mediante la selección del conjunto de pistas que usarás en la exportación. De esa manera, tendrás cierta coherencia interna y evitas tener transiciones molestas a medida que realizas el fundido cruzado de una pista a otra.

Garajeband

Luego, con la API de Web Audio, puedes importar todas estas muestras con algo como la clase BufferLoader mediante XHR (esto se describe en más detalle en el artículo de introducción de la API de Web Audio. La carga de sonidos lleva tiempo, por lo que los elementos 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 está jugando.

A continuación, crearás una fuente para cada nodo y un nodo de ganancia para cada fuente, y conectarás el grafo.

Después de hacer esto, 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 permanecerán 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 usando 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 juegan dos fuentes a la vez y encadenamos entre ellas usando curvas de igual potencia (como se describe en la introducción).

En la actualidad, muchos desarrolladores de juegos usan la etiqueta <audio> para la música de fondo, ya que es adecuada para transmitir contenido. Ahora puedes incorporar contenido de la etiqueta <audio> en un contexto de audio web.

Esta técnica puede ser útil, ya que la etiqueta <audio> puede funcionar con contenido de transmisión, lo que te permite reproducir inmediatamente la música en segundo plano en lugar de tener que esperar a que se descargue toda. Si llevas la transmisión a la API de Web Audio, puedes manipularla o analizarla. 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

A menudo, los juegos reproducen 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 en muy poco tiempo. Para evitar esto, suele ser útil tener un conjunto de sonidos similares pero diferentes para reproducir. Esto puede variar desde variaciones leves de las muestras de pasos hasta variaciones drásticas, como se ve en la serie de Warcraft, en respuesta a los clics en las unidades.

Otra función clave de los efectos de sonido en los juegos es que puede haber muchos de ellos al mismo tiempo. Imagina que estás en medio de un tiroteo con varios actores que disparan ametralladoras. Cada ametralladora se dispara muchas veces por segundo, lo que provoca que se reproduzcan decenas de efectos de sonido al mismo tiempo. La reproducción de sonido desde múltiples fuentes sincronizadas con precisión al mismo tiempo es un punto en el que la API de Web Audio realmente se destaca.

En el siguiente ejemplo, se crea una ronda de ametralladora a partir de varias muestras de balas individuales mediante la creación de 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 del juego sonaran exactamente así, sería bastante aburrido. Por supuesto, variarían en función del sonido en función de la distancia desde el objetivo y la posición relativa (hablaremos de esto más adelante), pero incluso eso podría no ser suficiente. Afortunadamente, la API de Web Audio proporciona una forma de modificar fácilmente el ejemplo anterior de dos maneras:

  1. Con un cambio de tiempo sutil entre los disparos
  2. Modificando laPlayRate de cada muestra (también cambiando el tono) para simular mejor la aleatorización del mundo real

Para ver un ejemplo más real de estas técnicas en acción, observa la demostración de la mesa de billar, que usa un muestreo aleatorio y varía la frecuencia de reproducción para lograr un sonido de colisión de la pelota más interesante.

Sonido posicional 3D

Por lo general, los juegos se desarrollan 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 en gran medida la experiencia envolvente. Afortunadamente, la API de Web Audio incluye funciones de audio posicional acelerado por hardware integradas que son fáciles de usar. Por cierto, deberías 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). El ejemplo anterior es un ejemplo simple del uso de AudioPannerNode para lograr este tipo de efecto. La idea básica del ejemplo anterior es establecer la posición de la fuente de audio para responder al movimiento del mouse, 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:

  • De forma predeterminada, el objeto de escucha se encuentra en el origen (0, 0, 0).
  • Las APIs de posicionamiento de Web Audio no tienen unidades, por lo que presentamos un multiplicador para que la demostración suene mejor.
  • Web Audio usa las coordenadas cartesianas de y-is-up (lo opuesto a la mayoría de los sistemas de gráficos por computadora). Por eso estoy intercambiando el eje Y del fragmento anterior

Avanzado: conos de sonido

El modelo posicional es muy potente y bastante avanzado, y se basa en gran medida en OpenAL. Para obtener más detalles, consulta las secciones 3 y 4 de la especificación antes vinculada.

Modelo de posición

Hay un solo AudioListener adjunto 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 objeto AudioPannerNode, que espacializa el audio de entrada. El nodo de desplazamiento lateral tiene posición y orientación, así como un modelo de distancia y dirección.

El modelo de distancia especifica la cantidad de ganancias según la proximidad a la fuente, mientras que el modelo direccional se puede configurar si especificas un cono interno y el exterior, que determinan la cantidad de ganancia (por lo general, negativa) si el objeto de escucha se encuentra 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 con facilidad a la tercera dimensión. Para ver un ejemplo de sonido espacial en 3D, consulta esta muestra posicional. Además de la posición, el modelo de sonido de Web Audio también incluye de manera opcional la velocidad para los cambios Doppler. En este ejemplo, se muestra el efecto Doppler en más detalle.

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

Filtros y efectos de habitación

En realidad, la forma en que se percibe el sonido depende en gran medida de la habitación en la que se escucha el sonido. La misma puerta crujiente sonará muy diferente en un sótano, en comparación con un gran salón abierto. Los juegos con alto valor de producción deberían imitar estos efectos, ya que crear un conjunto separado de muestras para cada entorno es demasiado costoso, y generaría aún más elementos y una mayor cantidad de datos del juego.

En pocas palabras, el término de audio que hace referencia a la diferencia entre el sonido crudo y la forma en que suena en realidad es la respuesta impulsiva. Estas respuestas impulsivas se pueden registrar minuciosamente y, de hecho, hay sitios que alojan muchos de estos archivos de respuesta impulsiva pregrabados (almacenados como audio) para tu comodidad.

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

Y, lo que es más importante para nuestros fines, la API de Web Audio proporciona una manera fácil de aplicar estas respuestas impulsivas 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 habitación en la página de especificaciones de la API de Web Audio, así como este ejemplo, que te da control sobre las mezclas secas (sin procesar) y húmedos (procesados mediante convolver) de un excelente estándar de jazz.

Cuenta regresiva final

Creaste un juego, configuraste tu audio posicional y ahora tienes una gran cantidad de AudioNodes en tu gráfico, todos reproducidos de forma simultánea. Perfecto, pero todavía hay una cosa más a tener en cuenta:

Dado que varios sonidos se apilan unos sobre otros sin normalización, es posible que te encuentres en una situación en la que superes el umbral de capacidad de tu bocina. Al igual que las imágenes que superan los límites del lienzo, los sonidos también pueden recortarse si la forma de onda excede su umbral máximo, lo que produce una distorsión clara. 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, por el contrario, mezclas demasiado discretas que obliguen a los oyentes a subir el volumen. Si estás en esta situación, realmente debes solucionarlo.

Detectar recortes

Desde una perspectiva técnica, el recorte ocurre cuando el valor de la señal en cualquier canal excede el rango válido, es decir, entre -1 y 1. Una vez que se detecta esto, es útil brindar información visual sobre lo que está sucediendo. Para hacerlo de manera 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 demasiado JavaScriptAudioNode por motivos de rendimiento. En este caso, una implementación alternativa de medición podría sondear un RealtimeAnalyserNode en el gráfico de audio para getByteFrequencyData, en el tiempo de renderización, como lo determina requestAnimationFrame. Este enfoque es más eficiente, pero pasa por alto la mayor parte de la señal (incluidos los lugares donde se puede recortar), ya que la renderización ocurre como máximo 60 veces por segundo, mientras que la señal de audio cambia mucho más rápido.

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

Evitar recortes

Si ajustas la ganancia del AudioGainNode principal, puedes atenuar la mezcla a un nivel que impida el recorte. Sin embargo, en la práctica, debido a que 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 principal que evita el recorte en todos los estados. En general, deberías ajustar las ganancias para anticiparte al peor de los casos, pero esto es más un arte que una ciencia.

Agregue un poco de azúcar

Los compresores se usan comúnmente en la producción de música y videojuegos para suavizar la señal y controlar los aumentos repentinos en la señal general. Esta funcionalidad está disponible en el mundo del audio web a través de DynamicsCompressorNode, que se puede insertar en tu gráfico de audio para proporcionar un sonido más fuerte, nítido y más completo, y también para ayudar con el recorte. que cita la especificación directamente, este nodo

Por lo general, el uso de la compresión dinámica es una buena idea, en especial en la configuración de un juego, en el que, como se explicó anteriormente, no se sabe exactamente qué sonidos se reproducirán y cuándo. Plink de los labs de DinahMoe es un excelente ejemplo de esto, ya que los sonidos que se reproducen dependen por completo de ti y de otros participantes. Un compresor es útil en la mayoría de los casos, excepto en algunos casos poco comunes, en los que se trata de pistas cuidadosamente maestras que ya se ajustaron para sonar "bien".

La implementación de esto es solo cuestión de incluir un DynamicsCompressorNode en tu gráfico de audio, por lo general, 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 atentamente si hay recortes y evita que se produzcan mediante la inserción de un nodo de ganancia principal. Luego, ajusta toda la mezcla con un nodo de compresor dinámico. Tu gráfico de audio podría verse de la siguiente manera:

Resultado final

Conclusión

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

Para obtener información adicional sobre Web Audio, consulta el artículo introductorio de introducción y, si tienes alguna pregunta, consulta 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 cerrarla, quiero mostrarte algunos de los usos increíbles de la API de Web Audio en juegos reales: