Sei sulla pagina 1di 142

SuperCollider

by Paolo Olocco

Personali traduzione dei tutorial Gettin-Started ed altri presenti nella


documentazione di SuperCollider con approfondimenti e riferimenti. Il
materiale presentato può essere un valido aiuto per iniziare a lavorare
con Supercollider.
Per approfondimenti e chiarimenti si rimanda il lettore sia ai file di
help originali (presenti nella directory /help/Tutorial/Gettin-Started
della cartella di SuperCollider), sia al forum.
Per errori o commenti: olocco.paolo@gmail.com
Indice
1 Primi Passi 4
1.1 Hello World, sono SuperCollider 4
1.2 Il mondo dentro SuperCollider 6

2 Accendiamo i motori 8

3 Funzioni e Funzionalità 10

4 Funzioni e Suoni 16
4.1 Divertiamoci con Funzioni e UGens 19

5 Living Stereo 21

6 Creiamo un Mix 24

7 Scoping and Plots 26


7.1 Scoping su richiesta 27
7.2 Local vs. Internal 28

8 Help di Supercollider 29
8.1 Classi and Metodi 29
8.2 Shortcut sintattici 30
8.3 Snooping, etc. 30

9 SuperCollider 3 Synth Server: architettura 31


9.1 Introduzione 31
9.2 Principali componenti 31

10 SynthDefs e Synths 35
10.1 SynthDef 35
10.2 SynthDefs vs. Functions 36
10.3 SynthDefs ‘variabili’ 39
10.4 Synth 40
10.5 Alcune nozioni su Symbols, Strings, SynthDef e Arg Names 40

11 I Buss 42
11.1 Buss: scrittura e lettura 43
11.2 Creiamo un Bus Object 44
11.3 Buss in azione! 46
11.4 Divertiamoci con i Control Buss 49
11.5 L’ordine è una cosa importante! 50

1
12 I Gruppi 53
12.1 Gruppi come tool per l’ordinamento 53
12.2 Tutte le addAction 55
12.3 ’queryAllNodes’e node IDs 55
12.4 Il Root Node 56
12.5 Il default Group 57
12.6 Gruppi come tool per l’invio di insiemi di messaggi 59
12.7 Gruppi: ereditarietà e non solo 59

13 Buffer 61
13.1 Creazione di un Buffer Object e allocazione di memoria 61
13.2 Uso dei Buffers con Sound Files 62
13.3 Streaming di File da e su Disco 63
13.4 OOP: Variabili di istanza e Funzioni azione 64
13.5 Registrare nel Buffer 65
13.6 Accesso ai dati 66
13.7 Plotting e Playing 68
13.8 Altri usi dei Buffers 68

14 La comunicazione 70
14.1 Impacchettamento automatico dei messaggi 72

15 Ordine di esecuzione 74
15.1 Richiami su Server e Target 74
15.2 Controllo dell’ordine di esecuzione 75
15.3 Utilizzare l’ordine di esecuzione a proprio vantaggio 77
15.4 Stile Messaggio 80
15.5 Un caso particolare: Feedback 80
15.6 Espansione multicanale 82
15.7 Protezione di array dall’espansione 84
15.8 Ridurre l’espansione multicanale con Mix 85
15.9 Uso di flop per l’espansione multicanale 86

16 Just In Time Programming 88


16.1 Overview delle differenti classi e tecniche 88
16.2 Placeholder in supercollider 89
16.3 Proxy Space: concetti base 93
16.4 Struttura interna di un node Proxy 100
16.4.1 NodeProxy slots 100
16.4.2 fade time 101
16.4.3 play/stop, send/free, pause/resume 102
16.4.4 Il parametro di contesto 105

2
16.5 Timing in NodeProxy 106

17 Esecuzione sincrona e asincrona 110


17.1 Scheduler 117
17.2 OSCSched 120
17.3 Task 124
17.4 Tempo 125
17.5 Routine 127
17.5.1 Variabili di istanza accessibili 129

18 Scrivere classi in SC 132


18.1 Oggetti e Messaggi 132
18.2 Classi, Variabili di instanza e metodi 133
18.2.1 Classi 133
18.2.2 Ereditarietà 133
18.2.3 Variabili di istanza 134
18.2.4 Variabili di Classe 136
18.2.5 Metodi di istanza 136
18.2.6 Metodi di classe 137
18.3 Creazione di una nuova istanza 138
18.4 Overriding dei metodi (Overloading) 139
18.5 Metodi definiti in file esterni 139
18.6 Tricks and Traps 140

19 Glossario 141

3
Primi Passi

Capitolo 1 Primi Passi

1.1 Hello World, sono SuperCollider


È tradizione quando ci si appresta ad imparare un nuovo linguaggio di programma-
zione partire con un semplice programma chiamato “Hello World”; ciò che fa questo
programma è stampare il testo “Hello World!” nella finestra di output chiamata, in
SuperCollider, post window che si apre all’avvio del programma, e in cui compaiono
una serie di righe di testo del tipo:

1 init_OSC
2 compiling class library..
3 NumPrimitives = 587
4 compiling dir: ’/Applications/SC3/SCClassLibrary’
5 pass 1 done
6 Method Table Size 3764776 bytes
7 Number of Method Selectors 3184
8 Number of Classes 1814
9 Number of Symbols 7595
10 Byte Code Size 180973
11 compiled 296 files in 1.34 seconds
12 compile done
13 RESULT = 256
14 Class tree inited in 0.14 seconds

Non è il caso di preoccuparsi molto ora del significato inerente questo testo, basta
tenere a mente che questa finestra è l’interfaccia in cui SC invierà le informazioni. È
anche il posto in cui si otterrà il risultato del nostro programma Hello World listato
qui sotto:

1 "Hello World!".postln;

Per eseguire il programma, occorre semplicemente posizionare il cursore sulla stessa


riga di codice in cui compare il codice e premere enter (e non return). Il tasto ‘enter’
è quello sul blocco dei numeri della tastiera. Sui laptops Mac si preme ‘return’ con
‘fn’.
Eseguendo l’esempio si potrà vedere nella post window:

1 Hello World!
2 Hello World!

4
Primi Passi

Ora guardiamo un più da vicino il codice.


La prima parte, “Hello World!”, è un tipo di Object , chiamato Stringa. Un oggetto
è semplicemente un modo di rappresentare qualcosa nel computer, per esempio un
testo, oppure un oscillatore che permette di controllare e inviare messaggi. Di tutto
questo ce ne occuperemo più avanti, ma ai fini di questo capitolo introduttivo, è
sufficiente capire che una Stringa è un modo di rappresentare una quantità di testo.
La seconda parte, .postln;, significa ‘stampami nella post window’. Questo metodo
postln, è il miglior amico del programmatore in SC. Può essere applicato a quasi
tutto in SC e restituisce informazioni rilevanti. Può essere veramente utile quando si
cerca un bug nel codice. Osservano il risultato ottenuto viene spostaneo domandarsi
perchè viene stampato due volte il risultato.
Quando si esegue codice in SC, viene sempre postata l’ultima istruzione eseguita; in
questo caso non occorreva la postln. Ma da qualche parte bisognava pur cominciare,
no?! Nell’esempio seguente selezioniamo entrambe le linee di testo cliccandoci sopra
e premiamo enter.

1 "Hello World!".postln;
2 "Hello SC!".postln;

La prima riga, “Hello World” non verrebbe stampata se non avesse la postln espli-
citata. Da notare inoltre che ogni linea di codice termina con un punto e virgola.
Questo del punto e virgola è il metodo in SC per separare le linee di codice. Se non
comparisse un punto e virgola fra due righe, si potrebbe incorrere in errore.

In generale quando si intende eseguire diverse linee di codice allo stesso tempo, è
necessario racchiudere il codice fra parentesi tonde, come l’esempio seguente. Que-
sto approccio è conveniente perchè permette di selezionare l’intero blocco di codice
semplicemente facendo doppio click dentro le parentesi. Si provi con il seguente
esempio:

1 (
2 "Call me,".postln;
3 "Ishmael.".postln;
4 )

Da notare che tutte le linee dentro il blocco di codice terminano con un punto e
virgola. La cosa importantissima quando si eseguono più linee di codice, è saper
come SC capisce dove separare i comandi. Senza i punti e virgola, si otterrebbe un
errore.

5
Primi Passi

1 (
2 "Call me?".postln
3 "Ishmael.".postln;
4 )

Eseguendo infatti il codice sopra riportato, si ottiene un Parse Error. Con un errore
di questo tipo, il ě nel messaggio di errore mostra dove SC rileva un errore. Qui
succede appena dopo “Ishmael.”.

1 ě ERROR: Parse error


2 in file ’selected text’
3 line 3 char 11 :
4 "Ishmael."ě.postln;

Usare il punto e virgola permette infine di avere più di una linea di codice nella
stessa linea di testo. Questo può essere comodo per l’esecuzione:

1 "Call me ".post; "Ishmael?".postln;

Vediamo ancora un paio di nozioni in più sulla post window.


È molto utile essere in grado di averla a disposizione e vederla, ma può essere
altrettanto utile nasconderla dietro le altre finestre. Può essere portata in avanti in
ogni momento premendo il tasto command e premendo /.
Per convenzione questo tipo di sequenza sarà scritto con Cmd-/.
Per ripulire la post window occorre premere Cmd-shift-k.

1.2 Il mondo dentro SuperCollider


SuperCollider è sostanzialmente formato da 2 programmi:

- il linguaggio o client app, che è ciò che stiamo vedendo ora

- il server o server app , che computa sintesi e calcoli sull’audio.

Il primo è un’applicazione grafica con menù, document window, divertenti GUI e


un sofisticato linguaggio di programmazione e può essere un’applicazione a riga di
comandi UNIX efficiente e agile (se non si utilizzano le divertenti GUI).

Le 2 applicazioni comunicano con un protocollo chiamato Open Sound Control


(OSC), e può essere impiegato sia sul protocollo di rete UDP che TCP. Non venga

6
Primi Passi

da pensare che le due applicazioni debbano per forza eseguire su due computer dif-
ferenti (è comunque possibile e può portare a numerosi vantaggi di performance) e
che essi siano connessi a internet (anche se è possibile avere clients e servers in parti
differenti del mondo!). La maggior parte delle volte gireranno sulla stessa macchina,
e la ‘rete’ sarà resa trasparente all’utente finale.

L’unico metodo di comunicazione client-server supportato è quello basato sull’utilizzo


di messaggi OSC, e fortunatamente il linguaggio di Supercollider dispone di alcuni
oggetti potenti che rappresentano diverse entità sul server e permettono di control-
larle facilmente ed elegantemente. Capire esattamente come lavorano è cruciale per
dominare SC, così ne parleremo a fondo.

Ma prima divertiamoci un po’e creiamo qualche suono.

Per maggiori informazioni:

[How-to-Use-the-Interpreter] [Literals] [String] [ClientVsServer] [Server-Architecture]

7
Accendiamo i motori

Capitolo 2 Accendiamo i motori


Prima di poter creare qualunque suono è necessario far partire o, in maniera più spe-
cifica, effettuare il boot dell’applicazione server. Il modo più semplice per effettuare
questa operazione è utilizzare una delle server windows che si creano automatica-
mente all’avvio dell’app del client e che vengono visualizzate di solito in basso a
sinistra nello schermo. La finestra del server localhost si presenta in questo modo:

Fig. 2.1 Server localhost window

Il termine localhost significa semplicemente sul proprio computer, come opposto


all’esecuzione su un altro computer connesso alla rete. Per avviare il server, occorre
cliccare sul bottone Boot o cliccare sulla finestra e premere spazio. Dopo un secondo
o due dovrebbe comparire qualcosa di simile a questo:

Fig. 2.2 Server localhost window

Da notare che il nome è diventato rosso e che il bottone Boot è cambiato in Quit.
Questo indica che il server è passato in stato running. Questa finestra offre inoltre
alcune informazioni sul server:

− Avg CPU : carico medio della CPU espresso in percentuale

− Peak CPU : picco di carico della CPU espresso in percentuale

− UGens : numero di UGen correntemente utilizzate

− Synths : numero di synth correntemente istanziati (capitolo 10)

8
Accendiamo i motori

− Goups : numero di group correntemente istanziati. All’avvio sono 2, il root node


e il default group. (capitolo 12)

− SynthDefs : numero di SynthDef conosciute dal server.(capitolo 10)

Osservando la post window in seguito al boot del server, vediamo che SC ha generato
alcune informazioni sull’esito positivo del boot:

1 booting 57110
2 SC_AudioDriver: numSamples=512, sampleRate=44100.000000
3 start UseSeparateIO?: 0
4 PublishPortToRendezvous 0 57110
5 SuperCollider 3 server ready..
6 notification is on

Nel caso in cui, per qualche motivo, il boot fallisca, potrebbero essere riportare
alcune informazioni che ne indicano il fallimento e la motivazione. Di default, ci
si può riferire al server localhost usando la lettera s. È possibile quindi mandare
messaggi di start e stop al server come i seguenti:

1 s.quit;
2 s.boot;

Proviamo a lasciare il server in esecuzione per ora. Molti esempi nella documenta-
zione di SC hanno l’istruzione s.boot all’inizio, ma in generale è bene assicurarsi che
il server sia in stato running prima di utilizzare qualunque esempio che genera audio
o che accede a funzioni del server. In generale per gli esempi in questo tutorial si
assume che il server sia in stato running. Ci si può anche riferire al server localhost
con il testo Server.local, per esempio:

1 Server.local.boot;

Per maggiori informazioni:

[Server]

9
Funzioni e Funzionalità

Capitolo 3 Funzioni e Funzionalità


Il modo più semplice per generare suoni in SC è utilizzare una Function. Di seguito
ne viene mostrato un semplice esempio. Si provi ad eseguire il codice (dopo essersi
accertati che il server sia in stato running), e dopo poco si prema Cmd-. ( tener
premuto il tasto command e premere la barra spaziatrice) per fermare il suono.
Questo comando fermerà sempre tutti i suoni in SC. Verrà usato parecchio in futuro
per permettere di ricordalo.

1 { [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)] }.play;

Non è troppo ispirato il suono generato da quest’esempio!? Non ci si preoccupi trop-


po, siamo all’inizio e questo è solo un semplice esempio per dimostrare le Functions
e il suono. Lo riprenderemo dopo con calma.

Prima di tutto impariamo qualche nozione generale sulle Functions.


Una Function è soltanto una parte di codice riutilizzabile. Si definisce racchiudendo
il codice fra parentesi graffe. Per esempio:

1 f = { "Function evaluated".postln; };

Ciò che viene inserito fra le parentesi graffe è ciò che sarà eseguito ogni volta che la
Function sarà riutilizzata o rivalutata. Da notare che è definita come un’equazione, f
=..., anche se non lo è nel vero senso matematico. Questa operazione è ciò che viene
detto essere un assegnamento. Di base un assegnamento permette di nominare la
Function creata salvandola in una variabile f. Una variabile è solo un nome che rap-
presenta uno slot in cui possiamo salvare qualcosa, come una Function, un numero,
una lista, ecc. Eseguendo le seguenti linee di codice una alla volta e osservando la
post window,

1 f = { "Function evaluated".postln; };
2 f;

vedremo che entrambe le volte verrà scritto a Function. Quindi, se vorremo riferirci
alla Function in futuro dopo che è stata assegnata, è possibile usare la lettera f. Ecco
cosa s’intende per riutilizzabile!! Se non fosse possibile assegnarla, sarebbe necessario
riscrivere ogni volta la Function stessa.

10
Funzioni e Funzionalità

Come è quindi possibile riusare una Function? Si eseguano le seguenti linee una alla
volta e si osservi la post window:

1 f = { "Function evaluated".postln; };
2 f.value;
3 f.value;
4 f.value;

La Function è un oggetto (un’entità che rappresenta qualcosa e ha funzionalità spe-


cifiche), che abbiamo definito e salvato nella variabile f. La parte di codice .value
valuta la funzione in quel momento e ne restituise il valore.
Questa operazione è un esempio di invio di un messaggio a un oggetto. Segue la
sintassi someObject.someMessage separati da un punto.

Per un dato oggetto, ogni invocazione message significa esecuzione di un particola-


re metodo. Tipi differenti di oggetti potrebbero avere metodi con lo stesso nome e
quindi rispondere allo stesso messaggio in modi differenti. Data l’importanza, evi-
denziamo il concetto appena espresso:

Tipi differenti di oggetti potrebbero avere metodi con lo stesso nome,


e quindi rispondere allo stesso messaggio in modi differenti.

La cosa interessante è che i metodi potrebbero differire nell’implementazione ef-


fettiva, anche se hanno lo stesso nome, rendendo intercambiabile nel codice il loro
utilizzo.

Un buon esempio di questo principio è proprio il metodo value. Tutti gli oggetti
in SC rispondono a questo messaggio. In generale, quando si richiama un metodo,
questo ritornerà sempre qualcosa come un valore o un risultato. Quando si invoca il
metodo value su una Function, quest’ultima verrà valutata e ritornerà il risultato
dell’ultima riga di codice della Function. Si veda l’esempio seguente: ritornerà un
valore uguale a 5.

1 f = { "Evaluating...".postln; 2 + 3; };
2 f.value;

Più spesso i metodi restituiscono l’oggetto stesso a cui fanno riferimento (in special
modo nel caso di molti oggetti con il messaggio value). Si consideri l’esempio se-
guente (Tutto ciò che è a destra di due backslash // è un commento che significa
semplicemente che SC ignorerà ciò che viene scritto durante il parse e l’escuzione).

11
Funzioni e Funzionalità

1 f = 3; // f viene inizializzata a un numero


2 f.value; // nella Post window viene stampato 3
3 f.value; // viene di nuovo stampato 3

5 f = { 3.0.rand; }; // f viene inizializzata a un numero random


6 f.value; //
7 f.value; // stampa 3 valori differenti
8 f.value; //

L’uso del metodo value di Functions permette agli altri oggetti di essere inter-
cambiabili nel codice. Questo è un esempio di polimorfismo, uno dei meccanismi
più potenti del paradigma della programmazione Object Oriented. Il polimorfismo,
in breve, permette l’intercambiabilità di oggetti differenti se rispondono allo stesso
messaggio. Vediamo un altro breve esempio che mostra questa tecnica in azione:

1 f = { arg a; a.value + 3 };
2 f.value(3);
3 g = { 3.0.rand; };
4 f.value(g);
5 f.value(g);

Come potrebbero essere sfruttati questo polimorfismo e questa tipologia di esempio?


Le Function possono essere definite con l’utilizzo di argomenti. Questi argomenti
sono valori che vengono passati alla funzione quando deve essere valutata. L’esempio
sotto dimostra quanto detto. Si provi ad indovinare il risultato prima dell’esecuzione.

1 (
2 f = { arg a, b;
3 a - b;
4 };
5 f.value(5, 3);
6 )

Gli argomenti sono dichiarati all’inizio della Function, usando la parola chiave arg.
Ci si può riferire ad essi nel codice come se fossero variabili. Quando si richiama il
metodo value su una Function, è possibile passare degli argomenti, ordinati, met-
tendoli fra parentesi. La sintassi è: someFunc.value(arg1, arg2). Questa metodologia
è lo stessa con qualsiasi metodo che prendere in input argomenti di qualsiasi tipo,
siano essi Function, espressioni o anche solo valori.

È possibile specificare un ordine differente usando ciò che viene detto keyword ar-
guments:

12
Funzioni e Funzionalità

1 f = { arg a, b; a / b; }; // ’/’significa dividi


2 f.value(10, 2); // regular style
3 f.value(b: 2, a: 10); // keyword style

È inoltre possibile miscelare i due stili, se necessario, ma gli argomenti regolari


devono essere antecedenti:

1 f = { arg a, b, c, d; (a + b) * c - d };
2 f.value(2, c:3, b:4, d: 1); // 2 + 4 * 3 - 1

(Da notare che SC non ha precedenza negli operatori: le operazioni vengono effet-
tuate nell’ordine in cui sono scritte)

A volte è utile impostare valori di default per degli argomenti, in questo modo:

1 f = { arg a, b = 2; a + b; };
2 f.value(2); // 2 + 2

I valori di default devono essere letterali. I letterali sono di base numeri, stringhe,
simboli o collezioni di questi. Di nuovo, non ci si preoccupi troppo se per ora non
ha tutto un senso completo, diverrà chiaro in seguito.

Esiste un modo alternativo per specificare argomenti, che è incapsularli dentro due
sbarre verticali (su molte tastiere il simbolo è Shift- ). Le seguenti due funzioni sono
equivalenti:

1 f = { arg a, b; a + b; };
2 g = { |a, b| a + b; };
3 f.value(2, 2);
4 g.value(2, 2);

Perchè coesistono due modi differenti per effettuare la stessa operazione!? ... perchè
alcune persone preferiscono il secondo stile e lo considerano una scorciatoia e altri
preferiscono il primo stile perchè è più esplicito. ( ... il mondo è bello perchè è vario
... )

SC ha un certo numero di scorciatoie sintattiche come quella illustrata, che per-


mettono di scrivere codice più velocemente. In ogni caso incontreremo questi due
approcci differenti in entrambe le forme per permettere di impararle entrambe.

13
Funzioni e Funzionalità

È possibile anche avere variabili in una Function; devono essere dichiarate all’inizio
della Function, subito dopo gli argomenti, usando la keyword var.

1 (
2 f = { arg a, b;
3 var firstResult, finalResult;
4 firstResult = a + b;
5 finalResult = firstResult * 2;
6 finalResult;
7 };
8 f.value(2, 3); // (2 + 3) * 2 = 10
9 )

I nomi delle variabili e degli argomenti possono consistere di lettere e numeri, ma


occorre che inizino con una lettera minuscola e non contengano spazi vuoti.

Le variabili sono valide solo per quello che è detto essere il loro scope. Lo scope di
una variabile dichiarata in una Function è la Function stessa, in altre parole l’area
fra due parentesi graffe. Eseguiamo questo esempio una riga alla volta:

1 f = { var foo; foo = 3; foo; };


2 f.value;
3 foo; // ERRORE!! foo compare fuori dal proprio scope.

È possibile dichiarare variabili all’inizio di ogni blocco di codice che verrà eseguito
insieme (selezionandolo tutto insieme). In tal caso il blocco di codice eseguito è lo
scope della variabile. Eseguiamo il blocco (in parentesi tonde) e in seguito l’ultima
linea.

1 (
2 var myFunc;
3 myFunc = { |input| input.postln; };
4 myFunc.value("foo"); // arg \‘e una String
5 myFunc.value("bar");
6 )

8 myFunc; // generare un errore

Ci si potrebbe meravigliare del perchè non è stato necessario dichiarare variabili come
f, e perchè queste non sembrano aver qualche scope particolare (mantengono i valori
anche quando eseguiamo una linea alla volta). Le lettere minuscoli dell’alfabeto sono
dette variabili dell’interprete. Queste variabili sono pre-dichiarate quando avviamo

14
Funzioni e Funzionalità

SC e hanno uno scope illimitato, o, detto in un altro modo, sono globali allo script
di SC.
Per maggiori informazioni:

[Functions] [Function] [Assignment] [Intro-to-Objects] [Literals] [Scope]

15
Funzioni e Suoni

Capitolo 4 Funzioni e Suoni


Dopo questi dettagli tecnici noiosi, torniamo alla creazione di rumore che è il motivo
principale per il quale credo si stia leggendo questo manuale. È pur vero però, che
tutto quello che abbiamo visto finora, tornerà molto utile in futuro.

Torniamo al nosto esempio sonoro, o almeno ad una versione più semplficata. Dopo
aver verificato che il server localhost è in stato running, eseguiamo il codice seguente
( ... e si stoppi il tutto con Cmd-. quando si vuole, non è un test di resistenza ... )

1 { SinOsc.ar(440, 0, 0.2) }.play;

In questo caso si è creata una Function racchiudendo del codice fra parentesi graf-
fe e quindi richiamando il metodo play sulla Function che semplicemente valuta il
codice e fa il play del risultato sul server. Se non si specifica un server, ne sarà sele-
zionato uno di default che, ricordiamo, è salvato nella variabile s e settata, durante
lo startup del programma, al server localhost.

In quest’ esempio la Function non viene salvata in una variabile, così non può essere
riusata. Questo approccio è spesso impiegato con Function-play, come modo veloce
per il testing. Ci sono altri modi per riutilizzare Functions per suoni, che sono spesso
migliori e più efficenti, come vedremo in seguito.

Osserviamo ora il codice fra le parentesi graffe dell’esempio. Si è fatto uso di qualco-
sa chiamato SinOsc a cui stiamo inviando il messaggio ar, con qualche argomento.
SinOsc è un esempio di ciò che chiamaremo class. Per capire cose è una classe, è
necessario conoscere qualcosa in più sugli oggetti e sull’Object Oriented.

Nella programmazione ad oggetti, un oggetto è formato da un insieme di dati e


informazioni, e un insieme di operazione che possono essere effettuate sui dati. Si
potrebbero avere differenti oggetti dello stesso tipo. Essi sono detti istanze. Il tipo
stesso è la classe dell’oggetto. Per esempio potremo avere una classe detta Studenti
e diverse istanze della classe, Bob, Dave e Sue. Tutti e tre sono dati dello stesso
tipo e potrebbero avere una parte di dati detta QI. Il valore di ogni dato potrebbe
essere differente comunque. Si vorrebbe che avessero anche tutti gli stessi metodi
per operare sui dati. Per esempio potrebbero avere un metodo detto calculateQI o
qualcosa di simile.

Una classe di oggetti definisce l’insieme di dati (o variabili di istanza) e i metodi.


Inoltre è possibile definire alcuni altri metodi che comunicano all’interno della classe

16
Funzioni e Suoni

stessa e altri dati usati da tutte le istanze. Questi sono detti metodi e variabili di
classe.

Tutte le classi cominciano con una letteta maiuscola, rendendo più semplice identi-
ficarle nel codice.

Le classi sono utilizzate, come fossero template, per creare gli oggetti. È possibile
creare oggetti attraverso metodi come new o, in caso della nostra classe SinOsc vista
prima, il metodo ar. Tali metodi restituiscono un oggetto o un’istanza e gli argomenti
passati al costruttore influenzano i dati dell’oggetto creato e il suo comportamento.
Prendiamo una parte dell’esempio in questione:

1 SinOsc.ar(440, 0, 0.2)

Viene considerata la classe SinOsc e ne viene creata un’istanza. Tutte le SinOsc sono
un esempio di ciò che viene detto unit generator, o UGens, oggetti che producono
segnali audio o di controllo. SinOsc e un oscillatore sinusoidale. Questo significa che
produrrà un segnale consistente di una sola frequenza. Un grafo della sua forma
d’onda potrebbe essere il seguente:

Questo ciclo d’onda viene creato dal segnale di output ar (ar crea l’istanza audio
rate). SC calcola l’audio in gruppi di campioni, detti blocks. Audio rate significa che
la UGen calcolerà un valore per ogni campione nel blocco. Esiste un altro metodo, kr
che significa control rate. Questo metodo invece calcola un singolo valore per tutto
il blocco di campioni. Questo permette di risparmiare in prestazioni ed è ottimo per
segnali che controllano altre UGens, ma non va bene per segnali audio sintetizzati
perchè non è abbastanza dettagliato.

I tre argomenti della Function SinOsc-ar data nell’esempio sono frequenza, fase e un
mul.

17
Funzioni e Suoni

− La frequenza è proprio la frequenza dell’oscillatore in Hertz (Hz) o cicli al secondo


(cps)

− La fase si riferisce al punto in cui inizierà il ciclo della forma d’onda. Per SinOsc
(ma non per tutte le UGens) la fase è data in radianti (compresi tra 0 e pigreco).
Se per esempio istanziamo un SinOsc con una fase di (pi * 0.5), o un quarto di
ciclo, la forma d’onda sarà:

Vediamo una serie di cicli per chiarire l’idea:

− Mul invece è un argomento speciale che quasi tutte le UGens hanno. È così comu-
ne che di solito non viene in sostanza mai spiegato nella documentazione. Il valo-
re o il segnale espresso da questo parametro sarà moltiplicato per l’output della
UGen. Nel caso di un segnale audio, questo moltiplicatore influenzerà l’ampiezza
del segnale. Il mul di default di molte UGens è 1, il che significa che il segnale
in uscita oscillerà fra -1 e 1. Questo è un buon valore di default, perchè ci mette
al sicuro da problemi di clipping o distorsione che segnali più ampi potrebbero
accusare. Un mul che vale 0 porta un segnale ad essere davvero nullo, come se la
manopola del volume fosse a 0.
Per rendere chiaro l’effetto del mul, vediamo un grafo di due SinOsc, una con il
mul di default a 1 e una con un mul di 0.25:

18
Funzioni e Suoni

Esiste un altro argomento simile chiamato add (anch’esso di solito non conside-
rato nella documentazione), che è qualcosa che viene addizionato al segnale di
output. Può essere utile per segnali di controllo. In generale ‘add’ ha un valore
di default uguale a 0 così da non essere necessario specificarlo.
Tenendo queste cose a mente, rivediamo il nostro esempio con i parametri com-
menti:

1 (
2 { // Apre la Function
3 SinOsc.ar( // Crea un SinOsc audio rate
4 440, // frequenza di 440 Hz, or accordato in
la
5 0, // fase iniziale a 0, o all’inizio del
ciclo
6 0.2) // mul di0.2
7 }.play; // chiude la Function e invoca il metodo ’play’
8 )

4.1 Divertiamoci con Funzioni e UGens


Presentiamo ora un altro esempio di polimorfismo e di come questo strumento sia
potente. Quando creiamo una Function composta da diverse UGens, è possibile
che per molti argomenti non si abbiano valori fissi, ma altre UGens! Vediamo un
esempio che dimostra questo principio:

1 (
2 { var ampOsc;
3 ampOsc = SinOsc.kr(0.5, 1.5pi, 0.5, 0.5);
4 SinOsc.ar(440, 0, ampOsc);
5 }.play;
6 )

19
Funzioni e Suoni

Si provi a eseguire il codice più volte (ancora, usando Cmd-. per fermare il suono).
Ciò che si è fatto in questo caso è stato inserire il primo SinOsc (control rate !!)
dentro l’argomento mul del secondo SinOsc. In questo modo l’output del primo
SinOsc viene moltiplicato per l’output del secondo. I parametri del primo SinOsc
sono:
− Frequenza = 0.5 cps completerà un ciclo in 2 secondi (1 / 0.5 = 2)
− Mul e add = 0.5 Se di default SinOsc va fra 1 e -1, allora una moltiplicazione
per 0.5 scalerà il segnale fra 0.5 e -0.5. Aggiungendo 0.5 il segnale viene
portato fra 0 e 1.
− Fase = 1.5pi significa 3/4 di ciclo, che come illustrato nel primo grafico, si
può vedere il punto più basso, o in questo caso, 0.

Il risultato finale sarà:

Quindi il risultato è un SinOsc su cui viene applicato un leggero fade in/fade out.
In pratica si è influenzato l’output di SinOsc usando l’ampOsc cioè un inviluppo
in ampiezza. Ci sono altri metodi per fare la stessa cosa, alcuni anche più semplici,
ma quest’esempio ne mostra il principale.
Collegare UGens insieme in questo modo è la metodologia base per creare suoni
in SC. Per una visuale generale dei vari tipi di UGens disponibili in SC, si veda
[UGens] o [Tour_of_UGens].
Per maggiori informazioni:

[Functions] [Function] [UGens] [Tour_of_UGens]

20
Living Stereo

Capitolo 5 Living Stereo


Fino a questo punto si sono visti concetti importati, ma il primo esempio del
capitolo precedente, che riportiamo:

1 { [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)] }.play;

... cosa significava? Si hanno due SinOsc con argomenti differenti racchiuse fra
2 parentesi quadre e con una virgola fra loro. Come le parentesi graffe stanno
ad indicare una funzione, le parentesi quadre definiscono qualcosa chiamato Ar-
ray. Un Array è un tipo di Collection, una collezione di Objects. Le Collections
sono esse stesse Objects e molti tipi di Collections possono gestire ogni tipo di
oggetti, mixarli insieme, includere altre Collections! Ci sono inoltre parecchi tipi
differenti di Collections in SC e impareremo come questi siano una delle features
più potenti e importanti in SC.

Un Array è un tipo particolare di Collection: una collezione ordinata di dimensio-


ne limitata. È possibile crearne uno come abbiamo fatto nel codice dell’esempio
appena visto, inserendo oggetti fra due parentesi quadre e separandoli con la
virgola. Per prelevare i differenti elementi di un Array si può usare il metodo
at che prende un indice come argomento. Gli indici corrispondono alla posizione
nell’ordinamento degli oggetti nell’Array e partono da 0.

1 a = ["foo", "bar"];// "foo" = posizione 0; "bar" = posizione 1


2 a.at(0);
3 a.at(1);
4 a.at(2);// restituisce "nil", non ci sono oggetti alla posizione 2

6 // alternativamente, si utilizza
7 a[0]; // uguale ad a.at(0);

Inoltre, per il fatto che può contenere collezioni di oggetti, un Array ha anche
un uso speciale in SC: viene utilizzato per implementare l’audio multicanale! Se
una Function restituisce un Array di UGens (si ricordi che una Function ritorna
il risultato della sua ultima linea di codice), allora l’output sarà un certo numero
di canali. Il numero di canali dipende dalla dimensione dell’Array, e ogni canale
corrisponderà univocamente a un elemento dell’Array. Nel nostro esempio:

1 { [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)] }.play;

21
Living Stereo

Il risultato è un output stereo, con un SinOsc a 440Hz nel canale sinistro e


un SinOsc a 442Hz nel canale destro. È possibile avere più canali di output
utilizzando un array di dimensione maggiore.

Ora si osservi il prossimo esempio. Dato che gli argomenti di fase e mul sono gli
stessi per entrambi i SinOsc, è possibile riscrivere il codice in questo modo:

1 { SinOsc.ar([440, 442], 0, 0.2) }.play;

Si è sostituito l’argomento frequenza con un Array di frequenze. Questo causa


quello che viene detto espansione multicanale che non è nient’altro che l’inserimento
di un Array in uno degli argomenti della UGen al posto di un singolo valore.

Consideriamo ora questo codice:

1 (
2 { var freq;
3 freq = [[660, 880], [440, 660], 1320, 880].choose;
4 SinOsc.ar(freq, 0, 0.2);
5 }.play;
6 )

Provando ad eseguire questo esempio più volte si otterranno risultati diversi; il


metodo choose seleziona uno degli elementi dell’Array in maniera random. In
questo caso il risultato potrebbe essere un singolo numero e si avrebbe un output
monofonico o un altro Array, nel qualcaso si avrebbe un output stereo. Questa
tipologia di finezze può rendere il nostro codice molto flessibile.

Ma se si volesse effettuare un crossfade fra due canali? Come si gestisce il pan? SC


ha un certo numero di UGens che risolvono la spazializzazione orizzontale in vari
modi, ma per ora ne introduciamo una solo: Pan2. Pan2 prevede due argomenti:
un input e una posizione e restituisce un Array di due elementi, il canale sinistro
e il canale destro oppurre il primo e il secondo canale. l’argomento posizione
varia fra -1 (sx) a 1 (dx), passando per lo 0 (centrale). Vediamo l’esempio:

1 { Pan2.ar(PinkNoise.ar(0.2), SinOsc.kr(0.5)) }.play;

In questo esempio un SinOsc controlla la posizione (si ricorda che il suo output
va da -1 a 1) ma usa una UGen differente, il rumore rosa, come input nel Pan2.
Questo che vediamo, il PinkNoise è solo un tipo di generatore di rumore e

22
Living Stereo

ha un argomento solo: mul. È possibile usare anche valori fissi per l’argomento
posizione.

1 { Pan2.ar(PinkNoise.ar(0.2), -0.3) }.play; // slightly to the left

Per ulteriori informazioni:

[MultiChannel] [Collections] [Pan2]

23
Creiamo un Mix

Capitolo 6 Creiamo un Mix


Si è già visto che, nel contesto di segnali audio, le moltiplicazioni cambiano il
livello di qualcosa, ma cosa succede e come si fanno a mixare UGens diverse
insieme? Tutto ciò che serve è l’addizione:

1 { PinkNoise.ar(0.2) + SinOsc.ar(440, 0, 0.2) + Saw.ar(660, 0.2) }.play;

Saw è un tipo di oscillatore, con una forma d’onda che assomiglia a un’onda a
dente di sega. Da notare che viene usato con un valore basso per l’argomento
mul per assicurarci che l’output finale sarà fra -1 e 1 e non si incorra in problemi
di clipping.

Esiste una comoda classe,Mix, che mixerà un array di canali in un singolo canale
o un array di array di canali in un singolo array di canali.
Si osservi la post window per vedere i risultati di Mix.

1 // un canale
2 { Mix.new([SinOsc.ar(440, 0, 0.2), Saw.ar(660, 0.2)]).postln }.play;

4 // combinazione di due array stereo


5 (
6 {
7 var a, b;
8 a = [SinOsc.ar(440, 0, 0.2), Saw.ar(662, 0.2)];
9 b = [SinOsc.ar(442, 0, 0.2), Saw.ar(660, 0.2)];
10 Mix([a, b]).postln;
11 }.play;
12 )

Nel primo caso viene passato un BinaryOpUGen (in questo caso un’operazione di
addizione delle due UGens), e nel secondo caso un Array di due BinaryOpUGens.

Da notare che nel primo esempio si usa Mix.new( ... ) e che nel secondo si usa
Mix( ... ). Il secondo è un’abbreviazione del primo. Il metodo new è quello più
comune per creare un oggetto nuovo. In alcuni casi gli oggetti hanno più di un
metodo per creare oggetti, come i metodi ar e kr delle UGens. (Mix è una classe
di convenienza: non crea un oggetto Mix, ma ritorna soltanto il risultato della
sua somma, o un BinaryOpUGen oppure un Array di questi).

Mix ha inoltre un altro metodo,fill, che prevede due argomenti. Il primo è un

24
Creiamo un Mix

numero, che determina quante volte il secondo argomento, una Function, sarà
valutata. Il risultato di questa valutazione verrà poi sommato. Per chiarire questi
concetti consideriamo il prossimo esempio:

1 (
2 var n = 8;
3 { Mix.fill(n, { SinOsc.ar(500 + 500.0.rand, 0, 1 / n) })
}.play;
4 )

La Function sarà valutata n volte, ogni volta creando un SinOsc, con una frequen-
za random da 500 a 1000 Hz (500 più un numero random fra 0 e 500). l’argomento
mul di ogni SinOsc è settato a 1/n, assicurando quindi che l’ampiezza totale non
andrà mai oltre i limiti di -1 e 1. Semplicemente cambiando il valore di n, si
può avere un numero variabile di SinOsc! Queto tipo di approccio rende il codice
estremamente flessibile e riusabile.

Ogni volta che viene valutata, alla Function viene passato come argomento un
numero. Così, se n è uguale a 8, la Function considererà valori da 0 a 7, in sequen-
za crescente. Dichiarando un argomento dentro la nostra Function, è possibile
usare questo valore come parametro. Vediamo:

1 // si osservi la post window per vedere frequenze e indici


2 (
3 var n = 8;
4 {
5 Mix.fill(n, { arg index;
6 var freq;
7 index.postln;
8 freq = 440 + index;
9 freq.postln;
10 SinOsc.ar(freq , 0, 1 / n)
11 })
12 }.play;
13 )

Dalla combinazione di addizioni e moltiplicazioni ( o qualunque altra possibile


operazione matematica che si può immaginare!) con l’uso di classi come Mix, si
hanno gli strumenti necessari per combinare sorgenti multicanali di suono al fine
di ottenere mix e submix complessi.
Per maggiori informazioni:

[Mix] [BinaryOpUGen] [Operators] [Syntax-Shortcuts]

25
Scoping and Plots

Capitolo 7 Scoping and Plots


Oltre ai metodi visti nei capitoli precedenti, l’oggetto Function dispone di altri
metodi legati all’audio. Il primo che vediamo è Function-plot:

1 { PinkNoise.ar(0.2) + SinOsc.ar(440, 0, 0.2) + Saw.ar(660, 0.2) }.plot;

Questo metodo crea un grafo del segnale prodotto dall’output della Function. È
possibile specificare alcuni argomenti come la durata. Il valore di default è 0.01
secondi, ma è possibile modificarlo a piacere.

1 {
2 PinkNoise.ar(0.2) + SinOsc.ar(440, 0, 0.2) + Saw.ar(660, 0.2)
3 }.plot(1);

Questo metodo può essere utile per verificare cosa succede in fase di testing, e
se si ottiene l’output desiderato.

Il secondo metodo,Function-scope, mostra un display simile ad un oscilloscopio


dell’ouput della funzione. Questo però funziona solo con il server interno, quindi
si rende necessario che questo server sia in stato running. È possibile avviarlo
usando la finestra:

oppure via codice:

1 Server.internal.boot;

Cliccando su ->default sulle finestre server localhost o internal, si setta il server


selezionato a default (quello cioè su cui verrà suonato l’audio) salvandone il
riferimento nella variabiles. Dal fatto che Function-scope lavora solo sul server
internal, comunque, funzionerà sempre e solo su esso.
Provando il codice:

26
Scoping and Plots

1 {
2 PinkNoise.ar(0.2) + SinOsc.ar(440, 0, 0.2) + Saw.ar(660, 0.2)
3 }.scope;

si dovrebbe aprire una finestra di questo tipo:

Questo metodo scope lavora anche per canali multipli:

1 { [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)] }.scope;

Lo Scope ha anche un argomento di zoom. Valori alti corrispondono allo zoom


out.

1 { [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)] }.scope(zoom: 10);

Come Function-plot, Function-scope può essere utile per testing e per vedere se
davvero si ottiene quello che si vuole.

7.1 Scoping su richiesta


Si può anche ottenere l’output del server interno in ogni momento, richiamando
il metodo scope su di esso.

27
Scoping and Plots

1 {
2 [ SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2) ]
3 }.play(Server.internal);
4 Server.internal.scope;
5 // si potrebbe usare la variabile s, se fosse settata a internal

Si può fare la stessa cosa cliccando la finestra del server interno premendo la s.

7.2 Local vs. Internal


La differenza fra i due tipi di server, interno e locale, è relativamente semplice: il
server interno gira come un processo dentro l’app client: un programma dentro
un programma. Il principale vantaggio di questo approccio è che permette a due
applicazioni di condividere memoria, utile per cose come lo scoping in tempo reale
dell’audio. Lo svantaggio evidente è che le due applicazioni sono interdipendenti,
così se il client va in crash, il server lo seguirà a ruota.
Per maggiori informazioni:

[Function] [Server] [Stethoscope]

28
Help di Supercollider

Capitolo 8 Help di Supercollider


Siamo giunti ad un buon punto di questo manuale per fermarci ed esplorare
alcuni metodi per trovare ulteriori informazioni di SC. Nei file di Help viene
utilizzato il link cliccabile come questo:
[Help]
Cliccando su questo link si aprirà la finestra principale dell’help, che contiene
un certo numero di link ad altri file di help. A questo punto, potrebbe essere
una buona idea familiarizzare con alcuni di questi. Quelli che rispondono ai link
Essential Topics e Language sono di particolare importanza.

8.1 Classi and Metodi


Abbiamo visto finora sufficiente teoria sull’OOP, da conoscere che ci sono le
classi, che sono come template per oggetti, e istanze, oggetti che sono creati da
questi template. Si possono anche avere classi e metodi di istanza, che possono
prendere in input argomenti o meno. Metodi di classe fanno cose come creare
istanze (come alcune funzioni convenienti che non richiedono un istanza attuale),
e metodi di istanza che controllano e manipolano le istanze stesse. Ci sono anche
le variabili d’istanza, che sono i dati specifici di ogni istanza e le variabili di
classe, che sono dati in comune fra tutte le istanze.

Ricordiamo che ogni cosa che comincia nel codice di SC con una lettera maiuscola
nel codice è una classe. Molte classi hanno l’help file. In SC, se si seleziona una
classe facendo doppio click e premendo Cmd-?, verrà aperto l’help file della classe,
se esiste, altrimenti si aprià la finestra main dell’help. Si provi con:

1 SinOsc

È possibile avere una finestra con una breve descrizione della classe e di cosa fa,
una lista di alcuni metodi e una descrizione dei loro argomenti.
Di solito, in fondo, ci sono alcuni esempi della classe in azione. Questi possono
essere molto utili per rendere chiaro esattamente cosa fa la classe, e possono
servire come punto di partenza per il proprio lavoro. È una buona idea copiare
e incollare questi esempi in una nuova finestra e provarli, apportando modifiche
o meno per capire davvero come funzionano.
Per accedere all’help file per Function e Array, dato che spesso appaiono nel
codice come {...} e [...], basta selezionarli e premere Cmd-? su:

29
Help di Supercollider

1 Function
2 Array

Alcuni metodi hanno anche help file, e alcuni di essi compaiono sui topics gene-
rali. Molti di questi sono elencati nella finestra main dell’help.

8.2 Shortcut sintattici


Vi ricordate dell’esempio di Mix(...) vs. Mix.new(...) del capitolo capitolo 6.?
SC ha un certo numero di tali forme di scorciatoie o sintassi alternative. Un
esempio comune è il seguente: someObject.someMethod(anArg) è equivalente a
someMethod(someObject, anArg).
Qui vediamo un esempio concreto. Le due righe di codice seguenti fanno la stessa
cosa:

1 { SinOsc.ar(440, 0, 0.2) }.play;

3 play({ SinOsc.ar(440, 0, 0.2) });

Vi sono numerosi altri esempi di scorciatoie di sintassi nella documentazione di


SC. Se si incontra qualcosa di sconosciuto, spesso un buon posto per cercare è
[Syntax-Shortcuts], che fornisce molti esempi di questi shortcut.

8.3 Snooping, etc.


SC ha numerosi altri metodi di tracciare informazioni su classi, metodi, etc.
Molti di questi potrebbero non essere troppo di aiuto a questo punto, ma è utile
conoscerle per un uso futuro. Li si può trovare nel file [More-On-Getting-Help] e
[Internal-Snooping].
Per maggiori informazioni:

[More-On-Getting-Help] [Internal-Snooping] [Syntax-Shortcuts]

30
SuperCollider 3 Synth Server: architettura

Capitolo 9 SuperCollider 3 Synth Server: archi-


tettura
Arrivati a questo punto possiamo considerare in dettaglio il Server di SuperCol-
lider; questo e i prossimi capitoli si occuperanno dell’architettura del Server e
delle sue principali componenti.

9.1 Introduzione
Il Server di SuperCollider v3 è un motore di sintesi semplice e potente. Mentre
questo motore è in stato running, possono essere creati nuovi moduli, distrutti
e/o ricollegati, possono essere creati e/o riallocati buffer; possono inoltre essere
creati e collegati dei processi di effettistica in un flusso di segnali in modo del
tutto dinamico a tempo di scheduling.
Tutti i moduli in stato running sono ordinati in un albero di nodi che definisce
l’ordine di esecuzione. Il patching (collegamento) fra i moduli è costruito attra-
verso i bus globali di audio e di controllo.
Tutti i comandi sono ricevuti via TCP o UDP usando una versione semplificata
di Open Sound Control (OSC). Il server di sintesi e il suo client (uno o più)
potrebbe essere sulla stessa macchina o nella rete. Il server di sintesi non invia o
riceve messaggi MIDI. Si aspetta che il client invierà solo comandi di controllo. Se
si rende necessario usare il protocollo e/o i messaggi MIDI, è compito del client
ricevere tali messaggi e convertirli in comandi OSC appropriati per il motore di
sintesi.
Le definizioni di synth sono salvate in file generati dall’applicazione linguaggio
SC (SCLang). Le definizioni delle UGens sono Mach-O bundles (da non confon-
dersi con CFBundles). Le API delle UGen sono un semplice interfaccia in C.

9.2 Principali componenti


Elenchiamo qui brevemente tutti i componenti dell’architettura del server di SC
dandone una veloce definizione. Per una trattazione più completa si rimanda il
lettore ai capitoli relativi.

• Nodo
Un nodo è un’entità indirizzabile in un albero di nodi eseguito dal motore di

31
SuperCollider 3 Synth Server: architettura

sintesi. Ci sono due tipi di nodi: Synths e Groups. l’albero definisce l’ordine
di esecuzione di tutti i Synths. Tutti i nodi hanno un ID intero.

• Synth
Un Synth è una collezione di unit generator (UGen) che vengono eseguite
insieme. Questi possono essere indirizzati e controllati da comandi inviati al
motore di sintesi. In genere si occupano di leggere dati in input e scrivere
dati in output sui bus globali di audio e di controllo. I Synths possono avere
i propri controlli locali che sono settati tramite comandi al server.
I Synth vengono trattati approfonditamente nel capitolo 10.

• Synth Definition
I Synths sono creati a partire dalle Synth Definition. I file di Synth Definition
sono creati dal SC language application e vengono caricati nel server di sintesi.
Ci si può riferire ad essi tramite il nome.
Le Synth Definition vengono trattate approfonditamente nel capitolo 10.

• Group
Un Group è una collezione di nodi rappresentati come una linked list. Un
nuovo nodo potrebbe essere aggiunto in testa o in coda al gruppo. I nodi
dentro un gruppo possono essere controllati insieme. I nodi in un Group
possono essere sia Synths che altri Group. Allo startup del server si ha un
top level Group con un ID=0 che definisce la radice dell’albero. Se il server
viene avviato dentro SCLang (invece di essere avviato da linea di comando)
ci sarà anche il default group con un ID=1 e sarà il target di default per tutti
i nuovi nodi.
I Group vengono trattati approfonditamente nel capitolo 12.

• Bus I Bus vengono trattati approfonditamente nel capitolo 11.

− Audio Buses I Synths inviano segnali audio fra di loro tramite un solo ar-
ray globale di bus audio. I Bus Audio sono indicizzati da interi, partendo
da 0. l’utilizzo di bus, come la connessione diretta fra synths, permette di
connettere i synths stessi all’insieme di altri synths senza particolari cono-
scenze su di essi. I bus con numero più basso scrivono direttamente sugli
output audio dell’hardware. Immediatamente seguenti ai bus di output ci

32
SuperCollider 3 Synth Server: architettura

sono quelli di input, che leggono dagli input audio dell’hardware. Il nu-
mero di canali bus definiti come input e output non ha però un riscontro
diretto e fisico con l’hardware.

− Control Buses I Synths possono inviare segnali di controllo fra di loro,


tramite un solo array globale di bus di controllo. I Bus sono indicizzati da
un numero intero cominciando da zero.

− Shared Control Buses Il server internal (che è in stato running con lo


stesso spazio di indirizzi dell’app del client) ha un certo numero di bus di
controllo condivisi con l’app del client con un accesso di lettura/scrittura
sincrono. Questi bus sono indicizzati da interi, partendo da zero.

• Buffers I Buffer sono array di valori da 32 bit floating point con un picco-
lo header descrittivo. I Buffers sono salvati in un array globale indicizzato
da interi partendo da zero. I Buffers potrebbero essere allocati in maniera
protetta, caricati e liberati mentre la sintesi sta eseguendo anche quando le
Ugen gli stanno usando. I Buffers sono usati per wave tables, sample buffers,
linee di ritardo, inviluppi e/o per ogni altra necessità che potrebbe usare un
array di valori in floating point. I file audio potrebbero essere caricati dentro
o scritti da buffers.
I Buffer vengono trattati approfonditamente nel capitolo 13.

• Unit Generator Definitions Le definizioni di Unit Generator sono caricate


automaticamente all’avvio del programma. Esse sono in librerie binary code e
vengono usate dai Synths per costruire algoritmi di sintesi. Le definizioni delle
UGens hanno un nome che corrisponde al nome della classe del linguaggio di
SC usato nei Synth Definitions.

Ora che abbiamo scoperto alcune informazioni base su SC e sul server, andiamo
a studiare le astrazioni server, che sono le varie classi nel linguaggio dell’app
del client che rappresentano qualcosa sul server. È importante capire che questi
oggetti sono solo rappresentazioni client-side di parti dell’architettura del server,
e non devono esser confuse con le parti stesse che rappresentano. Questi oggetti-
astrazioni sono utilizzati semplicemente per convenienza nel linguaggio.
Capire la distinzione fra le due cose può non essere così diretto e può portare a
confusione, così, in generale ci riferiremo alle classi client-side con nomi maiusco-
li, e gli aspetti corrispondenti dell’architettura del server con nomi con lettera
minuscola, per esempio Synth vs. synth.

Si è già incontrato un tipo di astrazione server, la classe Server stessa. Gli oggetti

33
SuperCollider 3 Synth Server: architettura

riferiti a Server.local e Server.internal (e qualsiasi cosa salvata nella variabile s


in ogni momento) sono istanze di Server.

Ora è il momento di prendere familiarità con altre astrazioni. Le prime che ve-
diamo sono le classe SynthDef e Synth, che presentiamo nel capitolo successivo.

34
SynthDefs e Synths

Capitolo 10 SynthDefs e Synths


La prima che vediamo è la classe SynthDef, che è un’abbreviazione di synth
definition.

10.1 SynthDef
Fino ad ora sono state usate Functions per generare audio. Il loro utilizzo è molto
utile per testing veloci, e in casi dove è necessaria la massima flessibilità. Questo
perchè ogni volta che si esegue il codice, la Function viene ri-valutata ex-novo
con la conseguenza che i risultati possono variare e di molto.

Il server, in generale, non conosce tuttavia Functions, o l’Object Oriented, o il


SC language, ma piuttosto vuole infomazioni su come creare output audio in una
forma speciale detta definizione di synth. Una definizione di synth è formata da
una serie di informazioni sulle UGens da utilizzare e come esse devono essere
interconnesse. Questa serie di informazioni viene inviata al server in una forma
speciale ottimizzata, detta ‘byte code’ con cui il server può lavorare in maniera
molto efficiente.

Una volta che il server ha una definizione di synth, può essere molto efficiente
creare un certo numero di synth basandosi su questa definizione. I synths sul
server sono, in sostanza, entità che creano o processano il suono, o producono
segnali di controllo per pilotare altri synths.

Questa relazione fra la definizione di synth e i synth è analoga alla relazione che
intercorre fra le classi e le istanze, in cui le primi sono un templare per le seconde.
l’analogia è però solo a livello concettuale in quanto le app server non conoscono
l’ OOP.

Fortunatamente per noi, nel linguaggio ci sono classi come SynthDef, che rendono
semplice creare il byte code necessario e inviarlo al server, e avere a che fare con
definizioni di synth in modalità object oriented.

Quando si usa qualunque metodo di Function per creare audio, ciò che succede
è che viene creata una istanza corrispondente di SynthDef ‘dietro le quinte’, per
creare il protocollo di dialogo. Viene generato il byte code necessario e inviato al
server, dove sarà creato un synth per suonare l’audio desiderato. I metodi audio
di Function offrono così un tipo di convenienza per il programmatore, liberandolo
dal compito di occuparsi di byte code e cose lontane al suo modo di lavorare.

35
SynthDefs e Synths

Vediamo quindi come si crea una SynthDef. Si usa il suo metodo new, allo
stesso modo delle Function. Inoltre, come per le Functions, SynthDef ha anche
un metodo play. In un certo senso sono proprio equivalenti:

1 //prima la funzione
2 { SinOsc.ar(440, 0, 0.2) }.play;

4 // ora una SynthDef equivalente alla funzione


5 SynthDef.new("tutorial-SinOsc", { Out.ar(0, SinOsc.ar(440, 0, 0.2))
}).play;

SynthDef-new prevede un certo numero di argomenti. Il primo è il nome, di solito


sotto forma di stringa. Il secondo è, di fatto, una Functione viene detto UGen
Graph Function, perchè esplicita al server come connettere i vari UGens.

10.2 SynthDefs vs. Functions


La UGen Graph Function, usata nella SynthDef, è simile ad una Function, e
presenta una non troppo lieve differenza: ha un UGen extra detta Out. Out si
occupa di scrivere in output un segnale ar o kr sui bus del server, che possono
essere canali di un mixer o degli output veri e propri. I bus verranno discussi in
dettaglio più avanti; per ora è sufficiente sapere che sono usati per inviare audio
in uscita e per leggerlo da sorgenti come microfoni in ingresso alla scheda audio.

La UGen Out prevede 2 argomenti:

− Il primo è l’indice numerico del bus su cui scrivere. La numerazione dei bus
comincia da 0, che, su un setup stereo, è il canale sinistro.

− Il secondo argomento è o una UGen o un Array di Ugens. Se si passa un array


(per esempio un output multicanale), allora il primo canale sarà suonato sul
bus con l’indice indicato, il secondo sul bus con l’indice + 1, e così via.

Vediamo un esempio stereo per far chiarezza su come effettivamente lavora questa
UGen.

36
SynthDefs e Synths

1 (
2 SynthDef.new("tutorial-SinOsc-stereo", { var outArray;
3 outArray = [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)];
4 Out.ar(0, outArray)
5 }).play;
6 )

Il SinOsc con la frequenza di 440 Hz sarà suonato sul bus 0 (il canale sinistro)
e il SinOsc con la frequenza di 442 Hz sarà suonato sul bus 1 (il canale destro).
Quando usiamo Function-play, viene di fatto creata automaticamente per noi
una UGen Out. Il bus di default per questa UGen è 0.

Sia Function-play che SynthDef-play restituiscono un altro tipo di oggetto, un


Synth, che rappresenta un synth sul server. Se salviamo questo oggetto assegnan-
dolo in una variabile, è possibile controllarne il comportamento tramite i metodi
di cui dispone. Per esempio il metodo free causa lo stop del synth sul server e
ne vengono liberate le risorse cpu e di memoria che gli erano state riservate.

1 x = { SinOsc.ar(660, 0, 0.2) }.play;


2 y = SynthDef.new("tutorial-SinOsc", { Out.ar(0, SinOsc.ar(440, 0, 0.2))
}).play;
3 x.free; // libera x
4 y.free; // libera y

l’utilizzo del metodo free è più flessibile del comando da tastiera Cmd-., che
‘libera’ tutti i synths insieme.

SynthDef ha 3 metodi che inviano il byte code corrispondente all’app server senza
creare immediatamente un synth: send e load e writedef. La differenza fra questi
3 è:

− send fa lo stream della definizione sulla rete

− load carica la definizione fra le definizioni correnti del server.

− writedef scrive la definizione sul disco sotto forma di file così da permettere
al server di caricarla.

Questi file avranno estensione .scsyndef ( nel nostro esempio tutorial-SinOsc.scsyndef),


e saranno salvato nella cartella synthdefs/ presente nella directory principale di
SC. Questi file saranno inoltre caricati automaticamente ogni qualvolta si farà il

37
SynthDefs e Synths

boot del server e rimarranno in questa cartella fino a che non saranno specifica-
mente cancellati.
In generale è possibile usare send senza riusare le definizioni tutte le volte. Si
rende tuttavia necessario in alcuni casi usare load, casi in cui si ha a che fare
con definizioni molto grandi o complicate, a causa del limite della dimensione
dei pacchetti sulla rete.

Usare le SynthDef comporta certi vantaggi e certe limitazioni.

Una volta che si ha una definizione di un synth in un app server, si possono creare
molti synths da questa con un overhead relativamente basso di CPU. È possi-
bile farlo con il metodo new, che prende il nome della definizione come primo
argomento.

1 SynthDef.new("tutorial-PinkNoise", { Out.ar(0, PinkNoise.ar(0.3))


}).send(s);
2 x = Synth.new("tutorial-PinkNoise");
3 y = Synth.new("tutorial-PinkNoise");
4 x.free; y.free;

È più efficiente che chiamare ripetutamente play sulla stessa Function, perchè si
risparmia lo sforzo di rivalutare la Function, compilare il byte code e inviarlo al
server per ogni valutazione. In molti casi però questo risparmio dell’uso di CPU è
così piccolo da essere insignificante; in altri casi diventa importante, specialmente
se si prevede una produzione massiccia di synths.

Una limitazione nel lavorare con SynthDef direttamente è che la UGen Graph
Function in un SynthDef è valutata una volta e solo una volta. (Si ricordi che
il server non conosce in sostanza niente del linguaggio SC). Questo significa che
la definizione del synth è meno flessibile di quanto si pensi. Si comparino questi
due esempi:

38
SynthDefs e Synths

1 // prima con una Function. Da notare la frequenza random, differente ad


ogni play
2 f = { SinOsc.ar(440 + 200.rand, 0, 0.2) };
3 x = f.play;
4 y = f.play;
5 z = f.play;
6 x.free; y.free; z.free;

8 // Ora con una SynthDef. No random!!


9 SynthDef("tutorial-NoRand", { Out.ar(0, SinOsc.ar(440 + 200.rand, 0,
0.2)) }).send(s);
10 x = Synth("tutorial-NoRand");
11 y = Synth("tutorial-NoRand");
12 z = Synth("tutorial-NoRand");
13 x.free; y.free; z.free;

Ogni volta che viene creato un nuovo Synth basandosi sulla definizione, la fre-
quenza è la stessa. Questo perchè la Function (e quindi 200.rand) viene valutata
una volta sola e cioè al momento della creazione.

10.3 SynthDefs ‘variabili’


Per superare le limitazioni viste in precedenza, esistono diversi modi per ottenere
valori di output non fissi da SynthDefs. Si può usare per esempio [Rand], che
calcola un numero random fra i valori min e max passati come parametri quando
alla creazione del synth:

1 // Se si usa l’oggetto Rand, allora si pu\‘o!


2 SynthDef("tutorial-Rand", { Out.ar(0, SinOsc.ar(Rand(440, 660), 0,
0.2)) }).send(s);
3 x = Synth("tutorial-Rand");
4 y = Synth("tutorial-Rand");
5 z = Synth("tutorial-Rand");
6 x.free; y.free; z.free;

Il modo più comune di avere variabilità nella SynthDef è attraverso il passaggio di


paramentri alla Ugen Graph Function. Questo permette di settare differenti valori
quando il synth viene creato. Viene pertanto passato, come secondo argomento
nella Synth-new, un array di argomenti formato da coppie nome-valore.

39
SynthDefs e Synths

1 (
2 SynthDef("tutorial-args", { arg freq = 440, out = 0;
3 Out.ar(out, SinOsc.ar(freq, 0, 0.2));
4 }).send(s);
5 )
6 x = Synth("tutorial-args"); // no args
7 y = Synth("tutorial-args", ["freq", 660]); // cambia la freq
8 z = Synth("tutorial-args", ["freq", 880, "out", 1]); // cambia freq
e canale di output
9 x.free; y.free; z.free;

Questa combinazione di argomenti e UGens permette di avere una singola defi-


nizione abbastanza flessibile, ma in qualche caso, dove è richiesta un alto grado
di flessibilità, è ancora necessario usare le Functions, o creare definizioni multiple
di synth.

10.4 Synth
La classe Synth prevede alcuni metodi che permettono di variare il valore degli
argomenti dopo che un synth è stato creato. Per ora ne vedremo uno, il metodo
set. Synth-set prevede come argomenti una o più coppie nome-valore dove nome
è il parametro della synthdef a cui si associa il valore.

1 Server.default = Server.internal;
2 s = Server.default;
3 s.boot;
4 (
5 SynthDef.new("tutorial-args", { arg freq = 440, out = 0;
6 Out.ar(out, SinOsc.ar(freq, 0, 0.2));
7 }).send(s);
8 )
9 s.scope; // per vedere l’effetto
10 x = Synth.new("tutorial-args");
11 x.set("freq", 660);
12 x.set("freq", 880, "out", 1);
13 x.free;

10.5 Alcune nozioni su Symbols, Strings, SynthDef e Arg Names


I nomi delle SynthDef e degli argomenti possono essere o una String, come visto
in precedenza, o un altro tipo di letterale detto Symbol. È possibile scrivere
simboli alternativamente in uno dei due modi seguenti:

40
SynthDefs e Synths

− racchiudendolo fra apici singoli: ’tutorial-SinOsc’

− preceduti da un backslash: " tutorial"-SinOsc.


Come le Strings, anche i Symbols sono composti da una sequenza alfa-numerica.
La differenza fra Stringhe e Simboli è che tutti i simboli con lo stesso testo sono
garantiti essere identici, ( xes esattamente lo stesso oggetto), mentre le stringhe
potrebbero differire. Per testare questa cosa, usiamo ’===’. Eseguiamo il codice
e osserviamo la post window

1 "a String" === "a String"; // false


2 \aSymbol === ’aSymbol’; // true

In generale nei metodi che comunicano con il server è possibile usare Strings
e Symbol in modo intercambiabile, ma potrebbe non essere vero in un codice
generico.

1 "this" === \this; // false

Per maggiori informazioni:

[SynthDef] [Synth] [String] [Symbol] [Literals] [Randomness] [UGens]

41
I Buss

Capitolo 11 I Buss
Vediamo ora qualche ulteriore informazione sui bus sul server. Esiste un’analogia
con i bus e mandate sui mixer analogici perchè presentano funzionalità simili:
definiscono il routing dei segnali fra un punto e un altro di una catena audio. In
SuperCollider questo significa da e per l’hardware audio o fra synth differenti.
Esistono due tipi di bus:

− Audio Rate

− Control Rate
Com’è intuibile, il primo indirizza segnali audio mentre il secondo segnali di con-
trollo.

I Bus control rate sono abbastanza semplici da comprendere, sono semplici canali
di comunicazione ognuno con un proprio indice, partendo da zero.

I Bus audio rate sono simili ai precedenti, ma richiedono una piccola spiegazio-
ne ulteriore. Un’ app Server avrà un certo numero di canali di input e output;
questi canali corrispondono a bus audio con indice più basso, con i canali di
output anteposti nell’ordine ai canali di input. Per esempio, se immaginiamo un
server con 2 canali di output e 2 canali di input (x es. stereo in e stereo out),
allora i primi 2 audio bus (indice 0 e 1) saranno gli output e i 2 immediatamen-
te seguenti (indice 2 e 3) saranno gli input. Scrivere audio su uno dei 2 bus di
output provocherà un’emissione sonora dagli altoparlanti e leggere audio dai 2
bus di input permetterà di acquisire suoni in SC, per processi di registrazione o
di elaborazione.

I bus audio rimanenti saranno privati. Essi sono usati per inviare audio e segnali
di controllo fra i vari synths. Inviare audio ad un bus privato non comporterà
emissione sonora negli altoparlanti se non sarà reindirizzerato su uno dei bus di
output. Questi bus privati sono spesso usati per esempio per funzionalità come
una mandata effetti, qualcosa cioè che richiede alcuni passi di elaborazione audio
prima di generare output.

Il numero di bus audio e di controllo disponibili, come il numero di canali di


output e di input è settato al momento del boot dell’App server.( Si veda [Ser-
verOptions] per informazioni su come settare il numero di canali e bus di input
e output)

42
I Buss

11.1 Buss: scrittura e lettura


Si è già visto Out.ar, che permette di scrivere audio (per esempio play out) su
un bus. Ricordiamo che prevede 2 argomenti: un indice e un output, che possono
essere un vettore di UGens (xes un output multicanale) o una singola UGen.

Per leggere da un bus server si utilizza un’altra UGen: In. Il metodo ar di In ha


anch’esso due argomenti: un indice, e il numero di canali da leggere. Se il numero
di canali è maggiore di 1, allora l’output di In sarà un Array. Si provi il seguente
esempio e si osservi la post window:

1 In.ar(0, 1); // restituir\‘a ’an OutputProxy’


2 In.ar(0, 4); // restituit\‘a un Array di 4 OutputProxies

Un OutputProxy (si veda l’esempio precedente) è un tipo speciale di UGen


che agisce come segnaposto per qualche segnale che sarà presente quando qual-
che synth starà eseguendo. Probabilmente non sarà mai necessario averci a che
fare direttamente, così non occorre preoccuparsi troppo di questo costrutto; è
sufficiente per i nostri scopi capire cosa sono in generale. Per una trattazione
completa su OutputProxy e l’interessante utilizzo di un proxyspace si veda
Le UGen In e Out hanno metodi kr, che leggeranno e scriveranno segnali control
rate da e in bus control rate.
Da notare che Out.kr convertirà un segnale audio rate in un segnale control
rate (operazione che viene detta sottocampionamento); l’operazione inversa non
è possibile: Out.ar necessita di un segnale audio rate come secondo argomento.

1 // Questo genera un errore: segnale control rate su un bus audio rate


2 {Out.ar(0, SinOsc.kr)}.play;

4 // Questo non genera errore: segnale audio rate sottocampionato a


control rate
5 Server.internal.boot;
6 {Out.kr(0, SinOsc.ar)}.scope;

(Questa limitazione non è universale fra le UGens audio rate, e molte accetterano
segnali control rate per alcuni o tutti i loro argomenti. Alcune convertiranno i
control rate in input in audio rate se necessario, calcolando i valori mancanti
tramite un processo di interpolazione.)
Da notare inoltre che quando più Synth scriveranno nello stesso bus, l’output
sarà sommato, o in altre parole, mixato.

43
I Buss

1 (
2 SynthDef("tutorial-args", { arg freq = 440, out = 0;
3 Out.ar(out, SinOsc.ar(freq, 0, 0.2));
4 }).send(s);
5 )
6 // sia x che y scrivono sul bus 1. L’output viene mixato
7 x = Synth("tutorial-args", ["out", 1, "freq", 660]);
8 y = Synth("tutorial-args", ["out", 1, "freq", 770]);

11.2 Creiamo un Bus Object


Esiste un comodo oggetto lato-client che rappresenta i bus server: Bus. Dato che
tutto ciò che serve è una UGen di In o di Out e un indice per scrivere su un bus,
ci si potrebbe chiedere a cosa dovrebbe servire un Bus Object. Molte volte infatti
non li si usa direttamente, specialmente se ciò che si sta facendo è solo far fluire
audio dall’ingresso all’uscita della nostra scheda audio. In generale tuttavia i Bus
offrono alcune utili funzionalità. Le vedremo in seguito, prima vediamo come è
possibile creare un’oggetto di questo tipo.
Come molte UGens che hanno metodi ar e kr, anche i Bus hanno 2 metodi di
creazione comuni: Bus-audio e Bus-Control. Ognuno prevede 2 argomenti: un
oggetto Server e il numero di canali.

1 b = Bus.control(s, 2); // Crea un Bus control a 2 canali


2 c = Bus.audio(s); // Crea un Bus audioprivato

Ci si potrebbe chiedere cos’è un bus due-canali, dato che fin’ora non è ancora sta-
to menzionato. Occore ricordare pertanto, che quanto Out ha un Array come suo
secondo argomento, scriverà i canali dell’Array in bus consecutivi. Richiamiamo
un esempio dal capitolo precendete:

1 (
2 SynthDef.new("tutorial-SinOsc-stereo", { var outArray;
3 outArray = [SinOsc.ar(440, 0, 0.2), SinOsc.ar(442, 0, 0.2)];
4 Out.ar(0, outArray); // scrive sui bus 0 e 1
5 }).play;
6 )

La verità è che non ci sono bus multicanali di per se, quindi non esiste un oggetto
bus due-canali, tuttavia gli oggetti Bus sono in grado di rappresentare una serie
di bus con indici consecutivi. L’incapsulamento di molti bus lato-server adiacenti

44
I Buss

in un singolo oggetto Bus, permette di trattarli come un gruppo, rendendo il loro


utilizzo molto più agevole.
Quando si lavora con i cosiddetti bus privati (cioè quelli oltre i canali audio di in-
put e output e tutti i bus di controllo), generalmente si vorrebbe che quest’ultimi
vengano usati con un certo ordine; si potrebbe farlo oculatamente se si potesse
decidere quale indice usare, come fossero array, ma i Bus gestiscono questi indici
automaticamente. Ogni oggetto Server ha un bus allocator e quando si crea un
oggetto Bus, questo gli riserva un indice privato che non potranno essere resti-
tuito fino alla fine dell’utilizzo del Bus. È possibile recuperare l’indice di un Bus
usando il suo metodo index.

1 // riavviamo il server e quindi azzeriamo il bus allocator


2 s.reboot;
3 b = Bus.control(s, 2); // un control Bus a 2 canali
4 b.index; // dovrebbe essere 0
5 b.numChannels // Bus ha anche un metodo numChannels
6 c = Bus.control(s);
7 c.numChannels; // il numero di default dei canali=1
8 c.index; // =2; b usa sia 0 che 1

Tramite l’utilizzo di oggetti Bus per rappresentare bus adiacenti, si può garantire
che non avvenga un conflitto. Dal momento che gli indici sono allocati dinamica-
mente, è possibile cambiare il numero di canali di un bus dal codice (per esempio
perchè si rende necessario indirizzare un segnale multicanale) e garantire che il
tutto sia ancora stabile e sicuro. Se fosse compito del programmatore l’allocazione
degli indici dei bus e se si rendesse necessario ad un certo punto riarrangiare tutto
per un canale adiacente extra, dal momento che gli indici devono essere conse-
cutivi, sarebbe davvero molto complicato! Questo è un buon esempio del potere
che ha la programmazione a oggetti; incapsulando negli oggetti stessi processi
come l’allocazione degli indici dei bus, si ha un certo livello di astrazione e si può
scrivere codice molto flessibile.
È possibile liberare e riallocare l’indice di un Bus in uso richiamando il metodo
free;

1 b = Bus.control(s, 2);
2 b.free; // libera gli indici. b diventa inutilizzabile se non reinstanziato

Da notare che questo metodo non "libera" il bus sul server, non viene cioè disal-
loccato; il metodo free semplicemente comunica all’allocatore che si è "liberato"
un bus e che può essere liberamente riallocato il suo indice.
Vediamo ora un ulteriore vantaggio dell’uso dei bus privati audio rate. Come
detto in precedenza, gli indici dei bus con valore più basso sono i canali di input

45
I Buss

e output. Detto ciò, se vogliamo usare il primo bus privato, quale indice dovremo
utilizzare? Consideriamo per esempio l’App Server con 2 canali di output e 2 di
input. Il primo audio bus privato è indicizzato con 4 (0, 1, 2, 3 ... 4!!). Così
quando scriviamo il nostro codice, daremo il valore 4 all’Ugen Out come indice
del bus.
Ma cosa succede se si decide in seguito di cambiare il numero di canali di output
e impostarlo a 6? Tutto ciò che è stato scritto sul nostro bus privato, finirà su
uno dei canali di output! In realtà un bus allocator di un Server audio assegnerà
solo indici privati, così se si rendesse necessario cambiare il numero di canali di
input o di output, gestirà gli indici considerando il numero di bus di input e di
output quando verra eseguito il codice. Ancora, questo principio rende il codice
più flessibile.

11.3 Buss in azione!


Vediamo qui due esempi che fanno uso di bus. Il primo è con un bus control rate.

1 (
2 SynthDef("tutorial-Infreq", { arg bus, freqOffset = 0;
3 // this will add freqOffset to whatever is read in from the bus
4 Out.ar(0, SinOsc.ar(In.kr(bus) + freqOffset, 0, 0.5));
5 }).send(s);

7 SynthDef("tutorial-Outfreq", { arg freq = 400, bus;


8 Out.kr(bus, SinOsc.kr(1, 0, freq/40, freq));
9 }).send(s);

11 b = Bus.control(s,1);
12 )

14 (
15 x = Synth.new("tutorial-Outfreq", [\bus, b.index]);
16 y = Synth.after(x, "tutorial-Infreq", [\bus, b.index]);
17 z = Synth.after(x, "tutorial-Infreq", [\bus, b.index, \freqOffset,
200]);
18 )
19 x.free; y.free; z.free; b.free;

Sia y che z fanno riferimento allo stesso bus; il secondo synth modifica la fre-
quenza del segnale di controllo aggiungendo un valore costate di 200. Questo
approccio è più efficiente rispetto ad avere 2 oscillatori di controllo separati per
controllare la frequenza. Questo tipo di strategia di connettere insieme synths,
ognuno dei quali fa cose differenti in un processo più grande, può essere molto
efficace in SC.

46
I Buss

Ora vediamo un esempio con un bus audio. Questo è l’esempio più complicato di
quelli visti fin’ora, ma potrebbe dar qualche idea su come mettere insieme tutte
le nozioni fin qui illustrate. Il codice presentato usa 2 Synths come sorgente,
uno crea pulsazioni di PinkNoise (un tipo di rumore che ha maggior energia alle
basse frequenze rispetto alle alte frequenze), e un altro crea impulsi di un’onda
sinusoidale. Gli impulsi sono creati usando la Ugens [Impulse] e [Decay2]. Questi
sono quindi riviberati usando una catena di [AllpassC], che è un tipo di delay.
Da notare inoltre il costrutto16.do( ... ) che crea la catena valutando la funzione
16 volte. Si tratta di una tecnica molto potente e flessibile, caratterizzata dal
fatto che semplicemente cambiando il numero è possibile modificare il numero di
valutazioni. (Si veda [Integer] per ulteriori informazioni su Integer-do.)

47
I Buss

1 (
2 // l’arg permette il controllo diretto
3 SynthDef("tutorial-DecayPink", { arg outBus = 0, effectBus, direct = 0.5;
4 var source;
5 // Decayi sul PinkNoise.
6 source = Decay2.ar(Impulse.ar(1, 0.25), 0.01, 0.2, PinkNoise.ar);
7 // il nostro main output
8 Out.ar(outBus, source * direct);
9 // il nostro effects output
10 Out.ar(effectBus, source * (1 - direct));
11 }).send(s);

13 SynthDef("tutorial-DecaySin", { arg outBus = 0, effectBus, direct = 0.5;


14 var source;
15 // Decay sull’onda Sinusoidale.
16 source = Decay2.ar(Impulse.ar(0.3, 0.25), 0.3, 1, SinOsc.ar(SinOsc.kr(0.2,
0, 110, 440)));
17 // il nostro main output
18 Out.ar(outBus, source * direct);
19 // il nostro effects output
20 Out.ar(effectBus, source * (1 - direct));
21 }).send(s);

23 SynthDef("tutorial-Reverb", { arg outBus = 0, inBus;


24 var input;
25 input = In.ar(inBus, 1);
26 // Viene valutata aNumber.do in corrispondenza del numero di volte
27 // {}.dup(n) valuta la funzione n volte, e restituisce un Array dei
risultati
28 // n=2 di default per un riverbero stereo
29 16.do({ input = AllpassC.ar(input, 0.04,
30 { Rand(0.001,0.04) }.dup, 3)});Out.ar(outBus, input);}).send(s);
31 b = Bus.audio(s,1); // this will be our effects bus
32 )

34 (
35 x = Synth.new("tutorial-Reverb", [\inBus, b.index]);
36 y = Synth.before(x, "tutorial-DecayPink", [\effectBus, b.index]);
37 z = Synth.before(x, "tutorial-DecaySin", [\effectBus, b.index,
\outBus, 1]);
38 )

40 // Istruzioni per variare il balance wet/dry del segnale


41 y.set(\direct, 1); // only direct PinkNoise
42 z.set(\direct, 1); // only direct Sine wave
43 y.set(\direct, 0); // only reverberated PinkNoise
44 z.set(\direct, 0); // only reverberated Sine wave
45 x.free; y.free; z.free; b.free;

48
I Buss

Da notare infine che potremmo avere molti più synth sorgenti che devono essere
processati da un singolo synth di riverbero. Se inseriamo il riverbero nella sor-
gente ovviamente duplichiamo lo sforzo. Usando un bus privato, siamo in grado
di essere più efficienti.

11.4 Divertiamoci con i Control Buss


Con i bus control rate è possibile creare script molto potenti. Per esempio, è
possibile mappare qualunque argomento di un synth che è in stato running e
leggerlo da un control bus. Questo significa che non si rende necessario l’utilizzo
di una UGen In. Inoltre è possibile scrivere valori costanti su un bus di controllo
usando il metodo set, e recuperare i valori usando il metodo get

1 (
2 // crea 2 bus control rate e setta il loro valori a 880
3 //e 884 rispettivamente
4 b = Bus.control(s, 1); b.set(880);
5 c = Bus.control(s, 1); c.set(884);
6 // crea un synth con 2 frequenze come argomenti
7 x = SynthDef("tutorial-map", { arg freq1 = 440, freq2 = 440;
8 Out.ar(0, SinOsc.ar([freq1, freq2], 0, 0.1));
9 }).play(s);
10 )
11 // vengono mappate freq1 e freq2 per leggere dai due bus
12 x.map(\freq1, b.index, \freq2, c.index);

14 // viene creato un Synth per scrivere su uno dei bus


15 y = {Out.kr(b.index, SinOsc.kr(1, 0, 50, 880))}.play(addAction:
\addToHead);

17 // libera y, e b mantiene il suo ultimo valore


18 y.free;

20 // con Bus-get si ottine questo valore e stamparlo nella post window


21 b.get({ arg val; val.postln; f = val; });

23 // viene settata la freq2, annullando la mappatura da c


24 x.set(\freq2, f / 2);

26 // freq2, non essndo mappata, la modifica su c non ha effetti


27 c.set(200);

29 x.free; b.free; c.free;

49
I Buss

Da notare che, diversamente dai bus audio rate, i bus control rate mantengono
i valori in essi contenuti finchè qualcosa di nuovo non viene sovrascritto. Da
notare inoltre in questo script che il metodo Bus-get prende una Function (detta
funzione-azione) come argomento, il che comporta una piccola quantità di tempo
per il server valutare la Function e restituire il risultato indietro. La funzione,
che è passata come argomento può restituire un valore o un Array di valori nel
caso di un bus multicanale. Questo concetto, cioè occupare una piccola quantità
di tempo per la risposta (usualmente detto latenza) è abbastanza importante da
capire. Ci sono un certo numero di altri metodi in SC che funzionano in questo
modo, e possono causare dei problemi se non sono trattati con attenzione. Per
illustrare questa cosa eseguiamo l’esempio seguente:

1 // crea un Bus object e setta i suoi valori


2 b = Bus.control(s, 1); b.set(880);

4 // eseguire tutto insieme


5 (
6 f = nil; // just to be sure
7 b.get({ arg val; f = val; });
8 f.postln;
9 )

11 f.postln;

Osservando la post window, perchè f era nil la prima volta e non la seconda?
La parte del linguaggio che esegue il codice (detto interprete) lavora in maniera
ottimale, cioè si può dire fa cosa gli si dice di fare, più veloce che può e quando
glielo si dice. Così nel blocco di codice fra le parentesi, invia il messaggio ’get’al
server, fa lo scheduling della Function per eseguirla quando riceve una risposta,
e quindi si muove sull’istruzione postln di f. La prima volta f sarà nil perchè non
si sarà ancora ricevuta una risposta dal server.

Il server impiega solo una piccolissima quantità di tempo per inviare una risposta,
così nel momento in cui stiamo eseguendo l’ultima linea di codice f è stato settato
a 880, come ci aspettavamo. Nell’esempio precedente questa latenza non era un
problema, dal fatto che si eseguiva una sola linea per volta. Ma ci potranno essere
sicuramente casi in cui sarà necessario eseguire blocchi interi di istruzioni e la
tecnica della funzione azione sarà la soluzione.

11.5 L’ordine è una cosa importante!


Negli esempi precedenti, si è fatto uso di istruzioni come Synth.after, e addAc-
tion: addToHead. Durante ogni ciclo (il periodo in cui viene calcolato un blocco

50
I Buss

di campioni), il server esegue istruzioni in un ordine particolare, seguendo la sua


lista di synth che sono in stato running. Il server inizia con il primo synth, e
calcola un blocco di campioni per la sua prima UGen. In seguito, a turno, cal-
cola un blocco di campioni per ognuno delle UGen rimanenti in base all’ordine
in cui sono configurate (ognuna delle quali potrebbe prendere l’output di UGen
precedenti come input). Questo output viene scritto su uno o più bus, Il server
allora si occupa del prossimo synth della lista e il processo prosegue fino a che
tutti i synth in stato running hanno calcolato un blocco di campioni. A questo
punto il Server ricomincia il ciclo.

La cosa importante da capire è che come regola generale, quando si connettono


synths usando dei bus, è importante che i synth che scrivono segnali sui bus siano
i primi nell’ordinamento seguito dal server rispetto a quelli che leggono segnali
da questi bus. Per esempio nel bus audio dell’esempio precedente, era molto im-
portante che il synth di riverbero fosse calcolato dopo che il synth di rumore e il
synth sinusoidale fossero processati dal server.

L’ordinamento è un fatto complicato, e ci sono alcune eccezioni, ma dovrebbe es-


sere chiaro che l’ordinamento dei synth diventa cruciale quando i synth vengono
connessi fra di loro. Il file [Order-of-execution] copre questo argomento in detta-
glio. Synth-new presenta fra gli altri, 2 argomenti che permettono di specificare
dove aggiungere un synth nell’ordinamento. Il primo è un target, e il secondo è
un addAction. che specifica posizione del synth in relazione al target.

1 x = Synth("default", [\freq, 300]);


2 // aggiunge un secondo synth immediatamente dopo x
3 y = Synth("default", [\freq, 450], x, \addAfter);
4 x.free; y.free;

Un target potrebbe essere un altro Synth (o qualche altra cosa ...), e un ad-
dAction è un simbolo. Si Veda il file [Synth] per una lista completa dei possibili
addActions. Metodi come Synth-after sono il modo più semplice e conveniente
di fare la stessa cosa di utilizzare un metodo Synth-new con un addAction spe-
cificato; la differenza sta nel fatto che prendono un target come il loro primo
argomento.

1 // queste due linee di codice sono EQUIVALENTI


2 y = Synth.new("default", [\freq, 450], x, \addAfter);
3 y = Synth.after(x, "default", [\freq, 450]);

51
I Buss

Per ulteriori informazioni:

[Bus] [In] [OutputProxy] [Order-of-execution] [Synth]

52
I Gruppi

Capitolo 12 I Gruppi
La nostra discussione sui synths sul server ci porta a parlare di gruppi. L’architettura
del server (capitolo 9) è costituita da nodi che corrispondono a synth o a gruppi.
I gruppi sono semplicemente collezioni di nodi, e possono contenere sia synths o
altri gruppi. Sono molto utili in 2 contesti:

1. nel processo di controllo dell’ordinamento di esecuzione

2. permettono di raggruppare insieme nodi e di inviare a tutti lo stesso messaggio


nello stesso istante.

Esiste ovviamente una comodo oggetto che permette l’astrazione Server per rap-
presentare gruppi di nodi nell app client: Group.

12.1 Gruppi come tool per l’ordinamento


I Gruppi possono essere abbastanza utili in termini di controllo dell’ordinamento
sul server. Come i synths, i gruppi prevedono un targets e un addAction come
argomenti, che rendono facile il loro posizionamento. Vediamo un esempio:

1 g = Group.new;
2 h = Group.before(g);
3 g.free; h.free;

Il loro utilizzo diventa fondamentale per aggiungere effettistica o processare se-


gnali audio in modo separato dalle sorgenti sonore e nel giusto ordine. Si ricon-
sideri il riverbero dell’esempio del capitolo precedente.

53
I Gruppi

1 (
2 // una versione stereo
3 SynthDef("tutorial-DecaySin2", {
4 arg outBus = 0, effectBus, direct = 0.5, freq = 440;
5 var source;

7 // 1.0.rand2 restituisce un numero random fra -1 e 1, usato qui


8 // per avere un pan random
9 source = Pan2.ar(Decay2.ar(Impulse.ar(Rand(0.3, 1), 0, 0.125),
0.3, 1,
10 SinOsc.ar(SinOsc.kr(0.2, 0, 110, freq))), Rand(-1.0,
1.0));
11 Out.ar(outBus, source * direct);
12 Out.ar(effectBus, source * (1 - direct));
13 }).send(s);

15 SynthDef("tutorial-Reverb2", {
16 arg outBus = 0, inBus;
17 var input;

19 input = In.ar(inBus, 2);


20 16.do({ input = AllpassC.ar(input, 0.04, Rand(0.001,0.04),
3)});
21 Out.ar(outBus, input);
22 }).send(s);
23 )

25 // Si crea un gruppo per i synth e uno per gli effetti


26 (
27 ~sources = Group.new;
28 ~effects = Group.after(~sources); // assicurarsi che sia AFTER
29 b = Bus.audio(s, 2); // ecco il nostro bus stereo effettato
30 )

32 // I synths nel gruppo. L’addAction di default \‘e \\addToHead


33 (
34 x = Synth("tutorial-Reverb2", [\inBus, b.index], ~effects);
35 y = Synth("tutorial-DecaySin2", [\effectBus, b.index, \outBus, 0],
~sources);
36 z = Synth("tutorial-DecaySin2", [\effectBus, b.index, \outBus, 0,
\freq, 660], ~sources);
37 )
38 ~sources.free; ~effects.free; // queste istruzioni liberano x,y e z
39 b.free;

Da notare che non ci si preoccupa con quale ordine le sorgenti e gli effetti sono
raggruppati dentro i gruppi; ciò che importa è che tutti i synth che si occupano di
effetti vengano posposti, nell’ordinamento, alle sorgenti synth che essi processano.

54
I Gruppi

I nomi ~sources e ~effects, che presentano una tilde (~) davanti a una parola,
sono variabili d’ambiente. Per il momento, tutto ciò che serve conoscere su queste
cose è che possono essere usate nello stesso modo delle variabili dell’interprete
(non è necessario dichiararle, sono persistenti), e permetto nomi più descrittivi.

12.2 Tutte le addAction


A questo punto può tornare utile scoprire i possibili add actions. Oltre a add-
Before e addAfter, esite anche addReplace usato raramente, e due add
actions applicabili ai gruppi:

− addToHead : aggiunge il ricevente all’inizio del gruppo, così che sia eseguito
per primo

− addToTail: aggiunge alla fine del gruppo, così che sia eseguito per ultimo
Come per le altre addAction, anche addToHead e addToTail hanno metodi
usati nell’app del client chiamati head e tail.

1 g = Group.new;
2 h = Group.head(g); // aggiunge h davanti a g
3 x = Synth.tail(h, "default"); // aggiunge x in coda ad h
4 s.queryAllNodes; // vediamo la gerarchia dei
nodi nella post window
5 x.free; h.free; g.free;

12.3 ’queryAllNodes’e node IDs


L’oggetto Server ha un metodo chiamato queryAllNodes che stamperà una
rappresentazione dell’albero dei nodi del server nella post window. Eseguendo
l’esempio precedente si potrebbe ottenere qualcosa come:

1 nodes on localhost:
2 a Server
3 Group(0) : Root Node
4 Group(1) : default group
5 Group(1000)
6 Group(1001)
7 Synth 1002

Da questa rappresentazione, ciò che compare sotto un gruppo e indentato sulla


destra è contenuto dentro qualcosa che è indentato in alto sulla sinistra in un

55
I Gruppi

sistema di scatole cinesi. l’ordine dei nodi è dalla cima alla base. I numeri accato
a i nodi sono gli ID, utilizzati dal server per tener traccia dei nodi stessi. Normal-
mente quando si lavora con oggetti astrazioni Server non è necessario occuparsi
degli ID e come gli oggetti ne tengono traccia, come vengono assegnati e liberati.

l’esempio precedente presentava 4 gruppi anche se ne venivano creati solo 2. I


primi due, con gli ID 0 e 1, sono gruppi speciali, detti RootNode e il default
group.

12.4 Il Root Node


Al momento del boot del server , viene creato un gruppo speciale con un ID di
nodo uguale a 0 e che rappresenta la radice dell’albero dei nodi del server. Esite
un’astrazione server che rappresenta questo oggetto che èRootNode.

Questo Root Node ha alcune caratteristiche:

− viene sempre utilizzato.

− è sempre in stato running.

− non può essere rimosso o spostato all’interno dell’albero.

− viene usato il meccanismo di Cacheing per assicurare che ci sia sempre un


nodo radice per l’albero dei nodi.
Per dimostrare che questo esista e sia univoco si provi ad eseguire il seguente
codice:

1 s = Server.local;
2 a = RootNode(s);
3 b = RootNode(s);

5 a === b; // identical object

Inviando messaggi /s_new al server, il target 0 rappresenta proprio questo


oggetto.

1 s.sendMsg("/s_new","default", -1, 0, 0);//the last argument is the target id

Importante: In generale non si dovrebbero aggiungere nodi al RootNode senza


una specifica ragione.

56
I Gruppi

12.5 Il default Group


Al boot del Server c’è un gruppo a livello top con un ID di 0 che definisce la root
dell’albero; come detto precedentemente esso è rappresentato da una sottoclasse
di Group: RootNode. Se il Server viene avviato da dentro SCLang (da riga di
comando) sarà creato automaticamente un default group con un ID=1. Questo
è il target di default per tutti i Nodi. Se non si specifica un target o si passa nil,
il target sarà il default group del Server di default.
La gerarchia dei gruppi alla base del Server è quindi:

root node (id:0) [


default group (id:1)
]

Eseguendo il codice seguente si può vedere che il synth creato fa parte del gruppo
di default con id=1.

1 Server.default.boot;
2 a = Synth.new(\default);
3 // viene creato un synth nel default group del Server di default
4 a.group;
5 // restituisce un oggetto Group. Da notare l’ID = 1 (il default group)
6 //nella post window

Il default group ha uno scopo importante: offrire un albero di nodi così che me-
todi come Server-scope, Server-record, etc. possono funzionare senza essere
vincolati dall’ordine di esecuzione. Nel seguente esempio, il node scoping è dopo
il default group.

1 Server.internal.boot;

3 { SinOsc.ar(mul: 0.2) }.scope(1);

5 // osserva la post window


6 Server.internal.queryAllNodes;

8 // Il synth (SinOsc) dentro il default group (ID 1)


9 // Il nodo dello scope viene dopo il default group

11 Server.internal.quit;

Da notare che il default group è persistente; viene creato nel metodo initTree del
Server (eseguito con ogni codice salvato nelle sue variabili di istanza dell’albero: si

57
I Gruppi

veda [Server] per maggiori dettagli) ed è ricreato nel reboot, dopo la pressione di
Cmd-. e dopo che tutti i nodi sono stati liberati. Con i messaggi OSC è possibile
usare sempre un nodo target di ID 1:

1 s.sendMsg("//s_new", "snd", 1832,0,1); // aggiungi in testa al group 1

In generale è possibile aggiungere nodi al gruppo di default, o gruppi dentro di es-


so e non prima o dopo. Quando si aggiunge un synth di effettistica, per esempio,
in generale si dovrebbe resistere alla tentazione di aggiungerlo dopo il gruppo di
default, e creare un gruppo sorgente separato dentro il gruppo di default. Questo
preverrà problemi con lo scoping e la registrazione. Quanto spiegato è raffigurato
di seguito. Se si procedesse ad un’allocazione del tipo:

1 default group [
2 source synth1
3 source synth2
4 ]
5 recording synth
6 effects synth

il syhnth di recording potrebbe non catturare l’output del synth di effetto dato
che è prima di quest’ultimo nell’ordinamento. In casi come questo, il metodo
migliore è creare un gruppo dentro il default group e mettere un synth di effetti
dopo tutti i synth sorgenti.

1 default group [
2 source group [
3 source synth1
4 source synth2
5 ]
6 effects synth DENTRO IL DEFAULT GROUP
7 ]
8 recording synth

Infine, è possibile "liberare" il default group, ma in generale non c’è ragione di


farlo. Inoltre in generale si potrebbero aggiungere nodi al default group come al
RootNode senza averne, anche qui, uno specifico motivo per farlo (per esempio
aggiungere alcune nuove funzionalità come recording e scoping che dipendono
dall’ordine dei nodi). Rimane comunque possibile farlo, ma solo per un motivo
davvero valido.

58
I Gruppi

12.6 Gruppi come tool per l’invio di insiemi di messaggi


Come accennato nelle prime righe di questo capitolo, l’altro maggior utilizzo dei
gruppi è legato al fatto che permettono di trattare facilmente un certo numero
(...gruppo..) di synths come un tutt’uno. Se si invia un messaggioset a un gruppo,
questo verrà inviato a tutti i nodi contenuti nel gruppo stesso.

1 g = Group.new;

3 // creiamo 4 synth dentro g


4 // 1.0.rand2 restituisce un numero random nel range -1;1
5 4.do({
6 { arg amp = 0.1;
7 Pan2.ar(SinOsc.ar(440 + 110.rand, 0, amp), 1.0.rand2) }.play(g);
8 });

10 g.set("amp", 0.005); // abbassa il volume di tutti

12 g.free;

12.7 Gruppi: ereditarietà e non solo


Vediamo ancora un po’di teoria Object Oriented. Sia i Gruppi che i Synth son esem-
pi di cosa possiamo definire sottoclassi. Possiamo pensare alle sottoclassi come figli
di classi genitori, dette superclassi. Tutte le sottoclassi ereditano i metodi delle loro
superclassi. Essi potrebbero ridefinire alcuni metodi con la loro implementazione
(traendo vantaggio dal polimorfismo), ma in generale le sottoclassi rispondono a
tutti i metodi della sopraclasse, e ai propri. Alcune classi sono definite astratte, che
significa che non si possono creare istanze di esse, infatti esse offrono solo un insieme
comune di metodi e variabili intese come interfacce alle loro sottoclassi.

Questo modo di lavorare porta alcuni vantaggi: se si rende necessario cambiare un


metodo ereditato, si può fare in un posto solo, nella superclasse, e tutte le sottoclassi
che ereditano questo metodo risentiranno del cambiamento. È possibile anche esten-
dere una classe per creare una variante personale o migliorata e/o automaticamente
prendere tutte le funzionalità della superclasse.

l’ereditarietà è un meccanismo che può anche propagarsi all’indietro attraverso mol-


ti livelli, che è come dire che una superclasse di una classe potrebbe avere una
superclasse. (una classe non può, comunque avere più di una sovraclasse al livello
precendente della gerarchia dell’ereditarietà). Tutti gli oggetti in SC ereditano di
fatto da una classe detta Object, che definisce un certo insieme di metodi, e che

59
I Gruppi

tutte le sue sottoclassi ereditano o sovrascrivono. Gruppi e Synth sono sottoclassi


di classi astratte della classe astratta [Node]. Alcuni dei loro metodi sono definiti in
Node, e sono documentati nell’helpfile di Node.

Così, se guardando l’helpfile se non si trova un metodo particolare della classe, po-
trebbe essere necessario andare nell’helpfile della superclasse della classe e così via
seguendo la catena. Molte classi hanno elencate all’inizio dell’helpfile le proprie so-
vraclassi. Possono anche essere usati i seguenti metodi per ottenere questo tipo di
infomazioni tracciate nella documentazione nella post window:

1 Group.superclass; // restituisce ’Node’


2 Group.superclass.openHelpFile;
3 Group.findRespondingMethodFor(’set’); // Node-set
4 Group.findRespondingMethodFor(’postln’); // Object-postln;
5 Group.helpFileForMethod(’postln’); // apre l’help file di Object

Per maggiori informazioni:

[Group] [Node] [default_group] [RootNode] [Intro-to-Objects] [Order-of-execution]


[Synth] [More-On-Getting-Help] [Internal-Snooping]

60
Buffer

Capitolo 13 Buffer
Gli oggetti Buffers sono astrazioni: rappresentano i buffer sul server, che non sono
altro che array ordinati di valori float. Float è l’abbrevazione di numero in floating
point, ossia un numero decimale; essi differiscono dagli integer, che possono essere
positivi o negativi (o zero) e sono scritti senza punto decimale.Per chiarezza: 1 è un
integer, 1.0 è un float.

I buffers Server possono essere singoli o multicanale, e sono tipicamente il mezzo


per salvare dati lato-server. Il loro uso più comune è quello di mantenere file audio
in memoria, ma in realtà qualunque tipo di dato che può essere rappresentato da
valori float può essere salvato in un buffer.

Come per i Bus, il numero di buffer disponibili è settato prima della fase di boot
del server (si veda [ServerOptions]), ma prima che possano essere usati, è necessario
allocargli memoria, effettuando così un passo asincrono. Inoltre, come per i bus, i
buffer sono numerati partendo da 0. Occorre fare attenzione, usando i buffer, dei
numeri allocati e eliminare il rischio di conflitti (non esiste evidentemente un mec-
canismo intrinseco per l’allocazione degli indici come per di bus).

Si può pensare ai buffer sul server all’equivalente di un array, ma senza tutte le


funzionalità utili ed eleganti dell’OOP. Per modificare i dati nei buffer, si può ri-
correre a (qualunque) operazione lato-client. I buffer lato-server sono globali, che
significa semplicemente che ogni synth può accedervi anche più di una volta. Infine
ricordiamo che in generale sui buffer:

− si può scrivere

− si possono leggere e contemporaneamente modificarli in dimensione.

Molti dei metodi dei buffer presentano numerosi argomenti, per informazioni com-
plete si faccia riferimento all’help file [Buffer].

13.1 Creazione di un Buffer Object e allocazione di memoria


Costruire un oggetto Buffer e allocargli la memoria necessaria nell’applicazione ser-
ver è abbastanza semplice. È possibile farlo in un passaggio solo con un metodo di
allocazione del tipo:

61
Buffer

1 s.boot;
2 b = Buffer.alloc(s, 100, 2); // alloca 2 canali e 100 frames
3 b.free; // libera la memoria

L’esempio mostrato alloca 2 canali buffer da 100 frames ognuno. Il numero attuale
dei valori salvati è numChannels * numFrames, così in questo caso ci saranno 200
valori float. Ogni frame quindi, in questo caso, è una coppia di valori, uno per ogni
canale. Se si vuole allocare in termini di secondi e non in numero di frame, si può
scrivere:

1 // un buffer stereo di 8 secondi


2 b = Buffer.alloc(s, s.sampleRate * 8.0, 2);
3 b.free;

Il metodofree dell’oggetto Buffer libera la memoria sul server, e restituisce il numero


del buffer per una futura riallocazione.

13.2 Uso dei Buffers con Sound Files


I Buffer hanno un metodo read, che permette di leggere un file sonoro residente in
memoria e restituire un oggetto Buffer che lo contiene. Usando la UGen PlayBuf,
è possibile fare il play del file.

1 // legge un file audio


2 b = Buffer.read(s, "sounds/a11wlk01.wav");

4 // lo suona
5 (
6 x = SynthDef("tutorial-PlayBuf",{ arg out = 0, bufnum;
7 Out.ar( out,
8 PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum))
9 )
10 }).play(s,[\bufnum, b.bufnum ]);
11 )
12 x.free; b.free;

Il metodo PlayBuf-ar ha un certo numero di argomenti che permettono di controllare


vari aspetti. Il rifermimento per dettagli su questa UGen è l’help file [PlayBuf].
Vediamo un semplice esempio:

62
Buffer

1 PlayBuf.ar(
2 1, // numero di canali
3 bufnum, // numero di buffer da suonare
4 BufRateScale.kr(bufnum) // tasso di playback
5 )

• Numero dei canali: quando si lavora con PlayBuf occore specificare quanti
canali ogni buffer leggerà. Non può essere un parametro in una Synthdef e venire
modificato in seguito. Perchè? SynthDefs deve avere un numero fisso di canali di
output. Così un canale PlayBuf è sempre un unico canale PlayBuf. Se si rendono
necessarie versioni che possano suonare un numero maggiore di canali, occorrono
più SynthDefs oppure si può ricorrere all’uso di una Function-play.

• Numero del Buffer: Come si è osservato prima, i Buffer sono numerati partendo
da zero. È possibile ottenere il numero del Buffer usando il suo metodobufnum.
Nell’esempio sopra presentato viene considerato alla fine della SynthDef, a cui
viene passato come argomento dal Synth risultante. (Da notare SynthDef-play
permette di includere un array di argomenti, proprio come Synth-new.)

• Tasso di riproduzione: Un tasso di 1 potrebbe essere la velocità normale, 2


il doppio più veloce, ecc. Ma qui viene presentata una particolare UGen detta
BufRateScale che verifica il samplerate del buffer ( che è settato nel corrispon-
dente sound file che è caricato nel buffer) e restituisce il tasso che corrisponde
alla veloctià normale. Questo risulta molto utile, perchè il file sonoro che cari-
chiamo (a11wlk01.wav) ha un samplerate di 11025 Hz. Con un rate di 1, PlayBuf
dovrebbe fare il play al samplerate del server che è tipicamente settato a 44100
Hz, oppure quattro volte più veloce! Quindi BufRateScale impone questo tasso
a quello normale.

13.3 Streaming di File da e su Disco


In alcuni casi, per esempio quando si lavora con file di dimensioni considerevoli, si
potrebbe non voler caricare un suono completamente in memoria. Si può quindi fare
uno stream del suono dal disco ad un certo bit/tempo usando la UGen DiskIn e il
metodo cueSoundFile dei buffer:

63
Buffer

1 (
2 SynthDef("tutorial-Buffer-cue",{ arg out=0,bufnum;
3 Out.ar(out,
4 DiskIn.ar( 1, bufnum )
5 )
6 }).send(s);
7 )

9 b = Buffer.cueSoundFile(s,"sounds/a11wlk01-44_1.aiff", 0, 1);
10 y = Synth.new("tutorial-Buffer-cue", [\bufnum,b.bufnum], s);

12 b.free; y.free;

Note: non è flessibile come PlayBuf (non si ha un controllo sul tasso di playback),ma
permette di risparmiare memoria.

13.4 OOP: Variabili di istanza e Funzioni azione


Vediamo a questo punto ancora un po’di teoria sull’OOP. Si ricorda che gli oggetti
salvano dati nelle variabili di istanza. Alcune variabili di istanza hanno metodi di get
e set che permettono di recuperare o settare il valore relativo. Si è già visto questo
meccanismo, con il metodo bufnum dei Buffer, che restituisce il numero del Buffer
contenuto nella variabile di istanza.

Il Buffer ha un certo numero di variabili di istanza i cui metodi di get possono re-
stituire i valori relativi. Quelli a cui siamo interessati per ora sono numChannels,
numFrames, e sampleRate. Questi possono essere particolarmente utili quando lavo-
riamo con file sonori di cui potremmo non avere queste informazioni a priori, prima
cioè del momento in cui il file venga caricato nel buffer.

1 b = Buffer.read(s, "sounds/a11wlk01.wav");
2 b.bufnum;
3 b.numFrames;
4 b.numChannels;
5 b.sampleRate;
6 b.free;

Da tener presente che a causa della bassa latenza dei messaggi fra client e server,
le variabili di istanza potrebbero non essere aggiornate immediatamente quando
si fa un’operazione dispendiosa come la lettura di un file in un buffer. Per questa
ragione molti metodi dell’oggetto Buffer prevedono come argomenti funzioni-azioni.
Si ricorda che una funzione-azione è una funzione che verrà valutata solo dopo che

64
Buffer

il client ha ricevuto una risposta e ha aggiornato le variabili del Buffer. Vediamo un


esempio:

1 // con una funzione azione


2 // da notare che le variabili non vengono
3 //aggiornate immediatamente
4 (
5 b = Buffer.read(s, "sounds/a11wlk01.wav", action: { arg buffer;
6 ("numFrames after update:" + buffer.numFrames).postln;
7 x = { PlayBuf.ar(1,
8 buffer.bufnum,
9 BufRateScale.kr(buffer.bufnum))
10 }.play;
11 });

13 // da notare che la linea seguente viene eseguita


14 //PRIMA della funziona azione
15 ("numFrames before update:" + b.numFrames).postln;
16 )
17 x.free; b.free;

Nell’esempio, il client invia il comando leggi all’applicazione server con una richiesta
per le informazioni necessarie al fine di aggiornare le variabili di istanza del Buffer. A
questo punto la funzione-azione sarà eseguita quando verrà ricevuta una risposta e
continuerà e verrà eseguito il blocco di codice relativo. Questo perchè la linea Before
update ... viene eseguita prima.

13.5 Registrare nel Buffer


Oltre a PlayBuf, c’è un’altra UGen dettaRecordBuf, che permette di registrare nel
Buffer.

65
Buffer

1 // un Buffer da 5 second e 1 channel


2 b = Buffer.alloc(s, s.sampleRate * 5, 1);

4 // registra per 4 secondi


5 (
6 x = SynthDef("tutorial-RecordBuf",{ arg out=0,bufnum=0;
7 var noise;
8 noise = PinkNoise.ar(0.3); // registra PinkNoise
9 RecordBuf.ar(noise, bufnum); // di default questo cicla
10 }).play(s,[\out, 0, \bufnum, b.bufnum]);
11 )

13 // libera il synth dopo alcuni secondi


14 x.free;

16 // riproduci il buffer
17 (
18 SynthDef("tutorial-playback",{ arg out=0,bufnum=0;
19 var playbuf;
20 playbuf = PlayBuf.ar(1,bufnum);
21 FreeSelfWhenDone.kr(playbuf);
22 // libera il synth quando PlayBuf ha riprodotto una volta
23 Out.ar(out, playbuf);
24 }).play(s,[\out, 0, \bufnum, b.bufnum]);
25 )
26 b.free;

Si veda l’help di [RecordBuf] per dettagli e opzioni.

13.6 Accesso ai dati


l’oggetto Buffer ha una serie di metodi che permettono di recuperare e/o settare
valori nel buffer. I metodi Buffer-get and Buffer-set prevedono come argomento un
indice, il riferimento al buffer su cui andranno ad operare.
I buffer multicanali presentano un interleave fra i loro dati, così per esempio, per un
buffer a due canali si avrà:

index 0 = frame1-chan1,
index 1 = frame1-chan2,
index 2 = frame2-chan1, e così via..

66
Buffer

1 b = Buffer.alloc(s, 8, 1);
2 b.set(7, 0.5); // set the value at 7 to 0.5
3 b.get(7, {|msg| msg.postln}); // get the value at 7 and post it when
the reply is
4
received
5 b.free;

Oltre ai semplici metodiget eset, sono implementati anche metodi qualigetn esetn
pemettono di recuperare e/o settare un range di valori adiacenti.

− setn prende un indice di partenza e un vettore di valori da impostare

− getn prende un indice, il numero di valori da recuperare e una funzione-azione.

Per esempio:

1 b = Buffer.alloc(s,16);

3 // setta i primi 3 valori


4 b.setn(0, [1, 2, 3]);

6 // recupera i 3 valori
7 b.getn(0, 3, {|msg| msg.postln});

9 // riempe il buffer con valori random


10 b.setn(0, Array.fill(b.numFrames, {1.0.rand}));

12 // li recupera
13 b.getn(0, b.numFrames, {|msg| msg.postln});
14 b.free;

C’è un limite superiore al numero di valori che si possono recuperare o settare alla
volta (tipicamente 1633 usando UDP, protocollo di default). Questo perchè dipende
proprio dal limite della grandezza del pacchetto di rete del protocollo UDP stesso.
Per superare questo limite, si sono implementati nell’oggetto Buffer altri 2 meto-
di, loadCollection e loadToFloatArray che permettono di settare o recuperare
grandi quantità di dati scrivendoli su disco e quindi caricandoli sul client o sul server
a seconda delle necessità.

67
Buffer

1 (
2 // Crea un rumore bianco
3 v = FloatArray.fill(44100, {1.0.rand2});
4 b = Buffer.alloc(s, 44100);
5 )

7 (
8 // carica il FloatArray dento b, quindi lo riproduce
9 b.loadCollection(v, action: {|buf|
10 x = {
11 PlayBuf.ar(buf.numChannels,
12 buf.bufnum,
13 BufRateScale.kr(buf.bufnum),
14 loop: 1) * 0.2 }.play;
15 });
16 )
17 x.free;

19 // recupera FloatArray, e lo compara con v;


20 // Gli args 0, -1 sono start dall’inizio e carica l’intero buffer
21 b.loadToFloatArray(0, -1, {|floatArray| (floatArray == v).postln });
22 b.free;

( Un FloatArray è una sottoclasse di Array che può contenere solo valori Float).

13.7 Plotting e Playing


Altri 2 metodi utili dell’oggetto Buffer sonoplot eplay.

1 // si vede la forma d’onda


2 b = Buffer.read(s,"sounds/a11wlk01.wav");
3 b.plot;

5 // si fa il play del contenuto del file


6 // solo un’argomento: loop.
7 //Se false (default) il synth viene liberato automaticamente
8 b.play; // libera se stesso
9 x = b.play(true); // oppure, se cicla, non si libera da solo
10 x.free; b.free;

13.8 Altri usi dei Buffers


Oltre ad essere usati per caricare file sonori, i buffer possono anche essere sfruttati per
quelle situazioni in cui si rende necessario gestire insiemi di dati grandi dimensioni

68
Buffer

accessibili globalmente sul server. Un esempio di un altro uso potrebbe essere una
lookup table per waveshaping.

1 b = Buffer.alloc(s, 512, 1);


2 b.cheby([1,0,1,1,0,1]);
3 (
4 x = play({
5 Shaper.ar(
6 b.bufnum,
7 SinOsc.ar(300, 0, Line.kr(0,1,6)),
8 0.5
9 )
10 });
11 )
12 x.free; b.free;

La UGen Shaper effettua waveshaping su una sorgente di input. Il metodo cheby


riempe il buffer con una serie di polinomi di chebyshev, necessari proprio per questo
tipo di sintesi. La classe Buffer ha molti altri metodi simili a questo per riempire il
buffer con valori di una forma d’onda ricavandoli o meno da una tabella di lookup.
Per maggior informazioni:

[Buffer] [PlayBuf] [RecordBuf] [SynthDef] [BufRateScale] [Shaker]

69
La comunicazione

Capitolo 14 La comunicazione
Dopo aver visto i vari componenti dell’architettura del server, passiamo ad occuparci
in maniera specifica della comunicazione fra i nodi e della comunicazione client-server
in supercollider.

Il modo più diretto e veloce per mandare comandi al server è inviare messaggi
all’oggetto Server, se si è dentro sc-lang; se si è in una shell è possibile utilizzare
sendOSC.
Quando si creano dei nodi sul server, synths o gruppi, le sole cose necessarie per la
comunicazione sono il nodeID e il server (il suo indirizzo per essere precisi).
Al fine di poter includere un synth in una comunicazione, s’inviano messaggi al server
con il proprio nodeID. Se invece non s’intende comunicare con un nodo dopo la sua
creazione (e il nodo terminerà da solo senza messaggi esterni), il nodeID può essere
settato a -1, che è l’equivalente di nil per il server. Quando si passa il riferimento
a un certo nodo, assumendo che si potrebbe avere non solo un server, può essere
utile creare un oggetto Synth o Group. Questi oggetti rispondono anche a messaggi
e, se necessario, possono essere utilizzati per ottenere lo stato interno del nodo lato
server. Vediamo un esempio per rendere tutto più chiaro.

1 n = s.nextNodeID;
2 s.sendMsg("/s_new", "default", n);
3 s.sendMsg("/n_free", n);

5 // equivalente
6 n = Synth("default");
7 n.free;

9 //------------------------------------------------
10 // passando gli argomenti:
11 n = s.nextNodeID;
12 s.sendMsg("/s_new", "default", n, 0, 0, \freq, 850);
13 s.sendMsg("/n_set", n, \freq, 500);
14 s.sendMsg("/n_free", n);

16 // equivalente:
17 n = Synth("default", [\freq, 850]);
18 n.set(\freq, 500)
19 n.free;

Viene spontaneo da chiedersi quando è meglio utilizzare oggetti node e quando uti-
lizzare direttamente i messaggi per la comunicazion. La risposta potrebbe dipendere
da qualche limite del contesto in cui si lavora, e il limite sul contesto dipende spesso

70
La comunicazione

dal gusto personale.

L’incapsulamento di oggetti nodo derivante da una certa generalizzazione teorica,


implica che altri oggetti composti possano rispondere allo stesso messaggio e quindi
sfruttare il polimorfismo. Questo meccanismo inoltre offre un certo livello di conve-
nienza, tenendo traccia di indici e ID.
In certi casi, come per una sintesi granulare mostrata nell’esempio seguente, si rac-
comanda di usare direttamente i messaggi, in quanto non si guadagnano benefici
dagli oggetti nodo (per esempio non serve inviarli via messaggio) e non si aggiunge
carico sulla cpu lato client inutilmente.

1 (
2 SynthDef("grain", {
3 Out.ar(0,
4 Line.kr(0.1, 0, 0.01, doneAction:2) * FSinOsc.ar(12000))
5 }).send(s);
6 )

8 (
9 Routine({
10 20.do({
11 s.sendMsg("/s_new", "grain", -1);
12 0.01.wait;
13 })
14 }).play;
15 )

Nei casi in cui è necessario tenere traccia dello stato del synth, è buona cosa utilizzare
gli oggetti nodo e registrarli con un NodeWatcher. Oltre ai casi particolari visti,
è solo questione di gusto del programmatore scegliere se usare la combinazione di
messaggi e una rappresentazione numerica globale o una rappresentazione a oggetti.
I due approcci possono essere miscelati, ottenendo certi vantaggi dello stile a oggetti
usando lo stile a messaggi. Per esempio Server.nextNodeID permette di usare ID
assegnati dinamicamente nello stile di messaggi. È una generalizzazione grossolana,
che probabilmente è lontana da dire che lo stile a oggetti è più conveniente, ma lo
stile con i messaggi è più efficiente a causa della riduzione di carico sulla CPU lato
client.
Importante: Se si vuole avere la funzionalità del default group (per esempio per l’uso
delle funzionalità di recording e scoping del server senza problemi) occorre trattare
l’ID 1 (il default group) come root al pari dell’ID 0 (il root node).
Da notare che Function-play e SynthDef-play restituiscono un oggetto synth che
può essere usato e a cui è possibile inviare messaggi, come mostrato nell’esempio
seguente.

71
La comunicazione

1 x = { arg freq=1000; Ringz.ar(Crackle.ar(1.95, 0.1), freq, 0.05)


}.play(s);
2 x.set(\freq, 1500);
3 x.free;

14.1 Impacchettamento automatico dei messaggi


Quando vengono usati oggetti Synth/Node/Group in sclang, compare spesso la ne-
cessità di costruire bundles (pacchetti) per poter inviare messaggi insieme; per esem-
pio quando si vuole avviare un synth che dovrebbe essere mappato istantaneamente
su un certo bus, oppure se si ha la necessità di assicurare che due synths vengano
avviati sincronizzati con precisione. Il modo più semplice per trattare queste cose è
passare attraverso il supporto di bundling automatico del Server. Questo permette
di aprire un pacchetto in cui saranno collezionati tutti i messaggi osc fino a che non
verrà inviato. Questa funzionalità è abbastanza conveniente nello “stile-oggetto” e
assicura un’esecuzione sincrona. Si veda [Server] e [bundledCommands].
Può essere interessante vedere la sintassi del metodo che ci permette di creare pac-
chetti da inviare al server.

makeBundle( time, func, bundle) - La Function func viene valutata, e tutti


i messaggi OSC generati sono differti e aggiunti a un pacchetto che potrà essere
utilizzato successivamente se necessario.
Se time:

• viene impostato a nil o a un valore numerico, il pacchetto verrà automaticamen-


te inviato ed eseguito dopo il corrispondente ritardo in secondi impostato nel
parametro.

• viene impostato a false, il pacchetto non sarà inviato.

L’argomento bundle permette di passare un pacchetto esistente e continuare ad


aggiungervi i messaggi OSC. Se viene riscontrato un errore durante la valutazione
della Function func, questo metodo genererà un Error e fermerà il differimento dei
messaggi.

72
La comunicazione

1 s.boot;
2 (
3 // inviamo una SynthDef al server
4 SynthDef("tpulse", { arg out=0,freq=700,sawFreq=440.0;
5 Out.ar(out, SyncSaw.ar(freq, sawFreq,0.1) )
6 }).send(s);
7 )

9 // tutti i comandi OSC generati nella function contenuta sotto saranno


10 // aggiunti al pacchetto ed inviati simultaneamente dopo 2 secondi.
11 (
12 s.makeBundle(2.0, {
13 x = Synth.new("tpulse");
14 a = Bus.control.set(440);
15 x.busMap(\freq, a);
16 });
17 )
18 x.free;

20 // il pacchetto non viene inviato


21 (
22 b = s.makeBundle(false, {
23 x = { PinkNoise.ar(0.1) * In.kr(0, 1); }.play;
24 });
25 )
26 // Si passa b come un pacchetto pre-esistente, e si avviano
27 //entrambi i synth in maniera sincrona

29 (
30 s.makeBundle(nil, { // nil esegue ASAP
31 y = { SinOsc.kr(0.2).abs }.play(x, 0, 0, \addBefore);
32 //inviluppo sinusoidale
33 }, b);
34 )
35 x.free; y.free;

37 // Genera un errore
38 (
39 try {
40 s.makeBundle(nil, {
41 s.farkermartin;
42 });
43 } { | error |
44 (error.errorString).postln;
45 x = { FSinOsc.ar(440, 0, 0.2) }.play; // This works fine
46 }
47 )
48 x.free;

73
Ordine di esecuzione

Capitolo 15 Ordine di esecuzione


lL ordine di esecuzione è uno degli aspetti più critici e apparentemente più difficili
dell’uso di SuperCollider.

L’ordine di esecuzione in questo contesto non significa l’ordine in cui gli statement
sono eseguiti nel linguaggio (il client). In realtà ci si riferisce all’ordine in cui sono
eseguiti i nodi synth sul server, che corrisponde in pratica all’ordine in cui i relativi
output vengono calcolati ad ogni ciclo di controllo (blockSize). A prescindere dal
fatto che si specifichi o meno l’ordine di esecuzione, ogni synth e ogni group vengono
inseriti in una locazione specifica nella catena computazionale del server.

Se si ha sul server:

1 synth 1 ---> synth 2

tutte le Ugen associate con il synth 1 saranno eseguite prima di quelle nel synth 2,
durante ogni ciclo di controllo.

Se non si hanno synth che usano In.ar, non occorre preoccuparsi dell’ordine di ese-
cuzione. Il problema si verifica solo quando un synth legge l’output prodotto da un
altro synth. La regola è semplice: se si ha un synth sul server (un effetto) che dipende
dall’output da un altro synth (la sorgente), allora l’effetto deve apparire dopo, nella
catena di nodi sul server, rispetto alla sorgente. In generale la sequenza è:

1 source ---> effect

Se si utilizzasse la configurazione opposta:

1 effect ---> source

il synth che si occupa dell’effetto non ”ascolterebbe”il synth sorgente, e quindi non
si otterrebbe il risultato richiesto.

15.1 Richiami su Server e Target


È sempre presente un default Server, a cui si può accedere o a cui settare la variabile
s attraverso il metodo Server.default. Allo startup il server di default viene di

74
Ordine di esecuzione

norma settato al Server locale (localhost) ed è anche assegnato automaticamente


alla variabile di interprete s.

1 // si esegua il codice seguente osservando i risultati


2 //nella post window
3 s === Server.default;
4 s === Server.local;
5 Server.default = Server.internal;
6 s === Server.default;
7 Server.default = Server.local;

Si ricorda che quando un Server viene avviato, c’è un gruppo al top level con un ID =
0 che definisce la radice dell’albero dei nodi. Questa gruppo è RootNode. C’è inoltre
un default group con un ID = 1, che è il gruppo di default per tutti i nodi. Come
anticipato nei capitoli precendenti, tutto ciò si ottiene se si considera un Server come
un target. Se non si specifica un target o lo si imposta a nil, verrà considerato il
default group sul server di default.

15.2 Controllo dell’ordine di esecuzione


Come considerazione generale, possiamo dire che utilizzare i gruppi è molto con-
veniente nell’organizzazione dell’ordine di esecuzione perchè semplifica, in special
modo su progetti grandi, sia in termini concettuali che operativi.

Esistono tre modi per controllare l’ordine di esecuzione:

1. usando addAction nel messaggio di creazione dei synth

2. muovendo nodi

3. inserendo i propri synth in un gruppo.

Vediamone ora uno per volta:

1. Utilizzo delle Add actions


Specificando un argomento addAction per Synth.new (or SynthDef.play, Func-
tion.play, etc.), si specifica il posizionamento del nodo relativamente a un target.
Il target potrebbe essere un nodo gruppo, un altro nodo synth o un server.
Come detto in precendenza, il target di default è il default group (il nodo con
nodeID = 1) del Server di default.

I Symbols seguenti sono addActions valide per Synth.new: addToHead, addTo-


Tail, addBefore, addAfter, addReplace.

75
Ordine di esecuzione

Vediamo come vengono applicati:


Synth.new(defName, args, target, addAction)
se target è:

− un Synth i metodi addToHead e addToTail saranno applicati al quel gruppo


di synth

− un Server, allora si riferirà default group del Server.

− nil, allora si riferirà al gruppo di defaulf del server di default.

Per ogni addAction c’è un corrispondente metodo detto “di convenienza” lato-
client della classe Synth:

− Synth.head(aGroup, defName, args) Aggiunge il nuovo synth in testa al grup-


po specificato da aGroup
Se aGroup è un nodo synth, il nuovo synth sarà aggiungo in testa al nodo del
gruppo
Se il target è un Server, verrà risolto con il default group del Server
Se il target è nil, verrà risolto con il default group del Server di default

− Synth.tail(aGroup, defName, args) Aggiunge il nuovo synth in coda del grup-


po specificato da aGroup
Se aGroup è un nodo synth, il nuovo synth sarà aggiungo in testa al nodo del
gruppo
Se il target è un Server, verrà risolto con il default group del Server
Se il target è nil, verrà risolto con il default group del Server di default

− Synth.before(aNode, defName, args) Aggiunge il nuovo nodo appena prima


del nodo specificato da aNode.

− Synth.after(aNode, defName, args) Aggiunge il nuovo nodo appena dopo del


nodo specificato da aNode.

− Synth.replace(synthToReplace, defName, args) Il nuovo nodo rimpiazza il


nodo specificato da synthToReplace.
Il nodo target viene ’liberato’.

76
Ordine di esecuzione

Usando Synth.new senza specificare un’addAction, verrà presa in considerazione


l’addAction di default. (Si possono verificare i valori di default per gli argomen-
ti di ogni metodo controllando dentro il codice sorgente di ogni clsse. Si veda
inoltre [Internal-Snooping] per maggiori dettagli.) In situazioni in cui l’ordine di
esecuzione è significativa, è importante specificare un addAction, o usare uno dei
metodi ’di convenienza’mostrati precedentemente.

2. Muovendo i nodi
I metodi per spostare i nodi sono: .moveBefore .moveAfter .moveToHead .move-
ToTail

Se si rende necessario cambiare l’ordine di esecuzione dopo che synth e gruppi


sono stati creati, è possibile farlo usando messaggi del tipo:

1 ~fx = Synth.tail(s, "fx");


2 ~src = Synth.tail(s, "src"); // l’effetto non viene udito
3 ~src.moveBefore(~fx); // metto la sorgente prima dell’effetto

3. Gruppi
I gruppi possono essere mossi nello stesso modo dei synth. Quando si sposta un
gruppo, tutti i synth in quel gruppo si muoveranno di conseguenza. Se conside-
riamo:

1 Group 1 ---> Group 2

tutti i synth nel gruppo 1 saranno eseguiti prima di tutti i synth del gruppo 2. In
questo modo viene determinato l’ordine di esecuzione. Il consiglio generale per
creare ordinamenti funzionali è: determinare prima l’architettura ottimale e poi
creare i gruppi che supportano tale architettura.

15.3 Utilizzare l’ordine di esecuzione a proprio vantaggio


Prima di cominciare a scrivere codice, è sempre consigliabile predisporre un piano in
termini di cosa si vuol ottenere e capire dove i synth andranno inseriti nella configu-
razione del server. Una configurazione comune è avere una routine che si occupi del
play dei nodi, nodi che necessitano di essere processati da un singolo effetto. Inol-
tre, si vuole che questo effetto sia separato dal resto e esegua nello stesso istante.
Per assicurarsi del corretto funzionamento di tutto ciò, è necessario creare la catena
synth -> effect su un bus audio privato e quindi trasferirlo al main output.

77
Ordine di esecuzione

Schematicamente:

1 [ Lots of synths] ----> effect ----> transfer

Questo esempio è perfetto per usare un gruppo:

1 Group ( [lots of synths] ) ----> effect ----> transfer

Per rendere chiara la struttura nel codice, è anche possibile creare un gruppo per
l’effetto (anche se contiene un solo synth), ottenendo:

1 Group ( [lots of synths] ) ----> Group ( [effect] ) ----> transfer

Vediamo un esempio che mette in pratica tutti questi concetti; in particolare modu-
leremo un parametro (lunghezza della nota) usando un synth di control rate. Ecco
l’esempio:

78
Ordine di esecuzione

1 s.boot;
2 ( l = Bus.control(s, 1); // un bus per LFO
3 //non rilevante l’ordine di esecuzione
4 b = Bus.audio(s, 2); // un bus stereo; questo per tenere
5 // la catena src->fx separata da altre catene
simili
6 ~synthgroup = Group.tail(s);
7 ~fxgroup = Group.tail(s);
8 // ora abbiamo synthgroup --> fxgroup dentro il default group di s
9 // creiamo qualche synthdefs da suonarci dentro with
10 SynthDef("order-of-ex-dist", { arg bus, preGain, postGain;
11 var sig;
12 sig = In.ar(bus, 2);
13 sig = (sig * preGain).distort;
14 ReplaceOut.ar(bus, sig * postGain);
15 }).send(s);
16 SynthDef("order-of-ex-pulse", { arg freq, bus, ffreq, pan, lfobus;
17 var sig, noteLen;
18 noteLen = In.kr(lfobus, 1);
19 sig = RLPF.ar(Pulse.ar(freq, 0.2, 0.5), ffreq, 0.3);
20 Out.ar(bus, Pan2.ar(sig, pan)
21 EnvGen.kr(Env.perc(0.1, 1),timeScale:noteLen,doneAction: 2));
22 }).send(s);
23 SynthDef("LFNoise1", { arg freq, mul, add, bus;
24 Out.kr(bus, LFNoise1.kr(freq, mul:mul, add:add));
25 }).send(s); )
26 // Creiamo il synth di LFO e inseriamolo:
27 ~lfo = Synth.head(s, "LFNoise1",
28 [\freq, 0.3, \mul, 0.68, \add, 0.7, \bus, l.index]);
29 // Quindi inseriamo l’effetto:
30 ~dist = Synth.tail(~fxgroup, "order-of-ex-dist",
31 [\bus, b.index, \preGain, 8, \postGain, 0.6]);
32 // trasferiamo i risultati al main out, con il livello scalato
33 // suonandolo il coda del default group
34 //(da notare che Function-play prende anche l’addActions!
35 ~xfer = { Out.ar(0, 0.25 * In.ar(b.index, 2))
36 }.play(s, addAction: \addToTail);
37 // Avviamo la nostra routine:
38 ( r = Routine({
39 {
40 Synth.tail(~synthgroup, "order-of-ex-pulse",
41 [\freq, rrand(200, 800), \ffreq, rrand(1000, 15000),
42 \pan, 1.0.rand2,\bus, b.index, \lfobus, l.index]);
43 0.07.wait;
44 }.loop;
45 }).play(SystemClock); )
46 ~dist.run(false); // per provare che la distorsione funzioni
47 ~dist.run(true);
48 // per ripulire il tutto:
49 ( r.stop;
50 [~synthgroup, ~fxgroup, b, l, ~lfo, ~xfer].do({ arg x; x.free });
51 currentEnvironment.clear; // pulisce tutte le variabili d’ambiente
)
79
Ordine di esecuzione

Da notare che nella routine, usando un gruppo per i synths sorgente, si può specifica-
re facilmente l’ordinamento relativo fra un synth e l’altro (essi sono aggiunti al grup-
po con il metodo tail) senza preoccuparsi del loro ordine rispetto al synth dell’effetto.
Da notare inoltre che questo metodo di operare previene errori nell’ordine di esecu-
zione, attraverso l’uso di un breve codice per l’organizzazione. Ovviamente aumen-
tando la dimensione del progetto, il principio non varia; semplicemente occorrerà
utilizzare più gruppi e la configurazione potrà essere più vasta.

15.4 Stile Messaggio


Gli esempi precedenti sono in ’stile-oggetto’. Se si preferisce lavorare in ’stile-messaggio’,
ci sono messaggi corrispondenti per tutti i metodi mostrati precedentemente.

15.5 Un caso particolare: Feedback


Quando le varie ugens di output (Out, OffsetOut, XOut) scrivono dati su un bus,
quest’ultimo si occuperà di mixarli con ogni dato del ciclo corrente, ma sovrascri-
vendo i dati del ciclo precedente. (ReplaceOut sovrascrive indifferentemente tutti
i dati).
Quindi, a seconda della posizione del nodo nell’ordinamento, i dati su un certo bus
potrebbero appartenere al ciclo corrente o al ciclo precedente.
Il metodo In.ar verifica il timestamp di ogni dato che legge e lo azzera se riscontra
che appartiene al ciclo precedente (per quel che riguarda il synth a cui afferisce; i
dati rimangono comunque sul bus). Questo è ottimo per i dati audio, evitando il
feedback, ma per i dati di controllo potrebbe invece essere utile poter leggere dati
da ogni posto nell’ordinamento dei nodi. Per questa ragione In.kr legge anche dati
il cui ordine non è legato al ciclo corrente.
In qualche caso si potrebbe anche voler leggere audio da un nodo successivo rispetto
all’ordine dei nodi. Questo è lo scopo di InFeedback. Il ritardo introdotto da que-
sto oggetto è al massimo della dimensione di un blocco, che equivale a circa 0,.0014
sec per un blocco e sample rate di default. Il comportamento variabile di mixing e
riscrittura delle UGens di output può rendere cruciale l’ordine di esecuzione quando
si usa In.kr o InFeedback.ar.
Per esempio con un ordinamento dei nodi come il seguente, la UGen InFeedback
nel Synth 2 riceverà solo dati dal Synth:

1 //Synth1 sovrascrive l’output di Synth3 prima che Synth2 lo utilizzi


2 Synth 1 scrive sul busA
3 Synth 2 (with InFeedback) legge dal busA
4 Synth 3 scrive sul busA

80
Ordine di esecuzione

Se spostiamo il Synth 1 dopo il Synth 2, allora il InFeedback del Synth 2 riceverà


un mix degli output del Synth 1 e Synth 3.

1 Synth 2 (with InFeedback) legge dal busA


2 Synth 1 scrive sul busA
3 Synth 3 scrive sul busA

Questo potrebbe anche essere vero se il Synth 2 venisse dopo Synth 1 e il Synth 3.

1 Synth 1 scrive sul busA


2 Synth 3 scrive sul busA
3 Synth 2 (with InFeedback) legge dal busA

In entrambi i casi tuttavia, i dati da Synth 1 e Synth 3 potrebbero avere lo stesso


timestamp (del ciclo corrente o di quello precedente); in questo modo non viene
effettuata nessuna sovrascrittura. (Se qualche In.ar avesse scritto sul busA prima
del Synth 2, potrebbe aver azzerato il bus prima che il Synth 3 ottenga i dati dal
Synth 2. Questo è vero anche se non ci fosse stato nessuno a scrivere sul busA prima
del Synth 2).
Da tutto ciò si capisce come può essere utile allocare un bus separato per il feedback.
Con questo espediente, il Synth 2 riceverà dati dal Synth 3 indifferentemente dalla
posizione del Synth 1 nell’ordinamento dei nodi.

1 Synth 1 scrive sul busA


2 Synth 2 (with InFeedback) legge dal busB
3 Synth 3 scrive su busB + busA

L’esempio seguente dimostra quanto detto con In.kr:

81
Ordine di esecuzione

1 (
2 SynthDef("help-Infreq", { arg bus;
3 Out.ar(0, FSinOsc.ar(In.kr(bus), 0, 0.5));
4 }).send(s);

6 SynthDef("help-Outfreq", { arg freq = 400, bus;


7 Out.kr(bus, SinOsc.kr(1, 0, freq/40, freq));
8 }).send(s);

10 b = Bus.control(s,1);
11 )
12 // aggiunge il primo Synth di controllo in coda al default server; no
audio
13 yetx = Synth.tail(s, "help-Outfreq", [\bus, b.index]);

15 // aggiunge il Synth che produce suono PRIMA;


16 // Questo riceve i dati di x dal ciclo precedente
17 y = Synth.before(x, "help-Infreq", [\bus, b.index]);

19 // aggiunge un altro Synth di controllo prima di y, in testa al server


20 // Questo ora sovrascrive i dati di x del ciclo precedente prima che
21 //il synth y li riceva
22 z = Synth.head(s, "help-Outfreq", [\bus, b.index, \freq, 800]);

24 // creiamo un altro bus


25 c = Bus.control(s, 1);

27 // ora y riceve i dati di x


28 y.set(\bus, c.index); x.set(\bus, c.index);
29 x.free; y.free; z.free;

15.6 Espansione multicanale


Si è accennato all’espansione multicanale nel capitolo Per espansione multicanale si
intente l’inserimento di un Array in uno degli argomenti della UGen al posto di un
singolo valore; canali audio multipli vengono rappresentati come Array.

1 s.boot;
2 // un canale
3 { Blip.ar(800,4,0.1) }.play;

5 // due canali
6 { [ Blip.ar(800,4,0.1), WhiteNoise.ar(0.1) ] }.play;

82
Ordine di esecuzione

Ogni canale di output viene dirottato verso uno speaker differente; limitiamo qui il
discorso a due canali, per un’output stereo. Se si dispone di un’interfaccia audio mul-
ticanale, allora è possibile gestire tanti output quanti sono supportati dall’interfaccia.
Tutte le UGens hanno un output singolo. Questa uniformità facilita l’uso di gruppi
di operazioni per manipolare strutture multicanale.
Per implementare output multicanali, le UGen creano una UGen separata conosciu-
ta come OutputProxy per ogni output. Un OuputProxy è solo un marcatore per
la UGen di output multicanale. Queste OutputProxy sono create internamente, non
è il caso pertanto di preoccuparsi di crearli, ma è buono avere la consapevolezza che
esistono così da sapere cosa sono quando ci si imbatte nella documentazione di SC.

1 // si osservino gli output di Pan2:


2 Pan2.ar(PinkNoise.ar(0.1), FSinOsc.kr(3)).dump;
3 play({ Pan2.ar(PinkNoise.ar(0.1), FSinOsc.kr(1)); });

Quando viene passato un Array come input a una unit generator, esso causerà una
serie di copie multiple della unit generator, ognuna con un valore differente dell’array
passato in input. Tutto ciò provoca l’espansione multicanale.
Importante: solo gli Array sono espansi, non altri tipi di Collection e non sottoclassi
di Array.
Vediamo un esempio:

1 { Blip.ar(500,8,0.1) }.play // un canale

3 // l’array nell’input delle frequenze causa un Array di 2 Blips :


4 {
5 Blip.ar([499,600],8,0.1) }.play // due canali
6 Blip.ar(500,8,0.1).postln // crea UNA UGen
7 Blip.ar([500,601],8,0.1).postln // crea DUE UGen
8 }

L’espansione multicanale si propagherà attraverso il grafo di espressioni. Quando


viene chiamato un costruttore di una UGen con un array di input, ritornerà un
array di istanze. Se questo array è l’input di un altro costruttore, allora viene creato
un altro array e così via. Vediamo un esempio:

1 {
2 RLPF.ar(Saw.ar([100,250],0.05), XLine.kr(8000,400,5), 0.05) }.play;

4 // L’array [100,250] di frequenze, input a Saw.ar, crea un array di 2


Saw;
5 // tale array in input a RLPF.ar crea due RLPF.
6 // Entrambi i RLPF condividono una singola instanza di XLine.

83
Ordine di esecuzione

Quando un costruttore è parametrizzato da due o più array, allora il numero di


canali creati è uguale alla dimensione dell’array più lungo, con i parametri inseriti
da ogni array in parallelo. L’array più corto sarà wrappato.

Per esempio, il seguente:

1 Pulse.ar([400, 500, 600],[0.5, 0.1], 0.2)


2 // \‘e equivalente a:
3 [ Pulse.ar(400,0.5,0.2), Pulse.ar(500,0.1,0.2), Pulse.ar(600,0.5,0.2) ]

Un esempio più complesso basato sulla Saw viene riportato di seguito; la XLine è
espansa da due istanze, una che va da 8000 Hz a 400 Hz e l’altra che va in direzione
opposta da 500 Hz a 700 Hz. Queste due XLine sono ’accoppiatè ai due oscillatori
Saw e usate per parametrizzare due copie di RLPF.
Così, sul canale sinistro si avrà una Saw a 100 Hz filtrata da 8000 Hz a 400 Hz e sul
canale sinistro una Saw a 250 Hz filtrata da 500 Hz a 7000 Hz.

1 { RLPF.ar(
2 Saw.ar(
3 [100,250],0.05), XLine.kr([8000,500],[400,7000],5)
4 , 0.05)
5 }.play;

15.7 Protezione di array dall’espansione


Alcune UGen come Klank richiedono in input alcuni array di valori. Dal momento
che tutti gli array sono espansi, è necessario proteggerne alcuni tramite un oggetto
Ref. Un istanza Ref è un oggetto con un singolo slot detto value che serve come
contenitore di un oggetto. Un modo per creare un’istanza di Ref è Ref.new(object),
ma esiste una scorciatoia sintattica: l’apostrofo ’è un operatore unario che è equi-
valente a chiamare Ref.new(...). Così per proteggere gli array che sono input in un
Klank o ad altre UGens simili, si può scrivere:

1 Klank.ar(’[[400,500,600],[1,2,1]], z)

È possibile inoltre creare Klanks multipli dando un array di array di Ref.

84
Ordine di esecuzione

1 Klank.ar([ ’[[400,500,600],[1,2,1]], ’[[700,800,900],[1,2,1]] ], z)

3 //\‘e equivalente a:

5 [
6 Klank.ar(’[[400,500,600],[1,2,1]], z),
7 Klank.ar(’[[700,800,900],[1,2,1]], z)
8 ]

15.8 Ridurre l’espansione multicanale con Mix


L’oggetto Mix ha lo scopo di ridurre array multicanali in un singolo canale.

1 Mix.new([a, b, c]) // array of channels

3 //\‘e equivalente a:

5 a + b + c // mixati in uno

L’utilizzo di Mix è più efficiente rispetto all’utilizzo del solo + perchè permette così
di effetturare addizioni multiple contemporaneamente. Ma il principale vantaggio è
che può lavorare in situazioni in cui il numero di canali è arbitrario o determinato
solo a tempo di esecuzione.

1 // 3 canali di Pulse sono mixati in un canale


2 { Mix.new( Pulse.ar([400, 501, 600], [0.5, 0.1], 0.1) ) }.play

L’espansione multicanale lavora differentemente per Mix. Mix prevede come input
un array (non protetto da un oggetto Ref). Tale array non causerà copie del Mix
che sarà effettuato. Tutti gli elementi dell’array saranno invece mixati insieme in
un oggetto singolo Mix. Se l’array contiene uno o più array, allora l’espansione
multicanale avverrà un livello sotto. Questo permette di mixare un array di array
stereo (due elementi) in un array a 2 canali. Per esempio:

1 // array di coppie di stereo input


2 Mix.new( [ [a, b], [c, d], [e, f] ] )

4 //\‘e equivalente a:
5 // mixare una singola coppia stereo
6 [ Mix.new( [a, c, e] ), Mix.new( [b, d, f] ) ]

85
Ordine di esecuzione

Importante: non è ricorsivo! Non si può usare Mix su array di array di array. Vediamo
un esempio finale che illustra un’espansione multicanale e Mix. Variando il valore
della variabile ”n”, è possibile variare il numero di voci nella patch.

1 (
2 {
3 var n;
4 n = 8; // numero di voci
5 // mix down di tutte le coppie stereo
6 Mix.new(
7 // spazializza la voce in una posizione stereo
8 Pan2.ar(
9 // un comb filter usato come uno string resonator
10 CombL.ar(
11 // impulsi random come una funzione eccitatrice
12 Dust.ar(
13 // un array che causa l’espansione di
14 // Dust in n canali;
15 // 1 : impulso per secondo
16 Array.fill(n, 1),
17 0.3 // ampiezza
18 ),
19 0.01, // delay max in secondi
20 // array di lunghezze random diverse per ogni ’string’
21 Array.fill(n, {0.004.rand+0.0003}),
22 4 // tempo di decay in secondi
23 ),
24 // ad ogni ’voce’una differente spazializzazione
25 Array.fill(n,{1.0.rand2})
26 )
27 )
28 }.play;
29 )

15.9 Uso di flop per l’espansione multicanale


Il metodo flop inverte colonne e righe, permettendo di derivare serie di insiemi di
argomenti:

86
Ordine di esecuzione

1 (
2 SynthDef("help_multichannel", { |out=0, freq=440, mod=0.1, modrange=20|
3 Out.ar(out,
4 SinOsc.ar(
5 LFPar.kr(mod, 0, modrange) + freq
6 ) * EnvGate(0.1)
7 )
8 }).send(s);
9 )

11 (
12 var freq, mod, modrange;
13 freq = Array.exprand(8, 400, 5000);
14 mod = Array.exprand(8, 0.1, 2);
15 modrange = Array.rand(8, 0.1, 40);

17 fork {
18 [\freq, freq, \mod, mod, \modrange, modrange].flop.do { |args|
19 args.postln;
20 Synth("help_multichannel", args);
21 0.3.wait;
22 }
23 };
24 )

In maniera analoga, una Function-flop restituisce una funzione non valutata che
espanderà i suoi argomenti quando verrà valutata:

1 (
2 SynthDef("blip", { |freq| Out.ar(0, Line.ar(0.1, 0, 0.05, 1, 0, 2)
3 Pulse.ar(freq * [1, 1.02])) }).send(s);

5 a = { |dur=1, x=1, n=10, freq=400|


6 fork { n.do {
7 if(x.coin) { Synth("blip", [\freq, freq]) };
8 (dur / n).wait;
9 } }
10 }.flop;
11 )

13 a.value(5, [0.3, 0.3, 0.2], [12, 32, 64], [1000, 710, 700]);

87
Just In Time Programming

Capitolo 16 Just In Time Programming


Just in time programming (or: live coding, on-the fly-programming, interactive pro-
gramming ) è un paradigma che include l’attività di programmazione stessa nelle
operazioni del programma. Questo significa un programma che non è considerabile
come un tool preconfezionato, ma un processo di costruzione dinamico di descri-
zione e conversazione - scrivere codice diventa una parte integrante della pratica
musicale. SC, essendo un linguaggio di programmazione dinamico, offre diverse pos-
sibilità di modificare un programma in stato di esecuzione; la libreria JITLib tenta
di estendere, sviluppare e semplificare questo processo principalmente offrendo dei
placeholders (proxies) astratti che possono essere modificati e usati nel calcolo
mentre stanno suonando. Ci sono alcune classi di rete specifiche, atte a semplificare
la distribuzione o l’attività di live coding.

Jitlib consiste di un numero di placeholders (sia server side che client side) e uno
schema di accesso. Questi due aspetti dello spazio corrispondo a inclusioni e riferi-
menti, a seconda del contesto - in quest’ottica i placeholders sono simili a regole che
hanno un certo comportamento e possono essere soddisfatte da certi oggeti.

È utile essere consapevole di tre aspetti riguardanti un placeholder:

− la loro sorgente può essere un certo insieme di elementi.

− possono essere usati in un certo insieme di contesti.

− hanno una certa sorgente di default, se nessuna è assegnata.

Questo capitolo si focalizza su alcuni concetti base usati in JITLib. Sono contemplate
svariate possibilità, come la messaggistica con il server o come i pattern proxies che
però, qui, non verranno considerati in modo specifico

16.1 Overview delle differenti classi e tecniche


Esistono alcuni modalità di accesso:

1. Uno stile di accesso è la classe def (Pdef, Ndef etc.) che collega un symbol a un
oggetto in un modo specifico:

− Pdef (name) restituisce il proxy

− Pdef(name, object) setta la sorgente e restituisce il proxy. Il resto del com-


portamento dipende dal suo uso.

88
Just In Time Programming

In generale consideriamo:

− client side: Pdef Pdefn, Tdef, Pbindef

− server side: Ndef

2. Un altro modo, per i NodeProxy lato server, è un ambiente che restituisce pla-
ceholders su richiesta:

1 ProxySpace.push
2 ~out = { ...}

(si veda l’helpfile di ProxySpace)

3. Esiste infine anche un accesso diretto senza usare gli schemi di accesso offerto da
NodeProxy, TaskProxy etc. Internamente si usano le seguenti classi:

• client side: PatternProxy, EventPatternProxy, TaskProxy, PbindProxy, Pdict

• server side: NodeProxy, RecNodeProxy


In reti, locali e remote, grazie all’architettura di SC i nodeproxy possono essere
usati da qualunque server, purchè venga notificato al client e si ha un nodo di
default correttamente inizializzato.Da notare che il client deve essere settato.
Usando le classi di rete, gruppi di partecipanti possono interferire in ogni altra
composizione condividendo un server comune, usando un SharedNodeProxy e
scambiando codici e commenti. Per il networking si usano le seguenti classi:
BroadcastServer e OSCBundle

16.2 Placeholder in supercollider


In questa sezione vengono descritti alcuni concetti base sulla programmazione inte-
rattiva con SC e il proxy space.

a. Cos’è un proxy?

Un proxy è un placeholder che viene spesso usato per operare su qualcosa che
non esiste ancora. Per esempio, un OutputProxy viene utilizzato per rappresen-
tare outputs multipli di una UGen, anche se viene creata soltato una UGen.

Ogni oggetto può avere un comportamento da proxy (per esempio potrebbe de-
legare chiamate di funzioni a oggetti differenti), in special modo funzioni e riferi-
menti possono essere impiegati come operandi mentre mantengono la loro qualità

89
Just In Time Programming

referenziale. (si veda anche: [OutputProxy] [Function] [Ref] )


È possibile effettuare riferimenti utilizzando diversi costrutti:

• Un Ref come proxy

Un’istanza dell’oggetto Ref è un oggetto con un singolo slot, value, che serve
come contenitore di un oggetto. Un modo per istanziare questo oggetto è
Ref.new(object), ma esiste uno shortcut sintaattico, l’apostrofo ’infatti è
un operatore unario equivalente al costruttore precedentemente illustrato.
Vediamo un esempio:

1 x = Ref.new(nil);
2 z = obj.method(x); // method mette qualcosa in riferimento
3 x.value.doSomething; // recupera il valore e fa qualcosa

Ref viene anche impiegato come un dispositivo di citazione per limitare


l’espansione multicanale in certe UGen che richiedono Array come input.

A questo punto vediamo un esempio di utilizzo di Ref come proxy:

90
Just In Time Programming

1 // creiamo un oggetto Ref


2 y = ‘(nil);
3 // si pu\‘o cominciare a calcolare con y, anche se il suo valore non
\‘e stato dato:
4 z = y + 10; // restituisce una funzione
5 // settiamo ora la sorgente:
6 y.value = 34;
7 // la funzione z viene valutata, A QUESTO PUNTO.
8 z.value

10 // la stessa cosa senza riferimento non funziona:


11 y = nil;
12 z = y + 10; // questo fallisce.
13 // anche un array non offre questa referenziazione:
14 y = [nil];
15 z = y + 10; // questo fallisce.

17 // un ambiente senza default sufficienti ha lo stesso problema:


18 currentEnvironment.postln; // anEnvironment
19 ~x; // accedo all’ambiente; per ora = nil
20 ~x = 9; // salvo qualcosa
21 ~x; // ora contiene 9
22 ~x + 100; // posso calcolare
23 currentEnvironment.postln; // the value is stored in the environment
24 ~y + ~x; // causa un errore: ~y = nil.
25 ~y = -90; // settiamo ~y
26 ~y + ~x; // adesso funziona.

• Una Function come proxy:

1 // la stesso esempio, ma con una funzione


2 y = nil; // y empty
3 z = { y } + 10;
4 // questo non fallisce, crea una nuova funzione, che
5 // non fallisce quando viene valutata dopo che y
6 //viene settato a 34
7 y = 34;
8 z.value;

Si vedano, per completezza, altri proxy lato client come [Tdef] [Pdefn] [Pdef]

b. NodeProxy

Per la programmazione interattiva potrebbe essere utile essere in grado di usa-


re qualcosa che non si ha ancora e crea un ordine di valutazione più flessibile
per permette di posporre decisioni in momenti successivi. Di solito vengono fatti

91
Just In Time Programming

anticipatamente alcuni passi: negli esempi precedenti, è stato creato un riferi-


mento come prima cosa. In altre situazioni, questo ordine di preparazione non è
abbastanza, per esempio se si vuole fare operazioni matematiche in processi in
running sul server.

L’output audio sul server ha principalmente due proprietà:

1. un calculation rate (audio o control)

2. un certo numero di canali.


Queste sono le proprietà statiche principali di un node proxy, che non possono
essere modificate mentre è in uso. Vediamo un esempio.

1 // boot del server


2 s.boot;

4 // due proxies su un server. Il rate di calcolo \‘e audio rate,


5 // il numero di canali = 2
6 y = NodeProxy.audio(s, 2);
7 z = NodeProxy.audio(s, 2);

9 // usiamoli nel calcolo


10 z.play;
11 z.source = y.sin * 0.2;

13 // settiamo la sua sorgente


14 y.source = { Saw.ar([300, 301], 4*pi) };
15 // la sorgente pu\‘o essere di vario tipo, per esempio un numero:
16 y.source = 0.0;

18 // post della sorgente


19 y.source;

21 // fine: vengono liberati i bus


22 y.clear;
23 z.clear;

Un semplice modo per creare node proxy implica che deve essere utilizzato un
proxy space, come nell’esempio seguente.

92
Just In Time Programming

1 p = ProxySpace.push(s.boot); // store proxy space in p so it can be


accessed easily.
2 ~z.play;
3 ~z = ~y.sin * 0.2;
4 ~y = { Saw.ar([300, 301], 4*pi) };

6 // ripulisce lo spazio (tutti i proxies)


7 p.clear;

9 // esce dal proxyspace


10 p.pop;

further readings: NodeProxy ProxySpace Ndef

16.3 Proxy Space: concetti base


Occorre precisare a questo punto alcuni concetti, dando le definizioni di ProxySpace
e Enviroment. Un ProxySpace è un ambiente di referenze sul server. Generalmente
un proxy è un placeholder per qualcosa, che in questo caso è qualcosa che suona
sul server e che scrive su un numero limitato di bus, quindi, per capirci, potrebbbe
essere l’esempio di un synth.
Quando si accede a un ProxySpace, si ottiene un NodeProxy determinato dal primo
oggetto inserito nell’ambiente. Una volta creato, questo può solo essere settato ad
una funzione che restituisce lo stesso oggetto e un numero di canali uguale a quello
iniziale o al più piccolo.

1 // si noti che le due espressioni sono equivalenti:


2 ~out = something;
3 currentEnvironment.put(\out, something);

È possibile creare un proxyspace quando il server non è in stato running e viene


avviato dopo.

Un Environment, ambiente in SC, è un IdentityDictionary che mappa simboli


a valori. Esiste sempre un ambiente corrente, che è salvato nella variabile di classe
currentEnvironment della classe Object.

In questa parte ci occupiamo di descrivere la struttura esterna di un node proxy,


referenziata in un proxyspace e/o in ambienti. Esistono diverse configurazioni pos-
sibili:

93
Just In Time Programming

a. Ambiente di default

b. Proxyspace come ambiente

c. Usando il proxyspace per cambiare processi al volo

d. Usando ProxySpace insieme ad altri ambienti

Vediamone ora uno per volta, dando le precisazioni del caso.

a. Ambiente di default Siamo nel caso in cui abbiamo a che fare con il classico
ambiente di default di SuperCollider. Si ricorda che le variabili d’ambiente sono
precedute dal simbolo~.

1 currentEnvironment.postln; // anEnvironment

3 // accediamo all’ambiente, per ora = nil


4 ~a;
5 ~a = 9; // salviamo qualcosa
6 ~a; // ora 9 viene salvato
7 ~a + 100; // = 109

9 currentEnvironment.postln; // il valore viene salvato nell’ambiente

11 ~b + ~a; // causa un error: ~b is nil.


12 ~b = -90; // setta ~b
13 ~b + ~a; // adesso funziona.

Da notare che l’accesso ad ambienti ( o ProxySpace) è sempre possibile dall’esterno


nel seguente modo:

1 x = currentEnvironment;
2 x[\a] + x[\b] // = ~b + ~a

oppure, se know è settata a true, in ques’altro modo

1 x.know = true;
2 x.a + x.b;

b. Proxyspace come ambiente


È possibile rimpiazzare l’ambiente corrente con un tipo speciale di ambiente, un
proxy space.

94
Just In Time Programming

1 p = ProxySpace.new(s); // crea un nuovo ambiente e lo salva in p


2 p.push; // push = diventa l’ambiente corrente
3 currentEnvironment.postln;
4 currentEnvironment === p; // sono identici

6 ~x;
7 // accedendo si crea automaticamente un NodeProxy (non inizializzato)
8 ~x + ~y;
9 // questo funziona immediatamente, il lookup non ritorna nil,
10 // ma un placeholder (proxy)

12 p.postln; // ci sono due placeholder nello spazio.

c. Usando il proxyspace per cambiare processi al volo

1 s.boot; // boot del server

3 // funzione assegnata ad un proxy


4 // viene fatto il play sul proprio bus privato (anche se non ancora
udibile)
5 (
6 ~x = {
7 RLPF.ar(Impulse.ar(4) * 20, [850, 950], 0.2)
8 }
9 )

11 // il proxy viene inizializzato dal suo primo assegnamento


12 // suona ad audio rate avendolo inizializzato con una UGen audio rate
13 // e ha due canali (stereo output)

15 ~x.index;
16 // viene visualizzato l’indice del bus, prima era .ir(nil), ora viene
inizializzato .ar(2)
17 ~x.bus

19 ~x.play;
20 // ora diventa udibile. Viene creato un monitor (Monitor) che suona
21 // il segnale su un bus pubblico, indipendentemente dal proxy stesso

La funzione può essere cambiata in ogni momento

95
Just In Time Programming

1 (
2 ~x = {
3 RLPF.ar(Impulse.ar([5, 7]) * 5, [1450, 1234], 0.2)
4 }
5 )

7 // filtra le alte frequenze:


8 ~x = { RLPF.ar(Impulse.ar([5, 7]) * 5, [1800, 2000], 0.2) }

10 // same pulse ratio (5/8), different pulse tempo:


11 ~x = { RLPF.ar(Impulse.ar([5, 8] * 3.2) * 5, [1800, 2000], 0.2) }

13 // different filter:
14 ~x = { Ringz.ar(Impulse.ar([5, 8] * 3.2), [1800, 2000], 0.05) }

16 // and if you set the proxy’s fadeTime, you can create little
17 // textures by hand:

19 ~x.fadeTime = 3;
20 // different filter freqs every time:
21 ~x = { Ringz.ar(Impulse.ar([5, 8] * rrand(0.5, 1.5)) * 0.5, ({
exprand(200, 4000) } ! 2), 0.05) }

23 // un altro proxy:
24 ~y = { Pan2.ar(Dust.ar(20), 0) };

26 ~y.bus;
27 // ha due canali, come ~x,ma suona su un altro bus privato.
28 // da notare che ~y non \‘e udibile direttamente
29 //ma pu\‘o venire usato in un altro proxy:
30 (
31 ~x = {
32 RLPF.ar(~y.ar * 8, [1450, 1234], 0.2)
33 }
34 )

36 // Modificando il proxy, il risultato cambia dinamicamente:

38 ~y = { Impulse.ar(MouseX.kr(2, 18, 1)) * [1, 1] };


39 ~y = { PinkNoise.ar(MouseX.kr(0, 0.2) * [1, 1]) };
40 ~y = { Impulse.ar([MouseX.kr(2, 18, 1), MouseY.kr(2, 18, 1)]) };

42 // fine dell’ascolto. i proxies girano in background.


43 ~x.stop;

45 // l’ascolto di ~y diretto
46 ~y.play;

48 // per rimuovere un input, si usa nil


49 ~y = nil;
50 // fine dell’ascolto
51 ~y.stop;
96
Just In Time Programming

Quando vengono inizializzati i node proxy?


L’inizializzazione dei bus di un node proxy avviene non appena viene impiegato
la prima volta; gli input successivi verranno ”adeguati”ai bus disponibili.

1 //un proxy control rate a 4 canali


2 ~z2 = { LFNoise0.kr([1, 2, 3, 4]) };
3 ~z2.bus.postln;

5 ~z100 = 0.5; // un valore costante equivale ad un proxy


6 //control rate di un singolo canale
7 ~z100.bus.postln;

9 ~z34.ar(3) //il primo accesso alloca il bus


10 ~z34.bus.postln; //un proxy audio a 3 canali

12 // queste inizializzazioni possono essere rimosse usando clear:


13 ~z34.clear;
14 ~z34.bus.postln;

Questa inizializzazione avviene appena il proxy viene utilizzato per la prima vol-
ta. In seguito, si può accedere al proxy con un’altra combinazione rate/numChannels
in base alle necessità (rates sono convertiti, numChannels sono estesi tramite il
wrapping).
Da notare che questo potrebbe causare inizializzazioni ambigue, nel qual caso il
proxy dovrebbe sempre essere inzializzato prima. Un problema tipico che si può
riscontrare è riportato nell’esempio seguente:

97
Just In Time Programming

1 ~u.play(0, 2); // inizilizzazione di 2 canali audio (default).


2 //0 = numero del bus di output
3 // se il proxy non \‘e inizializzato
4 //, suona di default su 2 canali.
5 // abbiamo esplicitato solo per chiarezza.
6 ~u = { PinkNoise.ar(0.2) }; // ne usa solo uno
7 ~u.numChannels; // dei 2 canali
8 ~u.clear;

10 se valutato, sono un canale \‘e utilizzato:

12 ~u = { PinkNoise.ar(0.2) }; // inizializza 1 canale audio


13 ~u.play(0, 2); // suona 2 canali: il canale 1 viene espanso in 2.
14 // numChannels di .play sono legati ai numChannels del proxy
15 // Qui viene esplicitato, cos\‘i espande i canali
16 ~u.numChannels; // 1 canale
17 ~u.clear;

19 //Quindi potrebbe essere utile inizializzare in modo esplicito


20 // dei proxy che usano input variabili:

22 ~b.kr(8);
23 ~c.ar; // inizializzazione esplicita
24 p.postln; // visualizza l’intero spazio proxy

e) uscire dal proxy space:

98
Just In Time Programming

1 ~x.play; //suona
2 ~x = { PinkNoise.ar(0.5) };
3 p.postln; //p = proxy space

5 // per terminare tutti i processi in p, si usa end:


6 p.end(2) // con 2 secondi di fade out.

8 // per rimuovere tutti gli oggetti busve liberarli dal bus allocator,
si usa clear:
9 p.clear;
10 currentEnvironment.postln;

12 // ripristinare l’ambiente originale


13 p.pop;
14 currentEnvironment.postln;

16 ~a + ~b; // i valora vecchi ci sono ancora.

18 p === currentEnvironment; // non sono uguali

20 // rimuoviamo il contenuto, per far rilasciare la memoria dal garbage


collector
21 p.clear;

23 // da notare che se si usa questo schema di accesso, gli oggetti non


sono
24 //considerati dal garbage collectore fino a che le loro chiavi non sono
poste a nil
25 // Questo appare un errore comune quando si usano ambienti normali

27 // ripulire completamente l’ambiente normale:


28 currentEnvironment.clear;

d. Usando ProxySpace insieme ad altri ambienti


Usando un proxy space come uno schema di accesso per i node proxy, potrebbe
creare confusione con l’uso consueto di ambienti come pseudo-variabili.Vediamo
alcuni modi per accoppiarli.

99
Just In Time Programming

1 // per restringere lo scope del proxyspace a un documento (solo mac)

3 EnvirDocument(p, "proxyspace");
4 // per testarlo,si verifica currentEnvironment
5 //e dentro il documento envir.

7 // per accedere indirettamente al proxyspacce:


8 p[\x].play;
9 p[\x] = { SinOsc.ar(450, 0, 0.1) };

11 // oppure, dopo una push, si usa un ambiente normale indirettamente:


12 p.push;
13 d = ();
14 d[\buffer1] = Buffer.alloc(s, 1024);
15 d.use { ~buffer1.postln; ~zz = 81; };

17 // senza creare un nuovo proxyspace,si usa .envir:


18 p.envir.postln;
19 p.envir[\x].postln;

16.4 Struttura interna di un node Proxy


Vedremo la struttura interna del node proxy, l’ordine dei nodi e il contesto dei
parametri.
Un NodeProxy ha due contesti interni in cui gli oggetti vengono inseriti: Il grup-
po, che è sul server, e il nodeMap che è un contesto di parametri lato client;
quindi, come il gruppo sul server può contenere un insieme di synth in un certo
ordine, esiste una rappresentazione lato client in cui gli oggetti sorgente vengono
salvati.

1 //creo un nuovo spazio


2 = ProxySpace.push(s.boot);
3 ~z.play; ~y.ar; // inizializzazione esplicita dei nodeproxy

16.4.1 NodeProxy slots


Un node proxy può contenere diversi oggetti in un ordine di esecuzione preciso.
L’indice può essere qualunque intero positivo. Lo slot iniziale ha indice 0 e viene
utilizzato con l’assegnazione diretta. Vediamo un esempio:

100
Just In Time Programming

1 // ~y non ancora utilizzato.


2 ~z = (~y * pi).sin * 0.1 * { LFSaw.kr(LFNoise1.kr(0.1 ! 3).sum * -
18).max(0.2) };

4 // altri numeri di slot sono accessibili tramite interi positivi:

6 ~y[1] = { Saw.ar([400, 401.3], 0.4) };


7 ~y[0] = { Saw.ar([300, 301], 0.4) };

9 // per rimuoverne uno:


10 ~y[0] = nil;

12 // cosa troviamo all’indice 1?


13 ~y[1] // un interfaccia
14 ~y[1].source.postcs // la funzione
15 ~y.source; // gli oggetti nello slot

È possibile effettuare assegnamenti multipli:

1 // viene assegnata la faunzione agli slot da 1 ta 4


2 ~z[1..4] = { SinOsc.ar(
3 exprand(300, 600), 0,
4 LFTri.kr({exprand(1, 3)} ! 3)
5 .sum.max(0)) * 0.1 };

7 // la funzione viene assegnata agli slot 1, 2 e 3


8 ~z[1..] = [ {SinOsc.ar(440) * 0.1 },
9 { SinOsc.ar(870) * 0.08 },
10 { SinOsc.ar(770) * 0.04 }];

12 // se non vengono esplicitati slot, tutti gli slot sono inzializzati


13 ~z = { OnePole.ar(Saw.ar([400, 401.3], 0.3), 0.95) };

15 ~z.end;
16 ~y.end;

16.4.2 fade time


Settare questo valore, fadeTime, permetterà di creare dei crossfade. Nel caso
di un proxy audio rate,il fade è pseudo- guassiano; nel caso di un proxy control
rate, il fade è lineare. Questo parametro può essere settato o modificato in ogni
momento. Vediamo e proviamo il seguente codice:

101
Just In Time Programming

1 ~z.play;

3 ~z.fadeTime = 5.0; // 5 secondi


4 ~z = { max(SinOsc.ar([300, 301]), Saw.ar([304, 304.3])) * 0.1 };
5 ~z = { max(SinOsc.ar(ExpRand(300, 600)), Saw.ar([304, 304.3])) * 0.1
};

7 ~z.fadeTime = 0.2;
8 ~z = { max(SinOsc.ar(ExpRand(3, 160)), Saw.ar([304, 304.3])) * 0.1 };

Da notare infine che il fadeTime è anche utilizzato per le operazioni xset and
xmap.

16.4.3 play/stop, send/free, pause/resume


Ci sono una serie di coppie di messaggi che un NodeProxy conosce e che sono
relative al play, stop, pausa, ecc.

a. play/stop

Questa coppia di messaggi è legata alla funzione di monitorizzazione del pro-


xy. play avvia il monitoring, stop temina il monitoring. Se il proxy group è
in stato di play (e questo può essere testato con il metodo .isPlaying), play
non influenzerà il comportament interno del proxy in alcun modo. Solo se
non è in stato play (perchè per esempio si ha liberato il gruppo attraverso il
messaggio cmd-period) esso avvia synth e oggetti nel proxy.Stop invece non
influisce mai sullo stato interno del proxy.

1 // si prema prima cmd-period.


2 ~z = { max(SinOsc.ar(ExpRand(3, 160)), Saw.ar([304, 304.3])) * 0.1 };
3 ~z.play; // monitorizza il proxy
4 ~z.stop; // da notare che ora il proxy sta ancora suonando, ma
solo in privato.
5 ~z.isPlaying; // Il gruppo suona? si.
6 ~z.monitor.isPlaying; // il monitor suona? no.

È possibile passare un argomento di volume al play per modificare il volme


del monitor senza influenzare il volume del bus interno. Questa operazione è
sempre possibile e può essere effettuata direttamente tramite il metodo .vol

102
Just In Time Programming

1 ~z.play(vol:0.3);
2 ~z.vol = 0.8;

b. send / release

Questa coppia di messaggi controlla i synth dentro il proxy. Non influisce


sul monitoring. Send avvia un nuovo synth, release rilascia il synth. Send di
default rilascia l’ultimo synth.

1 // si prema prima cmd-period.


2 ~z.play; // monitor. questo avvia anche il synth
3 //se il gruppo non era in play
4 ~z = { SinOsc.ar(ExpRand(20, 660) ! 2)
5 * Saw.ar(ExpRand(200, 960) ! 2) * 0.1 };

7 ~z.release; // rilascia il synth.


8 //Il fadeTime corrente viene usato per il fade
out
9 ~z.send; // invia un nuovo synth.
10 // Il fadeTime corrente viene usato per il fade
in
11 ~z.send; // invia un’altro synth, rilasciando quello
vecchio
12 ~z.release;
13 ~z.stop;
14 ~z.play; // monitor. se il gruppo stava ancora suonando,
15 //questo non avvia il proxy

Al fine di liberare i synth e il gruppo insieme, si può utilizzare il metodo free:

1 ~z.free; // questo non influenza il monitoring.


2 ~z.play; // monitor. se il monitor non era in play, avvia il
gruppo

Se oltre a liberare i synth e il gruppo, si vuole fermare la riproduzione, si può


utilizzare il metodo end:

1 ~z.end(3); // end in 3 sec

Per ricostruire la synthdef sul server, si può riutilizzare rebuild. Questo po-
trebbe aver senso quando la synthdef ha un’architettura statica, ma di sicuro
utilizzare questo metodo è meno efficiente che utilizzare il metodo send.

103
Just In Time Programming

1 (
2 ~z = {
3 sum(
4 SinOsc.ar(Rand(300,400) + ({exprand(1, 1.3)} ! rrand(1,
9)))
5 * LFCub.ar({exprand(30, 900)} ! rrand(1, 9))
6 * LFSaw.kr({exprand(1.0, 8.0)} ! rrand(1, 9)).max(0)
7 * 0.1
8 )
9 };
10 )

12 ~z.play;
13 ~z.rebuild;
14 ~z.send; // send crea un nuovo synth
15 ~z.rebuild; // ricostruisce la synthdef
16 ~z.end;

c. pause / resume
Quando viene messo in pausa, un node proxy rimane ancora attivo, ma tutti
i synth che erano stati avviati sono messi in pausa fino alla ricezione di un
messaggio di resume

1 ~z.play;
2 ~z.pause; // si mettono in pausa i synth
3 ~z = {
4 SinOsc.ar({ExpRand(300, 660)} ! 2) * 0.1 };
5 // si possono aggiungere nuove funzioni in situazioni di pausa
6 ~z.resume; // esce dallo stato di pausa

Da notare che pause/resume possono causare click atonali con proxy audio
rare, cosa che non succede quando si mettono in pausa proxy control rate.

d. clear
Il metodo clear rimuove tutti i synth, i gruppi, il monitor e rilascia i numero
di bus.

1 ~z.clear;
2 ~z.bus; // no bus
3 ~z.isNeutral; // non inizializzato.

Da notare che quando altri processi usano il nodeproxy, non sono notificati.
Quindi, il metodo clear deve essere fatto considerando questo inconveniente.

104
Just In Time Programming

16.4.4 Il parametro di contesto


Utilizzando i nodeproxy è possibile modificare argomenti di una funzione. Per
esempio:

1 ~y.play;
2 ~y = { arg freq=500; SinOsc.ar(freq * [1, 1.1]) * 0.1 };

Ora l’argomento freq è un controllo nel synth (proprio come in SynthDef) e


può essere modificato da un messaggio set. Diversamente dall’oggetto Synth,
questo valore viene conservato e applicato a tutti i nuovi synth che verranno
creati in seguito.

1 ~y.set(\freq, 440);
2 ~y = { arg freq=500; Formant.ar(50, freq * [1, 1.1], 70) * 0.1 };

Con xset, una variante di set, si può effettuare un crossfade durante il cambio
utilizzando il fadeTime corrente:

1 ~y.fadeTime = 3;
2 ~y.xset(\freq, 600);

4 // lo stesso contesto si applica a tutti gli slot.

6 ~y[2] = { arg freq=500; SinOsc.ar(freq * [1, 1.1]) * LFPulse.kr(Rand(1,


3)) * 0.1 };
7 ~y.xset(\freq, 300);

Il parametro di contesto può anche contenere il mapping del bus. Può inoltre
essere mappato un controllo ad ogni proxy di controllo.

1 ~c = { MouseX.kr(300, 800, 1) };
2 ~y.map(\freq, ~c);

4 // anche qui il contesto viene conservato

6 ~y = { arg freq=500; Formant.ar(4, freq * [1, 1.1], 70) * 0.1 };

xmap è una variante di map per effettuare il crossfade utilizzando il fadeTime


corrente:

105
Just In Time Programming

1 ~y.set(\freq, 440);
2 ~y.xmap(\freq, ~c);

Infine, per rimuovere un settaggio o un mapping, si utilizzano i metodi un-


map / unset.

1 ~y.unmap;

Per esempio un controllo multicanale potrebbe essere mappato ad un proxy


multicanale usando mapn:

1 ~c2 = { [MouseX.kr(300, 800, 1), MouseY.kr(300, 800, 1)] };


2 ~y = { arg freq=#[440, 550]; SinOsc.ar(freq) * SinOsc.ar(freq + 3) *
0.05 };
3 ~y.mapn(\freq, ~c2);

Oltre ai parametri esplicitamente settati, il parametro di contesto contiene


l’indice del bus utilizzato e il parametro fadeTime. È possibile esaminare il
suo contenuto con il metodo nodeMape

1 ~y.nodeMap;
2 p.clear(8); // ripulisce l’intero proxy space in 8 sec.

16.5 Timing in NodeProxy


Le variazioni che avvengono in un NodeProxy, fra le quali la più importante
riguarda la sorgente, sono normalmente effettuate appena il metodo put viene
richiamato (o, in ProxySpace, dopo l’operazione di assegnamento = ). Alcune
volte è desiderabile poter temporizzare questi cambiamenti in base a un clock.
Esistono 4 possibili approcci per questa temporizzazione:

a. clock

b. quant and offset

c. client and server tempo

d. sample accurate output


Vediamone uno per uno, corredando di esempi ogni approccio.

106
Just In Time Programming

a. clock Generalmente, ogni node proxy può avere il proprio tempo base, di solito
un tempo di clock. Il clock è responsabile per la temporizzazione dell’inserimento
di nuove funzioni che avverrà di default al successivo beat del clock.

1 p = ProxySpace.push(s.boot);
2 ~x.play; ~y.play;

4 // questi due synths vengono avviati al momento


5 //in cui sono inseriti:
6 ~x = { Ringz.ar(Impulse.ar(1), 400, 0.05) };
7 ~y = { Ringz.ar(Impulse.ar(1), 600, 0.05) };

9 // aggiungiamo un clock:
10 ~x.clock = TempoClock.default;
11 ~y.clock = TempoClock.default;

13 // ora sono in sync


14 ~x = { Ringz.ar(Impulse.ar(1), 400, 0.05) };
15 ~y = { Ringz.ar(Impulse.ar(1), 600, 0.05) };

17 // per offrire un clock per l’intero proxy space:

19 p.clock = TempoClock.default;
20 y = { Ringz.ar(Impulse.ar(1), 800, 0.05) };

22 ~z.play;
23 ~z = { Ringz.ar(Impulse.ar(1), [500, 514], 0.8) };
24 ~z = { Ringz.ar(Impulse.ar(1), exprand(300, 400 ! 2), 0.8) };
25 ~z = { Ringz.ar(Impulse.ar(2), exprand(300, 3400 ! 2), 0.08) };
26 ~z.end;

Sequenze di eventi:
Quando si inserisce una nuova funzione dentro il proxy, viene costruita la
synthdef e inviata al server che risponde un messaggio al completamento
dell’operazione. Quindi, il proxy aspetta il successivo beat per avviare il synth.
Quando si usano i node proxy con i patterns, i patterns sono messi in play
usando il clock come scheduler.
b. quant and offset
Al fine di essere in grado di controllare il offset/quant di inserimento, può
essere usata la variabile di istanza quant, che può essere un numero o un
array della forma [quant, offset], proprio come in pattern.play(quant).

1 ~y.quant = [1, 0.3]; // offset of 0.3, quant of 1.0


2 ~y = { Ringz.ar(Impulse.ar(1), 600, 0.05) };

107
Just In Time Programming

c. connecting client and server tempo


Un ProxySpace ha il metodo makeTempoClock, che crea un’istanza di
TempoBusClock insieme con un node proxy (t̃empo) che tiene il sync.

1 // creiamo un nuovo tempoclock con 2 beats/sec


2 p.makeTempoClock(2.0);

4 ~y.quant = 1; // settiamo il quant a 1 e l’offset a 0

6 // l’impulso usa tempo


7 ~y = { Ringz.ar(Impulse.ar(~tempo.kr), 600, 0.05) };
8 // pattern usa tempoclock
9 ~x = Pbind(\instrument, \default, \freq, Pseq([300, 400], inf));

11 p.clock.tempo = 1.0; // setta il tempo a 1


12 p.clock.tempo = 2.2; // setta il tempo a 2.2

14 ~x.free;
15 ~y.free;

d. sample accurate output


Per un fattore di efficienza, NodeProxy usa una normale UGen Out per scri-
vere sul suo bus. Se è necessario un sample rate di riproduzione accurato
(OffsetOut), la variabile di classe sampleAccurate di ProxySynthDef può
essere settata a true. Vediamo un esempio per chiarire:

108
Just In Time Programming

1 ProxySynthDef.sampleAccurate = false;
2 ~x.play;

4 // il grain libera se stesso


5 ~x = { SinOsc.ar(800) * EnvGen.ar(Env.perc(0.001, 0.03, 0.4), doneAction:2)
};

7 // tono di jitter.
8 (
9 r = Routine {
10 loop {
11 200.do { arg i;
12 ~x.spawn;
13 (0.005).wait;
14 };
15 1.wait;
16 }
17 }.play;
18 )

20 ProxySynthDef.sampleAccurate = true;

22 // tono stabile: sample accurata.

24 ~x.rebuild;
25 r.stop;
26 p.clear; // rimuove il tutto

109
Esecuzione sincrona e asincrona

Capitolo 17 Esecuzione sincrona e asincrona


In un programma come SC che si occupa di real time, vengono introdotti un
certo numero di problemi sulla temporizzazione e l’ordine di esecuzione.
La sintesi audio in tempo reale richiede che i campioni siano calcolati e suona-
ti ad un certo tasso e in un certo sistema di schedule, per eliminare dropouts,
glitches, ecc.; questi task sono detti sincroni in quanto devono essere coor-
dinati, sincronizzati, fra loro.
Altri tasks, come il caricamento di un campione in memoria, potrebbe impie-
gare una quantità arbitraria di tempo, e non necessariamente limitato dentro
un timeframe. ; in questo caso si può parlare di task asincroni

Nella gestione dei due tipi di task, si possono presentare problemi quando task
sincroni sono dipendenti dal completamento di uno asincrono. Per testare ciò
si provi a riprodurre un campione che potrebbe o meno essere completamente
caricato.

In SC2 tutto ciò era relativamente semplice da gestire. Si schedulavano task


sincroni durante la sintesi, (cioè dentro lo scope di un Synth.play). I task
asincroni erano eseguiti in ordine, esternamente alla sintesi. Quindi si creava
prima il buffer, vi si caricavano i campioni e la sintesi poteva cominciare.
L’interprete assicurava che ogni passo veniva effettuato solo quando il passo
precedente era stato completato.

In SC3 la separazione del linguaggio e l’app server crea un problema: come


fa un lato a sapere che l’altro lato ha completato i task necessari, o in altre
parole, come fa la mano sinistra a sapere che la destra ha finito?!
La flessibilità guadagnata dalla nuova architettura introduce nuovi livelli di
complessità e uno sforzo maggiore dell’utente.

Un semplice modo per occuparsi di ciò è eseguire il codice in blocchi. Nel


codice seguente, per esempio, ogni blocco o linea di codice è dipendente dal
fatto che la precedente sia completata.

110
Esecuzione sincrona e asincrona

1 // Si esegua questo codice a blocchi

3 // Boot del Server locale


4 (
5 s = Server.local;
6 s.boot;
7 )

9 // Compiliamo una SynthDef e scriviamola su disco


10 (
11 SynthDef("Help-SynthDef",
12 { arg out=0;
13 Out.ar(out, PinkNoise.ar(0.1))
14 }).writeDefFile;
15 )

17 // carichiamo la Synthdef
18 s.loadSynthDef("Help-SynthDef");

20 // Creiamo un synth a partire dalla SynthDef


21 x = Synth.new("Help-SynthDef", s);

23 // Liberiamo il nodo sul server


24 x.free;

26 // Azzeriamo l’oggetto Synth lato client


27 x = nil;

Nell’esempio precedente, era necessario utlizzare le variabili di interprete per


potersi riferire agli oggetti creati in blocchi precedenti, in blocchi o linee di
codice successive. Se si dichiara una variabile dentro ad un blocco di codice
(i.e. var mySynth;) essa sarà persistente solo dentro lo scope dichiarato.

Questo stile di lavoro, eseguendo linee di codice o blocchi di codice alla volta,
può essere molto dinamico e flessibile, e può essere abbastanza utile in una
situazione di performance dal vivo, specialmente in casi di improvvisazione,
ma presenta il problema dello scope e della persistenza. Un altro modo, simile,
che permette l’uso di nomi di variabili più descrittive è quello che utilizza
le variabili di ambiente. Comunque, in entrambi i metodi visti, ci si deve
occupare di assicurare che oggetti e nodi non siano persistenti quando non
sono più necessari.

111
Esecuzione sincrona e asincrona

1 (
2 SynthDef("Help-SynthDef",
3 { arg out=0;
4 Out.ar(out, PinkNoise.ar(0.1))
5 }).send(s);
6 )

8 // creiamo un Synth ed assegnamolo ad una


9 // variabile d’ambiente
10 ~mysynth = Synth.new("Help-SynthDef", s);

12 // liberaimo il synth
13 ~mysynth.free;

15 // ma abbiamo ancora un oggetto Synth


16 ~mysynth.postln;

18 // rimuoviamo il Synth dall’ambiente


19 currentEnvironment.removeAt(\mysynth);

In generale, quello che si vuole è avere un blocco di codice che contiene un


certo numero di task sincroni e asincroni. Il seguente codice causerà un errore,
dal fatto che la SynthDef che serve al server non è stata ancora ricevuta.

1 // Eseguire tutto il codice insieme produce l’errore


2 // ’’FAILURE /s_new SynthDef not found’’
3 (
4 var name;
5 // usiamo un nome random per assicurarci che
6 //non sia stata precedentemente caricata
7 name = "Rand-SynthDef" ++ 400.0.rand;
8 SynthDef(name,
9 { arg out=0;
10 Out.ar(out, PinkNoise.ar(0.1))
11 }).send(s);

13 Synth.new(name, s);
14 )

Una prima soluzione drastica a questo problema potrebbe essere schedulare la


parte di codice dipendente dall’esecuzione, dopo un ritardo sufficientemente
sensato usando un clock.

112
Esecuzione sincrona e asincrona

1 // Questo funziona se la definzione viene inviata al server prima.


2 (
3 var name;
4 name = "Rand-SynthDef" ++ 400.0.rand;
5 SynthDef(name,
6 { arg out=0;
7 Out.ar(out, PinkNoise.ar(0.1))
8 }).send(s);

10 // creiamo un Synth dopo 0.05 secondi


11 SystemClock.sched(0.05, {Synth.new(name, s);});
12 )

Anche se questo metodo funziona, non è molto elegante ed efficiente. Ciò


che si vorrebbe è che l’istruzione successiva venga eseguita immediatamente
dopo che quella precedente sia stata completata. Per esplorare questa strada,
vediamo un esempio già implementato.

Si potrebbe osservare che il primo esempio sopra non era necessariamente


complesso. SynhtDef-play farà tutta questa compilazione, invio e creazione
di Synth in un colpo solo, alla pressione di dell’enter.

1 // Tutto insieme
2 (
3 SynthDef("Help-SynthDef",
4 { arg out=0;
5 Out.ar(out, PinkNoise.ar(0.1))
6 }).play(s);
7 )

Osserviamo la definizione del metodo per SynthDef-play e cosa fa.

113
Esecuzione sincrona e asincrona

1 play { arg target,args,addAction=\\addToTail;


2 var synth, msg;
3 target = target.asTarget;

5 // crea un Synth, ma non un nodo Synth


6 synth = Synth.basicNew(name,target.server);

8 // crea un messaggio che aggiunge un nodo synth


9 msg = synth.newMsg(target, addAction, args);

11 // invia la def, e il messaggio come un messaggio


12 // di completamento
13 this.send(target.server, msg);
14 ^synth // restituisce l’oggetto Synth
15 }

Potrebbe sembrare complicato se non si usano molto spesso le definizioni di


classe; ai nostri scopi comunque focalizziamo l’attenzione su una sola parte, la
piı̆mportante importante, che è il secondo argomento this.send(target.server,
msg);. Questo argomento è un messaggio di completamento ed è un messag-
gio che il server eseguirà quando l’azione send è completata. In questo caso
possiamo tradurre grossolanamente il codice in questo modo: ”crea un nodo
synth sul server che corrisponde all’oggetto Synth”. Molti metodi in SC han-
no la possibilità di includere messaggi di completamento. Qui possiamo usare
SynthDef-send per realizzare la stessa cosa come SynthDef-play:

1 // Compila, invia, and start


2 (
3 SynthDef("Help-SynthDef",
4 { arg out=0;
5 Out.ar(out, PinkNoise.ar(0.1))
6 }).send(s, ["s_new", "Help-SynthDef", x = s.nextNodeID]);
7 // in ’stile messaggio’
8 )
9 s.sendMsg("n_free", x);

I messaggi di completamento necessitano di un messaggio OSC, ma può anche


essere una porzione di codice che, quando valutato, ne ritorna uno.

114
Esecuzione sincrona e asincrona

1 // Interpreta il codice e restituisce un messaggio di completamento.


2 //Il .value si rende necessario
3 // Questo e il precedente esempio sono essenzialmente
4 //la stessa cosa di example SynthDef.play
5 (
6 SynthDef("Help-SynthDef",
7 { arg out=0;
8 Out.ar(out, PinkNoise.ar(0.1))
9 }).send(s, {x = Synth.basicNew("Help-SynthDef"); x.newMsg;
}.value);
10 // in ’stile oggetto’
11 )
12 x.free;

Se si preferisce lavorare in ’stile messaggio’, questo meccanismo è più semplice.


Se invece si preferisce di lavorare in ’stile oggetto’, si possono usare comandi
come newMsg, setMsg, etc. con oggetti per creare messaggi server appropriati.
I due esempi precedenti ne mostrano la differenza. Si veda il file di help
NodeMessaging per maggiori dettagli.
Nel caso di oggetti Buffer, può essere utilizzata una funzione come messaggio
di completamento. Verrà valutata e passata all’oggetto Buffer come argomen-
to. Questo succederà dopo che l’oggetto buffer viene creato, ma prima che
il messaggio sia inviato al server. Può anche essere restituito un messaggio
OSC valido per essere eseguito nel completamento.

1 (
2 SynthDef("help-Buffer",{ arg out=0,bufnum;
3 Out.ar( out,
4 PlayBuf.ar(1,bufnum,BufRateScale.kr(bufnum))
5 )
6 }).load(s);

8 y = Synth.basicNew("help-Buffer"); // non ancora inviato


9 b = Buffer.read(s,"sounds/a11wlk01.wav",
10 completionMessage: { arg buffer;
11 // synth aggiunge il proprio msg s_new per proseguire
12 // dopo il completamento della lettura del buffer
13 y.newMsg(s,\addToTail,[\bufnum,buffer.bufnum])
14 }); // .value NON necessario

16 )
17 // quando fatto ...
18 y.free;
19 b.free;

115
Esecuzione sincrona e asincrona

Lo scopo principare dei messaggi di completamento è offrire messaggi OSC


da inviare al server da eseguire immediatamente sul completamento. Nel caso
di Buffer, non c’è differenza sostanziale fra i seguenti:

1 (
2 b = Buffer.alloc(s, 44100,
3 completionMessage: { arg buffer;
4 ("bufnum:" + buffer.bufnum).postln; }
5 );
6 )
7 // sono equivalenti:
8 (
9 b = Buffer.alloc;
10 ("bufnum:" + b.bufnum).postln;
11 )

Si può anche valutare una funzione in risposta a un messaggio done, o qua-


lunque altro messaggio, usando un OSCresponder o un OSCresponder-
Node. La differenza principare fra i due è che il primo premette solo un
singolo responder per comando, mentre il secondo permette più responder.
Si vedano i rispettivi helpfile.

116
Esecuzione sincrona e asincrona

1 (
2 SynthDef("help-SendTrig",{
3 SendTrig.kr(Dust.kr(1.0), 0, 0.9);
4 }).send(s);

6 // registriamo per ricevere questo messaggio

8 a = OSCresponderNode(s.addr, ’/done’, { arg time, responder, msg;


9 ("This is the done message for the SynthDef.send:" + [time,
responder, msg]).postln;
10 }).add.removeWhenDone;
11 // rimuovimi automaticamente quando fatto

13 b = OSCresponderNode(s.addr, ’/tr’, { arg time, responder, msg;


14 [time, responder, msg].postln;
15 }).add;

17 c = OSCresponderNode(s.addr, ’/tr’, { arg time, responder, msg;


18 "this is another call".postln;
19 }).add;
20 )

22 x = Synth.new("help-SendTrig");
23 b.remove;
24 c.remove;
25 x.free;

17.1 Scheduler
Vediamo in questo paragrafo la definizione dell’oggetto Scheduler al fine di
comprendere come lavora e come può essere utilizzato. Questo oggetto, come
tutti quelli di SC, è sottoclasse di Object. Il suo scopo è semplicemente
schedulare funzioni che saranno valutate in futuro.

Elenchiamo ora metodi e costruttori di tale oggetto:


*new(clock, drift)
clock: un clock, come SystemClock.
drift: se true, schedulerà gli eventi relativi a Main.elapsedTime, altrimenti in
base ai secondi dello scheduler.

play(aTask)
schedula il task da eseguire, con un deltatime restituito dal task.

sched(delta, aTask)

117
Esecuzione sincrona e asincrona

schedula il task.

advance(bySeconds)
permette di avanzare il tempo di n secondi. Ogni task che è schedulato
all’interno del nuovo tempo, viene valutato e, se viene restituito un nuovo
tempo, rischedulato.

seconds_(newSeconds)
permette di avanzare il tempo di n secondi. Ogni task che è schedulato
all’interno del nuovo tempo, viene valutato e, se viene restituito un nuovo
tempo, rischedulato.

isEmpty
restituisce ture se la coda di scheduling è vuota.

clear
svuota la coda di scheduling.

Vediamo un esempio:

118
Esecuzione sincrona e asincrona

1 a = Scheduler(SystemClock);

3 //schedula i task. Il primo parametro \‘e il delta in secondi


4 a.sched(3, { "now it is 3 seconds.".postln; nil });
5 .sched(5, { "now it is 5 seconds.".postln; nil });
6 a.sched(1, { "now it is 1 second.".postln; nil });

8 a.advance(0.5);
9 a.advance(0.5);
10 a.advance(2);
11 a.advance(2);

13 // i beats, secondi and clock sono passati nella funzione task:


14 a.sched(1, { arg beats, secs, clock; [beats, secs, clock].postln });
15 a.advance(1);

17 // lo scheduling \‘e relativo a "ora":


18 a.sched(3, { "now it was 3 seconds.".postln });
19 a.sched(5, { "now it was 5 seconds.".postln });
20 a.sched(1, { "now it was 1 second.".postln });

22 a.advance(0.5);
23 a.advance(0.5);
24 a.advance(2);
25 a.advance(2);

27 // una Routine o un task:


28 a.play(
29 Routine {
30 5.do { arg i; i.postln; 1.yield }
31 }
32 );
33 a.advance(0.9);

35 // scheduling tasks
36 (
37 x = Scheduler(TempoClock.default);
38 Task {
39 inf.do { |i|
40 ("next " ++ i ++ " in task." + Main.elapsedTime).postln;
41 0.5.wait;}
42 }.play(x)
43 )
44 x.advance(0.1);
45 x.seconds;
46 x.advance(5);
47 x.seconds;
48 (
49 Routine {loop { x.advance(0.1); 0.1.wait }}.play;
50 )
51 (
52 Task { 5.do {
53 x.advance(1);
54 119
2.0.rand.wait;}}.play;
55 )

57 x.advance(8.1);

59 Pbind(\degree, Pseries(0, 2, 8), \dur, 0.25).play(x);


Esecuzione sincrona e asincrona

17.2 OSCSched
Vediamo ora un oggetto molto utile e potente che permette di schedulare
l’invio di pacchetti OSC al server.
Il pacchetto è tenuto sul client fino all’ultimo istante possibile, e quindi in-
viato al server corredato di timestamp, appena prima che venga eseguito.
I Bundles possono essere schedulati per esecuzioni precise usando una tem-
porizzazione relativa o assoluta in secondi o beats. I pacchetti possono essere
schedulati su più server ed essere eseguiti simultaneamente.

I Bundles possono essere facilmente creati e modificati fino al momento


in cui verranno inviati al server. Essi sono inviati al server proprio prima
dell’esecuzione.

La classe Tempo viene impiegata per specificare il timing e viene usata per
tutti i calcoli di beats <-> secondi. Inoltre viene usato un oggetto globale
Tempo di default, ma è possibile creare un’istanza specifica di Tempo per i
propri scopi.

C’è un OSCSched globale di default che può essere indirizzato attraverso i


metodi delle classi; questa classe usa il SystemClock e il Tempo globale di
default. È comunque possibile crearne istanze che gestiscono le proprie code
di scheduling, tempistiche, ecc.

Il clock di default è il SystemClock, ma è possibile anche utilizzare l’AppClock.

Se specificata, una clientSideFunction opzionale, verrà valutata sul client


all’istante esatto in cui avviene la schedulazione dell’OSC bundle; potreb-
be essere usata per mostrare un cambiamento nella GUI o per aggiornare
qualche setting su oggetti client side.

Tutti questi metodi esistono in entrambi i modi:

1 class (lo scheduler di default globale)


2 OSCSched.tsched(seconds,server,bundle,clientSideFunction)

4 metodi di istanza (uno schedule specifico).


5 oscSched = OSCSched.new;
6 oscSched.tsched(seconds,server,bundle,clientSideFunction)

120
Esecuzione sincrona e asincrona

I metodi di istanza che possono essere utilizzati sono:

tsched(seconds,server,bundle,clientSideFunction)
time based scheduling
delta specified in seconds

xtsched( seconds,server,bundle,clientSideFunction)
exclusive time based schedule
Qualunque bundle precedentemente schedulato usando xtsched, xsched o xq-
sched sarà cancellato. Si rende utile in situazioni in cui potrebbero essere
inviati diversi bundle, ma si vuole che solo l’ultimo bundle sia usato come
risposta finale. Per esempio: una scimmia preme molti bottini, modificando
la sua mente relativamente a quale suono suona successivamente. Questo po-
trebbe risultare in molti bundle, essendo ammucchiati allo stesso temo, e il
server potrebbe ingolfarsi provando a eseguirli tutti. Così questo forza a una
specie di monofonia dei bundle. Un’altro esempio: un sequencer suona note
in successione, schedulando ognuna quando suona la precedente. Si vorrebbe
passarea a una sequenza differente senza che le note precedentemente schedu-
late siano prese in considerazione. Usando uno degli x-method, ciò è possibile
senza preoccuparsi di come viene gestito il tutto.

sched(beats,server,bundle,clientSideFunction)
delta specified in beats

xsched(beats,server,bundle,clientSideFunction)
exclusive beat based scheduling

qsched(quantize,server,bundle,clientSideFunction)
viene schedualto alla successiva division (4.0 significa il battere di un 4/4), o
immediatamente se si è precisamente sulla division.

xqsched(quantize,server,bundle,clientSideFunction)
exclusive quantized beat based scheduling

tschedAbs(time,server,bundle,clientSideFunction)
viene schedulato al time specificato in secondi.

schedAbs(beat,server,bundle,clientSideFunction)
viene schedulato al beat specificato. Questo metodo utilizza il TempoClock
per lo scheduling.

121
Esecuzione sincrona e asincrona

xblock
blocca qualunque bundle x-schdeulato pendente.

Esistono altri metodi per ottenere valori significativi relativi al tempo di sche-
duling. time
restituisce il time di scheduler.

time_(secondi)
setta il time di scheduler.

beat
restituisce il current beat dello scheduler.

beat_(beat)
setta il beat corrente dello scheduler. Viene utilizzato per avviare una “song”
: azzera il beat e tutti i time assoluti precedentemente schedulati
deltaTillNext(quantizeDivision)
restituisce il numero di secondi fino alla prossima quantizeDivision.
4.0 significa la prossima battuta
16.0 significa le prossima 4 battute
0.25 significa il 16-esimo
Questo valore è corretto solo se non viene modificato il tempo.

clear
elimina tutti gli eventi schedulati.

Tutti gli x-metodi stabiliscono un blocco tale che un uno schedale seguente
che usa un altro x-metodo causerà la cancellazione del precedente. Questè è
particolarmente utile quando si controlla qualcosa da una gui o da un proces-
so, e cambia la mente prima che l’evento triggerato avvenga. Un altro esempio
è un pattern che ritorna valori di delat beat Schedulando ripetutamente la
prossima nota all’istante di esecuzione della nota corrente. Per switchare il
pattern con un altro o avviarlo brutalmente sopra, semplicemente si faccia
così: se si usa xsched, allora l’evento schedulato precedentemente verrà cancel-
lato. In molti casi, occorrerà creare un’istanza privata di OSCSched quando
si usa questa tecnica.
Warning: sono esempi vecchi, non testati di recente.
Caricare tutti questi per usarli nel prossimo esempio

122
Esecuzione sincrona e asincrona

1 s = Server.local;
2 s.boot;
3 (
4 SynthDef("bubbles", {
5 var f, zout;
6 f = LFSaw.kr(0.4, 0, 24,
7 LFSaw.kr([8,7.23], 0, 3, 80)
8 ).midicps;
9 zout = CombN.ar(SinOsc.ar(f, 0, 0.04), 0.2, 0.2, 4);
10 Out.ar(0, zout);
11 }).send(s);

13 i = [ ’/s_new’, "bubbles", 1002, 1, 0 ];


14 o = [ ’/n_free’, 1002 ];
15 c = OSCSched.new;
16 )

18 // il time dello scheduler = numero di secondi dall’avvio di SC


19 c.time.postln;

21 // di defautl 1.0 beats per second


22 Tempo.tempo.postln;

24 // numero di beat dall’avvio di SC


25 c.beat.postln;

27 // settiamo il default global Tempo


28 Tempo.bpm = 96;

30 c.beat.postln;
31 c.beat = 0.0;
32 c.beat.postln;

34 // "starting" in 2.0 beats


35 c.sched(2.0,s,i,{
36 "starting".postln;
37 });

39 //libera il synth nella prossima battuta


40 c.qsched(4.0,s,o,{
41 c.beat.postln;
42 });

44 // avvia in 4.0 secondi


45 c.tsched(4.0,s,i,{
46 "starting".postln;
47 });

123
Esecuzione sincrona e asincrona

Absolute Beat / Time scheduling

1 c.clear;

3 (
4 c.beat = 32.0; // we are starting at beat 32
5 c.schedAbs(36.0,s,i); // in
6 c.schedAbs(39.0,s,o); // out
7 c.schedAbs(41.0,s,o); // out
8 c.schedAbs(40.0,s,i); // but first in
9 c.schedAbs(43.0,s,i,{
10 c.schedAbs(42.0,s,o,{
11 "this will never happen, its in the past".postln;
12 });
13 c.schedAbs(46.0,s,o);
14 });
15 )
16 \stocode
17 Exclusive
18 \startcode
19 (
20 c.xsched(4.0,s,i,{
21 "4.0".postln;
22 });
23 c.sched(8.0,s,o); // not affected
24 // changed my mind...
25 c.xsched(3.0,s,i,{ // the x-methods are exclusive
26 "3.0".postln;
27 });
28 )

17.3 Task
superclass: PauseStream Il Task di Supercollider è un processo che può esse-
re sospeso; è implementato dal wrapping di un PauseStream in una Routine.
Molti dei suoi metodi (start, stop, reset) sono quindi ereditati da Pause-
Stream.

Task.new(func, clock)
func - una Function da valutare.
clock - Un Clock in cui eseguire la Routine. Se non si passa un Clock, viene
considerato di default un’istanza di TempoClock. I metodi che richiamano le
primitive di Cocoa (per esempio funzioni di GUI) devono essere eseguite in
AppClock.

124
Esecuzione sincrona e asincrona

1 t = Task({
2 50.do({ arg i;
3 i.squared.postln;
4 0.5.wait
5 });
6 });

8 t.start;
9 t.pause;
10 t.resume;
11 t.reset;
12 t.stop;

17.4 Tempo
Questa classe rappresenta il concetto di tempo. Può essere impiegata per tra-
sformare il tempo in secondi, beats e bars. Questa classe detiene un’istanza
di TempoClock che viene settato al proprio tempo ogni volta che cambia.

Può essere impiegato un TempoBus avviato sul server, e conterrà il tem-


po dell’oggetto Tempo inteso come un valore floats su un Bus sul server. Le
UGens possono usarlo per scalare la loro frequenza per basi ritmiche, ecc.

Può essere anche impiegato per convertire beats <-> secondi, ma questo
valore è accurato solo nel momento della computazione. Se il tempo varia, il
valore non è più valido. TempoBus aggiunge se stesso come un dipendente
dell’oggetto Tempo, così quando il tempo cambia, è informato e aggiorna il
valore coerentemente sul bus.

1 Tempo.bpm = 170;
2 Tempo.tempo = 2.3; // in beats per second

4 Tempo.gui; // there is a gui class

6 Tempo.bpm = 170;
7 Tempo.beats2secs(4.0).postln;

9 Tempo.bpm = 10;
10 Tempo.beats2secs(4.0).postln;

Tutte i metodi della classe si riferiscono al tempo globale di default. È possi-


bile creare un’istanza di Tempo se si rendono necessari tempi separati indi-
viduali.

125
Esecuzione sincrona e asincrona

1 t = Tempo.new;
2 u = Tempo.new;

4 t.bpm = 170;
5 u.tempo = 1.3; // in beats per second
6 t.gui;

Tutti i metodi seguenti esistono come metodi di classe (il tempo di default)
e come metodi di istanze. bpm

bpm_(beatsPerMinute)
Tempo.bpm = 96; o Tempo.bpm_(96);

tempo
in beats per second

tempo_(beatsPerSecond)
Tempo.tempo = 2.0; o Tempo.tempo_(2.0);

beats2secs(beats)

secs2beats(seconds)

bars2secs(bars)
È possibile cambiare il beats per bar: Tempo.beatsPerBar = 7.0;

secs2bars(seconds)

sched(delta,function)

Schedula una funzione che viene valuta delta beats da ora.


Se si cambia il tempo dopo lo scheduling, la funzione sarà ancora valutata al
tempo originalmente calcolato. Una soluzione più sofisticata verrà presentata
in seguito.
schedAbs(beat,function)
Schedula una funzione che viene valutata ad un beat assoluto, come misurato
prima dell’istante in cui avviene il primo boot di SC. Se si utilizzano OSC-
Sched per controlli più sofisticati (in grado di resettare il beat).Se si cambia il

126
Esecuzione sincrona e asincrona

tempo dopo lo scheduling, la funzione verrà ancora valutata al tempo calco-


lato originariamente. Una soluzione più sofisticata sarà presentata in seguito.

17.5 Routine
Superclass: Thread
Le Routine sono funzioni che possono ritornare nel mezzo dell’esecuzione e
quindi essere riesumate dove sono state lasciate quando vengono richiamate
nuovamente. Le Routine possono essere usate per implementare co-routines,
tipiche di moltri linguaggi.
Le Routine sono inoltre utilizzate per scrivere cose che si comportano come
Streams.
Infine, essere ereditano comportamenti per operazioni matematiche e filtraggi
da Stream.

*new(func, stackSize, seed)

Crea un’istanza di Routine con la data funzione passata come argomento.


Gli argomenti stackSize e random seed possono essere sovrascritti se lo si
desidera.

1 (
2 a = Routine.new({ 1.yield; 2.yield; });
3 a.next.postln;
4 a.next.postln;
5 a.next.postln;
6 )

value(inval)
resume(inval)
next(inval)
Questi sono tutti sinonimi per lo stesso metodo.

La funzione Routine è sia avviata se non è ancora stata chiamata, sia se viene
remutata da dove era stata lasciata. L’argomento inval viene passato come
argomento alla Routine se è stata avviata, o come risultato del yeld metodo
se è stata resumata da un yeld.
Ci sono di base 2 condizioni per una Routine:

127
Esecuzione sincrona e asincrona

1. quando la Routine parte.


2. quando la Routine continua dopo che ha yeldato
Quando la Routine parte (richiamando i metodi precedente), viene passato
un primo inval. Questo inval è accessibile come argomento della funzione
Routine:

1 (
2 Routine { arg inval;
3 inval.postln;
4 }.value("hello routine");
5 )

Quando c’è un yield nella routine, la volta successiva si richiama next (o


un sinonimo) e la routine continua da lì, e si considera la possiblità che
venga restituito un valore all’esterno della routine. Per accedere ai quei valori
dentro la continuazione della routine, si assegna il result of the yield call a
una variabile:

1 (
2 r = Routine { arg inval;
3 var valuePassedInbyYield;
4 inval.postln;
5 valuePassedInbyYield = 123.yield;
6 valuePassedInbyYield.postln;

8 }
9 )

11 r.value("hello routine");
12 r.value("goodbye world");

Tipicamente il nome inval (o inevent) è riutilizzato, invece di dichiarare una


variabile come “valuePassedInbyYield” come si può vedere dall’esempio suc-
cessivo:

1 (
2 r = Routine { arg inval;
3 inval.postln;
4 inval = 123.yield;
5 inval.postln;
6 }
7 )
8 r.value("hello routine");
9 r.value("goodbye world");

128
Esecuzione sincrona e asincrona

Tipicamente una routine usa un yeld multiplo, in cui l’inval viene riassegnato
ripetutamente:

1 (
2 r = Routine { arg inval;
3 inval.postln;
4 5.do { arg i;
5 inval = (i + 10).yield;
6 inval.postln;
7 }
8 }
9 )
10 (
11 5.do {
12 r.value("hello routine").postln;
13 }
14 )

reset

Forza il restart dall’inizio la prossima volta che viene richiamata la Routine.


Una Routine non può resettare se stessa se non tramite il metodo yieldAn-
dReset.
play(clock, quant)
clock: un Clock, di default TempoClock
quant: un numero n (quantizzazione a n beats) oppure un array [n, m] (quan-
tizzazione a n beats, con offset m)

Nelle applicazioni SuperCollider, una Routine può essere utilizzata insieme a


un Clock, come si fa con gli Stream. Tutte le volte che la routine opera una
yeld, dovrà farlo con un float; usualmente verrà messa in pausa per i secondi
stabiliti, e quindi verrà resumata la routine, passando il tempo di clock defi-
nito.

17.5.1 Variabili di istanza accessibili


Routine eredita da Thread, e permette accesso ad alcuni dei suoi stati:

129
Esecuzione sincrona e asincrona

− beats restituisce i beat trascorsi della routine. Se la routine è ferma questo


valore non viene aggiornato.

− seconds restituisce i secondi trascorsi della routine. Se la routine è ferma


questo valore non viene aggiornato.

− clock restituisce il clock del thread. Se non sta girando, il valore è quello
del SystemClock.

1 (
2 r = Routine { arg inval;
3 loop {
4 // thisThread refers to the routine.
5 postf("beats: % seconds: % time: % \n",
6 thisThread.beats, thisThread.seconds, Main.elapsedTime
7 );
8 1.0.yield;

10 }
11 }.play;
12 )

14 r.stop;
15 r.beats;
16 r.seconds;
17 r.clock;

Esempio routine:

130
Esecuzione sincrona e asincrona

1 (
2 var r, outval;
3 r = Routine.new({ arg inval;
4 ("->inval was " ++ inval).postln;
5 inval = 1.yield;
6 ("->inval was " ++ inval).postln;
7 inval = 2.yield;
8 ("->inval was " ++ inval).postln;
9 inval = 99.yield;
10 });

12 outval = r.next(’a’);
13 ("<-outval was " ++ outval).postln;
14 outval = r.next(’b’);
15 ("<-outval was " ++ outval).postln;
16 r.reset; "reset".postln;
17 outval = r.next(’c’);
18 ("<-outval was " ++ outval).postln;
19 outval = r.next(’d’);
20 ("<-outval was " ++ outval).postln;
21 outval = r.next(’e’);
22 ("<-outval was " ++ outval).postln;
23 outval = r.next(’f’);
24 ("<-outval was " ++ outval).postln;
25 )

27 (
28 var r;
29 r = Routine.new({
30 10.do({ arg a;
31 a.postln;
32 // Often you might see Wait being used to pause a routine
33 // This waits for one second between each number
34 1.wait;
35 });
36 // Wait half second before saying we’re done
37 0.5.wait;
38 "done".postln;
39 });

41 SystemClock.play(r);
42 )

131
Scrivere classi in SC

Capitolo 18 Scrivere classi in SC

18.1 Oggetti e Messaggi


Il linguaggio di SuperCollider è un linguaggio a oggetti. Tutte le entità nel
linguaggio sono oggetti. Un oggetto è qualcosa che contiene dei dati che rap-
presentano lo stato dell’oggetto e un insieme di operazioni che possono essere
calcolate sull’oggetto. Tutti gli oggetti sono instanze di qualche classe che
descrive la struttura dell’oggetto e le sue operaioni. In SC sono anche oggetti
i numeri, stringhe di caratteri, collezioni di oggetti, ugens, campioni wavem
punti, rettangoli, finestre grafiche, bottoni, spider e molte altre cose.

Le operazioni sugli oggetti possono essere invocate tramite messaggi. Un mes-


sage è un richiesta per un oggetto, detto receiver, di effettuare una delle sue
operazioni. Questo significa che quale operazione calcolare viene determina-
ta dalla classe dell’oggetto. Oggetti di differenti classi possono implemen-
tare lo stesso messaggio in differenti modi, ognuno appropriato alla classe
dell’oggetto. Per esempio tutti gli oggetti capiscono il messaggio value. Molti
oggetti semplicemente ritornano se stessi in risposta a value, ma altri oggetti,
come fossero funzioni, prima valutano se stessi e poi rispondono il risultato
di questa valutazione. L’abilità di oggetti differenti di reagire differentemente
allo stesso messaggio è conosciuta come polymorphism ed è probabilmente il
concetto più importante in OOP dal fatto che permette di astrarre il com-
portamento degli oggetti dal punto di vista del user dell’oggetto (il client).

L’insieme di messagi che un oggetto conosce e a cui risponde è detto essere


la sua interface. Un insieme di messaggi che implementano uno speciale tipo
di comportamento è conosciuto come protocol. Un’interfaccia oggetto po-
trebbe includere diversi protocolli che permettono allìoggetto di interagire in
diversi contesti. Per esempio tutti gli oggetti implementano il protocollo ’de-
pendancy’che permette all’oggetto di notificare agli oggetti dipendendti che
ha subito cambiamenti e che i suoi dipendenti devono assolutamente aggior-
narsi. Uno stato interno di un oggetto potrebbe sono essere variato dall’invio
di messaggi. Questo permette che l’implementazione dell’oggetto sia nasco-
sta agli occhi del client. Il vantaggio di tutto ciò è che il client non dipende
dall’implementazione dell’oggetto e che l’implementazione può essere modifi-
cata senza dover modificare il client.

132
Scrivere classi in SC

18.2 Classi, Variabili di instanza e metodi


Un class di oggetti contiene la descrizione dei dati dell’oggetto e le operazio-
ni. Una calle descrive inoltre come creare un oggetto che è un’istanza della
classe.

Un dato di un oggetto è contenuto nella sua instance variables. Queste sono


variabili che descrivono lo stato dell’oggetto. I valori delle variabili di istanza
sono essistessi oggetti. Per esempio, le istanze della classe Point hanno varia-
bili di istanza x e y che contengono le coordinate di Point. Una variabile di
istanza è accessibile solo da dentro la classe stessa.ò L’autore di una classe
potrebbe decide di permettere l’accesso per il client alle sue variabili di istan-
za aggiungendo messaggi di getter e/o setter.

Un metodo è una descrizione delle operazioni necessarie per implementare un


messaggio di una particolare classe. I metodi nella classe dicono come imple-
mentare i mesaggi inviati alle istanze. Una classe contiene una definizione di
metodo per ogni messaggio a cui la sua istanza risponde. I metodi general-
mente possono ricadere in diverse categorie. Alcuni metodi rispondono con
alcune proprietà del ricevitore. Altri richiedono al ricevitore di modificare lo
stato interno. Altre ancora potrebbero richiedere al ricevitore di restituire il
valore di qualche computazione.

18.2.1 Classi
Tutti gli oggetti in SC sono membri di una classe che descrive dati e inter-
faccia degli oggetti stessi. Il nome di una classe deve iniziare con una lettera
maiuscola. I nomi delle classi sono gli unici valori globali in SCLang. Dal
momento che le classi sono esse stesse oggetti, il valore di un indentificatore
di nome di classe è l’oggetto che rappresenta la classe stessa.

18.2.2 Ereditarietà
Per specificare l’ereditarietà di una classe da un’altra, si segue la sintassi:

1 NewClass : SomeSuperclass {

3 }

Se non si specifica una superclasse, viene assunta di default la superclasse


Object.

133
Scrivere classi in SC

1 NewClass { // : Object \‘e implicito

3 }

18.2.3 Variabili di istanza


I dati di un oggetto sono contenuti nelle sue variabili di istanza; possono es-
sere di due tipi, named e indexed. Ogni oggetto contiene una copia separata
delle proprie variabili di istanza.

Alcune istanze non hanno variabili di istanza, ma hanno un valore atomico.


Classi di questo tipo sono Integer, Float, Symbol, True, False, Nil, Infinitum,
Char, Color.

La lista delle dichiarazioni delle variabili d’istanza appare dopo la parentesi


graffa aperta della definizione della classe; esse sono precedute dalla parola
riservata var. Le variabili di istanza in questa lista possono essere inizializzate
a un letterale di default usando il segno dell’uguale. Le variabili di istanza che
non sono esplicitamente inizializzate, saranno inizializzate automaticamente
a nil.

Le variabili di istanza possono essere lette e cambiate direttamente dai metodi


propri della classe, ma ugualmente dai client è possibile accedervi aggiungen-
do, nella classe, messaggi detti di getter e setter nella classe.
− Un messaggio getter è un messaggio con lo stesso nome della variabile e
restituisce il valore della variabile stessa quando inviata al ricevente.
− Un messaggio setter è un messaggio con lo stesso nome della variabile
seguita da un carattere di underscore _ e imposta il valore della variabile
di istanza al valore passato come argomento del messaggio stesso.
− I metodi getter e setter possono essere definiti nella dichiarazione della
variabile di istanza.
Inoltre:
− Per una variabile di istanza, anteponendo semplicemente un simbolo di
minore < al nome della variabile, viene creato automaticamente un mes-
saggio getter del tipo someObject.getMe;
− Per una variabile di istanza, anteponendo semplicemente un simbolo di
maggiore > al nome della variabile, viene creato automaticamente un
messaggio setter del tipo someObject.setMe_(value);

134
Scrivere classi in SC

In generale nulla vieta di utilizzare il meccanismo dell’override dei metodi


getter e setter in una sottoclasse scrivendo manualmente i metodi; l’unica
condizione da tenere in considerazione è che il metodo setter abbia un solo
argomento, del tipo:

1 variable_ { arg newValue;


2 variable = newValue.clip(minval,maxval);
3 }

Nel caso compaiano entrambi i simboli, l’ordine corretta da seguire è <>

1 var <getMe, >setMe, <>getMeOrSetMe;

3 In generale:

5 var a, <b, >c, <>d;


6 // a non ha metodi getter o setter.
7 // b ha un getter, ma non un setter.
8 // c ha un setter, ma non un getter.
9 // d ha entrambi i metodi getter e setter.

Vediamo un esempio:

135
Scrivere classi in SC

1 Point {
2 // x e y sono variabili di instanza che hanno entrambi metodi getter e
setter.
3 var <>x = 0, <>y = 0;
4 ...
5 }

7 p = Point.new;
8 p.x_(5); // invia un messaggio setter per settare x a 5
9 p.y_(7); // invia un messaggio setter per settare y a 7
10 p.x = 5; // invia un messaggio setter usando l’assegnamento ([03
Assignement]
11 p.y = 7; // invia un messaggio setter usando l’assegnamento ([03
Assignement]
12 a = p.x; // invia un messaggio getter per x
13 b = p.y; // invia un messaggio getter per y

15 Quindi, in una classe:

17 NewClass : Superclass {

19 var myVariable;

21 variable { //
22 ^variable // ? <variabile
23 } //

25 variable_ { arg newValue; //


26 variable = newValue; //? >variabile
27 } //
28 }

18.2.4 Variabili di Classe


Le variabili di classe sono valori che vengono condivisi fra tutti gli oggetti
nella classe. La lista di dichiarazione delle variabili di classe è preceduta dalla
parola chiave classvar e potrebbe essere interavallata da liste di dichiarazioni
di variabili di istanza. Per le variabili di classe, come per le variabili di istanza,
si potrebbe avere accesso solo tramite metodi della classe, ma è possibile avere
anche metodi getter e setter dello stesso tipo definito precedentemente per le
variabili di istanza.

18.2.5 Metodi di istanza


I messaggi di un’interfaccia di una classe sono implementati nei metodi della
classe. Quando viene inviato un messaggio a un oggetto, viene eseguito il

136
Scrivere classi in SC

metodo il cui nome corrisponde al messaggio. La definizione dei metodi segue,


nell’ordine, la dichiarazione delle variabili di classe e le variabili di istanza.

Le definizioni dei metodi sono simili per sintassi alle FunctionDefs: iniziano
con il selettore del messaggio, che deve essere un operatore binario o un
identificatore. I metodi possono avere argomenti e dichiarazioni di variabili
al loro intermo come per le FunctionDefs. I metodi, comunque hanno un
primo argomento implicito, this, che è il ricevente del messaggio. La variabile
this potrebbe essere riferita a qualunque altro metodo variabile del metodo.
E’possibile anche non assegnare un valore a this. In generale dentro il metodo
di istanza, la parola chiave this si riferisce all’istanza.

In generale questi metodi di istanza sono specificati nel seguento modo:

1 instanceMethod { arg argument;

3 }

Per avere un valore di ritorno dal metodi si usa il carattere ^.

1 someMethod {
2 ^returnObject
3 }

Sono possibili più punti di uscita:

1 someMethod { arg aBoolean;


2 if(aBoolean,{
3 ^someObject
4 },{
5 ^someOtherObject
6 })
7 }

Se non viene specificato ,̂ il metodo restituirà l’istanza (e nel caso dei metodi
di classe, restituirà la classe).

18.2.6 Metodi di classe


I metodi della classe sono metodi che implementano messaggio che vengono
inviati a un oggetto della classe. Un esempio comune è il messaggio new che

137
Scrivere classi in SC

viene inviato all’oggetto per creare una nuova istanza della classe. I nomi dei
metodi di classe sono preceduti, nella definizione, da un asterisco:

1 *classMethod { arg argument;

3 }

In generale dentro il metodo, la parola chiave this si riferisce alla classe.

18.3 Creazione di una nuova istanza


Il costrutto Object.new restituisce, com’è intuibile un nuovo Oggero. Quando
si utilizza l’override, il metodo di classe .new richiama la superclasse, che
a sua volta richiama la sua superclasse, risalendo l’albero fino ad Object; a
questo punto viene creato un oggetto e allocata la memoria necessaria.

1 // questo esempio non aggiunge funzionalit\‘a


2 *new {
3 ^super.new
4 }

6 // questo \‘e un normale costruttore


7 *new { arg arga,argb,argc;
8 ^super.new.init(arga,argb,argc)
9 }
10 init { arg arga,argb,argc;
11 // do initiation here
12 }

In questi casi, si noti che super.new richiama il metodo new della superclasse
e restituisce un oggetto nuovo. In sequenza viene richiamato il metodo di
istanza .init sull’oggetto appena creato.

Attenzione: Se la superclasse richiama a sua volta super.new.init, ci si aspetta


che venga richiamato il metodo .init della superclasse; invece il messaggio .init
troverà l’implementazione della classe dell’oggetto appena creato che è ora la
nuova sottoclasse. Così occore usare un nome di metodo unico, come per
esempio “myclassinit” se potrebbe essere un problema tutto questo.

138
Scrivere classi in SC

18.4 Overriding dei metodi (Overloading)


Per modificare il comportamento della superclasse, vengono spesso sovrascrit-
ti i suoi metodi. Da notare che un oggetto verifica prima la propria imple-
mentazione del metodo e, successivamente l’implementazione nell superclasse.
Qui NewClass.value(2) restituisce 6, non 4:

1 Superclass {

3 calculate { arg in; in * 2 }


4 value { arg in; ^this.calculate(in) }
5 }

7 NewClass : Superclass {

9 calculate { arg in; in * 3 }


10 }

Nel caso sia necessario il metodo della superclasse e la sua implementazione,


può essere invocato utilizzato la parola chiava super.

1 Superclass {
2 var x;

4 init {
5 x = 5;
6 }
7 }

9 NewClass : Superclass {
10 var y;
11 init {
12 super.init;
13 y = 6;
14 }
15 }

18.5 Metodi definiti in file esterni


I metodi possono essere aggiunti in classi in file separati. Per convenzione, il
nome del file comincia con una lettera minuscola; il nome del metodo o della
feature che i metodi supportano.

139
Scrivere classi in SC

1 Syntax:

3 + Class {
4 newMethod {

6 }
7 *newClassMethod {

9 }
10 }

18.6 Tricks and Traps

"Superclass not found..."

In un dato file di codice, è possibile solo inserire classi che ereditano da


ogni Object, ogni altro e una classe esterna. In altre parole, non è possibile
ereditare da due classi separate che sono definite in file separati. Se si vuo-
le dichiarare una variabile in una subclasse, e usare lo stesso nome di una
variabile dichiarata in una superclasse, occorrerà verificare che entrambe le
variabili esistano, ma solo una nella classe attuale è accessibile. E’meglio non
farlo! Porterà da qualche parte ad avere qualche errore di compilazione.

140
Glossario

Capitolo 19 Glossario
• buffer - un header e un array di sample espressi in floating poinf. I buffer
sono usati per i file sonoi, le linee di ritaro, array di controlli globali.

• group - una linked list di nodi. I Gruppi forniscono modi per controlla-
re l’esecuzione di molti node in un colpo solo. Un gruppo è un tipo di nodo.

• MIDI - un protocollo per inviare dati di controllo fra synth.

• node - un oggetto in un albero di oggetti eseguito dal synth engine del


server eseguito in una DFT. Ci sono due tipi di nodi: synths e gruppi.

• Open Sound Control - un protocollo definito da CNMAT a UCBerkeley


per il controllo dei sintetizzatori. Si veda: http://cnmat.cnmat.berkeley.edu/OSC/
.

• OSC - Open Sound Control.

• synth - modulo sonoro. Simile a "voice" in altri sistemi. I Synths sono


indicati da un numero.

• synth definition - una definizione per creare nuovi synths, simile a "in-
strument" in altri sistemi.

• TCP - un protocollo di rete per lo streaming dei dati.

• UDP - un protocol di rete per l’invio di datagrammi.

141

Potrebbero piacerti anche