Introducción a la API de Web Audio

Antes del elemento <audio> de HTML5, se necesitaba Flash o algún otro complemento para romper el silencio de la Web. Si bien el audio en la Web ya no requiere un complemento, la etiqueta de audio presenta limitaciones significativas para implementar juegos sofisticados y aplicaciones interactivas.

La API de Web Audio es una API de JavaScript de alto nivel para procesar y sintetizar audio en aplicaciones web. El objetivo de esta API es incluir las funciones que se encuentran en los motores de audio de juegos modernos y algunas de las tareas de mezcla, procesamiento y filtrado que se encuentran en las aplicaciones de producción de audio para computadoras de escritorio modernas. A continuación, se incluye una introducción sencilla al uso de esta potente API.

Un AudioContext sirve para administrar y reproducir todos los sonidos. Para producir un sonido con la API de Web Audio, crea una o más fuentes de sonido y conéctalas al destino de sonido que proporciona la instancia de AudioContext. Esta conexión no tiene que ser directa y puede pasar por cualquier cantidad de AudioNodes intermedios que actúan como módulos de procesamiento para la señal de audio. Este enrutamiento se describe con mayor detalle en la especificación de Web Audio.

Una sola instancia de AudioContext puede admitir varias entradas de sonido y gráficos de audio complejos, por lo que solo necesitaremos una de estas para cada aplicación de audio que creemos.

En el siguiente fragmento, se crea un AudioContext:

var context;
window.addEventListener('load', init, false);
function init() {
    try {
    context = new AudioContext();
    }
    catch(e) {
    alert('Web Audio API is not supported in this browser');
    }
}

Para navegadores más antiguos basados en WebKit, usa el prefijo webkit, como con webkitAudioContext.

Muchas de las funciones interesantes de la API de Web Audio, como la creación de AudioNodes y la decodificación de datos de archivos de audio, son métodos de AudioContext.

Cómo cargar sonidos

La API de Web Audio usa un AudioBuffer para sonidos de corta a mediana duración. El enfoque básico es usar XMLHttpRequest para recuperar archivos de sonido.

La API admite la carga de datos de archivos de audio en varios formatos, como WAV, MP3, AAC, OGG y otros. La compatibilidad del navegador con diferentes formatos de audio varía.

En el siguiente fragmento, se muestra cómo cargar una muestra de sonido:

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    // Decode asynchronously
    request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer = buffer;
    }, onError);
    }
    request.send();
}

Los datos del archivo de audio son binarios (no de texto), por lo que configuramos el responseType de la solicitud en 'arraybuffer'. Para obtener más información sobre ArrayBuffers, consulta este artículo sobre XHR2.

Una vez que se reciben los datos del archivo de audio (sin decodificar), se pueden conservar para decodificarlos más adelante o se pueden decodificar de inmediato con el método decodeAudioData() de AudioContext. Este método toma el ArrayBuffer de datos de archivos de audio almacenados en request.response y lo decodifica de forma asíncrona (no bloquea el subproceso de ejecución principal de JavaScript).

Cuando finaliza decodeAudioData(), llama a una función de devolución de llamada que proporciona los datos de audio PCM decodificados como un AudioBuffer.

Reproducción de sonidos

Un gráfico de audio simple
Un gráfico de audio simple

Una vez que se carguen uno o más AudioBuffers, podremos reproducir sonidos. Supongamos que acabamos de cargar un AudioBuffer con el sonido de un perro ladrando y que la carga terminó. Luego, podemos reproducir este búfer con el siguiente código.

var context = new AudioContext();

function playSound(buffer) {
    var source = context.createBufferSource(); // creates a sound source
    source.buffer = buffer;                    // tell the source which sound to play
    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source.noteOn(0);                          // play the source now
}

Se podría llamar a esta función playSound() cada vez que alguien presiona una tecla o hace clic en algo con el mouse.

La función noteOn(time) facilita la programación de la reproducción de sonido precisa para juegos y otras aplicaciones urgentes. Sin embargo, para que esta programación funcione correctamente, asegúrate de que los búferes de sonido estén precargados.

Cómo abstraer la API de Web Audio

Por supuesto, sería mejor crear un sistema de carga más general que no esté codificado para cargar este sonido específico. Existen muchos enfoques para controlar los muchos sonidos de corta a mediana duración que usaría una aplicación o un juego de audio. Esta es una forma de hacerlo con un BufferLoader (no forma parte del estándar web).

El siguiente es un ejemplo de cómo puedes usar la clase BufferLoader. Creemos dos AudioBuffers y, en cuanto se carguen, vamos a reproducirlos al mismo tiempo.

window.onload = init;
var context;
var bufferLoader;

function init() {
    context = new AudioContext();

    bufferLoader = new BufferLoader(
    context,
    [
        '../sounds/hyper-reality/br-jam-loop.wav',
        '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

    bufferLoader.load();
}

function finishedLoading(bufferList) {
    // Create two sources and play them both together.
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource();
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];

    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.noteOn(0);
    source2.noteOn(0);
}

Cómo trabajar con el tiempo: Cómo reproducir sonidos con ritmo

La API de Web Audio permite a los desarrolladores programar la reproducción con precisión. Para demostrar esto, configuremos una pista de ritmo simple. Probablemente, el patrón de batería más conocido es el siguiente:

Un patrón simple de tambores de rock
Un patrón de batería de rock simple

en el que se toca un hi-hat cada octavo de nota, y el bombo y el redoblante se alternan cada cuarto, en tiempo de 4/4.

Supongamos que cargamos los búferes kick, snare y hihat. El código para hacerlo es simple:

for (var bar = 0; bar < 2; bar++) {
    var time = startTime + bar * 8 * eighthNoteTime;
    // Play the bass (kick) drum on beats 1, 5
    playSound(kick, time);
    playSound(kick, time + 4 * eighthNoteTime);

    // Play the snare drum on beats 3, 7
    playSound(snare, time + 2 * eighthNoteTime);
    playSound(snare, time + 6 * eighthNoteTime);

    // Play the hi-hat every eighth note.
    for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
    }
}

Aquí, solo hacemos una repetición en lugar del bucle ilimitado que vemos en la música impresa. La función playSound es un método que reproduce un búfer en un momento determinado, de la siguiente manera:

function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.noteOn(time);
}

Cómo cambiar el volumen de un sonido

Una de las operaciones más básicas que puedes realizar en un sonido es cambiar el volumen. Con la API de Web Audio, podemos enrutar nuestra fuente a su destino a través de un AudioGainNode para manipular el volumen:

Gráfico de audio con un nodo de ganancia
Gráfico de audio con un nodo de ganancia

Esta configuración de conexión se puede lograr de la siguiente manera:

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

Después de configurar el gráfico, puedes cambiar el volumen de manera programática manipulando gainNode.gain.value de la siguiente manera:

// Reduce the volume.
gainNode.gain.value = 0.5;

Fundido cruzado entre dos sonidos

Ahora, supongamos que tenemos una situación un poco más compleja, en la que estamos reproduciendo varios sonidos, pero queremos hacer una transición entre ellos. Este es un caso común en una aplicación similar a un DJ, en la que tenemos dos tocadiscos y queremos poder desplazarnos de una fuente de sonido a otra.

Esto se puede hacer con el siguiente gráfico de audio:

Gráfico de audio con dos fuentes conectadas a través de nodos de ganancia
Gráfico de audio con dos fuentes conectadas a través de nodos de ganancia

Para configurar esto, simplemente creamos dos AudioGainNodes y conectamos cada fuente a través de los nodos con algo como esta función:

function createSource(buffer) {
    var source = context.createBufferSource();
    // Create a gain node.
    var gainNode = context.createGainNode();
    source.buffer = buffer;
    // Turn on looping.
    source.loop = true;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
    source: source,
    gainNode: gainNode
    };
}

Mezcla de igual potencia

Un enfoque simple de encadenado lineal muestra una caída de volumen a medida que te desplazas lateralmente entre las muestras.

Una transición lineal
Una transición lineal

Para abordar este problema, usamos una curva de potencia igual, en la que las curvas de ganancia correspondientes no son lineales y se cruzan en una amplitud más alta. Esto minimiza las disminuciones de volumen entre las regiones de audio, lo que genera una transición más uniforme entre regiones que pueden ser ligeramente diferentes en el nivel.

Un encadenado de potencias iguales.
Una transición suave de igual potencia

Mezcla de playlists

Otra aplicación común del crossfader es para una aplicación de reproductor de música. Cuando cambia una canción, queremos atenuar la pista actual y atenuar la nueva para evitar una transición discordante. Para ello, programa una transición suave en el futuro. Si bien podríamos usar setTimeout para realizar esta programación, no es precisa. Con la API de Web Audio, podemos usar la interfaz AudioParam para programar valores futuros para parámetros, como el valor de ganancia de un AudioGainNode.

Por lo tanto, dada una playlist, podemos realizar la transición entre pistas programando una disminución de la ganancia en la pista que se está reproduciendo y un aumento de la ganancia en la siguiente, ambas un poco antes de que termine de reproducirse la pista actual:

function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
    // Fade the playNow track in.
    gainNode.gain.linearRampToValueAtTime(0, currTime);
    gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
    // Play the playNow track.
    source.noteOn(0);
    // At the end of the track, fade it out.
    gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
    // Schedule a recursive track change with the tracks swapped.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) - 1000);
}

La API de Web Audio proporciona un conjunto conveniente de métodos RampToValue para cambiar gradualmente el valor de un parámetro, como linearRampToValueAtTime y exponentialRampToValueAtTime.

Si bien la función de tiempo de transición se puede elegir entre las funciones exponenciales y lineales integradas (como se indicó anteriormente), también puedes especificar tu propia curva de valores a través de un array de valores con la función setValueCurveAtTime.

Cómo aplicar un efecto de filtro simple a un sonido

Un gráfico de audio con un BiquadFilterNode
Un gráfico de audio con un BiquadFilterNode

La API de Web Audio te permite canalizar el sonido de un nodo de audio a otro, lo que crea una cadena potencialmente compleja de procesadores para agregar efectos complejos a tus formas de sonido.

Una forma de hacerlo es colocar BiquadFilterNode entre la fuente y el destino de sonido. Este tipo de nodo de audio puede realizar una variedad de filtros de bajo orden que se pueden usar para crear ecualizadores gráficos y hasta efectos más complejos, en su mayoría relacionados con la selección de las partes del espectro de frecuencia de un sonido que se deben enfatizar y las que se deben atenuar.

Entre los tipos de filtros admitidos, se incluyen los siguientes:

  • Filtro pasa-bajo
  • Filtro pasa alto
  • Filtro de pase de banda
  • Filtro de biblioteca baja
  • Filtro de biblioteca alta
  • Filtro de picos
  • Filtro de muesca
  • Filtro para permitir todo

Además, todos los filtros incluyen parámetros para especificar una cantidad de ganancia, la frecuencia con la que se debe aplicar el filtro y un factor de calidad. El filtro pasa-bajos conserva el rango de frecuencia más bajo, pero descarta las frecuencias altas. El punto de corte se determina según el valor de frecuencia, y el factor Q no tiene unidades y determina la forma del gráfico. La ganancia solo afecta a ciertos filtros, como los filtros de aumento y de banda baja, y no a este filtro de paso bajo.

Configuremos un filtro de paso bajo simple para extraer solo las bases de un ejemplo de sonido:

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);

En general, los controles de frecuencia deben ajustarse para funcionar en una escala logarithmica, ya que la audición humana funciona según el mismo principio (es decir, A4 es 440 Hz y A5 es 880 Hz). Para obtener más detalles, consulta la función FilterSample.changeFrequency en el vínculo de código fuente anterior.

Por último, ten en cuenta que el código de muestra te permite conectar y desconectar el filtro, lo que cambia de forma dinámica el gráfico de AudioContext. Podemos desconectar AudioNodes del gráfico llamando a node.disconnect(outputNumber). Por ejemplo, para volver a enrutar el gráfico de pasar por un filtro a una conexión directa, podemos hacer lo siguiente:

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

Más contenido para escuchar

Abordamos los conceptos básicos de la API, incluida la carga y reproducción de muestras de audio. Creamos gráficos de audio con nodos y filtros de ganancia, y ajustes de parámetros de audio y sonidos programados para habilitar algunos efectos de sonido comunes. En este punto, ya puedes comenzar a compilar algunas aplicaciones web de audio.

Si buscas inspiración, muchos desarrolladores ya realizaron un excelente trabajo con la API de Web Audio. Algunas de mis favoritas son

  • AudioJedit, una herramienta de unión de sonido en el navegador que usa permalinks de SoundCloud
  • ToneCraft, un secuenciador de sonido en el que se crean sonidos apilando bloques 3D.
  • Plink, un juego colaborativo para crear música que usa Web Audio y Web Sockets.