Trasformare lo spazio

Noi abbiamo chiamato la nostra musica concreta, poiché essa è costituita da elementi preesistenti, presi in prestito da un qualsiasi materiale sonoro, sia rumore o musica tradizionale. Questi elementi sono poi composti in modo sperimentale mediante una costruzione diretta che tende a realizzare una volontà di composizione senza l’aiuto, divenuto impossibile, di una notazione musicale tradizionale   P. Schaeffer

Memorie

Se vogliamo eseguire un playback o realizzare un qualche tipo di elaborazione di suoni fissati su di un audio file (o sound file) abbiamo generalmente a disposizione due possibilità:

  1. Leggere i dati contenuti nel file accedendo continuamente al supporto sul quale si trovano (Hard disk o altro).

  2. Caricare il file in un buffer (RAM o memoria temporanea) per poi leggerne il contenuto senza accedere al supporto.

N.B. In realtà anche la prima necessita di un buffer interno al codice operativo che effettua l'operazione, ma al momento possiamo ignorare la cosa.

Queste due differenti tecniche avevano ragione di esistere fino a qualche anno fa in quanto le memorie temporanee dei computer non permettevano di caricare files audio di grandi dimensioni nella RAM, ma oggigiorno con l'evolversi delle possibilità offerte dalla tecnologia per la maggior parte delle situazioni è preferibile adottare sempre la seconda. Questo tipo di operazione corrisponde idealmente a quello che facciamo quando assegnamo un qualche tipo di dato a una variabile ma, se nel caso delle variabili l'allocazione di memoria temporanea che riserviamo è nell'Interprete, nel caso dei buffers è nel Server.

Arrays, Buffers e Wavetables

I Buffers sono come degli Arrays (o List) ottimizzati per memorizzare temporaneamente non int, float, UGens, string o altri tipi di data ma campioni audio (samples) e come già accennato non vivono nell'Interprete ma nel Server. I Buffers possono essere utilizzati anche come Wavetables ma quali sono le differenze principali tra i due termini?

Usualmente quella appena descritta è solo una differenza terminologica che descrive l'utilizzo che facciamo dei dati contenuti in un Buffer, mentre nella programmazione dei software più comuni esiste solo un oggetto chiamato Buffer che possiamo utilizzare all'occorrenza in entrambi i casi.

Possiamo scrivere dati all'interno di un Buffer in tre modi:

Osserviamo la prima modalità nel dettaglio.

Sound files e buffers

Path

Nel momento in cui decidiamo di caricare un audio file in un Buffer dobbiamo dire a SuperCollider dove andare a cercarlo fornendogli l'indirizzo e il percorso (path) da compiere per trovarlo sotto forma di stringa. Per ottenere facilmente questa informazione possiamo selezionare e trascinare (click and drop) con il mouse il file desiderato nello spazio che delimita l'Interprete ottenendo automaticamente l'intero percorso che si chiama path assoluto:

"/Users/andreavigani/Desktop/21_giugno/samples/archi.wav"

Questa operazione è valida per tutti i tipi di file, non solo quelli audio e come facilmente intuibile separa le cartelle e sottocartelle con il simbolo '/' (slash) seguendo una direzione d'inclusione che va da sinistra a destra.

Specificando un percorso in queso modo (path assoluto) possiamo però incorrere in una problematica: se dovessimo spostare il file o la cartella all'interno del computer oppure eseguire il codice su di un altro computer dovremmo ogni volta sostituire il path precedente con quello nuovo. Fortunatamente esiste un metodo che ci permette di specificare un path relativo, ovvero fa aggiungere autmaticamente a SuperCollider il percorso prima della cartella o del file che specifichiamo fino al punto in cui si trova il file su quale lo richiediamo. In questo caso l'unica limitazione consiste nel fatto che il file deve essere stato salvato sull'Hard disk:

"samples".resolveRelative;

E' buona pratica posizionare gli audio files che vogliamo utilizzare in uno script di SuperCollider all'interno di una cartella che è essa stessa all'interno di un'altra cartella contenente anche il file con le istruzioni necessarie a caricarli in un buffer. In questo modo scrivendo:

"samples/archi.wav".resolveRelative; // "sample" è il nome della cartella contenente gli audio files

non dovremo ogni volta ridefinire il path assoluto anche nel caso di spostamento della cartella principale su di un altro computer.

Caricare un sound file in un Buffer

Possiamo caricare un solo audio files in un singolo buffer invocando il metodo .read sulla Classe Buffer. Il file può essere monofonico, stereofonico o multicanale in quanto il buffer si adatta automaticamente al suo numero di canali:

(
b = Buffer.read(s,                                   // Server
                "samples/archi.wav".resolveRelative, // Path del file da caricare
                action:{("File "++"samples/archi.wav".basename++" loaded in Buffer "
                         ++b.bufnum).postln});       // Un azione che compie quando il file è stato caricato
)                                                     

Eseguendo il codice precedente SuperCollider crea un nuovo buffer nel Server, gli assegna automaticamente un numero partendo da 0 (indicizzazione) e carica i dati dell'audio file al suo interno. Se ad esempio eseguiamo due o più volte il codice precedente, a ogni esecuzione sarà creato un nuovo buffer contenente sempre lo stesso audio file ma con numero d'indice crescente anche se assegnato sempre alla stessa variabile (che sarà di volta in volta sovrascritta). Per sapere quale numero è assegnato ad un buffer possiamo invocare sulla singola istanza il metodo .bufnum:

b.bufnum;

Nel blocco di codice di esempio abbiamo anche programmato un monitor visivo che stampa nella Post window questa informazione unita al nome dell'audio file caricato. Per farlo abbiamo recuperato il nome dal path invocando su di esso il metodo .basename che riporta solo questa informazione tagliando il resto del path e concatenato tra loro valori e stringhe attraverso la sintassi ++:

"File "++"samples/archi.wav".basename++" loaded in Buffer "++b.bufnum;

Possiamo anche caricare un sound file in un Buffer attraverso una finestra di dialogo invocando il metodo .loadDialog:

b = Buffer.loadDialog(s, action:{("File loaded in Buffer "++b.bufnum).postln})

Quando carichiamo un sound file in un Buffer potremmo aver bisogno di alcune informazioni sia sul sound file originale sia sul buffer sul quale lo caricheremo. Queste potranno tornarci utili quando andremo a rileggerne il contenuto.

SoundFile e Buffer info

Il modo più semplice per ottenere informazioni su di un sound file consiste nel generare una finestra grafica nella quale queste sono visualizzate automaticamente:

(
f = SoundFile.new;
f.openRead(Platform.resourceDir +/+ "sounds/a11wlk01.wav");
f.inspect;
f.close;
)

image not found

Eseguendo il codice precedente abbiamo:

In alternativa possiamo recuperare le singole informazioni invocando su un'istanza di SoundFlie diversi metodi ma prima dobbiamo riaprire il file:

f = SoundFile.new;
f.openRead(Platform.resourceDir +/+ "sounds/a11wlk01.wav");

Ricordiamo che il numero di samples (o frames) di un sound file non ci fornisce alcuna indicazione riguardo la sua durata se non lo mettiamo in relazione con la rata di campionamento: un suono registrato e riletto con una rata di campionamento di 44100 Hz dura un secondo, ma se registrato a 44100 e riletto a 88200 Hz dura mezzo secondo (lo stesso numero di campioni ha due durate differenti).

Per ottenere informazioni su di un Buffer possiamo invocare gli stessi metodi appena incontrati con l'aggiunta di altri riassunti nel codice seguente:

b = Buffer.read(s,Platform.resourceDir +/+ "sounds/a11wlk01.wav"); // carica un sound file

b.bufnum;        // numero (indice) del buffer
b.plot;          // visualizza il contenuto in un Plotter
b.play;          // 'suona' il buffer (solo per monitorarne il contenuto)
b.path;          // riporta il path assoluto del file caricato

b.numFrames;     // riporta il numero di Frames
b.numChannels;   // riporta il numero di canali
b.sampleRate;    // riporta la rata di campionamento (che portebbe essere diversa da quella del file caricato)
b.duration;      // riporta la durata in secondi

b.query;         // riporta le informazioni precedenti attraverso un solo comando

Pulire e liberare

Una volta che abbiamo creato un Buffer (allocato la memoria) possiamo cancellarne il contenuto invocando su di esso il metodo .zero:

b.zero;
b.plot;

In questo caso il Buffer assegnato alla variabile b continua a esistere nel Server e occupa una memoria di tanti frames quanti quelli del file che conteneva. Se invece vogliamo eliminare il Buffer dal Server e liberare la variabile alla quale lo avevamo associato possiamo invocare il metodo .free:

b.free;
b.plot; // WARNING: Buffer not allocated, can't plot data

Se invece vogliamo eliminare tutti i Buffers allocati su un Server possiamo invocare il metodo .freeAll:

Buffer.freeAll;

Questo comando ritorna utile in quanto c'è un limite nel numero di buffers che possono essere creati in un Server:

s.options.numBuffers;        // Per ottenerne il numero
s.options.numBuffers = 2000; // Si può cambiare ma bisogna fare il reboot del server

Caricare un solo canale

Se vogliamo caricare solo un numero parziale di canali di un file multicanale lo possiamo fare invocando il metodo .readChannel. Se non specifichiamo argomenti carica tutti i canali:

b = Buffer.readChannel(s, "samples/celesta.wav".resolveRelative); // il file è stereo
b.plot;
b.play;

Se invece specifichiamo come argomento (channels:) quale canale o quali canali vogliamo caricare sotto forma di Array carica solo quelli:

b = Buffer.readChannel(s, "samples/celesta.wav".resolveRelative,channels:[0]); // solo canale sinistro
b.plot;
b.play;

Caricare una porzione di sound file

Se vogliamo caricare solamente una porzione di file possiamo specificare inizio e fine in frames come secondo e terzo argomento (in questo caso il numero di frames corrisponde ai sample anche nel caso di files multicanale):

Buffer.freeAll;

(
b = Buffer.read(s,
	            "samples/celesta.wav".resolveRelative,
	              3456,   // primo frame (onset)
	             12345)   // numero di frames da leggere (durata)
)

b.numFrames;
b.plot;
b.play;

// Se vogliamo specificare la durata in secondi:

(
b = Buffer.read(s, "samples/celesta.wav".resolveRelative,
                0.0,                // inizio
                s.sampleRate * 0.1) // durata (sample_rate * secondi)
)

b.numFrames;
b.plot;
b.play;

// Se vogliamo specificare inizio e durata in secondi:

(
b = Buffer.read(s, "samples/archi.wav".resolveRelative,
                s.sampleRate * 0.1,    // 4410
                s.sampleRate * 0.3)    // 13230
)

b.numFrames;
b.plot;
b.play;

// Se vogliamo specificare inizio e fine in secondi:

(
var inizio = 0.3, fine = 0.6;
b = Buffer.read(s, "samples/archi.wav".resolveRelative,
                s.sampleRate * inizio,
                s.sampleRate * abs(fine-inizio))  // calcolo la durata
)

b.numFrames;
b.plot;
b.play;

Caricare una cartella di sound files

Abbiamo suggerito in precedenza di posizionare tutti gli audio files in una cartella. Vediamo allora come caricarli tutti automaticamente in diversi buffers (uno per ogni file).

  1. Definiamo il path assoluto della cartella e selezioniamo tutti i files con estensione .wav ivi contenuti invocando il metodo .pathMatch. Se volessimo solo files con altre estensioni basterà cambiare il suffisso .wav con l'estensione desiderata:

    ~paths = ("samples".resolveRelative ++ "/*.wav").pathMatch;

    Eseguendo il codice precedente, assegnamo alla variabile globale ~paths un'Array di path assoluti dei file con estensione .wav contenuti nella cartella specificata.

  2. Generiamo un'Array di buffers con il metodo ..collect invocato sull'Array ~paths:

    ~bufs  = ~paths.collect{arg i; Buffer.read(s, i)};
    
  3. Richiamiamo i singoli buffer invocando sull'Array il metodo .at come di consueto:

    ~bufs.at(0).play;
    ~bufs.at(1).play;
    ~bufs.at(2).play;
    ~bufs.at(3).play;
    ~bufs.at(4).play;
    ~bufs.at(rand(~files.size-1)).play;
    
    Buffer.freeAll;
    

GUI

Possiamo creare un Pop-up menu con i nomi dei file contenuti nei buffers in modo da poter interagire con una GUI.

  1. Generiamo un Array contenente solo i nomi dei sound files ricavandoli automaticamente dall'Array di path generato in precedenza:

    ~nomi = ~paths.collect{arg i; PathName(i).fileName};
  2. Verifichiamo che i sound files corrispondono agli indici dei buffers:

    [~nomi, ~bufs].flop.do{arg i; i.postln}; "" // "" Per stampare tutto nella post window

    Il metodo .flop inverte righe e colonne di un Array bidimensionale. O meglio prende l'indice 0 di due Array diversi e ne restituisce uno nuovo con i due items collocati a quell'indice, poi passa all'indice 1, e così via.

  3. Generiamo un Popup menu popolando automaticamente i nomi con l'Array nomi:
    (
    w = Window.new("popup", Rect(800, 200, 200, 30))
              .alwaysOnTop_(true)
              .front;
    
    p = PopUpMenu.new(w, Rect(2, 2, 196, 20));
    p.items_(~nomi); 
    
    // Quando interagiamo con il pop-up, restituisce l'indice del menu, partendo da 0.
    
    p.action_({arg view; view.value.postln});
    )
    
  4. Per selezionare ed eseguire i diversi files interagendo con la GUI possiamo usare il metodo p.value per ottenere lo stato del PopUpMenu e mapparlo come indice di selezione dei buffers contenuti nell'Array ~bufs:

    • p.value == 0 --> ~bufs.at(0)
    • p.value == 1 --> ~bufs.at(1)
    • p.value == n --> ~bufs.at(n)

    Separando di fatto l'azione di selezione del file da quella di esecuzione:

    ~bufs.at(p.value).play;

    Se vogliamo invece triggerare il playback del buffer contestualmente alla sua scelta sul PopUpMenu possiamo utilizzare il valore riportato da view.value:

    (
    w = Window.new("popup", Rect(800, 200, 200, 40)).alwaysOnTop_(true).front;
    p = PopUpMenu.new(w, Rect(2, 2, 196, 20))
                 .items_(~nomi)                                   
                 .action_({arg view; ~bufs.at(view.value).play}); 
    )
    

In entrambi i casi tutti i buffers saranno eseguiti fino alla fine.

Visualizzare sound files

Come per altri oggetti di SuperCollider (Array, Env, etc.) abbiamo a disposizione due modi per visualizzare un sound file:

Plot

Nel primo caso possiamo creare un Plotter per ogni Buffer che contiene un sound file invocando su di esso il metodo .plot:

(
b = Buffer.read(s, "samples/celesta.wav".resolveRelative);

{b.plot(name: "buffer "++b.bufnum++" - "++"samples/archi.wav".basename, 
	    bounds:540@200,   
	    minval: -1,       
	    maxval: 1)}.defer(0.2)
)

image not found

Realizzando un Plotter possiamo solo visualizzare il contenuto di un Buffer ma non possiamo interagire in alcun modo con esso.

SoundFileView

Nel secondo caso invece possiamo anche interagire con l'interfaccia grafica ed eventualmente ricavare alcuni parametri che possono tornarci utili. Quando utilizziamo questa Classe in realtà non visualizziamo il contenuto di un Buffer ma un Soundfile che possiamo eventualmente caricare in un buffer:

f = SoundFile.openRead("samples/celesta.wav".resolveRelative;);  // Crea un'istanza di Soundfile

w = Window.new("SoundFileView", 550@210).front; // Crea una finestra vuota e la visualizza
a = SoundFileView.new(w, Rect(5,5, 540, 200));  // Crea una visualizzazione vuota

a.soundfile = f;                                // Assegna il Sounfile alla visualizzazione
a.read(0, f.numFrames);                         // Legge il file dal frame 0 all'ultimo,
                                                // N.B. Per file molto lunghi è meglio usare: a.readWithTask;
a.refresh;                                      // Ripulisce e visualizza

image not found

Vediamo ora gli elementi grafici presenti in questa finestra:

Riassumendo una semplice visualizzazione di un solo Soundfile caricato in un buffer, con un'unica porzione da selezionare potrebbe essere:

(
var path,w,vsf,sf;

    Buffer.freeAll;
    path = Platform.resourceDir +/+ "sounds/a11wlk01.wav";
    w = Window.new("SoundFileView", 550@210).front;
    vsf = SoundFileView.new(w, Rect(5,5, 540, 200));
    sf = SoundFile.new;
    sf.openRead(path);
    vsf.soundfile_(sf)
       .readWithTask(0, sf.numFrames)  // a.read;
       .refresh
       .timeCursorOn_(true)
       .timeCursorColor_(Color.white)
       .setSelectionColor(0, Color.blue)
       .gridColor_(Color.gray)
       .gridOn_(true)
       .gridResolution_(0.01)
       .mouseUpAction_( {var sel, cpos, start, size, end;
	                     sel   = vsf.selections[vsf.currentSelection];
	                     cpos  = vsf.timeCursorPosition;
	                     start = sel[0];
	                     size  = sel[1];
	                     if(cpos==start,
		                      {end = start+size; start = start},
		                      {end = start;      start = cpos});
	                  [start, end, size].postln});

~buffer = Buffer.read(s,path);   // carica l'audio file in un buffer
)

~buffer.play;

image not found

Playback

L'operazione successiva alla scrittura dei valori di ampiezza in un Buffer consiste nel rileggerli dinamicamente nel tempo per ricostruire (riprodurre) un segnale audio così come è sempre avvenuto all'interno di tutte le catene elettroacustiche che impiegano un qualche tipo di memorizzazione delle informazioni audio su supporto (vinile, nastro magnetico, CD, etc.). Nell'ambito digitale semplificando, possiamo ottenere un flusso temporale dei valori y precedentemente memorizzati in un Buffer richiamandone gli indici (valori x) uno alla volta a una determinata rata di campionamento che, può corrispondere o meno con quella eventualmente utilizzata nella registrazione del Buffer (o del Sound file) e/o con quella utilizzata dal software che stiamo utilizzando. Le tecniche da adottare per compiere questa operazione devono essere scelte in base alle diverse esigenze musicali che possiamo incontrare e che sono riassumibili in:

Osserviamole nel dettaglio.

Riprodurre

Lettura di un Sound file da supporto

Come accennato all'inizio di questo paragrafo questa tecnica consiste nel leggere i dati contenuti in Sound files particolarmente lunghi accedendo continuamente al supporto sul quale sono memorizzati (HD o SD). Per la sua particolarità si presta solamente alla semplice riproduzione (eventualmente anche in loop) ed esclude qualsiasi tipo di elaborazione. L'operazione viene ottimizzata attraverso la lettura consecutiva di porzioni del Sound file caricate dinamicamente in un Buffer di lunghezza ridotta. In genere la sua lunghezza deve corrispondere a un multiplo di 2 * block size del sistema audio impiegato. Vediamo ora come realizzarla in SuperCollider.

  1. Allochiamo un Buffer sul Server e carichiamo la prima porzione di Sound file invocando su di esso il metodo .cueSoundFile():

    p = "samples/bach.wav".resolveRelative; // Path del file
    
    b = Buffer.cueSoundFile(s,              // Server
                            p,              // 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)
                            );
    
  2. 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 bufnum, out=0, amp=1, loop=0;
                        var sig;
                            sig  = DiskIn.ar(2, bufnum, loop); // n.canali, n_buffer, loop
                        Out.ar(out,sig * amp)
             }).add;
    
    {a = Synth(\diskIn1, [\bufnum,b,  // Buffer temporaneo
                          \out,   0,
                          \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)
    
  3. Aggiungiamo un fade In e un fade Out nel caso ci siano discontinuità nel segnale all'inizio e alla fine della riproduzione (attenzione non del Sound file ma della rilettura sia singola che in loop), programmando anche l'autodistruzione dell'istanza di Synth al termine del fade Out. Per farlo possiamo applicare segnale in uscita un inviluppo trapezoidale con fase di sostegno (Linen.ar())) e utilizzare l'argomento gate come trigger (1 = fade In, 0 = fade Out).

    (
    p = "samples/bach.wav".resolveRelative;                    // Path del file    
    
    SynthDef(\diskIn2, {arg bufnum, out=0, atk=0, rel=0.01, amp=1,loop=0,gate=0;
                        var sig,fade;
                            sig  = DiskIn.ar(2, bufnum, loop);
                            fade = Linen.kr(gate,atk,1,rel,2); // Inviluppo trapezoidale con 'doneAction:2'
                        Out.ar(out,sig * fade * amp)
            }).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, [\bufnum,b,
                         \out,   0,
                         \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. Per ricominciare dall'inizio una eventuale lettura successiva, dobbiamo necessariamente ricaricare nel Buffer la porzione di Sound file iniziale.

  4. Aggiungiamo un secondo inviluppo trapezoidale (Env.linen()) senza fase di sostegno nel caso ci siano discontinuità all'inizio e alla fine del Sound file. A differrenza del precedente in questo caso dobbiamo recuperare automaticamente la durata in secondi del sound File per calcolare la durata della fase di sostegno:

    (
    p = "samples/bach.wav".resolveRelative;
    d = SoundFile.openRead(p).duration;        // recupera la durata del Sound File e la assegna a 'd'
    
    SynthDef(\diskIn3, {arg bufnum, out=0, dur=1, atk=0, rel=0.01,amp=1,loop=0,gate=0;
                        var sig,fade,env;
                            sig  = DiskIn.ar(2, bufnum, loop);
                            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/d)); // gate ad ogni inizio di lettura
                            fade = Linen.kr(gate,atk,1,rel,2);
                       Out.ar(out,sig * env * fade * amp)
                       }).add;
    
    {
    b = Buffer.cueSoundFile(s,p,0,2); 
    a = Synth(\diskIn3, [\bufnum,b,
                         \out,   0,
                         \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);  
    

Esercizio 10.1

Realizzare con SuperCollider un lettore di frammenti di un singolo Sound file con accesso continuo al supporto e una GUI con le seguenti caratteristiche:

Lettura di un Sound file caricato in un Buffer

Un'alternativa alla tecnica appena illustrata consiste nel caricare l'intero Sound file in un Buffer e leggerlo con le funzioni basilari della UGen PlayBuf.ar():

(
p = "samples/bach.wav".resolveRelative; // Path del file    
d = SoundFile.openRead(p).duration;     // recupera la durata del Sound File e la assegna a 'd'
b = Buffer.read(s,p);                   // Lo carica interamente nel Buffer

SynthDef(\diskIn3, {arg bufnum, out=0, dur=1, atk=0, rel=0.01,amp=1,loop=0,gate=0;
                    var sig,fade,env;
                        sig  = PlayBuf.ar(2, bufnum, loop:loop);           // PlayBuf al posto di DiskIn
                        env  = Env.linen(atk,dur-atk-rel,rel).kr(0,Impulse.kr(1/d));
                        fade = Linen.kr(gate,atk,1,rel,2);
                    Out.ar(out,sig * env * fade * amp)
                    }).add;

{
a = Synth(\diskIn3, [\bufnum,b,
                     \out,   0,
                     \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);   

In questo caso, a differenza del 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 dell'accesso alla memoria su supporto. Di regola è da preferire sempre questa seconda tecnica tranne nel caso di Sound files particolarmente lunghi che occuperebbero troppa memoria temporanea.

Elaborare

Wavetable lookup

Un Sound file caricato in un Buffer può essere anche utilizzato come materiale sonoro da ri-elaborare per ottenere suoni differenti. Prima di esplorare le diverse tecniche di elaborazione osserviamo però il concetto che sta alla base di molte di esse ovvero il modo in cui possiamo ottenere in uscita i valori y di un Buffer richiamandone gli indici (x) attraverso un segnale puntatore (non-interpolating wavetable lookup):

image not found

L'immagine precedente mostra come i valori in output (y) del segnale puntatore possono corrispondere ai valori degli indici (x) dei campioni nel Buffer e in questo caso se il tempo delta che intercorre tra i campioni del segnale puntatore è lo stesso di quello utilizzato per registrare i campioni del Sound file caricato nel Buffer (in altre parole hanno la stessa rata di campionamento) otterremo una fedele riproduzione dell'audio memorizzato nel Buffer esattamente come abbiamo visto nel paragrafo precedente. In SuperCollider possiamo effettuare questa operazione utilizzando la UGen BufRd.ar() che legge il contenuto di un Buffer in base ai valori di un segnale puntatore specificato come terzo argomento:

(
b = Buffer.read(s,"samples/bach.wav".resolveRelative); // Carica nel Buffer

SynthDef(\bufRd1, {arg bufnum, out=0, amp=1;
                   var sig,punta;
                       punta = Line.ar(0, BufFrames.ir(bufnum), // va da 0 al valore dell'ultimo frame
                                       BufDur.ir(bufnum),       // nella durata del Buffer
                                       doneAction:2);           // distrugge il Synth alla fine della rampa
                       sig   = BufRd.ar(2, bufnum, punta);
                   Out.ar(out,sig *  amp)
                   }).add;

{Synth(\bufRd1, [\bufnum,b,\out,0,\amp,1])}.defer(0.5)
)

Notiamo come il numero di frames e la durata in secondi del Buffer siano ottenuti dinamicamente attraverso le UGens BufFrames.ir(bufnum) e BufDur.ir(bufnum), entrambe a inizialization rate ovvero il valore viene restituito una sola volta alla creazione del Synth. Possiamo inoltre intuire che per realizzare un loop con un segnale puntatore l'onda a dente di sega o un fasore siano quelli che fanno al caso nostro:

image not found

Nell'immagine è illustrata la realizzazione di un loop dove la frequenza dell'onda a dente di sega (in Hz) deve essere il reciproco della durata in secondi del Buffer (1/dur) e la sua ampiezza riscalata sul numero di frames del Buffer:

(
b = Buffer.read(s,"samples/bach.wav".resolveRelative); // Carica nel Buffer

SynthDef(\bufRd2, {arg bufnum, out=0, amp=1;
                   var sig,punta;
                       punta = LFSaw.ar(                              // Onda a dente di sega
                                        BufDur.ir(bufnum).reciprocal) // 1/Dur in secondi = Hz
                                    .range(0, BufFrames.ir(bufnum));  // riscalato nel range
                       sig   = BufRd.ar(2, bufnum, punta);
                   Out.ar(out,sig *  amp)
                   }).add;

{Synth(\bufRd2, [\bufnum,b,\out,0,\amp,1])}.defer(0.5)
)

Se in questo caso volessimo applicare un inviluppo al contenuto del Buffer dovremmo cambiare strategia generando un secondo Buffer di breve durata (512 o 1024 campioni dovrebbero bastare) contenente la forma dell'inviluppo desiderata da leggere in sincrono con il Buffer contenente il Sound file:

image not found

Nell'illustrazione precedente:

E la realizzazione in SuperCollider:

(
b = Buffer.read(s,"samples/bach.wav".resolveRelative); // Carica il Sound file in un Buffer
c = Env.linen(1,2,1).asSignal(512);                    // Genera un inviluppo di 512 campioni
                                                       // con valori proporzionali per attacco
                                                       // e release
d = Buffer.loadCollection(s,c);                        // lo carica in un Buffer

SynthDef(\bufRd2, {arg bufnum, bufenv,out=0, amp=1;
                   var sig,punta,env;
                       punta = LFSaw.ar(BufDur.ir(bufnum).reciprocal,1); // un unico puntatore...
                                                   // legge sia il Buffer del file (riscalato sulla sua lunghezza)
                       sig   = BufRd.ar(2, bufnum, punta.range(0, BufFrames.ir(bufnum)));
                                                   // che il Buffer dell'inviluppo (riscalato sulla sua lunghezza)
                       env   = BufRd.ar(1, bufenv, punta.range(0, BufFrames.ir(bufenv))); 
                   Out.ar(out, sig * amp * env)
                   }).add;

{Synth(\bufRd2, [\bufnum,b, // Buffer del Sound file
                 \bufenv,d, // Buffer dell'inviluppo
                 \out,0,
                 \amp,1])
}.defer(0.5)
)

Infine applichiamo i fade In e fade Out iniziale e finale:

(
b = Buffer.read(s,"samples/bach.wav".resolveRelative); 
c = Env.linen(1,2,1).asSignal(512);                   
d = Buffer.loadCollection(s,c);                        

SynthDef(\bufRd2, {arg bufnum, bufenv, out=0, amp=1, atk=2, rel=0.2, gate=0;
                   var sig,punta,env,fade;
                       punta = LFSaw.ar(BufDur.ir(bufnum).reciprocal,1); 
                       sig   = BufRd.ar(2, bufnum, punta.range(0, BufFrames.ir(bufnum)));
                       env   = BufRd.ar(1, bufenv, punta.range(0, BufFrames.ir(bufenv)));
                       fade  = fade = Linen.kr(gate,atk,1,rel,2);
                   Out.ar(out, sig * amp * env * fade)
                   }).add;

{e = Synth(\bufRd2, [\bufnum,b, // Buffer del Sound file
                     \bufenv,d, // Buffer dell'inviluppo
                     \out,0,
                     \amp,1,
                     \atk,2,
                     \rel,0.2,
                     \gate,1])
}.defer(0.5)
)

e.release(0.4);
e.set(\gate,0);

Esercizio 10.2

Realizzare con SuperCollider un lettore Sound files monofonici caricati su diversi Buffers e una GUI con le seguenti caratteristiche:

A questo punto vediamo quali sono le tecniche principali che possiamo utilizzare nella realizzazione di elaborazioni complesse del suono originale caricato nel Buffer:

Looping

Prima di affrontare alcune tecniche di looping che permettono di iterare la lettura di porzioni del Buffer in modo continuo, osserviamo più nel dettaglio i tre parametri principali che ci permettono di delimitare porzioni di Buffer:

Tutti e tre possono essere specificati sia in secondi che in frames ed a seconda della situazione si potrebbe rendere necessaria una conversione da una forma all'altra. Nei codici seguenti potremo di volta in volta ritrovare le principali commentate all'interno del codice.

  1. Sequencing. In questo primo esempio realizziamo un looper i cui parametri possono essere specificati e controllati direttamente dall'Interprete sotto forma di valori numerici. In questo caso per comodità li specificheremo in secondi per poi convertirli in frames nella SynthDef in quanto come sappiamo BufRd.ar() accetta valori in quest'ultima unità di misura. Il codice è ampiamente commentato.

    (
    b = Buffer.read(s,"samples/bach.wav".resolveRelative);
    c = Env.linen(10,80,10).asSignal(512);
    d = Buffer.loadCollection(s,c);
    
    SynthDef(\looper, {arg bufnum,bufenv,out=0,amp=1,atk=0.2,rel=0.2,gate=0,
                           str=0,end=0.1,dur=0.1;              // aggiunto inizio, fine e durata in secondi
    
                       var sig,punta,env,fade;
                           punta = LFSaw.ar(dur.reciprocal,1); // converte da secondi --> Hz
                           sig   = BufRd.ar(2, bufnum,         // riscala da secondi --> frames
                                                               // dinamicamente
                                                               // (a seconda della BufSampleRate):
                                            punta.range(str * BufSampleRate.kr(bufnum),
                                                        end * BufSampleRate.kr(bufnum))
                                            );
                           env   = BufRd.ar(1, bufenv, punta.range(0, BufFrames.ir(bufenv)));
                           fade  = Linen.kr(gate,atk,1,rel,2); // DoneAction:2
    	               Out.ar(out, sig * env * amp * fade)
                       }).add;
    
    {e = Synth(\looper,[\bufnum,b,\bufenv,d,   // Crea una prima istanza (Init) per il primo muting
                        \atk,0.02,\rel,0.02,
                        \amp,0,\gate,1])
    }.defer(0.5)
    )
    
    // ------------------- Sequencing
    
    (
    r = Routine.new({var ini,end,dur;                       // Dichiara le variabili locali solo la prima volta
    
                     inf.do({
                             ini = rrand(0,b.duration-0.5); // calcola inizio random (fino a 0.5 dalla fine)
                             dur = rrand(0.1,0.5);          // calcola dur random (max 0.5)
                             end = ini + dur;               // calcola fine
    
                             e.set(\gate,0);                // Muting (distrugge il Synth alla fine della rampa)
                             0.02.wait;                     // tempo minimo (= a fade out)
                            // rrand(0.05,4).wait;          // se volessimo pause tra le sequenze...
                             e = Synth(\looper, [\bufnum,b,\bufenv,d,
                                                 \out,0,   \amp,rand(1.0),       // Ampiezza ramdom
                                                 \atk,0.1, \rel,0.1,
                                                 \str, ini,\end, end, \dur, dur, // Invia inizio fine e durata (secondi)
                                                 \gate,1]);                      // Fade in
                             rrand(0.5,6).wait;                                  // durata della sequenza
    	                     })
                      }).reset.play
    )
    
    r.stop; e.release(3);  // Stop e Fade Out
    

    Esercizio 10.3

    Realizzare un soundscape secondo le seguenti regole:

    • Generare quattro looper che leggono quattro diversi frammenti monofonici del Sound File "bach.wav" (scelti randomicamente o in modo deterministico).
    • I quattro frammenti devono avere posizioni diverse o sul fronte stereofonico oppure in una diffusione multicanale (anche in questo caso le posizioni possono essere scelte sia in modo deterministico che non deterministico).
    • Realizzare una GUI con i seguenti elementi:
      • quattro CheckBox: accensione o spegnimento di ogni singolo looper.
      • un Button: funzione che cambia randomicamente inizio e fine di tutti e quattro i frammenti.
      • uno Slider: controllo del Master Gain.
  2. Mouse e GUI. In questo secondo esempio andiamo a programmare una GUI che visualizza il Sound file caricato nel Buffer e permette di selezionare con il mouse (click and drop) la prozione di buffer che vogliamo mettere in loop. Funziona anche con la lettura retrograda del frammento: basterà selezionare al contrario.

    (
    // ------------------- Buffers, SynthDef e Synth
    
    b = Buffer.read(s,"samples/bach.wav".resolveRelative);
    c = Env.linen(10,80,10).asSignal(512);
    d = Buffer.loadCollection(s,c);
    
    SynthDef(\looper_gui, {arg bufnum,bufenv,out=0,amp=1,atk=0.1,rel=0.1,gate=0,
                               str=0,end=100,dur=100;                              // In Frames...
                           var sig,punta,env,fade;
                               punta = LFSaw.ar((dur/BufSampleRate.kr(bufnum))     // Frames --> sec
                                                                     .reciprocal,  // sec --> Hz
    	                                        1);
                               sig   = BufRd.ar(2, bufnum, punta.range(str, end)); // start, end
                               env   = BufRd.ar(1, bufenv, punta.range(0, BufFrames.ir(bufenv)));
                               fade  = Linen.kr(gate,atk,1,rel,2);
                           Out.ar(out, sig * amp * env * fade)
                           }).add;
    
    {e = Synth(\looper_gui,[\bufnum,b,\bufenv,d,
                            \atk,0.02,\rel,0.02,
                            \amp,0,\gate,1])
    }.defer(0.2);
    
    // ------------------- GUI
    
    f = SoundFile.openRead("samples/bach.wav".resolveRelative;);        // Visualizzazione
    w = Window.new("SoundFileView", 1100@420).front.alwaysOnTop_(true);
    w.onClose_({e.release(0.1);b.free;c.free;d.free;a.free});           // Alla chiusura della window
    a = SoundFileView.new(w, Rect(5,5, 1090, 410)).soundfile_(f)
                                                 .read(0, f.numFrames)
                                                 .gridOn_(false)        // Senza griglia
                                                 .timeCursorOn_(true)   // Con il cursore
                                                 .refresh;
    
    // ------------------- Recupera valori da azioni sulla GUI
    
    a.mouseUpAction = {var sel, start, cpos, size, end;               // Al rilascio del mouse...
                           sel   = a.selections[a.currentSelection];  // Selezione in uso
                           start = sel[0];                            // Inizio
                           cpos  = a.timeCursorPosition;              // Posizione del cursore
                           size  = sel[1];                            // Fine
    
    // Per permettere l'eventuale lettura al contrario del file dobbiamo aggiungere alcune operazioni:
    
                        if(cpos==start,                       // Se posizione corrente è = 'start'
                           {end = start+size; start = start}, // Calcola 'end' altrimenti
                           {end = start;      start = cpos}); // Inverti 'start' e 'end'
                        [start, end, size].postln;            // Stampa valori in frames
    
                        Routine.new({
                                     e.set(\gate,0);          // Muting
                                     0.02.wait;
                                     e = Synth(\looper_gui,[\bufnum,b,\bufenv,d,
                                                            \str,start,\end,end,\dur,size, // In Frames...
                                                            \gate,1])
                                     }).play
                       }
    )
    

    Esercizio 10.4

    Realizzare un soundscape secondo le seguenti regole:

    • Generare quattro looper che leggono quattro diversi frammenti stereofonici del Sound File "bach.wav" (scelti randomicamente o in modo deterministico).
    • La scelta dei frammenti (posizione e durata) deve essere effettuata con il mouse sulla stessa interfaccia grafica (SoundFileView) sfruttando le possibilità delle selezioni.
    • Prevedere la possibilità di passare da una selezione ad un'altra attraverso un PopUpMenu o un Botton.
    • Ogni frammento stereofonico deve essere riprodotto da due altoparlanti specifici di un sistema ottofonico (due altoparlanti diversi per ogni Synth...).
  3. MIDI o OSC. In questo terzo esempio andiamo a modificare dinamicamente l'intonazione del frammento (pitch) modificando la velocità di lettura attraverso un knob di un controller MIDI (oppure OSC). Decidiamo di specificare un fattore di trasposizione in semitoni (0 = senza trasposizione, 1= un semitono sopra, -1= un semitono sotto, etc...) per poi convertirlo automaticamente in fattore di moltiplicazione della velocità di lettura del puntatore all'interno della SynthDef con il metodo n.midiratio. Se vogliamo utilizzare microtoni o frequenze non temperate basterà modificare i parametri del metodo .round() all'interno della Classe MidiDef (oppure OSCdef).

    MIDIIn.connectAll; // connetto tutti i devices MIDI
    
    (
    b = Buffer.read(s,"samples/bach.wav".resolveRelative);
    c = Env.linen(10,80,10).asSignal(512);
    d = Buffer.loadCollection(s,c);
    
    SynthDef(\looper_gui2, {arg bufnum,bufenv,out=0,amp=1,atk=0.1,rel=0.1,gate=0,
                                str=0,end=100,dur=100,transp=0;                        // Aggiunto transp
                            var sig,punta,env,fade;
    	                        punta = LFSaw.ar((dur/BufSampleRate.kr(bufnum)).reciprocal
                                                                  * transp.midiratio, // Velocità di lettura
                                                                                  1);
                                sig   = BufRd.ar(2, bufnum, punta.range(str, end));    
                                env   = BufRd.ar(1, bufenv, punta.range(0, BufFrames.ir(bufenv)));
                                fade  = Linen.kr(gate,atk,1,rel,2);
                            Out.ar(out, sig * amp * env * fade)
                            }).add;
    
    {e = Synth(\looper_gui2,[\bufnum,b,\bufenv,d,\atk,0.02,\rel,0.02,\amp,0,\gate,1])}.defer(0.2);
    
    f = SoundFile.openRead("samples/bach.wav".resolveRelative;);
    w = Window.new("SoundFileView", 1100@420).front.alwaysOnTop_(true);
    w.onClose_({e.release(0.1);b.free;c.free;d.free;a.free;g.free});
    a = SoundFileView.new(w, Rect(5,5, 1090, 410)).soundfile_(f)
                                                 .read(0, f.numFrames)
                                                 .gridOn_(false)
                                                 .timeCursorOn_(true)
                                                 .refresh;
    a.mouseUpAction = {var sel, start, cpos, size, end;
                           sel   = a.selections[a.currentSelection];
                           start = sel[0];
                           cpos  = a.timeCursorPosition;
                           size  = sel[1];
    
                        if(cpos==start,{end = start+size; start = start},{end = start; start = cpos});
                        [start, end, size].postln;
    
                        Routine.new({e.set(\gate,0);
                                     0.02.wait;
                                     e = Synth(\looper_gui2,[\bufnum,b,\bufenv,d,\str,start,\end,end,\dur,size,\gate,1])
                                     }).play
                        };
    
    g = MIDIdef.cc(\vel,{arg val; e.set(\transp,val.linlin(0,127,-12,12).round.postln)}); // trasp in semitoni
    )
    

Windowing

Campionare