Primeiros passos com a API Web Audio

Antes do elemento <audio> do HTML5, o Flash ou outro plug-in era necessário para que a Web não fosse silenciosa. 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 processar e sintetizar áudio em aplicativos da Web. O objetivo dessa API é incluir recursos encontrados em mecanismos de áudio de jogos modernos e algumas das tarefas de mixagem, processamento e filtragem encontradas em aplicativos modernos de produção de áudio para computadores. A seguir, apresentamos uma introdução simples ao uso dessa API avançada.

Como começar a usar o AudioContext

Um AudioContext é usado 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 de 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 para o sinal de áudio. Esse roteamento é descrito em mais detalhes na especificação da 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 aplicativo de áudio que criamos.

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 em 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 curta a média duração. A abordagem básica é usar XMLHttpRequest para buscar arquivos de som.

A API é compatível com o carregamento de dados de arquivos de áudio em vários formatos, como WAV, MP3, AAC, OGG e outros. A compatibilidade do navegador com diferentes formatos de áudio varia.

O snippet a seguir demonstra como carregar 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();
}

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) forem recebidos, eles poderão ser mantidos para decodificação posterior ou decodificados imediatamente usando o método AudioContext decodeAudioData(). Esse método usa o ArrayBuffer de dados de arquivos de áudio armazenados em request.response e os 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.

Tocar sons

Depois que um ou mais AudioBuffers forem carregados, estaremos prontos para tocar 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 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 sempre que alguém pressiona uma tecla ou clica em algo com o mouse.

A função noteOn(time) facilita a programação da reprodução precisa de som para jogos e outros aplicativos em que o tempo é um fator crítico. 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 que 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 curta a média duração que um aplicativo de áudio ou jogo usaria. Veja uma maneira de usar um BufferLoader (não faz parte do padrão da Web).

Veja a seguir um exemplo de como usar a classe BufferLoader. Vamos criar dois AudioBuffers e, assim que 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: tocar 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 bateria mais conhecido é o seguinte:

Um padrão simples de bateria de rock

em que um chimbal é tocado a cada colcheia, e o bumbo e a caixa são tocados alternadamente a cada semínima, em um compasso 4/4.

Supondo que carregamos 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 horário especificado, da seguinte maneira:

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

Fluxograma simples que mostra o áudio fluindo da origem para o GainNode e para o destino.

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, será possível mudar o volume de forma programática manipulando o gainNode.gain.value da seguinte maneira:

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

Transição gradual entre dois sons

Agora, suponha que temos um cenário um pouco mais complexo, em que estamos reproduzindo vários sons, mas queremos fazer um crossfade entre eles. Esse é um caso comum em um aplicativo de DJ, em que temos dois toca-discos e queremos fazer o panning 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 a nós de ganho separados, que são roteados para o mesmo destino.

Para configurar isso, basta criar dois AudioGainNodes e conectar cada fonte pelos nós, usando uma função 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
    };
}

Crossfading de potência igual

Uma abordagem de crossfade linear simples apresenta uma queda de volume ao fazer o panning entre as amostras.

Um crossfade linear representado como duas linhas retas complementares mapeadas como níveis de amplitude ao longo do tempo.
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 ter níveis ligeiramente diferentes.

Um crossfade de potência igual representado como duas linhas curvas mapeadas como níveis de amplitude ao longo do tempo.
Um crossfade de potência igual

Transição gradual de playlists

Outra aplicação comum do crossfader é em um player de música. Quando uma música muda, queremos que a faixa atual desapareça gradualmente e que a nova apareça gradualmente, para evitar uma transição brusca. Para fazer isso, programe um crossfade para o futuro. Embora seja possível 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, em uma playlist, podemos fazer a transição entre as faixas programando uma diminuição de ganho na faixa que está sendo reproduzida e um aumento de ganho na próxima, ambos um pouco antes do fim da reprodução da faixa atual:

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 as lineares e exponenciais integradas (como acima), também é possível 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

Fluxograma simples que mostra o áudio fluindo da origem para o BiquadFilterNode e para o destino.

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

Uma maneira de fazer isso é colocar BiquadFilterNodes entre a origem 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 efeitos ainda mais complexos, principalmente para selecionar quais partes do espectro de frequência de um som enfatizar e quais atenuar.

Os tipos de filtros aceitos incluem:

  • Filtro passa-baixa
  • Filtro passa-alta
  • Filtro de passagem de banda
  • Filtro de prateleira baixa
  • Filtro de prateleira alta
  • Filtro de pico
  • Filtro de entalhe
  • Filtro passa-tudo

Todos os filtros incluem parâmetros para especificar uma quantidade de ganho, a frequência em que o filtro é aplicado e um fator de qualidade. O filtro passa-baixa mantém a faixa 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 unidade e determina a forma do gráfico. O ganho afeta apenas determinados filtros, como os de prateleira baixa e de pico, e não esse filtro de passagem baixa.

Vamos configurar um filtro passa-baixa 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 é 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. Podemos desconectar AudioNodes do gráfico chamando node.disconnect(outputNumber). Por exemplo, para redirecionar 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);

Para ouvir mais

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 de ganho e filtros, além de sons programados e ajustes de parâmetros de áudio para ativar alguns efeitos sonoros comuns. Agora você está pronto para criar aplicativos de áudio da Web incríveis!

Se você está buscando inspiração, muitos desenvolvedores já criaram trabalhos incríveis usando a API Web Audio. Alguns dos meus favoritos incluem:

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