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

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

Technitone.com associe WebGL, Canvas, Web Sockets, CSS3, JavaScript, Flash et 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 certains des workflows que nous utilisons pour concevoir des expériences interactives. 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 de téléchargement qui vous permettra de les télécharger tous en un seul fichier ZIP.

L'équipe de production de gskinner.com

Le concert

Nous ne sommes pas des ingénieurs du son chez gskinner.com, mais si vous nous proposez un défi, nous trouverons une solution:

  • Les utilisateurs tracent des tons sur une grille,"inspirés" par la ToneMatrix d'Andre.
  • Les tonalités sont connectées à des instruments échantillonnés, des kits de batterie ou même aux enregistrements des utilisateurs.
  • Plusieurs utilisateurs connectés jouent simultanément sur la même grille
  • …ou passer en mode solo pour explorer seul
  • Les sessions sur invitation permettent aux utilisateurs d'organiser un groupe et de faire une jam impromptue.

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

Technitone par gskinner.com

Nous faisons également ce qui suit:

  • Stocker les compositions et les effets des utilisateurs sous forme de données et les synchroniser entre les clients
  • Proposez-lui des options de couleurs pour qu'il puisse dessiner des morceaux sympas.
  • Proposer une galerie pour que les utilisateurs puissent écouter, aimer ou même modifier le travail d'autres utilisateurs

Nous avons conservé la métaphore de la grille familière, l'avons flottante dans l'espace 3D, ajouté des effets d'éclairage, de texture et de particules, et l'avons intégrée dans une interface flexible (ou en plein écran) basée sur CSS et JS.

Road trip

Les données sur les instruments, les effets et la grille sont consolidées et sérialisées sur le client, puis envoyées à notre backend Node.js personnalisé pour être résolues pour plusieurs utilisateurs à la manière de 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 correspondantes chargées de l'affichage de l'interface utilisateur, des échantillons et des effets lors de la lecture multi-utilisateur.

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

Schéma du serveur Technitone

Nous utilisons Node pour tous les aspects du serveur. Il s'agit d'un serveur Web statique et de notre serveur de sockets tout-en-un. Nous avons finalement utilisé Express, un serveur Web complet entièrement basé sur Node. Il est extrêmement évolutif, hautement personnalisable et gère les aspects de serveur de bas niveau à votre place (comme le ferait Apache ou Windows Server). En tant que développeur, vous n'avez plus qu'à vous concentrer sur la création de votre application.

Démonstration multi-utilisateur (en réalité, il ne s'agit que d'une capture d'écran)

Cette démonstration doit être exécutée à partir d'un serveur Node.js. Comme cet article n'en est 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é la démonstration en local. 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 par tous.

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

Node est simple. En combinant Socket.io et des requêtes 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. Le JSON est transmis.

À quel point ? Regarde ça.

Avec trois lignes de code JavaScript, nous avons 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/'));

Quelques éléments supplémentaires pour associer socket.io à la communication 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);

});

Nous allons maintenant commencer à écouter les connexions entrantes à partir 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 à l'aide d'une réponse impulsionnelle)…

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

… puis de 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. Plutôt que de définir un intervalle de minuteur égal à notre tempo pour traiter les sons à chaque battement, nous définissons un intervalle plus petit qui gère et planifie les sons dans une file d'attente. Cela permet à l'API d'effectuer la tâche préalable de résolution des données audio et de traitement des filtres et des effets avant de demander au processeur de les rendre audibles. Lorsque ce moment arrive, le système dispose déjà de toutes les informations nécessaires pour présenter le résultat net aux orateurs.

Dans l'ensemble, tout devait être optimisé. Lorsque nous poussions nos processeurs trop loin, des processus étaient ignorés (pop, click, scratch) pour respecter le calendrier. Nous avons mis tout en œuvre pour arrêter toute cette folie si vous passez à un autre onglet dans Chrome.

Spectacle de lumières

Au premier plan se trouvent notre grille et notre tunnel de particules. Il s'agit de la couche WebGL de Technitone.

WebGL offre des performances nettement supérieures à la plupart des autres approches de rendu visuel sur le Web, en demandant au GPU de travailler en collaboration avec le processeur. Ce gain de performances s'accompagne d'un développement beaucoup plus complexe, avec une courbe d'apprentissage beaucoup plus raide. Toutefois, si vous êtes vraiment passionné par l'interactivité sur le Web et que vous souhaitez limiter autant que possible les contraintes de performances, WebGL offre une solution comparable à Flash.

Démonstration WebGL

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

  • sommets de l'objet (géométrie)
  • matrices de position (coordonnées 3D)
    • des nuanceurs (description de l'apparence de la géométrie, directement liée 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 (ajuste les paramètres de la caméra qui explore l'espace 3D, définissant le plan d'image).
  2. Définissez la matrice de position (déclarez une origine dans les coordonnées 3D par rapport auxquelles les positions sont mesurées).
  3. Remplissez les tampons avec des données (position des sommets, couleur, 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 avec les données et de mettre à jour le canevas.

Voici comment cela se passe:

Définissez 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 positions…

// 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 une géométrie et une 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 appelez la méthode de dessin.

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

À chaque frame, n'oubliez pas d'effacer le canevas si vous ne voulez pas que les visuels basés sur l'alpha se superposent.

Le site

À l'exception de la grille et du tunnel de particules, tous les autres éléments d'interface utilisateur ont été créés en HTML / CSS et la logique interactive en JavaScript.

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

Nous avons donc dû examiner attentivement comment guider un utilisateur novice dans ses interactions. Nous avons inclus des indices subtils, comme la modification de la propriété du curseur CSS en fonction de la position de la souris de l'utilisateur dans l'espace WebGL. Si le curseur se trouve sur la grille, nous le remplaçons par un curseur en forme de main (car il peut interagir en traçant des tons). Si le curseur se trouve dans l'espace négatif autour de la grille, nous le remplaçons par un curseur en forme de croix directionnelle (pour indiquer qu'il peut faire pivoter la grille ou la diviser en calques).

Préparation de l'émission

LESS (un préprocesseur CSS) et CodeKit (développement Web surpuissant) ont considérablement réduit le temps nécessaire pour traduire les fichiers de conception en HTML/CSS. Ils nous permettent d'organiser, d'écrire et d'optimiser le CSS de manière beaucoup plus polyvalente, en utilisant des variables, des mix-ins (fonctions) et même des mathématiques.

Effets de scène

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

Couleurs de Technitone

Backbone.js nous permet de détecter 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 GPU géraient les modifications de style de couleur avec un impact minime ou nul sur les performances.

La plupart des transitions de couleur sur les éléments de l'interface ont été créées en modifiant les couleurs d'arrière-plan. Sur cette couleur d'arrière-plan, nous plaçons des images d'arrière-plan avec des zones de transparence stratégiques pour laisser transparaître la couleur d'arrière-plan.

HTML: les bases

Nous avions besoin de trois régions de couleur pour la démonstration: deux régions de couleur sélectionnées par l'utilisateur et une troisième région de couleur mélangée. Pour notre illustration, nous avons créé la structure DOM la plus simple possible, qui prend en charge les transitions CSS3 et le moins de requêtes HTTP possible.

<!-- 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 à sa place et ajusté la propriété background-position pour aligner l'illustration de fond dans chaque région. Toutes les régions (avec la même image de fond) ressemblent alors à 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 GPU ont été appliquées pour écouter les événements de changement de couleur. Nous avons augmenté la durée et modifié l'atténuation sur .color-mixed pour donner l'impression que le mélange des couleurs a pris du temps.

/* 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 HTML5please pour connaître la compatibilité actuelle des navigateurs et l'utilisation recommandée des transitions CSS3.

JavaScript: Fonctionnement garanti

Attribuer des couleurs de manière dynamique est simple. Nous recherchons dans le DOM tout élément avec notre classe de couleur et définissons la couleur d'arrière-plan en fonction des sélections de couleur de l'utilisateur. Nous appliquons notre effet de transition à n'importe quel élément du DOM en ajoutant une classe. Cela crée 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 primaire et secondaire sélectionnées, nous calculons leur valeur de couleur mélangée 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 de la personnalité à trois zones de changement de couleur

Notre objectif était de créer un effet d'éclairage amusant et réaliste qui conserve son intégrité lorsque des couleurs contrastantes sont placées dans des régions de couleurs adjacentes.

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

Transparences d&#39;image

Les cases colorées créent des bords nets là où les différentes couleurs se rencontrent. Cela entrave les effets d'éclairage réalistes et a été l'un des plus grands défis lors de la conception de l'illustration.

Régions de couleur

La solution a consisté à concevoir l'illustration de sorte que les bords des régions de couleur ne soient jamais visibles à travers les zones transparentes.

Colorer les bords de la région

La planification de la compilation était essentielle. Une session de planification rapide entre le concepteur, le développeur et l'illustrateur a permis à l'équipe de comprendre comment tout devait être construit pour fonctionner ensemble une fois assemblé.

Consultez le fichier Photoshop pour voir comment le nom des calques peut communiquer des informations sur la construction CSS.

Colorer les bords de la région

Encore

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

Couleur des bordures de la région.

Pour en savoir plus sur Technitone, suivez notre blog.

Le bracelet

Merci de nous avoir lu. Nous espérons bientôt jouer avec vous !