Primeiros passos com a API Web Audio

Antes do elemento <audio> do HTML5, o Flash ou outro plug-in era necessário para quebrar o silêncio da Web. Embora o áudio na Web não precise mais de um plug-in, a tag de áudio traz limitações significativas para a implementação de jogos sofisticados e aplicativos interativos.

A API Web Audio é uma API JavaScript de alto nível para processamento e síntese de áudio em aplicativos da Web. O objetivo dessa API é incluir recursos encontrados nos mecanismos modernos de áudio de jogos e algumas das tarefas de mixagem, processamento e filtragem encontradas em aplicativos modernos de produção de áudio para computadores. A seguir, uma introdução simples sobre como usar essa API poderosa.

Introdução ao AudioContext

Um AudioContext serve para gerenciar e tocar todos os sons. Para produzir um som usando a API Web Audio, crie uma ou mais fontes de som e conecte-as ao destino do som fornecido pela instância AudioContext. Essa conexão não precisa ser direta e pode passar por qualquer número de AudioNodes intermediários que atuam como módulos de processamento do sinal de áudio. Esse roteamento é descrito com mais detalhes na especificação do Web Audio.

Uma única instância de AudioContext pode oferecer suporte a várias entradas de som e gráficos de áudio complexos. Portanto, só precisamos de uma delas para cada aplicação de áudio criada.

O snippet a seguir cria um 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 mais antigos baseados em WebKit, use o prefixo webkit, como webkitAudioContext.

Muitas das funcionalidades interessantes da API Web Audio, como a criação de AudioNodes e a decodificação de dados de arquivos de áudio, são métodos de AudioContext.

Sons de carregamento

A API Web Audio usa um AudioBuffer para sons de comprimento curto a médio. A abordagem básica é usar XMLHttpRequest para buscar arquivos de som.

A API oferece suporte ao carregamento de dados de arquivos de áudio em vários formatos, como WAV, MP3, AAC, OGG e outros. A compatibilidade dos navegadores com diferentes formatos de áudio varia.

O snippet abaixo demonstra o carregamento de um exemplo de som:

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

Os dados do arquivo de áudio são binários (não texto), então definimos o responseType da solicitação como 'arraybuffer'. Para mais informações sobre ArrayBuffers, consulte este artigo sobre XHR2.

Depois que os dados do arquivo de áudio (não decodificados) são recebidos, eles podem ser mantidos para decodificação posterior ou decodificados imediatamente usando o método decodeAudioData() do AudioContext. Esse método usa o ArrayBuffer dos dados do arquivo de áudio armazenados em request.response e o decodifica de forma assíncrona, sem bloquear a linha de execução principal do JavaScript.

Quando decodeAudioData() é concluído, ele chama uma função de callback que fornece os dados de áudio PCM decodificados como um AudioBuffer.

Como reproduzir sons

Um gráfico de áudio simples
Um gráfico de áudio simples

Quando um ou mais AudioBuffers são carregados, podemos reproduzir sons. Vamos supor que acabamos de carregar um AudioBuffer com o som de um cachorro latindo e que o carregamento foi concluído. Em seguida, podemos reproduzir esse buffer com o seguinte 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
}

Essa função playSound() pode ser chamada sempre que alguém pressionar uma tecla ou clicar em algo com o mouse.

A função noteOn(time) facilita o agendamento de reproduções de som precisas para jogos e outros aplicativos urgentes. No entanto, para que essa programação funcione corretamente, verifique se os buffers de som estão pré-carregados.

Como abstrair a API Web Audio

Claro, seria melhor criar um sistema de carregamento mais geral que não seja fixado para carregar esse som específico. Há muitas abordagens para lidar com os muitos sons de duração curta a média que um aplicativo de áudio ou jogo usaria. Aqui está uma maneira de usar um BufferLoader (não faz parte do padrão da Web).

Confira a seguir um exemplo de como usar a classe BufferLoader. Vamos criar dois AudioBuffers e, assim que eles forem carregados, vamos reproduzir ao mesmo tempo.

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

Como lidar com o tempo: tocando sons com ritmo

A API Web Audio permite que os desenvolvedores programem a reprodução com precisão. Para demonstrar isso, vamos configurar uma faixa de ritmo simples. Provavelmente, o padrão de drumkit mais conhecido é o seguinte:

Um padrão de bateria simples de rock
Um padrão simples de bateria de rock

em que um hihat é tocado a cada colcheia, e o chute e a caixa são tocados alternadamente a cada trimestre, em 4/4.

Supondo que tenhamos carregado os buffers kick, snare e hihat, o código para fazer isso é simples:

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

Aqui, fazemos apenas uma repetição em vez do loop ilimitado que vemos na partitura. A função playSound é um método que reproduz um buffer em um momento especificado, conforme mostrado abaixo:

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

Como mudar o volume de um som

Uma das operações mais básicas que você pode fazer com um som é mudar o volume dele. Usando a API Web Audio, podemos encaminhar nossa origem para o destino usando um AudioGainNode para manipular o volume:

Gráfico de áudio com um nó de ganho
Gráfico de áudio com um nó de ganho

Essa configuração de conexão pode ser feita da seguinte maneira:

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

Depois que o gráfico for configurado, você poderá mudar o volume de maneira programática, manipulando o gainNode.gain.value da seguinte maneira:

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

Transição entre dois sons

Agora, suponha que temos um cenário um pouco mais complexo, em que tocamos vários sons, mas queremos fazer um crossfade entre eles. Esse é um caso comum em um aplicativo semelhante a um DJ, em que temos dois toca-discos e queremos poder fazer a panorâmica de uma fonte de som para outra.

Isso pode ser feito com o seguinte gráfico de áudio:

Gráfico de áudio com duas fontes conectadas por nós de ganho
Gráfico de áudio com duas fontes conectadas por nós de ganho

Para configurar isso, basta criar dois AudioGainNodes e conectar cada fonte pelos nós, usando algo como esta função:

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

Crossfading de mesma potência

Uma abordagem de transição linear simples mostra uma queda de volume à medida que você alterna entre as amostras.

Um cross-fading linear
Um crossfade linear

Para resolver esse problema, usamos uma curva de potência igual, em que as curvas de ganho correspondentes não são lineares e se cruzam em uma amplitude maior. Isso minimiza as quedas de volume entre as regiões de áudio, resultando em um crossfade mais uniforme entre regiões que podem ser ligeiramente diferentes no nível.

Um crossfade de potência igual.
Um crossfade de igual potência

Transição entre músicas em playlists

Outro aplicativo comum de crossfader é um aplicativo de player de música. Quando uma música muda, queremos esmaecer a faixa atual e a nova para evitar uma transição desagradável. Para fazer isso, programe uma transição suave para o futuro. Embora possamos usar setTimeout para fazer essa programação, ela não é precisa. Com a API Web Audio, podemos usar a interface AudioParam para programar valores futuros de parâmetros, como o valor de ganho de um AudioGainNode.

Assim, com uma playlist, podemos fazer a transição entre as faixas programando uma redução de ganho na faixa que está sendo reproduzida e um aumento de ganho na próxima, ambos um pouco antes de a faixa atual terminar de ser reproduzida:

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

A API Web Audio oferece um conjunto conveniente de métodos RampToValue para mudar gradualmente o valor de um parâmetro, como linearRampToValueAtTime e exponentialRampToValueAtTime.

Embora a função de tempo de transição possa ser escolhida entre linear e exponencial integradas (como acima), você também pode especificar sua própria curva de valor usando uma matriz de valores com a função setValueCurveAtTime.

Aplicar um efeito de filtro simples a um som

Um gráfico de áudio com um BiquadFilterNode
Um gráfico de áudio com um BiquadFilterNode

A API Web Audio permite transmitir o som de um nó de áudio para outro, criando uma cadeia potencialmente complexa de processadores para adicionar efeitos complexos aos seus sons.

Uma maneira de fazer isso é colocar BiquadFilterNodes entre a fonte e o destino do som. Esse tipo de nó de áudio pode fazer uma variedade de filtros de ordem baixa que podem ser usados para criar equalizadores gráficos e até efeitos mais complexos, principalmente para selecionar quais partes do espectro de frequência de um som serão enfatizadas e quais subtrair.

Os tipos de filtros compatíveis incluem:

  • Filtro passa-baixa
  • Filtro passa-alta
  • Filtro passa-banda
  • Filtro de prateleira baixa
  • Filtro de nível alto
  • Filtro de pico
  • Filtro de entalhe
  • Filtro "aprovar tudo"

Todos os filtros incluem parâmetros para especificar uma quantidade de ganho, a frequência de aplicação do filtro e um fator de qualidade. O filtro passa-baixa mantém o intervalo de frequência mais baixa, mas descarta as frequências altas. O ponto de interrupção é determinado pelo valor da frequência, e o fator Q não tem unidades e determina a forma do gráfico. O ganho afeta apenas alguns filtros, como os filtros de prateleira baixa e de pico, e não esse filtro passa-baixa.

Vamos configurar um filtro de passagem de baixas frequências simples para extrair apenas as bases de uma amostra de som:

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

Em geral, os controles de frequência precisam ser ajustados para funcionar em uma escala logarítmica, já que a audição humana funciona com base no mesmo princípio (ou seja, A4 é 440 Hz e A5 é 880 Hz). Para mais detalhes, consulte a função FilterSample.changeFrequency no link do código-fonte acima.

Por fim, o código de exemplo permite conectar e desconectar o filtro, mudando dinamicamente o gráfico AudioContext. Podemos desconectar AudioNodes do gráfico chamando node.disconnect(outputNumber). Por exemplo, para rotear novamente o gráfico de um filtro para uma conexão direta, podemos fazer o seguinte:

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

Mais músicas

Abordamos os conceitos básicos da API, incluindo o carregamento e a reprodução de amostras de áudio. Criamos gráficos de áudio com nós e filtros de ganho, além de sons programados e ajustes de parâmetros de áudio para ativar alguns efeitos sonoros comuns. Agora você está pronto para criar alguns aplicativos de áudio da Web.

Se você está em busca de inspiração, muitos desenvolvedores já criaram ótimos trabalhos usando a API Web Audio. Alguns dos meus favoritos incluem:

  • AudioJedit, uma ferramenta de junção de áudio no navegador que usa os links permanentes do SoundCloud.
  • ToneCraft, um sequenciador de som em que os sons são criados empilhando blocos 3D.
  • Plink, um jogo colaborativo de criação de música que usa o Web Audio e o WebSockets.