Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

SoundJS, suoni per giochi e applicazioni nel browser

Scopriamo le funzionalità principali di SoundJS e creiamo un semplice pianoforte, con una variante 3D realizzata con BabylonJS
Scopriamo le funzionalità principali di SoundJS e creiamo un semplice pianoforte, con una variante 3D realizzata con BabylonJS
Link copiato negli appunti

SoundJS è un audio engine, una libreria che permette di gestire l'audio all'interno dei browser. Grazie ad essa possiamo associare suoni agli eventi e alle azioni nelle nostre applicazioni Web o costruire l'ambiente nei nostri giochi. Del resto SoundJS è parte della suite CreateJS, nota per essere il framework con cui è stato realizzato il progetto Atari Games.

Il tag audio HTML5 e oltre

Le tecnologie su cui si basa principalmente il framework sono quelle di HTML5 e possiamo pensare che, per i browser più attuali, il tag audio e le API WebAudiosiano sufficienti a manipolare tutti gli aspetti del sonoro. Anche se non è ancora proprio così (sul browser Android ad esempio il volume dell'audio si può regolare solo con i tasti del device), la resa in generale è buona.

Come sempre poi bisognerà tenere in considerazione i browser meno recenti, dove non troviamo il supporto alle tecnologie più recenti.

La buona notizia è che quasi tutto il "lavoro sporco" viene compiuto dietro le quinte dal framework. Per quanto ci riguarda possiamo sviluppare con SoundJS quasi ignorando ciò che accade a basso livello. Ci sono infatti tre plugins che garantiscono una omogeneità di funzioni costante:

WebAudioPlugin è il plugin utilizzato di default, che sfrutta le API WebAudio per la riproduzione dei suoni. Poiché per caricare i suoni utilizza una XMLHttpRequest, per alcuni browser può non funzionare in locale e rilevare una problema di violazione di dominio (Cross Origin request).
HTMLAudioPlugin è il plugin di fallback utilizzato se qualcosa non va con le API WebAudio o se non sono supportate. Esso effettua la riproduzione dei suoni utilizzando il tag HTML5 audio.
FlashPlugin è il plugin addizionale per i browser più datati, si appoggia su un riproduttore realizzato con tecnologia Flash e su SWFObject per l'inclusione all'interno del progetto. Per attivare questo plugin dobbiamo agire manualmente per caricarlo e configurarlo.

In sostanza la libreria permette di adattare il risultato anche ad ambienti che non sono moderni o compatibili con gli standard.

In questo articolo vediamo prendere rapidamente confidenza con questa libreria per sfruttare le potenzialità dei browser moderni come Internet Explorer 11, per farlo ci aiuteremo con un piccolo esempio

Il progetto di base con SoundJS

Partiamo definendo una progetto minimale, con un file index.html, una cartella per le librerie e il codice e una per sistemare i nostri file audio:

index.html
[js][/js]
-- test_soundjs.js
[audio-assets]

Per utilizzare SoundJS, possiamo scaricare il pacchetto dal sito ufficiale o da gitHUB. Oppure caricare l'API pubblica ogni volta sfruttando la CDN del progetto CreateJS:

<script src="http://code.createjs.com/createjs-2013.09.25.min.js"></script>

Cosa che faremo anche per semplificarci le cose in questo primo esempio. Perciò la pagina index.html conterrà quanto segue:

<!doctype html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<script src="http://code.createjs.com/createjs-2013.09.25.min.js"></script>
	<script src="js/test_soundjs.js"></script>
	<title>SoundJS template</title>
</head>
<body onload="init()">
</body>
</html>

Fatto questo non ci rimane che cominciare a scrivere il codice della funzione init() all'interno del file test_soundjs.js.

La classe Sound

Per il caricamento (preload) e la riproduzione dei suoni e per l'eventuale registrazione manuale di plugin, ci serviamo della potentissima classe Sound.

registerSound: associare i suoni all'applicazione

In SoundJS prima di poter riprodurre un suono dobbiamo "registrarlo". In altre parole associamo i file audio a specifici id, cui poter far riferimento durante il programma.

Ad esempio se abbiamo un file con il rumore di piatti rotti, possiamo associargli il nome "crash". Per effettuare praticamente questa associazione ci serviamo del metodo registerSound, cui possiamo passare un oggetto in cui specifichiamo l'URL del nostro file e il relativo id:

createjs.Sound.registerSound({
		src:"assets/crash.mp3",
		id:"crash"
	});

Questo incherà al browser di effettuare una richiesta GET al server per ottenere il nostro asset. Possiamo specificare più di formato di file per lo stesso suono, separando con il carattere pipe (|) i diversi file:

createjs.Sound.registerSound({
		src:"path/crash.mp3|path/crash.ogg",
		id:"crash"
	});

play: riprodurre i suoni

Una volta ottenuto il file possiamo riprodurlo con il metodo play, al quale passiamo l'id che abbiamo assegnato al nostro suono:

// riproduce il suono con id="crash"
createjs.Sound.play("crash");

Il metodo play ritorna un'oggetto di tipo SoundInstance, che possiamo utilizzare per interrompere l'esecuzione (pause), farla riprendere (resume), regolarne il volume (setVolume), etc.

Mandando in esecuzione l'esempio in locale ascolteremo un bel fragore di piatti rotti. Ma non è affatto tutto qui. Del resto parliamo di preload, quindi di qualcosa che per sua natura richiede un meccanismo asincrono. Vediamo allora come affrontare la faccenda.

Effettuare il preload, l'evento fileload

Questo piccolo esperimento in locale potrebbe anche funzionare bene, ma online le cose potrebbero non andare sempre liscie, soprattutto se stiamo maneggiando file di grosse dimensioni o in caso di connettività non troppo performante.

Per questo SoundJS ci offre l'opportunità di conoscere il momento in cui vengono caricati i file, grazie all'evento "fileload". Vediamo come creare un semplice handler per riprodurre il crash solo a caricamento avvenuto:

createjs.Sound.addEventListener("fileload", function (event) {
	// riproduce il suono con id="crash"
	createjs.Sound.play("crash");
});

Ora che abbiamo fatto un minimo di pratica e capito i fondamentali possiamo dilettarci in qualcosa di più articolato.

Suonare il Piano

Ciò che vedremo in questo secondo esempio ha a che fare con il precaricamento multiplo di suoni e con l'associazione dell'esecuzione dell'audio ad eventi. Quello che realizzeremo è un mini-piano da un'ottava con 13 tasti, da suonare digitando sulla tastiera.

ALT_TEXT

Il nostro schema di base rimane lo stesso: inseriremo nella nostra cartella degli asset audio 13 suoni, che preleviamo da internet, di una scala cromatica che va da un do al do dell'ottava superiore.

index.html
[js][/js]
-- piano.js
[audio-assets]
--3c.mp3
--3cs.mp3
--3d.mp3
--3ds.mp3
...

registerManifest: caricamento multiplo di file audio

Una volta che ci siamo procurati i file possiamo importarli in un unica operazione di preload, grazie al metodo registerManifest, che prende in input il path di base in cui trovare i file e un array di oggetti in cui scriveremo le informazioni (nome del file e id) di ciascun elemento.

Vediamo anzitutto come caricare il nostro array di note:

var notes = [
		{ src:"3c.mp3",  id:"c"  },
		{ src:"3cs.mp3", id:"cs" },
		{ src:"3d.mp3",  id:"d"  },
		{ src:"3ds.mp3", id:"ds" },
		{ src:"3e.mp3",  id:"e"  },
		{ src:"3f.mp3",  id:"f"  },
		{ src:"3fs.mp3", id:"fs" },
		{ src:"3g.mp3",  id:"g"	 },
		{ src:"3gs.mp3", id:"gs" },
		{ src:"3a.mp3",  id:"a"  },
		{ src:"3as.mp3", id:"as" },
		{ src:"3b.mp3",  id:"b"  },
		{ src:"4c.mp3",  id:"c2" }
	];

Anche se questa operazione è banale, avere i suoni logicamente raggruppati e identificabili tramite array di oggetti, può farci immaginare di poter gestire i dati in modo semplice, più avanti ci tornerà utile.

Non ci resta che tirar su tutto il pacchetto di suoni:

createjs.Sound.registerManifest(notes, "audio-assets/");

Ora possiamo impostare la logica che assocerà gli eventi della tastiera alla riproduzione dei suoni. La dichiariamo all'interno della callback:

createjs.Sound.addEventListener("fileload", function (event) {

    createjs.Sound.defaultInterruptBehavior = createjs.Sound.INTERRUPT_ANY;

	// elaborazione degli eventi della tastiera
	window.onkeydown = function(event) {
		switch (event.which) {
			case 90: createjs.Sound.play("c");   break;
			case 83: createjs.Sound.play("cs");	 break;
			case 88: createjs.Sound.play("d");   break;
			case 68: createjs.Sound.play("ds");  break;
			case 67: createjs.Sound.play("e");   break;
			case 86: createjs.Sound.play("f");   break;
			case 71: createjs.Sound.play("fs");  break;
			case 66: createjs.Sound.play("g");   break;
			case 72: createjs.Sound.play("gs");  break;
			case 78: createjs.Sound.play("a");   break;
			case 74: createjs.Sound.play("as");  break;
			case 77: createjs.Sound.play("b");   break;
			case 188: createjs.Sound.play("c2"); break;
				
			default: return; 
		}
		return false;
	};
});

Cosa faccia questo codice è chiaro, che sia un po' lungo forse anche. Ma prima di passare ad snellire questo pezzo di codice è utile descrivere brevemente l'ipostazione della proprietà defaultInterruptBehavior. Grazie ad essa possiamo stabilire come si comporta l'engine quando arriviamo a utilizzare il numero massimo di istanze per i suoni. Nel nostro caso decidiamo di interrompere tutti i canali e lasciar spazio alle chiamate più recenti.

Dedichiamo qualche istante a sistemare un po' il codice, più che altro a ridurlo anche in vista della prossima evoluzione. Ad esempio possiamo sfruttare l'array di oggetti per aggiungere lì alcune informazioni di interfaccia (anche se la cosa è opinabile è comunque interessante da guardare). Riscriviamo il nostro array in questo modo:

var notes = [
		{ src:"3c.mp3",  id:"c",  key: 90 },
		{ src:"3cs.mp3", id:"cs", key: 83 },
		{ src:"3d.mp3",  id:"d",  key: 88 },
		{ src:"3ds.mp3", id:"ds", key: 68 },
		{ src:"3e.mp3",  id:"e",  key: 67 },
		{ src:"3f.mp3",  id:"f",  key: 86 },
		{ src:"3fs.mp3", id:"fs", key: 71 },
		{ src:"3g.mp3",  id:"g",  key: 66 },
		{ src:"3gs.mp3", id:"gs", key: 72 },
		{ src:"3a.mp3",  id:"a",  key: 78 },
		{ src:"3as.mp3", id:"as", key: 74 },
		{ src:"3b.mp3",  id:"b",  key: 77 },
		{ src:"4c.mp3",  id:"c2", key: 188 }
	];

e possiamo riscrivere il nostro event handler in questo modo:

createjs.Sound.addEventListener("fileload", function (event) {

		createjs.Sound.defaultInterruptBehavior = createjs.Sound.INTERRUPT_ANY;

		// elaborazione degli eventi della tastiera
		window.onkeydown = function(event) {

		var keydown = event.which;
		for (n in notes) {
			if (notes[n].key == keydown) {
				createjs.Sound.play(notes[n].id);
				break;
			}
		}
		return;
	};
});

I vantaggi di questa costruzione della demo saranno ancora più evidenti quando collegheremo oggetti più complessi agli elementi audio. L'idea è quella di collegare oggetti o eventi direttamente ai suoni che dovranno produrre.

Naturalmente questo concetto si applica bene all'esempio della simulazione di uno strumento musicale, ma non è detto che sia così valido per un platform game.

Un minimo di interfaccia

Completiamo quest'esempio realizzando una interfaccia minimale, composta da una immagine di una tastiera di pianoforte, con le note contrassegnate dalle lettere e dai simboli corrispondenti ai suoni sulla tastiera del PC. La inseriamo nel body dell'applicazione:

<img src="piano.png" />

e le diamo uno stile "responsive":

<style>img {max-width: 100%; height: auto }</style>

Ecco il risultato (basta cliccare dentro e provare a suonare):

Pianoforte 3D

Anche se l'esempio è affascinante l'interfaccia non è delle migliori, molto legata all'utilizzo della tastiera del PC. Per concludere facciamoci prendere la mano dal 3D e creiamo un'interfaccia più interessante servendoci di BabylonJS per creare una tastiera e per intercettare anche gli eventi del puntatore.

Il nostro index.html diventerà:

<!doctype html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<script src="http://code.createjs.com/createjs-2013.09.25.min.js"></script>
	<script src="js/babylon.1.7.js"></script>
	<script src="js/piano.js"></script>
	<style>canvas {width: 100%; height: auto }</style>
	<title>SoundJS template</title>
</head>
<body onload="init()">
<canvas id="scene3d" width="100%" height="100%"></canvas>
</body>
</html>

A parte l'introduzione del file di BabylonJS e la definizione del canvas non abbiamo cambiato molto.

Vediamo cosa accade al nostro codice in piano.js. Non ci soffermeremo troppo sui dettagli di BabylonJS, cui dedichiamo spazio in articoli specifici, inoltre le parti che utilizziamo saranno molto semplici.

Anzitutto istanziamo l'engine e impostiamo la scena 3d, quindi impostiamo la posizione di una telecamera e di una luce:

var engine = new BABYLON.Engine(document.getElementById('scene3d'), true);
var scene  = new BABYLON.Scene(engine);
var camera = new BABYLON.FreeCamera("Camera", new BABYLON.Vector3(0,0,-10), scene);
var light0 = new BABYLON.PointLight("Onmi0",  new BABYLON.Vector3(60,-210,-530), scene);

Fatto questo creiamo i tasti del nostro mini piano e li associamo ai relativi suoni con la stessa tecnica utilizzata prima per la tastiera del PC. Inoltre aggiungiamo un'informazione per il rendering, ovvero se si tratta di tasti bianci o neri:

var notes = [
		{ src:"3c.mp3",  id:"c",  key: 90,  obj:BABYLON.Mesh.CreateBox( "c",  1, scene), color: "w" },
		{ src:"3cs.mp3", id:"cs", key: 83,  obj:BABYLON.Mesh.CreateBox( "cs", 1, scene), color: "b" },
		{ src:"3d.mp3",  id:"d",  key: 88,  obj:BABYLON.Mesh.CreateBox( "d",  1, scene), color: "w" },
		{ src:"3ds.mp3", id:"ds", key: 68,  obj:BABYLON.Mesh.CreateBox( "ds", 1, scene), color: "b" },
		{ src:"3e.mp3",  id:"e",  key: 67,  obj:BABYLON.Mesh.CreateBox( "e",  1, scene), color: "w" },
		{ src:"3f.mp3",  id:"f",  key: 86,  obj:BABYLON.Mesh.CreateBox( "f",  1, scene), color: "w" },
		{ src:"3fs.mp3", id:"fs", key: 71,  obj:BABYLON.Mesh.CreateBox( "fs", 1, scene), color: "b" },
		{ src:"3g.mp3",  id:"g",  key: 66,  obj:BABYLON.Mesh.CreateBox( "g",  1, scene), color: "w" },
		{ src:"3gs.mp3", id:"gs", key: 72,  obj:BABYLON.Mesh.CreateBox( "gs", 1, scene), color: "b" },
		{ src:"3a.mp3",  id:"a",  key: 78,  obj:BABYLON.Mesh.CreateBox( "a",  1, scene), color: "w" },
		{ src:"3as.mp3", id:"as", key: 74,  obj:BABYLON.Mesh.CreateBox( "as", 1, scene), color: "b" },
		{ src:"3b.mp3",  id:"b",  key: 77,  obj:BABYLON.Mesh.CreateBox( "b",  1, scene), color: "w"	},
		{ src:"4c.mp3",  id:"c2", key: 188, obj:BABYLON.Mesh.CreateBox( "c2", 1, scene), color: "w" }
	];

Grazie a questa definizione abbiamo già sulla scena tutti i tasti sotto forma di cubetti bianchi, dovremo quindi dare a ciascuno la giusta forma e posizione.

// variabili di servizio per impostare le posizioni dei tasti
	var wk_pos = -3.5, bk_pos = -3, step = 1.2;
	var material = new BABYLON.StandardMaterial("blackmaterial", scene);
	material.diffuseColor = new BABYLON.Color3(0, 0, 0);
	for (k in notes) {
		// se è un tasto bianco
		if(notes[k].color == "w") {
			notes[k].obj.position = new BABYLON.Vector3(wk_pos++*step, 0, 0);
			notes[k].obj.scaling.y = 4; // lunghezza dei tasti bianchi
		// se invece è un tasto nero
		} else {
			if(notes[k].obj.id=="fs") bk_pos++; // saltiamo un tasto
			notes[k].obj.position = new BABYLON.Vector3(bk_pos++*step, 1, -0.2);
			notes[k].obj.scaling.y = 3; // lunghezza dei tasti neri
			notes[k].obj.material = material;
		}
	}
	engine.runRenderLoop( function() { scene.render(); });

Con runRendeLoop finalmente abbiamo disegnato la nostra scena. Anche se può sembrare un po' criptico si tratta veramente di operazioni molto semplici per posizionare i tasti e dare le dimensioni ai bianchie e ai neri.

Passiamo al preload e alla gestione degli eventi, manteniamo il supporto alla tastiera (anche se non abbiamo aggiunto i simboli sui tasti 3d).

// elaborazione degli eventi della tastiera
		window.onkeydown = function(event) {
			var keydown = event.which;
			console.log(keydown);
			for (n in notes) {
				if (notes[n].key == keydown)
				{
					createjs.Sound.play(notes[n].id);
					notes[n].obj.position.z = (notes[n].color=="w")?.2:-0.1;
					return;
				}
			}
			return false;
		};
		// elaborazione degli eventi della tastiera
		window.onkeyup = function(event) {
			var keyup = event.which;
			for (n in notes) {
				if (notes[n].key == keyup)
				{
					notes[n].obj.position.z = (notes[n].color=="w")?0:-0.2;
					break;
				}
			}
			return false;
		};

Oltre al keydown gestiamo anche il rilascio del tasto e in entrambi modifichiamo anche la posizione del tasto che si alzerà e abbasserà all'occorrenza.

La ciliegina sulla torta ce la regala BabylonJS, che ci permette di sapere quale oggetto sulla scena tridimensionale abbiamo cliccato.

window.addEventListener("click", function (evt) {
			// Proviamo a intercettare un oggetto
			var pickResult = scene.pick(evt.clientX, evt.clientY);
			var noteID = pickResult.pickedMesh.id;
			console.log(noteID);
			for(n in notes){
				if (notes[n].id == noteID)
				{
					createjs.Sound.play(notes[n].id);
					notes[n].obj.position.z = (notes[n].color=="w")?.2:-0.1;
					window.setTimeout(function() {
						notes[n].obj.position.z = (notes[n].color=="w")?0:-0.2;
					}, 100);
					return;
				}
			}
		});

Passiamo al metodo pick della classe scene la posizione rilevata al click del mouse e leggiamo l'id dell'oggetto (Mesh) corrispondente. Il resto è banale associazione con la nota.

Anche qui ci preoccupiamo di muovere i tasti che premiamo, per dare una migliore risposta utente, e per il rilascio, invece di leggere un'altro evento, ci limitiamo ad aspettare un decimo di secondo.

Ecco il risultato:

Le possibilità sono veramente illimitate e la nostra piccola applicazione sta già diventando qualcosa che occhieggia al touch, per questo sarebbe interessante sperimentare anche con una libreria come handjs o con i pointerEvents direttamente. Ma non voglio togliervi tutto il divertimento!

Ti consigliamo anche