Technitone.com es una fusión de WebGL, Canvas, Web Sockets, CSS3, JavaScript, Flash y la nueva API de Web Audio en Chrome.
En este artículo, se abordarán todos los aspectos de la producción: el plan, el servidor, los sonidos, las imágenes y algunos de los flujos de trabajo que aprovechamos para diseñar contenido interactivo. La mayoría de las secciones contienen fragmentos de código, una demostración y una descarga. Al final del artículo, hay un vínculo de descarga en el que puedes obtenerlos todos como un archivo ZIP.
El concierto
En gskinner.com, no somos ingenieros de audio, pero si nos presentas un desafío, elaboraremos un plan:
- Los usuarios dibujan tonos en una cuadrícula, “inspirados” en ToneMatrix de Andre.
- Los tonos se conectan a instrumentos muestreados, baterías o incluso a las propias grabaciones de los usuarios.
- Varios usuarios conectados juegan en la misma cuadrícula de forma simultánea
- …o bien, ir al modo solo para explorar por su cuenta
- Las sesiones de invitación permiten a los usuarios organizar una banda y tener una improvisación espontánea.
Ofrecemos a los usuarios la oportunidad de explorar la API de Web Audio a través de un panel de herramientas que aplica filtros y efectos de audio en sus tonos.
También hacemos lo siguiente:
- Almacena las composiciones y los efectos de los usuarios como datos y sincronízalas en todos los clientes
- Proporciona algunas opciones de color para que puedan dibujar canciones que se vean geniales.
- Ofrece una galería para que las personas puedan escuchar, marcar como favoritos o incluso editar el trabajo de otras personas.
Nos quedamos con la metáfora familiar de la cuadrícula, la flotamos en el espacio 3D, agregamos algunos efectos de iluminación, textura y partículas, y la alojamos en una interfaz flexible (o de pantalla completa) impulsada por CSS y JS.
Viaje por carretera
Los datos de instrumentos, efectos y cuadrículas se consolidan y serializan en el cliente y, luego, se envían a nuestro backend personalizado de Node.js para resolverlos para varios usuarios a la manera de Socket.io. Estos datos se vuelven a enviar al cliente con las contribuciones de cada jugador incluidas, antes de dispersarse a las capas relativas de CSS, WebGL y WebAudio encargadas de renderizar la IU, los samples y los efectos durante la reproducción para varios usuarios.
La comunicación en tiempo real con sockets alimenta JavaScript en el cliente y JavaScript en el servidor.
Usamos Node para cada aspecto del servidor. Es un servidor web estático y nuestro servidor de sockets todo en uno. Terminamos usando Express, que es un servidor web completo compilado por completo en Node. Es altamente escalable y personalizable, y controla los aspectos de servidor de bajo nivel por ti (al igual que lo haría Apache o Windows Server). Luego, como desarrollador, solo debes enfocarte en compilar tu aplicación.
Demostración multiusuario (en realidad, es solo una captura de pantalla)
Esta demostración se debe ejecutar desde un servidor de Node y, como este artículo no es uno, incluimos una captura de pantalla de cómo se ve la demostración después de instalar Node.js, configurar el servidor web y ejecutarlo de forma local. Cada vez que un usuario nuevo visite tu instalación de demostración, se agregará una cuadrícula nueva y el trabajo de todos será visible para todos.
Node es fácil. Con una combinación de Socket.io y solicitudes POST personalizadas, no tuvimos que compilar rutinas complejas para la sincronización. Socket.io controla esto de forma transparente; se pasa JSON.
¿Qué tan fácil? Mira esto.
Con 3 líneas de JavaScript, tenemos un servidor web en funcionamiento con Express.
//Tell our Javascript file we want to use express.
var express = require('express');
//Create our web-server
var server = express.createServer();
//Tell express where to look for our static files.
server.use(express.static(__dirname + '/static/'));
Algunos más para vincular socket.io para la comunicación en tiempo real.
var io = require('socket.io').listen(server);
//Start listening for socket commands
io.sockets.on('connection', function (socket) {
//User is connected, start listening for commands.
socket.on('someEventFromClient', handleEvent);
});
Ahora, solo comenzamos a escuchar las conexiones entrantes desde la página HTML.
<!-- Socket-io will serve it-self when requested from this url. -->
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<!-- Create our socket and connect to the server -->
var sock = io.connect('http://localhost:8888');
sock.on("connect", handleConnect);
function handleConnect() {
//Send a event to the server.
sock.emit('someEventFromClient', 'someData');
}
```
## Sound check
A big unknown was the effort entailed with using the Web Audio API. Our initial findings confirmed that [Digital Signal Processing](http://en.wikipedia.org/wiki/Digital_Signal_Processing) (DSP) is very complex, and we were likely in way over our heads. Second realization: [Chris Rogers](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html) has already done the heavy lifting in the API.
Technitone isn't using any really complex math or audioholicism; this functionality is easily accessible to interested developers. We really just needed to brush up on some terminology and [read the docs](https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html). Our advice? Don't skim them. Read them. Start at the top and end at the bottom. They are peppered with diagrams and photos, and it's really cool stuff.
If this is the first you've heard of the Web Audio API, or don't know what it can do, hit up Chris Rogers' [demos](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html). Looking for inspiration? You'll definitely find it there.
### Web Audio API Demo
Load in a sample (sound file)…
```js
/**
* The XMLHttpRequest allows you to get the load
* progress of your file download and has a responseType
* of "arraybuffer" that the Web Audio API uses to
* create its own AudioBufferNode.
* Note: the 'true' parameter of request.open makes the
* request asynchronous - this is required!
*/
var request = new XMLHttpRequest();
request.open("GET", "mySample.mp3", true);
request.responseType = "arraybuffer";
request.onprogress = onRequestProgress; // Progress callback.
request.onload = onRequestLoad; // Complete callback.
request.onerror = onRequestError; // Error callback.
request.onabort = onRequestError; // Abort callback.
request.send();
// Use this context to create nodes, route everything together, etc.
var context = new webkitAudioContext();
// Feed this AudioBuffer into your AudioBufferSourceNode:
var audioBuffer = null;
function onRequestProgress (event) {
var progress = event.loaded / event.total;
}
function onRequestLoad (event) {
// The 'true' parameter specifies if you want to mix the sample to mono.
audioBuffer = context.createBuffer(request.response, true);
}
function onRequestError (event) {
// An error occurred when trying to load the sound file.
}
…configurar el enrutamiento modular…
/**
* Generally you'll want to set up your routing like this:
* AudioBufferSourceNode > [effect nodes] > CompressorNode > AudioContext.destination
* Note: nodes are designed to be able to connect to multiple nodes.
*/
// The DynamicsCompressorNode makes the loud parts
// of the sound quieter and quiet parts louder.
var compressorNode = context.createDynamicsCompressor();
compressorNode.connect(context.destination);
// [other effect nodes]
// Create and route the AudioBufferSourceNode when you want to play the sample.
…aplicar un efecto de tiempo de ejecución (convolución con una respuesta al impulso)…
/**
* Your routing now looks like this:
* AudioBufferSourceNode > ConvolverNode > CompressorNode > AudioContext.destination
*/
var convolverNode = context.createConvolver();
convolverNode.connect(compressorNode);
convolverNode.buffer = impulseResponseAudioBuffer;
…aplicar otro efecto de tiempo de ejecución (demora)…
/**
* The delay effect needs some special routing.
* Unlike most effects, this one takes the sound data out
* of the flow, reinserts it after a specified time (while
* looping it back into itself for another iteration).
* You should add an AudioGainNode to quieten the
* delayed sound...just so things don't get crazy :)
*
* Your routing now looks like this:
* AudioBufferSourceNode -> ConvolverNode > CompressorNode > AudioContext.destination
* | ^
* | |___________________________
* | v |
* -> DelayNode > AudioGainNode _|
*/
var delayGainNode = context.createGainNode();
delayGainNode.gain.value = 0.7; // Quieten the feedback a bit.
delayGainNode.connect(convolverNode);
var delayNode = context.createDelayNode();
delayNode.delayTime = 0.5; // Re-sound every 0.5 seconds.
delayNode.connect(delayGainNode);
delayGainNode.connect(delayNode); // make the loop
…y, luego, haz que sea audible.
/**
* Once your routing is set up properly, playing a sound
* is easy-shmeezy. All you need to do is create an
* AudioSourceBufferNode, route it, and tell it what time
* (in seconds relative to the currentTime attribute of
* the AudioContext) it needs to play the sound.
*
* 0 == now!
* 1 == one second from now.
* etc...
*/
var sourceNode = context.createBufferSource();
sourceNode.connect(convolverNode);
sourceNode.connect(delayNode);
sourceNode.buffer = audioBuffer;
sourceNode.noteOn(0); // play now!
Nuestro enfoque de la reproducción en Technitone se basa en la programación. En lugar de establecer un intervalo de temporizador igual a nuestro tempo para procesar los sonidos en cada tiempo, configuramos un intervalo más pequeño que administra y programa los sonidos en una fila. Esto permite que la API realice el trabajo inicial de resolver los datos de audio y procesar los filtros y efectos antes de que le demos la tarea a la CPU de hacerlo audible. Cuando llega ese momento, ya tiene toda la información que necesita para presentar el resultado neto a las bocinas.
En general, todo debía optimizarse. Cuando exigimos demasiado a nuestras CPUs, se omitieron procesos (pop, click, scratch) para seguir el cronograma. Nos esforzamos mucho por detener toda la locura si cambias a otra pestaña en Chrome.
Espectáculo de luces
En primer plano, se encuentra nuestra cuadrícula y el túnel de partículas. Esta es la capa de WebGL de Technitone.
WebGL ofrece un rendimiento considerablemente superior al de la mayoría de los otros enfoques para renderizar elementos visuales en la Web, ya que asigna tareas a la GPU para que funcione en conjunto con el procesador. Esa ganancia de rendimiento conlleva el costo de un desarrollo mucho más involucrado con una curva de aprendizaje mucho más pronunciada. Dicho esto, si te apasionan los elementos interactivos en la Web y quieres que haya la menor cantidad posible de limitaciones de rendimiento, WebGL ofrece una solución comparable a Flash.
Demostración de WebGL
El contenido de WebGL se renderiza en un lienzo (literalmente, el lienzo HTML5) y consta de los siguientes componentes básicos:
- Vértices de objetos (geometría)
- matrices de posición (coordenadas 3D)
- sombreadores (una descripción de la apariencia de la geometría, vinculada directamente a la GPU)
- el contexto ("atajos" a los elementos a los que hace referencia la GPU)
- búferes (canalizaciones para pasar datos de contexto a la GPU)
- el código principal (la lógica empresarial específica del elemento interactivo deseado)
- el método"draw" (activa los sombreadores y dibuja píxeles en el lienzo)
El proceso básico para renderizar contenido de WebGL en la pantalla es el siguiente:
- Establece la matriz de perspectiva (ajusta la configuración de la cámara que observa el espacio 3D y define el plano de la imagen).
- Establece la matriz de posición (declara un origen en las coordenadas 3D en relación con el que se miden las posiciones).
- Llena los búferes con datos (posición del vértice, color, texturas…) para pasarlos al contexto a través de los sombreadores.
- Extrae y organiza datos de los búferes con los sombreadores y pásalos a la GPU.
- Llama al método de dibujo para indicarle al contexto que active los sombreadores, que se ejecute con los datos y que actualice el lienzo.
En acción, se ve de la siguiente manera:
Establece la matriz de perspectiva…
// Aspect ratio (usually based off the viewport,
// as it can differ from the canvas dimensions).
var aspectRatio = canvas.width / canvas.height;
// Set up the camera view with this matrix.
mat4.perspective(45, aspectRatio, 0.1, 1000.0, pMatrix);
// Adds the camera to the shader. [context = canvas.context]
// This will give it a point to start rendering from.
context.uniformMatrix4fv(shader.pMatrixUniform, 0, pMatrix);
…establece la matriz de posiciones…
// This resets the mvMatrix. This will create the origin in world space.
mat4.identity(mvMatrix);
// The mvMatrix will be moved 20 units away from the camera (z-axis).
mat4.translate(mvMatrix, [0,0,-20]);
// Sets the mvMatrix in the shader like we did with the camera matrix.
context.uniformMatrix4fv(shader.mvMatrixUniform, 0, mvMatrix);
…definen una geometría y una apariencia…
// Creates a square with a gradient going from top to bottom.
// The first 3 values are the XYZ position; the last 4 are RGBA.
this.vertices = new Float32Array(28);
this.vertices.set([-2,-2, 0, 0.0, 0.0, 0.7, 1.0,
-2, 2, 0, 0.0, 0.4, 0.9, 1.0,
2, 2, 0, 0.0, 0.4, 0.9, 1.0,
2,-2, 0, 0.0, 0.0, 0.7, 1.0
]);
// Set the order of which the vertices are drawn. Repeating values allows you
// to draw to the same vertex again, saving buffer space and connecting shapes.
this.indices = new Uint16Array(6);
this.indices.set([0,1,2, 0,2,3]);
…llenan los búferes con datos y los pasan al contexto…
// Create a new storage space for the buffer and assign the data in.
context.bindBuffer(context.ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ARRAY_BUFFER, this.vertices, context.STATIC_DRAW);
// Separate the buffer data into its respective attributes per vertex.
context.vertexAttribPointer(shader.vertexPositionAttribute,3,context.FLOAT,0,28,0);
context.vertexAttribPointer(shader.vertexColorAttribute,4,context.FLOAT,0,28,12);
// Create element array buffer for the index order.
context.bindBuffer(context.ELEMENT_ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ELEMENT_ARRAY_BUFFER, this.indices, context.STATIC_DRAW);
…y llama al método de dibujo.
// Draw the triangles based off the order: [0,1,2, 0,2,3].
// Draws two triangles with two shared points (a square).
context.drawElements(context.TRIANGLES, 6, context.UNSIGNED_SHORT, 0);
En cada fotograma, recuerda borrar el lienzo si no quieres que las imágenes basadas en alfa se apilen una sobre otra.
The Venue
Además de la cuadrícula y el túnel de partículas, todos los demás elementos de la IU se compilaron en HTML / CSS y la lógica interactiva en JavaScript.
Desde el principio, decidimos que los usuarios deberían interactuar con la cuadrícula lo más rápido posible. Sin pantalla de presentación, sin instrucciones ni instructivos, solo "Go". Si la interfaz está cargada, no debería haber nada que los ralentice.
Esto requirió que analicemos cuidadosamente cómo guiar a un usuario nuevo a través de sus interacciones. Incluimos indicadores sutiles, como que la propiedad del cursor CSS cambie en función de la posición del mouse del usuario dentro del espacio WebGL. Si el cursor está sobre la cuadrícula, lo cambiamos a un cursor de mano (porque pueden interactuar trazando tonos). Si se coloca el cursor sobre el espacio en blanco alrededor de la cuadrícula, lo reemplazamos por un cursor de cruz direccional (para indicar que se puede rotar o dividir la cuadrícula en capas).
Prepárate para el espectáculo
LESS (un preprocesador de CSS) y CodeKit (desarrollo web con esteroides) realmente reducen el tiempo que se tarda en traducir archivos de diseño a HTML/CSS sin conexión. Estos nos permiten organizar, escribir y optimizar el CSS de una manera mucho más versátil, aprovechando las variables, los complementos (funciones) y hasta las matemáticas.
Efectos de escenario
Con las transiciones CSS3 y backbone.js, creamos algunos efectos muy simples que ayudan a dar vida a la aplicación y proporcionan a los usuarios indicadores visuales que indican qué instrumento están usando.
Backbone.js nos permite detectar eventos de cambio de color y aplicar el color nuevo a los elementos DOM adecuados. Las transiciones de CSS3 aceleradas por GPU controlaron los cambios de estilo de color con poco o ningún impacto en el rendimiento.
La mayoría de las transiciones de color en los elementos de la interfaz se crearon con transiciones de colores de fondo. Sobre este color de fondo, colocamos imágenes de fondo con áreas estratégicas de transparencia para que se destaque el color de fondo.
HTML: La base
Necesitamos tres regiones de color para la demostración: dos regiones de color seleccionadas por el usuario y una tercera región de color mixto. Para nuestra ilustración, creamos la estructura de DOM más simple que se nos ocurrió que admita transiciones CSS3 y la menor cantidad de solicitudes HTTP.
<!-- Basic HTML Setup -->
<div class="illo color-mixed">
<div class="illo color-primary"></div>
<div class="illo color-secondary"></div>
</div>
CSS: Estructura simple con estilo
Usamos el posicionamiento absoluto para colocar cada región en su ubicación correcta y ajustamos la propiedad background-position para alinear la ilustración de fondo dentro de cada región. Esto hace que todas las regiones (cada una con la misma imagen de fondo) se vean como un solo elemento.
.illo {
background: url('../img/illo.png') no-repeat;
top: 0;
cursor: pointer;
}
.illo.color-primary, .illo.color-secondary {
position: absolute;
height: 100%;
}
.illo.color-primary {
width: 350px;
left: 0;
background-position: top left;
}
.illo.color-secondary {
width: 355px;
right: 0;
background-position: top right;
}
Se aplicaron transiciones aceleradas por GPU que escuchan eventos de cambio de color. Aumentamos la duración y modificamos la atenuación en .color-mixed para crear la impresión de que los colores tardaron en mezclarse.
/* Apply Transitions To Backgrounds */
.color-primary, .color-secondary {
-webkit-transition: background .5s linear;
-moz-transition: background .5s linear;
-ms-transition: background .5s linear;
-o-transition: background .5s linear;
}
.color-mixed {
position: relative;
width: 750px;
height: 600px;
-webkit-transition: background 1.5s cubic-bezier(.78,0,.53,1);
-moz-transition: background 1.5s cubic-bezier(.78,0,.53,1);
-ms-transition: background 1.5s cubic-bezier(.78,0,.53,1);
-o-transition: background 1.5s cubic-bezier(.78,0,.53,1);
}
JavaScript: Lograr que todo funcione
Asignar colores de forma dinámica es sencillo. Buscamos en el DOM cualquier elemento con nuestra clase de color y establecemos el color de fondo según las selecciones de color del usuario. Para aplicar nuestro efecto de transición a cualquier elemento del DOM, agregamos una clase. Esto crea una arquitectura liviana, flexible y escalable.
function createPotion() {
var primaryColor = $('.picker.color-primary > li.selected').css('background-color');
var secondaryColor = $('.picker.color-secondary > li.selected').css('background-color');
console.log(primaryColor, secondaryColor);
$('.illo.color-primary').css('background-color', primaryColor);
$('.illo.color-secondary').css('background-color', secondaryColor);
var mixedColor = mixColors (
parseColor(primaryColor),
parseColor(secondaryColor)
);
$('.color-mixed').css('background-color', mixedColor);
}
Una vez que se seleccionan los colores primario y secundario, calculamos su valor de color mezclado y asignamos el valor resultante al elemento DOM apropiado.
// take our rgb(x,x,x) value and return an array of numeric values
function parseColor(value) {
return (
(value = value.match(/(\d+),\s*(\d+),\s*(\d+)/)))
? [value[1], value[2], value[3]]
: [0,0,0];
}
// blend two rgb arrays into a single value
function mixColors(primary, secondary) {
var r = Math.round( (primary[0] * .5) + (secondary[0] * .5) );
var g = Math.round( (primary[1] * .5) + (secondary[1] * .5) );
var b = Math.round( (primary[2] * .5) + (secondary[2] * .5) );
return 'rgb('+r+', '+g+', '+b+')';
}
Ilustración para la arquitectura de HTML/CSS: Darle personalidad a tres cuadros de cambio de color
Nuestro objetivo era crear un efecto de iluminación divertido y realista que mantuviera su integridad cuando se colocaran colores contrastantes en regiones de colores adyacentes.
Un PNG de 24 bits permite que el color de fondo de nuestros elementos HTML se muestre a través de las áreas transparentes de la imagen.
Los cuadros de colores crean bordes definidos donde se encuentran diferentes colores. Esto dificulta los efectos de iluminación realistas y fue uno de los mayores desafíos a la hora de diseñar la ilustración.
La solución fue diseñar la ilustración para que nunca permita que los bordes de las regiones de color se muestren a través de las áreas transparentes.
La planificación de la compilación fue fundamental. Una sesión de planificación rápida entre el diseñador, el desarrollador y el ilustrador ayudó al equipo a comprender cómo se debía crear todo para que funcionara en conjunto cuando se ensamblara.
Consulta el archivo de Photoshop como ejemplo de cómo los nombres de las capas pueden comunicar información sobre la construcción de CSS.
Encore
Para los usuarios que no tienen Chrome, nos propusimos extraer la esencia de la aplicación en una sola imagen estática. El nodo de cuadrícula se convirtió en el elemento principal, las tarjetas de fondo aluden al propósito de la aplicación y la perspectiva presente en la reflexión insinúa el entorno 3D envolvente de la cuadrícula.
Si te interesa obtener más información sobre Technitone, no te pierdas nuestro blog.
La banda
Gracias por leer. ¡Quizás nos juntaremos contigo pronto!