Cómo logramos que el audio fuera excelente
Introducción
JAM with Chrome es un proyecto musical basado en la Web creado por Google. JAM con Chrome permite que personas de todo el mundo formen una banda y toquen en tiempo real dentro del navegador. En DinahMoe, tuvimos el gran placer de ser parte de este proyecto. Nuestra función era producir música para la aplicación y diseñar y desarrollar el componente musical. El desarrollo consistió en tres áreas principales: una “estación de trabajo musical” que incluye la reproducción de MIDI, samplers de software, efectos de audio, enrutamiento y mezcla; un motor de lógica musical para controlar la música de forma interactiva en tiempo real y un componente de sincronización que se asegura de que todos los jugadores de una sesión escuchen la música exactamente al mismo tiempo, un requisito previo para poder tocar juntos.
Para lograr el nivel más alto posible de autenticidad, precisión y calidad de audio, optamos por usar la API de Web Audio. En este caso de éxito, se analizarán algunos de los desafíos que tuvimos y cómo los resolvimos. Ya hay varios artículos introductorios excelentes en HTML5Rocks para comenzar a usar Web Audio, así que pasaremos directamente a la parte más difícil.
Cómo escribir efectos de audio personalizados
La API de Web Audio tiene una serie de efectos útiles incluidos en la especificación, pero necesitábamos efectos más elaborados para nuestros instrumentos en JAM con Chrome. Por ejemplo, hay un nodo de retardo nativo en Web Audio, pero hay muchos tipos de retardos: retardo estéreo, retardo de ping-pong, retardo de slapback, y la lista sigue. Por suerte, todo esto se puede crear en Web Audio con los nodos de efectos nativos y un poco de imaginación.
Como queríamos poder usar los nodos nativos y nuestros propios efectos personalizados de la forma más transparente posible, decidimos que necesitábamos crear un formato de wrapper que pudiera lograr esto. Los nodos nativos de Web Audio usan su método de conexión para vincular nodos, por lo que tuvimos que emular este comportamiento. Así es como se ve la idea básica:
var MyCustomNode = function(){
this.input = audioContext.createGain();
var output = audioContext.createGain();
this.connect = function(target){
output.connect(target);
};
};
Con este patrón, estamos muy cerca de los nodos nativos. Veamos cómo se usaría esto.
//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);
La única diferencia entre nuestro nodo personalizado y uno nativo es que debemos conectarnos a la propiedad de entrada de los nodos personalizados. Estoy seguro de que hay formas de evitar esto, pero esta es una solución lo suficientemente cercana para nuestros fines. Este patrón se puede desarrollar aún más para simular los métodos de desconexión de AudioNodes nativos, así como para admitir entradas y salidas definidas por el usuario cuando se conecta, etcétera. Consulta la especificación para ver lo que pueden hacer los nodos nativos.
Ahora que teníamos nuestro patrón básico para crear efectos personalizados, el siguiente paso era darle al nodo personalizado un comportamiento personalizado. Veamos un nodo de retardo de slapback.
Slapback con convicción
La demora de slapback, a veces llamada eco de slapback, es un efecto clásico que se usa en varios instrumentos, desde voces de estilo de los años 50 hasta guitarras de surf. El efecto toma el sonido entrante y reproduce una copia del sonido con una ligera demora de aproximadamente 75 a 250 milisegundos. Esto da la sensación de que el sonido se está deteniendo, de ahí el nombre. Podemos crear el efecto de la siguiente manera:
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);
};
};
Como algunos de ustedes ya habrán notado, este retraso también se puede usar con tiempos de retraso más grandes y, así, convertirse en un retraso mono normal con comentarios. Este es un ejemplo en el que se usa esta demora para que puedas escuchar cómo suena.
Enrutamiento de audio
Cuando se trabaja con diferentes instrumentos y partes musicales en aplicaciones de audio profesionales, es esencial tener un sistema de enrutamiento flexible que permita mezclar y modular los sonidos de manera eficaz. En JAM con Chrome, desarrollamos un sistema de bus de audio, similar al que se encuentra en las mesas de mezclas físicas. Esto nos permite conectar todos los instrumentos que necesitan un efecto de reverberación a un bus o canal común y, luego, agregar la reverberación a ese bus en lugar de agregar una reverberación a cada instrumento por separado. Esta es una optimización importante y se recomienda hacer algo similar en cuanto comiences a crear aplicaciones más complejas.
Por suerte, esto es muy fácil de lograr en Web Audio. Básicamente, podemos usar el esqueleto que definimos para los efectos y usarlo de la misma manera.
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);
};
};
Se usaría de la siguiente manera:
//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);
Y listo, aplicamos la demora, la ecualización y la reverberación (que es un efecto bastante costoso en términos de rendimiento) a la mitad del costo que tendríamos si los aplicaremos a cada instrumento por separado. Si quisiéramos agregarle un poco más de sabor al bus, podríamos agregar dos nodos de ganancia nuevos: preGain y postGain, que nos permitirían apagar o atenuar los sonidos de un bus de dos maneras diferentes. El preGain se coloca antes de los efectos, y el postGain se coloca al final de la cadena. Si atenuamos preGain, los efectos seguirán resonando después de que la ganancia llegue al final, pero si atenuamos postGain, todo el sonido se silenciará al mismo tiempo.
¿Qué sigue después?
Estos métodos que describí aquí se pueden y deben desarrollar más. Elementos como la entrada y la salida de los nodos personalizados, y los métodos de conexión, se pueden o deben implementar con la herencia basada en prototipos. Los buses deben poder crear efectos de forma dinámica cuando se les pasa una lista de efectos.
Para celebrar el lanzamiento de JAM con Chrome, decidimos que nuestro framework de efectos sea de código abierto. Si esta breve introducción te llamó la atención, no dudes en echarle un vistazo y contribuir. Hay una discusión aquí sobre la estandarización de un formato para entidades de Web Audio personalizadas. Participa