Étude de cas : création de Technitone.com

Sean Middleditch
Sean Middleditch
Technitone : une expérience audio sur le Web

Technitone.com est une fusion de WebGL, Canvas, Web Sockets, CSS3, JavaScript et Flash, et de la nouvelle API Web Audio dans Chrome.

Cet article aborde tous les aspects de la production: le plan, le serveur, les sons, les visuels et une partie du workflow que nous utilisons pour concevoir une vidéo interactive. La plupart des sections contiennent des extraits de code, une démonstration et un téléchargement. À la fin de l'article, vous trouverez un lien pour les télécharger sous la forme d'un seul fichier ZIP.

L'équipe de production de gskinner.com

Le travail

Nous ne sommes en aucun cas des ingénieurs du son sur le site gskinner.com, mais vous pouvez nous tenter avec un challenge et nous trouverons un plan:

  • Les utilisateurs inventent les tons sur une grille,"inspirés" de ToneMatrix d'Andre
  • Les tonalités sont associées à des instruments samplés, des kits de batterie ou même des enregistrements des utilisateurs.
  • Plusieurs utilisateurs connectés jouent simultanément sur la même grille
  • ... ou passez en mode solo pour explorer le monde en solo
  • Les sessions sur invitation permettent aux utilisateurs d'organiser un groupe et d'organiser un jam impromptu

Nous offrons aux utilisateurs la possibilité de découvrir l'API Web Audio à l'aide d'un panneau d'outils qui applique des filtres audio et des effets à leurs tonalités.

Technitone par gskinner.com

Nous:

  • Stocker les compositions et les effets des utilisateurs sous forme de données et les synchroniser avec différents clients
  • proposer quelques couleurs pour créer des chansons qui ont l'air cool ;
  • Proposez une galerie permettant d'écouter, d'aimer ou même de retoucher les œuvres d'autres personnes

Nous avons conservé la métaphore familière de la grille, l'avons fait flotter dans un espace 3D, ajouté des effets d'éclairage, de texture et de particules, et l'avons hébergée dans une interface CSS (ou plein écran) flexible.

Road trip

Les données d'instrumentation, d'effet et de grille sont consolidées et sérialisées sur le client, puis envoyées à notre backend Node.js personnalisé à des fins de résolution pour plusieurs utilisateurs sur Socket.io. Ces données sont renvoyées au client avec les contributions de chaque joueur, avant d'être dispersées dans les couches CSS, WebGL et WebAudio relatives, en charge de l'affichage de l'UI, des échantillons et des effets lors de la lecture multi-utilisateur.

La communication en temps réel avec les sockets transmet JavaScript sur le client et JavaScript sur le serveur.

Schéma du serveur Technitone

Nous utilisons Node pour tous les aspects du serveur. C'est un serveur web statique et notre serveur de socket tout-en-un. Express est ce que nous avons fini par utiliser, c'est un serveur Web complet entièrement basé sur Node. Il est très évolutif et personnalisable, et gère pour vous les aspects du serveur de bas niveau (tout comme Apache ou Windows Server). En tant que développeur, vous n'avez ensuite qu'à vous concentrer sur la création de votre application.

Démonstration multi-utilisateur (ok, ce n'est qu'une capture d'écran)

Cette démonstration doit être exécutée à partir d'un serveur Node. Comme cet article n'en a pas un, nous avons inclus une capture d'écran de la démonstration une fois que vous avez installé Node.js, configuré votre serveur Web et exécuté localement. Chaque fois qu'un nouvel utilisateur accède à votre installation de démonstration, une nouvelle grille est ajoutée et le travail de chacun est visible.

Capture d'écran de la démo Node.js

Node est facile. Grâce à une combinaison de requêtes Socket.io et POST personnalisées, nous n'avons pas eu à créer de routines complexes pour la synchronisation. Socket.io gère cela de manière transparente, et le JSON est transmis.

C'est facile ? Regarde ça.

Avec trois lignes de code JavaScript, nous disposons d'un serveur Web opérationnel avec 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/'));

Encore quelques éléments pour associer socket.io à socket.io afin de communiquer en temps réel.

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

});

À présent, nous commençons simplement à écouter les connexions entrantes provenant de la page 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.
}

...configurer le routage modulaire...

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

... appliquer un effet d'exécution (convolution utilisant une réponse impulsive)...

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

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

... appliquer un autre effet d'exécution (retard)...

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

... et la rendre 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!

Notre approche de la lecture dans Technitone est axée sur la planification. Au lieu de définir un intervalle de minuteur égal à notre tempo pour traiter les sons à chaque battement, nous configurons un intervalle plus petit qui gère et planifie les sons dans une file d'attente. Cela permet à l'API d'effectuer le travail initial de résolution des données audio et de traitement des filtres et des effets avant que le processeur ne soit chargé de la rendre audible. Lorsque le rythme arrive enfin, il dispose déjà de toutes les informations nécessaires pour présenter le résultat net aux locuteurs.

Dans l'ensemble, tout le travail devait être optimisé. Lorsque nous avions trop poussé nos processeurs, nous ignorions des processus (pop, clic, grattage) afin de respecter le calendrier. Nous nous efforçons de mettre fin à toute folie si vous passez à un autre onglet dans Chrome.

Spectacle de lumières

Au centre se trouvent notre grille et notre tunnel de particules. Il s'agit du calque WebGL de Technitone.

WebGL offre des performances considérablement supérieures à la plupart des autres approches du rendu des visuels sur le Web, car il fait travailler le GPU avec le processeur. Cette amélioration des performances s'accompagne d'un coût de développement beaucoup plus complexe et d'une courbe d'apprentissage beaucoup plus raide. Cela dit, si vous êtes vraiment passionné par l'interaction sur le Web et que vous souhaitez limiter au maximum les performances, WebGL offre une solution comparable à Flash.

Démonstration de WebGL

Le contenu WebGL est rendu sur un canevas (littéralement, le canevas HTML5) et se compose des éléments de base suivants:

  • Sommets d'objet (géométrie)
  • matrices de position (coordonnées 3D)
    • nuanceurs (description de l'apparence de la géométrie, liée directement au GPU)
    • le contexte ("raccourcis" vers les éléments auxquels le GPU fait référence) ;
    • Tampons (pipelines permettant de transmettre des données de contexte au GPU)
    • le code principal (la logique métier spécifique à l'interaction souhaitée) ;
    • La méthode"draw" (active les nuanceurs et dessine des pixels sur le canevas)

Le processus de base pour afficher le contenu WebGL à l'écran se présente comme suit:

  1. Définissez la matrice de perspective (permet d'ajuster les paramètres de la caméra qui regarde dans l'espace 3D, ce qui définit le plan d'image).
  2. Définissez la matrice de position (déclarez une origine dans les coordonnées 3D à partir desquelles les positions sont mesurées).
  3. Remplissez les tampons avec des données (position des sommets, couleurs, textures, etc.) à transmettre au contexte via les nuanceurs.
  4. Extrayez et organisez les données des tampons avec les nuanceurs, puis transmettez-les au GPU.
  5. Appelez la méthode de dessin pour indiquer au contexte d'activer les nuanceurs, d'exécuter les données et de mettre à jour le canevas.

Elle se présente comme suit:

Définir la matrice de perspective...

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

...définir la matrice de position...

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

...à définir la géométrie et l'apparence...

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

... remplir les tampons de données et les transmettre au contexte...

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

... et appeler la méthode draw

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

Pour chaque image, n'oubliez pas d'effacer le canevas si vous ne souhaitez pas que les éléments visuels alpha se superposent.

Le site

Hormis la grille et le tunnel de particules, tous les autres éléments d'interface utilisateur ont été conçus en HTML / CSS avec une logique interactive en JavaScript.

Dès le départ, nous avons décidé que les utilisateurs devaient interagir avec la grille le plus rapidement possible. Pas d'écran de démarrage, pas d'instructions, pas de didacticiel, juste "Go". Si l'interface est chargée, rien ne devrait la ralentir.

Pour ce faire, nous avons dû examiner attentivement comment guider un nouvel utilisateur dans ses interactions. Nous avons inclus des indications subtiles, comme le fait de modifier la propriété du curseur CSS en fonction de la position de la souris de l'utilisateur dans l'espace WebGL. Si le curseur est au-dessus de la grille, nous le placerons sur un curseur en forme de main (car ils peuvent interagir en tracé des tonalités). S'il est passé au-dessus de l'espace blanc autour de la grille, nous le remplaçons par un curseur croisé directionnel (pour indiquer qu'il peut pivoter ou faire éclater la grille en calques).

Se préparer pour le spectacle

LESS (un préprocesseur CSS) et CodeKit (développement Web sur les stéroïdes) ont vraiment réduit le temps nécessaire à la conversion des fichiers de conception en code HTML/CSS. Celles-ci nous permettent d'organiser, d'écrire et d'optimiser les CSS de manière beaucoup plus polyvalente, en exploitant des variables, des combinaisons (fonctions) et même des calculs mathématiques.

Effets de scène

À l'aide des transitions CSS3 et de backbone.js, nous avons créé des effets très simples qui donnent vie à l'application et fournissent aux utilisateurs des files d'attente visuelles indiquant l'instrument qu'ils utilisent.

Les coloris de Technitone.

Backbone.js nous permet d'identifier les événements de changement de couleur et d'appliquer la nouvelle couleur aux éléments DOM appropriés. Les transitions CSS3 accélérées par le GPU gèrent les changements de style de couleur avec peu ou pas d'impact sur les performances.

La plupart des transitions de couleur des éléments d'interface ont été créées par des transitions de couleurs d'arrière-plan. En plus de cette couleur d'arrière-plan, nous plaçons des images de fond avec des zones de transparence stratégiques pour laisser passer la couleur d'arrière-plan.

HTML: Les bases

Nous avions besoin de trois zones de couleur pour la démonstration: deux zones de couleurs sélectionnées par l'utilisateur et une troisième zone de couleurs mixtes. Nous avons créé la structure DOM la plus simple qui soit compatible avec les transitions CSS3 et le moins de requêtes HTTP pour notre illustration.

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

CSS: structure simple avec style

Nous avons utilisé le positionnement absolu pour placer chaque région à son emplacement correct et ajusté la propriété de position de l'arrière-plan pour aligner l'illustration de l'arrière-plan dans chaque région. De cette façon, toutes les régions (chacune avec la même image de fond) ressemblent à un seul élément.

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

Des transitions accélérées par le GPU ont été appliquées pour écouter les événements de changement de couleur. Nous avons augmenté la durée et modifié le lissage de vitesse pour les styles en ".color-mix" afin de donner l'impression qu'il fallait un certain temps pour mélanger les couleurs.

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

Consultez la page HTML5Please pour connaître les navigateurs compatibles et l'utilisation recommandée pour les transitions CSS3.

JavaScript: Fonctionnement

L'attribution dynamique de couleurs est simple. Nous recherchons dans le DOM n'importe quel élément avec notre classe de couleur et définissons la couleur d'arrière-plan en fonction des couleurs sélectionnées par l'utilisateur. Nous appliquons notre effet de transition à n'importe quel élément du DOM en ajoutant une classe. Cela permet de créer une architecture légère, flexible et évolutive.

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

Une fois les couleurs principale et secondaire sélectionnées, nous calculons la valeur de leur combinaison de couleurs et attribuons la valeur obtenue à l'élément DOM approprié.

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

Illustration pour l'architecture HTML/CSS: donner une personnalité à trois zones de couleurs changeantes

Notre objectif était de créer un effet de lumière amusant et réaliste qui préserve son intégrité en plaçant des couleurs contrastées dans des zones de couleur adjacentes.

Un fichier PNG 24 bits permet d'afficher la couleur d'arrière-plan de nos éléments HTML à travers les zones transparentes de l'image.

Transparences des images

Les cases colorées créent des arêtes dures là où différentes couleurs se rejoignent. Cela entrave les effets d'éclairage réalistes et représentait l'un des plus grands défis de la conception de l'illustration.

Colorer les régions

La solution consistait à concevoir l'illustration de sorte qu'elle ne permette jamais de voir les bords des zones de couleur à travers les zones transparentes.

Couleur des bords de la région

La planification de la compilation était essentielle. Une session de planification rapide entre concepteur, développeur et illustrateur a aidé l'équipe à comprendre comment tout ce qui devait être construit afin qu'il fonctionne ensemble une fois assemblé.

Consultez le fichier Photoshop pour voir comment l'attribution de noms de calques peut communiquer des informations sur la construction CSS.

Couleur des bords de la région

Encore

Pour les utilisateurs qui ne disposent pas de Chrome, notre objectif est de transmettre l'essence de l'application en une seule image statique. Le nœud de la grille est devenu le héros, les tuiles d'arrière-plan font allusion à l'objectif de l'application et la perspective présente dans la réflexion reflète l'environnement 3D immersif de la grille.

Colorer les bords de la région.

Si vous souhaitez en savoir plus sur Technitone, consultez régulièrement notre blog.

Le groupe

Merci d'avoir pris le temps de lire ce contenu. Peut-être serez-vous bientôt prêt à jouer avec vous !