Il filmato contenente il suono pesa circa 0,5 MByte. Sono stato costretto a lasciarlo così perchè le barre si muovessero in modo sostanziale, altrimenti avrei duovuto dominuire troppo la qualità del suono. Consiglio quindi di lasciar caricare la pagina, e poi leggerla dopo aver avviato il caricamento del filmato.
Il Flash, da solo, non può analizzare in alcun modo lo spettro di un suono.
Il funzionamento dell'analizzatore, si basa su una serie di variabili create da un programma che analizza lo spettro, e ne ricava i picchi. Il flash legge i valori (inseriti in un filmato esterno), e in base a questi setta le dimensioni delle barre. C'è più di un programma in grado di generare queste variabili, ma quello che vediamo è lo Swif-tMP3 (un altro, ad esempio, è il FlashAmp, mentre gli analizzatori di Flashkit sono basati su un cgi prodotto da loro, il cui codice non è libero).
Swift-MP3
Lo Swift-MP3 è un programma della Swift-Tools, e può essere scaricato gratuitamente da questa pagina, dove è disponibile in varie versioni per ogni piattaforma. Il programma può essere utilizzato sia in remoto (come CGI), che in locale, come eseguibile, per creare uno o più filmati (anche in sequenza batch), partendo da semplici file mp3.
L'argomento di questo articolo non è l'uso del programma, ma la creazione dell'analizzatore. Per questo motivo, mi limito a spiegare il funzionamento del programma in locale su Windows (quello in remoto è molto facile, si tratta solo di passare alcune variabili tramite url).
Scarichiamo quindi la versione 2.1 da questo link (46k), e scompattiamo lo zip nella cartella C:swiftmp3 (può essere una cartella qualsiasi, uso questa per semplicità di percorsi). All'interno della cartella, troveremo 4 file:
- instructions.txt (istruzioni per l'uso del programma, in inglese)
- license.txt (termini della licenza d'uso)
- readme.txt (descrizione dei file, modifiche dall'ultima versione)
- swiftmp3.exe (l'eseguibile che genera i filmati, che può essere rinominato in cgi per l'uso in remoto)
Cliccando sull'eseguibile non succede niente, dal momento che bisogna accedervi tramite linea di comando. Quindi, nei sistemi Windows, dovremo aprire una finestra di "ms dos": andiamo al menu di Avvio, e scegliamo: "Prompt di MS-DOS" in win95/98 e "Prompt dei comandi" in winxp/2k.
Questa è la classica finestra che ci si presenterà davanti:
Potrebbe esserci scritto: C:Documents and Settingsutente ma non ha importanza. Quello che dobbiamo fare, è scrivere in ogni caso:
in modo da ritrovarci in:
Digitiamo ancora "swiftmp3", e vedremo questa schermata (tagliata per ragioni di spazio):
In questo modo, possiamo vedere le modalità di utilizzo dell'eseguibile.
Innanzitutto, bisogna digitare swiftmp3 (e non swift-mp3, dicitura dovuta probabilmente alle precedenti versioni), poi scrivere le opzioni di creazione del filmato, eventualmente il nome del filmato finale, e il nome del file mp3 da cui partire.
Le opzioni sono:
- fps N, dove N è il frame rate del filmato generato
- spectrum 0:1: scrivendo -spectrum 1 verranno generate le variabili per i picchi
- stop 0:1:2: omettendo questa opzione, il filmato esterno si riprodurrà in streaming al caricamento, scrivendo -stop 1 si fermerà al frame iniziale, scrivendo -stop 2 si fermerà a quello finale
- goto framelabel: con questa opzione, si può rimandare il filmato principale (non quello generato), ad una label specifica quando il filmato esterno è terminato
- out file.swf: si può specificare il nome per il filmato generato. Non scrivendo niente, avrà lo stesso nome dell'mp3 con estensione swf
Il filmato generato, conterrà il suono in streaming, non compresso (quindi con dimensioni uguali o maggiori dell'mp3). Inoltre, per ogni frame, verranno specificati i valori di 18 variabili, i cui nomi sono: s1, s2, s3, s4...s17, e il cui valore varia da 0 a 30.
Le dimensioni dell'swf, vengono quindi determinate dal frame rate (maggiore l'fps, maggiore il numero di variabili, maggiore il peso), in modo comunque non determinante (pochi kb).
(a dire il vero non vengono specificate tutte e 18 in ogni frame, ma solo quelle che cambiano rispetto al frame precedente: quindi se il valore di s13 al frame 23 è uguale a quello del frame 22, nel frame 23 non viene specificato).
Ora, se nel filmato con l'analizzatore noi rileviamo i valori dei picchi 24 volte al secondo (quindi il filmato ha un frame rate di 24 FPS), è inutile settare 36 nel frame rate del filmato esterno, perchè perderemmo alcuni valori. Allo stesso modo, è inutile usare un frame rate di 12, perchè avremmo dei cicli inutili in quello principale. In conclusione, è opportuno settare lo stesso frame rate in entrambi i filmati.
Facciamo un esempio. Copiamo nella cartella C:swiftmp3 il file "mio file.mp3". Non vogliamo mettere degli stop, perchè vogliamo che venga eseguito in ciclo, vogliamo che vengano generate le variabili, e che il frame rate sia 24 FPS. Scriveremo quindi nel prompt:
Nella scrittura dei comandi, bisogna stare molto attenti ai possibili errori, come unire i nomi delle opzioni ai rispettivi valori, o omettere i trattini, o usare spazi nel nome senza le virgolette. In questo caso, al più, verranno generati più filmati, magari vuoti, o senza le variabili.
A questo punto, abbiamo il filmato contenente il suono, senza stop o rimandi a diversi frame, con un frame rate di 24 FPS (con un frame rate minore di 24, l'analizzatore è abbastanza brutto, mancando di fluidità), e fingiamo che si chiami suono.swf: vediamo adesso la costruzione dell'analizzatore.
Innanzitutto, dobbiamo considerare un preloader per il filmato esterno, a meno che non vogliamo riprodurlo in streaming. Quindi, nella prima scena del fla scaricabile, ho inserito un movieclip con il seguente script:
onClipEvent (load) {
_root.stato = "stop";
_root.stop();
loadMovieNum ("suono.swf", 1);
_root.snd = new Sound(_level1);
_root.snd.setVolume(0);
}
onClipEvent (enterFrame) {
_level1.stop();
car = _level1.getBytesLoaded();
tot = _level1.getBytesTotal();
if(car == tot && car != undefined)_root.nextFrame();
perc = Math.round((car/tot)*100);
perc = (perc<10) ? "0" + perc : perc;
testo = "LOADING SOUND... " + perc + "%";
}
Il codice ferma la riproduzione del filmato principale, carica il filmato esterno sul livello 1, definisce un'istanza dell'oggetto Sound legata al livello1, e ne setta il volume a 0 (non sentiremo il suono fino a che non vorremo).
Quindi, ad ogni riproduzione del movieclip, vengono determinati i valori dei bytes caricati e totali: sulla base di questi, viene riempito un campo di testo che riporterà la percentuale di caricamento, e quando saranno uguali, la testina di riproduzione verrà spostata al frame seguente della timeline principale.
// al caricamento del movieclip
onClipEvent (load) {
// settiamo la variabile "stato" nella _root con il
// valore di "stop" ( ci servirà per i pulsanti)
_root.stato = "stop";
// fermiamo la riproduzione della timeline principale
_root.stop();
// carichiamo il filmato "suono.swf" sul livello 1
loadMovieNum ("suono.swf", 1);
// creiamo un'istanza dell'oggetto Sound con nome "snd"
_root.snd = new Sound(_level1);
// settiamo il volume dell'istanza sullo 0
_root.snd.setVolume(0);
}
// ad ogni riproduzione del movieclip
onClipEvent (enterFrame) {
// fermiamo il livello 1
_level1.stop();
// calcoliamo i bytes caricati e totali
car = _level1.getBytesLoaded();
tot = _level1.getBytesTotal();
// se il filmato esterno è carico, riprendiamo la
// riproduzione dal frame seguente della root
if(car == tot && car != undefined)_root.nextFrame();
// calcoliamo la percentuale di caricamento
perc = Math.round((car/tot)*100);
// se la percentuale è inferiore a 10, aggiungiamo
// davanti uno 0
perc = (perc<10) ? "0" + perc : perc;
// scriviamo i dati nel campo di testo
testo = "LOADING SOUND... " + perc + "%";
}
A questo punto, passiamo alla scena principale. Nel primo frame è presente questo script:
stop();
stato = "stop";
_level1.gotoAndStop(1);
snd.setVolume(100);
con cui fermiamo la riproduzione della timeline principale, settiamo la variabile "stato" come "stop" (già fatto nel preloader, ma utile per altre combinazioni), fermiamo il filmato con il suono nel frame 1, e settiamo il valore del volume di "snd" su 100. Inoltre sono presenti tre pulsanti, play, stop, e pausa.
Play:
on (release) {
_root.stato = "play";
_level1.play();
}
Stop:
on (release) {
_root.stato = "stop";
_level1.gotoAndStop(1);
}
Pausa:
on (release) {
if (_root.stato == "play") {
_root.stato = "pausa";
_level1.stop();
}
}
Il pulsante play, avvia la riproduzione del livello 1, dove è presente il filmato contenente il suono, e setta la variabile "stato" nella root con il valore di "play" (la variabile ci servirà anche più avanti per aggiungere altri pulsanti). Il pulsante stop, ferma il filmato esterno sul primo frame, e setta la variabile "stato" con il valore "stop". Il pulsante pausa, invece, solo quando il filmato esterno è in riproduzione, ne interrompe la riproduzione sul frame presente.
Nella libreria, è presente un movieclip chiamato barra, il cui linkage è "bar". Questo movieclip, contiene un rettangolo nero di dimensioni 4x1, con l'angolo inferiore sinistro sul punto di registrazione. Ci servirà per creare sia le barre, che i picchi.
Nel layer contenitore, è presente un movieclip vuoto, a cui è associato lo script che "crea" l'analizzatore:
onClipEvent (load) {
fscommand ("allowscale", false);
eR = 96;
eG = 122;
eB = 140;
sR = sG = sB = 255;
pR = Math.abs(Math.round((eR-sR)/30));
pG = Math.abs(Math.round((eG-sG)/30));
pB = Math.abs(Math.round((eB-sB)/30));
old = new Array();
for (i = 0; i < 14; i++) {
n = "bar"+i;
m = "pic"+i;
this.attachMovie("bar", n, i);
this.attachMovie("bar", m, 20+i);
this[n].colore = new Color(this[n]);
this[n]._x = this[m]._x = 26+(i*5);
old[i] = getTimer();
}
}
onClipEvent (enterFrame) {
for (i = 0; i < 14; i++) {
val = Number(_level1["s"+i]);
n = "bar"+i;
m = "pic"+i;
if (_root.stato == "play") {
this[n]._height = val;
this[n].colore.setRGB((sR-pR*val)<<16|(sG-pG*val)<<8|(sB-pB*val));
if (this[m]._y > -val) {
this[m]._y = -val;
old[i] = getTimer();
}
if (getTimer()-old[i]>1000)this[m]._y -= this[m]._y/10;
} else if (_root.stato == "stop") {
this[m]._y = Math.abs(this[m]._y>0.5) ? this[m]._y++ : 0;
if (this[n]._height>0)this[n]._height--;
}
}
}
Lo script potrebbe essere molto più semplice (basterebbe togliere i riferimenti al colore delle barre e all'accellerazione della caduta dei picchi), ma così è mmolto più completo e personalizzabile.
Innanzitutto, prenderemo solo i primi 14 dei 18 valori che vengono forniti dal filmato creato con lo Swift-MP3: questo perchè gli ultimi 4 rimangono quasi sempre a valori minimi, disturbando quindi l'estetica generale. Su questi 14 valori, ad ogni riproduzione del movieclip allungheremo o accorceremo le 14 barre create con l'attachMovie dal movieclip "barra" nella libreria, e alzeremo e abbasseremo i 14 picchi. Inoltre, coloreremo le barre a seconda della loro lunghezza, considerando un colore iniziale e un colore finale.
Creazione delle barre
for (i = 0; i < 14; i++) {
n = "bar"+i;
m = "pic"+i;
this.attachMovie("bar", n, i);
this.attachMovie("bar", m, 20+i);
this[n].colore = new Color(this[n]);
this[n]._x = this[m]._x = 26+(i*5);
old[i] = getTimer();
}
// con un ciclo di 14 iterazioni sul valore di "i"
for (i = 0; i < 14; i++) {
// alla variabile n associamo il valore della stringa
// composta da "bar" più il valore di "i"
n = "bar"+i;
// stessa cosa con la variabile "m" e la stringa "pic"
m = "pic"+i;
// attacchiamo al contenitore un'istanza del movieclip
// linkato come "bar", con nome "n" e profondità "i"
this.attachMovie("bar", n, i);
// stessa cosa per i movieclip con nome "m", e
// profondità 20+i 8altrimenti i movieclip si cancellano
// gli uni con gli altri
this.attachMovie("bar", m, 20+i);
// creiamo un'istanza dell'oggetto Color per ciascuna
// della barre
this[n].colore = new Color(this[n]);
// posizioniamo i movieclip a coppie secondo lo spostamento
// determinato dal valore di 26 più "i" per 5
this[n]._x = this[m]._x = 26+(i*5);
// riempiamo l'array "old" con 14 rilevazioni del tempo
// passato dall'inizio del filmato
old[i] = getTimer();
}
Con un ciclo che va da 0 a 13 (14 iterazioni), creiamo due variabili, m e n, che conterranno un riferimento a due diverse serie di movieclip attaccati dentro il movieclip contenitore. Nel primo caso i riferimenti saranno del tipo bar0, bar1, bar2, ...., bar13, nel secondo caso del tipo pic0, pic1, pic2, ...pic13. Con questi nomi, verranno attaccate 28 istanza (14 per ciascuna serie) del movieclip "barra" presente in libreria con il linkage "bar". Posizioniamo i movieclip a coppie (un picco e una barra) alla distanza di 26 pixel dal centro del contenitore, e con una aggiunta del prodotto tra 5 e il valore di i: i movieclip si sposteranno mano mano verso destra, alla distanza di un pixel l'uno dall'altro (5 è lsa distanza, 4 la larghezza del movieclip).
Per ogni elemento della serie di barre, creiamo un'istanza dell'oggetto Color, e riempiamo l'array old, composto da 14 elementi), con il valore attuale del tempo passato dall'inizio del filmato (questi tempi ci serviranno per le cadute dei picchi).
Determinazione dei colori
eR = 96;
eG = 122;
eB = 140;
sR = sG = sB = 255;
pR = Math.abs(Math.round((eR-sR)/30));
pG = Math.abs(Math.round((eG-sG)/30));
pB = Math.abs(Math.round((eB-sB)/30));
Consideriamo due colori, uno iniziale e uno finale, e i rispettivi valori RGB. Faremo in modo che partendo da uno dei due, le barre sfumino verso l'altro a seconda della loro lunghezza. Quindi chiamiamo come eR, eG, eB, i tre valori del colore finale, e come sR, sG, sB i tre valori del colore finale (nel nostro caso, un violetto e il bianco).
Dal momento che i valori delle variabili passate dal filmato esterno, vanno da 0 a 30, dividiamo le differenze rispettive tra i tre valori in trenta passi. Queste differenze, che si chiameranno pR, pG, pB, e di cui teniamo il valore assoluto, andranno moltiplicate per il valore estratto dallo spettro.
// valori del colore finale
eR = 96;
eG = 122;
eB = 140;
// valori del colore finale
sR = sG = sB = 255;
// valori assoluti delle differenze
// tra i due, divise per trenta
pR = Math.abs(Math.round((eR-sR)/30));
pG = Math.abs(Math.round((eG-sG)/30));
pB = Math.abs(Math.round((eB-sB)/30));
Ridimensionamento e colorazione delle barre
onClipEvent (enterFrame) {
for (i = 0; i < 14; i++) {
val = Number(_level1["s"+i]);
n = "bar"+i;
m = "pic"+i;
if (_root.stato == "play") {
this[n]._height = val;
this[n].colore.setRGB((sR-pR*val)<<16|(sG-pG*val)<<8|(sB-pB*val));
if (this[m]._y > -val) {
this[m]._y = -val;
old[i] = getTimer();
}
if (getTimer()-old[i]>1000)this[m]._y -= this[m]._y/10;
} else if (_root.stato == "stop") {
this[m]._y = Math.abs(this[m]._y>0.5) ? this[m]._y++ : 0;
if (this[n]._height>0)this[n]._height--;
}
}
}
Con un ciclo che va da 0 a 13, assegniamo a "val" il valore della variabile, presente nel livello 1 (il filmato esterno), con il nome composto dalla stringa "s" più i, quindi, in ciclo, s0, s1, s2 eccetera. Alla variabile "n", come già visto, attribuiremo invece il valore della stringa composta da "bar" più "i", creando quindi il riferimento a bar0, bar1, bar2.
Se il valore della variabile "stato" sulla root è uguale a "play", e quindi il suono è in riproduzione, setteremo l'altezza delle barre sulla base del valore di "val", e i rispettivi colori settando il valore esadecimale derivato dallo spostamento bit a bit dei tre componenti visti prima.
Ad esempio, stabilito che pR è un trentesimo della differenza del valore del rosso tra il bianco e il violetto, lo moltiplichiamo per il valore di "val", e lo sottraiamo a sR.
Nel caso in cui la variabile "stato" sia uguale a "stop", finche l'altezza di ciascuna barra è maggiore di 0, ne diminuiamo il valore di una unità (le barre spariscono quando non c'è suono).
// ad ogni riproduzione del movieclip
onClipEvent (enterFrame) {
// con un ciclo che setta il valore di "i" da 0 a 13
for (i = 0; i < 14; i++) {
// assegniamo alla variabile "val" il valore delle variabile il cui nome
// è composto da "s" più il valore di "i", nel filmato del livello 1
val = Number(_level1["s"+i]);
// assegniamo a "n" il valore della stringa composta da "bar" più "i"
n = "bar"+i;
m = "pic"+i;
// se il valore di "stato" è "play"
if (_root.stato == "play") {
// settiamo l'altezza del movieclip con il valore di "val"
this[n]._height = val;
// settiamo il colore del movieclip sulla base della differenza
// tra il colore iniziale e quello finale
this[n].colore.setRGB((sR-pR*val)<<16|(sG-pG*val)<<8|(sB-pB*val));
if (this[m]._y > -val) {
this[m]._y = -val;
old[i] = getTimer();
}
if (getTimer()-old[i]>1000)this[m]._y -= this[m]._y/10;
// se il valore di "stato" è uguale a "stop"
} else if (_root.stato == "stop") {
this[m]._y = Math.abs(this[m]._y>0.5) ? this[m]._y++ : 0;
// finchè l'altezza del movieclip è maggiore di 0, ne diminuiamo il
// valore di una unità
if (this[n]._height>0)this[n]._height--;
}
}
}
Posizionamento e caduta dei picchi
onClipEvent (enterFrame) {
for (i = 0; i < 14; i++) {
val = Number(_level1["s"+i]);
n = "bar"+i;
m = "pic"+i;
if (_root.stato == "play") {
this[n]._height = val;
this[n].colore.setRGB((sR-pR*val)<<16|(sG-pG*val)<<8|(sB-pB*val));
if (this[m]._y > -val) {
this[m]._y = -val;
old[i] = getTimer();
}
if (getTimer()-old[i]>1000)this[m]._y -= this[m]._y/10;
} else if (_root.stato == "stop") {
this[m]._y = Math.abs(this[m]._y>0.5) ? this[m]._y++ : 0;
if (this[n]._height>0)this[n]._height--;
}
}
}
All'interno dello stesso ciclo visto per le barre, settiamo il riferimento ai picchi con la variabile "m". Se il suono è in riproduzione, se il valore della posizione sull'asse dell y del picco (non vengono ridimensionati, ma spostati) è maggiore del valore di - val, settiamo la posizione su "val", e nell'array "old", sostituiamo l'elemento corrispondente con una nuova rilevazione del tempo dall'inizio del filmato.
Bisogna capire il perchè di questa condizione, che appare alquanto strana. I picchi, all'inizio del filmato, vengono attaccati nel movieclip contenitore, e posizionati sull'asse dell x procedendo gradualmente verso destra. La posizione sull'asse delle Y, invece, non viene settata, e quindi vengono posizionati automaticamente a valore 0.
Ora, dal momento che giacciono in partenza nel centro del movieclip, considerando il sistema cartesiano di flash, in cui il quadrante positivo è quello in basso a destra, i picchi, per muoversi verso l'alto, devono assumere valori negativi.
Quindi, per decidere di spostare verso l'alto un picco, valutiamo se la posizione che dovrà assumere la _y sarà minore di quella attuale, e quindi maggiore di -val.
Il controllo più importante, avviene nella discesa. Se dall'ultima volta in cui il picco è andato verso l'alto, è passato un secondo, il picco inizia a scendere, accellerando verso il basso.
this[m]._y -= this[m]._y/10;
Questa, infatti, è la formula per il moto accellerato, considerando come posizione finale lo 0:
posizione_attuale -= (posizione_finale + posizione_attuale)/10;
mentre quella per il moto decelerato sarebbe:
posizione_attuale -= (posizione_finale - posizione_attuale)/10;
Quindi, il ciclo si svolge così:
- "val" è maggiore del valore attuale della posizione del picco, allora alzo il picco e azzero il tempo
- se è passato un secondo, e in questo secondo il picco non si è alzato, lo abbasso
Se invece interrompiamo la riproduzione del suono, e quindi "stato" diventa ugaule a "stop", allora valutiamo nuovamente la posizione del picco.
Se il valore assoluto della posizione del picco (in positivo o in negativo, appunto), è maggiore di 0.5, allora diminuiamo la posizione di una unità. Se invce è minore di 0.5, posizioniamo il picco con _y uguale a 0. Questo serve ad evitare che durante la discesa, il picco scenda sotto lo zero.
onClipEvent (enterFrame) {
for (i = 0; i < 14; i++) {
val = Number(_level1["s"+i]);
n = "bar"+i;
// settiamo il riferimento ciclico ai picchi
m = "pic"+i;
// se "stato" è uguale a "play"
if (_root.stato == "play") {
this[n]._height = val;
this[n].colore.setRGB((sR-pR*val)<<16|(sG-pG*val)<<8|(sB-pB*val));
// se la posizione attuale del picco è maggiore del valore di -val
if (this[m]._y > -val) {
// settiamo la posizione del picco come -val
this[m]._y = -val;
// resettiamo il valore dell'elemento corrispondente dell'array
// "old" con una nuova rilevazione del tempo
old[i] = getTimer();
}
// se è passato un secondo dall'ultima rilevazione
// inneschiamo la formula per l'accellerazione verso il basso
if (getTimer()-old[i]>1000)this[m]._y -= this[m]._y/10;
// altrimenti, se il valore di "stato" è uguale a "stop"
} else if (_root.stato == "stop") {
// diminuiamo il valore della posizione del picco sull'asse delle Y
// di una unità, o lo settiamo uguale a 0
this[m]._y = Math.abs(this[m]._y>0.5) ? this[m]._y++ : 0;
if (this[n]._height>0)this[n]._height--;
}
}
}
In tutti i casi abbiamo considerato il valore di "stato" uguale a "play" o a "stop". Questo perchè, quando premiamo il pulsante "pausa", i picchi e le barre rimangano fermi nella posizione, colore e dimensioni attuali.