Contornare lo spazio

Un mito narra che per prima cosa un Drago, identificabile nella moderna teoria del Big bang, si rannicchiò su se stesso chiudendosi come l’uovo generatore per poi alzarsi in piedi e stendersi in tutta la sua altezza aprendo le braccia, che diventarono gigantesche e possenti ali, dispiegandole in tutta la loro estensione. A questo punto il Drago lanciò il suo possente urlo verso il grande vuoto che lo circondava, tanto forte da risvegliare la vita che esso nascondeva. Il suo urlo rappresentava il primo suono della Natura. La vibrazione archetipale che avrebbe creato le forme e gli esseri umani..   G.Barbadoro

Ampiezze dinamiche

Nel capitolo precedente abbiamo visto come controllare dinamicamente la frequenza dei suoni (esclusivamente sotto l'aspetto tecnico/informatico, tralasciando pressochè totalmente l'aspetto linguistico/musicale dell'organizzazione delle altezze che è affrontato in altre parti di questo sito) mentre in questo ci occuperemo della loro ampiezza. Partiamo dal presupposto che quasi tutte le UGen forniscono valori in output compresi tra +/-1. Per modificare l'ampiezza di un segnale di questo tipo come abbiamo già detto nel Capitolo dedicato alle dinamiche e all'ampiezza dei suoni possiamo moltiplicare tutti i valori dei campioni per un fattore compreso tra 0.0 e 1.0 e in SuperCollider possiamo farlo in due modi:

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

(
{[SinOsc.ar,          // Out compreso tra +/-1
  SinOsc.ar * 0.1,    // Moltiplicando direttamente la UGen per il fattore
  SinOsc.ar(mul: 0.4) // Utilizzando l'argomento 'mul' 
  ]
}.plot(minval:-1,maxval:1) 
)

image not found

Le due sintassi si equivalgono, la prima è più "generale" (e personalmente la trovo più chiara), la seconda più specifica del software, possiamo utilizzare una o l'altra a seconda dei diversi costrutti sintattici. Nell'esempio appena esposto l'ampiezza è costante, vediamo allora le principali tecniche utilizzate per modificarla dinamicamente nel tempo. Ricordiamo che Qualsiasi parametro di un algoritmo di sintesi come in questo caso l'ampiezza di un segnale, può essere controllato in due modi:

  1. Inviando valori tramite un processo di scheduling che come sappiamo in SuperCollider possiamo realizzare nell'Interprete (Client side) in tre diversi modi:

    • inviando un valore alla volta,
    • utilizzando tecniche di sequencing (Routine, Task, Pattern),
    • interagendo con devices esterni come tastiere e interfacce MIDI o OSC o mouse e tastiera del computer,
  2. Direttamente all'interno del Server utilizzando segnali di controllo che possono essere sia ad audio che a control rate.

Controllo da Interprete

Per quanto riguarda il punto 1) ovvero il controllo da Interprete vale tutto quanto detto per le frequenze nel capitolo precedente, sottolineando la necessità di applicare tecniche di smoothing per evitare clicks (se non desiderati) dovuti a discontinuità del segnale. Il codice seguente riassume le diverse possibilità di controllo:

image not found

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

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

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

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

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

w = Window.new("GUI", 115@228); 
w.alwaysOnTop;                 
w.front;                       
w.onClose_({~synth.free;w.free;a.free;b.free;c.free;d.free}); 
                        
a = StaticText.new(w, Rect(12, 205, 200, 15)).string_("amp       smth");      
b = NumberBox.new(w,  Rect(5, 185, 50, 20)).value_(0.0);                       
c = NumberBox.new(w,  Rect(60, 185, 50, 20)).value_(0.02);  
d = Slider.new(w,     Rect(5, 5, 50, 180)).value_(0.0);     // Slider amp
e = Slider.new(w,     Rect(60, 5, 50, 180)).value_(0.02);   // Slider smooth
	
// ------------------------------ 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)                        // cresc. e dim. quartici
                             .normalize(inizio,fine);       
               dPasso = dur/nPassi;

Routine.new({                                                
             aPasso.do({arg item;
                        ~synth.set(\amp,item,\smooth,smth); // invia al Synth  
                        {b.value_(item);                    // invia ai NumberBoxes
                         c.value_(smth);
                         d.value_(item);                    // invia agli Sliders    
                         e.value_(smth)}.defer(0);           
                         inizio = item;                      
                        dPasso.wait  
                        })
             }).play;
};

d.action_({arg val; ~synth.set(\amp, val.value);        // invia al Synth
                   {b.value_(val.value)}.defer(0)       // invia al NumberBox
	       });         
e.action_({arg val; ~synth.set(\smooth, val.value);     // invia al Synth
                   {c.value_(val.value)}.defer(0)       // invia al NumberBox
	       });
	
MIDIIn.connectAll;                                        
MIDIdef.cc(\amp,{arg vel;
                        ~synth.set(\amp,vel/127);       // invia al Synth 
                       {d.value_(vel/127);              // invia allo Slider
                        b.value_(vel/127)}.defer(0) },  // invia al NumberBox
                        0);                             // cc number
MIDIdef.cc(\smoothing, {arg val;
                        ~synth.set(\smooth,val/127);    // invia al Synth
                        {e.value_(val/127);             // invia allo Slider
                         c.value_(val/127)}.defer(0)},  // invia al NumberBox
                       1);                              // cc number
}
)

// ------------------------------ Controllo esterno dei parametri - Sequencing
(
r = Routine.new({
                 inf.do({var amp,dur,passi,smth,delta;
                             amp  = rand(1.0);
                             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(amp,dur,passi,smth);
                             delta.wait
                         })
                  }).reset.play
)

r.stop;

In questo caso il tempo di lag assume una funzione prettamente tecnica mentre i crescendi e diminuendi possono essere realizzati o a mano interagendo con devices o con il mouse sulla GUI oppure attraverso rampe generate nel processo di sequencing dove possiamo eventualmente controllare anche il tipo di curva. Un'ulteriore particolarità consiste nell'utilizzo di due Slider.new() all'interno dell'interfaccia ed al necessario routing dei valori nella parte di codice dedicata alle operazioni di basso livello come ad esempio l'aggiunta del terzo argomento a MIDIdef.cc() che specifica da quale cc number leggere i valori.

Controllo con 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 l'ampiezza di un'altro segnale, a patto che il primo produca valori compresi tra 0 e 1. Vediamo come sfruttare le diverse caratteristiche dei sengali di controllo.

Rampe

Una tecnica classica per modificare dinamicamente l'ampiezza di un segnale è dato dalla generazione di interpolazioni o rampe tra valori compresi in un abito che va da 0.0 a 1.0. Abbiamo già affrontato questo argomento in precedenza generando rampe e interpolazioni nell'Interprete a livello di scheduling, mentre per quanto riguarda la generazione di rampe direttamente nel Server (segnali audio o di controllo) abbiamo a disposizione diverse UGens dedicate. In questo caso la principale differenza tra i due mondi è data dal fatto che per le rampe generate direttamente nel Server il numero di passi necessari per passare da un valore ad un altro è dato dalla moltiplicazione della rata di campionamento per il tempo di interpolazione espresso in secondi: se vado da a a b in un secondo con una rata di campionamento di 44100 Hz ci saranno 44100 passi. Anche in questo caso il percorso tra i due valori della rampa può essere lineare o non lineare:

image not found

Per quanto riguarda la generazione di rampe lineari possiamo utilizzare Line.kr() i cui argomenti sono: Line.kr(a, b, tempo)

(
SynthDef(\myLine,
                 {arg a=0,b=1,tmp=1;
                  Out.ar(0, Line.ar(a,b,tmp))
                  }).add;

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

a.set(\a,1,\b,0,\tmp,0.5); // non accade nulla...

Come possiamo notare osservando l'oscilloscopio, se proviamo a modificare dinamicamente uno dei parametri (in questo caso il tempo) di Line la rampa non si ripete, ovvero viene generata solo alla creazione del Synth e questa è la più grande limitazione di questa UGen che viene principalmente utilizzata per evitare click alla creazione di un Synth automatizzando un fade in.

Facciamo attenzione a non confondere Line.kr() di SuperCollider con line o line~ di Max, in quanto questi ultimi sono molto più versatili e utilizzati del primo.

Uno degli argomenti di Line.kr() è doneAction:n che è presente in diverse UGen dedicate al controllo dell'ampiezza di un segnale in quanto estremamente utile per una allocazione dinamica delle voci (dynamic voice allocation) e per risparmiare risorse della CPU. Possiamo assegnare a questo argomento un valore compreso tra 0 e 14 e ognuno di questi rappresenta un'azione automatica che sarà effettuata sul Synth contenente la UGen nella quale è specificato questo argomento quando questa ha finito di eseguire il suo compito (ad esempio nel caso di Line al termine della rampa).

image not found

Tra tutte queste possibilità quelle che utilizzeremo più spesso sono doneAction:0 e doneAction:2:

Per generare rampe esponenziali invece possiamo utilizzare la UGen XLine.kr() del tutto simile a Line.kr() tranne che per due piccole differenze:

(
{[SinOsc.ar,
  XLine.ar(0.001,1,1,doneAction:0),
  SinOsc.ar*XLine.ar(0.001,1,1,doneAction:0),
  XLine.ar(1,0.001,1,doneAction:0),
  SinOsc.ar*XLine.ar(1,0.001,1,doneAction:0)
]}.plot(2);
)

image not found

Segnali unipolari impulsivi

Per controllare l'ampiezza di un segnale possiamo utilizzare dei segnali impulsivi ovvero dei segnali che generano singoli campioni maggiori di 0 separati da tempi delta regolari o irregolari. In SuperCollider abbiamo a disposizione due UGens:

I suoni appena ottenuti sia utilizzando Impulse.kr() che Dusr.kr() sono estremamente percussivi in quanto la discontinuità tra i valori dei campioni del segnale di controllo è massima: y = 0 oppure y > 0. Possiamo però "arrotondare" (smoothing) questi segnali con altre due UGens: Decay.ar(sig,atk) e Decay2.ar(sig,atk,rel). Il primo applica un decadimento esponenziale a un segnale impulsivo, mentre il secondo applica anche un attacco. Gli argomenti sono: sig = il segnale al quale applicare il decay, atk = tempo di attacco in secondi (solo per Decay2), rel = tempo di decadimento in secondi

image not found

(
SynthDef(\pulse,
              {arg delta = 10, freq = 1200, decay = 0.2;
               var amp,sig;
                   amp  = Decay.kr(Impulse.kr(delta),decay);
                   sig  = SinOsc.ar(freq);
                Out.ar(0,sig*amp)
                }
          ).add;

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

a.set(\delta,rrand(1,10),\freq,rrand(200,2000),\decay,rrand(0.02,0.5));
a.free;

(             // esegui più volte
s.freeAll;
5.do({Synth(\pulse,[\delta,rrand(1,10),\freq,rrand(200,2000),\decay,rrand(0.02,0.5)])});
)

Segnali unipolari trapezoidali (Trig.kr())

Possiamo generare inviluppi trapezoidali monitorando in tempo reale un qualsiasi tipo di segnale caratterizzato da una marcata discontinuità nel passaggo da negativo a positivo come ad esempio quello generato da LFPulse.kr() (triggers regolari) oppure LFNoise0.kr (triggers irregolari). Infatti se li utilizziamo in congiunzione con la Ugen Trig.kr() viene generato un segnale che campiona e mantiene (sample and hold) per un tempo specificato come secondo argomento di Trig.kr() il valore immediatamente successivo al passaggio da negativo a positivo del segnale monitorato generando di fatto un inviluppo trapezoidale al quale possiamo eventualmente applicare le tecniche di smoothing che conosciamo.

(
{var freq, ksig;
	 freq = 3;
	 ksig= LFPulse.kr(freq);
 [ksig,
  Trig.kr(ksig,1/freq),      // 1 ciclo nota, 1 ciclo pausa
  Trig.kr(ksig,1/freq-0.02), // release 20 ms prima del nuovo trigger
  Trig.kr(ksig,1/freq*0.5),  // 1/2 ciclo nota 1/2 ciclo pausa
  Trig.kr(ksig,1/freq*0.3),  // 1/3 ciclo nota 2/3 ciclo pausa
  Trig.kr(ksig,1/freq*2)     // 2 cicli = 1 nota poi 1 ciclo pausa
]}.plot(4)
)

image not found

// Inviluppo trapezoidale senza smoothing
(
SynthDef(\envi,
              {arg tfreq=10, dur=1;
               var ksig,trig,bpf,env,sig;
                   ksig = LFNoise0.kr(tfreq);
                   trig = Trig.kr(ksig,1/tfreq*dur);
                   sig = SinOsc.ar(1970);
                Out.ar(0,sig*trig)
                }
          ).add;

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

a.set(\amp,rand(1.0),\tfreq, rrand(2,10),\dur,rrand(0.01,1)); // esegui più volte

// Inviluppo trapezoidale con smoothing
(
SynthDef(\envi,
              {arg tfreq=10, dur=1;
               var ksig,trig,bpf,env,sig;
                   ksig = LFNoise0.kr(tfreq);
                   trig = Trig.kr(ksig,1/tfreq*dur).lag(0.02);
                   sig = SinOsc.ar(1970);
                Out.ar(0,sig*trig)
                }
          ).add;

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

a.set(\amp,rand(1.0),\tfreq, rrand(2,10),\dur,rrand(0.01,1)); // esegui più volte

Segnali unipolari continui

In generale possiamo utilizzare un qualsiasi tipo di segnale continuo per controllare l'ampiezza di un altro segnale a patto che sia unipolare (tra 0 e 1). Se questo (come la stragrande maggioranza dei casi) è invece bipolare (tra +/- 1), per poterlo utilizzare come moltiplicatore d'ampiezza dobbiamo prima trasformarlo da bipolare a unipolare. Per farlo abbiamo a disposizione due modi:

Vediamo in quest'ultimo esempio come si possano utilizzare entrambi i modi per rendere unipolare qualsiasi tipo di segnale compreso tra +/-1 per poi moltiplicarlo per il segnale a cui vogliamo riscalare l'ampiezza:

(
{[SinOsc.ar(400),
  SinOsc.ar(5).unipolar,
  SinOsc.ar(400) * SinOsc.ar(5).unipolar,
  SinOsc.ar(400, mul:SinOsc.ar(10).unipolar)
  ]
}.plot(duration:1,minval:-1,maxval:1) 
)

image not found

Come è facilmente riscontrabile dall'immagine precedente il segnale di controllo però non parte da 0 a causa del DC offset che abbiamo introdotto nel renderlo unipolare e, per evitare una discontinuità iniziale dobbiamo modificarne la fase iniziale.

(
{var ksig, amp, sig;
     ksig = SinOsc.ar(5).unipolar;	   // bipolare --> unipolare
     amp  = SinOsc.kr(5,-0.5*pi).unipolar; // correzione di fase
     sig  = SinOsc.ar;                     // segnale audio
     [ksig,amp,sig*amp]
}.plot(1)
)

image not found

Possiamo sostituire SinOsc.kr() con una qualsiasi UGen a patto che generi un segnale unipolare sia periodico che aperiodico come negli esempi sottostanti:

(
{[SinOsc.ar(400, mul:SinOsc.ar(10).unipolar),
  SinOsc.ar(400) * (LFSaw.ar(5) * 0.5 + 0.5),
  LFTri.ar(400, mul: LFPulse.ar(10).unipolar),
  SinOsc.ar(400) * (LFNoise0.ar(5) * 0.5 + 0.5)
  ]
}.plot(duration:1,minval:-1,maxval:1)
)

// Audio

a = {SinOsc.ar(400, mul:SinOsc.ar(4).unipolar.scope).scope}.play; // eseguire una riga alla volta
a.free; 
a = {SinOsc.ar(420).scope * (LFSaw.ar(5) * 0.5 + 0.5).scope}.play;
a.free; 
a = {LFTri.ar(440, mul: LFPulse.ar(7).unipolar.scope).scope}.play;
a.free; 
a = {SinOsc.ar(1500).scope * (LFNoise0.ar(15) * 0.5 + 0.5).scope}.play;

image not found

Alcune forme d'onda come l'onda a dente di sega però, se utilizzate come segnali di controllo ci pongono di fronte a un'ulteriore problematica: a differenza di un onda sinusoidale, triangolare o di altro tipo, alla fine di ogni ciclo si verifica una discontinuità ovvero il fattore di moltiplicazione dell'ampiezza va da 1 a 0 in soli due campioni e genera di fatto un click. Per eliminare questo artefatto (sempre che lo si voglia eliminare) dobbiamo "arrotondarlo" con una qualche tecnica di smoothing:

(
{
 [LFSaw.kr(5).unipolar,
  LFSaw.ar(5).unipolar.lag(0.001),//  1 ms
  LFSaw.ar(5).unipolar.lag(0.01), // 10 ms
  LFSaw.ar(5).unipolar.lag(0.1),  // 100 ms
  LFSaw.ar(5).unipolar.lag(0.5),  // 500 ms
  LFSaw.ar(5).unipolar.lag(1)]    //   1 s
}.plot(1)
)

(
SynthDef(\ksig,
              {arg smtime=0.1;
               var amp, sig;
                   amp = LFSaw.kr(3).unipolar.lag(smtime); // smoothing
                   sig = SinOsc.ar(100);
               Out.ar(0,sig*amp)
               }
          ).add;

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

a.set(\smtime,0.001);
a.set(\smtime,0.01);
a.set(\smtime,0.1);
a.set(\smtime,0.5);
a.set(\smtime,1);

image not found

Facciamo attenzione perchè come possiamo osservare nell'immagine precedente in questo caso a differenza dei cambiamenti d'ampiezza controllati dall'Interprete il valore che assegnamo allo smoothing time può modificare notevolmente la forma d'onda. Infine se volessimo impiegare una forma d'onda non periodica ci sono diverse UGens dedicate caratterizzate proprio da questo parametro:

image not found

(
{
 [LFNoise0.kr(15).unipolar, // senza interpolazione
  LFNoise1.kr(15).unipolar, // interpolazione lineare
  LFNoise2.kr(15).unipolar, // interpolazione cubica
  LFClipNoise.kr(15).unipolar]
}.plot(1)
)

Mouse e key

Infine possiamo utilizzare anche i segnali di controllo generati dalle UGens dedicate al monitoraggio del mouse e della tastiera del computer mappandoli direttamente sugli argomenti della SynthDef. Esempi al riguardo sono già stati illustrati in precedenza.

Ulteriori sviluppi

Tutti i parametri (argomenti) dei segnali di controllo possono a loro volta essere controllati attraverso le tecniche appena esposte sia Client (singoli valori o sequencing) che Server side (modulazioni). In seguito alcuni esempi legati all'ampiezza ma che sono validi per qualsiasi altro parametro (argomento).

Ampiezza dei segnali di controllo

Abbiamo appena visto come modificare dinamicamente l'ampiezza di un segnale audio attraverso segnali di controllo unipolari. Questi ultimi però, ad eccezione di Dust.kr(), dei generatori randomici (LFNoise0.kr e simili) e dei segnali trapezoidali ottenuti con Trig.kr() hanno loro stessi un'ampiezza fissa:

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

(
SynthDef(\ksig,
              {var amp, sig;      	 
                   amp = LFSaw.kr(2).unipolar.lag(0.001); 
                   sig = SinOsc.ar(100);
               Out.ar(0,sig*amp)
               }
          ).add;

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

Volendo però, possiamo variare nel tempo anche l'ampiezza di questi segnali con le stesse tecniche già incontrate:

  1. inviando dinamicamente dall'Interprete al Server i valori di ampiezza del segnale di controllo (Client side):

    image not found

    // ------------------------------ Utilità a monitor visivi
    
    s.scope(1);
    s.meter(1,1);
    
    (
    s.waitForBoot{
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\ksig,
                  {arg amp=0,smooth=0.02,delta=4;
                   var samp, kamp, sig;
                       samp = LFPulse.kr(delta).unipolar.lag(smooth); // ampiezza segnale audio
                       kamp = K2A.ar(amp).lag(smooth);                // ampiezza segnale controllo
                       sig  = SinOsc.ar(100);                         // segnale audio
                   Out.ar(0,sig*samp*kamp)
                   }
              ).add;
    
    {~synth = Synth(\ksig)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("GUI", 115@228); 
    w.alwaysOnTop;                 
    w.front;                       
    w.onClose_({~synth.free;w.free;a.free;b.free;c.free;d.free;e.free}); 
                            
    a = StaticText.new(w, Rect(12, 205, 200, 15)).string_("amp       smth      freq");      
    b = NumberBox.new(w,  Rect(5, 185, 50, 20)).value_(0.0);                       
    c = NumberBox.new(w,  Rect(60, 185, 50, 20)).value_(0.02);  
    d = Slider.new(w,     Rect(5, 5, 50, 180)).value_(0.0);     // Slider amp
    e = Slider.new(w,     Rect(60, 5, 50, 180)).value_(0.02);   // Slider smooth
    	
    // ------------------------------ Operazioni di basso livello
    
    ~ksynth = {arg amp=0,smooth=0.2;
               ~synth.set(\amp,amp,\smooth,smooth);         // invia al Synth
                     
               {b.value_(amp);                              // invia al NumberBox
                c.value_(smooth);
                d.value_(amp);
                e.value_(smooth);
                }.defer(0)             
    
    };
    
    d.action_({arg val; ~synth.set(\amp, val.value);        // invia al Synth
                       {b.value_(val.value)}.defer(0)       // invia al NumberBox
    	       });         
    e.action_({arg val; ~synth.set(\smooth, val.value);     // invia al Synth
                       {c.value_(val.value)}.defer(0)       // invia al NumberBox
    	       });
    	
    MIDIIn.connectAll;                                        
    MIDIdef.cc(\amp,{arg vel;
                            ~synth.set(\amp,vel/127);       // invia al Synth 
                           {d.value_(vel/127);              // invia allo Slider
                            b.value_(vel/127)}.defer(0) },  // invia al NumberBox
                            0);                             // cc number
    MIDIdef.cc(\smoothing, {arg val;
                            ~synth.set(\smooth,val/127);    // invia al Synth
                            {e.value_(val/127);             // invia allo Slider
                             c.value_(val/127)}.defer(0)},  // invia al NumberBox
                           1);                              // cc number
    }
    )
    
    // ------------------------------ Controllo esterno dei parametri 
    // Invio singolo
    (
    var amp,smooth;
        amp = rand(1.0); 
        smooth = rrand(0.01,1);
    
    ~ksynth.value(amp:amp,smooth:smooth);
    )
    
    // Sequencing
    (
    r = Routine({
                 inf.do({var amp,smooth,delta;
                             amp = rand(1.0); 
                             smooth = rrand(0.01,1);
                             delta = rrand(0.01,5);
    		
                             ~ksynth.value(amp:amp,smooth:smooth);
                         delta.wait
                        })
                }).reset.play
    )
    
    r.stop;
    

    Nel codice precedente nelle operazioni di basso livello non ci sono i loops per le interpolazioni dei valori da interprete, ma volendo possiamo aggiungerne uno per ogni parametro. Questo tipo di scelte dipende dalle necessità di controllo musicali che di volta in volta abbiamo.

  2. utilizzando un ulteriore segnale di controllo per variare l'ampiezza del segnale di controllo che modifica quella del segnale audio (Server side):

    image not found

    (
    {var amp, kamp, sig;
         amp  = LFPulse.kr(3).unipolar.lag(0.001); // ampiezza segnale audio
         kamp = LFSaw.kr(1).unipolar;              // ampiezza segnale controllo
         sig  = SinOsc.ar(100);                    // segnale audio
    	[sig,
             amp,
    	 sig*amp,
    	 kamp,
    	 amp*kamp,
    	 sig*amp*kamp]
    }.plot(4)
    )
    
    (
    SynthDef(\ksig,
                  {arg smtime=0.001,delta=1;
                   var amp, kamp, sig;
                       amp  = LFPulse.kr(delta).unipolar.lag(smtime); 
                       kamp = LFSaw.kr(1).unipolar;
                       sig  = SinOsc.ar(100);
                   Out.ar(0,sig*amp*kamp)
                   }
              ).add;
    
    {a = Synth(\ksig)}.defer(0.1);
    )
    
    a.set(\delta,4);
    a.set(\delta,3);
    a.set(\delta,2);
    a.set(\delta,1);
    

    Ecco un esempio simile al precedente con un segnale di controllo aperiodico:

    image not found

    (
    {var amp, kamp, sig;
         amp  = LFPulse.kr(3).unipolar.lag(0.001); // ampiezza segnale audio
         kamp = LFNoise1.kr(10).unipolar;          // ampiezza segnale controllo
         sig  = SinOsc.ar(100);                    // segnale audio
    	[sig,
             amp,
    	 sig*amp,
    	 kamp,
    	 amp*kamp,
    	 sig*amp*kamp]
    }.plot(4)
    )
    
    (
    SynthDef(\ksig,
                  {arg smtime=0.001,delta=1;
                   var amp, kamp, sig;
                       amp  = LFPulse.kr(delta).unipolar.lag(smtime);
                       kamp = LFNoise1.kr(10).unipolar;
                       sig  = SinOsc.ar(100);
                   Out.ar(0,sig*amp*kamp)
                   }
              ).add;
    
    {a = Synth(\ksig)}.defer(0.1);
    )
    
    a.set(\delta,4);
    a.set(\delta,3);
    a.set(\delta,2);
    a.set(\delta,1);
    

    Tutti i segnali di controllo utilizzati fino ad ora sono a bassa frequenza (Low Frequency - LFqualchecosa) e le UGen che li generano corrispondono a quello che nel mondo dei sintetizzatori analogici modulari si chiama Low Frequency Oscillator (LFO o oscillatore a bassa frequenza).

Sottolineiamo infine due particolarità musicali che riguardamo i segnali impulsivi:

A parte questo possiamo utilizzarli come gli altri segnali di controllo in tutte le tecniche appena illustrate.

Frequenza dei segnali di controllo

Il tempo nei segnali periodici continui è definito dalla lunghezza del ciclo mentre nei segnali periodici impulsivi dal tempo delta che intercorre tra due impulsi successivi (in pratica è la stessa cosa, ma cambia il concetto teorico). In entrambi i casi è espresso in Hertz (cicli per secondo). Per quanto riguarda i segnali non periodici sia continui che impulsivi invece la questione è un po più complessa e in genere dobbiamo specificare la densità di eventi probabili all'interno di un secondo come nel caso di Dust.kr() oppure il tempo di un segnale che modula dinamicamente una frequenza (come nel caso di segnali periodici continui).

image not found

(
{
[SinOsc.kr(5),                    // segnale periodico continuo
 LFNoise0.kr(5),                  // Segnale periodico continuo
 Impulse.kr(5),                   // Segnale periodico impulsivo

 SinOsc.kr(5+(LFNoise0.kr(10)*4)),// segnale aperiodico continuo
 Dust.kr(5)]                      // Segnale aperiodico impulsivo
}.plot(4)
)

Come abbiamo già visto nella prima Parte di questo scritto potremmo voler specificare la durata dell'inviluppo oltre che in Hertz anche in millisecondi o in frazioni di bpm. Per farlo però dobbiamo effettuare delle semplici conversioni in quanto le UGen utilizzate accettano solo valori espressi in Hertz.

(
a = {arg bpm,ms,sec;
    ("bpm -> Hz:"++ (bpm/60)).postln;
    ("ms -> Hz:"++ (1000/ms)).postln;
    ("sec -> Hz:"++ (1/sec)).postln
    }
)

a.value(100,200,0.5);

Chiarita l'unità di misura attraverso la quale specificare i valori temporali possiamo utilizzare tutte le tecniche esposte per le variazioni dei livelli riassunte a livello esemplificativo nei codici seguenti:

  1. inviando dinamicamente dall'Interprete al Server i valori:

    image not found

    // ------------------------------ Utilità a monitor visivi
    
    s.scope(1);
    s.meter(1,1);
    
    (
    s.waitForBoot{
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\ksig,
                  {arg amp=0,smooth=0.02,delta=1;
                   var samp, kamp, sig;
                       samp = LFPulse.kr(delta).unipolar.lag(smooth); // ampiezza segnale audio
                       kamp = K2A.ar(amp).lag(smooth);                // ampiezza segnale controllo
                       sig  = SinOsc.ar(100);                         // segnale audio
                   Out.ar(0,sig*samp*kamp)
                   }
              ).add;
    
    {~synth = Synth(\ksig)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("GUI", 170@228); 
    w.alwaysOnTop;                 
    w.front;                       
    w.onClose_({~synth.free;w.free;a.free;b.free;c.free;d.free;e.free}); 
                            
    a = StaticText.new(w, Rect(12, 205, 200, 15)).string_("amp       smth      freq");      
    b = NumberBox.new(w,  Rect(5, 185, 50, 20)).value_(0.0);                       
    c = NumberBox.new(w,  Rect(60, 185, 50, 20)).value_(0.02);  
    d = Slider.new(w,     Rect(5, 5, 50, 180)).value_(0.0);     // Slider amp
    e = Slider.new(w,     Rect(60, 5, 50, 180)).value_(0.02);   // Slider smooth
    f = Slider.new(w,     Rect(115, 5, 50, 180)).value_(0.4);   // Slider freq
    g = NumberBox.new(w,  Rect(115, 185, 50, 20)).value_(50);  
    	
    // ------------------------------ Operazioni di basso livello
    
    ~ksynth = {arg amp=0,smooth=0.2,delta=1;
               ~synth.set(\amp,amp,                         // invia al Synth
                          \smooth,smooth,
                          \delta,delta); 
               {b.value_(amp);                              // invia al NumberBox
                c.value_(smooth);
                d.value_(amp);
                e.value_(smooth);
                f.value_(delta*0.1);
                g.value_(delta);
                }.defer(0)             
    
    };
    
    d.action_({arg val; ~synth.set(\amp, val.value);        // invia al Synth
                       {b.value_(val.value)}.defer(0)       // invia al NumberBox
    	       });         
    e.action_({arg val; ~synth.set(\smooth, val.value);     // invia al Synth
                       {c.value_(val.value)}.defer(0)       // invia al NumberBox
    	       });
    f.action_({arg val; ~synth.set(\delta, val.value*10);   // invia al Synth
                       {g.value_(val.value*10)}.defer(0)    // invia al NumberBox
    	       });
    	
    MIDIIn.connectAll;                                        
    MIDIdef.cc(\amp,{arg vel;
                            ~synth.set(\amp,vel/127);       // invia al Synth 
                           {d.value_(vel/127);              // invia allo Slider
                            b.value_(vel/127)}.defer(0) },  // invia al NumberBox
                            0);                             // cc number
    MIDIdef.cc(\smoothing, {arg val;
                            ~synth.set(\smooth,val/127);    // invia al Synth
                            {e.value_(val/127);             // invia allo Slider
                             c.value_(val/127)}.defer(0)},  // invia al NumberBox
                           1);                              // cc number
    MIDIdef.cc(\freq, {arg val;
                            ~synth.set(\delta,val/12.7);    // invia al Synth
                            {f.value_(val/127);             // invia allo Slider
                             g.value_(val/12.7)}.defer(0)}, // invia al NumberBox
                           2);                              // cc number
    }
    )
    
    // ------------------------------ Controllo esterno dei parametri 
    // Invio singolo
    (
    var amp,smooth,delta;
        amp = rand(1.0); 
        smooth = rrand(0.01,1);
        delta = rrand(1,5);
    
    ~ksynth.value(amp:amp,smooth:smooth,delta:delta);
    )
    
    // Sequencing
    (
    r = Routine({
                 inf.do({var amp,smooth,delta;
                             amp = rand(1.0); 
                             smooth = rrand(0.01,1);
                             delta = rrand(1,5);
    		
                             ~ksynth.value(amp:amp,smooth:smooth,delta:delta);
                         delta.wait
                        })
                }).reset.play
    )
    
    r.stop;
    

    Le novità nel codice precedente sono:

    • l'introduzione di un ulteriore Slider di controllo con i relativi collegamenti e delle didascalie sulla GUI

    • una prassi sintattica non necessaria ma vivamente consigliata: quando controlliamo una SynthDef dall'interprete specifichiamo i paramteri al suo interno sempre come valori assoluti (frequenze in Hertz, ampiezze tra 0 e 1, etc.) e nel caso volessimo usare unità di misura differenti (millisecondi, velocity midi, mdi note, dB, etc.) effettuiamo le dovute conversioni nell'Interprete, possibilmente nella porzione di codice dedicata alle operazioni di basso livello.

  2. utilizzando un ulteriore segnale di controllo per variare sia la frequenza del segnale di controllo che l'ampiezza:

    (
    {arg amp=1,smtime=0.001,delta=2;
     var kfreq, samp, kamp, sig;
         kfreq = LFNoise0.kr(delta);  // segnale di controllo sync +/- 1
         samp  = LFPulse.kr(kfreq *3  // +/- 3
                                  +5  // da 2 a 8 Hz Frequenza
                            ).unipolar.lag(smtime);
         kamp = kfreq.unipolar.lag(smtime); // Ampiezza
         sig  = SinOsc.ar(100);
    	[sig,
    	 kfreq,
             samp,
    	 sig*samp,
    	 kamp,
    	 samp*kamp,
    	 sig*samp*kamp]
    }.plot(4)
    )
    
    (
    SynthDef(\ksig,
                  {arg amp=1,smtime=0.001,delta=2;
                   var kfreq, samp, kamp, sig;
                       kfreq = LFNoise0.kr(delta);  
                       samp  = LFPulse.kr(kfreq*3+5).unipolar.lag(smtime);
                       kamp  = kfreq.unipolar.lag(smtime); 
                       sig   = SinOsc.ar(100);
                   Out.ar(0,sig*kamp*samp)
                   }
              ).add;
    
    {a = Synth(\ksig)}.defer(0.1);
    )
    

    image not found

    Notiamo come nel codice precedente un solo segnale (kfreq) controlli, debitamente riscalato sia la frequenza che l'ampiezza di un'altro segnale. Questo significa mettere in sync diversi segnali di controllo.

Anche riguardo al tempo sottolineiamo una particolarità dei segnali impulsivi usati in congiunzione con Decay.kr() oppure con Decay2.kr() e quelli ottenuti con Trig.kr() o Trig1.kr(). In questi casi infatti dobbiamo specificare una durata che è indipendente dalla frequenza del trigger:

// Decay.kr()
(
SynthDef(\pulse,
              {arg delta=5, dur = 0.1;
               var trig,amp,sig;
                   trig = Impulse.kr(delta);
                   amp  = Decay.kr(trig,dur);
                   sig  = SinOsc.ar(1200);
                Out.ar(0,sig*amp)
                }
          ).add;

{a = Synth(\pulse)}.defer(0.1);
)

a.set(\delta,5,\dur, 0.1); // tempo di decay < tempo delta dei triggers
a.set(\delta,5,\dur, 2);   // tempo di decay > tempo delta dei triggers

// Trig1.kr()
(
SynthDef(\pulse,
              {arg delta=5, dur = 0.1;
               var trig,amp,sig;
                   trig = SinOsc.kr(delta);
                   amp  = Trig1.kr(trig,dur);
                   sig  = SinOsc.ar(1200);
                Out.ar(0,sig*amp)
                }
          ).add;

{a = Synth(\pulse)}.defer(0.1);
)

a.set(\delta,5,\dur, 0.1); // tempo di decay < tempo delta dei triggers
a.set(\delta,5,\dur, 2);   // tempo di decay > tempo delta dei triggers

Se la durata è inferiore al tempo delta che intercorre tra due trigger non si verifica alcun problema, se è superiore invece:

Per controllare meglio questo rapporto può essere conveniente uniformare le unità di misura effettuando automaticamente le dovute conversioni in quanto il numero di impulsi è espresso in Hertz mentre la durata in secondi:

(
SynthDef(\pulse,
              {arg delta=5, dur = 0.1;
               var trig,amp,sig;
                   trig = Impulse.kr(delta);
                   amp  = Decay.kr(trig,dur);
                   sig  = SinOsc.ar(1200);
                Out.ar(0,sig*amp)
                }
          ).add;

{a = Synth(\pulse)}.defer(0.1);
)

// Secondi come unità di misura 
a.set(\delta,1/0.3,\dur, 0.1); // tempo di decay < tempo delta dei triggers
a.set(\delta,1/0.3,\dur, 2.3); // tempo di decay > tempo delta dei triggers

// Hertz come unità di misura
a.set(\delta,1,\dur, 1/2);   // tempo di decay < tempo delta dei triggers
a.set(\delta,5,\dur, 1/0.9); // tempo di decay > tempo delta dei triggers

Curve dei segnali di controllo

Infine possiamo cambiare dinamicamente il profilo d'onda di alcuni segnali periodici continui ma per farlo dobbiamo distinguerli in base alla loro forma d'onda originale:

Break Point Function (BPF)

Abbiamo visto in precedenza come generare rampe formate da un solo segmento con le UGens Line.kr() e XLine.kr() ma se volessimo invece generare un inviluppo formato da segmenti multipli come potremmo fare? Il metodo migliore consiste nel definire i diversi punti su un piano cartesiano attraverso coordinate x/y dove le ascisse (x) corrispondono al tempo mentre le ordinate (y) ai livelli:

image not found

Questo tipo di descrizione e visualizzazione si chiama BPM (Break Point Function) e in quasi tutti i software musicali è presente un oggetto o un'interfaccia grafica che ci permette di realizzare tutte le successioni di rampe che desideriamo, ad esempio in SuperCollider utilizzeremo la classe Env sulla quale possiamo invocare diversi metodi mentre in Max esiste l'oggetto BPF che è una GUI (Graphical User Interface).

Env

Per ragioni esemplificative creiamo una nuova istanza di un inviluppo custom invocando il metodo .new (che ricordiamo può essere sottinteso) sulla Classe Env. I suoi primi tre argomenti descrivono tutte le caratteristiche morfologiche di qualsiasi tipo di rampa multisegmento. Possiamo inoltre invocare su di esso il metodo .test che serve per sentire immediatamente l'inviluppo (monitor uditivo) e anche il metodo .plot che lo visualizza in una finestra grafica (monitor visivo) esattamente come abbiamo fatto per Array e segnali.

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

Env.new([0,0.5,0],[1,1]).test.plot(minval: 0, maxval: 1);       // [livelli],[tempi_delta]
Env([0,0.7,0.1,0],[0.1,0.2,1]).test.plot(minval: 0, maxval: 1); // Abbreviazione...

Osserviamo nel dettaglio gli argomenti:

image not found

Env.plot()

Abbiamo visto che invocando il metodo .plot() su un'istanza di Env questa viene visualizzata in una nuova finestra.

(
Env([0,0.5,0.1,0],[0.1,0.2,1], \wel).plot(
         4,       // size o numero di punti visualizzati (non necessariamente uguali a quelli del BPF)
         800@200, // bounds (dimensioni grafiche in pixels)
         0,1)     // minval e maxval.
)

image not found

Ricordiamo che premendo il tasto 'm' passiamo attraverso diverse modalità di visualizzazione e che un singolo punto dell'inviluppo specificato da una coppia di valori xy si chiama nodo.

EnvelopeView.new() - GUI

Così come avviene per altri parametri del suono, in SuperCollider possiamo creare una GUI (Graphical User Interface) dedicata, che in questo caso visualizza dinamicamente gli inviluppi in una sola finestra e con la quale (a differenza delle visualizzazioni realizzate con il metodo .plot() possiamo interagire attraverso il mouse modificando dinamicamente le posizioni dei punti (nodi) di un inviluppo: EnvelopeView. Il numero dei punti e le curve che intercorrono tra loro però non possono essere modificati col mouse. Vediamo come costruire l'interfaccia grafica:

  1. Creiamo una nuova finestra all'interno della quale posizionare la visualizzazione dell'inviluppo esattamente come abbiamo fatto in precedenza per lo Slider:

    (
    w = Window.new("EnvelopeView", 500@400);     // Crea una finestra di 500x400 pixels
    w.alwaysOnTop;                               // La posiziona sempre sopra tutte le altre finestre
    w.front;                                     // Visualizza la finestra
    w.onClose_({e.free;w.free});                 // Libera le variabili 'e' e 'w' quando si chiude la finestra
    
    e = EnvelopeView.new(w, Rect(10,10,480,380));// Crea un EnvelopeWiev all'interno della finestra
    e.resize_(5);                                // Permette il resize con il mouse della finestra
    )
    
    w.close;                                     // chiude la finestra dal codice
    

    image not found

    Una particolarità di EnvelopeView consiste nell'utilizzo del metodo .resize(n) che permette di modificare le dimensioni della finestra con il mouse una volta che è stata creata. Come argomento dobbiamo specificare un numero intero che corrisponde a determinate caratteristiche:

    image not found

  2. Definiamo un inviluppo e recuperiamo alcune informazioni su di esso assegnandole a variabili:
    ~myEnv   = Env.new([0,1,0.8,0],[0.1,0.6,0.7]); // inviluppo
    ~livelli = ~myEnv.levels;                      // Array dei livelli
    ~deltaT  = ~myEnv.times;                       // Array dei tempi delta
    ~envDur  = ~myEnv.duration;                    // Durata totale dell'inviluppo
    
    ~myEnv.plot(minval:0,maxval:1);                // Plotter
    
    • .levels: riporta l'Array dei livelli.
    • .times: riporta l'Array dei tempi delta.
    • .duration: riporta la durata totale dell'inviluppo.
  3. Riscaliamo i valori dell'inviluppo con il metodo .normalizeSum che riporta tutti i valori di un Array in un ambito compreso tra 0 e 1 in quanto l'interfaccia EnvelopeView.new() accetta solo valori compresi in questo ambito sia per le ascisse che per le ordinate.

    ~deltaTNorm = ~deltaT.normalizeSum; // riscala linearmente in valori compresi tra 0 e 1
  4. Trasformiamo i tempi delta in onsets con il metodo .integrate che riporta un Array con la somma incrementale di tutti gli elementi e aggiungiamo il tempo 0 (inizio) per lo stesso motivo del punto precedente:

    ~onsets     = [0] ++ ~deltaTNorm.integrate; // aggiungiamo l'inizio [0] e trasformiamo in onset i tempi delta
    
  5. Carichiamo e visualizziamo i valori riscalati sull'interfaccia con il metodo .valueAction_() e li recuperiamo nel codice (eventualmente modificati con il mouse) invocando il metodo .action_({}) come abbiamo visto per lo Slider:
    (
    e.valueAction_([~onsets, ~livelli]); // li inviamo all'interfaccia
    e.action_({arg i;
                   ~envOra = i.value; // legge i valori xy visualizzati e li assegna alla variabile
                   ~envOra.postln     // stampa
               })
    )
    
  6. Modifichiamo i punti dell'inviluppo con il mouse interagendo con l'interfaccia (vedremo il risultato dell'interazone stampato nella Post window).

  7. Trasformiamo gli onset in tempi delta con il metodo differentiate ed eliminiamo lo 0 iniziale con il metodo .drop(n) (elimina i primi 'n' items di un Array) in quanto Env li accetta in questa forma.

    ~onsets     = ~envOra.at(0);                       // recupera l'Array degli onsets tra 0 e 1
    ~deltaTNorm = ~envOra.at(0).differentiate;         // da onset a tempi delta tra 0 e 1
    ~deltaTNorm = ~envOra.at(0).differentiate.drop(1); // elimina lo 0 iniziale
    
  8. Riscaliamo i valori nel range temporale originale.

    ~deltaT     = ~deltaTNorm * ~envDur; // riscala i valori alla durata originale
    
  9. Creiamo un nuovo inviluppo con i nuovi valori

    ~livelli = ~envOra.at(1);                                     // recupera l'Array dei livelli
    ~myEnv   = Env.new(~livelli,~deltaT).plot(minval:0,maxval:1); // Crea un nuovo inviluppo
    

Se volessimo semplificare i passaggi precedenti, automatizzare e testare il suono in loop:

(
// ------------------------------ GUI 

w = Window.new("EnvelopeView", 500@400);
w.alwaysOnTop;
w.onClose_({e.free;w.free;r.stop;});
w.front;
e = EnvelopeView.new(w, Rect(10,10,480,380)).resize_(5);

// Inviluppo
~myEnv = Env.new([0,1,0.8,0],[0.1,0.6,0.7]);
~myEnv.plot(minval:0,maxval:1);

// ------------------------------ Riscala e visualizza 

e.valueAction_([[0] ++ ~myEnv.times.normalizeSum.integrate, ~myEnv.levels]);

// ------------------------------ Riscala e ricrea l'inviluppo

e.action_({arg i;
           ~deltaT  = i.value[0].differentiate.drop(1) * ~myEnv.duration;
           ~livelli = i.value[1];
           ~myEnv   = Env.new(~livelli,~deltaT);
	       [~livelli,~deltaT].postln
           });

// ------------------------------ Suona in loop

r = Routine({
             inf.do({
                     {SinOsc.ar*EnvGen.ar(~myEnv,doneAction:2)}.play;
                     ~myEnv.duration.wait
	                 })
             }).reset.play
)

r.stop;

Infine possiamo modificare l'aspetto dell'interfaccia invocando alcuni metodi dopo che l'abbiamo creata:

(
e.drawLines_(true);            // Visualizza le linee tra i nodi
e.drawRects_(true);            // Visualizza i punti dei nodi
e.thumbSize_(20);              // Cambia il size dei nodi
e.strokeColor_(Color.blue);    // Colore delle connessioni
e.fillColor_(Color.green);     // Colore dei nodi
e.selectionColor_(Color.red);  // Colore della selezione
e.gridColor_(Color.black);     // Colore della cornice
e.resize_(5);                  // Permette il resize della finestra
e.step_(0.01);                 // Risoluzione in decimali
)

image not found

BPF e segnali di controllo

Abbiamo visto che una Break Point Function è formata da punti su un piano cartesiano descritti da valori x e y. Nelle visualizzazioni (sia in Plotter che in EnvelopeView) questi punti sono collegati tra loro da linee che in realtà non esistono, sono solo una convenzione grafica e, se vogliamo applicare inviluppi a un segnale audio di qualsiasi tipo dobbiamo trasformare le BPF in segnali di controllo attraverso interpolazioni (lineari e non) che generino i valori intermedi tra due punti esattamente come abbiamo fatto quando abbiamo trasformato i singoli valori provenienti dall'Interprete in segnali di controllo e gli abbiamo applicato uno smoothing. Per fare questo in SuperCollider c'è una UGen chiamata EnvGen che come la maggior parte di UGen può generare segnali sia ad audio rate (.ar) che a control rate (.kr).

EnvGen

La struttura sintattica è la stessa di tutte le UGen mentre i principali argomenti sono:

A questo punto possiamo moltiplicare i valori in uscita da EnvGen.kr() per un segnale audio così come abbiamo fatto in precedenza per le rampe e le tecniche di smoothing:

(
SynthDef(\envi,
              {arg freq, gate=0;
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env.new([0,0.5,0.1,0],[0.01,0.2,1],\cub);
                   env = EnvGen.kr(bpf,gate,doneAction:2);
               Out.ar(0,sig*env)
               }
          ).add
)

Synth(\envi,[\freq,rrand(200,2000), \gate,1]);

Notiamo come utilizzando l'argomento doneAction:2 a ogni valutazione si crea una nuova istanza di Synth che si autodistrugge alla fine dell'inviluppo d'ampiezza permettendo una polifonia limitata solo dalla potenza del computer su cui stiamo lavorando e un risparmio dinamico della CPU (sono attivi solo i Synth che suonano).

Trigger e inviluppi

Cos'è un trigger? Un trigger nel linguaggio informatico è il momento in cui comincia una qualsiasi azione o evento come la partenza di una sola nota, di una sequenza di note o suoni, l'invio di parametri di controllo a un Synth oppure l'inizio di un inviluppo. Possiamo suddividere gli inviluppi in due categorie proprio in base al numero di trigger necessario per compiere completamente il loro corso.

In tutti i tipi di inviluppo possiamo specificare il trigger come secondo argomento di EnvGen.kr(Env, gate:1). Un qualsiasi valore maggiore di 1 fa partire l'inviluppo (note On), un valore uguale a 0 genera il messaggio di rilascio (note Off o release) mentre valori negativi forzano il rilascio come vedermo più avanti. In SuperCollider ci sono numerose Classi dedicate alla generazione di diversi tipi di inviluppi, vediamole nel dettaglio.

Inviluppi senza sostegno

Env.new()

Come abbiamo già visto, se invochiamo il metodo .new() sulla Classe Env generiamo un'istanza di un inviluppo custom ovvero che possiamo disegnare a piacere definendone singolarmente tutti i parametri (livelli, tempi_delta, curve) e che può avere o meno la fase di sostegno. Nella sua versione senza fase di sostegno i suoi argomenti sono:

Env.new([livelli], [tempi], \curva o [\curve])

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

Env.new([0.0001,1,0.65,0.8,0.0001],[0.1,0.8,0.5,1], \exp).test.plot;
Env.new([0]++({1.0.rand}!14)++[0],{1.0.rand}!15,{6.rand2}!15).test.plot;  // 16 segnenti random...

image not found

In queste righe lo assumeremo come modello degli inviluppi senza sostegno per approfondire una problematica legata al trigger. Potremmo infatti pensare che per questo tipo di inviluppi sia superfluo inviare il messaggio gate:0 in quanto terminano il loro percorso automaticamente, ma stiamo attenti perchè non è sempre così. Verifichiamolo.

(
SynthDef(\envi,
              {arg freq=890, gate=0;
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env.new([0,1,0.3,0],[0.01,0.2,3],\cub);
                   bpf.plot;
                   env = EnvGen.kr(bpf,gate,doneAction:0);     
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\gate,1);   // Suona solo alla prima valutazione

Alla prima valutazione del codice l'inviluppo è partito grazie al messaggio gate:1 e ha terminato automaticamente il suo percorso, se però proviamo ad eseguire nuovamente la riga di codice che lo triggera (inviare nuovamente gate:1) non accade nulla. Questo perchè anche gli inviluppi senza sostegno una volta ricevuto un valore maggiore di 0 che ha fatto partire il loro percorso hanno bisogno di ricevere gate:0 prima di essere triggerati nuovamente.

(
SynthDef(\envi,
              {arg freq=980, gate=0;
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env.new([0,1,0.3,0],[0.01,0.2,0.3],\cub);
                   bpf.plot;
                   env = EnvGen.kr(bpf,gate,doneAction:0);     
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\gate,1);
a.set(\gate,0);
a.set(\gate,1);
a.set(\gate,0); 

Come possiamo notare questa particolarità rende piuttosto macchinoso il controllo musicale e la programmazione di sequenze sonore con questo tipo di inviluppi. Fortunatamente possiamo ovviare facendo precedere il nome dell'argomento assegnato a gate:n da t_qualsiasicosa generando in questo modo il messaggio di gate:0 (note Off) automaticamente alla fine della rampa.

(
SynthDef(\envi,
              {arg freq=890, t_gate=0;       // t_gate al posto di gate
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env([0,1,0.3,0],[0.01,0.2,0.3],\cub);
                   bpf.plot;
                   env = EnvGen.kr(bpf,t_gate,doneAction:0);
               Out.ar(0,sig*env)
                }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\t_gate,1);

Env.pairs()

Invocando il metodo .pairs() sulla Classe Env possiamo specificare come argomenti coppie di valori inclusi in Arrays e il tipo di curva che seguono:

Env.pairs([[tempo1,livello1], [tempo2,livello2], [tempo3,livello3], [tempo4,livello4]], \exp)

Env.pairs([[0,0.001],[0.2,1],[0.5,0.6],[2,0.001]], \exp).test.plot;
Env.pairs({{1.0.rand}!2}!16,\lin).test.plot;  // 16 segnenti random...

image not found

Praticamente ogni sub-Array contiene i valori x e y che descrivono un singolo punto (nodo) sul piano cartesiano. Prestiamo attenzione che i tempi sono onsets e non tempi delta.

(
SynthDef(\envi,
              {arg freq=890, t_gate=0;
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env.pairs([[0,0],[0.01,1],[0.1,0.1],[0.3,0]], \cub);
                   bpf.plot;
                   env = EnvGen.kr(bpf,t_gate,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\t_gate,1);

Per visualizzare questi inviluppi in un EnvelopeView.new() possiamo utilizzare la stessa sintassi adottata per Env.new().

Env.xyc()

Questo tipo di inviluppo è una variante del precedente in quanto possiamo specificare curve diverse per ogni segmento. Come argomenti accetta:

Env.pairs([[tempo1,livello1,curva1], [tempo2,livello2,curva2], [tempo3,livello3,curva3], [tempo4,livello4,curva4]])

Env.xyc([[0,0.001,\exp],[0.1,0.5,\wel],[2,0.1,\sin],[3,0,\cub]]).test.plot;
Env.xyc({[10.0.rand,1.0.rand,-4.rand2]}!16).plot; // 16 segnenti random...

image not found

Anche in questo caso i tempi sono onsets e non tempi delta.

(
SynthDef(\envi,
              {arg freq=890, t_gate=0;
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env.xyc([[0,0,\lin],[0.1,0.7,\wel],[2,0.1,\sin],[3,0,\cub]]);
                   bpf.plot;
                   env = EnvGen.kr(bpf,t_gate,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\t_gate,1);

Env.triangle()

Vediamo ora alcune UGens dedicate a inviluppi tradizionali. Questa genera un inviluppo triangolare e accetta due argomenti:

Env.triangle(durata,livello)

Env.triangle(0.3,0.6).test.plot;

image not found

La durata deve essere espressa in secondi mentre il livello è l'ampiezza di picco.

(
SynthDef(\envi,
              {arg freq=890, t_ciao=0, dur=1, amp=1;
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env.triangle(dur,amp);
                   env = EnvGen.kr(bpf,t_ciao,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\t_ciao,1);

(
a.set(\dur,rrand(0.01,1),
      \amp,rand(1.0),
      \t_ciao,1)
)

Env.sine()

Una variante del precedente, cambia solo la forma mentre gli argomenti sono gli stessi:

Env.sine(durata,livello)

Env.sine(0.3,0.6).test.plot;

image not found

(
SynthDef(\envi,
              {arg freq=890, t_ciao=0, dur=1, amp=1;
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env.sine(dur,amp);
                   env = EnvGen.kr(bpf,t_ciao,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\t_ciao,1);
a.set(\dur,rrand(0.01,1),\amp,rand(1.0),\t_ciao,1);

Env.perc()

Invocando il metodo .perc() otteniamo un inviluppo percussivo. I suoi argomenti sono:

Env.perc(Dur_attacco, Dur_rilascio, livello, curva)

Env.perc(0.05,  1, 1, -4).test.plot;
Env.perc(0.001, 1, 1, -4).test.plot;
Env.perc(0.001, 1, 1, -8).test.plot;
Env.perc(1, 0.01, 1, 4).test.plot;

image not found

Facciamo attenzione perchè i tempi di attacco e di rilascio sono tempi delta e la durata totale dell'inviluppo sarà dunque:

Dur_totale = Dur_attacco + Dur_rilascio.

(
SynthDef(\envi,
              {arg freq=890, t_ciao=0, atk=0.01, dec=0.5, amp=1;
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env.perc(atk,dec,amp);
                   env = EnvGen.kr(bpf,t_ciao,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\t_ciao,1);

(
a.set(\atk,rrand(0.01,0.2),
      \dec,rrand(0.01,1),
      \amp,rand(1.0),
      \t_ciao,1)
)

Env.linen()

Invocando il metodo linen otteniamo un classico inviluppo trapezoidale. Gli argomenti sono gli stessi del precedente a cui si aggiunge solamente il tempo di sostegno:

Env.linen(Dur_attacco, Dur_rilascio, Dur_sostegno, livello, curva)

Env.linen(0.1, 0.2, 0.1, 0.6).test.plot;
Env.linen(1, 2, 3, 0.6).test.plot;
Env.linen(1, 2, 3, 0.6, \sine).test.plot;
Env.linen(1, 2, 3, 0.6, \welch).test.plot;
Env.linen(1, 2, 3, 0.6, -3).test.plot;
Env.linen(1, 2, 3, 0.6, -3).test.plot;
Env.linen(1, 2, 3, 0.6, [[\sine, \welch, \lin, \exp]]).plot; // Confronto grafico...

image not found

Anche in questo caso i tempi di attacco, sostegno e rilascio sono tempi delta e la durata totale sarà dunque:

Dur_totale = Dur_attacco + Dur_sostegno + Dur_rilascio.

(
SynthDef(\envi,
              {arg freq=890, t_ciao=0, atk=0.01, sust=0.5, dec=0.2, amp=1;
               var sig,bpf,env;
                   sig = SinOsc.ar(freq);
                   bpf = Env.linen(atk,sust,dec,amp,\welch);
                   env = EnvGen.kr(bpf,t_ciao,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\t_ciao,1);

(
a.set(\atk,rrand(0.01,0.2),
      \sust,rrand(0.01,1),
      \dec,rrand(0.01,1),
      \amp,rand(1.0),
      \t_ciao,1)
)

Inviluppi con sostegno

Env.new()

L'inviluppo custom che abbiamo già incontrato può diventare di questo tipo se aggiungiamo un argomento (nodo di sostegno) a quelli che già conosciamo: l'inviluppo ferma il suo corso in questo punto fino a quando non riceve un messggio di gate:0 come illustrato nell'immagine sottostante. Dobbiamo specificare il numero del nodo di sostegno come indice dell'Array dei livelli (partendo da 0).

image not found

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

(
a = Env.new([0,0.8,0.2,0],[0.1,0.2,3],\cub, 2);
a.plot;
a.isSustained; // Riporta se c'è un sustain o meno
)

(
SynthDef(\envi,
              {arg freq=935, gate=0;
               var sig,nodo,bpf,env;
                   sig  = SinOsc.ar(freq);
                   nodo = 2;                // (ID 0 = 0, ID 1 = 0.8, ID 2 = 0.2)
                   bpf  = Env.new([0,0.8,0.2,0],[0.1,0.2,3],\cub, nodo);  // Nodo 2
                   env  = EnvGen.kr(bpf,gate,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\gate,1); // Note on  (trigger inizio e si ferma al nodo specificato)
a.set(\gate,0); // Note off (trigger per proseguire)

Se vogliamo testare questo tipo di inviluppi invocando il metodo .test possiamo specificare un tempo delta di ritardo misurato dal trigger iniziale (note on) dopo il quale verrà automaticamente generato il messaggio di gate 0 (note off):

Env.new([0,0.5,0.05,0],[0.1,1,2], \cub, 1).test(5).plot; // dopo 5" --> gate:0

Se aggiungiamo un ulteriore argomento a Env.new() specifichiamo il nodo di loop. Possiamo utilizzare questo nodo per realizzare un loop (ritornello) tra il nodo di sostegno e questo, ovviamente il suo indice deve essere inferiore al nodo di sostegno. Il loop prosegue all'infinito fino a quando non riceve un messaggio di gate 0.

a = Env.new([0,0.7,0.1,0.15,0.1,0.3,0], [0.1,0.2,0.15,0.15,0.2,1], \cub, 2, 4).plot;

image not found

(
SynthDef(\envi,
              {arg freq=1025, gate=0;
               var sig,startloop,endloop,bpf,env;
                   sig  = SinOsc.ar(freq);
                   startloop = 2;  // punto del loop (loop node)
                   endloop   = 4;  // punto del release (release node)
                   bpf       = Env.new([0,0.7,0.1,0.15,0.1,0.3,0],
                                       [0.1,0.2,0.15,0.15,0.2,1],
                                       \cub,
                                       endloop, startloop);
                   env       = EnvGen.kr(bpf,gate,doneAction:2);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\gate,1); // Note on  (trigger inizio e si ferma al nodo specificato)
a.set(\gate,0); // Note off (trigger per proseguire)

Così come per gli inviluppi senza sostegno in SuperCollider esistono alcuni metodi dedicati che possiamo invocare per realizzare inviluppi con percorsi "tradizionali".

Env.circle()

Questo metodo serve per realizzare un inviluppo in loop esattamente come quello appena illustrato. I suoi argomenti sono:

Env.circle([livelli], [tempi], curva)

Env.circle([0.0, 0.6,0.3, 0.8,0.0], 0.02,0.2,0.01,0.3,0.2], \cub).plot;

image not found

Ci sono però tre differenze con il precedente:

(
SynthDef(\envi,
              {arg freq=1025, gate=0;
               var sig,bpf,env;
                   sig  = SinOsc.ar(freq);
                   bpf  = Env.circle([0.0, 0.3, 0.1, 0.5, 0.0],
                                     [0.02,0.2,0.01,0.3,0.2],\cub);
                   env  = EnvGen.kr(bpf,gate,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\gate,1); // trigger del loop
a.set(\gate,0); // stop del loop

Env.asr()

Assieme al successivo è uno degli inviluppi tradizionali della musica elettronica ed è l'acronimo di attack, sustain, release. I suoi argomenti sono:

Env.asr(attackT, sustainL, releaseT, curva)

dove attackT corrisponde al tempo d'attacco espresso in secondi, sustainL al livello di sostegno espresso come fattore di moltiplicazione del valore di picco e releaseT al tempo di rilascio espresso in secondi.

Env.asr(0.08, 0.4, 2, \sin).test(4).plot;

image not found

(
SynthDef(\envi,
              {arg freq=1025, gate=0;
               var sig,bpf,env;
                   sig  = SinOsc.ar(freq);
                   bpf  = Env.asr(0.08, 0.4, 2, \sin);
                   env  = EnvGen.kr(bpf,gate,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\gate,1); 
a.set(\gate,0); 

Env.adsr()

Infine il più classico degli inviluppi che possiamo considerare come il fratello maggiore del precedente, in quanto per realizzarlo dobbiamo solo aggiungere i valori di un segmento di decadimento del suono che avviene subito dopo l'attacco:

Env.adsr(attackT, decayT, sustainL, releaseT, picco, curva)

Env.adsr(0.08, 0.3, 0.4, 2, 0.6, \cub).test(1).plot;

image not found

(
SynthDef(\envi,
              {arg freq=1025, gate=0;
               var sig,bpf,env;
                   sig  = SinOsc.ar(freq);
                   bpf  = Env.adsr(0.08, 0.3, 0.4, 2, 0.6, \cub);
                   env  = EnvGen.kr(bpf,gate,doneAction:0);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);
)

a.set(\gate,1); 
a.set(\gate,0);

Fade in e fade out

In SuperCollider possiamo anche utilizzare alcune tecniche per generare diversi tipi di fade in e out automatici: In genere queste tecniche vengono utilizzate per evitare discontinuità del segnale in situazioni particolari in cui il trigger della rampa non è strettamente legato alla situazione musicale. Un primo caso è quello in cui si verifica la necessità di avere un fade in automatico alla creazione di un'istanza di Synth:

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

(
SynthDef(\envi,
              {arg freq=100;
               var sig;
                   sig  = SinOsc.ar(freq);
               Out.ar(0,sig)
               }
          ).add;
)

a = Synth(\envi);   // c'è un click dovuto alla discontinuità del segnale

(
SynthDef(\envi,
              {arg freq=300;
               var sig,fadein;
                   sig     = SinOsc.ar(freq);
                   fadein  = Line.kr(dur:3);  // oppure XLine.kr
               Out.ar(0,sig*fadein)
               }
          ).add;
)

a = Synth(\envi);

In questo caso per generare automaticamente la Rampa di fade in possiamo utilizzare due UGens che conosciamo già: Line.kr() o XLine.kr().

image not found

Nel momento in cui viene creata una nuova istanza del Synth precedente questa comincia a produrre suono ma non abbiamo alcun modo di fermarla se non attraverso il metodo .free.

(
SynthDef(\envi,
              {arg freq=300;
               var sig,fadein;
                   sig     = SinOsc.ar(freq);
                   fadein  = Line.kr(dur:3);  // oppure XLine.kr
               Out.ar(0,sig*fadein)
               }
          ).add
)

a = Synth(\envi);
a.free;

Come abbiamo potuto udire, con questa sintassi si verifica una discontinuità nel segnale e se non vogliamo udire un click dobbiamo necessariamente programmare un fade out automatico. Per farlo dobbiamo necessariamente prevedere un'inviluppo.

(
SynthDef(\envi,
              {arg freq=300,gate=0;
               var sig,fadein,fadeout;
                   sig     = SinOsc.ar(freq);
                   fadein  = Line.kr(dur:3);  // oppure XLine.kr
                   fadeout = EnvGen.kr(Env.new([1,0],[1],\cub),gate,doneAction:2);
               Out.ar(0,sig*fadein*fadeout)   // il segnale è moltiplicato sia per il fadein che per il fadeout
               }
          ).add
)

a = Synth(\envi);

Come possiamo notare questo inviluppo produce una rampa che va da 1 a 0 in un secondo con una curva cubica e possiamo triggerarlo come di consueto inviando il messaggio gate:1

image not found

Possiamo anche modificare dinamicamente il tempo di fadeout in due diversi modi:

a = Synth(\envi);
a.set(\gate,1);   // il tempo è quello previsto nella SynthDef (1 secondo)

a = Synth(\envi); 
a.set(\gate,-5);  // il tempo è di 5 secondi

a = Synth(\envi);
a.release(8);     // il tempo è di 8 secondi

Questi due modi di forzare il fadeout sono validi per tutti gli inviluppi incontrati finora. Infine in SuperCollider c'è un metodo di Env che ci consente di generare un fadeout con diversi tipi di curva:

Env.cutoff()

Possiamo considerare questo inviluppo come una versione avanzata di Decay.kr() in quanto ci permette di realizzare solo un decadimento a 0 che segue una curva a piacere. I suoi argomenti sono:

Env.cutoff(releaseTime, picco, curva)

Env.cutoff(1, 0.4, \sin).test(1).plot;

image not found

Il suo principale utilizzo avviene quando vogliamo realizzare dei fade outs da un livello prestabilito. Oltre al fatto che Decay.kr() è una UGen (Server side) mentre Env.cutoff() una BPF (Client side) la differenza principale tra i due consiste proprio nel fatto che il primo comincia il suo percorso dal livello del segnale sul quale agisce, mentre il secondo parte da un livello che abbiamo stabilito noi, indipendentemente da quello del segnale per il quale è moltiplicato.

(
SynthDef(\envi,
              {arg freq=876, gate=1;    // Necessita di un gate:1 di default
               var sig,bpf,env;
                   sig  = SinOsc.ar(freq);
                   bpf  = Env.cutoff(1, 0.4, \sin);
                   env  = EnvGen.kr(bpf,gate,doneAction:2);
               Out.ar(0,sig*env)
               }
          ).add
)

a = Synth(\envi);
a.set(\gate,0); // trigger del rilascio

Uno degli usi più tipici di queste tecniche avviene con elementi GUI:

// ------------------------------ SynthDef e Synth
(
SynthDef(\envi,
              {arg freq=300,gate=0,amp=1;
               var sig,fadein,fadeout;
                   sig     = SinOsc.ar(freq);
                   fadein  = Line.kr(dur:3);  // oppure XLine.kr
                   fadeout = EnvGen.kr(Env.cutoff(1, 1, \sin),gate,doneAction:2);
               Out.ar(0,sig*fadein*fadeout*amp)
               }
          ).add;

{a = Synth(\envi)}.defer(0.1);                  

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

w = Window.new("Volume", 200@200); // creo una finestra
w.alwaysOnTop; 
w.onClose_({a.release(2)});        // fadeout e autodistruzione del Synth quando chiuderemo la window
w.front;

e = Slider.new(w,Rect(75, 10, 50, 180)); // creo uno slider
e.value_(1);                             // con valore di default = 1
e.action_({arg i;
           a.set(\amp, i.value)          // modifico l'ampiezza con lo slider
           });
)                                        // chiudendo la finestra partirà il fadeout automatico                              

Polifonia e voice allocation

DA FAREEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE

Inviluppi dinamici

Prima di addentrarci nella descrizione delle diverse possibilità che abbiamo a disposizione per modificare dinamicamente i parametri degli inviluppi (così come abbiamo fatto per l'ampiezza) poniamo l'attenzione su tre argomenti che ci torneranno utili nello sviluppo dei temi successivi:

  1. Possiamo utilizzare un'abbreviazione sintattica che unisce idealmente EnvGen con Env trasformando una BPF in segnale:

    s.boot;
    s.scope(1);
    s.meter(1,1);
    
    (
    SynthDef(\envi,
                  {arg freq=890, t_gate=0;       
                   var sig,env;
                       sig = SinOsc.ar(freq);
                       env = Env.new([0,1,0.3,0],[0.01,0.2,0.3],\cub).kr(doneAction:0, gate:t_gate);
                   Out.ar(0,sig*env)
                   }
              ).add;
    
    {a = Synth(\envi)}.defer(0.1);  
    )
    
    a.set(\t_gate,1);
    

    Personalmente trovo questa abbreviazione sintattica più chiara del codice scritto per esteso e la adotteremo negli esempi seguenti.

  2. Nei codici di esempio precedenti e in quelli seguenti abbiamo utilizzato quasi sempre una scelta randomica dei valori (non deterministica). Questa scelta è stata fatta esclusivamente per scopi esemplificativi, tutto quanto esposto vale anche per l'utilizzo di tecniche deterministiche.

  3. Abbiamo visto come ci siano tre diversi modi di specificare i valori temporali di un inviluppo:

    • un'Array di tempi delta [1,1,1] come nel caso di Env.new, Env.perc, Env.adsr, etc.
    • un'Array di onsets [0,1,2] come nel caso di Env.pair e Env.xyc
    • la Durata totale come nel caso di Env.triangle e Env.sine

    Tra questi il modo più vicino al pensiero musicale tradizionale è quest'ultimo ovvero specificare la durata totale di un suono:

    (
    SynthDef(\envi,
                  {arg dur=0.1,t_gate=0;
                   var sig,amp;
                       sig  = SinOsc.ar(1200);
                       amp  = Env.triangle(dur).kr(0,t_gate);
                    Out.ar(0,sig*amp)
                    }
              ).add;
    
    {a = Synth(\envi)}.defer(0.1);  
    )
    
    a.set(\dur,rrand(0.01,1),\t_gate,1); 
    

    ma, siccome non tutti i metodi dedicati agli inviluppi accettano questo parametro, ricordiamo alcune utili conversioni di valori temporali tra le diverse modalità appena esposte:

    [1,2,3,4].sum             // Tempi delta --> durata totale
    [1,2,3,4].normalizeSum    // Riscala tutti i valori tra 0 e 1
    
    [1,2,3,4].integrate       // Tempi delta --> onsets
    [0]++[1,2,3,4].integrate; // aggiunge il tempo 0 (inizio
    
    [0,1,3,6,10].drop(1);     // rimuove il primo item (tempo 0)
    [1,3,6,10].differentiate  // Onsets --> tempi delta
    

Esattamente come abbiamo fatto per le ampiezze possiamo modificare dinamicamente nel tempo tutti i parametri di un invilupo utilizzando le stesse tecniche che già conosciamo.

Client side

La prima consiste nell'inviare dall'Interprete al Server i diversi parametri con il messaggio .set(). Ognuno dei tre parametri principali degli inviluppi ha delle specificità. Vediamole nel dettaglio:

Livelli

Se vogliamo controllare dinamicamente i livelli degli inviluppi inviando i valori dall'Interprete al Synth (Server) abbiamo a disposizione due tecniche (o modi di pensare) differenti, ognuna è propria di una specifica esigenza musicale:

  1. modificare dinamicamente tutti i livelli specificandoli in valori assoluti (così come abbiamo fatto finora):

    s.scope(1)
    s.meter(1,1);
    
    (
    s.waitForBoot{
    
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\envi,
                  {arg t_gate=0,liv=#[0,0.65,0.43,0];      // literal Array...
                   var sig,env;
                       sig = SinOsc.ar(986);
                       env = Env.new(liv,[0.02,0.3,0.2],\cub).kr(0,t_gate);
                    Out.ar(0,sig*env)
                    }
              ).add;
    
    {a = Synth(\envi)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("EnvelopeView", 500@400);
    w.alwaysOnTop;
    w.onClose_({e.free;w.free;r.stop;});
    w.front;
    e = EnvelopeView.new(w, Rect(10,10,480,380)).resize_(5);
    
    // ------------------------------ Sequencing
    
    r = Routine.new({
                     inf.do({var liv;
                                 liv = [0]++({rand(1.0)}!2)++[0]; // livelli random ad eccezione
                                                                  // del primo e dell'ultimo
                             a.set(\liv,liv,\t_gate,1);           // invia al Synth
    
                             {e.value_([                          // invia all'interfaccia
                                       [0]++[0.02,0.3,0.2].normalizeSum.integrate, // onsets tra 0 e 1                              
                                       liv                                         // livelli
                                       ])
                             }.defer(0);                                  
    		
                             0.6.wait // Deve essere >= della durata dell'inviluppo
                             })
                     }).reset.play;
    }
    )
    
    r.stop;
    

    Due cose da notare:

    • Nella SynthDef l'Array specificato come argomento di default è un Literal Array.

    • Se un trigger arriva prima che l'inviluppo sia finito questo riparte dal valore in cui si trova.

  2. modificare dinamicamente solo l'ampiezza di picco lasciando invariata la forma dell'inviluppo. In questo caso dobbiamo specificare tutti i livelli in valori relativi (proporzioni) con 0 come minimo e 1 come massimo per poi moltiplicarli per un fattore di ampiezza così come abbiamo fatto per gli inviluppi con forma d'onda fissa (triangolare, sinusoidale, trapezoidale, etc).

    (
    s.waitForBoot{
    
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\envi,
                  {arg t_gate=0,amp=0;
                   var sig,env;
                       sig = SinOsc.ar(1986);
                       env = Env.new([0,1,0.35,0],[0.02,0.02,0.2],\cub).kr(0,t_gate)*amp; // min=0 max=1 * ampiezza                 
                    Out.ar(0,sig*env)
                    }
              ).add;
    
    {a = Synth(\envi)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("EnvelopeView", 500@400);
    w.alwaysOnTop;
    w.onClose_({e.free;w.free;r.stop;});
    w.front;
    e = EnvelopeView.new(w, Rect(10,10,480,380)).resize_(5);
    
    // ------------------------------ Sequencing
    
    r = Routine.new({
                     inf.do({var amp=rand(1.0);
                             a.set(\amp,amp,\t_gate,1);
    
                            {e.value_([                                   
                                       [0]++[0.02,0.02,0.2].normalizeSum.integrate,                               
                                       [0,1,0.35,0]*amp                                
                                       ])
                             }.defer(0);                                  
    
                             0.6.wait
                             })
                     }).reset.play;
    }
    )
    
    r.stop;
    

Tempi

Anche per quello che riguarda i cambiamenti dinamici del tempo o dei tempi di un'inviluppo abbiamo a disposizione le stesse due modalità che abbiamo appena visto per i livelli:

  1. modificare dinamicamente tutti i tempi dei singoli nodi specificandoli in valori assoluti (così come abbiamo fatto finora): In questo caso dobbiamo però distinguere tra i diversi tipi di inviluppo:

    • Inviluppi con i tempi specificati in tempi delta come Env.new() e Env.circle():

      (
      s.waitForBoot{
      
      // ------------------------------ SynthDef e Synth
      
      SynthDef(\envi,
                    {arg t_gate = 0,
                         liv    = #[0,0.65,0.34,0.78,0],
                         delta  = #[0.1,0.3,0.4,0.5]; 
                     var sig,env;
                         sig = SinOsc.ar(986);
                         env = Env.new(liv,delta,\cub).kr(0,t_gate);
                         sig = SinOsc.ar(986);
                     Out.ar(0,sig*env)
                     }
                ).add;
      
      {a = Synth(\envi)}.defer(0.1);
      
      // ------------------------------ GUI
      
      w = Window.new("EnvelopeView", 500@400);
      w.alwaysOnTop;
      w.onClose_({e.free;w.free;r.stop;});
      w.front;
      e = EnvelopeView.new(w, Rect(10,10,480,380)).resize_(5);
      
      // ------------------------------ Sequencing
      
      r = Routine.new({
                       inf.do({var amp,delta,dur;
                                   amp   = [0.8,0.3,0.5];      //{rand(1.0)}!3; 
                                   delta = {rrand(0.01,1)}!4;
                                   dur   = delta.sum;         // Durata totale
      
                               a.set(\liv,[0]++amp++[0],      // invia i valori al Synth
                                     \delta, delta,
                                     \t_gate, 1
                                     );
      
                              {e.value_([                    // invia i valori all'interfaccia
                                         [0]++delta.normalizeSum.integrate, // onsets tra 0 e 1
                                         [0]++amp++[0]
                                         ])
                               }.defer(0);                    // AppClock
      
                              dur.wait
      	                    })
                       }).reset.play;
      }
      )
      
      r.stop
      
    • Inviluppi con i tempi specificati in onsets come Env.pairs() e Env.xyc():

      (
      s.waitForBoot{
      
      // ------------------------------ SynthDef e Synth
      
      SynthDef(\envi,
                    {arg t_gate = 0,
                         n1=#[0,0.001], // SynthDef non accetta Array 2D...
                         n2=#[0.2,1],
                         n3=#[0.5,0.6],
                         n4=#[2,0.001]; 
                     var sig,bpf,env;
                         sig = SinOsc.ar(986);
                         env = Env.pairs([n1,n2,n3,n4],\cub).kr(0,t_gate);
                     Out.ar(0,sig*env)
                      }
                ).add;
      
      {a = Synth(\envi)}.defer(0.1);
      
      // ------------------------------ GUI
      
      w = Window.new("EnvelopeView", 500@400);
      w.alwaysOnTop;
      w.onClose_({e.free;w.free;r.stop;});
      w.front;
      e = EnvelopeView.new(w, Rect(10,10,480,380)).resize_(5);
      
      // ------------------------------ Sequencing
      
      r = Routine.new({
                       inf.do({var amp,tempi,dur,xy,tempi_e;
                                   amp   = [0,0.7,0.3,0];
                                   tempi = {1.0.rand}!4;       // Array di tempi delta
                                   tempi = tempi.putFirst(0);  // primo = 0
                                   tempi_e = tempi.normalizeSum.integrate; // per visualizzazione
                                   tempi = tempi.integrate;    // Tempi delta -->  onsets
                                   dur   = tempi.last;         // Durata globale
                                   xy   = [tempi,amp].flop;    // Crea Array 2D dei nodi
                                   xy.postln;                  // Stampa
      
                               a.set(                          // invia i valori al Synth
                                     \n1, xy[0],
                                     \n2, xy[1],
                                     \n3, xy[2],
                                     \n4, xy[3],
                                     \t_gate, 1
                                     );
      
                              {e.value_([tempi_e, amp])}.defer(0); // invia i valori all'interfaccia
      
                              dur.wait
                              })
                       }).reset.play;
      }
      )
      
      r.stop
      
    • Inviluppi con i tempi specificati in durata totale come Env.triangle(), Env.sine() e Env.perc():

      (
      s.waitForBoot{
      
      // ------------------------------ SynthDef e Synth
      
      SynthDef(\envi,
                    {arg t_gate=0,
                         dur=1;
                     var sig,env;
                         sig = SinOsc.ar(986);
                         env = Env.triangle(dur).kr(0,t_gate);
                     Out.ar(0,sig*env)
                     }
                ).add;
      
      {a = Synth(\envi)}.defer(0.1);
      
      // ------------------------------ GUI
      
      w = Window.new("EnvelopeView", 500@400);
      w.alwaysOnTop;
      w.onClose_({e.free;w.free;r.stop;});
      w.front;
      e = EnvelopeView.new(w, Rect(10,10,480,380)).resize_(5);
      
      // ------------------------------ Sequencing
      
      r = Routine.new({
                       inf.do({var dur;
                               dur=rrand(0.1,1);
                               a.set(\dur, dur,\t_gate, 1);
       
                              {e.setEnv(Env.triangle(dur))}.defer(0); // .setEnv() al posto di .value_()
      
                              dur.wait
                              })
                       }).reset.play;
      }
      )
      
      r.stop
      
    • Inviluppi con i tempi specificati in valori legati alle loro caratteristiche come Env.linen(), Env.asr() e Env.adsr():

      (
      s.waitForBoot{
      
      // ------------------------------ SynthDef e Synth
      
      SynthDef(\envi,
                    {arg t_gate = 0,
                         atk=0.02,
                         sust=0.5,
                         rel=0.02,
                         amp=0;
                     var sig,env;
                         sig = SinOsc.ar(986);
                         env = Env.linen(atk,sust,rel,amp,\cub).kr(0,t_gate);
                     Out.ar(0,sig*env)
                     }
                ).add;
      
      {a = Synth(\envi)}.defer(0.1);
      
      // ------------------------------ GUI
      
      w = Window.new("EnvelopeView", 500@400);
      w.alwaysOnTop;
      w.onClose_({e.free;w.free;r.stop;});
      w.front;
      e = EnvelopeView.new(w, Rect(10,10,480,380)).resize_(5);
      
      // ------------------------------ Sequencing
      
      r = Routine.new({
                       inf.do({var atk,sust,rel,dur,amp;
                                   atk = rrand(0.01,0.2);
                                   sust= rrand(0.1,0.7);
                                   rel = rrand(0.01,0.7);
                                   dur = atk+sust+rel;
                                   amp = 0.7;
                               a.set(
                                     \atk, atk,
                                     \sust, sust,
                                     \rel, rel,
                                     \amp, amp,
                                     \t_gate, 1
                                     );
       
                              {e.value_([
                                         [0]++[atk,sust,rel].normalizeSum.integrate,
                                         [0,amp,amp,0]])
                               }.defer(0);
      
                              dur.wait
                              })
                       }).reset.play;
      }
      )
      
      r.stop
      
  2. modificare dinamicamente solo la durata totale lasciando invariata la forma dell'inviluppo. In questo caso dobbiamo specificare tutti i tempi in valori relativi (proporzionali) compresi tra 0 e 1 per poi moltiplicarli per un fattore di durata così come abbiamo fatto per le ampiezze. In alternativa se i valori specificati per qualche motivo eccedono il range possiamo invocare su un'istanza di Env il metodo .duration_(n) che riscala dinamicamente le porporzioni temporali nella durata desiderata. Nel caso si debbano specificare i tempi delta la loro somma deve essere 1 e il metodo .normalizeSum può risultare estremamente utile in quanto riscala i valori di un Array facendo si che la somma sia 1.

    [23,34,65,87].normalizeSum; // Riscalati con somma = 1
    

    Un altra possibile strategia consiste nello specificare i segmenti temporali in valori percentuali (tra 0 e 100):

    [5,45,50].normalizeSum;
    [23,45,50].normalizeSum; // anche se la somma supera 100 la converzione riporta entro 1
    

    Il codice seguente riporta due esempi esemplificativi validi per tutti i tipi di inviluppo, nel primo i valori sono tempi delta e vengono moltiplicati per la durata, il secondo sono espressi in onsets e utilizza il metodo .duration_(dur)

    (
    s.waitForBoot{
    
    var liv,delta,dur;  
    
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\envi,
                  {arg t_gate = 0,
                       liv    = #[0,0.65,0.34,0.78,0],
                       delta  = #[0.1,0.3,0.4,0.5],  // Valori proporzionali
                       dur    = 1,                   // Durata totale
                       amp    = 0;
                   var sig,env;
                       sig = SinOsc.ar(986);
                       env = Env.new(liv,delta*dur,\cub).kr(0,t_gate); // Durate dinamiche
                   Out.ar(0,sig*env)
                   }
              ).add;
    
    {a = Synth(\envi)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("EnvelopeView", 500@400);
    w.alwaysOnTop;
    w.onClose_({e.free;w.free;r.stop;});
    w.front;
    e = EnvelopeView.new(w, Rect(10,10,480,380)).resize_(5);
    
    liv   = [0,1,0.3,0.6,0];
    delta = {rrand(0.01,1)}!4;
    delta = delta.normalizeSum;            // tempi delta porporzionali
    
    a.set(\liv,liv,\delta, delta,\amp,1);  // setta i livelli, i tempi delta e l'ampiezza solo una volta
    
    // ------------------------------ Sequencing
      
    r = Routine.new({
                     inf.do({var dur;
                                 dur = rrand(0.1,1);
                                 dur.postln;
    
                             a.set(\dur,dur,\t_gate, 1); // durata totale
    
                            {e.value_([ [0]++delta.integrate, liv ])}.defer(0);                    
    
                            dur.wait
    	                    })
                     }).reset.play;
    }
    )
    
    r.stop
    
    // ------------------------------------------------------------------------------
    // ------------------------------------------------------------------------------
    
    (
    s.waitForBoot{
    
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\envi,
                  {arg t_gate = 0,dur=1,amp=0;
                   var sig,env;
                       sig = SinOsc.ar(986);
                       env = Env.pairs([[0,0],[0.01,0.51],[0.03,0.23],[0.12,0.51],
                                        [0.33,0.76],[0.59,0.47],[0.84,0.84],[1,0]],
                                        \cub
                                       ).duration_(dur).kr(0,t_gate)*amp; // metodo .duration_()
                   Out.ar(0,sig*env)
                    }
              ).add;
    
    {a = Synth(\envi)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("EnvelopeView", 500@400);
    w.alwaysOnTop;
    w.onClose_({e.free;w.free;r.stop;});
    w.front;
    e = EnvelopeView.new(w, Rect(10,10,480,380)).resize_(5);
    e.setEnv(Env.pairs([[0,0],[0.01,0.51],[0.03,0.23],[0.12,0.51],
                        [0.33,0.76],[0.59,0.47],[0.84,0.84],[1,0]],
                        \cub));
    
    // ------------------------------ Sequencing
    
    r = Routine.new({
                     inf.do({var dur;
                                 dur = rrand(1,5);
                                 dur.postln;
    
                             a.set(\dur,dur,\amp,0.7,\t_gate, 1);
    
                            dur.wait
    	                    })
                     }).reset.play;
    }
    )
    
    r.stop
    

Curve

Per quanto riguarda le curve possiamo modificare dinamicamente:

Env.newClear()

Un'altra tecnica che possiamo utilizzare per modificare dinamicamente nel tempo un inviluppo consiste nel crearne uno vuoto con un numero predefinito di nodi e passargli successivamente tutti i valori in modo dinamico modificandone la forma. L'unico limite è dato dal fatto che il numero di nodi non è modificabile dinamicamente.

Nel caso precedente la durata globale dell'inviluppo è data dalla somma dei valori dei tre segmenti e va da un minimo di 0.03 a un massimo di 1.5. Se invece volessimo specificare una durata globale e scegliere i valori dei singoli segmenti all'interno di questa, possiamo utilizzare il metodo .duration_() già illustrato in precedenza.

(
d = 0.02;                   // Definiamo una durata
y = {rand(0.5)}!3;          // Creiamo un Array di 3 valori random
x = Env.new([0.001,rand(1.0),rand(1.0),0.001],y,\cub).duration_(d); // riscaliamo
a.set(\env,x, \t_trig,1);   // Inviamolo al Synth
e.setEnv(x)                 // Visualizzazione dul GUI
)					

Infine vediamo come sia possibile creare un database di inviluppi e richiamarli dinamicamente uno alla volta.

(
var env;
    env = [Env.perc(0.1,1,0.3),
           Env.perc(1,0.1,0.7),
           Env.perc(0.3,0.5,0.4),
           Env.perc(0.9,0.01,0.7),
           Env.perc(0.01,0.2,0.9),
          ].choose;
a.set(\env,env,\t_trig,1);
e.setEnv(env)
)

Interazione e GUI

MIDI

Se vogliamo modificare dinamicamente i diversi parametri degli inviuppi interagendo con devices esterni o GUI dobbiamo:

  1. stabilire quali sono i parametri sonori o musicali (argomenti della SynthDef) che vogliamo poter controllare dinamicamente interagendo con l'interfaccia e che nel caso degli inviluppi potrebbero essere:

    • Trigger (gate:0 e gate:1)
    • Ampiezza di picco
    • Durata
    • Curva
    • I valori xy dei singoli nodi
    • ...
  2. mettere in relazione (mappare) i dati provenienti da un singolo slider (o bottone o knob o altro cc) con uno dei parametri scelti nel punto precedente, separandolo dai dati provenienti dagli altri. Per farlo dobbiamo innanzitutto monitorare su quali canali MIDI o control number (CC) arrivano i valori dei diversi fader (o knob o bottoni) nel modo che già conosciamo:

    MIDIIn.connectAll;     // 1 Connettere tutti i devices MIDI
    MIDIFunc.trace(true);  // 2 Stampare tutti i messaggi midi in entrata per verificare il numero
                           // del controller (num:) di due o più slider
    MIDIFunc.trace(false); // 3 spegnere il monitoraggio
    

    Dopodichè invece che ricevere i valori provenienti da tutti i canali e da tutti i controller come abbiamo fatto in precedenza:

    MIDIdef.cc(\mioK1, {arg ...args; args.postln})

    Possiamo separarli e filtrarli specificando due ulteriori argomenti di MIDIdef.cc():

    MIDIdef.cc(\test1, {arg ...args; args.postln}, 1);    // riceve solo i valori dal cc 1
    MIDIdef.cc(\test2, {arg ...args; args.postln}, 1, 1); // riceve solo i valori dal cc 1 e dal canale 1	
    

    A questo punto possiamo creare una MIDIdef.cc() per ogni slider (knob, etc.) che vogliamo utilizzare, specificando questi argomenti con i valori monitorati in precedenza.

I codici seguenti illustrano tre esempi significativi di questa modalità di controllo dei parametri degli inviluppi. I primi due riguardano i due diversi tipi di inviluppo (con e senza sfase di sostegno) mentre il terzo la modularità di questo processo e rappresenta un semplice esempio di live sequencing.

Inviluppi senza fase di sostegno

image not found

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

(
s.waitForBoot{

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

SynthDef(\no_sust,
              {arg t_gate=0,amp=0,dur=1,curva=1;     
               var sig,env;
                   sig = SinOsc.ar(1970);
                   env = Env.new([0,1,0.3,0],[0.01,0.2,1],          // tempi, livelli
                                  K2A.ar(curva).lag(0.2)            // curva
                                 ).duration_(K2A.ar(dur).lag(0.02)) // durata globale
                                  .kr(0,t_gate)*                    // trigger
                                  K2A.ar(amp).lag(0.02);            // ampiezza           
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\no_sust)}.defer(0.1);

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

w = Window.new("Controlli", 200@230);
w.onClose_({a.free;e.free}).front.alwaysOnTop;

MIDIIn.connectAll;

c = CheckBox.new(w,Rect(77, 10)).action_({arg i; a.set(\t_gate,i.value.asInt)});
d = StaticText.new(w,Rect(95,10, 50, 15)).string = "Gate";
MIDIdef.cc(\mioK1, {arg val; {c.valueAction_(val/127)}.defer(0)},41);    // cc number 41 (Play)

e = Slider.new(w,Rect(10, 50, 50, 170)).action_({arg i; a.set(\amp, i.value)});
f = StaticText.new(w,Rect(22, 30, 50, 15)).string = "Amp";
MIDIdef.cc(\mioK2, {arg val; {e.valueAction_(val/127)}.defer(0)},0); 

g = Slider.new(w,Rect(75, 50, 50, 170)).action_({arg i;a.set(\dur, i.value*0.9+0.1)});
h = StaticText.new(w,Rect(77, 30, 50, 15)).string = "Tempo";
MIDIdef.cc(\mioK3, {arg val; {g.valueAction_(val/127)}.defer(0)},1); 

i = Slider.new(w,Rect(135, 50, 50, 170)).action_({arg i;a.set(\curva, i.value*10+10)}); // da -5 a +5
l = StaticText.new(w,Rect(140, 30, 50, 15)).string = "Curva";
MIDIdef.cc(\mioK4, {arg val; {i.valueAction_(val/127)}.defer(0)},2);
}
)

Inviluppi con fase di sostegno

image not found

// N.B. le durate non possono essere riscalate
(
s.waitForBoot{

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

SynthDef(\sust,
              {arg gate=0,amp=0,dur=1,curva=1;     
               var sig,env;
                   sig = SinOsc.ar(1970);
                   env = Env.new([0,1,0.3,0],[0.01,0.2,1],
                                 K2A.ar(curva).lag(0.2),2
                                 ).kr(0,gate)*
                                 K2A.ar(amp).lag(0.02);
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\sust)}.defer(0.1);

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

w = Window.new("Controlli", 135@230);
w.onClose_({a.free;e.free}).front.alwaysOnTop;

MIDIIn.connectAll;

c = CheckBox.new(w,Rect(50, 10)).action_({arg i; a.set(\gate,i.value.asInt)});
d = StaticText.new(w,Rect(70,10, 50, 15)).string = "Note";
MIDIdef.cc(\mioK1, {arg val; {c.valueAction_(val/127)}.defer(0)},41);    // cc number 41 (Play)

e = Slider.new(w,Rect(10, 50, 50, 170)).action_({arg i; a.set(\amp, i.value)});
f = StaticText.new(w,Rect(22, 30, 50, 15)).string = "Amp";
MIDIdef.cc(\mioK2, {arg val; {e.valueAction_(val/127)}.defer(0)},0); 

g = Slider.new(w,Rect(75, 50, 50, 170)).action_({arg i;a.set(\curva, i.value*10+10)});
h = StaticText.new(w,Rect(77, 30, 50, 15)).string = "Curva";
MIDIdef.cc(\mioK3, {arg val; {g.valueAction_(val/127)}.defer(0)},1);
}
)

Live sequencing

image not found

(
s.waitForBoot{

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

SynthDef(\no_sust,
              {arg t_gate=0,amp=0,dur=1,curva=1;     
               var sig,env;
                   sig = SinOsc.ar(1970);
                   env = Env.new([0,1,0.3,0],[0.01,0.2,1],          // tempi, livelli
                                  K2A.ar(curva).lag(0.2)            // curva
                                 ).duration_(K2A.ar(dur).lag(0.02)) // durata globale
                                  .kr(0,t_gate)*                    // trigger
                                  K2A.ar(amp).lag(0.02);            // ampiezza           
               Out.ar(0,sig*env)
               }
          ).add;

{a = Synth(\no_sust)}.defer(0.1);

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

w = Window.new("Controlli", 200@230);
w.onClose_({a.free;e.free;r.free}).front.alwaysOnTop;

MIDIIn.connectAll;

c = CheckBox.new(w,Rect(77, 10)).action_({arg i;
	                                      if(i.value == true, {r.reset.play},{r.stop})
                                          });

d = StaticText.new(w,Rect(95,10, 100, 15)).string = "Play Seq";
MIDIdef.cc(\mioK1, {arg val; {c.valueAction_(val/127)}.defer(0)},41);    // cc number 41 (Play)

e = Slider.new(w,Rect(10, 50, 50, 170)).action_({arg i; a.set(\amp, i.value)});
f = StaticText.new(w,Rect(22, 30, 50, 15)).string = "Amp";
MIDIdef.cc(\mioK2, {arg val; {e.valueAction_(val/127)}.defer(0)},0); 

g = Slider.new(w,Rect(75, 50, 50, 170)).action_({arg i;a.set(\dur, i.value*0.9+0.1)});
h = StaticText.new(w,Rect(77, 30, 50, 15)).string = "Tempo";
MIDIdef.cc(\mioK3, {arg val; {g.valueAction_(val/127)}.defer(0)},1); 

i = Slider.new(w,Rect(135, 50, 50, 170)).action_({arg i;a.set(\curva, i.value*10+10)}); // da -5 a +5
l = StaticText.new(w,Rect(140, 30, 50, 15)).string = "Curva";
MIDIdef.cc(\mioK4, {arg val; {i.valueAction_(val/127)}.defer(0)},2);

// ------------------------------ Sequencing

r = Routine.new({
                 inf.do({
                         a.set(\t_gate,rand(2));
                         0.1.wait;
                         })
                });
}
)	

Segnali e triggers

Infine vediamo alcune strategie di programmazione per la generazione dinamica di triggers (gate:1/0) per inviluppi attraverso il monitoraggio dei valori in output di diversi tipi di segnali di controllo sia unipolari che bipolari. Due esempi:

Attraverso quest'ultima tecnica possiamo utilizzare qualsiasi tipo di segnale (eventualmente modificato e ottimizzato allo scopo) sia generato direttamente in SuperCollider che proveniente dal mondo esterno per generare il trigger di qualsiasi tipo di processo.