Synth di servizio

Prima di cominciare la dissertazione dui Pattern definiamo due Synth di servizio:

s.boot;
s.scope;
s.meter;
s.plotTree;

(
SynthDef(\sperc,
               {arg freq=440, amp=0.5, dur=0.5;
                var env, osc;
                    env = Env.perc(0.01, dur-0.01).kr(2,1);        // Inviluppo percussivo
                    osc = SinOsc.ar(freq, 0, amp.lag(0.02));
                Out.ar(0, env*osc)}
        ).add;

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

SynthDef(\samp,
               {arg pos=0, dur=1, amp=0.5;		
                var rata,inizio,fine,env,osc;
                    rata   = SampleRate.ir;                         // Sample rate
                    inizio = pos*rata;                              // punto iniziale in frame
                    fine   = dur*rata+inizio;                       // punto finale in frame
                    env    = Env.linen(0.01,dur-0.02,0.01).kr(2,1); // Inviluppo trapezoidale
                    osc    = BufRd.ar(1, b.bufnum, Line.ar(inizio,fine,dur),0);
                Out.ar(0, env*osc*amp.lag(0.02))}
        ).add;
)

Sequencing e pulsazioni

In SuperCollider c’è una una specifica collezione di oggetti chiamata Pattern. Questi sono ottimizzati per generare sequenze di eventi contenenti informazioni (streams) che, spesso sono numeri (numeric streams), ma in linea di principio possono essere qualsiasi tipo di data.

I Patterns sono un’alternativa di alto livello a Routine e Task e uno dei vantaggi nel loro utilizzo sta nel fatto che le costruzioni sintattiche da utilizzare sono strutturate per pensare solo a ciò che vogliamo accada nel tempo e non a come questo avviene.

Tutti gli oggetti appartenenti a questa collezione cominciano con la lettera P maiuscola (Pbind, Pseries, Pwhite, Pfunc, etc.) e per questo motivo facilmente identificabili.

Infinite - Pbind()

Il primo che incontriamo è Pbind(). Per ora pensiamo che serve a generare una sequenza infinita di pulsazioni regolari se invochiamo su di esso il metodo .play:

Pbind().play;

Valutando il codice precedente sentiremo suonare una sequenza di pulsazioni regolari separate tra loro da tempi delta di un secondo, con un’altezza che nel sistema temperato occidentale corrisponde al do centrale.

Nel codice valutato però non c’è specificato nulla di tutto ciò: ne il tipo di strumento che esegue la sequenza (come nel caso di Routine e Task), ne i parametri necessari a definirne le caratteristiche. L’arcano si svela nell’apprendere il compito che ha Pbind(), ovvero inviare in modo dinamico uno o più parametri (quasi sempre numerici) a specifici indirizzi esattamente come per gli argomenti di una funzione, e se non specifichiamo nulla, utilizza i parametri di default che invia allo strumento (Synth) di default. Stabiliamo ora un valore in beat per i tempi delta:

Pbind(\delta, 0.5).play; // (\etichetta, valore)

Come possiamo osservare la sintassi è la stessa già utilizzata per inviare parametri a un Synth attraverso il metodo .set():

Comprendiamo ora come Pbind(), più che per generare sequenze di eventi nel tempo come affermato nelle prime righe di questo Paragrafo, serva principalmente per inviare dinamicamente nel tempo valori a specifiche etichette o indirizzi definiti in precedenza.

Utilizzando la stessa sintassi infatti, possiamo modificare qualsiasi tipo di parametro come ad esempio frequenze e ampiezze, oppure chiedere a SuperCollider di non utilizzare come strumento il Synth di default ma uno da noi programmato in precedenza (come quelli che utilizzeremo in questo paragrafo al posto del Synth di default (Synth di servizio).

Synth(\sperc);

(
Pbind(\instrument,\sperc,     // specifica il Synth)
      \freq,      90.midicps, // frequenze (midinote)
      \amp,       64/127,     // ampiezze (velocity)
      \delta,     0.33        // tempo delta (beats)
      ).play;
)

Synth(\samp);

(
Pbind(\instrument,\samp, // specifica il Synth)
      \pos,      0.76,   // posizione puntatore
      \dur,      0.1,    // durata
      \amp,      0.6,    // ampiezza
      \delta,    0.33    // tempo delta (beats)
      ).play;
)

Finite - Pn()

Attraverso la sintassi appena ilustrata possiamo però generare solamente sequenze infinite. Per il momento infatti abbiamo sempre interrotto la computazione con il comando da tastiera (ctrl+.).

Se vogliamo invece realizzare sequenze finite di valori che si ripetono uguali nel tempo (come i tempi delta della tipologia di pulsazioni in oggetto) dobbiamo sostituire il singolo valore numerico con l’oggetto Pn().

Pbind(\delta, 0.5      ).play; // (\etichetta, valore)
Pbind(\delta, Pn(0.5,4)).play; // (\etichetta, pattern)

Valutando la seconda riga, dopo quattro pulsazioni la computazione si ferma automaticamente, infatti l’oggetto Pn() serve per ripetere n volte un numero, un altro Pattern o qualsiasi altro tipo di data. I suoi argomenti sono (valore, numero_di_ripetizioni). Così come per n.do({}) possiamo utilizzare la keyword inf per ottenere ripetizioni infinite (sintassi questa più propria rispetto a quella illustrata in precedenza):

Pbind(\dur, Pn(0.5,inf)).play;

La sola limitazione al momento consiste nel fatto che se volessimo fermare una sequenza infinita usando il Synth di default lo potremmo fare solo con il comando ctrl+. e non dal codice:

(
b = 92;
t = TempoClock(b/60);
p = Pbind(\delta, Pn(0.5,inf)).play(t);
)

p.stop;  // non funziona...
t.clear; // non funziona...

Mentre se utilizziamo un Synth programmato da noi in modo proprio possiamo fermare la computazione anche dal codice:

(
b = 92;
t = TempoClock(b/60);
p = Pbind(\instrument,\sperc,  // Custom Synth
          \freq, 678,
          \amp, 0.5,
          \delta, Pn(0.5,inf)
).play(t);
)

p.stop; t.clear; // funziona...

Variabili - Pstutter()

Negli esempi precedenti potevamo realizzare solo pulsazioni regolari me se volessimo invece cambiare dinamicamente i tempi delta ad ogni pulsazione?. Introduciamo un nuovo oggetto: Pstutter():

Pbind(\dur, Pstutter(4, 0.5)).play;

A prima vista potrebbe sembrare del tutto simile a Pn() con gli argomenti invertiti (n_ripetizioni, pattern), invece si differenzia da esso per una caratteristica fondamentale: se utilizzato in coppia con un altro oggetto che ancora non conosciamo (Pkey()), accetta valori in input:

(
Pbind(
      \sudd, 4,                                    // come una variabile locale
      \delta, Pstutter(Pkey(\sudd), 1/Pkey(\sudd)) // (n_puls, td)
      ).play
)

Analizziamo brevemente il codice precedente, alla riga 3, all’interno di Pbind() assegnamo un valore numerico (4) a un’etichetta (\sudd) il cui nome abbiamo scelto a nostro piacere esattamente come se fosse la dichiarazione e l’assegnazione di una variabile locale. Dopodichè, sempre all’interno dello stesso Pbind(), richamiamo due volte il valore assegnato utilizzando l’oggetto Pkey(\nome_etichetta) che diventa sia il numero di ripetizioni di Pstutter() sia il denominatore di 1 nella divisione che calcola automaticamente i tempi delta. Dopo quattro ripetizioni la computazione si ferma automaticamente.

Randomiche - Pwhite()

Se nel codice precedente sostituiamo il valore costante di ripetizioni assegnato a \sudd con l’oggetto Pwhite() otteniamò un numero di suddivisioni scelto pseudocasualmente tra un limite minimo e un limite massimo ad ogni nuovo beat. Questo oggetto è infatti il corrispondente nei Patterns della funzione astratta rrand() e accetta tre argomenti: (minimo, massimo, numero_di_ripetizioni):

(
Pbind(
      \sudd,  Pwhite(1,8,inf),                     // esegue ogni n_puls
      \delta, Pstutter(Pkey(\sudd), 1/Pkey(\sudd)) // (n_puls, td)
      ).play

Il codice precedente genera sequenze infinite. Se vogliamo generare sequenze finite possiamo sostituire la parola riservata inf con un valore numerico tenendo però in considerazione il fatto che questo corrisponde al numero totale di pulsazioni da generare e non al numero di beats:

(
Pbind(
      \sudd,  Pwhite(1, 8, 10),                    // 10 pulsazioni non beats
      \delta, Pstutter(Pkey(\sudd), 1/Pkey(\sudd))
      ).play
)

Infine se vogliamo aggiungere un metronomo per verificare il codice, dobbiamo utilizzare un secondo Pbind(), realizzando in questo modo due strumenti polifonici separati ma sincronizzati allo stesso Clock:

(
b = 92;
t = TempoClock(b/60);

m = Pbind(\freq,  1480,        // metronomo Synth di default
          \amp,   0.3,
          \delta, Pn(1,inf)
          ).play(t);

p = Pbind(\instrument, \sperc, // pulsazioni Synth \sperc
          \freq,  80,
          \amp,   64,
          \sudd,  Pwhite(1,8,inf),
          \delta, Pstutter(Pkey(\sudd), 1/Pkey(\sudd))
          ).play(t)
)

m.stop;p.stop;t.clear;

Pause - \type

Nei paragrafi precedenti abbiamo visto come alcuni nomi di parametri usati comunemente come etichette all’interno di un Pbind() sono delle parole riservate o keyword ovvero hanno un significato preciso e non possiamo utilizzarle per altri fini. Fino a questo punto abbiamo incontrato: \instrument,\freq,\amp e \dur. Aggiungiamo ora \type.

Questa etichetta dice a SuperCollider che sta per essere definito il tipo di evento (Event types) da generare e al posto di valori o pattern accetta diversi simboli, ognuno dei quali specifica appunto il tipo di evento. Quelli che ci interessano in questo frangente sono:

(
Pbind(\dur, 0.5,
      \type, \note // genera note
).play
)

(
Pbind(\dur, 0.5,
      \type, \rest // genera pause...
).play
)

Condizioni - Pif() e Pfunc({})

Come abbiamo già intuito molti oggetti di SuperCollider hanno un loro alias nella famiglia dei Patterns. Pif() è l’alias di if() ed ha gli stessi argomenti con una sintassi leggermente variata: vero e falso non sono funzioni, ma simboli, valori o altri Patterns.

Verifichiamone il funzionamento programmando una scelta pseudocasuale tra note e pause. Siccome il test che stabilisce se eseguire una nota o fare una pausa deve essere effettuato a ogni passo (Evento) dobbiamo utilizzare l’oggetto Pfunc() che valuta a ogni nota (Evento) la funzione scritta al suo interno. Dopodichè in questo caso se il test è vero esegue una nota (\note), altrimenti fa una pausa (\rest):

(
Pbind(
      \sudd,  Pwhite(1,8,inf),
      \delta, Pstutter(Pkey(\sudd), 1/Pkey(\sudd)),
      \type,  Pif(Pfunc({rand(2)==0}),\note,\rest), // nota o pausa
).play;
)

Infine così come abbiamo fatto per le tipologie di pulsazioni precedenti confrontiamolo con un metronomo per monitorare la correttezza del codice:

(
b = 92;
t = TempoClock(b/60);

m = Pbind(\freq, 2637,         // metronomo Synth di default
          \amp,  0.3,
          \delta,  Pn(1,inf)).play(t);

p = Pbind(\instrument, \sperc, // pulsazioni Synth \sperc
          \freq, 90,
          \amp,  60,
          \sudd, Pwhite(1,8,inf),
          \delta,Pstutter(Pkey(\sudd), 1/Pkey(\sudd)),
          \type, Pif( Pfunc({rand(2)==0}), \note, \rest),
          ).play(t)
)

m.stop;p.stop;t.clear;b.free;

Sequenze

Per generare sequenze deterministiche dobbiamo leggere dinamicamente nel tempo i valori contenuti in Array o List, esattamente come avviene per Routine.new() e Task.new() ma prima di introdurre nel dettaglio gli oggetti che compiono questa operazione dobbiamo focalizzare due concetti:

Pattern vs Stream

Per meglio comprendere cominciamo da un esempio astratto: generare una sequenza numerica da 0 a infinito con incremento di 1 a ogni passo (valutiazione). Programmiamolo sia con una Routine che con un Pattern:

(
r = Routine({
             var i = 0;
             {i.yield;       // stop e stampa
              i = i+1}.loop; // contatore
             });
)

r.next;                      // valuta un passo alla volta

p = Pseries(0,1,inf);
p.next;                      // riporta 'a Pseries', non i valori

Le Routine come risulta evidente dall’esempio precedente sono strutture di controllo che possono essere fermate per poi riprendere dal punto in cui sono state interrotte (a ogni passo il valore della variabile i viene tenuto in memoria e aggiornato).

Questo è un esempio di uno Stream che può essere definito come la rappresentazione di una sequenza di valori incrementali ottenuti dall’invocazione del metodo .next e che può ripartire da capo invocando il metodo .reset.

I Patterns invece sono come il progetto di una casa, le piantine e gli spezzati, non la casa stessa, che sarà creata solo nel momento in cui gli operai la costruiranno passo dopo passo seguendo i progetti.

Non hanno il concetto di stato corrente. Possiamo però trasformare i Patterns in Streams invocando su di loro il metodo .asStream:

(
r = Routine({
             var i = 0;
             {i.yield;
              i = i + 1}.loop;
              })
)

r.nextN(10); // esegue 10 volte '.next'

p = Pseries(0,1,inf).asStream;
p.nextN(10); // esegue 10 volte '.next'

Se come in questo caso c’è un loop infinito all’interno di una Routine possiamo utilizzare il metodo .nextN() per ottenere un Array con i valori generati dal numero di valutazioni specificate come argomento. In pratica, i Patterns definiscono le azioni, gli Streams le eseguono un po come la differenza tra una partitura e la sua esecuzione, una Classe e un’istanza da essa derivata. Questa separazione di compiti ci permette ad esempio di generare Streams diversi dallo stesso Pattern:

p = Pseries(0,1,inf);
u = p.asStream;
u.next;
f = p.asStream;
[u.next, f.next];

Eventi e prototipi

Abbiamo visto in precedenza come Pbind() possa essere considerato un modo per inviare dinamicamente valori a nomi (backslashkey). Inoltre abbiamo appurato che ogni singolo evento temporale (azione) può essere definito da un insieme di questi.

Se per esempio consideriamo l’azione di eseguire una singola nota musicale possiamo specificare numerosi parametri tra i quali lo strumento che la deve eseguire, l’altezza, l’intensità, la durata, etc. In questo caso, l’insieme di coppie \nome, valore è un oggetto chiamato Event.

Un Event è dunque un insieme di comandi che specificano un azione da compiere. Se trasformiamo un Pbind() in uno Stream possiamo richiamare un Event per volta invocando il metodo .next(Event.new). In questo caso il metodo .new non può essere sottinteso. Possiamo anche utilizzare l’abbreviazione sintattica .next(()):

(
p = Pbind(\freq, Pwhite(60,90,inf).midicps,
          \amp,  Pwhite(60,90,inf),
          ).asStream;
)

p.next(Event.new);
p.next(());       // abbreviazione sintattica

Osserviamo come usando l’abbreviazione sintattica l’ordine sia invertito.

Pseq([])

Se sostituiamo Pwhite() con Pseq([]) e leggiamo gli eventi nella Post window, dovrebbe risultare estremamente chiaro cosa fa Pbind():

(
p = Pbind(\freq, Pseq(#[60,61,62,63].midicps,1),
          \amp,  Pseq(#[64,74,84,94]/127,1),
          ).asStream;
)

p.next(());

L’oggetto Pseq([]) definisce un Pattern che creerà uno Stream che itererà un Array. Ha due argomenti:

(
p = Pbind(\freq, Pseq(#[60,61,62,63].midicps,inf),
          \amp,  Pseq(#[64,74,84,94]/127,inf),
          ).asStream;
)

p.next(());

Fino a questo punto abbiamo solo generato data trasformando Pbind() in Stream e richiamando un Event alla volta. Sappiamo però che così come per le Routine anche su Pbind() possiamo invocare il metodo .play:

Pbind().play;

Eseguendo il codice precedente, oltre che udire dei suoni leggeremo nella Post window la scritta an EventStreamPlayer. Questo perchè ogni volta che invochiamo il metodo .play su un Pattern un oggetto così chiamato viene generato e legge gli Event uno dopo l’altro, separati da un tempo delta (\delta) espresso in beats e non in secondi.

Nell’esempio precedente però nessun parametro è specificato da coppie \key, value e, anche se sappiamo già che in questi casi Pbind() adotta i valori di default, non conosciamo quali. Possiamo ottenere questa informazione nella Post window valutando la riga seguente che è la rappresentazione di un singolo Event:

('freq': 440, 'amp': 0.1).play;

L’insieme delle azioni riportate è definito un event prototype.

Questi per essere tale deve includere una funzione (’msgFunc’: a Function) che viene eseguita quando invochiamo il metodo .play.

Gli altri parametri di default sono specificati da un evento di default che è stato da altri già programmato. Contiene le istruzioni per suonare un Synth, un insieme di coppie \key, value predefinite che ne definiscono i parametri di default e le più comuni conversioni e manipolazioni di questi.

Event.default;

Riassumendo

Un esempio di loop infinito:

(
p = Pbind(\instrument, \sperc,
          \freq, Pseq(#[60,61,62,63].midicps, inf),
          \amp,  Pseq(#[34,54,84,104]/127,    inf),
          ).play;
)

p.stop

Se invece vogliamo denerare una sequenza finita ma all’interno dello stesso Pbind() gli Array inclusi in due o più Pseq([]) sono di lunghezza differente oppure se il numero di ripetizioni non è lo stesso, Pbind() interrompe la sequenza esauriti gli eventi di quello con minor lunghezza:

(
Pbind(\instrument, \sperc,
      \freq, Pseq(#[60,61,62,63,64,65,66].midicps, 1),
      \amp,  Pseq(#[34,54,84,104]/127,             1),
      ).play;
)

(
Pbind(\instrument, \sperc,
      \freq, Pseq(#[60,61,62,63].midicps, 2), // 2 ripetizioni
      \amp,  Pseq(#[34,54,84,104]/127,    1), // 1 ripetizione
      ).play;
)

Se però specifichiamo inf nella sequenza con lunghezza minore, questa sarà letta ciclicamente da sinistra a destra fino al’esaurirsi degli eventi della sequenza con lunghezza maggiore:

(
Pbind(\instrument, \sperc,
      \freq, Pseq(#[60,61,62,63,64,65,66,67].midicps, 1),
      \amp,  Pseq(#[34,54,84,104]/127,              inf),
      ).play;
)

Per quanto riguarda pause e durate possiamo specificare le prime nell'Array delle frequenze con la keyword \rest mentre le seconde con una Pseq dedicata assegnata all'etichetta di default \dur. Se ci sono pause dovremo specificare la loro durata esattamente come per le note:

(
Pbind(\instrument, \sperc,
      \freq, Pseq(#[60,61,\rest,62,63,64,\rest,65,66,67].midicps, 1),
      \amp,  Pseq(#[34,54,84,104]/127, inf),
      \dur,  Pseq(#[0.2,0.5,1,0.55,0.74,1], inf)
      ).play;
)