Controlli e strategie

Nel corso dei secoli, la rappresentazione musicale si è progressivamente evoluta adottando di volta in volta soluzioni sempre più efficaci e dettagliate allo stesso tempo: dalle lettere greche ai neumi fino alla sistematica affermazione delle linee di riferimento (da una o due colorate) sulle quali porre i simboli grafici delle note, prima di forma rettangolare poi arrotondata. Con l’andar del tempo, la sofisticazione dei sistemi notazionali ha perfezionato la rappresentazione dei parametri primari e via via ha incluso progressivamente anche gli altri: dalle altezze alle durate, dal sistema delle chiavi alla divisione del tempo metronomico fino alla indicazione dell’agogica d’esecuzione.   E.Giordani

Parametri e controlli

Qualsiasi parametro di un algoritmo di sintesi o elaborazione del suono può essere controllato in due modi:

Svisceriamo ora le particolarità di queste due modalità modificando dinamicamente il parametro delle frequenze di un'onda sinusoidale, ma teniamo ben presente che le stesse tecniche e le relative sintassi possono essere impiegate per qualsiasi parametro di una qualsiasi tecnica di sintesi o elaborazione del suono.

Scheduling

Singolo valore

In SuperCollider la prima possibilità consiste nell'inviare i singoli valori del parametro desiderato dall'Interprete al Server con il metodo Synth.set(\arg, val) così come abbiamo fatto nell'intera prima parte di questo scritto:

s.boot;
s.scope(1);
s.meter(1,1);

// ------------------------------ SynthDef e Synth
(
SynthDef(\ksig,
              {arg freq=400;        // parametro da controllare
               var sig;
                   sig = SinOsc.ar(freq);
               Out.ar(0,sig)
               }
        ).add;

{~synth = Synth(\ksig)}.defer(0.1)  // ritarda di 0.1 secondi la valutazione della funzione
)

// ------------------------------ Controllo esterno dei parametri

~synth.set(\freq, rrand(200,2000).postln); // eseguire più volte
~synth.free;

Nel codice precedente la creazione del Synth e la sua assegnazione ad una variabile sono incluse all'interno di una funzione sulla quale è invocato il metodo {}.defer(0.1). Questa sintassi ritarda del tempo specificato come argomento di .defer() la valutazione del codice incluso nella funzione e si rende necessaria in situazioni come questa in cui SuperCollider impiega alcuni millisecondi per istanziare la SynthDef sul Server e dobbiamo attendere obbligatoriamente questo tempo prima di generare un'istanza di Synth derivata da quella SynthDef.

Nel caso precedente le frequenze cambiano repentinamente ad ogni valutazione del codice. Ipotizziamo ora di voler aggiungere un portamento (un veloce glissato) ad ogni cambio di frequenza e per farlo dobbiamo prima trasformare i valori provenienti dall'Interprete da numeri (int o float) in segnali di controllo o segnali audio per poter poi utilizzare una tecnica chiamata smoothing che "arrotonda" il segnale realizzando un qualche tipo di interpolazione. Questa tecnica deriva dall'audio analogico e consiste appunto nell'arrotondare le discontinuità di un segnale facendolo passare per un filtro a un polo (onepole). In SuperCollider esiste una UGens chiamata Lag.kr() che effettua questa operazione e per utilizzarla possiamo adottare due sintassi differenti:

(
{[LFPulse.kr(1),
  Lag.kr(LFPulse.kr(1),0.2), // Come UGen
  LFPulse.kr(1).lag(0.2)     // Come metodo
]
}.plot(1)
)

Personalmente preferisco la sintassi che utilizza i metodi perchè mi sembra più rappresentativa di una trasformazione del segnale in uscita dalla UGen ma a livello computazionale si equivalgono.

image not found

Notiamo dall'immagine come il secondo argomento di Lag.kr() serva per specificare il tempo di interpolazione in secondi (smoothing time) o meglio il tempo che il segnale impiega nel diminuire (o aumentare) di 60 dB seguendo una curva esponenziale. Il codice seguenti è identico a quello illustrato in precedenza con due importanti differenze:

  1. prima trasformiamo il valore inviato dall'interprete in un segnale di controllo con la UGen K2A.ar() che genera un segnale audio formato dai valori specificati come argomento.

  2. poi effettuiamo uno smoothing dinamico su questo segnale.
// ------------------------------ SynthDef e Synth
(
SynthDef(\ksig,
              {arg freq=400,smooth=0.02;
               var sig, port;
                   port = K2A.ar(freq).lag(smooth); // genera il portamento
                   sig  = SinOsc.ar(port);
               Out.ar(0,sig)
               }
          ).add;

{~synth = Synth(\ksig)}.defer(0.1)
)

// ------------------------------ Controllo esterno dei parametri

~synth.set(\freq,rrand(200,2000).postln,\smooth,rrand(0.02,10).postln);
~synth.free;

Le due operazioni appena esposte possono essere semplificate in un abbreviazione sintattica che automatizza la trasformazione del valore numerico in segnale.

// ------------------------------ SynthDef e Synth
(
SynthDef(\ksig,
              {arg freq=400,smooth=0.02;
               var sig, port;
                   port = freq.lag(smooth); // abbreviazione sintattica
                   sig  = SinOsc.ar(port);
               Out.ar(0,sig)
               }
          ).add;

{~synth = Synth(\ksig)}.defer(0.1)
)

// ------------------------------ Controllo esterno dei parametri

~synth.set(\freq,rrand(200,2000).postln,\smooth,rrand(0.02,10).postln);
~synth.free;

Nel caso delle frequenze la differenza tra cambio repentino del valore e portamento ha una valenza musicale, ma in altri (come ad esempio nel controllo dinamico dell'ampiezza di un segnale) lo smoothing assume esclusivamente una valenza tecnica (eliminare eventuali clicks generati dalle discontinuità del segnale di controllo) e in questi casi possiamo fissare un tempo di smoothing che normalmente corrispondente a 20 millisecondi (0.02 secondi).

Infine la UGen Lag.kr() e il metodo corrispondente hanno diverse varianti con le quali possiamo modificare la curva di smooth:

(
{[LFPulse.kr(1,0.999),
  LFPulse.kr(1,0.999).varlag(0.2),
  LFPulse.kr(1,0.999).lag(0.2),
  LFPulse.kr(1,0.999).lag2(0.2),
  LFPulse.kr(1,0.999).lag3(0.2),
  LFPulse.kr(1,0.999).lagud(0.2,0.4), // u = up, d = down
  LFPulse.kr(1,0.999).lag2ud(0.2,0.4),
  LFPulse.kr(1,0.999).lag3ud(0.2,0.4)]
}.plot(1)
)

image not found

Notiamo dall'immagine che con LagUD.kr() e i suoi simili possiamo specificare un tempo per quando il valore del segnale aumenta (up) e un'altro tempo per quando diminuisce (down). Le UGens "derivate" sono come 2 o 3 Lag.kr() in serie e il tempo di smoothing che andiamo a specificare non è sempre preciso proprio perchè la risposta non è lineare. Un'ultima particolarità: VarLag.kr() ritarda sempre di uno step.

Graphical User Interface (GUI)

In SuperCollider possiamo generare un'interfaccia grafica (GUI) contenente uno o più oggetti grafici come sliders, knobs, number boxes, etc. (generalmente ad un oggetto grafico corrisponde un singolo parametro/argomento) per poi utilizzarla in due diversi modi:

Visualizzazione

Prendiamo come esempio il codice precedente e, invece che stampare i valori randomici di frequenza e smoothing nella Post window con il metodo n.postln li visualizziamo in due NumberBox inclusi in una Window:

image not found

s.boot;
s.scope(1);
s.meter(1,1);

(
var w,a,b,c;                               // variabili locali

// ------------------------------ SynthDef e Synth

SynthDef(\ksig,
              {arg freq=400,smooth=0.02;
               var sig, port;
                   port = freq.lag(smooth); // abbreviazione sintattica
                   sig  = SinOsc.ar(port);
               Out.ar(0,sig)
               }
          ).add;

{~synth = Synth(\ksig)}.defer(0.1);

// ------------------------------ GUI

w = Window.new("GUI", 200@30); // Crea una finestra di 200x30 pixels
w.alwaysOnTop;                 // La posiziona sempre sopra tutte le altre finestre
w.front;                       // Visualizza la finestra
w.onClose_(                    // Esegue la funzione quando si chiude la finestra
           {~synth.free;w.free;a.free;b.free;c.free}); 
                        
a = StaticText.new(w,                      // Crea un testo statico nella finestra
                     Rect(5, 7, 200, 15)); // Le coordinate del testo 
a.string_("freq:               smooth:");  // Il contenuto del testo       

b = NumberBox.new(w,                       // Crea un number box nella finestra
                    Rect(35, 5, 50, 20));  // Le coordinate del number box
b.value_(400);                             // Il primo valore visualizzato 

c = NumberBox.new(w,                       // Crea un number box nella finestra
                    Rect(145, 5, 50, 20)); // Le coordinate del number box
c.value_(0.02);                            // Il primo valore visualizzato 
)

// ------------------------------ Controllo esterno dei parametri
(
var freq,smth;
    freq = rrand(200,2000);
    smth = rrand(0.02,10);

~synth.set(\freq,freq,\smooth,smth); // al Synth
b.value_(freq);                      // alla GUI
c.value_(smth)                       // alla GUI
)

Analizziamo la parte di codice che genera la GUI punto per punto.

  1. Creiamo una nuova finestra grafica con Window.new() e la assegnamo a una variabile (tipicamente ma non necessariamente la lettera w). Come argomenti specifichiamo il nome (come stringa) e la lunghezza dei lati in pixel attraverso l'abbreviazione sintattica x@y. La finestra sarà posizionata di default nell'angolo in basso a sinistra dello schermo e se volessimo crearla in una altro punto dovremmo sostituire x@y con un istanza della classe Rect.new() in quanto con essa possiamo scegliere le coordinate dell'angolo in alto a sinistra rispetto allo schermo.

    (
    r = Routine({
                 10.do({var w;
                         w = Window.new("Finestra",
                                         Rect.new(rand(800),rand(800),100,100) // angolo a sinistra random
                                        );
                         w.alwaysOnTop;     // sempre sopra a tutte le altre finestre presenti sullo schermo
                         w.front;           // fa apparire la finestra
                         0.5.wait;
                         w.close.free;      // chiude la finestra e libera le variabili
                         })
    }).reset.play(AppClock);                // essendo grafica animata dobbiamo utilizzare 'AppClock'
    ) 
    
  2. Invochiamo su di essa il metodo .alwaysOnTop che posiziona la finestra sempre davanti a tutte le altre presenti sullo schermo (essendo un utile per visualizzare dei valori o con la quale interagire con il mouse credo che questa sia la soluzione migliore).

  3. Invochiamo su di essa il metodo .front che la fa comparire sullo schermo (altrimenti viene istanziata e assegnata ma non visualizzata).

  4. Invochiamo su di essa il metodo .onClose_({azione}) che valuta la funzione specificata come argomento nel momento in cui chiudiamo la finestra, sia attraverso un'interazione del mouse (click sul bottone rosso) sia dal codice invocando sulla finestra il metodo .close. Questo metodo è utile ad esempio per fermare la computazione audio alla chiusura di una finestra oppure come in qesto caso a liberare variabili legate all'esistenza della finestra in modo automatico.

  5. Inseriamo nella finestra gli oggetti grafici che visualizzeranno i dati (in questo caso uno StaticText e due NumberBox) specificandone i due argomenti principali (e comuni a quasi tutti gli oggetti GUI):

    • l'istanza di Window sulla quale vogliamo posizionarlo (ci possono essere più finestre aperte allo stesso momento).

    • le coordinate in pixel specificate con Rect.new(). In questo caso i primi due argomenti non corrispondono all'angolo in basso a sinistra ma a quello in alto a sinistra. Rect.new(0,0,10,10) ad esempio significa un quadrato di 10 pixel per lato il cui angolo in alto a sinistra corrisponde allo stesso angolo della Window sul quale è posizionato.

  6. Invochiamo il metodo .value_(n) sulle istanze degli oggetti per visualizzare i valori di n (in fase di creazione setta il primo valore).

Notiamo infine che i valori sono inviati separatamente al Synth e alla GUI rendendo di fatto indipendente il suono dal monitoraggio dei suoi parametri. Questa è una strategia da perseguire sempre con qualsiasi software musicale in quanto la computazione grafica è generalmente più "pesante" di quella audio e in situazioni particolarmente complesse potrebbe essere utile in fase di prova ma da eliminare se non strettamente necessaria prima della performance.

Per semplificare il controllo e inviare una sola volta i valori possiamo programmare una funzione all'interno della quale sono eseguite automaticamente azioni di basso livello come splittare i valori inviarli agli indirizzi corretti:

(
var w,a,b,c;                               // variabili locali

// ------------------------------ SynthDef e Synth

SynthDef(\ksig,
              {arg freq=400,smooth=0.02;
               var sig, port;
                   port = freq.lag(smooth); // abbreviazione sintattica
                   sig  = SinOsc.ar(port);
               Out.ar(0,sig)
               }
          ).add;

{~synth = Synth(\ksig)}.defer(0.1);

// ------------------------------ GUI

w = Window.new("GUI", 200@30); // Crea una finestra di 200x30 pixels
w.alwaysOnTop;                 // La posiziona sempre sopra tutte le altre finestre
w.front;                       // Visualizza la finestra
w.onClose_(                    // Esegue la funzione quando si chiude la finestra
           {~synth.free;w.free;a.free;b.free;c.free}); 
                        
a = StaticText.new(w,                      // Crea un testo statico nella finestra
                     Rect(5, 7, 200, 15)); // Le coordinate del testo 
a.string_("freq:               smooth:");  // Il contenuto del testo       

b = NumberBox.new(w,                       // Crea un number box nella finestra
                    Rect(35, 5, 50, 20));  // Le coordinate del number box
b.value_(400);                             // Il primo valore visualizzato 

c = NumberBox.new(w,                       // Crea un number box nella finestra
                    Rect(145, 5, 50, 20)); // Le coordinate del number box
c.value_(0.02);                            // Il primo valore visualizzato 

// ------------------------------ Operazioni di basso livello

~ksynth = {arg freq, smth;

           ~synth.set(\freq,freq,\smooth,smth); // al Synth
           b.value_(freq);                      // alla GUI
           c.value_(smth)                       // alla GUI
           }
)

// ------------------------------ Controllo esterno dei parametri

~ksynth.value(freq:rrand(200,2000),smth:rrand(0.02,2)); // alla funzione (.value() al posto di .set())

Interazione

Possiamo anche modificare dinamicamente i parametri del Synth interagendo con l'interfaccia in tre modi differenti:

Per poter compiere queste azioni dobbiamo però dobbiamo aggiungere al codice precedente alcuni comandi che ci permettono di recuperare i valori modificati sull'interfaccia per poterli utilizzare nell'Interprete ed eventualmente inviarli al Server. Il metodo .action({}) serve a questo: esegue la funzione al suo interno ogni volta che interagiamo con l'oggetto sul quale è invocato e, se specifichiamo un argomento riporta il valore nel momento dell'esecuzione.

(
var w,a,b,c;                               // variabili locali

// ------------------------------ SynthDef e Synth

SynthDef(\ksig,
              {arg freq=400,smooth=0.02;
               var sig, port;
                   port = freq.lag(smooth); // abbreviazione sintattica
                   sig  = SinOsc.ar(port);
               Out.ar(0,sig)
               }
          ).add;

{~synth = Synth(\ksig)}.defer(0.1);

// ------------------------------ GUI

w = Window.new("GUI", 200@30); // Crea una finestra di 200x30 pixels
w.alwaysOnTop;                 // La posiziona sempre sopra tutte le altre finestre
w.front;                       // Visualizza la finestra
w.onClose_(                    // Esegue la funzione quando si chiude la finestra
           {~synth.free;w.free;a.free;b.free;c.free}); 
                        
a = StaticText.new(w,                      // Crea un testo statico nella finestra
                     Rect(5, 7, 200, 15)); // Le coordinate del testo 
a.string_("freq:               smooth:");  // Il contenuto del testo       

b = NumberBox.new(w,                       // Crea un number box nella finestra
                    Rect(35, 5, 50, 20));  // Le coordinate del number box
b.valueAction_(400);                       // Il primo valore visualizzato 

c = NumberBox.new(w,                       // Crea un number box nella finestra
                    Rect(145, 5, 50, 20)); // Le coordinate del number box
c.valueAction_(0.02);                      // Il primo valore visualizzato 

// ------------------------------ Operazioni di basso livello

~ksynth = {arg freq, smth;

           ~synth.set(\freq,freq,\smooth,smth); // al Synth
           b.value_(freq);                      // alla GUI
           c.value_(smth)                       // alla GUI
	       };

b.action_({arg val;                             // da GUI al Synth
           ~synth.set(\freq,val.value)  
          });

c.action_({arg val;                             // da GUI al Synth
          ~synth.set(\smooth,val.value)  
          });
)

// ------------------------------ Controllo esterno dei parametri

~ksynth.value(freq:rrand(200,2000),smth:rrand(0.02,2)); // alla funzione

Nel codice precedente abbiamo anche sostituito il metodo .value_() utilizzato per settare il valore di default con .valueAction_() in quanto il primo invia solo il valore dall'Interprete all'interfaccia, mentre il secondo esegue anche l'azione di .action ovvero riporta il valore all'interprete.

Con il codice precedente possiamo modificare dinamicamente i parametri sia eseguendo righe di codice dall'Interprete, sia interagendo in vari modi con la GUI.

Interpolazioni e rampe

Vai da a a b in tot

Oltre che variare dinamicamente dall'Interprete il singolo valore, nei controlli di alcuni parametri musicali avremo bisogno di generare delle interpolazioni di diverso tipo per risolvere esigenze tecniche o musicali che rispondano al seguente concetto:

parti da 'a' e vai a 'b' in 'n' passi piccoli o grandi

In musica nella descrizione dei parametri di alto livello il numero di passi è generalmete sostituito dal tempo, anche se come vedremo è un parametro di cui tener conto nelle operazioni matemetiche da effettuare.

parti da 'a' e vai a 'b' in tot secondi con passi piccoli o grandi

image not found

Per quanto riguarda le frequenze ad esempio si tratta di un concetto che esprime un glissato o un qualche tipo di scala che suddivide un intervallo, per le ampiezze un crescendo o diminuendo mentre per il panning i movimenti della sorgente, etc. Possiamo realizzare questo concetto sia con segnali generati da UGens dedicate all'interno del Server come abbiamo fatto per esempio per il portamento (.lag() e suoi simili) sia nell'Interprete temporizzando e automatizzando alcune operazioni matematiche. Per ora occupiamoci di quest'ultimo caso passo dopo passo.

  1. definiamo i parametri di alto livello dichiarandoli come argomeni di una funzione:

    parti da 200 e vai a 800 in 3 secondi seguendo una curva lineare con passi piccoli (indice di definizione uguale a 0.01)

    (
    ~rampa = {arg a     = 200,  // inizio
                  b     = 800,  // fine
                  dur   = 3,    // durata 
                  defId = 0.01; // indice di definizione tra 0.1 (10 passi), 0.01 (100 passi) e 0.001 (1000 passi)
              }
    )
    
  2. calcoliamo l'intervallo assoluto tra a e b:

    (
    ~rampa = {arg a=200,b=800,dur=3,defId=0.01;
              var range;
                  range  = abs(a-b);
              }
    )
    
    ~rampa.value; // risultato...
    
  3. calcoliamo il numero di passi utilizzando come valori limite 0 e 1 perchè quando dovremo generare curve non linerari questo è l'unico intervallo che mantiene il valore di arrivo invariato:

    (
    ~rampa = {arg a=200,b=800,dur=3,defId=0.01;
              var range,nPassi;
                  range  = abs(a-b);
                  nPassi = 1/defId;
               // nPassi = defId.reciprocal; // alternativa...
              }
    )
    
    ~rampa.value; // risultato...
    

    Volendo possiamo definire direttamente in modo assoluto il numero di passi invece che un indice di definizione relativo, ma questa rimane una scelta personale.

  4. creiamo un Array i cui items corrispondono ai valori di tutti i passi compresi tra il valore di partenza e quello di arrivo sotto forma di onsets:

    • invochiamo sull'Array il metodo [].interpolation() che realizza in n passi un'interpolazione lineare tra due valori (in questo caso tra 0 e 1 per la stessa ragione illustrata nel punto precedente).
    • riscaliamo i valori nel range compreso tra a e
  5. (
    ~rampa = {arg a=200,b=800,dur=3,defId=0.01;
              var range,nPassi,aPasso;
                  range  = abs(a-b);
                  nPassi = 1/defId;
                  aPasso = Array.interpolation(nPassi,0,1) // array tra 0 e 1
                                .normalize(a,b);           // riscala
              }
    )
    
    ~rampa.value; // risultato...
    
  6. calcoliamo il tempo delta tra i passi in secondi dividendo la durata del passaggio per il numero di passi:

    (
    ~rampa = {arg a=200,b=800,dur=3,defId=0.01;
              var range,nPassi,aPasso,dPasso;
                  range  = abs(a-b);
                  nPassi = 1/defId;
                  aPasso = Array.interpolation(nPassi,0,1) 
                                .normalize(a,b);           
                  dPasso = dur/nPassi;
              }
    )
    
    ~rampa.value; // risultato...
    
  7. richiamiamo i singoli valori dell'Array nel tempo con il metodo n.do() inserito in una Routine:

    (
    ~rampa = {arg a=200,b=800,dur=3,defId=0.01;
              var range,nPassi,aPasso,dPasso;
                  range  = abs(a-b);
                  nPassi = 1/defId;
                  aPasso = Array.interpolation(nPassi,0,1) 
                                .normalize(a,b);           
                  dPasso = dur/nPassi;
    
    Routine.new({
                 aPasso.do({arg item;
                            var val;
                            val = item;
                            val.postln;  // lo stampa
                            dPasso.wait  // tempo delta tra i passi
                            })
                 }).play
    }
    )
    
    ~rampa.value(200,800,3,0.01);   // incremento
    ~rampa.value(450,100,1,0.01);   // decremento      
    (      
    ~rampa.value(rand(1000).postln, // random...
    	         rand(1000).postln,
    	         rrand(0.1,3),0.01); 
    )
    

Vai a b in tot

Una variante del pensiero (e del codice) precedente è rappresentata dalla seguente situazione:

dal valore che hai vai a 'b' in tot secondi con passi piccoli o grandi

Rispetto al codice precedente basterà eliminare l'argomento a (il valore iniziale) e dichiararlo come variabile al di fuori della funzione, assegnandogli un valore iniziale per permettere di calcolare il range della prima valutazione ed aggiornare il suo contenuto con l'ultimo item letto dal loop nella Routine:

(
var a=0;     // dichiarato al di fuori dela funzione

~rampa = {arg b=100,dur=1,defId=0.01;
          var range,nPassi,aPasso,dPasso;
              range  = abs(a-b);
              nPassi = 1/defId;
              aPasso = Array.interpolation(nPassi,0,1) 
                            .normalize(a,b);           
              dPasso = dur/nPassi;

Routine.new({
             aPasso.do({arg item;
                        a = item;    // aggiorna il valore iniziale
                        a.postln;  
                        dPasso.wait  
                        })
             }).play
}
)

~rampa.value(800,3,0.01);                          // incrementa
~rampa.value(100,1,0.1);                           // decrementa
~rampa.value(rand(1000).postln,rrand(0.1,3),0.1);  // random

Curve non lineari

Negli esempi precedenti le interpolazioni erano di tipo lineari, ovvero il delta tra i passi rimaneva costante per tutta la rampa. Con una piccola aggiunta nel codice illustrato possiamo generare anche interpolzaioni con curve diverse:

image not found

(
var n,lin,scale;
    n    = 100;
    lin  = Array.interpolation(n,0,1);
    scale = [lin,
             lin.squared,
             lin.cubed,
             lin.sqrt,
             lin.pow(2),
             lin.pow(4),
             lin.pow(6),
             lin.pow(0.3)
            ].plot("interpola",600@600,true)
             .plotMode_(\plines)
)

Dobbiamo dunque:

(
var a=0;     // dichiarato al di fuori dela funzione

~rampa = {arg b=100,dur=1,defId=0.01;
          var range,nPassi,aPasso,dPasso;
              range  = abs(a-b);
              nPassi = 1/defId;
              aPasso = Array.interpolation(nPassi,0,1) // Array tra 0 e 1
                            .squared                   // curva
                            .normalize(a,b);           // riscala      
              dPasso = dur/nPassi;

Routine.new({
             aPasso.do({arg item;
                        a = item;    // aggiorna il valore iniziale
                        a.postln;  
                        dPasso.wait  
                        })
             }).play
}
)

~rampa.value(800,3,0.01);                          // incrementa
~rampa.value(100,1,0.1);                           // decrementa
~rampa.value(rand(1000).postln,rrand(0.1,3),0.1);  // random

Integriamo ora le rampe nel codice che abbiamo utilizzato per l'invio del singolo valore, se non vogliamo alcuna rampa basta fissare il tempo a 0.

(
var inizio=0;                  // operazioni di init

// ------------------------------ SynthDef e Synth

SynthDef(\ksig,
              {arg freq=400,smooth=0.02;
               var sig, port;
                   port = freq.lag(smooth); // abbreviazione sintattica
                   sig  = SinOsc.ar(port);
               Out.ar(0,sig)
               }
          ).add;

{~synth = Synth(\ksig)}.defer(0.1);

// ------------------------------ GUI

w = Window.new("GUI", 200@30); 
w.alwaysOnTop;                 
w.front;                       
w.onClose_({~synth.free;w.free;a.free;b.free;c.free}); 
                        
a = StaticText.new(w, Rect(5, 7, 200, 15)).string_("freq:               smooth:");      
b = NumberBox.new(w, Rect(35, 5, 50, 20)).valueAction_(400);                       
c = NumberBox.new(w, Rect(145, 5, 50, 20)).valueAction_(0.02);                      

// ------------------------------ Operazioni di basso livello

~ksynth = {arg fine=100,dur=1,defId=0.01,smth=0.02;          // aggiunto smth
           var range,nPassi,aPasso,dPasso;
               range  = abs(inizio-fine);
               nPassi = 1/defId;
               aPasso = Array.interpolation(nPassi,0,1).pow(4).normalize(inizio,fine);       
               dPasso = dur/nPassi;

Routine.new({
             aPasso.do({arg item;
                        ~synth.set(\freq,item,\smooth,smth); // al Synth
                        {b.value_(item);                     // alla GUI
                         c.value_(smth)}.defer(0);           // {}.defer(0) o AppClock
                         inizio = item;                      // aggiorna l'inizio
                        dPasso.wait  
                        })
             }).play;
           };

b.action_({arg val; ~synth.set(\freq,val.value)});
c.action_({arg val; ~synth.set(\smooth,val.value)});
)

// ------------------------------ Controllo esterno dei parametri

~ksynth.value(800,3,0.01);                          // incrementa
~ksynth.value(100,1,0.1);                           // decrementa
~ksynth.value(rrand(300,1000).postln,rrand(0.1,3),[0.1,0.01,0.001].choose);  // random
~ksynth.value(800,0,0.01);                          // senza rampa

Sequencing

Possiamo automatizzare il controllo dei parametri di un segnale dall'Interprete automatizzando l'invio dei valori con Routine, Task o Pattern atttraverso le sintassi già conosciute:

(
var inizio=0;                  // operazioni di init
// ------------------------------ SynthDef e Synth

SynthDef(\ksig,
              {arg freq=400,smooth=0.02;
               var sig, port;
                   port = freq.lag(smooth); // abbreviazione sintattica
                   sig  = SinOsc.ar(port);
               Out.ar(0,sig)
               }
          ).add;

{~synth = Synth(\ksig)}.defer(0.1);

// ------------------------------ GUI

w = Window.new("GUI", 200@30); 
w.alwaysOnTop;                 
w.front;                       
w.onClose_({~synth.free;w.free;a.free;b.free;c.free}); 
                        
a = StaticText.new(w, Rect(5, 7, 200, 15)).string_("freq:               smooth:");      
b = NumberBox.new(w, Rect(35, 5, 50, 20)).valueAction_(400);                       
c = NumberBox.new(w, Rect(145, 5, 50, 20)).valueAction_(0.02);                      

// ------------------------------ Operazioni di basso livello

~ksynth = {arg fine=100,dur=1,defId=0.01,smth=0.02;          // aggiunto smth
           var range,nPassi,aPasso,dPasso;
               range  = abs(inizio-fine);
               nPassi = 1/defId;
               aPasso = Array.interpolation(nPassi,0,1).pow(4).normalize(inizio,fine);       
               dPasso = dur/nPassi;

Routine.new({
             aPasso.do({arg item;
                        ~synth.set(\freq,item,\smooth,smth); // al Synth
                        {b.value_(item);                     // alla GUI
                         c.value_(smth)}.defer(0);           // {}.defer(0) o AppClock
                         inizio = item;                      // aggiorna l'inizio
                        dPasso.wait  
                        })
             }).play;
           };

b.action_({arg val; ~synth.set(\freq,val.value)});
c.action_({arg val; ~synth.set(\smooth,val.value)});
)

// ------------------------------ Controllo esterno dei parametri - Sequencing
(
r = Routine.new({
                 inf.do({var freq,dur,passi,smth,delta;
                             freq = rrand(300,1000);
                             dur  = rrand(0.01,1.0);
                             passi= [0.1,0.01,0.001].choose;
                             smth = 0.02;
                             delta= dur + rrand(0.01,0.5);

                             ~ksynth.value(freq,dur,passi,smth);
                             delta.wait
                         })
                  }).reset.play
)

r.stop;

Devices

Se vogliamo invece controllare i parametri di un Synth interagendo fisicamente con devices esterni come prima cosa dobbiamo cercare nelle specifiche tecniche dello strumento o del controller se ha o meno bisogno di essere connesso al computer con un cavo o se utilizza una qualche connessione Wi-fi. In secondo luogo dobbiamo verificare quale protocollo di comunicazione utilizza per parlare con il mondo esterno ovvero che "lingua" utilizza. Ai nostri giorni nel mondo delle comunicazioni fra strumenti audio sono presenti due principali protocolli:

In SuperCollider i valori in ingresso da entrambi i protocolli possono essere letti (ed eventualmente inviati ad altri software o devices) nell'Interprete per poi essere inviati al Server con le stesse tecniche illustrate nel paragrafo precedente, mentre per quanto riguarda il solo protocollo OSC se il devices permette una formattazione dei dati corretta potremmo inviare i valori direttamente al Server.

MIDI

Per quanto riguarda il protocollo MIDI, dopo aver connesso fisicamente i devices al computer dobbiamo:

  1. connettere virtualmente a SuperCollider tutti i devices connessi fisicamente al computer:

    MIDIIn.connectAll; // connetto tutti i devices MIDI
    

    Eseguendo il codice precedente comparirà una lista contenente tutti i devices dai quali possiamo ricevere dati (MIDI Sources) e una di quelli ai quali possiamo inviare dati (MIDI Destinations):

    MIDI Sources:
    	MIDIEndPoint("Driver IAC", "Bus 1")
    	MIDIEndPoint("nanoKONTROL2", "SLIDER/KNOB")
    	MIDIEndPoint("ARIUS", "ARIUS")
    MIDI Destinations:
    	MIDIEndPoint("Driver IAC", "Bus 1")
    	MIDIEndPoint("nanoKONTROL2", "CTRL")
    	MIDIEndPoint("ARIUS", "ARIUS")
    -> MIDIIn
    
  2. recuperare alcune informazioni utili riguardanti i valori in ingresso monitorando tutti i dati che passano in tutte le porte midi eseguendo la seguente sintassi:

    MIDIFunc.trace(true);  // legge tutti i messaggi MIDI in ingresso e riporta i dati nella Post window
    MIDIFunc.trace(false); // Termina il monitoraggio 
    

    In questo modo possiamo monitorare ad esempio la corrispondenza tra controller fisico e ccn piuttosto che la porta MIDI sulla quale trasmette.

  3. scegliere il tipo di data MIDI che vogliamo recuperare filtrando tutti gli altri utilizzando la Classe MIDIdef:

    (
    MIDIdef.noteOn( \noteOn, {arg ...args; args.postln}); // velocity, note,      canale, uid
    MIDIdef.noteOff(\noteOff,{arg ...args; args.postln}); // velocity, note,      canale, uid
    MIDIdef.cc(     \control,{arg ...args; args.postln}); // valore,   numero cc, canale, uid
    )
    

    Nel codice precedente abbiamo recuperato i parametri MIDI più comuni come .noteOn(), noteOff() e .cc() ma attraverso la Classe MIDIdef possiamo eventualmente recuperare anche i valori di: .polytouch, .touch, .bend, .program, .sysex, .smpte, .sysrt. Per i dettagli è meglio consultare l'help file. Gli argomenti principali di tutti questi metodi sono:

    • un nome sotto forma di simbolo.
    • una funzione che viene valutata ogni volta che arriva un dato MIDI del tipo specifico.

    Ad esempio se eseguiamo il codice precedente e schiacciamo un tasto di una tastiera musicale connessa o muoviamo uno slider di un controller connesso vedremo comparire un Array di valori nella Post window. Per ogni metodo questi rappresentano un parametro differente anche se i principali sono mantenuti sempre nelle stesse posizioni dell'Array come commentato direttamente nel codice precedente. Possiamo ora isolare i singoli parametri dell'Array nel modo usuale ([].at(id)) e assegnarli ad una variabile per utilizzarli nel codice.

Il codice successivo sintetizza i diversi controlli di un Synth:

(
// ------------------------------ SynthDef e Synth (monofonico)

SynthDef(\midi,
              {arg freq=440, amp=0, smooth=0.02, gain=0;
               var sig;
                   sig = SinOsc.ar(freq)*amp.lag(smooth);    
               Out.ar(0,sig*gain)
               }
          ).add;

{~synth = Synth(\midi)}.defer(0.1);

// ------------------------------ GUI (solo visualizzazione)

w = Window.new("GUI", 200@30); 
w.alwaysOnTop;                 
w.front;                       
w.onClose_({~synth.free;w.free;a.free;b.free;c.free}); 
                        
a = StaticText.new(w, Rect(5, 7, 200, 15)).string_("freq:                  vel:");      
b = NumberBox.new(w, Rect(35, 5, 50, 20)).valueAction_(400);                       
c = NumberBox.new(w, Rect(145, 5, 50, 20)).valueAction_(0.02);   

// ------------------------------ Connette i devices esterni
	
MIDIIn.connectAll;                                // connetto tutti i devices MIDI
MIDIdef.cc(\masterOut, {arg val;                  // solo primo argomento (valore del cc)
                        ~synth.set(\gain,val/127) // master out tra 0 e 1
           });
MIDIdef.noteOn(\noteOn,{arg vel,note;
                        ~synth.set(\freq,note.midicps, // frequenze in Hz
                                   \amp,vel/127);      // ampiezza tra 0 e 1
                        {b.value_(note);c.value_(vel)}.defer(0) // alla GUI
               });
MIDIdef.noteOff(\noteOff,{~synth.set(\amp,0)});        // note off
)

~synth.set(\smooth,5); // cambia il tempo di fade in e fade out

Più avanti, all'interno di esempi specifici vedremo come discriminare diversi devices che trasmettono lo stesso tipo di data e come realizzare Synth polifonici.

OSC

Al giorno d'oggi esistono in commercio numerose interfacce touch screen attraverso le quali possiamo controllare dinamicamente con le dita diversi parametri di uno o più Synth tramite messaggi OSC. Entreremo nel dettaglio di questi argomenti nel Capitolo su Segnali e controlli (METTI LINK) mentre in questo paragrafo per semplicità utilizzeremo messaggi OSC inviati da un patch di Max in quanto le procedure e la sintassi sono le stesse.

  1. Scarichiamo e lanciamo il patch di Max

  2. Così come abbiamo fatto per i controller MIDI monitoriamo su quale porta OSC sono trasmessi i dati in ingresso e annotiamo il numero di porta sulla quale avviene la trasmissione:

    OSCFunc.trace(true);  // legge tutti i messaggi OSC in ingresso e riporta i dati nella Post window
                          // muovere il cursore sul patch di Max
    OSCFunc.trace(false); // Termina il monitoraggio 
    

    La porta è quella che corrisponde a: recvPort: 57121

  3. Apriamo il patch di Max e in udpsend specifichiamo come IP: 127.0.0.1 (che significa sullo stesso computer) e come porta il valore che abbiamo monitorato nel punto precedente (in questo caso 57121).

  4. Utilizziamo la Classe OSCdef.new() specificando come argomenti:

    • un nome sotto forma di simbolo.
    • una funzione che sarà valutata ogni volta che arriva un messaggio OSC indirizzato all'istanza di OSCdef.new().
    • un indirizzo (path) sotto forma di simbolo che comincia con uno slash: '/nota'
    • un instanza di NetAddr.new("127.0.0.1",53856) uguale a quella che compare quando inviavamo un messaggio OSC dal patch di Max (address: a NetAddr(127.0.0.1, 53856)). Facciamo attenzione perchè cambia quando inviamo i messaggi.
    OSCdef.new(\notam, {arg msg; msg.postln}, '/nota', NetAddr.new("127.0.0.1",53856))

    Il contenuto del messaggio è un'Array che ha come primo item l'indirizzo al quale è inviato sotto forma di simbolo, seguito dalla lista di valori inviata, nel nostro caso:

    [ /nota, 57, 69 ]

    Possiamo ora isolare i singoli parametri nel modo usuale ([].at(id)) e assegnarli ad una variabile per utilizzarli nel codice.

Il codice successivo sintetizza i diversi controlli di un Synth:

(
// ------------------------------ SynthDef e Synth (monofonico)

SynthDef(\midi,
              {arg freq=440, amp=0, smooth=0.02, gain=0;
               var sig;
                   sig = SinOsc.ar(freq)*amp.lag(smooth);    
               Out.ar(0,sig*gain)
               }
          ).add;

{~synth = Synth(\midi)}.defer(0.1);

// ------------------------------ GUI (solo visualizzazione)

w = Window.new("GUI", 200@30); 
w.alwaysOnTop;                 
w.front;                       
w.onClose_({~synth.free;w.free;a.free;b.free;c.free}); 
                        
a = StaticText.new(w, Rect(5, 7, 200, 15)).string_("freq:                  vel:");      
b = NumberBox.new(w, Rect(35, 5, 50, 20)).valueAction_(400);                       
c = NumberBox.new(w, Rect(145, 5, 50, 20)).valueAction_(0.02);   

// ------------------------------ Connette i devices esterni
	
OSCdef.new(\master, {arg msg;
                     ~synth.set(\gain,msg[1]/127) },     // master out tra 0 e 1
                     '/gain', NetAddr.new("127.0.0.1",53856));

OSCdef.new(\notam, {arg msg;
                    ~synth.set(\freq,msg[1].midicps,               // frequenze in Hz
                               \amp,msg[2]/127);                   // ampiezza tra 0 e 1
                    {b.value_(msg[1]);c.value_(msg[2])}.defer(0)}, // alla GUI
	                '/nota', NetAddr.new("127.0.0.1",53856));
)

~synth.set(\smooth,5)

A differenza dell'esempio riguardante il protocollo midi non c'è un messaggio di noteoff che in questo caso deve essere programmato in direttamente in Max o nel device utilizzato.

Segnali

Nelle tecniche precedenti abbiamo trasformato i singoli valori (oneshot) provenienti dll'Interprete in segnali audio o di controllo (che vivono nel Server) e li abbiamo eventualmente modificati attraverso una qualche tecnica di smoothing. Risulta evidente allora che possiamo utilizzare direttamente un qualsiasi tipo di segnale per modificare qualsiasi parametro di un'altro segnale, a patto che il primo produca valori compresi in un range corretto per quel parametro specifico (0.0 e 1.0 per l'ampiezza, 20 e 20.000 per la frequenza, etc.). Vediamo come sfruttare le diverse caratteristiche dei segnali di controllo.

Audio vs controllo

Nello sviscerare le diverse caratteristiche dei segnali la prima distinzione da fare è quella tra segnali audio contraddistinti in SuperCollider dal metodo .ar (audio rate) e segnali di controllo che sono invece indicati dal metodo .kr. Come abbiamo già detto, entrambi sono generati da UGens ma, i primi generano una sequenza di numeri pari alla sample rate (ad esempio se la sample rate è di 44.100 Hz dall'output della UGen escono 44.100 numeri al secondo) e sono utilizzati principalmente per i segnali che vengono scritti sull'output di SuperCollider, ovvero quelli trasdotti in suono dagli altoparlanti, mentre i secondi generano un numero di samples per secondo pari a sample_rate/block_size che, nel caso di default corrisponde a 44100/64 = 689.0625 samples. Da ciò risulta evidente come la definizione dei segnali a control rate sia minore di quella dei segnali ad audio rate e che possiamo utilizzarli in tutti i casi in cui un segnale controlla un'altro segnale senza uscire direttamente dagli altoparlanti. La maggior parte delle UGens può generare entrambi i tipi di segnale rispondendo semplicemente a uno dei due metodi dedicati (.ar o .kr). Nell'immagine sottostante lo stesso segnale ad audio e a control rate:

image not found

Le conversioni da una tipologia all'altra saranno affrontate in un paragrafo dedicato (METTI LINK)

Ambiti

A priori tutti i segnali possono essere compresi in qualsiasi ambito numerico (range) compreso tra +/- ∞ in quanto le UGen generano semplicemente numeri non suoni, ma se questo può valere per i segnali di controllo (anche quelli ad audio rate) non vale per i segnali audio in uscita da SuperCollider che devono essere compresi tra +/- 1. Questo è il motivo per il quale molte UGens hanno questo ambito di default. Un segnale che ha un ambito di questo tipo viene definito bipolare in quanto genera numeri sia positivi che negativi (non necessariamente compresi tra +/- 1):

image not found

Tutti i segnali compresi in un ambito con un solo segno (sia esso positivo o negativo) invece sono chiamati unipolari. Generalmente i segnali unipolari sono di controllo, in quanto se un segnale audio in uscita è unipolare significa che ha un DC offset:

image not found

Le diverse tecniche per modificare l'ambito dei segnali saranno affrontate sia nei paragrafi dedicati all'applicazione dei segnali di controllo su parametri specifici che in un paragrafo dedicato (METTI LINK). Il codice seguente è un semplice esempio che riporta un range di un segnale compreso tra +/-1 (bipolare) in un altro compreso tra 500 e 1200 (unipolare) in grado da poter essere utilizzato come parametro frequenziale di un oscillatore sinusoidale e che ci servirà anche negli esempi successivi:

(
SynthDef(\gliss,
               {arg freq=1;
                var ksig,sig;
                    ksig = SinOsc.kr(freq).range(500,1200); // da +/- 1 a 500/1200
                    sig  = SinOsc.ar(ksig);
		Out.ar(0,sig)}
	).add;

{a = Synth(\gliss)}.defer(0.1)
)

a.set(\freq,rrand(1,200)); // frequenza del segnale di controllo (glissato)

Tipologie

Attraverso la combinazione di segnali in algoritmi di sintesi ed elaborazione del suono, possiamo generare un'infinità di timbri con caratteristiche morfologiche estremamete differenti. I segnali che li descrivono però (siano essi audio o di controllo) possono assumerne solo poche e le principali sono quattro:

image not found

(
{[
Impulse.kr(100),
LFNoise0.kr(10),
BrownNoise.kr(1).lag,
WhiteNoise.kr(1)
]}.plot(0.5,bounds:600@400)
)

Richiamando l'Help file delle diverse UGens presenti nel codice precedente possiamo cominciare autonomamente una piccola esplorazione delle specifiche carattteristiche.

Ogni segnale compreso in ognuna di queste categorie a sua volta può inoltre essere:

image not found

(
{[
Impulse.kr(100),Dust.kr(100),
LFSaw.kr(10),   LFNoise0.kr(10),
SinOsc.kr(10),  WhiteNoise.kr(1)
]}.plot(1,bounds:1000@600)
)

Di seguito un esempio di alcuni segnali periodici classici:

image not found

(
{var sine = SinOsc.ar(50),
     saw  = Saw.ar(50),
	 quad = Pulse.ar(50),
 	 addi = Blip.ar(50,5);
[sine,saw,quad,addi]}.plot(0.1,bounds:1000@600,minval:-1,maxval:1)
)

E di alcuni segnali a-periodici (tipi di noise):

image not found

Un esempio classico è quello della generazione di frequenze randomiche comprese in un ambito:

(
SynthDef(\rand,
               {arg freq=1;
                var ksig,sig;
                    ksig = LFNoise0.kr(freq).range(500,1200); // da +/- 1 a 500/1200
                    sig  = SinOsc.ar(ksig);
		Out.ar(0,sig)}
	).add;

{a = Synth(\rand)}.defer(0.1)
)

a.set(\freq,rrand(1,20)); // frequenza del segnale di controllo 

Anche in questo caso entreremo nel merito delle singole morfologie sia nei paragrafi dedicati all'applicazione dei segnali di controllo su parametri specifici che in un paragrafo dedicato (METTI LINK)

Mouse

Possiamo generare un segnale di controllo di un qualsiasi parametro anche attraverso il mouse del computer sfruttando le potenzialità di tre diverse UGens dedicate:

Nel codice seguente un semplice esempio riassuntivo:

(
SynthDef(\mouse,
                {var freq,amp,trig,sig;
                     freq = MouseX.kr(200,1000,0,0.2); // frequenza tra 200 e 1000Hz lineare
                     amp  = MouseY.kr(0.01,1.1,1,0.2); // ampiezza tra 0 e 1 esponenziale
                     trig = MouseButton.kr(0,1,0.2);   // noteon/off
                     sig  = SinOsc.ar(freq,0,amp);
		Out.ar(0,sig*trig)
}).add;

{a = Synth(\mouse)}.defer(0.1)
)

Tastiera del computer

Anche i tasti sulla tastiera del computer possono essere utilizzati come elementi di controllo. Per prima cosa dobbiamo monitorare i valori ASCII dei singoli tasti e per farlo in SuperCollider dobbiamo obbligatoriamente generare una finestra grafica:

(
w = Window.new("key");
w.view.keyDownAction_({arg ...args;
                       args.postln
                       });
w.front;
)

Con il metodo w.view.keyDownAction_({}) invocato su una Window possiamo infatti recuperare nel consueto modo (argomenti di una funzione) alcune informazioni ogni volta che viene premuto un tasto. A priori possiamo utilizzare a piacimento uno qualsiasi di questi valori, ma per lo scopo che ci proponiamo in questo paragrafo ci interessa solamente il valore ASCII, che è l'ultimo item dell'Array:

(
w = Window.new("key");
w.view.keyDownAction_({arg view,char,modifiers,unicode,keycode,key;
                      [char,keycode].postln
                      });
w.front;
)

Ora che possiamo conoscere tutti i valori ASCII dei tasti, possiamo specificarne uno come primo argomento di KeyState.kr():

{KeyState.kr(35,0,1,0.2).poll(10)}.play; // se si schiaccia 'p'

Così facendo ogni volta che premiamo il tasto corrispondente si comporterà esattamente come MouseButton.kr():

(
SynthDef(\tasti,
                {var freq,amp,trig,sig;
                     freq = MouseX.kr(200,1000,0,0.2);
                     amp  = MouseY.kr(0.01,1.1,1,0.2);
                     trig = KeyState.kr(35,0,1,0.2);   // noteon/off se schiacci ìp'
                     sig  = SinOsc.ar(freq,0,amp);
		Out.ar(0,sig*trig)
}).add;

{a = Synth(\tasti)}.defer(0.1)
)

Ulteriori possibilità di controllo dai devices legati al computer saranno investigate nella parte dedicata alle interazioni umane (METTI LINK).

Ottimizzazione del codice

Dai codici illustrati fino a questo punto possiamo intuire uno schema generale di programmazione ottimale valido per la maggior parte degli algoritmi di sintesi e di elaborazione del suono sviluppati in uno qualsiasi dei software realizzati fino a questo momento. In generale è sempre meglio separare il codice in parti distinte, ognuna delle quali è dedicata a una precisa funzione:

  1. eventuali utilità e monitor visivi di servizio come numero di canali, audio dirivers, oscilloscopi, meter, etc.,

  2. gli strumenti virtuali da caricare sul Server:

    • eventuali dichiarazioni di variabili (locali e/o globali), cercando di utilizzare variabili globali o d'ambiente solo nei casi in cui potremmo voler accerdere al contenuto da altri blocchi di codice. Tipicamente sono le variabili a cui sono assegnati i Synth, i Buffers e i parametri di basso livello.

    • le SynthDef, i cui argomenti dovrebbero essere rappresentati nelle unità di misura più neutre possibili (frequenze in Hz, ampiezze in valori compresi tra 0.0 e 1.0, durate in tempo assoluto, etc.),

    • le diverse istanze di Synth.

  3. eventuali interfacce grafiche (GUI) come quelle illustrate nel Paragrafo precedente che devono poter essere cancellate dal file (o patch) senza modificare il codice degli strumenti a cui si riferiscono,

  4. gli eventuali codici per operazioni di basso livello ad esempio per effettuare conversioni di unità di misura oppure duplicare i valori dei parametri che arrivano dalla parte di controllo per direzionarne una copia verso i Synth e l'altra verso le GUI, etc.,

  5. gli eventuali controlli del Synth e delle interfacce grafiche esterni al Server (Routine, Task o altro) che devono essere inviati dall'Interprete come parametri unici (di alto livello) ed eventualmente splittati da codici di basso livello come esposto nel punto 2.

Se strutturiamo il codice in questo modo, in SuperCollider terminata la prototipazione possiamo includere i punti da 2 a 4 (e in alcuni casi anche il 5) all' interno del metodo s.waitForBoot{codice}. In questo modo SuperCollider accenderà il Server ed in seguito valuterà il codice in un corretto Scheduling riducendo al minimo le azioni da effettuare prima di un qualsiasi tipo di performance. Questo tipo di organizzazione del codice sarà seguita in tutti gli esempi ed è illustrata a titolo esemplificativo nel codice seguente che riassume inoltre quanto esposto in questo paragrafo:

// ------------------------------ Utilità a monitor visivi

s.scope(2);
s.meter(2,2);

(
s.waitForBoot{
var inizio=0;                  // operazioni di init
// ------------------------------ SynthDef e Synth

SynthDef(\ksig,
              {arg freq=400,smooth=0.02;
               var sig, port;
                   port = freq.lag(smooth); 
                   sig  = SinOsc.ar(port);
               Out.ar(0,sig)
               }
          ).add;

{~synth = Synth(\ksig)}.defer(0.1);

// ------------------------------ GUI

w = Window.new("GUI", 200@30); 
w.alwaysOnTop;                 
w.front;                       
w.onClose_({~synth.free;w.free;a.free;b.free;c.free}); 
                        
a = StaticText.new(w, Rect(5, 7, 200, 15)).string_("freq:               smooth:");      
b = NumberBox.new(w, Rect(35, 5, 50, 20)).valueAction_(400);                       
c = NumberBox.new(w, Rect(145, 5, 50, 20)).valueAction_(0.02);                      

// ------------------------------ Operazioni di basso livello

~ksynth = {arg fine=100,dur=1,defId=0.01,smth=0.02;          
           var range,nPassi,aPasso,dPasso;
               range  = abs(inizio-fine);
               nPassi = 1/defId;
               aPasso = Array.interpolation(nPassi,0,1).pow(4).normalize(inizio,fine);       
               dPasso = dur/nPassi;

Routine.new({                                               // rampe da Interprete
             aPasso.do({arg item;
                        ~synth.set(\freq,item,\smooth,smth); 
                        {b.value_(item); c.value_(smth)}.defer(0);           
                         inizio = item;                      
                        dPasso.wait  
                        })
             }).play;
};

b.action_({arg val; ~synth.set(\freq, val.value)});        // interazione su GUI
c.action_({arg val; ~synth.set(\smooth, val.value)});
	
MIDIIn.connectAll;                                         // interazione con devices MIDI
MIDIdef.cc(\smoothing, {arg val;
                        ~synth.set(\smooth,val/127);
                        {c.value_(val/127)}.defer(0) });
MIDIdef.noteOn(\noteOn,{arg vel,note;
                        ~synth.set(\freq,note.midicps);      
                        {b.value_(note)}.defer(0) });     
}
)

// ------------------------------ Controllo esterno dei parametri - Sequencing
(
r = Routine.new({
                 inf.do({var freq,dur,passi,smth,delta;
                             freq = rrand(300,1000);
                             dur  = rrand(0.01,1.0);
                             passi= [0.1,0.01,0.001].choose;
                             smth = 0.02;
                             delta= dur + rrand(0.01,0.5);

                             ~ksynth.value(freq,dur,passi,smth);
                             delta.wait
                         })
                  }).reset.play
)

r.stop;