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 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 sencilla para usar esta potente API.

Comienza a usar AudioContext

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 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 ellas 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 webkit prefijo, como con webkitAudioContext.

Muchas de las interesantes funciones 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 longitud 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 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 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 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 termina decodeAudioData(), llama a una función de devolución de llamada que proporciona los datos de audio PCM decodificados como un AudioBuffer.

Cómo reproducir sonidos

Una vez que se cargan uno o más AudioBuffers, estamos listos 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 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 de forma rígida para cargar este sonido específico. Existen muchos enfoques para abordar los numerosos sonidos de longitud corta a media que usaría una aplicación o un juego de audio. Aquí te mostramos una forma de usar un BufferLoader (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 trabajar con el tiempo: reproducir 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 sea el siguiente:

Un patrón de batería de rock simple

en el que se reproduce un hihat cada corchea, y el bombo y la caja se reproducen de forma alterna cada negra, en un compás de 4/4.

Si 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 especificado, 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 quizás quieras realizar con 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 audio que fluye desde 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 configura el gráfico, puedes cambiar el volumen de forma programática manipulando el gainNode.gain.value de la siguiente manera:

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

Cómo hacer un 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 hacer una panorámica 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 separados, que luego se enrutan al mismo destino.

Para configurarlo, simplemente creamos dos AudioGainNodes y conectamos cada fuente a través de los nodos con 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 ingenuo muestra una caída de volumen a medida que haces una panorámica entre las muestras.

Un fundido cruzado lineal representado como dos líneas rectas complementarias que se asignan 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 intersecan 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 pueden ser ligeramente diferentes en el nivel.

Una transición de fundido cruzado de potencia igual representada como dos líneas curvas que se asignan como niveles de amplitud a lo largo del tiempo.
Un fundido cruzado de potencia igual

Fundido cruzado de playlist

Otra aplicación común de fundido cruzado es para una aplicación de reproductor de música. Cuando cambia una canción, queremos que la pista actual se desvanezca y que la nueva se desvanezca para evitar una transición brusca. Para ello, programa un fundido cruzado en el futuro. Si bien podríamos usar setTimeout para hacer 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 hacer la transición entre las pistas programando una disminución de la ganancia en la pista que se está reproduciendo actualmente 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 RampToValue métodos 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 entre las funciones 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.

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

Diagrama de flujo simple que muestra el audio que fluye desde Source a BiquadFilterNode y a Destination.

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 BiquadFilterNodes entre la fuente y el destino del sonido. 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 se deben enfatizar y cuáles se deben atenuar.

Entre los tipos de filtros compatibles, 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 pico
  • Filtro de muesca
  • Filtro de paso total

Todos los filtros incluyen parámetros para especificar una cierta cantidad de ganancia, la frecuencia en la que se debe aplicar 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 filtros de estante bajo y de pico, y no a este filtro de paso bajo.

Configuremos un filtro de paso bajo simple 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 modificarse 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 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 modo que pase 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 información

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 excelentes.

Si buscas inspiración, muchos desarrolladores ya crearon un excelente trabajo con la API de Web Audio. Algunos de mis favoritos incluyen los 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 de creación de música colaborativo que usa Web Audio y Web Sockets.