Rampe

Una tecnica classica per passare gradualmente da un valore a un altro nel controllo di un parametro qualsiasi di un segnale consiste nella generazione di interpolazioni o rampe.

Abbiamo già affrontato questo argomento generando 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 in SuperCollider possiamo utilizzare la UGen Line.kr() i cui argomenti sono: Line.kr(a, b, tempo)

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

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

a.set(\a,1,\b,0,\tmp,0.5); // se eseguiamo 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.

doneAction:2

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

Break Point Function (BPF)

Le UGens Line.kr() e XLine.kr() generano rampe di un solo segmento, 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

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

Info al paragrafo dedicato nella sezione GUI.

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 UGens 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 triggers 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.

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                              

Monofonia, polifonia e voice allocation

Gli inviluppi e nello specifico la loro caratteristica di poter distruggere automaticamente le istanze di Synth al termine del loro percorso attraverso l'argomento doneActrion:n sono le UGens più adatte a controllare la monofonia polifonia in SuperCollider, ottimizzando in questo modo le prestazioni del Computer.

Principalmente abbiamo a disposizione tre possibilità:

Negli esempi seguenti utilizzeremo sia controlli da tastiera MIDI che tecniche di sequencing in quanto in alcuni casi le due tecniche presentano problematiche differenti a seconda del tipo di inviluppo che utilizzeremo.

Direct voice allocation (mono)

Quando vogliamo utilizzare uno o più Synth monofonici possiamo creare un'istanza per ogni voce e assegnarla direttamente a una variabile per poterne controllare i parametri (in questo caso il Synth non si deve autodistruggere e dobbiamo specificare doneAction:0):

Inviluppi senza sostegno

(
s.boot;
s.scope(1);
s.meter(1,1);
s.plotTree;   // visualizza i nodi
MIDIIn.disconnectAll;                       
MIDIIn.connectAll;      
)

(
SynthDef(\envi,
              {arg freq=400, amp=0, t_gate=0;
               var sig,env;
                   sig = SinOsc.ar(freq);
                   env = Env.new([0,1,0.6,0],[0.01,0.2,1],\cub)
                            .kr(gate:t_gate,doneAction:0);
               Out.ar(0,sig*env*amp)
               }
          ).add;

{~synth = Synth(\envi)}.defer(0.01);        // crea una sola istanza
                        
MIDIdef.noteOn(\on,{arg vel,note;           // Interazione MIDI (solo noteon)
                    ~synth.set(\freq,note.midicps,\amp,(vel/127).pow(2),\t_gate,1)})
)

// Sequencing

b = {{~synth.set(\freq,rrand(200,2000),\amp,rand(1.0),\t_gate,1); rrand(0.2,2).wait}.loop}.fork;
b.stop;b.free;~synth.free;

Inviluppi con sostegno

(
s.boot;
s.scope(1);
s.meter(1,1);
s.plotTree;   // visualizza i nodi
MIDIIn.disconnectAll;                       
MIDIIn.connectAll;      
)

(
SynthDef(\envi,
              {arg freq=400, amp=0, gate=0;  // senza t_
               var sig,env;
                   sig = SinOsc.ar(freq);
                   env = Env.new([0,1,0.6,0],[0.01,0.2,1],\cub, 2) // Nodo di sostegno
                            .kr(gate:gate,doneAction:0);
               Out.ar(0,sig*env*amp)
                }
          ).add;

{~synth = Synth(\envi)}.defer(0.01);         // crea una sola istanza

MIDIdef.noteOn(\on,{arg vel,note;            // NoteOn
                    ~synth.set(\freq,note.midicps,\amp,(vel/127).pow(2),\gate,1)});
MIDIdef.noteOff(\off,{~synth.set(\gate,0)}); // NoteOff
)

// Sequencing

(
b = {{var pitch,vel,delta;
          pitch  = rrand(60,90);
          vel    = rrand(60,127);
          delta  = rrand(0.05,1);

          Routine.new({
                        ~synth.set(\freq,pitch.midicps,   // note on
                                   \amp,(vel/127).pow(2),
                                   \gate,1);
                        (delta*rrand(0.1,0.8)).wait;      // sustain
                        ~synth.set(\gate,0)               // note off
                        }).play;
        delta.wait}.loop
    }.fork
)
b.stop;b.free;~synth.free;

Dynamic voice allocation

Quando vogliamo realizzare una polifonia dinamica ovvero una polifonia che varia continuamente il numero di voci sovrapposte possiamo utilizzare questa tecnica che però può essere utilizzata solamente con inviluppi senza fase di sostegno e consiste nel creare un nuovo Synth ad ogni nuovo evento per poi farlo autodistruggere (doneAction:2) alla fine dell'inviluppo (generalmente d'ampiezza). In questo caso non ci dobbiamo preoccupare del numero di voci e dell'ottimizzazione della cpu.

Solo inviluppi senza sostegno

(
s.boot;
s.scope(1);
s.meter(1,1);
s.plotTree;   // visualizza i nodi
MIDIIn.disconnectAll;                       
MIDIIn.connectAll;      
)

(
SynthDef(\envi,
              {arg freq=400, amp=0, t_gate=0;   // t_gate
               var sig,env;
                   sig = SinOsc.ar(freq);
                   env = Env.new([0,1,0.3,0],[0.01,0.2,1],\cub)
                            .kr(gate:t_gate,doneAction:2);
               Out.ar(0,sig*env*amp)
                }
          ).add;

MIDIdef.noteOn(\on,{arg vel,note;               // Interazione MIDI (solo noteon)
                    Synth(\envi,[\freq,note.midicps,\amp,(vel/127).pow(2),\t_gate,1])})
)

// Sequencing

b = {{Synth(\envi,[\freq,rrand(200,2000),\amp,rand(1.0),\t_gate,1]); rrand(0.02,1).wait}.loop}.fork;
b.stop;b.free;

Static voice allocation

Quando vogliamo invece realizzare una polifonia dinamica e utilizzare inviluppi con fase di sostegno le cose si complicano in quanto dobbiamo indicizzare i messaggi di note on (gate:1) inviati per poi inviare quelli di note off (gate:0) al Synth corretto. Per farlo dobbiamo creare un Array con tanti items quanti i valori delle frequenze desiderati e mapparli per gate:1 e gate:0 - noteOn e noteOff. I dettagli sono direttamente nel codice:

Solo inviluppi con sostegno e valori in MIDI notes

(
s.boot;
s.scope(1);
s.meter(1,1);
s.plotTree;   // visualizza i nodi
MIDIIn.disconnectAll;                       
MIDIIn.connectAll;      
)

(
var notes = Array.newClear(128);   // Un Array vuoto di 128 items (tutte le note midi)

SynthDef(\envi,
              {arg freq=400, amp=0, gate=1;                       // gate e non t_
               var sig,env;
                   sig = SinOsc.ar(freq);
                   env = Env.new([0,1,0.3,0],[0.01,0.2,1],\cub,2) // Nodo di sostegno
                            .kr(gate:gate,doneAction:2);
               Out.ar(0,sig*env*amp)
               }
          ).add;

~ksynth = {arg vel, pitch;             // valori da Client
           var item = notes.at(pitch); // Richiama l'item corrispondente alla midi note

           if(item.notNil,             // se non è 'nil'

              {item.release;           // parte il release (gate:0 o note Off)
               notes.put(pitch, nil)}, // e mette 'nil' nell'Array

              {item = Synth(\envi,[\freq,pitch.midicps,     // crea un Synth (noteOn)
                                   \amp,(vel/127).pow(2)]);
               notes.put(pitch, item)}                      // lo mette nell'Array
             )
           };

MIDIdef.noteOn(\on,                         // noteOn
                   {arg vel, pitch;
                    var item = notes.at(pitch);
 
                    if(item.notNil, {item.release; notes.put(pitch, nil)});  // noteOff di sicurezza
  
                    item = Synth(\envi,[\freq,pitch.midicps,    // crea un Synth (noteOn)
                                        \amp,(vel/127).pow(2)]);
                    notes.put(pitch, item)}                     // lo mette nell'Array
               );

MIDIdef.noteOff(\off,                        // noteOff
	            {arg vel, pitch;
	             var item = notes.at(pitch);

	             if(item.notNil,{item.release;notes.put(pitch, nil)})
	             })
)

// Sequencing

(
b = Routine.new({

                 inf.do({var pitch,vel,delta;
                             pitch = rrand(60,90);
                             vel = rrand(60,127);
                             delta  = rrand(0.05,1);

                          Routine.new({
                                       ~ksynth.value(vel,pitch);    // note on
                                       (delta*rrand(0.1,0.8)).wait; // sustain
                                       ~ksynth.value(0,pitch)       // note off
                                       }).play;
                          delta.wait
                          })

	            }).play
)

b.stop;b.free;~ksynth.free;

Solo inviluppi con sostegno e valori in frequenze

(
s.boot;
s.scope(1);
s.meter(1,1);
s.plotTree;   // visualizza i nodi
MIDIIn.disconnectAll;                       
MIDIIn.connectAll;      
)

// • Cambia il numero di items dell'Array e le conversioni dei valori
// • No interazione MIDI (solo controllo da Interprete)

(
var notes = Array.newClear(15000);   // Un Array vuoto di 15000 items (tutte le frequenze)

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

~ksynth = {arg vel, pitch;
           var item = notes.at(pitch);

           if(item.notNil,
                          {item.release; notes.put(pitch, nil)},
                          {item = Synth(\envi,[\freq,pitch,        // Hz
                                               \amp, vel.pow(2)]); // 0/1
           notes.put(pitch, item)})
           };
)

// Sequencing

(
b = Routine.new({

                 inf.do({var pitch,vel,delta;
                             pitch = rrand(200,7000);
                             vel = rrand(0.1,1);
                             delta  = rrand(0.05,1);

                          Routine.new({
                                       ~ksynth.value(vel,pitch);    // note on
                                       (delta*rrand(0.1,0.8)).wait; // sustain
                                       ~ksynth.value(0,pitch)       // note off
                                       }).play;
                          delta.wait
                          })

	            }).play
)

b.stop;b.free;~ksynth.free;

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;
                         })
                });
}
)