Primeiros passos com a API Web Audio

Antes do elemento HTML5 <audio>, 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 usada para processar e sintetizar áudio em aplicativos da Web. O objetivo dessa API é incluir recursos encontrados em mecanismos modernos de áudio de jogos e algumas das tarefas de mistura, processamento e filtragem encontradas em aplicativos modernos de produção de áudio para computadores. Confira abaixo uma introdução simples ao uso dessa API avançada.

Introdução ao AudioContext

O AudioContext serve para gerenciar e reproduzir todos os sons. Para produzir um som usando a API Web Audio, crie uma ou mais fontes de som e conecte-as ao destino 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, precisaremos apenas de uma delas para cada aplicativo de áudio que criarmos.

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

Carregando sons

A API Web Audio usa um AudioBuffer para sons de curto a médio duração. 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. O suporte do navegador para diferentes formatos de áudio varia.

O snippet a seguir demonstra o carregamento de uma amostra 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();
}

Como os dados do arquivo de áudio são binários (não texto), 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 podem ser decodificados imediatamente usando o método decodeAudioData() do AudioContext. Esse método usa o ArrayBuffer de dados do arquivo de áudio armazenados em request.response e o decodifica de maneira 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.

Reproduzindo sons

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

Depois que um ou mais AudioBuffers forem carregados, estará tudo pronto para tocar sons. Vamos supor que acabamos de carregar uma AudioBuffer com o som de um cachorro latindo e que o carregamento foi concluído. Depois, podemos reproduzir esse buffer com o código a seguir.

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 toda vez que alguém pressionar uma tecla ou clicar em algo com o mouse.

A função noteOn(time) facilita a programação 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 de áudio da Web

Obviamente, seria melhor criar um sistema de carregamento mais geral que não seja codificado para carregar esse som específico. Há muitas abordagens para lidar com os vários sons de comprimento curto a médio que um app ou jogo de áudio usaria. Confira uma maneira de usar um BufferLoader, que não faz parte do padrão da Web.

Confira abaixo um exemplo de como usar a classe BufferLoader. Vamos criar dois AudioBuffers e, assim que eles forem carregados, vamos reproduzi-los 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: como tocar sons com ritmo

A API de áudio da web permite que os desenvolvedores programem a reprodução de forma precisa. Para demonstrar isso, vamos configurar uma faixa de ritmo simples. Provavelmente, o padrão de bateria mais conhecido é este:

Um padrão simples de bateria 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 de forma alternada a cada quarto, em 4/4.

Supondo que os buffers kick, snare e hihat foram carregados, 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 horário especificado, desta forma:

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 a um som é mudar o volume dele. Com a API Web Audio, podemos rotear nossa origem ao destino por meio de um AudioGainNode para manipular o volume:

Gráfico de áudio com um nó de ganho
Gráfico de áudio com 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);

Após a configuração do gráfico, é possível alterar o volume de forma programática manipulando o gainNode.gain.value da seguinte maneira:

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

Cross-fading entre dois sons

Agora, suponha que temos um cenário um pouco mais complexo, em que estamos reprotando vários sons, mas queremos fazer a transição entre eles. Esse é um caso comum em um aplicativo para DJs, em que temos dois toca-discos e queremos fazer a movimentação 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 pelos nós de ganho
Gráfico de áudio com duas fontes conectadas pelos nós de ganho

Para configurar isso, basta criar dois AudioGainNodes e conectar cada origem por meio dos nós, usando algo semelhante a 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 linear simples de fading cruzado exibe uma queda de volume à medida que você movimenta as amostras.

Um cross-fading linear
Um fading cruzado linear

Para resolver esse problema, usamos uma curva de igual potência, 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 cross-fading mais uniforme entre as regiões que podem ter níveis um pouco diferentes.

Um cross-fading de mesma potência.
Um cross-fading de mesma potência

Crossfading de playlist

Outro aplicativo comum que usa crossfader é para aplicativos de reprodução de música. Quando uma música muda, queremos esmaecer a faixa atual e mostrar a nova para evitar uma transição desagradável. Para fazer isso, programe um crossfade para o futuro. Embora possamos usar setTimeout para fazer essa programação, isso não é preciso. Com a API Web Audio, podemos usar a interface AudioParam para programar valores futuros para 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 em reprodução e um aumento de ganho na próxima faixa, ambos um pouco antes de a faixa atual terminar:

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 fornece 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 as lineares e exponenciais integradas (como acima), você também pode especificar sua própria curva de valor por meio de uma matriz de valores usando a função setValueCurveAtTime.

Como aplicar um efeito de filtro simples a um som

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

A API Web Audio permite canalizar o som de um nó de áudio para outro, criando uma cadeia de processadores potencialmente complexa para adicionar efeitos complexos às formas sonoras.

Uma maneira de fazer isso é colocar BiquadFilterNodes entre a origem e o destino do som. Esse tipo de nó de áudio pode realizar vários 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 diminuir.

Os tipos de filtros compatíveis incluem:

  • Filtro de passagem de baixas frequências
  • Filtro de passagem de altas frequências
  • Filtro de passagem de banda
  • Filtro de nível baixo
  • Filtro de nível alto
  • Filtro de pico
  • Filtro de entalhe
  • Filtro de todos os cartões

Além disso, todos os filtros incluem parâmetros para especificar uma determinada quantidade de ganho, a frequência com que o filtro será aplicado e um fator de qualidade. O filtro de passagem baixa mantém o intervalo de frequência mais baixo, mas descarta as frequências altas. O ponto de interrupção é determinado pelo valor da frequência, o fator Q não tem unidades e determina a forma do gráfico. O ganho afeta apenas determinados filtros, como os de nível inferior e de pico, e não o filtro de passagem de baixas frequências.

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 própria audição humana funciona com base no mesmo princípio (ou seja, A4 é de 440 hz e A5 é 880 Hz). Para mais detalhes, consulte a função FilterSample.changeFrequency no link do código-fonte acima.

Por fim, observe que o exemplo de código permite conectar e desconectar o filtro, mudando dinamicamente o gráfico do AudioContext. É possível desconectar AudioNodes do gráfico chamando node.disconnect(outputNumber). Por exemplo, para redirecionar o gráfico de um filtro a uma conexão direta, faça o seguinte:

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

Como ouvir mais

Abordamos os conceitos básicos da API, incluindo como carregar e reproduzir amostras de áudio. Criamos gráficos de áudio com nós e filtros de ganho e programamos sons e ajustes de parâmetros de áudio para permitir alguns efeitos sonoros comuns. Agora, você está pronto para criar alguns aplicativos incríveis de áudio da Web.

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

  • AudioJedit, uma ferramenta de fusão de som no navegador que usa links permanentes do SoundCloud.
  • ToneCraft, um sequenciador de som em que os sons são criados ao empilhar blocos 3D.
  • Plink, um jogo colaborativo de criação musical que usa Web Audio e Web Sockets.