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 implementar jogos sofisticados e aplicativos interativos.

A API Web Audio é uma API JavaScript de alto nível para processamento e sintetização de á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 de produção de áudio para computadores modernos. A seguir, uma introdução simples sobre como usar essa API poderosa.

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 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 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ó vamos precisar 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 curtos a médios. A abordagem básica é usar o 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 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) forem recebidos, eles poderão 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 de execução 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

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 pressiona uma tecla ou clica em algo com o mouse.

A função noteOn(time) facilita a programação de reprodução de som precisa para jogos e outros aplicativos de tempo 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, 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);
}

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 drumkit mais conhecido é o seguinte:

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

em que um hi-hat é tocado a cada colcheia, e o bumbo e a caixa são tocados alternadamente a cada quarto, em tempo 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. 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
   
};
}

Equalização de potência

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

Um crossfade 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 potência igual

Transição entre músicas em playlists

Outro aplicativo comum de crossfader é para um app de player de música. Quando uma música muda, queremos diminuir o volume da faixa atual e aumentar o volume da nova para evitar uma transição brusca. 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 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 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.

Aplicação de 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 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 fonte e o destino do som. Esse tipo de nó de áudio pode fazer 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 devem ser enfatizadas e quais devem ser atenuadas.

Os tipos de filtros compatíveis incluem:

  • Filtro passa-baixa
  • Filtro passa-alta
  • Filtro passa-banda
  • Filtro de prateleira baixa
  • Filtro de prateleira alta
  • 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 este filtro passa-baixa.

Vamos configurar um filtro de passagem 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 audição humana funciona com o 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 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);

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 Web Sockets.