Scit Tutorial
Scit Tutorial
by Paolo Olocco
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
8 Help di Supercollider 29
8.1 Classi and Metodi 29
8.2 Shortcut sintattici 30
8.3 Snooping, etc. 30
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
2
16.5 Timing in NodeProxy 106
19 Glossario 141
3
Primi Passi
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;
1 Hello World!
2 Hello World!
4
Primi Passi
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.”.
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:
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.
7
Accendiamo i motori
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:
8
Accendiamo i motori
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;
[Server]
9
Funzioni e Funzionalità
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;
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à
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);
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, 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
... )
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 )
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:
È 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 )
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:
15
Funzioni e Suoni
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 ... )
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.
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 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à:
− 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 )
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.
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:
20
Living Stereo
... 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.
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:
21
Living Stereo
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 (
2 { var freq;
3 freq = [[660, 880], [440, 660], 1320, 880].choose;
4 SinOsc.ar(freq, 0, 0.2);
5 }.play;
6 )
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.
23
Creiamo un Mix
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;
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).
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:
25
Scoping and Plots
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.
1 Server.internal.boot;
26
Scoping and Plots
1 {
2 PinkNoise.ar(0.2) + SinOsc.ar(440, 0, 0.2) + Saw.ar(660, 0.2)
3 }.scope;
Come Function-plot, Function-scope può essere utile per testing e per vedere se
davvero si ottiene quello che si vuole.
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.
28
Help di Supercollider
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.
30
SuperCollider 3 Synth Server: architettura
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.
• 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.
− 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.
• 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.
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
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
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.
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;
− 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.
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.
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 è:
− writedef scrive la definizione sul disco sotto forma di file così da permettere
al server di caricarla.
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.
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.
È 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
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.
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;
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;
40
SynthDefs e Synths
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.
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.
42
I Buss
(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]);
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
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.
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);
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);
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 )
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.
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);
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:
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.
50
I Buss
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.
51
I Buss
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:
Esiste ovviamente una comodo oggetto che permette l’astrazione Server per rap-
presentare gruppi di nodi nell app client: Group.
1 g = Group.new;
2 h = Group.before(g);
3 g.free; h.free;
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;
15 SynthDef("tutorial-Reverb2", {
16 arg outBus = 0, inBus;
17 var input;
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.
− 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;
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
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.
1 s = Server.local;
2 a = RootNode(s);
3 b = RootNode(s);
56
I Gruppi
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;
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 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
58
I Gruppi
1 g = Group.new;
12 g.free;
59
I Gruppi
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:
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.
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ò scrivere
Molti dei metodi dei buffer presentano numerosi argomenti, per informazioni com-
plete si faccia riferimento all’help file [Buffer].
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:
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;
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.)
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.
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
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.
65
Buffer
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;
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.
Per esempio:
1 b = Buffer.alloc(s,16);
6 // recupera i 3 valori
7 b.getn(0, 3, {|msg| msg.postln});
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;
( Un FloatArray è una sottoclasse di Array che può contenere solo valori Float).
68
Buffer
accessibili globalmente sul server. Un esempio di un altro uso potrebbe essere una
lookup table per waveshaping.
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
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
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 )
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
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:
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 è:
il synth che si occupa dell’effetto non ”ascolterebbe”il synth sorgente, e quindi non
si otterrebbe il risultato richiesto.
74
Ordine di esecuzione
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.
2. muovendo nodi
75
Ordine di esecuzione
Per ogni addAction c’è un corrispondente metodo detto “di convenienza” lato-
client della classe Synth:
76
Ordine di esecuzione
2. Muovendo i nodi
I metodi per spostare i nodi sono: .moveBefore .moveAfter .moveToHead .move-
ToTail
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:
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.
77
Ordine di esecuzione
Schematicamente:
Per rendere chiara la struttura nel codice, è anche possibile creare un gruppo per
l’effetto (anche se contiene un solo synth), ottenendo:
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.
80
Ordine di esecuzione
Questo potrebbe anche essere vero se il Synth 2 venisse dopo Synth 1 e il Synth 3.
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);
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]);
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.
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 {
2 RLPF.ar(Saw.ar([100,250],0.05), XLine.kr(8000,400,5), 0.05) }.play;
83
Ordine di esecuzione
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;
1 Klank.ar(’[[400,500,600],[1,2,1]], z)
84
Ordine di esecuzione
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 ]
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.
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:
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 )
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);
13 a.value(5, [0.3, 0.3, 0.2], [12, 32, 64], [1000, 710, 700]);
87
Just In Time Programming
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.
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
1. Uno stile di accesso è la classe def (Pdef, Ndef etc.) che collega un symbol a un
oggetto in un modo specifico:
88
Just In Time Programming
In generale consideriamo:
2. Un altro modo, per i NodeProxy lato server, è un ambiente che restituisce pla-
ceholders su richiesta:
1 ProxySpace.push
2 ~out = { ...}
3. Esiste infine anche un accesso diretto senza usare gli schemi di accesso offerto da
NodeProxy, TaskProxy etc. Internamente si usano le seguenti classi:
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
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
90
Just In Time Programming
Si vedano, per completezza, altri proxy lato client come [Tdef] [Pdefn] [Pdef]
b. NodeProxy
91
Just In Time Programming
Un semplice modo per creare node proxy implica che deve essere utilizzato un
proxy space, come nell’esempio seguente.
92
Just In Time Programming
93
Just In Time Programming
a. Ambiente di default
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
1 x = currentEnvironment;
2 x[\a] + x[\b] // = ~b + ~a
1 x.know = true;
2 x.a + x.b;
94
Just In Time Programming
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)
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
95
Just In Time Programming
1 (
2 ~x = {
3 RLPF.ar(Impulse.ar([5, 7]) * 5, [1450, 1234], 0.2)
4 }
5 )
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 )
45 // l’ascolto di ~y diretto
46 ~y.play;
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
22 ~b.kr(8);
23 ~c.ar; // inizializzazione esplicita
24 p.postln; // visualizza l’intero spazio proxy
98
Just In Time Programming
1 ~x.play; //suona
2 ~x = { PinkNoise.ar(0.5) };
3 p.postln; //p = proxy space
8 // per rimuovere tutti gli oggetti busve liberarli dal bus allocator,
si usa clear:
9 p.clear;
10 currentEnvironment.postln;
99
Just In Time Programming
3 EnvirDocument(p, "proxyspace");
4 // per testarlo,si verifica currentEnvironment
5 //e dentro il documento envir.
100
Just In Time Programming
15 ~z.end;
16 ~y.end;
101
Just In Time Programming
1 ~z.play;
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.
a. play/stop
102
Just In Time Programming
1 ~z.play(vol:0.3);
2 ~z.vol = 0.8;
b. send / release
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
1 ~y.play;
2 ~y = { arg freq=500; SinOsc.ar(freq * [1, 1.1]) * 0.1 };
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);
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);
105
Just In Time Programming
1 ~y.set(\freq, 440);
2 ~y.xmap(\freq, ~c);
1 ~y.unmap;
1 ~y.nodeMap;
2 p.clear(8); // ripulisce l’intero proxy space in 8 sec.
a. clock
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;
9 // aggiungiamo un clock:
10 ~x.clock = TempoClock.default;
11 ~y.clock = TempoClock.default;
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).
107
Just In Time Programming
14 ~x.free;
15 ~y.free;
108
Just In Time Programming
1 ProxySynthDef.sampleAccurate = false;
2 ~x.play;
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;
24 ~x.rebuild;
25 r.stop;
26 p.clear; // rimuove il tutto
109
Esecuzione sincrona e asincrona
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.
110
Esecuzione sincrona e asincrona
17 // carichiamo la Synthdef
18 s.loadSynthDef("Help-SynthDef");
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 )
12 // liberaimo il synth
13 ~mysynth.free;
13 Synth.new(name, s);
14 )
112
Esecuzione sincrona e asincrona
1 // Tutto insieme
2 (
3 SynthDef("Help-SynthDef",
4 { arg out=0;
5 Out.ar(out, PinkNoise.ar(0.1))
6 }).play(s);
7 )
113
Esecuzione sincrona e asincrona
114
Esecuzione sincrona e asincrona
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);
16 )
17 // quando fatto ...
18 y.free;
19 b.free;
115
Esecuzione sincrona e asincrona
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 )
116
Esecuzione sincrona e asincrona
1 (
2 SynthDef("help-SendTrig",{
3 SendTrig.kr(Dust.kr(1.0), 0, 0.9);
4 }).send(s);
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.
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);
8 a.advance(0.5);
9 a.advance(0.5);
10 a.advance(2);
11 a.advance(2);
22 a.advance(0.5);
23 a.advance(0.5);
24 a.advance(2);
25 a.advance(2);
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);
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.
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.
120
Esecuzione sincrona e asincrona
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);
30 c.beat.postln;
31 c.beat = 0.0;
32 c.beat.postln;
123
Esecuzione sincrona e asincrona
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 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
6 Tempo.bpm = 170;
7 Tempo.beats2secs(4.0).postln;
9 Tempo.bpm = 10;
10 Tempo.beats2secs(4.0).postln;
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)
126
Esecuzione sincrona e asincrona
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.
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 (
2 Routine { arg inval;
3 inval.postln;
4 }.value("hello routine");
5 )
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");
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
129
Esecuzione sincrona e asincrona
− 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
132
Scrivere classi in SC
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 }
133
Scrivere classi in SC
3 }
134
Scrivere classi in SC
3 In generale:
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
17 NewClass : Superclass {
19 var myVariable;
21 variable { //
22 ^variable // ? <variabile
23 } //
136
Scrivere classi in SC
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.
3 }
1 someMethod {
2 ^returnObject
3 }
Se non viene specificato ,̂ il metodo restituirà l’istanza (e nel caso dei metodi
di classe, restituirà la classe).
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:
3 }
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.
138
Scrivere classi in SC
1 Superclass {
7 NewClass : Superclass {
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 }
139
Scrivere classi in SC
1 Syntax:
3 + Class {
4 newMethod {
6 }
7 *newClassMethod {
9 }
10 }
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.
• synth definition - una definizione per creare nuovi synths, simile a "in-
strument" in altri sistemi.
141