Riproduttori
Se vogliamo un semplice playback di una o più registrazioni memorizzate in un sound file senza alcun tipo di elaborazione abbiamo generalmente a disposizione due possibilità:
Leggere i dati contenuti nel file accedendo continuamente al supporto sul quale si trovano (Hard disk o altro).
Caricare il file in un buffer (RAM o memoria temporanea) per poi leggerne il contenuto senza accedere al supporto.
In entrambi i casi dobbiamo richiamare gli indici dei campioni memorizzati uno alla volta a una determinata rata di campionamento che deve corrispondere sia con quella utilizzata in fase di registrazione sia con quella utilizzata dal software che stiamo utilizzando per la rilettura.
Lettura da supporto
Questa tecnica si utilizza principalmente quando abbiamo la necessità di effettuare una semplice riproduzione di suond files molto lunghi senza alcun tipo di elaborazione. L'operazione in realtà avviene attraverso la lettura consecutiva di porzioni del sound file caricate dinamicamente in un Buffer di lunghezza ridotta che deve corrispondere a un multiplo di 2 * block size del sistema audio impiegato.
Allochiamo un Buffer sul Server e carichiamo la prima porzione di Sound file invocando su di esso il metodo .cueSoundFile():
( s.boot; s.scope; s.meter; s.plotTree; ) ( b = Buffer.cueSoundFile(s, // Server "bach.wav".resolveRelative, // Path del Sound File da caricare 0, // Il primo frame da leggere 2, // Numero di canali da caricare 32768 // buffersize temporaneo (multiplo di 2*block size) ); )
Leggiamo il Buffer con la UGen DiskIn.ar() che, al termine della lettura ne sostituisce dinamicamente il contenuto caricando la porzione di Sound file successiva. Abbiamo anche la possibilità di leggere l'intero Sound file in loop specificandolo come terzo argomento.
( SynthDef(\diskIn1, {arg buf=0, amp=1, loop=0; var sig; sig = DiskIn.ar(2, buf, loop); // n.canali, n_buffer, loop Out.ar(0,sig * amp.lag(0.2)) }).add; {a = Synth(\diskIn1, [\buf, b, // Buffer temporaneo \amp, 1, \loop,1]) // 0 = no loop / 1 = loop }.defer(0.5) ) a.set(\loop,0); // unico modo per stopparlo (quando ha finito l'ultima lettura)
Aggiungiamo un fade In e un fade Out "di servizio" per correggere eventuali discontinuità nel segnale all'inizio e alla fine della riproduzione (Linen.ar()))
( p = "bach.wav".resolveRelative; // Path del file SynthDef(\diskIn2, {arg buf=0, atk=0, rel=0.01, amp=1,loop=0,gate=0; var sig,fade; sig = DiskIn.ar(2, buf, loop); fade = Linen.kr(gate,atk,amp.lag(0.2),rel,doneAction:2); // Fade Out.ar(0,sig * fade) }).add; { b = Buffer.cueSoundFile(s,p,0,2); // nel caso dovessimo stoppare la riproduzione ricarica // l'inizio del Sound File nel Buffer temporaneo // (altrimenti ripartirebbe da dove l'abbiamo stoppata // diventando fuori sincro (sfasato) con l'inviluppo) a = Synth(\diskIn2, [\buf, b, \atk, 0.01,// Tempo di Fade in \rel, 5, // Tempo di Fade Out \amp, 1, \loop, 1, \gate, 1 // Primo e ultimo inviluppo ]) }.defer(0.5) ) a.release(5); // possiamo stopparlo quando vogliamo (e distrugge l'istanza). a.set(\loop,0); // si ferma alla fine del loop ma non non effettua il fadeout e non distrugge l'istanza
Facciamo attenzione che se fermiamo la riproduzione con .release o .set(\gate,0) distruggendo di fatto l'istanza di Synth, nel Buffer temporaneo rimane memorizzato l'ultima porzione di Sound file letta. Se volessimo ricominciare dall'inizio una eventuale lettura successiva, dovremmo necessariamente ricaricare nel Buffer la porzione di Sound file iniziale.
-
Aggiungiamo un secondo inviluppo trapezoidale (Env.linen()) senza fase di sostegno nel caso ci siano discontinuità all'inizio e alla fine del soundfile. In questo caso dobbiamo recuperare automaticamente la durata in secondi del soundfile per calcolare la durata della fase di sostegno:
( p = "bach.wav".resolveRelative; d = SoundFile.openRead(p).duration; // recupera la durata del Sound File e la assegna a 'd' SynthDef(\diskIn3, {arg buf=0, dur=1, atk=0, rel=0.01,amp=1,loop=0,gate=0; var sig,fade,env; sig = DiskIn.ar(2, buf, loop); fade = Linen.kr(gate,atk,amp.lag(0.2),rel,doneAction:2); env = Env.linen(atk, dur-atk-rel, // calcola il tempo della fase di sostegno rel).kr(0, // doneAction:0 per l'eventuale loop Impulse.kr(1/dur)); // gate ad ogni inizio di lettura Out.ar(0,sig * env * fade) }).add; { b = Buffer.cueSoundFile(s,p,0,2); a = Synth(\diskIn3, [\buf, b, \dur, d, // invia la durata dell'intero Sound File \atk, 1, // Tempo di Fade in \rel, 5, // Tempo di Fade Out \amp, 1, \loop, 1, \gate, 1 // Primo e ultimo inviluppo ]) }.defer(0.5) ) a.release(0.2); a.set(\loop,0);
Lettura da Buffer
Un'alternativa alla tecnica appena illustrata consiste nel leggere i campioni all'interno di un Buffer con le funzioni basilari della UGen PlayBuf.ar(). In questo caso possiamo sia caricare il contenuto di un sound files nel Buffer che registrarlo in tempo reale recuperandone la durata dinamicamente all'interno del Synth con la UGen BufDur.kr().
( p = "bach.wav".resolveRelative; // Path del file b = Buffer.read(s,p); // Lo carica interamente nel Buffer SynthDef(\buf1, {arg buf=0,atk=0,rel=0.01,amp=1,loop=0,gate=0; var sig,dur,fade,env; sig = PlayBuf.ar(2, buf, loop:loop); // PlayBuf al posto di DiskIn dur = BufDur.kr(buf); fade = Linen.kr(gate,atk,amp.lag(0.2),rel,doneAction:2); env = Env.linen(atk,dur-atk-rel,rel).kr(0,Impulse.kr(1/dur)); Out.ar(0,sig * env * fade * amp) }).add; { a = Synth(\buf1, [\buf,b, \atk, 1, // Tempo di Fade in \rel, 5, // Tempo di Fade Out \amp, 1, \loop, 1, \gate, 1 // Primo e ultimo inviluppo ]) }.defer(0.5) ) a.release(0.2); a.set(\loop,0);
Impiegando questa tecnica a differenza della precedente possiamo creare quante istanze di player vogliamo senza particolari problemi in quanto l'accesso alla memoria temporanea del computer (nella quale sono allocati i Buffer) è molto meno complesso rispetto all'accesso alla memoria su supporto. Come regola generale è da preferire sempre questa seconda modalità tranne nel caso di Sound files particolarmente lunghi che occuperebbero troppa memoria temporanea.
Cross players
Nella programmazione di playback complessi all'interno di un brano di musica mista o di uno spettacolo teatrale, può essere pratico utilizzare due o più players che si alternano automaticamente nella riproduzione di una sequenza di Buffers (o sound files) realizzati attraverso il taglio in più parti (cues) di una registrazione di ampia durata. Rispetto all'utilizzo di un solo soundfile dall'inizio alla fine del brano, questa tecnica infatti ci permette maggiore flessibilità sia nella gestione del tempo musicale o drammaturgico che nella gestione pratica delle prove attraverso la possibilità di sovrapporre per qualche secondo il suono del player che finisce con quello che comincia. Così facendo liberiamo l'esecutore della parte strumentale o l'attore o il performer dalla preoccupazione di dover seguire uno stretto tempo cronometrico, limitiamo i danni in caso di eventuali errori umani che potrebbero accadere nel corso della performance e abbiamo la possibilità di riprendere l'esecuzione da più punti nel corso del brano durante le prove.
Ovviamente non è la tecnica più adatta a brani musicali caratterizzati da griglie ritmiche estrememente precise. Bisognerà inoltre scegliere accuratamente gli onsets nella sequenza dei diversi Buffers che in generale dovranno coincidere con necessità di sincronizzazione tra la parte registrata e la parte eseguita dagli strumenti acustici.
( Buffer.freeAll; b = 10.collect({var onset,dur; // Collezione di cues (possono essere anche onset = rand(10.0); // di diversi sound files) e non scelti dur = rrand(0.1,3); // randomicamente come in questo caso... Buffer.read(s,"bach.wav".resolveRelative, onset*s.sampleRate, // Onsets in frames onset+dur*s.sampleRate) // Durate in frames }); ) ( SynthDef(\cross, {arg buf=0, amp=0, fade=0.1, gate=0; var sig,dur,fad,env; sig = PlayBuf.ar(2, buf); dur = BufDur.kr(buf); fad = Linen.kr(gate,fade,amp.lag(0.2),fade,doneAction:2); // Crossfade env = EnvGen.kr(Env.linen(0.2,dur-0.4,0.2),gate,doneAction:2); Out.ar(0,sig * env * fad) }).add; ~cue = -1; // Valore di inizializzazione per cominciare dal cue 0 ) ( // Ad ogni valutazione procede al Buffer successivo ~cue = ~cue + 1 % b.size; // Counter per leggere i cues in sequenza con ordine crescente ~cue.postln; if(s.numSynths > 0, {a.set(\gate,0)}); // PRIMA Fade Out e distruzione a = Synth(\cross, [\fade,2,\amp,1,\buf, b[~cue],\gate,1]); // POI creazione e Fade In )
In questo codice i players si creano e autodistruggono automaticamente e a differenza dei Synth creati in precedenza abbiamo stabilito un solo tempo di fade e tolto l'opzione di loop.
Una tecnica classica consiste nell'assegnare alla barra spaziatrice il trigger del counter per fare progredire i cues da visualizzare su di una GUI. Possiamo anche navigare tra i cues con le frecce della tastiera o scrivere direttamente nel NumberBox il valore del cue successivo che sarà però triggerato solo con la barra spaziatrice.
( Buffer.freeAll; b = 10.collect({var onset,dur; // Collezione di cues onset = rand(10.0); dur = rrand(2,5); Buffer.read(s,"bach.wav".resolveRelative, onset*s.sampleRate, onset+dur*s.sampleRate) }); SynthDef(\cross, {arg buf=0, amp=0, fade=0.1, gate=0; var sig,dur,fad,env; sig = PlayBuf.ar(2, buf); dur = BufDur.kr(buf); fad = Linen.kr(gate,fade,amp.lag(0.2),fade,doneAction:2); env = EnvGen.kr(Env.linen(0.2,dur-0.4,0.2),gate,doneAction:2); Out.ar(0,sig * env * fad) }).add; ~cue = -1; // Valore di inizializzazione per cominciare dal cue 0 w = Window.new("Num", Rect.new(0,0,300,300)); h = NumberBox.new(w, Rect(5,5,290,290)).align_(\center).font_(Font("Helvetica", 200);); w.front; w.alwaysOnTop_(true); w.onClose_({w.free;h.free}); w.view.keyDownAction_({arg ...args; switch(args[3], 32, {~cue = ~cue + 1 % b.size; // barra spazio --> incrementa h.value = ~cue; // visualizza if(s.numSynths > 0, {a.set(\gate,0)}); // Fade out a = Synth(\cross, [\fade,2,\amp,1,\buf, b[~cue],\gate,1])}, 105, {~cue = -1; // 'i' --> reset a 0 h.value = 0; if(s.numSynths > 0, {a.set(\gate,0)}); }) }); h.action_({arg val;~cue = val.value-1}); // Recupera i valori dalle azioni sulla GUI )