Estudo de caso: JAM com Chrome

Como fizemos o áudio ser incrível

Oskar Eriksson
Oskar Eriksson

Introdução

O JAM with Chrome é um projeto musical criado pelo Google. O JAM with Chrome permite que pessoas de todo o mundo formem uma banda e toquem em tempo real no navegador. Nós, da DinahMoe, tivemos o prazer de participar deste projeto. Nosso papel era produzir músicas para o aplicativo e projetar e desenvolver o componente musical. O desenvolvimento consistiu em três áreas principais: uma estação de trabalho de música, incluindo reprodução MIDI, samplers de software, efeitos de áudio, roteamento e mixagem; um mecanismo de lógica musical para controlar a música de forma interativa em tempo real e um componente de sincronização que garante que todos os músicos em uma sessão ouçam a música exatamente ao mesmo tempo, um pré-requisito para tocar juntos.

Para alcançar o maior nível possível de autenticidade, precisão e qualidade de áudio, optamos por usar a API Web Audio. Neste estudo de caso, vamos discutir alguns dos desafios que enfrentamos e como os resolvemos. Já existem vários artigos introdutórios aqui no HTML5Rocks para você começar a usar o Web Audio. Por isso, vamos direto ao ponto.

Como gravar efeitos de áudio personalizados

A API Web Audio tem vários efeitos úteis incluídos na especificação, mas precisávamos de efeitos mais elaborados para nossos instrumentos no JAM com o Chrome. Por exemplo, há um nó de atraso nativo no Web Audio, mas há muitos tipos de atrasos: delay estéreo, delay ping pong, delay slapback e a lista continua. Felizmente, é possível criar todos esses efeitos no Web Audio usando os nós de efeito nativo e um pouco de imaginação.

Como queríamos usar os nós nativos e nossos próprios efeitos personalizados da maneira mais transparente possível, decidimos que precisávamos criar um formato de wrapper que pudesse fazer isso. Os nós nativos no Web Audio usam o método de conexão para vincular nós. Por isso, precisamos emular esse comportamento. A ideia básica é assim:

var MyCustomNode = function(){
    this.input = audioContext.createGain();
    var output = audioContext.createGain();

    this.connect = function(target){
       output.connect(target);
    };
};

Com esse padrão, estamos muito próximos dos nós nativos. Vamos conferir como isso seria usado.

//create a couple of native nodes and our custom node
var gain = audioContext.createGain(),
    customNode = new MyCustomNode(),
    anotherGain = audioContext.createGain();

//connect our custom node to the native nodes and send to the output
gain.connect(customNode.input);
customNode.connect(anotherGain);
anotherGain.connect(audioContext.destination);
Roteamento do nó personalizado

A única diferença entre nosso nó personalizado e um nativo é que precisamos nos conectar à propriedade de entrada de nós personalizados. Tenho certeza de que há maneiras de contornar isso, mas isso foi suficiente para nossos propósitos. Esse padrão pode ser desenvolvido para simular os métodos de desconexão de AudioNodes nativos, além de acomodar entradas/saídas definidas pelo usuário ao se conectar e assim por diante. Consulte a especificação para saber o que os nós nativos podem fazer.

Agora que temos o padrão básico para criar efeitos personalizados, a próxima etapa é dar ao nó personalizado um comportamento personalizado. Vamos analisar um nó de atraso de slapback.

Reação negativa

O slapback delay, também chamado de slapback echo, é um efeito clássico usado em vários instrumentos, de vocais no estilo dos anos 50 a guitarras de surf. O efeito pega o som recebido e reproduz uma cópia dele com um pequeno atraso de aproximadamente 75 a 250 milissegundos. Isso dá a sensação de que o som está sendo rebatido, daí o nome. Podemos criar o efeito assim:

var SlapbackDelayNode = function(){
    //create the nodes we'll use
    this.input = audioContext.createGain();
    var output = audioContext.createGain(),
        delay = audioContext.createDelay(),
        feedback = audioContext.createGain(),
        wetLevel = audioContext.createGain();

    //set some decent values
    delay.delayTime.value = 0.15; //150 ms delay
    feedback.gain.value = 0.25;
    wetLevel.gain.value = 0.25;

    //set up the routing
    this.input.connect(delay);
    this.input.connect(output);
    delay.connect(feedback);
    delay.connect(wetLevel);
    feedback.connect(delay);
    wetLevel.connect(output);

    this.connect = function(target){
       output.connect(target);
    };
};
Roteamento interno do nó de slapback

Como alguns de vocês já perceberam, esse atraso também pode ser usado com tempos de atraso maiores e, assim, se tornar um delay mono regular com feedback. Confira um exemplo de uso desse atraso para saber como ele soa.

Roteamento de áudio

Ao trabalhar com diferentes instrumentos e partes musicais em aplicativos de áudio profissionais, é essencial ter um sistema de roteamento flexível que permita mixar e modular os sons de maneira eficaz. No JAM com o Chrome, desenvolvemos um sistema de áudio semelhante aos encontrados em placas de mixagem físicas. Isso permite conectar todos os instrumentos que precisam de um efeito de reverberação a um bus ou canal comum e adicionar o reverberação a esse bus em vez de adicionar um reverberação a cada instrumento separado. Essa é uma otimização importante, e é bastante recomendável fazer algo semelhante assim que você começar a criar aplicativos mais complexos.

Roteamento do AudioBus

Felizmente, isso é muito fácil de fazer no Web Audio. Basicamente, podemos usar o esqueleto definido para os efeitos da mesma maneira.

var AudioBus = function(){
    this.input = audioContext.createGain();
    var output = audioContext.createGain();

    //create effect nodes (Convolver and Equalizer are other custom effects from the library presented at the end of the article)
    var delay = new SlapbackDelayNode(),
        convolver = new tuna.Convolver(),
        equalizer = new tuna.Equalizer();

    //route 'em
    //equalizer -> delay -> convolver
    this.input.connect(equalizer);
    equalizer.connect(delay.input);
    delay.connect(convolver);
    convolver.connect(output);

    this.connect = function(target){
       output.connect(target);
    };
};

Ele seria usado assim:

//create some native oscillators and our custom audio bus
var bus = new AudioBus(),
    instrument1 = audioContext.createOscillator(),
    instrument2 = audioContext.createOscillator(),
    instrument3 = audioContext.createOscillator();

//connect our instruments to the same bus
instrument1.connect(bus.input);
instrument2.connect(bus.input);
instrument3.connect(bus.input);
bus.connect(audioContext.destination);

E pronto, aplicamos delay, equalização e reverb (um efeito bastante caro em termos de performance) pela metade do custo, como se tivéssemos aplicado os efeitos a cada instrumento separadamente. Se quiséssemos dar um toque especial ao ônibus, poderíamos adicionar dois novos nós de ganho: preGain e postGain. Isso permitiria desativar ou atenuar os sons em um ônibus de duas maneiras diferentes. O preGain é colocado antes dos efeitos, e o postGain é colocado no final da cadeia. Se o preGain for desativado, os efeitos ainda vão ressoar depois que o ganho atingir o mínimo, mas se o postGain for desativado, todo o som será silenciado ao mesmo tempo.

O que fazer depois disso?

Os métodos descritos aqui podem e devem ser desenvolvidos. Coisas como a entrada e a saída dos nós personalizados e os métodos de conexão podem/devem ser implementados usando a herança baseada em protótipo. Os buses precisam ser capazes de criar efeitos dinamicamente ao receber uma lista de efeitos.

Para comemorar o lançamento do JAM com o Chrome, decidimos tornar nosso framework de efeitos de código aberto. Se você gostou dessa breve introdução, confira o projeto e contribua. Há uma discussão aqui sobre a padronização de um formato para entidades personalizadas do Web Audio. Faça parte