TP1 programmable Web 2015-2016

De $1

Si vous ne l'avez pas déjà fait: installer NodeJS

Allez sur http://nodejs.org/ téléchargez et installez le serveur NodeJS. 

Update 10h30 ce matin: VERSION A JOUR DU ZIP, IL Y AVAIT LA MAUVAISE JUSQU'A PRESENT!!!!

Récupérez cette archive : Multitrackv1.1corrigé.zip

VOUS ETES PARTIS DANS D'AUTRES SALLES SANS LES FICHIERS AUDIO et le répertoire multitrack!!!!! Revenez me voir ou contactez moi ou voyez avec les élèves qui sont restés dans la classe.

Dézippez la dans un répertoire qui sera le répertoire de votre projet, par exemple "multitrackTP1".

Demandez ensuite à vos enseignants de vous donner l'archive du dossier "multitrack" contenant des chansons multipistes. Il faut que ce dossier soit aussi dans le dossier "multitrackTP1". Dézippez le. On vous passera un dossier beaucoup plus complet bientôt, avec une trentaine de chansons, ou bien chercher "mogg files", les fichiers de Rock Band, Guitar Hero et autres jeux se balladent sur le Web... On peut aussi importer des morceaux de SoundCloud...

Faites un cd à l'intérieur de ce répertoire, vous devriez voir :

  • index.html : la page d'accueil du projet
  • server.js : le code de l'application côté serveur. 
  • un dossier js contenant sound.js, buffer-loader.js, libs, ce dernier répertoire contenant la librairie jQuery
  • Un dossier "multitrack" contenant pour le moment deux chansons en multipistes, si vous regardez à l'intérieur, pour chaque piste mp3 il y a aussi une image png qui est le dessin de l'échantillon correspondant.

Il vous faut maintenant installer le module express pour NodeJS: tapez juste "nom install" dans le répertoire.

  • "npm" (pour Node Package Manager) est la commande qui permet d'installer dans des projets des modules complémentaires pour Node JS.

Si tout se passe bien, vous devriez avoir maintenant un répertoire comme ceci :

multi3.jpg 

 Test de l'application

Vous pouvez tester l'application en lançant la commande "node server.js" et en ouvrant "localhost:8081" dans votre navigateur.

Avant de faire quoi que ce soit, ouvrez la console de debug de votre navigateur pour regarder les traces (onglet "console").

L'exemple doit marcher avec tous les navigateurs récents, Desktop ou Mobiles.

Pour tester : ouvrir le menu déroulant et choisir la seconde piste disponible (deep purple), attendre que le bouton "start" devienne actif et cliquer dessus. La chanson démarre. Vous pouvez faire pause/stop/start et muter des pistes.

multi1.jpg

Etude de l'application côté serveur

Regardons le code de l'application côté serveur :

var fs = require("fs");
// We need to use the express framework: have a real web server that knows how to send mime types etc.
var express=require('express');

// Init globals variables for each module required
var app = express()
, http = require('http')
, server = http.createServer(app);

// Indicate where static files are located  
app.configure(function () {  
	app.use(express.static(__dirname + '/'));  
});  

// Config
var PORT = 8081,
	TRACKS_PATH = './multitrack/';

// launch the http server on given port
server.listen(PORT);

// routing
app.get('/', function (req, res) {
	res.sendfile(__dirname + '/index.html');
});

// routing
app.get('/track', function (req, res) {
	function sendTracks(trackList) {
		if (!trackList)
			return res.send(404, 'No track found');
		res.writeHead(200, { 'Content-Type': 'application/json' });
		res.write(JSON.stringify(trackList));
		res.end();
	}

	getTracks(sendTracks); 
});

// routing
app.get('/track/:id', function (req, res) {
	var id = req.params.id;
	
	function sendTrack(track) {
		if (!track)
			return res.send(404, 'Track not found with id "' + id + '"');
		res.writeHead(200, { 'Content-Type': 'application/json' });
		res.write(JSON.stringify(track));
		res.end();
	}

	getTrack(id, sendTrack); 

});

// routing
app.get(/\/track\/(\w+)\/(?:sound|visualisation)\/((\w|.)+)/, function (req, res) {
	res.sendfile(__dirname + '/' + TRACKS_PATH + req.params[0] + '/' + req.params[1]);
});

function getTracks(callback) {
	getFiles(TRACKS_PATH, callback);
}

function getTrack(id, callback) {
	getFiles(TRACKS_PATH + id, function(fileNames) {
		var track = {
			id: id,
			instruments: []	
		};
		fileNames.sort();
		for (var i = 0; i < fileNames.length; i += 2) {
			var instrument = fileNames[i].match(/(.*)\.[^.]+$/, '')[1];
			track.instruments.push({
				name: instrument,
				sound: instrument + '.mp3',
				visualisation: instrument + '.png'
			});
		}
		callback(track);
	})
}

function getFiles(dirName, callback) {
	fs.readdir(dirName, function(error, directoryObject) {
		callback(directoryObject);
	});
}

Bon, ça a l'air un peu compliqué mais n'ayons pas peur et regardons d'abord ce que ça fait...

Lignes 1-19 : initialisation du serveur

On va charger les modules nécessaires : "fs" pour "filesystem", permet de manipuler des fichiers, des répertoires. C'est avec ce module qu'on a pu implémenter la fonction getFiles() située tout à la fin du fichier. Cette fonction lit le contenu d'un répertoire et renvoie un tableau contenant les fichiers dans ce répertoire.

La ligne :

// Indicate where static files are located  
app.configure(function () {  
	app.use(express.static(__dirname + '/'));  
});

Indique que le serveur sera capable de renvoyer des fichiers statiques (par ex "/multitrack/amy_rehab/voix.mp3"), sans cette ligne on ne peut demander de fichiers via l'URL, seuls les routages explicites figurant dans la configuration du serveur seront pris en compte. 

Un tel routage est présent aussi dans la configuration, pour indiquer la page qui sera envoyée par défaut :

/ routing
app.get('/', function (req, res) {
	res.sendfile(__dirname + '/index.html');
});

Etc...

Ensuite nous avons les différents web services qui répondent à des requêtes GET... Celui-ci par exemple réponds à l'URL /track et il renvoie la liste des pistes disponibles sur le serveur, en JSON. Essayez donc d'ouvrir locahost:8081/track et regardez ce que vous recevez !

// routing
app.get('/track', function (req, res) {
	function sendTracks(trackList) {
		if (!trackList)
			return res.send(404, 'No track found');
		res.writeHead(200, { 'Content-Type': 'application/json' });
		res.write(JSON.stringify(trackList));
		res.end();
	}

	getTracks(sendTracks); 
});

On reçoit quelque chose comme: 

 ["amy_rehab","beatles_64","beatles_submarine","bob_love","bowis_dance","clash_should","deep_smoke","depeche_never",
"dire_sultans","doors_2times","eagles_hotel","handrix_castles","jamesbrown_get","megadeth_peace","metallica_sandman",
"mj_beatit","motorhead_ace","muse_uprising","nirvana_smell","offspring_come","police_roxanne","queen_champions",
"queen_dsmn","radiohead_creep","ramones_sedated","ratm_killing","rem_religion","rhcp_bridge","rhcp_give",
"sg_soundsilence","soundgarden_black","sp_zero","spice_wannabe","sublime_wig","system_chop","trust_antisocial",
"village_ymca","weezer_say","yes_owner"]

C'est un tableau JSON. Vous vous doutez qu'on l'utilise pour la construction du menu déroulant dans l'interface graphique.

De même, on a défini des web services pour les URLs suivants :

  • "/track" : renvoie la liste des chansons,
  • "/track/song_name", par exemple "/track/amy_rehab" ou "/track/deep_smoke" renvoie la liste des pistes pour une chanson, exemple: 

    {"id":"deep_smoke","instruments":[{"name":"basse","sound":"basse.mp3","visualisation":"basse.png"},{"name":"batterie","sound":"batterie.mp3","visualisation":"batterie.png"},{"name":"guitare","sound":"guitare.mp3","visualisation":"guitare.png"},{"name":"voix","sound":"voix.mp3","visualisation":"voix.png"}]}

  • "/track/song_name/visualisation/image.png", par exemple "http://localhost:8081/track/deep_smo...ation/voix.png", renvoie l'image de l'echantillon sonore correspondant à la piste de voix de la chanson 'deep_smoke"

     
  • "/track/song_name/sound/sound.mp3", par exemple http://localhost:8081/track/deep_smoke/sound/voix.mp3, renvoie le fichier audio de la piste de voix de la chanson "deep_smoke"

Voilà, vous pourrez regarder en détails comment ceci est implémenté.

Vous avez compris qu'avec ce code serveur, on va pouvoir obtenir toutes les informations que nous désirons depuis un client Ajax. 

Contact avec la librairie Web Audio

Supportée par tous les browsers récents dans leurs dernières version sauf IE. 

Le principe de WebAudio consiste à construire un graphe connectant une ou plusieurs sources audio (des échantillons chargés en mémoire ou un flux streamé ou encore le son provenant du micro ou d'une des entrées d'une carte son) aux haut parleurs et démarrer la lecture. Si on veut, on peut ajouter des noeuds intermédiares dans ce graphe, comme des controles de volume, des analyseurs de fréquence, des effets spéciaux (égaliseurs, compresseurs, echo, reverb, etc.).

Ce petit exemple qui montre comment charger un échantillon et le lire : 

<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Web Audio API</title>
<script>
    var context, 
        soundSource, 
        soundBuffer,
        buttonPlay, buttonStop, buttonLoad,
        url = '/multitrack/deep_smoke/guitare.mp3';

    // Step 1 - Initialise the Audio Context
    // There can be only one!
    function init() { 
        context = new AudioContext();

        buttonPlay = document.querySelector("#play");
        buttonStop = document.querySelector("#stop");
        buttonLoad = document.querySelector("#load");
    }

    // Step 2: Load our Sound using XHR
    function loadSound() {
        console.log("loading " + url + " using Xhr2");
        // Note: this loads asynchronously
        var request = new XMLHttpRequest();
        request.open("GET", url, true);
        // BINARY TRANSFERT !
        request.responseType = "arraybuffer";

        // Our asynchronous callback
        request.onload = function() {
            var audioData = request.response;
            // We got the sound file from the server, let's decode it
            decode(audioData);
        };

        request.send();
    }

    // Finally: tell the source when to start
    function playSound() {
        // play the source now. 
        // First parameter = delay in seconds before starting to play
        // Second parameter = where do we start (0 = beginning of song)
        console.log("playing sound");

        // connect sound samples to the speakers
        buildGraph();

        // BEWARE : the graph should be connected, if sound has been stopped,
        // and if the graph is not built (i.e the previous line of code is not present)
        // Then the next line will do nothing, we need to rebuild the graph
        soundSource.start(0, 0);

        buttonStop.disabled = false;
        buttonPlay.disabled = true;
    }

    function stopSound() {
         console.log("Stopping sound, Graph destroyed, cannot be played again without rebuilding the graph !");
        // stop the source now.
        // Parameter : delay before stopping
        // BEWARE : THIS DESTROYS THE NODE ! If we stop, we need to rebuid the graph again !
        // We do not need to redecode the data, just to rebuild the graph
        soundSource.stop(0);
        buttonPlay.disabled = false;
        buttonStop.disabled = true;
    }

    function buildGraph() {
        console.log("Building the audio graph : connecting decoded sound sample to the speakers");
        // create a node with the decoded sound source
        soundSource = context.createBufferSource();
        soundSource.buffer = soundBuffer;

      // Plug the cable from one thing to the other
      // Here we connect the decoded sound sample to the speakers
        soundSource.connect(context.destination);
    }

    function decode(audioData) {
        console.log("decoding audio data... WebAudio uses RAW sample in memory, not compressed one");
           
        // The Audio Context handles creating source buffers from raw binary
        context.decodeAudioData(audioData, function onSuccess(soundBufferDecoded) {
            soundBuffer = soundBufferDecoded;

            console.log("sample ready to be played, decoded. It just needs to be inserted into an audio graph");
            
            buttonPlay.disabled = false;
            buttonLoad.disabled = true;
        }, function onFailure() {
            alert("Decoding the audio buffer failed");
        });             
    }
</script>
</head>
<body onload=init();>
  <button id = "load" onclick="loadSound();">Charger son via XhR2</button>
  <button id="play" onclick="playSound();" disabled>Lecture du son</button>
  <button id="stop" onclick="stopSound();" disabled>Stopper le son</button>
</body>
</html>

Copiez ce code dans un fichier "test.html" et copiez le fichier dans le répertoire "multitrackTP1" et testez avec l'URL "http://localhost:8081/test.html", attention, l'échantillon sonore commence par 5s de silence...

A remarquer :

  1. On charge le fichier en Ajax en mode binaire, dans la fonction loadSound, avec request.responseType = "arraybuffer";

  2. Les chargements sont asynchrones, car en JavaScript, d'une manière générale, tout ce qui peut prendre du temps, est asynchrone. On donne donc une fonction de callback qui sera appelée quand le travail est terminé. Ici, c'est la fonction request.onload qui est appelée lorsque le fichier demandé est arrivé.
     
  3. Une fois le fichier arrivé, WebAudio a besoin de le décoder. En effet, on va charger un fichier mp3 mais ce fichier est compressé. Pour des besoins de performances on travaille avec des échantillons non compressés en mémoire. La fonction loadSound appelle donc la fonction decode() quand le fichier est arrivé. Comme le décodage peut prendre du temps, il est lui aussi asynchrone ! Regardez le callback dans la fonction decode() !
     
  4. Une fois le fichier récupéré et décodé il est conservé dans la variable globale soundBuffer, on va donc pouvoir l'utiliser !
     
  5. Enfin, on va pouvoir jouer le son, voir la fonction playSound(), pour jouer un son il faut construire un graphe puis appeler la méthode start(delay, tempsDépart) sur un noeud correspondant à un échantillon décodé.
     
  6. Pour jouer un son il est nécessaire de construire un "graphe audio", on va donc en faire un très simple avec comme origine un noeud correspondant à l'échantillon décodé, et comme destination les haut parleurs de l'ordinateur (noeud prédéfini appelé context.destination), c'est la fonction buildGraph() qui s'en charge.
     
  7. Attention, pour arrêter un son, la méthode stop(delay) d'un noeud de type échantillon, le détruit. Les noeuds de type BufferSource ne sont utilisables qu'une seule fois! C'est pour cela que chaque fois qu'on va vouloir rejouer un son après l'avoir arrêté il faut reconstruire le noeud et le reconnecter : on appelle buildGraph systématiquement depuis playSound();

Pour charger plusieurs sons et les décoder avant de pouvoir les jouer

Comment faire dans le cas qui nous intéresse, c'est à dire charger plusieurs fichiers audio, puis les décoder avant de pouvoir construire un graphe pour les lire ? Le code JavaScript pour faire cela n'est pas évident car tous les appels Ajax sont asynchrones, et les appels au décodage des fichiers également.

L'article http://www.html5rocks.com/en/tutoria...ebaudio/intro/ propose une classe BufferLoader permettant d'effectuer un chargement asynchrone de plusieurs fichiers audio + leur décodage. L'article donne un exemple d'utilisation. Nous l'avons également utilisée dans le prototype du lecteur multipistes, dans le fichier sound.js, fonction loadAllSoundSamples()...

var bufferLoader;
function loadAllSoundSamples(tracks) {

    bufferLoader = new BufferLoader(
            context,
            tracks,
            finishedLoading
            );
    bufferLoader.load();
}
function finishedLoading(bufferList) {
    console.log("finished loading");
    resetAllBeforeLoadingANewSong();
    buffers = bufferList;
    buttonPlay.disabled = false;
}

Ou tracks est le tableau des fichiers audio à charger.

Travail à faire

Bon, vous avez vu comment implémenter un petit serveur qui propose via des URLs de web services de fournir les données nécessaires à l'application. Nous avons vu le principe de chargement asynchrone et de décodage des échantillons pour les jouer avec Web Audio, puis également comment on peut charger plusieurs échantillons en Ajax.

Nous vous proposons 1) de regarder un peu le code du prototype fourni et 2) de partir de zéro pour en écrire un à vous. Pour le moment oubliez la partie "canvas/dessin/animation", vous pourrez vous contenter de récupérer simplement les images des échantillons, les afficher, etc.

3) de voir comment on a pu connecter au graphe un contrôleur de volume, comment on a pu muter les pistes, etc.

Le but du jeu n'est pas que vous refassiez notre proto, mais que vous l'ayez sous la main pour voir comment certaines fonctionnalités ont été implémentées en vue de créer votre propre prototype.

Et bien sûr: regardez et lisez la partie du Mooc HTML5 part 2 sur Web Audio!