Caso de éxito - Creación de Technitone.com

Sean Middleditch
Sean Middleditch
Technitone: una experiencia de audio web

Technitone.com es una fusión de WebGL, Canvas, Web Sockets, CSS3, JavaScript, Flash y la nueva API de Web Audio en Chrome.

Este artículo tratará sobre 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 para ser 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 tomarlos todos como un solo archivo ZIP.

El equipo de producción de gskinner.com.

El recital

No somos ingenieros de audio en gskinner.com, pero nos tienta con un desafío y idearemos un plan:

  • Los usuarios trazan los tonos en una cuadrícula,"Inspirado" por Andre's ToneMatrix
  • Los tonos se conectan a instrumentos de muestra, kits de batería o incluso propias grabaciones de los usuarios.
  • Varios usuarios conectados juegan en la misma red de manera simultánea
  • ...o puedes usar el modo individual para explorar por su cuenta
  • Las sesiones por invitación permiten a los usuarios organizar una banda y hacer una improvisación improvisada

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 a sus tonos.

Technitone de gskinner.com

También:

  • Almacena las composiciones y los efectos de los usuarios como datos, y sincronízalos entre los clientes
  • Brinda algunas opciones de color para que puedan dibujar canciones geniales.
  • Ofrece una galería para que las personas puedan escuchar, amar o incluso editar el trabajo de otras personas

Nos ajustamos a la conocida metáfora de la cuadrícula, la utilizamos en un espacio 3D, le agregamos efectos de iluminación, textura y partículas, y la alojamos en una interfaz flexible (o en pantalla completa) de 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 de Node.js personalizado para resolverlos para varios usuarios a través de Socket.io. Estos datos se envían de vuelta al cliente con las contribuciones de cada jugador incluidas, antes de propagarse a las capas relativas de CSS, WebGL y WebAudio a cargo de renderizar la IU, las muestras y los efectos durante la reproducción multiusuario.

La comunicación en tiempo real con sockets alimenta JavaScript en el cliente y JavaScript en el servidor.

Diagrama del servidor de Technitone

Usamos Node para todos los aspectos del servidor. Es un servidor web estático y nuestro servidor de sockets, todo en uno. Express es lo que terminamos usando, es un servidor web completo compilado completamente en Node. Es muy escalable, altamente personalizable y se encarga de los aspectos de bajo nivel del servidor por ti (al igual que lo harían Apache o Windows Server). Luego, tú, como desarrollador, solo debes enfocarte en compilar tu aplicación.

Demostración multiusuario (de acuerdo, 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 lo es, incluimos una captura de pantalla de cómo se ve la demostración después de que instales Node.js, configures tu servidor web y la ejecutes de manera local. Cada vez que un usuario nuevo visite la instalación de demostración, se agregará una nueva cuadrícula y el trabajo de todos será visible entre sí.

Captura de pantalla de la demostración de Node.js

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 con transparencia; se pasa JSON.

¿Qué tan fácil? Mira esto.

Con tres 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.

...aplica un efecto de tiempo de ejecución (convolución mediante una respuesta de impulso)...

/**
 * Your routing now looks like this:
 * AudioBufferSourceNode > ConvolverNode > CompressorNode > AudioContext.destination
 */

var convolverNode = context.createConvolver();
convolverNode.connect(compressorNode);
convolverNode.buffer = impulseResponseAudioBuffer;

...aplica otro efecto de tiempo de ejecución (retraso)...

/**
 * 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 hacer 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 para 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 pulso, configuramos un intervalo más pequeño que administra y programa los sonidos de una cola. Esto permite que la API realice el trabajo inicial de resolución de datos de audio y procesamiento de filtros y efectos antes de asignar a la CPU la capacidad de hacerla audible. Cuando ese ritmo finalmente se active, ya tendrá toda la información necesaria para presentar el resultado neto a los oradores.

En general, todo debía optimizarse. Cuando sobrecargamos nuestras CPUs, se omitían procesos (pop, clic, raspar) para cumplir con el cronograma. Nos esforzamos mucho para detener toda la locura si pasas a otra pestaña en Chrome.

Espectáculo de luces

Frente y centro está nuestra cuadrícula y nuestro túnel de partículas. Esta es la capa WebGL de Technitone.

WebGL ofrece un rendimiento considerablemente superior al de la mayoría de los otros enfoques de renderización de imágenes en la Web, ya que se encarga de que la GPU funcione en conjunto con el procesador. Esa mejora en el rendimiento conlleva el costo de un desarrollo significativamente más comprometido con una curva de aprendizaje mucho más pronunciada. Dicho esto, si de verdad te apasiona la interacción en la Web y deseas el menor nivel de limitaciones de rendimiento posible, WebGL ofrece una solución comparable con Flash.

Demostración de WebGL

El contenido de WebGL se renderiza en un lienzo (literalmente, el lienzo de HTML5) y está compuesto por estos componentes básicos:

  • vértices del objeto (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 ("accesos directos" 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 objeto 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:

  1. 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).
  2. Establece la matriz de posición (declara un origen en las coordenadas 3D respecto de las cuales se miden las posiciones).
  3. Rellena los búferes con datos (posición del vértice, color, texturas, etc.) para pasarlos al contexto a través de los sombreadores.
  4. Extrae y organiza los datos de los búferes con los sombreadores y pásalos a la GPU.
  5. Llama al método de dibujo para indicarle al contexto que active sombreadores, ejecute con los datos y actualice el lienzo.

Se ve de la siguiente manera en acción:

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

...configurar la matriz de posición...

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

Definir la geometría y el aspecto

// 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]);

...cargar los búferes con datos y pasarlos 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);

Recuerda borrar el lienzo de cada fotograma si no quieres que las imágenes basadas en alfa se agrupen entre sí.

The Venue

Además de la cuadrícula y el túnel de partículas, todos los demás elementos de la IU se compilaban con HTML / CSS y lógica interactiva en JavaScript.

Desde el principio, decidimos que los usuarios debían interactuar con la cuadrícula lo más rápido posible. Sin pantalla de presentación, instrucciones ni tutoriales, solo tienes que decir "Ir". Si la interfaz está cargada, no debería haber nada que los ralentiza.

Esto nos obligó a analizar cuidadosamente cómo guiar a un usuario nuevo a través de sus interacciones. Incluimos señales sutiles, como hacer que la propiedad del cursor CSS cambie en función de la posición del mouse del usuario dentro del espacio de WebGL. Si el cursor está sobre la cuadrícula, lo cambiamos a una mano (porque pueden interactuar dibujando tonos). Si se coloca el cursor sobre el espacio en blanco que rodea la cuadrícula, lo reemplazamos por un cursor de cruz direccional (para indicar que pueden rotar o expandir la cuadrícula para que se convierta en capas).

Prepárate para el espectáculo

LESS (un preprocesador de CSS) y CodeKit (desarrollo web con esteroides) realmente redujeron el tiempo que llevaba traducir los archivos de diseño a HTML/CSS descartados. Nos permiten organizar, escribir y optimizar CSS de una forma mucho más versátil, ya que nos permiten aprovechar variables, combinaciones (funciones) y hasta cálculos matemáticos.

Efectos de etapas

Con las transiciones de CSS3 y backbone.js creamos algunos efectos muy simples que ayudan a dar vida a la aplicación y proporcionan a los usuarios colas visuales que indican qué instrumento están usando.

Los colores de Technitone.

Backbone.js nos permite capturar eventos de cambio de color y aplicar el nuevo color a los elementos correspondientes del DOM. Las transiciones CSS3 aceleradas por GPU controlaron los cambios de estilo de color con un impacto mínimo o nulo en el rendimiento

La mayoría de las transiciones de color en los elementos de la interfaz se crearon con la transición de colores de fondo. Encima de este color de fondo, colocamos imágenes de fondo con áreas estratégicas de transparencia para que se destaque el color de fondo.

HTML: Los fundamentos

Necesitábamos tres regiones de color para la demostración: dos regiones de color seleccionadas por el usuario y una tercera región de colores mixtos. Creamos la estructura de DOM más simple que pudimos considerar que admite transiciones CSS3 y la menor cantidad de solicitudes HTTP para nuestra ilustración.

<!-- 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 de posición del fondo para alinear la ilustración del fondo dentro de cada región. Esto hace que todas las regiones (cada una con la misma imagen de fondo) parezcan 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 detectan eventos de cambio de color. Aumentamos la duración y modificamos la aceleración en el archivo .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);
}

Visita HTML5Por favor para obtener información sobre la compatibilidad del navegador actual y el uso recomendado para las transiciones CSS3.

JavaScript: cómo lograr que funcione

Asignar colores de forma dinámica es sencillo. Buscamos en el DOM cualquier elemento con nuestra clase de color y configuramos el color de fondo según las selecciones de color del usuario. Agregamos una clase para aplicar nuestro efecto de transición a cualquier elemento del DOM. Esto crea una arquitectura que es ligera, 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 seleccionados los colores primario y secundario, calculamos su valor de color mixto y asignamos el valor resultante al elemento del DOM correspondiente.

// 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 HTML/CSS: Dar 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 color adyacentes.

Un archivo 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.

Transparencia de imagen

Los cuadros de colores crean bordes rígidos donde se unen diferentes colores. Esto obstaculiza los efectos de iluminación realistas y fue uno de los mayores desafíos al diseñar la ilustración.

Regiones de color

La solución fue diseñar la ilustración de modo que nunca permitira que los bordes de las regiones de color se mostraran a través de las áreas transparentes.

Bordes de región de color

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 debía construirse todo para que funcionara en conjunto cuando se ensamblara.

Échale un vistazo al archivo de Photoshop como ejemplo de cómo los nombres de las capas pueden comunicar información sobre la construcción de CSS.

Bordes de región de color

Encore

Para los usuarios que no tienen Chrome, el objetivo es sintetizar la esencia de la aplicación en una única imagen estática. El nodo de cuadrícula se convirtió en el héroe, los mosaicos de fondo aluden al propósito de la aplicación y la perspectiva presente en la reflexión insinúa el entorno 3D inmersivo de la cuadrícula.

Bordes de la región de color

Si quieres obtener más información sobre Technitone, consulta nuestro blog.

La banda

Gracias por leer esta información. Quizás graciemos contigo pronto.