Introducción a la API de Web Audio

Antes del elemento <audio> de HTML5, se requería 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 capacidades 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 de escritorio modernas. A continuación, se incluye una introducción sencilla para usar esta potente API.

Comienza a usar AudioContext

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 necesita 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 más 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 los 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.

Cargando sonidos

La API de Web Audio usa un AudioBuffer para sonidos de duración corta a media. 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 de los navegadores con los 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 texto), por lo que configuramos el responseType de la solicitud como '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 tarde o se pueden decodificar de inmediato con el método decodeAudioData() de AudioContext. Este método toma el ArrayBuffer de los datos del archivo de audio almacenados en request.response y lo decodifica de forma asíncrona (sin bloquear el subproceso de ejecución principal de JavaScript).

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

Reproduciendo sonidos

Una vez que se carguen uno o más AudioBuffers, todo estará listo para reproducir sonidos. Supongamos que acabamos de cargar un AudioBuffer con el sonido de un perro ladrando y que la carga finalizó. 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 precisa de sonido para juegos y otras aplicaciones de tiempo crítico. Sin embargo, para que esta programación funcione correctamente, asegúrate de que los búferes de sonido estén precargados.

Abstracción de la API de Web Audio

Por supuesto, sería mejor crear un sistema de carga más general que no esté hard-coded para cargar este sonido específico. Existen muchos enfoques para abordar los numerosos sonidos de duración corta a media que usaría una aplicación o un juego de audio. Aquí se muestra una forma de usar un BufferLoader (que no forma parte del estándar web).

A continuación, se muestra un ejemplo de cómo puedes usar la clase BufferLoader. Creemos dos AudioBuffers y, en cuanto se carguen, reproduzcámoslos 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 lidiar con el tiempo: Reproduce sonidos con ritmo

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

Un patrón de batería de rock simple

en el que se toca un charles cada corchea, y el bombo y la caja se tocan alternando cada negra, en un compás de 4/4.

Suponiendo 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í, hacemos solo una repetición en lugar del bucle ilimitado que vemos en la partitura. La función playSound es un método que reproduce un búfer en un momento específico, 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 su volumen. Con la API de Web Audio, podemos enrutar nuestra fuente a su destino a través de un AudioGainNode para manipular el volumen:

Diagrama de flujo simple que muestra el flujo de audio de Source a GainNode y a Destination.

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

Una vez que se haya configurado el gráfico, puedes cambiar el volumen de manera programática manipulando el 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 reproducimos varios sonidos, pero queremos hacer un fundido cruzado entre ellos. Este es un caso común en una aplicación similar a la de un DJ, en la que tenemos dos tocadiscos y queremos poder pasar 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 nodos de ganancia independientes, que luego se enrutan al mismo destino.

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

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

Fundido cruzado de potencia igual

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

Un fundido cruzado lineal representado como dos líneas rectas complementarias asignadas como niveles de amplitud a lo largo del tiempo.
Un fundido cruzado lineal

Para solucionar 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 caídas de volumen entre las regiones de audio, lo que genera un fundido cruzado más uniforme entre las regiones que podrían tener niveles un poco diferentes.

Un fundido cruzado de igual potencia representado como dos líneas curvas asignadas como niveles de amplitud a lo largo del tiempo.
Un fundido cruzado de potencia constante

Fundido de playlists

Otro uso común del crossfader es en una aplicación de reproductor de música. Cuando cambia una canción, queremos que la pista actual se atenúe y que la nueva aparezca gradualmente para evitar una transición brusca. Para ello, programa un fundido cruzado para 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 de 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 sincronización de transición se puede elegir entre las lineales y exponenciales 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.

Aplicar un efecto de filtro simple a un sonido

Diagrama de flujo simple que muestra el flujo de audio desde la fuente hasta el BiquadFilterNode y el destino.

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

Una forma de hacerlo es colocar BiquadFilterNode entre la fuente de sonido y el destino. Este tipo de nodo de audio puede realizar una variedad de filtros de orden bajo que se pueden usar para crear ecualizadores gráficos y efectos aún más complejos, principalmente para seleccionar qué partes del espectro de frecuencia de un sonido enfatizar y cuáles atenuar.

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

  • Filtro de paso bajo
  • Filtro de paso alto
  • Filtro de paso de banda
  • Filtro de estante bajo
  • Filtro de estante alto
  • Filtro de realce
  • Filtro de muesca
  • Filtro de paso total

Todos los filtros incluyen parámetros para especificar una cantidad de ganancia, la frecuencia con la que se aplica el filtro y un factor de calidad. El filtro de paso bajo mantiene el rango de frecuencia más bajo, pero descarta las frecuencias altas. El punto de corte se determina por 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 de estante bajo y de pico, y no a este filtro de paso bajo.

Configuremos un filtro de paso bajo simple para extraer solo los graves de una muestra 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 que funcionen en una escala logarítmica, ya que la audición humana funciona con 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 del código fuente que se encuentra más arriba.

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 modo que pase de 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

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

Si buscas inspiración, muchos desarrolladores ya crearon un gran trabajo con la API de Web Audio. Algunas de mis favoritas son las siguientes:

  • AudioJedit, una herramienta de empalme de sonido en el navegador que usa vínculos permanentes de SoundCloud
  • ToneCraft, un secuenciador de sonido en el que los sonidos se crean apilando bloques 3D
  • Plink, un juego colaborativo de creación de música que usa Web Audio y WebSockets