Tempo e sequenze

Fonemi e morfemi, ringtones e incisi

La sillaba, nel campo letterario, deve aggregarsi ad altre sillabe per formare il più piccolo organismo avente senso compiuto: la parola, così dall’unione di più tempi primi nasce il piede musicale.    (R.Dionisi)

Come abbiamo chiarito nel primo capitolo della prima parte di questo scritto i suoni per diventare elementi di un linguaggio musicale devono essere organizzati (composti) secondo regole e procedure (per i più pignoli anche l’assenza totale di regole è una regola e una scelta procedurale). Per chiarire questo concetto pensiamo alla nascita della tradizione musicale occidentale. Si presume, in assenza di fonti dirette, che le primissime forme di musica siano nate nel paleolitico soprattutto dal ritmo. Il battere le mani e i piedi usato per imitare il battito del cuore o il ritmo cadenzato della corsa durante la caccia. L’alternare per gioco o per noia le fonazioni spontanee durante un lavoro faticoso o monotono come il pestare il grano per farne farina o il chinarsi per raccogliere piante e semi. Probabilmente accompagnandosi con strumenti a percussione in quanto di facile costruzione. Possiamo paragonare queste prime forme a flussi di eventi sonori nel tempo. Non costituivano un linguaggio nè verbale nè sonoro ma semplici imitazioni più o meno elaborate di un fenomeno naturale. Possiamo affermare dunque che il linguaggio musicale occidentale sia nato dalla parola. La poesia greca, la cantillazione e la salmodia costituiscono i primi sviluppi musicali della parola verso il canto. Da queste prime forme comincia un passaggio graduale che porta da una prevalenza di regole proprie del linguaggio parlato (come il ritmo definito dalla metrica del verso) a regole proprie del linguaggio musicale (come gli schemi ritmici di lunga e breve) che nell’emancipazione dal senso e nella conseguente astrazione diventano indipendenti dalla parola stessa. Una graduale sovrapposizione di significato e significante fino alla totale coincidenza. Ricordiamo che un suono significa solo se stesso. Affronteremo le diverse regole e procedure in Capitoli dedicati, ma prima di farlo dobbiamo partire dall’osservazione di alcuni elementi minimi del linguaggio. Nella lingua parlata le singole lettere, o meglio i singoli fonemi sono definiti come "un segmento fonico-acustico non suscettibile di ulteriore segmentazione in unità dotate di capacità distintiva". Nella figura sottostante una tavola di fonemi e grafemi.

image not found

Nel linguaggio musicale il singolo fonema può essere paragonato a un singolo evento (pulsazione, nota, suono o silenzio) di durata breve e variabile:

image not found

I fonemi di una lingua parlata sono caratterizzati da numerosi parametri come altezza, timbro, inviluppo, etc. Per non complicare troppo le cose però, al momento prenderemo in considerazione solo quello temporale.

Inanellando nel tempo due o più fonemi in una sequenza lineare alfabetica otteniamo un morfema che è "la minima unità grammaticale isolabile di significato proprio". Nel ritmo musicale può essere paragonato alle possibili successioni di due suoni:

image not found

Uno o più morfemi formano una parola, ovvero "l’unità minima necessaria all’espressione orale o scritta di un’informazione o di un concetto". Nel linguaggio musicale possiamo paragonarla all’inciso ritmico, che a sua volta può essere sviluppato in semifrasi e frasi:

image not found

Se coincide con un singolo morfema (morfema libero), può assumere la forma minima di Ringtone o Jingle:

image not found

se invece necessita di altri morfemi per assumere significato (morfema legato), corrisponde all’ inciso ritmico.

Ringtone è un termine della lingua inglese utilizzato per identificare la suoneria del telefono, mentre Jingle è traducibile in "tintinnio", breve intermezzo musicale che nelle trasmissioni radiofoniche o televisive indica l’inizio di uno spazio pubblicitario o il cambio di programma. Entrambi, seppur nella loro brevità, hanno un significato musicale compiuto, bastano a se stessi. Si possono dunque definire forme musicali al pari di una Fuga, di uno Scherzo o di una Forma Sonata. La differenza principale sta nel fatto che sono due forme funzionali, ovvero hanno una funzione precisa come segnalare un avvenimento, mentre le altre sono forme d’arte.

SuperCollider

Sintetizzatore percussivo

Da questo Paragrafo in poi per facilitarne la comprensione degli esempi a livello percettivo useremo un sintetizzatore sinusoidale percussivo virtuale che, rispondendo ai nostri comandi "suonerà" il codice. Nel riquadro seguente sono definite le istruzioni che dobbiamo dare a SuperCollider per generare un prototipo di questo Synth. Eseguendo l’intero codice inviamo queste istruzioni al Server e al contempo accendiamo la computazione audio (boot del server). Entreremo nel merito di questi argomenti nella Sezione dedicata all’audio, per ora non poniamoci troppe domande al riguardo.

// frequenze in Hz
// ampiezze tra 0.0 e 1.0
// durate in secondi

(
s = Server.local.boot;
SynthDef(\sperc, {|freq =440, amp=0.5, dur=0.5, atk=10, gate=1.0|
                  var durms = dur*1000,
                      env   = EnvGen.ar(Env.perc(0.01, dur-0.01),gate,doneAction: 2),
                      osc   = SinOsc.ar(freq, 0, amp);
                  Out.ar(0, env *osc ! 2)
                  }).add;
)

Dopo aver compiuto questa operazione torniamo agli esempi di codice illustrati nei paragrafi precedenti dove abbiamo definito sequenze di eventi nel tempo. Una delle azioni che abbiamo chiesto di compiere a SuperCollider è stata quella di stampare qualcosa nella post window all’onset di ogni evento.

(
b = 120;
t = TempoClock(b/60);
r = Routine({"trigger".postln; // stampa
             0.5.yield;        // aspetta
             "trigger".postln;
             0.5.yield;
             "trigger".postln;
             }).reset.play(t);
)

Ora che abbiamo a disposizione un Synth possiamo aggiungere una seconda azione: fargli suonare una nota. Anche in questo caso possiamo utilizzare la sintassi usata nelle righe 5, 8 e 11 del prossimo riquadro così come è, senza entrare nel merito, ci basti sapere che contiene le istruzioni necessarie per eseguire una singola nota con una altezza una durata e un’intensità predefinite (valori di default) da inviare al Synth definito in precedenza.

(
b = 120;
t = TempoClock(b/60);
r = Routine({"trigger".postln; // stampa
             Synth("sperc");   // suona
             0.5.wait;         // aspetta
             "trigger".postln;
             Synth("sperc");
             0.5.wait;
             "trigger".postln;
             Synth("sperc");
             }).reset.play(t)
)

Se non abbiamo la necessità di monitorare visivamente il trigger degli eventi o altre informazioni riguardanti la computazione, possiamo omettere le righe dove chiediamo a SuperCollider di stampare qualcosa nella post window.

(
b = 120;
t = TempoClock(b/60);
r = Routine({Synth("sperc"); // suona
             0.5.wait;       // aspetta
             Synth("sperc");
             0.5.wait;
             Synth("sperc");
             }).reset.play(t);
)

Usando la sintassi Synth("sperc") in questo punto del codice abbiamo uno strumento polifonico che virtualmente crea e distrugge un Synth a ogni nota, utilizzando una tecnica chiamata dynamic voice allocation che permette di ridurre notevolmente il consumo di cpu del computer. Non essendo però questa tematica propria della sezione corrente, per ora pensiamolo semplicemente come un pianoforte virtuale.

Loop con n.do({})

Nei Paragrafi precedenti dedicati alla programmazione di SuperCollider abbiamo visto come programmare sequenze di eventi specificando esclusivamente un solo valore per i tempi delta oppure scrivendo un solo evento per riga:

// Un solo valore costante
SystemClock.sched(0, {Synth("sperc"); 0.3});

// Un solo valore pseudo-casuale
SystemClock.sched(0,{Synth("sperc"); rrand(0.1,0.5)});

(
Routine.new({Synth("sperc");
	     1.00.wait;
	     Synth("sperc");
	     0.75.wait;
	     Synth("sperc");
	     0.25.wait;
	     Synth("sperc");
	     0.33.wait;
	     Synth("sperc");
	     0.33.wait;
	     Synth("sperc");
	     0.33.wait;
	     Synth("sperc");
	     0.5.wait;
	     Synth("sperc");
	     0.5.wait;
}).play;
)

Con queste due sintassi ci troviamo però di fronte a due distinte problematiche:

  1. Sebbene sotto un punto di vista pratico e percettivo l'uso dei Clock per generare eventi può funzionare, sotto un punto di vista più strettamente legato al linguaggio musicale occidentale, essi infatti dovrebbero essere utilizzati solo per generare il beat generale, il tempo metronomico di un brano, non le sue suddivisioni interne. (vedi il Paragrafo dedicato). Per queste infatti è più corretto o meglio più informaticamente proprio utilizzare Routine, Task o una collezione di oggetti dedicati di SuperCollider chiamata Patterns.

  2. Se utilizziamo Routine però, in base alla conoscenze acquisite finora, dobbiamo scrivere tutte le azioni che compongono la sequenza, azione dopo azione, riga dopo riga e questo porterebbe a scrivere patch della lunghezza pari a quella dei poemi epici. Fortunatamente per ovviare a questo inconveniente in SuperCollider (e praticamente in tutti i software OOP) ci sono degli oggetti che generano dei loop nel codice e che possono essere usati anche all’interno di una Routine. Il più importante per quello che riguarda questo Capitolo segue la sintassi n.do({}).

Partiamo con la più semplice delle sequenze di eventi nel tempo: la successione di pulsazioni regolari. Possiamo definire questo tipo di sequenza con il ripetersi di due azioni che si susseguono uguali per un numero finito o infinito di volte nel tempo (in questo caso la durata dell’evento è sottintesa e coincide con il tempo delta).

(
b = 92;
t = TempoClock(b/60);        // beat
r = Routine({
//------------------------------------------- 1 ripetizione
             Synth("sperc"); // onset evento
             1.wait;         // tempo delta di attesa            
//------------------------------------------- 2 ripetizione
             Synth("sperc");
             1.wait;
//------------------------------------------- 3 ripetizione
             Synth("sperc");
             1.wait;
//------------------------------------------- etc.
             }).reset.play(t);
)

La sintassi n.do({}) permette di abbreviare il codice per operazioni di questo tipo (iterazioni), ed è traducibile in: ripeti n volte la valutazione delle righe contenute nella funzione specificata come argomento. Quando hai finito eventualmente prosegui oltre. Possiamo anche pensare questo tipo di loop come al corrispondente informatico di un ritornello musicale.

(
b = 92;
t = TempoClock(b/60);
r = Routine({
             3.do(                        // ripeti 3 volte
//------------------------------------------- (ritornello)
                   {Synth("sperc");
                    1.wait;}
//------------------------------------------- 
                  )                       // fine loop
           }).reset.play(t);              // fine Routine
)

Dovrebbe ora risultare chiaro il perchè in alcuni paragrafi precedenti per scopi puramente esemplificativi abbiamo sostenuto l’assunto: 1 riga di codice = 1 evento specificando al contempo che questa non è un’affermazione sempre valida. Allontanandoci ora dalle specifiche esigenze di temporizzazione degli eventi musicali vediamo come questo oggetto possa essere utilizzato anche al di fuori delle Routine, in qualsiasi punto del codice. In questi casi l’esecuzione del loop avverrà a velocità massima di scheduling:

10.do({"ciao".postln}) // valuta 10 volte il contenuto della funzione

Eseguendo la riga precedente vedremo stampata dieci volte la parola "ciao" nella post window ad altissima velocità (una parola per riga), seguita dal numero 10 che riporta quante volte la funzione è stata valutata (anche se in questo caso lo abbiamo specificato noi). L’esempio seguente invece illustra come le due situazioni (loop temporizzato all’interno di una Routine o a tempo di scheduling) possano convivere all’interno dello stesso patch e quali caratteristiche musicali differenti possono assumere. Ricordiamo che la computazione del codice di SuperCollider avviene in quasi tutti i casi dall'alto verso il basso e, una volta terminato un loop prosegue alla riga successiva.

(
b = 92;
t = TempoClock(b/60);
r = Routine({

            3.do({                       // ripeti questo 3 volte
                  Synth("sperc");
                  1.wait;
                });

            4.do({                       // poi ripeti questo 4 volte
                  0.5.wait;
                  5.do({Synth("sperc",[\freq,rrand(90,100)]); 
                        0.25.wait         // questo loop
                       });
                  0.125.wait
                  });

             2.wait;                     // aspetta 2 beats

             5.do({Synth("sperc",[\freq,rrand(20,110)])}) // accordo di 5 note...
                                         
           }).reset.play(t);             // fine Routine
)
r.stop;t.clear;                          // per stop prima del tempo

image not found

Notazione musicale con ritornelli (le altezze cambiano in modo pseudo-casuale a ogni esecuzione)

image not found

Notazione musicale estesa (le altezze cambiano in modo pseudo-casuale a ogni esecuzione)

La sintassi n.do({}) genera sequenze finite, che hanno una lunghezza pari al numero di ripetizioni che specifichiamo. Se vogliamo invece generare sequenze infinite possiamo semplicemente sostituire il numero di ripetizioni con la parola riservata (keyword) inf. In questo caso il loop va avanti all’infinito fino a quando non stoppiamo la computazione in uno dei modi che conosciamo.

(
b = 120;
t = TempoClock(b/60);
r = Routine({ inf.do({Synth("sperc"); 0.5.yield}) }).reset.play(t);
)

r.stop;          // stoppa la Routine
r.reset.play(t); // rewind, play
t.clear;         // distrugge l'istanza di TempoClock            

In tutti i casi in cui l’unico argomento di un oggetto è una funzione (come nel caso di Routine({}), Task({}) e n.do({})), le parentesi tonde possono essere omesse.

5.do{"ciao".postln};

Routine{Synth("sperc"); 0.5.wait; Synth("sperc")}.play;

(
b = 120;
t = TempoClock(b/60);
r = Routine{ 6.do{Synth("sperc"); 0.5.yield} }.reset.play(t);
)

Tuttavia fino a quando non saremo più familiari con il linguaggio di SuperCollider è preferibile utilizzare la sintassi tradizionale. Un altro oggetto che possiamo usare per iterare all’infinito una funzione è loop{} (oppure {}.loop).

(
b = 120;
t = TempoClock(b/60);
r = Routine({
             loop{Synth("sperc"); 0.5.yield} 
             });
r.reset.play(t);
)

r.stop;

// Altra scrittura...
(
b = 120;
t = TempoClock(b/60);
r = Routine({
            {Synth("sperc"); 0.5.yield}.loop 
            });
r.reset.play(t);
)

r.stop;

Quando generiamo loop infiniti con una qualsiasi delle sintassi appena illustrate, dobbiamo obbligatoriamente specificare un tempo delta con i metodi .wait o .yield in quanto se omettiamo queste "pause" tra valutazioni successive della funzione, SuperCollider le esegue il più veloce possibile e all’infinito, andando in crash.

Tutto ciò che è stato appena esposto funziona nella realizzazione di sequenze di eventi che seguono pulsazioni regolari, ma se volessimo generare sequenze con ritmi diversi senza scrivere 1 evento per riga di codice? Dovremmo utilizzare delle collezioni di tempi delta differenti rappresentate sotto forma di liste o Array.

Array

Un Array è una sequenza di valori che può essere indicizzata con numeri interi, come in un piano cartesiano (xy). I tempi delta che descrivono una sequenza ritmica sono valori e possono dunque essere indicizzati come illustrato nella parte infieriore della Figura seguente:

image not found

In questo caso si parla di sequenza numerica bidimensionale (2D).

Valori = y = 1.00, 0.75, 0.25, 0.33, 0.33, 0.33, 0.50, 0.50
Indici = x = 0 ,   1 ,   2 ,   3 ,   4 ,   5 ,   6 ,   7

Questo tipo di sequenze in SuperCollider sono ottimizzate in una superClasse di oggetti chiamata SequenceableCollection. Gli indici x sono sottintesi e partono sempre da 0 mentre i valori o gli oggetti che compongono la collezione (chiamati items) sono inclusi tra parentesi quadre e separati tra loro da una virgola. La super-Classe SequenceableCollection contiene numerose Classi. Le due più utilizzate sono:

Qalsiasi tipo di data può essere collezionato in un Array o in una List (int, float, Synth, Envelope, midi, etc.) così come illustrato nel riquadro seguente:

[60,63,78,98,12]                // sequenza di midi note
[0.5,0.25,0.25,1]               // sequenza di tempi delta
[12,67,89,127,45]               // sequenza di velocity
[440,569,890,987]               // insieme di frequenze
[0.2,0.4,0.8,0.2]               // insieme di ampiezze
[SinOsc.ar(440),SinOsc.ar(441)] // insieme di segnali
[rand(10),rand(0.1),rand(1000)] // insieme di funzioni

Attenzione, in realtà le prime cinque righe contengono solo numeri int e float compresi in ambiti che possono essere mappati su parametri corrispondenti alle unità di misura indicate (midi note, tempi delta, etc.).

Se includiamo una collezione di oggetti tra due parentesi quadre SuperColllider la interpreta automaticamente come un Array, mentre se vogliamo la interpreti come List dobbiamo specificarlo utilizzando una sintassi che apprenderemo più avanti. Sugli elementi che sono all’interno di un Array (items) possiamo effettuare numerose operazioni, manipolazioni, sostituzioni di posizione, di data, e altre diavolerie, ma per gli scopi di questo Capitolo ci interessa solo un particolare tipo di Array chiamato Literal array. La sua caratteristica è che non può essere modificato e che come tipo di data fa parte dei Literals già illustrati in precedenza. E’ caratterizzato dal simbolo # prima della parentesi di apertura.

#[1,0.75,0.25,0.33,0.33,0.33, 0.5,0.5];

L’aver illustrato le caratteristiche principali di List, Array e Literal Array ci permette di osservare una problematica tipica dei software musicali (e non solo) ovvero il dover spesso scegiere tra più oggetti che apparentemente hanno le stesse funzioni (come in questo caso collezionare e indicizzare data). Solitamente dobbiamo effettuare questa scelta tenendo conto dell’assunto: versatilità vs costo computazionale o meglio scegliere in base alle nostre esigenze di programmazione quello che ha un minor costo computazionale. Prendiamo ad esempio le tre Classi di cui sopra.

Per collezioni di valori numerici la differenza di costo computazionale è solitamente irrisoria ma per collezioni di segnali audio, inviluppi o altri tipi di data può essere rilevante e dunque da tenere in considerazione.

Visualizzazione

Quando un Array contiene valori numerici, possiamo visualizzarli graficamente su un piano cartesiano utilizzando gli indici (sottintesi) come ascisse (x) e i valori (items) come ordinate (y). Per farlo abbiamo a disposizione due possibili sintassi: la prima utilizza il metodo .plot() invocato direttamente sull'Array mentre la seconda la Classe Plotter.

Plotting

Se vogliamo generare una visualizzazione grafica (Plotter) dei dati contenuti in un Array a ogni valutazione, possiamo farlo invocando su di esso il metodo .plot(). Apparirà una nuova finestra come quella illustrata nella Figura seguente e la scritta a Plotter sarà stampata nella Post window.

image not found

#[12,34,56,3,78,98,23,9].plot;  // valori determinati
Array.rand(100, -1.0,2.5).plot; // valori pseudo-casuali tra -1 e 2.5

Questo metodo come molti altri può essere invocato su diversi tipi di oggetti (polimorfismo) , tra i quali gli Array. Per sapere quali sono dobbiamo selezioniarlo e schiacciare cmd+d, come quando richiamiamo un Help file. Se però eseguiamo questa operazione sui metodi, nell’Help browser non compare un Help file ma un elenco di oggetti sui quali possono appunto essere invocati.

image not found

L’aspetto grafico di un Plotter è personalizzabile attraverso gli argomenti di .plot(). Facciamo attenzione che a seconda del tipo di data sul quale è invocato, gli argomenti di questo metodo cambiano. Nell’esempio seguente sono presenti quelli spendibili nella visualizzazione di Array numerici.

image not found

(
a = #[12,34,56,3,78,98,23,9];
a.plot(name: "mio plot", // nome 
       bounds:540@200,   // dimensioni x@y in pixels
       minval: -100,     // valore limite inferiore
       maxval: 100,      // valore limite superiore
       discrete:true)    // true = punti, false = linee
) 

Fra questi è sempre raccomandabile specificare minval e maxval perchè di default i limiti si "adattano" ai valori contenuti nell’Array e questo potrebbe generare errori di lettura e valutazione. Nell’esempio seguente la visualizzazione dei valori può trarre in inganno (nell’Array in alto sono compresi tra 0.1 e 1.0 mentre in quello in basso tra 1 e 10).

image not found

(
[#[0.1,0.5,0.6,0.1,0.9,0.3],
 #[1,5,2,4,8,10,5,6]].plot;
)

Specificando i limiti invece la visualizzazione sarà decisamente più corrispondente alla realtà e più leggibile:

image not found

(
[#[0.1,0.5,0.6,0.1,0.9,0.3],
 #[1,5,2,4,8,10,5,6]]..plot(minval:0,maxval:10);
)

Plotter

Se invece vogliamo creare una sola finestra, all’interno della quale visualizzare e cancellare a ogni esecuzione diversi valori possiamo utilizzare direttamente la Classe Plotter.

Plotter.new;
a = Plotter.new("segmenti",300@200); 
a.value = #[23,12,45,32,65]; 
a.value_(#[1,2,3]);  
a.value = #[10,2,33,41,50,23,45,56];          

Per farlo dobbiamo compiere due distinte operazioni.

  1. creare una finestra vuota o meglio un’istanza della Classe Plotter (riga 1 e 2 del codice precedente):

    image not found

    Ricordiamo che il metodo .new può essere sottinteso. Gli argomenti sono gli stessi che abbiamo visto per il metodo .plot().

  2. riempire la finestra specificando i valori y con il metodo che già conosciamo .value = [items] (riga 3 del codice precedente):

    image not found

    Questa seconda operazione possiamo effettuarla quante volte vogliamo. Il numero di items può variare a piacere e i limiti della finestra si adattano automaticamente ai nuovi valori. Notiamo la differenza rispetto all’utilizzo di questo metodo quando è assegnato a una funzione:

    // Funzioni
    a = {arg a,b; a+b};
    a.value(3,6);
    
    // Plotter
    a = Plotter("segmenti",300@200);
    a.value = #[23,12,45,32,65];
    a.value_( #[1,2,3] );      
    

    Come abbiamo già visto nel Paragrafo dedicato a TempoClock le sintassi usate nelle ultime due righe si equivalgono e possiamo utilizzarne una o l’altra a nostro piacimento.

I due modi appena esposti di visualizzare i valori di un Array si equivalgono in quanto entrambi generano un’istanza della Classe Plotter. Il primo chiede a SuperCollider di farlo per noi attraverso il metodo .plot(). Il secondo ci permette di farlo noi direttamente sottointendendo il metodo .new.

Una volta generata una finestra grafica contenente un Plotter possiamo modificare il suo aspetto grafico anche con il metodo .setProperties():

image not found

(
a = Plotter("segmenti",300@200);
a.value = #[23,12,45,32,65];
a.setProperties(
    \backgroundColor, Color.black,  // sfondo
    \plotColor, Color.yellow,       // data
    \fontColor, Color.magenta,      // numeri
    \gridColorX, Color.white,       // griglia x
    \gridColorY, Color.white,       // griglia Y
                );
)          

Una lista esaustiva di tutte le proprietà grafiche che possiamo modificare la troviamo richiamando l’Help file di questo metodo. Infine possiamo interagire con un Plotter attraverso alcune abbreviazioni da tastiera. Selezioniamo la finestra con un click.

Lettura

Abbiamo visto come qualsiasi tipo di sequenza ritmica (e non solo) può essere rappresentata in un Array. Questi però soto un punto di vista informatico sono collezioni di data e non hanno nulla a che fare con il tempo. Leggere un Array che descrive una sequenza di valori musicali è come leggere una partitura tradizionale senza suonarla. Il parametro temporale entra in gioco solo nel momento dell’esecuzione, nota dopo nota. Anche per gli Array dobbiamo dunque avere la possibilità di richiamare a nostro piacere nel tempo ogni singolo item. Possiamo farlo grazie al fatto che sono collezioni indicizzate (id, item) e ottenere di volta in volta ogni singolo elemento (item) richiamandone l’indice sottinteso (id) attraverso diversi metodi.

[].at()

Il primo e il più utilizzato è [].at().

a = #[1, 0.75, 0.25, 0.33, 0.33, 0.33, 0.5, 0.5]; // items
//    0  1     2     3     4     5     6    7     // indici sottointesi
a.at(0); // 1
a.at(1); // 0.75
a.at(2); // 0.25
a.at(3); // 0.33 etc....

Osserviamo come sia uso comune assegnare un Array a una variabile per poi richiamare i singoli items specificandone l’indice sottointeso come argomento del metodo [].at() in valutazioni successive. Il primo indice è sempre 0. Possiamo anche utilizzare un’abbreviazione sintattica che corrisponde alla notazione adottata nel linguaggio Java, anche se personalmente (tranne che in alcuni casi particolari) preferisco la notazione tradizionale.

a = #[1, 0.75, 0.25, 0.33, 0.33, 0.33, 0.5, 0.5]; // items
//    0  1     2     3     4     5     6    7     // indici
a[0]; // 1
a[1]; // 0.75
a[2]; // 0.25
a[3]; // 0.33 etc....

Un’informazione che come vedremo può tornare utile in numerose occasioni è conoscere il numero di items contenuti in un Array e la possiamo ottenere invocando il metodo .size.

a = #[1, 0.75, 0.25, 0.33, 0.33, 0.33, 0.5, 0.5];
a.size; // --> 8

Se in assenza di quest’ultima informazione e per qualsiasi motivo dovesse essere richiamato un indice maggiore della lunghezza dell’Array (size-1 in quanto il primo indice è 0) o minore di 0, SuperCollider riporta nil come dimostrato nell’esempio seguente.

(
var a = #[1, 0.75, 0.25, 0.33, 0.33, 0.33, 0.5, 0.5], // items
//        0  1     2     3     4     5     6    7     // indici
    id   = rrand(-5,10),                   // valori tra -5 e 10
    item = a.at(id);                       // richiama
[id, item]                                 // stampa
)

Per ovviare a quanto appena esposto in questo caso possiamo utilizzare o il metodo [].size come argomento di rand(),

(
var a = #[1, 0.75, 0.25, 0.33, 0.33, 0.33, 0.5, 0.5], 
    id   = rand(a.size), // valori tra 0 e 7 (size-1)
    item = a.at(id);                       
[id, item]                                
)

oppure altri tre metodi che sono parenti stretti di [].at().

[].clipAt()

Se per qualsiasi motivo il numero che richiama gli indici è superiore al size o inferiore a 0, riporta l’ultimo item sia oltre il size che per numeri negativi:

(
var a = #[1, 0.75, 0.25, 0.33, 0.33, 0.33, 0.5, 0.5], // items
//        0  1     2     3     4     5     6    7     // indici
    id   = rrand(-5,10),
    item = a.clipAt(id);
[id, item]
)

[].wrapAt()

Se per qualsiasi motivo il numero che richiama gli indici è superiore al size torna all’indice 0 e continua a contare. Come in un loop. Nell’esempio seguente se rand(15) genera 10 riporta l’elemento che è all’indice 2 (10 - 8 che è il size), se invece esce 14 quello a indice 6 (14 - 8)e così via. Funziona anche con indici negativi.

(
var a = #[1, 0.75, 0.25, 0.33, 0.33, 0.33, 0.5, 0.5], // items
//        0  1     2     3     4     5     6    7     // indici
    id   = rand(15),
    item = a.wrapAt(id);
[id, item]
)

[].foldAt()

Se per qualsiasi motivo il numero che richiama gli indici è superiore al size o inferiore a 0 continua la conta in modo speculare. Nell’esempio seguente se esce 13 riporta l’elemento all’indice 1, se invece esce 9 quello all’indice 5. Funziona anche con indici negativi.

(
var a = #[1, 0.75, 0.25, 0.33, 0.33, 0.33, 0.5, 0.5], // items
//        0  1     2     3     4     5     6    7     // indici
    id   = rrand(-5,10),
    item = a.foldAt(id);
[id, item]
)

Questi metodi non hanno esclusivamente una valenza informatica ma anche musicale. Infatti come vedremo in seguito potremo utilizzarli per generare e controllare alcuni tipi di variazioni procedurali.

Sequencing

Così come abbiamo visto parlando di scheduling, possiamo richiamare gli items di un Array "a mano" valutando di volta in volta le singole righe di codice, oppure automatizzare il processo sfruttando una caratteristica non ancora illustrata del metodo n.do({}). Sappiamo che questo metodo implica la presenza di una funzione e che all’interno di questa possiamo specificare degli argomenti. Questi però quando la funzione è essa stessa argomento di un metodo (come in questo caso o come quando invochiamo .sched su di un Clock) non sono utilizzati come indirizzi ai quali inviare valori dall’esterno ma per recuperare automaticamente informazioni istantanee sullo stato delll'oggetto. Nello specifico di n.do({}) se scriviamo un solo argomento, indipendentemente dal nome che gli diamo, assume valenza di contatore riportando a ogni valutazione della funzione interna il numero di volte che è stata già valutata, partendo da 0. Come ultimo valore riporta come sappiamo già il numero totale di valutazioni effettuate;

(
10.do{
      arg ciao;   // contatore    
      ciao.postln // stampa
      }
)

Osserviamo la Post window dopo aver valutato il codice: vedremo stampato un numero per riga da 0 a 9 (tutti i passi del loop) e infine 10 che corrisponde al numero totale di valutazioni. Se sostituiamo ora il numero di iterazioni (in questo esempio 10) con un Array, gli argomenti che possiamo specificare sono due e corrispondono a due distinte informazioni:

(
#["a","b","c"].do{arg ciao, miao;
	             [ciao, miao].postln}
)

L’ultima informazione in questo caso non è il numero totale di valutazioni ma l’Array sul quale abbiamo invocato il metodo. Sebbene l’ordine nella dichiarazione degli argomenti sia prefissato e invariabile (item, contatore), le due informazioni sono indipendenti e legate al nome che gli diamo. Se infatti volessimo postare prima il contatore e dopo l’item potremmo invertire i nomi delle loro etichette nel comando di stampa.

(
#["a","b","c"].do{arg ciao, miao;
	             [miao, ciao].postln} // nomi invertiti
)

Possiamo anche sostituire i due nomi con la parola riservata ...args che abbiamo già incontrato parlando di TempoClock, ma in questo caso l’ordine non è modificabile.

(
a = #["a","b","c"];
a.do{arg ...args;       // argomenti: item, id
            args.postln // stampa
    }
)

Se invece specifichiamo un solo argomento, otterremo semplicemente un iterazione degli items.

(
a = #["a","b","c"];
a.do{arg biro;       // 1 argomento: item
         biro.postln
    }
)

Le sintassi appena esposte sono funzionali alla lettura completa da sinistra a destra di un Array. Se però volessimo leggerlo in modo parziale o randomico oppure utilizzare i metodi [].clipAt(), [].wrapAt() o [].foldAt() non potremmo farlo in quanto una volta terminata l’iterazione il loop si ferma. Per questo motivo torna molto utile l’argomento che riporta numero di valutazioni della funzione (contatore), che possiamo utilizzare come argomento di [].at() o dei suoi simili:

(
a = #["a","b","c"];    // Array da iterare
n = 10;                // numero di iterazioni (da 0 a 9)
n.do{arg biro;         // riporta il numero di iterazione a ogni passo
     a.at(biro).postln // richiama gli items dagli indici
    }
)

Valutando il codice precedente dopo i tre items contenuti nell’Array vedremo stampati nella Post window sette nil e infine il numero 10. In questo modo possiamo utilizzare anche gli altri metodi:

(
a = #["a","b","c","d","e"];
n = 10;
3.do{arg biro;
     a.at(biro).postln};
"-----------".postln;
n.do{arg biro;
     a.clipAt(biro).postln};
"-----------".postln;
n.do{arg biro;
     a.wrapAt(biro).postln};
"-----------".postln;
n.do{arg biro;
     a.foldAt(biro).postln};
)

Per realizzare un sequencing possiamo temporizzare i processi di lettura di un Array includendoli in Routine e Task. Di seguito alcuni esempi:

// Richiamando direttamente gli items.
(
var bpm = 100,                                   // bpm
    t   = TempoClock(bpm/60),                    // metronomo
    dt  = #[1,0.75,0.25,0.33,0.33,0.33,0.5,0.5], // tempi delta
    r   = Routine({
                   dt.do({arg delta;             // items                        
                          Synth("sperc");        // suona
                          delta.postln;          // stampa
                          delta.wait})           // aspetta
                  });
r.play(t)
)

// Richiamando gli indici con '[].wrapAt(id)' e simili.
(
var bpm = 120,
    n   = 20,                                     // numero di eventi
    t   = TempoClock(bpm/60),
    dt  = #[1,0.75,0.25,0.33,0.33,0.33,0.5,0.5],
    r   = Routine({
                   n.do({arg id;                   // indici
                         var delta = dt.wrapAt(id);// itera gli items                        
                         Synth("sperc");           // suona
                         [id,delta].postln;        // stampa
                         delta.wait})              // aspetta
                  });
r.play(t)
)

Come possiamo osservare valutando questo secondo esempio l’argomento di n.do({}) non riporta gli indici dell’Array ma il numero di valutazioni della funzione. Non confondiamoci.

Con questa tecnica possiamo infine sincronizzare tra loro diversi parametri, ognuno memorizzato in un Array fifferente:

(
var bpm,t,tsec,note,vels,durs,dt,r;
bpm  = 150;
t    = TempoClock(bpm/60);
tsec = 60/bpm;                
note = [90,      100,  98,   97,    96, 93].midicps;
vels = [60,       90, 120,   60,    40, 10]/127;
durs = [0.125, 0.125, 0.5, 0.25, 0.125,  1]*tsec;
dt   = [ 0.25,  0.25,   1,    1,     1,  1];

r = Routine({dt.do{arg delta, id;

                   Synth("sperc",[\freq, note.at(id), // frequenze richiamate come indici
                                  \amp,  vels.at(id), // ampiezze richiamate come indici
                                  \dur,  durs.at(id)  // durate richiamate come indici
                                 ]
                        );
                   delta.wait}                        // tempi delta richiamati come items
             }).reset.play(t);
)

Nel prossimo Capitolo (Esercizi 4) entreremo nel dettaglio del codice precedente.