Parametri del Tempo

La struttura, nella musica è la sua divisibilità in parti sempre maggiori, dalle frasi alle lunghe sezioni. La forma è il contenuto, è la continuità. Il metodo è il mezzo per controllare la continuità di nota in nota. Materiali della musica sono il suono e il silenzio. Integrarli significa comporre.   (J.Cage)

I seguenti tre paragrafi (Misurare il tempo, Organizzare il tempo e Convertire il tempo) sono uguali agli omonimi presenti nella prima parte in quanto trattano tematiche generali e comuni a tutti i diversi approcci compositivi e tecnico/informatici.

Misurare il tempo

La misurazione del tempo è stata la prima scienza esatta dell’antichità. Le uniche certezze provenivano dai fenomeni astronomici.    (E.Salvatori)

La musica è un espressione umana che si svolge nel tempo, in un tempo non reversibile se non nella memoria e una delle prime scelte che dobbiamo compiere nell’approcciarci alla scrittura di un brano, consiste nel definire l’andamento temporale dello stesso (veloce, lento, moderato, etc.), o meglio stabilire che tipo di rapporto ci sarà tra gli eventi sonori e con quale frequenza e quale densità saranno distribuiti all’interno della composizione.

L.v.Beethoven - Tema e variazioni

Contestualmente dobbiamo scegliere un un organico strumentale e se decidiamo di utilizzare uno o più strumenti acustici, sarà necessaria la realizzazione di una partitura sulla quale annotare in vari modi le sequenze di azioni che lo strumentista deve compiere per ricreare l’idea musicale in un tempo futuro. Per trasmettere e codificare queste informazioni, nel corso dei secoli compositori e teorici della musica hanno sviluppato la notazione musicale , o meglio un insieme di notazioni musicali, adattate di volta in volta alle rinnovate esigenze espressive e/o esecutive. Per contro se all’interno dell’organico è presente uno strumento elettroacustico/informatico ci troviamo di fronte a un problema. L’esecutore o interprete della parte elettronica in primo luogo è un software, che non ha mai seguito un corso di teoria e solfeggio o di semiografia della musica e quindi non è in grado di interpretare i simboli usati nelle notazioni musicali più o meno tradizionali. Per questo motivo dobbiamo in primo luogo definire alcuni parametri atti a misurare il tempo, o meglio misurare una sequenza di eventi o azioni che si svolgono nel tempo per poterli poi organizzare (comporre) in ritmo e tradurli in un linguaggio simbolico interpretabile sia da esecutori umani che da un software. Questi parametri sono tre:

In realtà i parametri strettamente necessari alla descrizione di una sequenza di eventi temporali sono solo i △t e le durate. Attraverso la loro combinazione è possibile realizzare sia sequenze di eventi senza soluzione di continuità (quando i △t coincidono con le durate), sia sequenze di eventi separate da pause (quando le durate sono inferiori ai △t). La misurazione dei tempi parziali (onsets) invece è legata al fatto che un brano musicale ha un tempo finito (un inizio e una fine, o quantomeno un inizio) e in alcuni casi può essere utile trovare un punto preciso al suo interno, ad esempio per fare ripartire un playback o una registrazione da quel punto preciso specificando dei marker tanto cari agli ingegneri del suono.

Per meglio comprendere questi parametri eseguiamo il codice di SuperCollider illustrato nell’esempio sottostante. Compare una finestra. Schiacciare, tenere premuto e rilasciare un tasto qualsiasi della tastiera del computer. Ogni volta che compiamo quest’azione, vengono visualizzati onset, △t e durata espressi in secondi. Onsets e △t vengono aggiornati quando il tasto viene schiacciato, mentre la durata quando viene rilasciato, in quanto fino a quando un’azione non termina non è possibile misurarne la durata. Immaginiamo di sostituire i tasti del computer con i tasti di un pianoforte, l’azione compiuta è la stessa ed è possibile descriverla sia con una notazione musicale per essere interpretata da un esecutore umano su uno strumento acustico, sia numericamente per essere interpretata da un computer attraverso i codici ASCII della sua tastiera.

(
var start = SystemClock.seconds,
    prev  = 0,
    onset = 0,
    delta,
    vora,
    vprec = 0,
    w     = Window('Tempo')
                  .background_(Color.black)
                  .setTopLeftBounds(Rect(0,0,210,135))
                  .alwaysOnTop = true;
    c     = StaticText(w,Rect(10,10,200,40))
                  .font_(Font("Times",36))
                  .stringColor_(Color.white)
                  .string_("Onset");
    d     = StaticText(w,Rect(10,50,200,40))
                  .font_(Font("Times",36))
                  .stringColor_(Color.white)
                  .string_("Delta");
    e     = StaticText(w,Rect(10,90,200,40))
                  .font_(Font("Times",36))
                  .stringColor_(Color.white)
                  .string_("Durate");

w.front.view.keyDownAction_({
                             arg ...args;
                                vora = args[3];
                                if(vora != vprec,
                                      {onset = SystemClock.seconds start;
                                       delta = onset prev;
                                       prev  = onset;
                                       c.string_("Onset   "++onset.round(0.01));
                                       d.string_("Delta    "++delta.round(0.01));
                                       }
								   );
                                vprec = vora})
            .keyUpAction_({var release = SystemClock.seconds start,
	                           dur = release   onset;
                           e.string_("Durate  "++dur.round(0.01));
                           vprec = 0
						   })
)

Organizzare il tempo

Stabiliti i parametri necessari alla descrizione di una sequenza di eventi nel tempo, dobbiamo scegliere un’unità di misura e, in base a questa adottare il sistema più adatto alla loro organizzazione. Per farlo abbiamo a nostra disposizione solo due possibilità: Tempo relativo e Tempo assoluto.

Tempo relativo

Tutti i valori sono definiti attraverso suddivisioni di una pulsazione regolare come nella tradizione musicale occidentale dal Rinascimento ad oggi (Tactus). In questo caso le unità di misura possono essere diverse:

Tempo assoluto

Il sistema proprio del linguaggio informatico. Anche questo modo di misurare il tempo si basa su pulsazioni regolari, ma non sono previste suddivisioni metriche strutturali, o meglio ci possono essere ma non rappresentano un elemento fondante di un sistema come quello ritmico/musicale. Per quello che riguarda il range entro il quale si svolgono gli eventi temporali musicali le unità di misura che possiamo utilizzare sono due:

Questi due sistemi non rappresentano esclusivamente la migliore scelta nella risoluzione di una problematica tecnica o una semplice convenzione di comodo, ma ognuno possiede caratteristiche musicali proprie e ben definite come possiamo verificare eseguendo il codice sottostante:

// 1) Accendere l'audio (Boot)
// 2) Eseguire il codice tra le due prime parentesi tonde (si definisce un Synth):
(
SynthDef(\sperc, {|freq =440, amp=1, dur=75, atk=10|
var env = EnvGen.ar(Env.perc(atk*0.001, (dur atk)*0.001), doneAction: 2),
    osc = SinOsc.ar(freq, 0, amp);
        Out.ar(0, env *osc ! 2);
}).add;
)
// 3) Eseguire il codice seguente:
// Pulsazione irregolare assoluta tra 75ms e 250ms: (
{var freq = rrand(800,1900);
  {inf.do{
          Synth("sperc",[\freq,freq,\amp,1]);
          rrand(0.075,0.25).wait
  }}.fork} ! 3;
)
// 4) Fermare l'esecuzione
// 5) Eseguire il codice seguente:
// Pulsazione irregolare con suddivisioni di un beat. (
{var bpm = 52,
     freq = rrand(800,1900),
     t = TempoClock(bpm/60);
  {inf.do {
           Synth("sperc",[\freq, freq,\amp,1]);
           [1/2,1/4,1/8,1/16].choose.wait
  }}.fork(t)} ! 3;
)

La scelta di utilizzare un sistema piuttosto che l’altro deve essere dunque dettata sia dal risultato musicale che vogliamo ottenere, sia dalle problematiche tecniche relative alla programmazione dello strumento informatico in relazione a organici misti (strumenti elettronico/informatici combinati con strumenti acustici). Se ad esempio il linguaggio musicale utilizzato necessità di una certa rigidità metrica (sebbene all’interno di una flessibilità temporale, sarà più adatta una misurazione in tempo relativo (bpm).

image not found

Se invece vogliamo realizzare un sound design con soli suoni di sintesi, oppure un brano di musica mista dove sia previsto che gli esecutori seguano un ”timer” come in certe attività improvvisative o di alea controllata, sarà più opportuno organizzare il tempo con un sistema di valori assoluti.

image not found

Convertire il tempo

Nella maggior parte dei casi, quando utilizziamo un software musicale, ci viene richiesto di specificare △t, durate e onsets in secondi, millisecondi o bps, utilizzando un sistema assoluto. Se però vogliamo pensare in valori relativi dobbiamo effettuare delle conversioni dell'unità di misura con delle semplici operazioni matematiche:

Se vogliamo per esempio generare una sequenza di semicrome con semiminima = 92 bpm come quella illustrata nella figura sottostante è necessario calcolare il △t in millisecondi nel modo seguente:

△t = (60000/92)*(1/4) = 652.17*0.25 = 652/4 = 163 ms

image not found

SuperCollider

Scheduler e post window

In informatica attivare automaticamente determinati processi o eventi in una sequenza temporale è un compito gestito da una parte del sistema operativo chiamata scheduler. Attraverso il processo di scheduling possiamo temporizzare una lista di eventi o far eseguire al software alcune operazioni prima di altre ad un tempo estremamente rapido. La velocità di computazione non temporizzata dipende dalla quantità di operazioni che il computer deve effettuare e dalle sue caratteristiche tecniche.

Facciamo attenzione a non confondere il tempo di scheduling con la sample rate (il tempo della computazione dei segnali audio digitali). In SuperCollider il primo è proprio dell’Interprete (sclang) e la sua velocità può variare a seconda di diversi fattori non sempre predicibili tra cui ad esempio il modello di computer utilizzato, mentre la seconda è propria del Server (scsynth) e nel momento in cui è stabilita è uguale su tutti i computers.

Per meglio comprendere lo scheduling in SuperCollider, calcoliamo le conversioni temporali illustrate pocanzi eseguendo il codice seguente, una riga alla volta oppure selezionando l’intero blocco incluso tra le parentesi tonde.

(
82 / 60;    // conversione bpm --> bps: bpm/60.
1.3 * 60;   // conversione bps --> bpm: bps*60.
82 / 60000; // conversione bpm --> ms:  60000./bpm
82 / 60;    // conversione bpm --> s:   60./bpm
1/2;
1/3;
1/4;
)

A ogni esecuzione il risultato viene stampato nella Post window. Se procediamo una riga alla volta possiamo scegliere l’ordine andando su e giù tra le righe col cursore, mentre se eseguiamo l’intero blocco di codice vedremo postati i risultati nello stesso ordine delle operazioni scritte nell’Interprete, riga dopo riga, dall’alto verso il basso. Quando eseguiamo il codice, i risultati delle operazioni sono scritti nella Post window in quanto come abbiamo visto nel primo Capitolo, questo è lo spazio dove possiamo leggere o scrivere dati e informazioni utili riguardo ciò che stà accadendo in SuperCollider. Il monitoraggio dello scheduling ne è un tipico esempio. La Post window diventa inoltre uno strumento indispensabile anche quando dobbiamo effettuare un debugging, ovvero cercare e correggere eventuali errori presenti nel codice. Per verificare eseguiamo le seguenti righe:

(
var freq;
    freq = rrand(300,600);
      {SinOsc.ar(freq,0,0.2)}.play;
)

Se abbiamo già fatto il Boot dell’audio (cmd+b), vedremo postato qualcosa riguardo a un Synth (un’insieme di informazioni che potrebbero tornare utili).

Synth(”temp_0” : 1000)

Se invece le eseguiamo senza aver fatto il Boot (se già acceso fare prima ”Quit Server”), comparirà il seguente messaggio di errore:

RESULT = 0
WARNING: server ’localhost’ not running.

nil

Quando SuperCollider incontra un errore nella compilazione del codice, oltre a non funzionare (nel migliore dei casi) stampa nella Post window un messaggio di errore e indica con una freccia o un pallino il punto esatto dove si è interrotta la compilazione. Attenzione, non dove si è verificato l’errore, ma dove si è fermato. Sapendo che l’esecuzione avviene dall’alto al basso, riga dopo riga, quasi sempre da sinistra a destra, probabilmente l’errore è poco prima e a noi non resta che compiere le verifiche del caso per trovarlo. Un altro esempio:

play({SinOsc.ar(LFNoise0.kr(12,mul:600,add:1000)0.1)});  //...manca una virgola...  

La maggior parte delle volte si tratta di errori di battitura (manca una virgola, non abbiamo chiuso una parentesi, abbiamo usato una lettera maiuscola dove non potevamo, etc.) ma altre volte è possibile imbattersi in situazioni più complesse da risolvere. Per questi e altri casi, possiamo consultare i messaggi di errore più comuni sul sito di Daniel Nouri.

Controllo del tempo

Ora che abbiamo appreso il processo di scheduling, prima di cominciare a pensare quali possono essere le possibilità di controllare il tempo in SuperCollider adottiamo a-criticamente la seguente regola (non necessariamente valida ma che al momento può aiutarci a comprendere):

una riga di codice = un singolo evento nel tempo

sia esso una nota, una pausa, un trigger, il play di audio file, un grano di una sintesi granulare, etc. Seguendo questo assunto il codice seguente descrive cinque eventi in successione:

(
"riga uno -> primo evento".postln;
"riga due -> secondo evento".postln;
"riga tre -> terzo evento".postln;
"riga quattro -> quarto evento".postln;
"riga cinque -> quinto evento".postln;
)

Al momento dell’esecuzione la computazione sarà ordinata (sequenziata) ma molto veloce, quasi contemporanea all’azione delle dita sulla tastiera del computer. Non abbiamo alcun tipo di controllo sul tempo e come abbiamo detto in precedenza, la velocità di esecuzione dipenderà dalle caratteristiche dell’hardware e dalla mole di dati che devono essere computati. Una possibile trascrizione musicale è illustrata nella figura sottostante, dove i righi del pentagramma non indicano le altezze musicali, ma le righe dell’Interprete.

image not found

In musica abbiamo però la necessità di poter controllare sequenze di eventi nel tempo in diversi modi, con diversi parametri e unità di misura, non basta inanellarli in un ordine più o meno consequenziale. Dobbiamo avere ad esempio la possibilità di modificare i Tempi Delta che intercorrono tra una riga di codice (evento) e quella successiva o di controllare (idealmente) la durate delle operazioni effettuate nella computazione interna alle righe di codice, come illustrato nella figura seguente.

image not found

Per fare questo in SuperCollider, abbiamo a disposizione tre diversi Clocks (orologi) che sono appunto degli Scheduler.

(
SystemClock.sched(0.0, {"------".postln; "System".postln; 1.5});
TempoClock.sched(0.5, {"Tempo".postln; 1.5});
AppClock.sched(1.0, {"App".postln; 1.5});
)

Ognuno possiede delle proprie peculiarità e, in base alle necessità musicali o di ottimizzazione del software di volta in volta sceglieremo di usarne uno piuttosto che un altro.

SystemClock

SystemClock è un oggetto di SuperCollider. Osserviamo nel dettaglio le sue caratteristiche richiamando il suo Help file. Gli Help files generalmente sono divisi in due o tre parti, una chiamata Description, un’altra Class Methods e la terza, quando presente Instance Methods.

image not found

Nella prima (Description è documentato lo stato dell’oggetto, ovvero a quale tipo di Data appartiene a quali messaggi può rispondere e quali sono le operazioni può compiere. Nella fattispecie dell’oggetto in trattazione:

Proseguiamo nel parallelo esemplificativo Clock = Metronomo. Se vogliamo utilizzare un metronomo reale dobbiamo:

In SuperCollider, essendo il nostro clock "virtuale", oltre alle operazioni elencate qui sopra, dobbiamo anche crearlo e dirgli cosa fare. La sequenza di queste operazioni può essere riassunta in uno schema sintattico fondamentale, applicabile a quasi tutti gli oggetti di SuperCollider:

SystemClock;                             // crea un Clock...
SystemClock.sched;                       // ...gli dice quale azione compiere...
SystemClock.sched(0.0,{"a".postln;0.5}); // ...e in che modo

Osserviamo i singoli passaggi:

image not found

SystemClock è come gli altri due Clocks un orologio, e come tutti gli orologi o i cronometri può servire a monitorare il tempo che è trascorso a partire da un determinato istante (onset). Questo istante nel caso di SystemClock è il momento in cui abbiamo lanciato SuperCollider. Infatti se invochiamo un altro Metodo di Classe (.schedAbs()) e specifichiamo un argomento nella funzione in esso contenuta il valore di questo argomento diventa a ogni valutazione quello del tempo trascorso dall’apertura di SuperCollider.

(
t = SystemClock;
t.schedAbs(0,
              {arg ora;        // riporta il tempo trascorso dall'apertura di SC
                   ora.postln; // posta l'informazione
               0.5}
           )
)  

Nel caso non volessimo utilizzare SystemClock, possiamo recuperare la stessa informazione attraverso il codice seguente:

Main.elapsedTime;

In questo caso però non essendo all’interno di uno scheduler non restituisce un valore a intervalli regolari nel tempo ma solo quando eseguiamo la specifica riga di codice (trigger). Questa e altre informazioni che al momento ci possono sembrare inutili, diventano preziose in alcune situazioni performative. Se ad esempio volessimo far partire uno scheduling dopo n secondi dall’esecuzione del codice e con la stessa esecuzione automatizzare il suo stop dopo altri n secondi potremmo scrivere il codice seguente:

(
t = Main.elapsedTime + 10;                     // tempo dall'apertura di SC + 10 secondi
a = SystemClock.sched(2,                       // aspetta 2 secondi dall'esecuzione del codice
                        {arg ora;              // riporta l'onset ad ogni valutazione della funzione
                              if(ora >= t,     // se 'ora' è maggiore o uguale a 't'
                                  {a.clear},   // ferma lo scheduling
                                  {ora.postln} // altrimenti posta il valore di onset
                                );
                        1}
                      );
)  

Detto questo ai fini pratici SystemClock viene solitamente utilizzato solo in poche situazioni simili a quella appena descritta e che incontreremo più avanti. Per gestire il tempo musicale in modo molto più versatile è meglio utilizzare un altro Clock: TempoClock. Prima di affrontare le sue caratteristiche tecnico/musicali specifiche però penso sia necessario aprire una breve parentesi su alcuni concetti fondamentali riguardanti il linguaggio informatico sul quale si basa SuperCollider che spero inoltre possano chiarire ulteriormente le conoscenze sintattico informatiche fino ad ora acquisite.

Programmazione Orientata ad Oggetti

SuperCollider così come altri software segue un paradigma di programmazione chiamato Object Oriented Programming. Tutte le entità presenti in questo tipo di linguaggio sono chiamate oggetti e questi sono generalmente istanze di qualche classe programmata in precedenza da James McCartney o da altri sviluppatori. Conseguentemente, per programmare SuperCollider dobbiamo anche noi conoscere e seguire i principi di questo paradigma e gli elementi principali sul quale è fondato. Questi, semplificando notevolmente sono: classi, istanze, metodi e messaggi e servono sia per descrivere un oggetto che per fargli compiere alcune azioni.

Classi

La Classe di un oggetto contiene la sua descrizione astratta, le eventuali istruzioni per costruirlo e le operazioni che possono essere eseguite su di esso o meglio che può eseguire. Pensiamo per esempio a un oggetto reale come una porta. Un’ipotetica classe Porta potrebbe contenere le seguenti informazioni:

In SuperCollider tutte le classi cominciano con una lettera maiuscola e sono di colore Blu.

Istanze

Se la classe Porta è stata definita (da noi o dai programmatori del software), è possibile creare una o più porte derivate da essa, ognuna con caratteristiche diverse (più o meno larghe, più o meno alte). Ogni nuova porta sarà una nuova istanza della classe Porta e si differenzierà dalle altre grazie all’assegnazione di valori differenti alle variabili d’istanza che nel caso specifico sono: larghezza e altezza.

Metodi e messaggi

Infine abbiamo già visto che è possibile richiedere a una classe o a un’istanza di compiere delle azioni che devono essere obbligatoriamente previste e descritte all’interno della classe. Questa descrizione è chiamata metodo. La richiesta di effettuare l’azione avviene attraverso un messaggio, o meglio quando un’istanza o una classe ricevono un messaggio specifico, collegano questo messaggio al relativo metodo interno. Se ad esempio un’istanza della classe Porta riceve il messaggio Porta.apri si apre solamente se questa azione è descritta al suo interno come metodo. Ora dovrebbe essere più chiara anche la differenza tra functional notation e receiver notation già affrontata alla fine del Capitolo precedente. Infine abbiamo visto che negli Help files di alcuni oggetti sono presenti sia metodi di classe (Class Methods) che metodi d’istanza (Instance Methods). Questo perchè nella scrittura del codice possiamo utilizzare alcuni oggetti sia come classi che come istanze e, a seconda dei casi potremo richiamare dei metodi piuttosto che altri. A chiosa di questo paragrafo una breve terminologia generale in inglese riguardante i linguaggi OOP che può tornare utile nella lettura di manuali dedicati.

Terminologia inglese

TempoClock

Dopo aver chiarito i concetti fondamentali della programmazione orientata ad oggetti proseguiamo nell’esplorazione dei Clock disponibili in SuperCollider. Anche TempoClock è uno scheduler ma rispetto a SystemClock presenta alcune differenze sostanziali che lo rendono musicalmente più versatile ma tecnicamente meno preciso. Osserviamo nel dettaglio quali sono le sue caratteristiche:

AppClock

Ai fini pratici AppClock. è uno scheduler del tutto simile a SystemClock in quanto:

Ci sono però due differenze. La prima è tecnica e consiste nel fatto che vive nello stesso thread dell’applicazione mentre SystemClock in uno separato. La seconda è più pratica: SystemClock è uno scheduler ad alta priorità mentre AppClock a bassa priorità e questa è la discriminante maggiore nel decidere di utilizzare uno o l’altro. Infatti ogni volta che vorremo visualizzare parametri dinamici (che cambiano nel tempo) su una GUI (Graphic User Interface) dovremo obbligatoriamente utilizzare AppClock che essendo a bassa priorità darà la preferenza alla computazione dell’audio e solo se il computer avrà ancora risorse disponibili computerà anche la parte visiva.

Routine e Task

I Clock attraverso il metodo .sched generano un beat generale, il tempo metronomico di un brano. Se però vogliamo specificare le suddivisioni interne al beat dobbiamo utilizzare due nuove Classi di SuperCollider: Routine e Task.

Routine

Prendiamo nuovamente in considerazione sempre a fini esemplificativi e non assoluti l’assunto che afferma:

una riga di codice = un singolo evento nel tempo

Secondo questa affermazione, il codice seguente specifica una sequenza di cinque eventi in rapidissima successione (”il più veloce possibile”) così come illustrato in notazione musicale nella Figigura sottostante. Ricordiamo inoltre che SuperCollider computa il codice in quasi tutti i casi dall’alto al basso, riga dopo riga, da sinistra a destra.

image not found

(
"riga uno -> primo evento".postln;
"riga due -> secondo evento".postln;
"riga tre -> terzo evento".postln;
"riga quattro -> quarto evento".postln;
"riga cinque -> quinto evento".postln;
)

Immaginiamo ora di volere stoppare automaticamente la computazione alla fine di ogni riga, come se ci fosse una fermata musicale (corona o punto coronato) e di dover compiere una nuova azione per riprendere la valutazione del codice da dove si è fermata così come illustrato in notazione musicale nella Figura seguente.

image not found

Questo è quello che fanno le Routine. Per verificarlo eseguiamo l’esempio nel box seguente. Prima le righe da 1 a 9 e poi la riga 10 oppure la 11, più volte.

(
r = Routine.new({        // Assegno la Routine alla variabile globale 'r'
                1.yield; // yield = fermata
                2.wait;  // wait  = sinonimo di yield
                3.yield; 
                4.yield; 
                5.yield; 
               })
)
r.value; // valuta riga per riga
r.next;  // sinoinimo di .value
r.reset  // riporta all'inizio

Risulta evidente che lo schema sintattico delle Routine è simile a quello che abbiamo già incontrato per i Clock:

Routine.new( {func} );.

Analizziamo brevemente ma nel dettaglio i metodi utilizzati nel codice precedente.

Invece che richiamare il contenuto di una Routine un passo alla volta con il metodo .value, possiamo automatizzare l’esecuzione dell’intera sequenza (sequencing) invocando sulla Routine il metodo .play.

(
r = Routine({            
             0.5.yield;       // tempo delta (fermata)
             "trig_1".postln; // evento
             2.yield;
             "trig_2".postln;
             0.1.yield;
             "trig_3".postln;
           });
)

r.reset.play; // Esegue la Routine 

In questo caso se scriviamo numeri int o float come oggetti sui quali invocare .yield o .wait all’interno della Routine, questi vengono interpretati da SuperCollider come tempi delta di attesa prima di passare oltre (le fermate o corone musicali di cui sopra). Il metodo .play() accetta inoltre come argomento uno dei tre Clocks che già conosciamo e a seconda di quello utilizzato i valori numerici specificati all’interno della Routine corisponderanno a secondi, millisecondi (tempo assoluto) o beats (tempo relativo).

(
d = SystemClock;       // secondi
b = 120;               // bpm
t = TempoClock(b/60);  // bpm --> bps
a = AppClock;          // secondi (per le GUI)

r = Routine({         
             0.5.wait; // sinonimo di .yield     
             "trig_1".postln;
             2.wait;
             "trig_2".postln;
             0.1.wait;
             "trig_3".postln;
            });
)
r.reset.play(d); // Esegue la Routine a SystemClock (tempi in secondi 1 = 1 secondo)
r.reset.play(t); // Esegue la Routine a TempoClock  (tempi in beats   1 = 1 beat metronomico)
r.reset.play(a); // Esegue la Routine a AppClock    (tempi in secondi 1 = 1 secondo)

Se vogliamo fermare lo scheduling di una Routine possiamo farlo utilizzando due comandi differenti:

Passiamo ora a illustrare una sintassi che può essere utilizzata per leggere in loop una Routine (nei prossimi Capitoli incontreremo sintassi più proprie per effettuare questa operazione ma il codice seguente è un buon esempio di come mettere in pratica i concetti e le nozioni apprese fino a questo punto.

(
t = SystemClock;
r = Routine({
             "trigger".postln; 
              0.3.yield
            }).play(t);

t.sched(0, {r.reset.play(t); 0.6}); // ogni 0.6 secondi triggera nuovamente la Routine 
)                                  
                                 
t.clear;                            // ferma la computazione

Nell’esempio precedente usiamo SystemClock sia per stabilire l’unità di misura temporale usata all’interno della Routine sia per triggerarla a intervalli regolari. E’ questo il motivo per il quale abbiamo interrotto lo scheduling di SystemClock e non invocato il metodo .stop sulla Routine nel fermare il loop. Volendo, avremmo potuto usare anche gli altri due Clock.

Abbreviazioni

Per scrivere una Routine possiamo utilizzare anche un’abbreviazione sintattica invocando il metodo .fork() su di una funzione.

(
var b = 52,               // bpm
    t = TempoClock(b/60); // bpm --> bps
    
{
"trig_1".postln; // azione (evento o nota)
1.wait;          // aspetta 1 beat (non 1 secondo)
"trig_2".postln; 
1.wait;        
"trig_3".postln;     
}.fork(t)        // equivale a '.play(t)'
)

In SuperCollider così come in altri software a causa della compresenza di diversi linguaggi informatici alcune operazioni e istruzioni possono essere scritte in molti modi differenti. Le Routine tradizionali e l’abbreviazione sintattica (Syntax Shortcut) {}.fork(t) ne sono un esempio così come la possibilità di scrivere il codice in functional notation o receiver notation. A questo link possiamo trovarne un elenco esaustivo. Nell’usare questa abbreviazione sintattica dobbiamo però tenere conto di alcuni particolari e usare alcuni accorgimenti. Se ad esempio vogliamo usare il metodo .stop per interrompere la computazione è necessario invocare il metodo .fork() contestualmente all’assegnazione della funzione a una variabile, altrimenti non si fermerà in quanto SuperCollider interpreta tutto ciò che è incluso tra parentesi graffe come una funzione, non come una Routine. Se eseguiamo le righe da 1 a 11 del codice seguente, SuperCollider prima riporta la scritta a Routine e poi comincia a computare la sequenza. Se prima della fine eseguiamo la riga 12 si interrompe.

(
b = 52;
t = TempoClock(b/60);
r = {"trigger_1".postln; 1.wait;
	 "trigger_2".postln; 1.wait;
	 "trigger_3".postln; 1.wait;
	 "trigger_4".postln; 1.wait;
	 "trigger_5".postln; 1.wait;
	 "trigger_6".postln;
}.fork(t)
)

r.stop; 

Se invece eseguiamo le righe da 1 a 11 del codice seguente SuperCollider innanzitutto restituisce a Function e non a Routine e se volessimo interrompere la sequenza dal codice dovremmo distruggere il Clock invece che stoppare la Routine.

(
b = 52;
t = TempoClock(b/60);
r = {"trigger_1".postln; 1.wait;
	 "trigger_2".postln; 1.wait;
	 "trigger_3".postln; 1.wait;
	 "trigger_4".postln; 1.wait;
	 "trigger_5".postln; 1.wait;
	 "trigger_6".postln;
}
)

r.fork(t);  // parte ma...
r.stop;     // non si stoppa, per farlo
t.clear;    // dobbiamo distruggere il Clock

La Routine parte eseguendo la riga 12 perchè il metodo .fork() quando invocato "trasforma" una funzione in una Routine, ma nel momento in cui invochiamo .stop sulla variabile r, SuperCollider la interpreta come funzione e non come Routine, non eseguendo ciò che gli stiamo chiedendo. Distruggendo il Clock tagliamo la testa al toro.

Task

Un Task all’apparenza è esattamente come una Routine. Accetta gli stessi metodi e segue la stessa sintassi. La principale differenza tra queste due Classi sta nel fatto che i primi possono essere messi in pausa e riprendere la computazione dal punto in cui li abbiamo fermati, mentre le Routine come abbiamo visto se stoppate possono solo essere resettate e ripartire dall’inizio.

(
b = 52;
t = TempoClock(b/60);
r = Task({"trigger_1".postln; 1.wait;
          "trigger_2".postln; 1.wait;
          "trigger_3".postln; 1.wait;
          "trigger_4".postln; 1.wait;
          "trigger_5".postln; 1.wait;
          "trigger_6".postln; 1.wait;
          "trigger_7".postln; 1.wait;
          "trigger_8".postln; 1.wait;
          "trigger_9".postln;
})
)

r.play(t);
r.pause;  // pausa
r.resume; // riprende
r.stop;   // come pause
r.reset;  // resetta all'inizio la prossima volta che viene eseguito
r.start;  // riparte da capo

I metodi .play e .resume sono sinonimi così come .stop e .pause. I primi servono per far partire la computazione dal punto in cui si trova, i secondi per stopparla o metterla in pausa. Il metodo .start invece fa partire la computazione di un Task sempre dall’inizio, come se avessimo usato .reset.play().

Facciamo attenzione a non confondere la differenza tra Routine e Task appena esposta con la spiegazione data all’inizio del paragrafo sulle prime. Riassumendo le Routine possono solo essere eseguite da capo a fondo anche se interrotte prima del loro termine mentre i Task possono essere messi in pausa e riprendere dal punto in cui li abbiamo interrotti.

I Task sono dunque musicamente più funzionali delle Routine ma questa maggiore duttilità (come spesso avviene in informatica) ha un prezzo da pagare ovvero ci sono alcune limitazioni:

Per chi volesse approfondire queste e altre problemetiche riguardanti i Task può consultare il loro Help file dove troverà anche diversi esempi che illustrano queste limitazioni.

Nesting

Nel Paragrafo dedicato alle Routine abbiamo "concatenato" tra loro due metodi o meglio abbiamo invocato due metodi sullo stesso oggetto.

Routine({ 1.wait; "ciao".postln; 1.wait; "ciao".postln}).reset.play; 

Questo tipo di costrutto sintattico si chiama nesting ("a nido d’ape") e rappresenta una delle una delle maggiori potenzialità di SuperCollider: ogni oggetto può diventare diventare argomento di altri oggetti, come un insieme di scatole cinesi o matrioske. L’esempio seguente è scritto interamente in functional notation ed estremamente esemplificativo al riguardo.

                                 10;                   // un numero
                            rand(10.0);                // random   
                       dup({rand(10.0)}, 8);           // replica  
                  sort(dup({rand(10.0)}, 8));          // ordina
            round(sort(dup({rand(10.0)}, 8)), 0.01);   // arrotonda  
     postln(round(sort(dup({rand(10.0)}, 8)), 0.01));  // stampa 
plot(postln(round(sort(dup({rand(10.0)}, 8)), 0.01))); // visualizza 

Possiamo anche utilizzare la receiver notation ricordando che il codice su una riga viene eseguito quasi sempre da sinistra a destra fino al punto e virgola.

10.0;                                            // un numero
10.0.rand;                                       // random
10.0.rand.round(0.01);                           // arrotonda
{10.0.rand.round(0.01)}.dup(8);                  // replica
{10.0.rand.round(0.01)}.dup(8).sort;             // ordina
{10.0.rand.round(0.01)}.dup(8).sort.postln;      // stampa
{10.0.rand.round(0.01)}.dup(8).sort.postln.plot; // plot

Ricordiamo che la scelta di una o dell’altra notazione (come già enunciato nel paragrafo dedicato all’argomento) è esclusivamente personale, da compiere tenendo sempre ben presente che un codice "pulito" e chiaramente leggibile deve essere il fine da perseguire.

Organizzazione del codice

Seguendo quest’ultima indicazione proviamo a rendere ancora più leggibile il codice precedente assegnando ogni passaggio a variabili i cui nomi (etichette o indirizzi) possano assumere valenza mnemonica riguardo al tipo di richiesta fatta all’oggetto.

(
var nelementi,range,valori,lista,ordina,semplifica;

    nelementi  = 8;             // variabili locali (input)
    range      = 100.0; 
    
    valori     = {rand(range)}; // algoritmo
    lista      = dup(valori, nelementi);
    ordina     = sort(lista);
    semplifica = round(ordina,0.01);
    
                                // risultati (output)
semplifica.postln;              // stampa
semplifica.plot;                // visualizza
)

Come possiamo notare il blocco di codice è inoltre suddiviso idealmente in quattro parti che ricordano lo schema delle funzioni.

Se cambiamo il range dei valori numerici l’esempio precedente può assumere valenza musicale, ovvero se il range entro il quale scegliere i valori random diventa da 0 a 12 e se sommiamo ai valori ottenuti un offset di 60 possiamo ottenere una sequenza di n altezze assimilabile a midi pitch compresi tra il do3 (do centrale valore 60) e il do4 (un ottava sopra valore 72 ovvero 60 + 12).

plot(postln(round(sort(dup({60 + rand(12.0)}, 8)))));

Come vedremo nei prossimi Capitoli questo tipo di organizzazione del codice può avere numerosi parallelismi e similitudini con l’organizzazione di una partitura musicale, il che facilita la ricerca di quel terreno comune tra i due linguaggi illustrato nel paragrafo iniziale. Infine quando affronteremo i segnali audio, il nesting e l’organizzazione del codice saranno fondamentali nell’effettuare collegamenti (plugs) tra segnali nella generazione di algoritmi di sintesi e elaborazione del suono. Di seguito un esempio.

(
s.waitForBoot{
play(
    {
    CombN.ar(
             SinOsc.ar(
                       midicps(
                               LFNoise1.ar(3, 24,
                                                 LFSaw.ar([5, 5.123], 
                                                           0, 3, 80)
                                           )
                               ),
                      0, 0.4),
            1, 0.3, 2)
    }
)}
)
// Piu' ordinato e comprensibile:
(
s.waitForBoot{
{var sawfreq,freqHz,freqcps,sine,filtro; 

    sawfreq = LFSaw.ar([5, 5.123], 0, 3, 80); // Onda a dente di sega
                                              // tra 77 e 83 con
                                              // frequenza di 5 Hz
    freqHz  = LFNoise1.ar(3, 24, sawfreq);    // cambia l'offset
    freqcps = freqHz.midicps;                 // Hz --> cps
    sine    = SinOsc.ar(freqcps,0,0.4);       // Oscillatore sinus
    filtro  = CombN.ar(sine,1, 0.3, 2);       // Comb filter
		filtro}.play}
)