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 aplicaciones interactivas y juegos sofisticados.

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 modernas de producción de audio para computadoras de escritorio. A continuación, se incluye una introducción gradual al uso de esta potente API.

Cómo comenzar a usar AudioContext

Un objeto AudioContext se usa 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 proporcionado por la instancia de AudioContext. No es necesario que esta conexión sea 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 para cada aplicación de audio que creemos.

El siguiente fragmento 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 AudioBuffer para sonidos de corto a media. El enfoque básico consiste en 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 demuestra 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 su decodificación 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 de archivos de audio almacenados en request.response y lo decodifica de forma asíncrona (sin bloquear el subproceso principal de ejecución de JavaScript).

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

Reproducción de sonidos

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

Una vez que se carguen uno o más AudioBuffers, estará todo listo para reproducir sonidos. Supongamos que acabamos de cargar un elemento AudioBuffer con el sonido del ladrido de un perro 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 presione una tecla o haga 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 la 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é hard-coded para cargar este sonido específico. Existen muchos enfoques para abordar los diversos sonidos de corta a media duración que usaría una aplicación o un juego de audio. Aquí te mostramos una forma de usar BufferLoader (que 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, reprodúcelos 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);
}

Lidiar con el tiempo: reproducir sonidos con ritmo

La API de Web Audio permite a los desarrolladores programar con precisión la reproducción. Para demostrarlo, configuremos una pista rítmica simple. Probablemente, el patrón de drumkit más conocido sea el siguiente:

Un patrón simple de tambor de rock
Un patrón de tambor de rock sencillo

en el que el hihat se toca cada octava nota, y el patada y el redo se tocan alternados cada cuarto, en 4/4.

Si suponemos 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);
}

Cambiar el volumen de un sonido

Una de las operaciones más básicas que puedes llevar a cabo con un sonido es cambiar su volumen. Con la API de Web Audio, podemos enrutar nuestro origen 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 establecer 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 un fundido cruzado 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, solo 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
    };
}

Encadenado de igual potencia

Un enfoque simple de encadenado lineal muestra una disminución de volumen a medida que te desplazas lateralmente por las muestras.

Un encadenado lineal
Un encadenado lineal

Para solucionar este problema, usamos una curva de igual potencia, en la que las curvas de ganancia correspondientes no son lineales y se cruzan a una amplitud más alta. Esto minimiza las caídas de volumen entre las regiones de audio, lo que genera un encadenado más uniforme entre las regiones que puede tener un nivel ligeramente diferente.

Encadenado de igual potencia
Un encadenado de igual potencia

Encadenamiento de playlists

Otra aplicación de Crossfader común es para una aplicación de reproducción de música. Cuando cambia una canción, queremos atenuar la pista actual y la nueva para evitar una transición molesta. Para ello, programa un fundido cruzado en el futuro. Si bien podríamos usar setTimeout para hacer esta programación, esto no es preciso. 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, en una playlist, podemos realizar la transición entre pistas programando una disminución significativa en la pista que se está reproduciendo y un aumento de ganancia en la siguiente, ambos ligeramente 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 la transición se puede elegir de las lineales y exponenciales integradas (como se muestra más arriba), también puedes especificar tu propia curva de valores a través de un array de valores con la función setValueCurveAtTime.

Aplicación de 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 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 BiquadFilterNodes entre la fuente y el destino del sonido. Este tipo de nodo de audio puede hacer una variedad de filtros de bajo orden 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 sustituir.

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

  • Filtro de pase bajo
  • Filtro de pase alto
  • Filtro de pase de banda
  • Filtro de biblioteca baja
  • Filtro de la biblioteca alta
  • Filtro de picos
  • Filtro de muesca
  • Filtro de todos los pases

Todos los filtros incluyen parámetros para especificar cierta 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 interrupción está determinado por el valor de la 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 biblioteca baja y máximo, y no a este filtro de paso bajo.

Configuremos un filtro simple de paso bajo para extraer solo las bases 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 en sí funciona en el mismo principio (es decir, A4 es 440 Hz y A5 es 880 Hz). Para obtener más información, consulta la función FilterSample.changeFrequency en el vínculo del 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 manera dinámica el gráfico de AudioContext. Podemos desconectar AudioNodes del gráfico si llamamos a node.disconnect(outputNumber). Por ejemplo, para redirigir 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);

Escuchar más

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 y filtros de ganancia, y sonidos programados y ajustes de parámetros de audio para habilitar algunos efectos de sonido comunes. Ya tienes todo listo para compilar algunas aplicaciones de audio web dulces.

Si buscas inspiración, muchos desarrolladores ya realizaron un excelente trabajo con la API de Web Audio. Algunos de mis favoritos son los siguientes:

  • AudioJedit, una herramienta de empalme de sonido integrada 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 para crear música que usa Web Audio y Web Sockets.