p5.sound.js per l'audio visualizzazione con p5.js 2
Questa guida è stata scritta da Claude Opus 5.5, un modello di IA, e non è ancora stata verificata nel dettaglio. Il codice è stato provato solo in Chrome, su Windows.
Con p5.js 2 l’audio si gestisce con la nuova versione della libreria p5.sound.js, riscritta sulla base di Tone.js: la versione precedente, la 1.0, non si carica con p5.js 2. Questa pagina raccoglie quello che serve per l’audio visualizzazione: l’inclusione della libreria, lo sketch di base, le differenze rispetto alla versione precedente e il modo di ottenere le funzioni che non ci sono più, come getEnergy() e addCue().
Il codice è stato provato con p5.js 2.3.3 e p5.sound.js 0.4.1. Quasi tutti gli esempi che si trovano online riguardano la versione precedente, e gli agenti di programmazione tendono a scrivere codice per quella, con metodi che non esistono più o che ora si comportano in modo diverso, spesso senza nessun messaggio di errore: quando le due versioni non coincidono valgono le indicazioni di questa pagina, che conviene indicare anche all’agente.
Per le slide e gli sketch con p5.js 1 resta la guida precedente, P5.sound.js per l’audio visualizzazione.
1. Inclusione della libreria
Nell’index.html dello sketch la libreria si carica subito dopo p5.js:
Il file audio (MP3, oppure WAV o OGG) va nella cartella dello sketch, e lo sketch va aperto con Live Server: aperto direttamente come file, loadSound() non termina mai e la tela non compare, senza nessun messaggio di errore.
2. Lo sketch di base
Carica un brano, lo collega all’analisi e lo avvia o lo ferma a ogni clic, disegnando la forma d’onda del frammento in esecuzione.
Carica il file audio. Con p5.js 2 loadSound() si usa con await dentro async function setup(), al posto di preload().
Creano l’oggetto che analizza l’audio e gli collegano il brano: senza connect() l’analisi restituisce solo zeri. Il numero di bande dello spettro si stabilisce qui, con una potenza di 2 fra 16 e 1024 (32 se non indicato).
Attiva la ripetizione, ma non avvia il brano: lo avvia play().
Riattiva l’audio, che il browser tiene sospeso finché l’utente non compie un’azione: un brano avviato in setup() resta muto. Per questo play() sta in mousePressed(), o in keyPressed().
Restituisce i 1024 campioni del frammento in esecuzione, con valori fra -1 e 1. Il numero è fisso: un parametro fra le parentesi viene ignorato.
3. Che cosa è cambiato
| Versione precedente (p5.js 1) | p5.sound.js 0.4.1 |
|---|---|
loadSound() in preload() | await loadSound() in async function setup() |
loop() avvia il brano in ripetizione | loop() non avvia il brano: serve play() |
p5.FFT e p5.Amplitude analizzano tutto l’audio in uscita | vanno collegati al suono: brano.connect( fft ) |
amplitude.setInput( brano ) | dà un errore: si usa brano.connect( amplitude ) |
fft.analyze( 256 ), fft.waveform( 512 ) | bande nel costruttore, new p5.FFT( 256 ); i parametri dei due metodi vengono ignorati, e waveform() restituisce sempre 1024 campioni |
analyze() restituisce valori da 0 a 255 | valori da 0 a 1, su una scala lineare, quasi sempre molto piccoli, oppure in decibel (capitolo 5) |
getEnergy() | non esiste (capitolo 6) |
currentTime() e addCue() | non esistono (capitolo 7) |
getPeaks() | non esiste (capitolo 8) |
p5.PeakDetect | non esiste (capitolo 9) |
getCentroid(), linAverages(), logAverages(), getOctaveBands() | non esistono |
pan() e getPan() | non esistono: c’è la classe p5.Panner |
p5.MonoSynth, p5.PolySynth, p5.SoundLoop e altre classi | solo un avviso, e restano mute (capitolo 11) |
Altri comportamenti della versione 0.4.1, verificati, che la documentazione non segnala:
- un secondo
play()durante la riproduzione fa ricominciare il brano dall’inizio; - dopo una
pause()il brano riprende conplay(), mastop()seguito daplay()non lo fa più ripartire, anche seisPlaying()restituiscetrue: per fermare e ricominciare conviene usare solostop()eplay(); isPlaying()restatrueanche quando un brano senza ripetizione arriva alla fine, che si intercetta invece cononended();p5.AudioInmanda il suono del microfono anche agli altoparlanti (capitolo 10).
4. Classi e metodi
p5.SoundFile
Il brano caricato con loadSound() (reference della classe):
play()estop(): avviano e fermano il brano, che dopostop()riparte dall’inizio;loop(): attiva la ripetizione, eloop( false )la disattiva;pause(): ferma la riproduzione nel punto raggiunto (vedi il capitolo 3);isPlaying()eisLooping(): dicono se il brano è in riproduzione e se si ripete;jump( secondi ): sposta la riproduzione al punto indicato, mentre il brano suona;rate( velocità ): cambia insieme la velocità e l’altezza del suono (1è la velocità normale,0.5la metà);amp( volume ): volume, da 0 a 1;duration(),frames(),channels()esampleRate(): durata in secondi, numero dei campioni, numero dei canali e campioni al secondo;onended( funzione ): funzione chiamata quando il brano finisce o viene fermato;connect( analisi ): collega il brano a un’analisi, comep5.FFTop5.Amplitude, che altrimenti restituisce solo zeri; il brano continua a uscire dagli altoparlanti;disconnect(): stacca il brano da tutti i collegamenti, altoparlanti compresi.
amp(), connect() e disconnect() valgono per quasi tutte le classi della libreria, che li ereditano dalla classe p5soundNode: nella reference stanno nella sua pagina, non in quella di p5.SoundFile.
p5.FFT
L’analisi delle frequenze e dei campioni (reference della classe):
new p5.FFT( bande ): crea l’analisi, con il numero di bande dello spettro, una potenza di 2 fra 16 e 1024 (32 se non indicato); va collegata al suono conconnect();analyze(): restituisce lo spettro, un array con un valore fra 0 e 1 per ogni banda, dalle frequenze basse alle alte, oppure in decibel confft.analyzer.normalRange = false(capitolo 5);waveform(): restituisce i 1024 campioni del frammento in esecuzione, fra -1 e 1.
p5.Amplitude
L’analisi del volume (reference della classe):
new p5.Amplitude(): crea l’analisi, che va collegata al suono conconnect();getLevel(): volume del frammento in esecuzione, da 0 a 1, ma di solito molto più basso: con il brano di Liszt arriva al massimo a 0,09;smooth( valore ): smussa le variazioni, da 0 (nessuna smussatura) a quasi 1.
Funzioni
userStartAudio(): riattiva l’audio dopo un’azione dell’utente;getAudioContext(): restituisce il contesto audio del browser, da cui si leggono la frequenza di campionamento (getAudioContext().sampleRate) e l’orologio dell’audio (getAudioContext().currentTime, capitolo 7).
5. Valori in decibel
analyze() restituisce l’ampiezza di ogni banda su una scala lineare, con valori quasi sempre molto piccoli: nei primi 15 secondi del brano di Liszt il massimo è 0,016, e un diametro ricavato direttamente da questi valori resta sotto il pixel. L’orecchio percepisce invece il volume su una scala logaritmica, quella dei decibel, e la versione precedente restituiva i valori già convertiti, da 0 a 255. I decibel si ottengono con una riga, subito dopo aver creato l’analisi:
analyzer è l’analisi della libreria Tone.js usata da p5.FFT, e la sua proprietà normalRange decide se i valori vanno da 0 a 1 (true, l’impostazione di p5.sound.js) o sono in decibel (false). La proprietà non è nella reference di p5.js, e una versione successiva della libreria potrebbe cambiarla. Con i due brani delle slide di Codestesie i decibel vanno da circa -200, nelle bande quasi silenziose, a circa -25, e prima che il brano parta valgono -Infinity. La versione precedente trasformava -100 decibel in 0 e -30 decibel in 255, e la funzione seguente fa la stessa conversione:
Con true i valori sotto -100 decibel, -Infinity compreso, danno 0. Senza, diventerebbero negativi: p5.js disegna un cerchio con un diametro negativo come se fosse positivo, e una media calcolata con lerp() (capitolo 9) passerebbe a -Infinity e poi a NaN, senza più tornare un numero. Se non si vuole usare normalRange, i decibel si ricavano dai valori fra 0 e 1 con un logaritmo: 20 * Math.log10( valore ).
I valori tornano così quasi uguali a quelli della versione precedente: con il brano di Liszt, nei primi 15 secondi, il massimo dello spettro è 234 contro 231, e l’energia media dei bassi 70 contro 71. Il codice scritto per la versione precedente funziona quindi passando ogni valore per livello():
6. Energia delle frequenze
getEnergy() restituiva il livello medio delle frequenze comprese fra due valori, o di una delle cinque fasce predefinite. Con l’analisi in decibel e livello() (capitolo 5) si ricostruisce così:
Le cinque fasce della versione precedente:
| Fascia | Frequenze (Hz) | Al posto di getEnergy() |
|---|---|---|
"bass" | 20-140 | energia( spettro, 20, 140 ) |
"lowMid" | 140-400 | energia( spettro, 140, 400 ) |
"mid" | 400-2600 | energia( spettro, 400, 2600 ) |
"highMid" | 2600-5200 | energia( spettro, 2600, 5200 ) |
"treble" | 5200-14000 | energia( spettro, 5200, 14000 ) |
Lo spettro si legge una volta sola per fotogramma, e lo stesso array si passa a tutte le chiamate di energia(). Per le frequenze basse servono molte bande: con new p5.FFT( 1024 ) ognuna copre poco più di 20 Hz, e i bassi ne comprendono una mezza dozzina; con 32 bande, il valore predefinito, la prima va da 0 a circa 700 Hz e comprende da sola bassi e medio-bassi.
7. Posizione e cue
La libreria non dice in che punto del brano è arrivata la riproduzione, e non ha un equivalente di addCue(), che chiamava una funzione in un momento stabilito del brano: la mancanza è segnalata anche da una richiesta aperta nel repository della libreria. La posizione si ricava dall’orologio dell’audio, getAudioContext().currentTime, che conta i secondi con precisione: basta memorizzare l’istante d’avvio, tenendo conto che il suono parte circa un decimo di secondo dopo la chiamata a play(). Misurata con un brano di prova, la posizione così calcolata coincide con il suono entro un centesimo di secondo.
I cue si ottengono controllando a ogni fotogramma quali momenti la posizione ha superato dall’ultimo controllo, compreso il passaggio dalla fine all’inizio di un brano in ripetizione. Nell’esempio lo sfondo cambia a 3, 6 e 9 secondi dall’inizio, a ogni giro del brano.
Memorizza l’istante in cui il suono comincia davvero, un decimo di secondo dopo play().
Calcola i secondi passati dall’inizio del suono, che nel primo decimo di secondo valgono 0. Se il brano si ripete, % li riporta a 0 a ogni giro.
Come addCue(): a 3 secondi dall’inizio del brano chiama cambiaSfondo( 220 ).
Chiamata a ogni fotogramma durante la riproduzione, esegue i cue con un momento compreso fra l’ultimo controllo e la posizione attuale.
Il calcolo vale per la riproduzione continua a velocità normale: con rate() o pause() la posizione non corrisponde più al suono. Dopo jump() basta riallineare l’istante d’avvio:
8. Forma d’onda del brano
getPeaks() restituiva i picchi del volume lungo tutto il brano, per disegnarne la forma d’onda completa anche prima della riproduzione. I campioni si leggono dal buffer del brano, che la libreria tiene nella proprietà node: non è documentata, quindi potrebbe cambiare nelle versioni successive.
I valori vanno da 0 a 1. Con let forma = picchi( brano, width ); in setup(), dopo loadSound(), si ottiene un valore per ogni colonna della tela; per un brano di due minuti il calcolo richiede pochi millesimi di secondo.
9. Picchi del volume
p5.PeakDetect segnalava i colpi, per esempio quelli della batteria, quando l’energia di una fascia di frequenze superava una soglia. Un risultato simile si ottiene confrontando l’energia con la sua media recente, che si adatta al volume del brano:
Con questi valori, nei primi 20 secondi dei due brani usati nelle slide di Codestesie i picchi dei bassi sono circa due al secondo; uno scarto più alto li rende più rari. Per esempio, in draw():
10. Microfono
p5.AudioIn legge il suono del microfono dopo il permesso dell’utente, che il browser chiede solo alle pagine servite da Live Server o da un sito in https. A differenza della versione precedente manda il suono anche agli altoparlanti, con il rischio di un fischio (effetto Larsen): va staccato con disconnect() prima di collegarlo all’analisi.
11. Classi mancanti e Tone.js
Le classi tolte dalla libreria (p5.MonoSynth, p5.PolySynth, p5.SoundLoop, p5.Compressor, p5.EQ e altre) non danno errore: mostrano solo un avviso nella console, e il codice che le usa resta muto. p5.Filter è diventata p5.Biquad, con le varianti p5.LowPass, p5.HighPass e p5.BandPass. Per il resto si usano le classi di Tone.js, la libreria su cui è costruita p5.sound.js, caricata dopo di essa nella stessa versione che usa internamente:
Le due librerie devono usare lo stesso contesto audio. Per esempio, p5.Amplitude misura il volume dei due canali insieme, mentre Tone.Meter può misurarli separatamente:
brano.connect( canali ) dà invece un errore: il collegamento da p5.sound.js a Tone.js passa per Tone.connect() e per la proprietà output, che non è documentata. Nel senso opposto vale setInput(), per esempio fft.setInput( oscillatore ) per analizzare un suono generato direttamente con Tone.js.
12. Riferimenti
- reference di p5.sound.js, con un esempio per ogni metodo;
- esempi del repository di p5.sound.js: alcuni usano
setInput()con un brano caricato, che nella versione 0.4.1 dà errore; - differenze fra le due versioni, nella guida al passaggio a p5.js 2 (in inglese);
- Tone.js, con la documentazione e gli esempi.