Case study - Creazione di Technitone.com

Sean Middleditch
Sean Middleditch
Technitone: un'esperienza audio sul web.

Technitone.com è una fusione di WebGL, Canvas, Web Socket, CSS3, JavaScript, Flash e della nuova API Web Audio in Chrome.

In questo articolo parleremo di ogni aspetto della produzione: il piano, il server, gli audio, le immagini e parte del flusso di lavoro che utilizziamo per la progettazione interattiva. La maggior parte delle sezioni contiene snippet di codice, una demo e un download. Alla fine dell'articolo, c'è un link per il download da cui puoi recuperarli tutti in un unico file ZIP.

Il team di produzione di gskinner.com.

Il concerto

Non siamo assolutamente tecnici del suono in gskinner.com, ma mettici alla prova con una sfida e creeremo un piano:

  • Gli utenti tracciano i toni su una griglia,"ispirato" da Andre's ToneMatrix
  • I toni sono collegati a strumenti campionati, batterie o persino registrazioni personali degli utenti.
  • Più utenti connessi giocano contemporaneamente sulla stessa griglia
  • ... o vai in modalità singola per esplorare in autonomia
  • Le sessioni su invito consentono agli utenti di organizzare una band e avere un jam improvvisato

Offriamo agli utenti l'opportunità di esplorare l'API Web Audio mediante un riquadro degli strumenti che applica filtri audio ed effetti sui loro toni.

Technitone di gskinner.com

Inoltre:

  • Archivia le composizioni e gli effetti degli utenti come dati e sincronizzali tra i client
  • Fornisci alcune opzioni di colore per creare brani accattivanti
  • Offri una galleria in modo che le persone possano ascoltare, amare o persino modificare il lavoro di altre persone

Abbiamo mantenuto la nota metafora della griglia, l'abbiamo fatta fluttuare nello spazio 3D, abbiamo aggiunto qualche effetto di illuminazione, texture e particolato, l'abbiamo inserito in un'interfaccia CSS e gestita da JavaScript e flessibile (o a schermo intero).

Viaggio in macchina

I dati degli strumenti, degli effetti e della griglia vengono consolidati e serializzati sul client, quindi vengono inviati al nostro backend Node.js personalizzato per risolvere i problemi per più utenti a Socket.io. Questi dati vengono inviati al client con i contributi di ogni player, prima di essere distribuiti ai relativi livelli CSS, WebGL e WebAudio incaricati di visualizzare l'interfaccia utente, i campioni e gli effetti durante la riproduzione multiutente.

La comunicazione in tempo reale con i feed socket JavaScript sul client e JavaScript sul server.

Diagramma del server Technitone

Utilizziamo Node per ogni aspetto del server. È un server web statico e il nostro server socket in uno. Express è quello che abbiamo finito per utilizzare, è un server web completo basato interamente su Node. È super scalabile, altamente personalizzabile e gestisce gli aspetti di basso livello del server al posto tuo (proprio come sarebbe Apache o Windows Server). Poi, come sviluppatore, devi concentrarti solo sulla creazione della tua applicazione.

Demo multiutente (Ok, in realtà si tratta solo di uno screenshot)

Questa demo richiede l'esecuzione da un server Node e, poiché questo articolo non fa parte di questo articolo, abbiamo incluso uno screenshot dell'aspetto della demo dopo che hai installato Node.js, configurato il tuo server web ed eseguito localmente. Ogni volta che un nuovo utente visita la tua installazione demo, viene aggiunta una nuova griglia e il lavoro di tutti è visibile gli uni agli altri.

Screenshot della demo Node.js

Node è facile. Grazie a una combinazione di richieste POST e Socket.io personalizzate, non abbiamo dovuto creare routine complesse per la sincronizzazione. Socket.io gestisce in modo trasparente questo aspetto, mentre JSON viene trasferito.

Quanto è facile? Guarda questo.

Con 3 righe di JavaScript, abbiamo un server web operativo 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/'));

Qualche altro ancora da collegare a socket.io per una comunicazione in tempo reale.

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

});

Ora iniziamo semplicemente ad ascoltare le connessioni in arrivo dalla pagina 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.
}

...configurare il routing modulare...

/**
 * 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.

...applicare un effetto di runtime (convoluzione usando una risposta all'impulso)...

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

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

...applicare un altro effetto di runtime (ritardo)...

/**
 * 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

...e poi rendilo udibile.

/**
 * 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!

Il nostro approccio alla riproduzione in Technitone riguarda esclusivamente la programmazione. Invece di impostare un intervallo timer uguale al nostro tempo per elaborare i suoni a ogni beat, abbiamo impostato un intervallo più piccolo che gestisce e pianifica i suoni in una coda. Ciò consente all'API di eseguire il lavoro iniziale per la risoluzione dei dati audio e dell'elaborazione dei filtri e degli effetti prima di dare il lavoro alla CPU per renderla effettivamente udibile. Quando finalmente il ritmo arriva, ha già tutte le informazioni necessarie per comunicare il risultato finale agli interlocutori.

Nel complesso, tutto doveva essere ottimizzato. Quando abbiamo spinto troppo le nostre CPU, i processi sono stati saltati (pop, clic, zero) per rimanere al passo con la pianificazione; ci siamo impegnati seriamente per bloccare tutta la follia se passiamo a un'altra scheda in Chrome.

Spettacolo di luci

Al centro ci sono la griglia e il tunnel delle particelle. Questo è il livello WebGL di Technitone.

WebGL offre prestazioni notevolmente superiori rispetto alla maggior parte degli altri approcci per il rendering delle immagini sul web, in quanto assegna alla GPU il compito di lavorare congiuntamente al processore. Questo miglioramento delle prestazioni ha comportato il costo di uno sviluppo significativamente maggiore e una curva di apprendimento molto più ripida. Detto questo, se ti piace davvero l'interattività sul web e vuoi il minor numero possibile di limitazioni di prestazioni, WebGL offre una soluzione paragonabile a Flash.

Demo WebGL

I contenuti WebGL vengono visualizzati su una tela (ovvero il Canvas HTML5) e sono costituiti dai seguenti componenti di base fondamentali:

  • vertici degli oggetti (geometria)
  • matrici di posizione (coordinate 3D)
    • shabby (una descrizione dell'aspetto della geometria, collegata direttamente alla GPU)
    • il contesto ("scorciatoie" agli elementi a cui fa riferimento la GPU)
    • buffer (pipeline per il passaggio dei dati di contesto alla GPU)
    • il codice principale (la logica di business specifica dell'elemento interattivo desiderato)
    • Il metodo "draw" (attiva gli ombrelli e disegna pixel sull'area di disegno)

La procedura di base per eseguire il rendering dei contenuti WebGL sullo schermo è la seguente:

  1. Imposta la matrice prospettica (regola le impostazioni della fotocamera che sbircia nello spazio 3D, definendo il piano dell'immagine).
  2. Imposta la matrice di posizione (dichiara un'origine nelle coordinate 3D a cui vengono misurate le posizioni rispetto).
  3. Riempi i buffer con i dati (posizione del vertice, colore, texture e così via) da passare al contesto tramite gli ombreggiatori.
  4. Estrarre e organizzare i dati dai buffer con gli mesh e passarli alla GPU.
  5. Richiama il metodo di disegno per indicare al contesto di attivare i cursori, eseguire i dati e aggiornare il canvas.

Ecco come funziona:

Imposta la matrice prospettica...

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

...imposta la matrice della posizione...

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

...definisci qualche geometria e aspetto...

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

...riempire i buffer con i dati e passarli al contesto...

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

...e richiamiamo il metodo disegno

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

Ricordati di cancellare ogni frame se non vuoi che le immagini basate su alpha si sovrappongano l'una all'altra.

The Venue

Oltre alla griglia e al tunnel delle particelle, ogni altro elemento dell'interfaccia utente è stato creato in HTML / CSS e nella logica interattiva in JavaScript.

Fin dall'inizio, abbiamo deciso che gli utenti dovevano interagire con la rete il più rapidamente possibile. Nessuna schermata iniziale, istruzioni, tutorial, basta "Vai". Se l'interfaccia è caricata, non dovrebbe esserci nulla che li rallenta.

Ciò ci ha richiesto di esaminare attentamente come guidare un nuovo utente nelle sue interazioni. Abbiamo incluso degli indizi, come la modifica della proprietà del cursore CSS in base alla posizione del mouse dell'utente all'interno dello spazio WebGL. Se il cursore si trova sulla griglia, lo passiamo a un cursore a forma di mano (perché possono interagire tracciando i toni). Se passi il mouse sopra lo spazio vuoto intorno alla griglia, lo sostituiamo con un cursore a croce direzionale (per indicare che possono ruotare o far esplodere la griglia in livelli).

Prepararsi per lo spettacolo

LESS (un pre-processore CSS) e CodeKit (sviluppo web su steroidi) riducono davvero i tempi necessari per tradurre i file di progettazione in HTML/CSS simulati. Queste ci consentono di organizzare, scrivere e ottimizzare il codice CSS in un modo molto più versatile, sfruttando variabili, mix-in (funzioni) e persino matematica.

Effetti teatrali

Usando le transizioni CSS3 e backbone.js abbiamo creato alcuni effetti semplicissimi che ci aiutano a dare vita all'applicazione e a fornire agli utenti code visive che indicano quale strumento stanno utilizzando.

I colori della tecnologia Technitone.

Backbone.js ci consente di rilevare gli eventi di variazione del colore e applicare il nuovo colore agli elementi DOM appropriati. Le transizioni CSS3 con accelerazione GPU hanno gestito le modifiche dello stile di colore con un impatto minimo o nullo sulle prestazioni.

La maggior parte delle transizioni di colore negli elementi dell'interfaccia è stata creata dalla transizione dei colori di sfondo. Oltre a questo colore di sfondo, posizioniamo immagini di sfondo con aree strategiche di trasparenza per far risaltare il colore di sfondo.

HTML: gli elementi di base

Avevamo bisogno di tre regioni di colore per la demo: due regioni di colore selezionate dall'utente e una terza regione con colori misti. Per la nostra illustrazione, abbiamo creato la struttura DOM più semplice che ci si possa pensare, che supporti le transizioni CSS3 e il minor numero di richieste HTTP.

<!-- Basic HTML Setup -->
<div class="illo color-mixed">
  <div class="illo color-primary"></div>
  <div class="illo color-secondary"></div>
</div>

CSS: struttura semplice con stile

Abbiamo utilizzato il posizionamento assoluto per posizionare ogni regione nella posizione corretta e modificato la proprietà posizione dello sfondo per allineare l'illustrazione dello sfondo all'interno di ogni regione. In questo modo tutte le regioni (ognuna con la stessa immagine di sfondo) appaiono come un singolo 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;
  }

Sono state applicate transizioni con accelerazione GPU che rilevano gli eventi di cambio di colore. Abbiamo aumentato la durata e modificato l'easing nel formato .color-mixed per dare l'impressione che ci sia voluto del tempo prima che i colori si mescolassero.

/* 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 HTML5Please per ricevere il supporto del browser corrente e l'utilizzo consigliato per le transizioni CSS3.

JavaScript: come funziona

L'assegnazione dinamica dei colori è semplice. Cerchiamo nel DOM qualsiasi elemento con la nostra classe di colore e impostiamo il colore di sfondo in base alle selezioni di colori dell'utente. Applichiamo l'effetto di transizione a qualsiasi elemento del DOM aggiungendo una classe. Ciò crea un'architettura leggera, flessibile e scalabile.

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 volta selezionati i colori primari e secondari, calcoliamo il valore dei colori misti e assegniamo il valore risultante all'elemento DOM appropriato.

// 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+')';
}

Illustrazione per l'architettura HTML/CSS: la personalità di tre riquadri che cambiano colore

Il nostro obiettivo era creare un effetto di illuminazione divertente e realistico che mantenesse la sua integrità quando i colori in contrasto venivano posizionati nelle regioni di colore adiacenti.

Un file PNG a 24 bit consente di mostrare il colore di sfondo degli elementi HTML attraverso le aree trasparenti dell'immagine.

Trasparenza delle immagini

Le caselle colorate creano bordi duri nel punto in cui si incontrano diversi colori. Ciò ostacolava gli effetti di luce realistici ed è stata una delle sfide maggiori della progettazione dell'illustrazione.

Regioni di colore

La soluzione consisteva nel progettare l'illustrazione in modo che non consentisse mai di mostrare i bordi delle regioni di colore attraverso le aree trasparenti.

Bordi regione colore

La pianificazione della build era fondamentale. Una rapida sessione di pianificazione tra designer, sviluppatore e illustratore ha aiutato il team a capire come doveva essere realizzato tutto in modo da funzionare insieme una volta assemblato.

Dai un'occhiata al file Photoshop per vedere un esempio di come la denominazione dei livelli può comunicare informazioni sulla creazione dei CSS.

Bordi regione colore

Encore

Per gli utenti che non dispongono di Chrome, definiamo l'obiettivo di sintetizzare l'essenza dell'applicazione in un'unica immagine statica. Il nodo della griglia è diventato hero, i riquadri di sfondo rappresentano lo scopo dell'applicazione e la prospettiva presente nei suggerimenti di riflessione nell'ambiente 3D immersivo della griglia.

Bordi regione colore.

Se ti interessa saperne di più su Technitone, consulta il nostro blog.

Il cinturino

Grazie per l'attenzione, forse presto ci occuperemo di te.