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.

    let brano;      // il file audio
let fft;        // analisi delle frequenze e dei campioni

async function setup() {
  createCanvas( 670, 200 );
  textSize( 16 );
  brano = await loadSound( 'liszt.mp3' );
  fft = new p5.FFT( 256 );   // bande: potenza di 2 fra 16 e 1024
  brano.connect( fft );      // senza connect() l'analisi resta a zero
  brano.loop();              // ripete, ma non avvia il brano
}

function draw() {
  background( 255 );
  if (!brano.isPlaying()) {
    text( 'clic per avviare il brano', 20, 30 );
  }
  let campioni = fft.waveform();   // 1024 valori fra -1 e 1
  noFill();
  beginShape();
  for (let i = 0; i < campioni.length; i++) {
    let x = map( i, 0, campioni.length, 0, width );
    let y = map( campioni[i], -1, 1, height, 0 );
    vertex( x, y );
  }
  endShape();
}

function mousePressed() {
  userStartAudio();          // sblocca l'audio, fermo fino a un clic
  if (brano.isPlaying()) {
    brano.stop();
  } else {
    brano.play();
  }
}
  

  

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 ripetizioneloop() non avvia il brano: serve play()
p5.FFT e p5.Amplitude analizzano tutto l’audio in uscitavanno 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 255valori 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.PeakDetectnon 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 classisolo un avviso, e restano mute (capitolo 11)

Altri comportamenti della versione 0.4.1, verificati, che la documentazione non segnala:

4. Classi e metodi

p5.SoundFile

Il brano caricato con loadSound() (reference della classe):

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):

p5.Amplitude

L’analisi del volume (reference della classe):

Funzioni

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:

FasciaFrequenze (Hz)Al posto di getEnergy()
"bass"20-140energia( spettro, 20, 140 )
"lowMid"140-400energia( spettro, 140, 400 )
"mid"400-2600energia( spettro, 400, 2600 )
"highMid"2600-5200energia( spettro, 2600, 5200 )
"treble"5200-14000energia( 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.

    let brano;                  // il file audio
let inizio;                 // istante d'avvio, sull'orologio dell'audio
let ultimaPosizione = 0;    // posizione all'ultimo controllo dei cue
let cue = [];               // momenti del brano a cui succede qualcosa
let grigio = 255;           // colore dello sfondo, cambiato dai cue

async function setup() {
  createCanvas( 670, 200 );
  textSize( 16 );
  brano = await loadSound( 'liszt.mp3' );
  brano.loop();
  aggiungiCue( 3, cambiaSfondo, 220 );
  aggiungiCue( 6, cambiaSfondo, 170 );
  aggiungiCue( 9, cambiaSfondo, 255 );
}

function draw() {
  background( grigio );
  if (brano.isPlaying()) {
    controllaCue();
    text( posizione().toFixed( 1 ) + ' secondi', 20, 30 );
  } else {
    text( 'clic per avviare il brano', 20, 30 );
  }
}

function cambiaSfondo( valore ) {
  grigio = valore;
}

function mousePressed() {
  userStartAudio();
  if (brano.isPlaying()) {
    brano.stop();
  } else {
    avvia();
  }
}

function avvia() {
  brano.play();
  inizio = getAudioContext().currentTime + 0.1;  // 0.1 s di ritardo
  ultimaPosizione = 0;
}

function posizione() {
  let trascorso = max( 0, getAudioContext().currentTime - inizio );
  if (brano.isLooping()) {
    return trascorso % brano.duration();   // a ogni giro riparte da 0
  }
  return min( trascorso, brano.duration() );
}

function aggiungiCue( tempo, azione, valore ) {
  cue.push( { tempo: tempo, azione: azione, valore: valore } );
}

function controllaCue() {
  let ora = posizione();
  for (let c of cue) {
    let superato;
    if (ora >= ultimaPosizione) {
      superato = c.tempo > ultimaPosizione && c.tempo <= ora;
    } else {   // il brano è ricominciato dall'inizio
      superato = c.tempo > ultimaPosizione || c.tempo <= ora;
    }
    if (superato) {
      c.azione( c.valore );
    }
  }
  ultimaPosizione = ora;
}
  

  

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