Estudo de caso: JAM com Chrome

Como criamos o áudio de sucesso

Oskar Eriksson
Oskar Eriksson

Introdução

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

Para atingir o mais alto nível possível de autenticidade, precisão e qualidade de áudio, optamos por usar a API de áudio da Web. Este estudo de caso discutirá alguns dos desafios que surgiram e como os resolvemos. Já existem vários artigos de introdução no HTML5Rocks para você começar a usar o Web Audio. Vamos direto ao ponto.

Como criar efeitos de áudio personalizados

A API de áudio da Web 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 áudio da Web, mas há muitos tipos de atrasos: atraso estéreo, atraso de pingue-pongue, atraso de transição e a lista continua. Felizmente, é possível criar tudo isso no Web Audio usando os nós de efeito nativo e um pouco de imaginação.

Como queríamos poder 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 seu método de conexão para vincular os nós. Por isso, foi necessário 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 bem próximos dos nós nativos. Vamos conferir como ela seria usada.

//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);
Como rotear o nó personalizado

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

Agora que já tínhamos nosso padrão básico para criar efeitos personalizados, a próxima etapa era realmente dar ao nó personalizado algum comportamento personalizado. Vamos dar uma olhada em um nó de atraso de slapback.

Tapa-se como você realmente

O delay slapback, às vezes chamado de slapback echo, é um efeito clássico usado em vários instrumentos, desde vocais no estilo dos anos 50 até guitarras de surfe. O efeito recebe o som recebido e reproduz uma cópia dele com um pequeno atraso de aproximadamente 75 a 250 milissegundos. Isso dá uma sensação do som sendo batido de volta, por isso 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ó slapback

Como alguns de vocês já devem ter percebido, esse atraso também pode ser usado com tempos de atraso maiores e, portanto, se tornar um atraso mono regular com feedback. Este é um exemplo que usa esse atraso para que você ouça o som.

Áudio de roteamento

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

Roteamento do AudioBus

Felizmente, isso é realmente fácil de conseguir com o áudio da Web. Basicamente, podemos usar o esqueleto que definimos para os efeitos e usá-lo da mesma forma.

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

Isso 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 reverberação (que é um efeito bastante caro em termos de desempenho) pela metade do custo, como se tivéssemos aplicado os efeitos a cada instrumento separado. Se quiséssemos acrescentar um tempero extra ao ônibus, poderíamos adicionar dois novos nós de ganho, PreGain e PostGain, o que permitiria desativar ou esmaecer os sons em um ônibus de duas maneiras diferentes. O preGain é colocado antes dos efeitos e o postGain é colocado no final da cadeia. Se esmaecermos o preGain, os efeitos ainda ressoam depois que o ganho atinge a parte inferior, mas se esmaecemos o postGain, todo o som será silenciado ao mesmo tempo.

De onde vem?

Esses métodos que descrevi aqui podem e devem ser desenvolvidos com mais detalhes. 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 ônibus devem ser capazes de criar efeitos de forma dinâmica recebendo uma lista de efeitos.

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