Riempire il tempo

Come in ogni modello di spazio-tempo, ogni punto dello spazio ha quattro coordinate (x, y, z, t), tre delle quali rappresentano un punto dello spazio, e la quarta un preciso momento temporale: intuitivamente, ciascun punto rappresenta quindi un evento, un fatto accaduto in un preciso luogo in un preciso istante. Il movimento di un oggetto puntiforme è quindi descritto da una curva, con coordinata temporale crescente, detto linea di universo.   P. Davies

Ugens

Ora che sappiamo accendere il Server di SuperCollider e ricevere o inviare alle porte del computer segnali audio vediamo come generarli o elaborarli utilizzando un insieme di oggetti chiamati UGens o Unit Generator. Questi sono Classi che possono appunto generare, leggere o elaborare segnali audio, o meglio compire operazioni su flussi di numeri (numeric stream) che decrivono campioni. Cominciano sempre con una lettera maiuscola e di default sono colorate di blu, possono avere molti input ma un solo output (quelle che naturalmente avrebbereo bisogno di molti output come i panner in realtà riportano un array di UGens)

Server vs Interprete

Se volessimo azzardare un paragone con quanto illustrato nella prima Parte di questo scritto ovvero con un processo di scheduling è come se le UGens contenessero al loro interno il seguente codice:

(
t = SystemClock;
r = Routine({
             1000.do({var y;                // idealmente 'inf'
                      y = rrand(-1.0,1.0);
                      y.round(0.01).postln; // stampa il valore di ogni sample
                      (1/44100).wait        // 44.100 volte al secondo
	                  })
             });
r.play(t);
)

L'esempio precedente è la realizzazione 'Client side' di un rumore bianco (white noise) ed è puramente dimostrativo. Da questo momento in avanti infatti dobbiamo prestare molta attenzione a non confondere i processi di scheduling che sono realizzati nell'Interprete (Client side) con le UGens che "vivono" nel Server (Server side) e che possono leggere, modificare o generare due diversi tipi di segnali:

Eseguendo il codice seguente possiamo vedere una visualizzazione della differenza tra i due.

{[SinOsc.ar,SinOsc.kr]}.plot(1/50, bounds:1000@500).plotMode_(\levels).refresh;

image not found

Concludendo le UGens sono delle Classi ottimizzate per computare segnali audio, 'vivono' nel Server e possiamo crearle invocando i metodi .ar, .kr o .ir (che vedermo in seguito) sul loro nome.

Forme d'onda classiche

I tre metodi precedenti descrivono quanti valori vengono generati in un secondo mentre i nomi delle specifiche UGen indicano quale tipo di relazione intercorre fra i valori generati (forma d'onda o funzione d'onda). In seguito possiamo osservare la visualizzazione delle forme d'onda di alcuni segnali audio e di controllo classici:

(
{[SinOsc.ar,     // Oscillatore sinusoidale
  WhiteNoise.ar, // Generatore di rumore bianco
  Saw.ar,        // Generatore di onde a dente di sega
  Pulse.ar,      // Generatore di onde quadre
  Blip.ar,       // Treno d'impulsi (PulseTrain)

  LFPar.kr,      // Parabolica (Quasi-Sine)
  LFNoise0.kr,   // Generatore di valori pseudo-casuali
  LFSaw.kr,
  LFPulse.kr,
  Impulse.kr]
}.plot(1/10, bounds:1100@800).plotMode_(\plines).refresh;
)

image not found

N.B. LF = Low Frequency

Nel proseguio della trattazione ci soffermeremo su queste ed altre UGens, per ora possiamo curiosare nel consueto modo ovvero richiamando l'Help file di ognuna, nel quale possiamo trovare anche degli esempi sonori oppure ottenere un elenco di tutte le UGens andando nella finestra degli Help files e cliccando prima su Browse e poi su UGens (495):

image not found

Argomenti

Tutte le UGen possiedono degli argomenti (variabili d'istanza) che specificano i valori di alcuni parametri del segnale da generare o modificare come ad esempio frequenza, ampiezza o altro. Nonostante siano argomenti dei metodi .ar(), .kr() o .ir(), sono differenti per ogni specifica UGen sulla quale sono invocati e per conoscere quali sono dobbiamo richiamare il suo Help file.

image not found

Entreremo nello specifico ogni volta che ne incontreremo una nuova. Come esempio osserviamo quelli di SinOsc che è un oscillatore sinusoidale:

SinOsc.ar(freq:440, phase:0, mul:1, add:0);

Questi ultimi due (mul e add) sono comuni a quasi tutte le UGens e come risulta evidente dai codici precedenti possiamo specificare anche solo alcuni argomenti. Quelli non indicati assumeranno il valore di default che possiamo trovare nell'help file. Inoltre come per gli argomenti delle Classi possiamo utilizzare due sintassi differenti:

Monitors

Nel Paragrafo precedente abbiamo visto come attivare alcuni oggetti grafici che ci restituiscono informazioni riguardanti l'attività computazionale e i segnali audio attivi in entrata o in uscita dal Server in uso. Possiamo monitorare allo stesso modo le singole UGen, invocando su di esse alcuni dei metodi già incontrati. Per quanto riguarda i segnali audio ci sono tre possibili tipi di monitoraggio:

Per ognuna di queste modalità di monitoring esiste un metodo che possiamo invocare sull'output dei segnali:

{}.play

Un modo veloce per far suonare e monitorare attraverso l'ascolto una o più UGens consiste nell'includerla tra parentesi graffe (ovvero all'interno di una funzione) e invocare su di essa il metodo .play. Questa è un'abbreviazione sintattica che possiamo utilizzare per prototipare velocemente algoritmi di sintesi ma come vedremo in seguito non è il modo migliore per programmare patch informaticamente corretti e performanti.

{SinOsc.ar}.play;
{SinOsc.ar+WhiteNoise.ar}.play;

{}.scope

Per avere un monitoraggio visivo della forma d'onda (oscillogramma) del segnale in uscita di una specifica UGen (o da un network di UGens) possiamo invocare il metodo .scope():

{}.plot

Possiamo anche visualizzare in un plot i valori dell'output di una UGen invocando l'omonimo metodo su una funzione che include una UGen o un algoritmo di sintesi. In questo caso gli argomenti sono differenti rispetto al plotting di un Array. Nell'esempio seguente sono illustrati i principali, per tutti gli altri richiamare l'Help file di .plot e scegliere Function.

{Pulse.ar}.plot;
{Pulse.ar+Saw.ar}.plot(duration: 0.01 );              // Durata della finestra in secondi
{Pulse.ar*Saw.ar}.plot(bounds: Rect(0,0,500,100));    // Dimensioni grafiche finestra
{Pulse.ar*PinkNoise.ar}.plot(bounds: 500@100);        // Abbreviazione
{Pulse.ar+WhiteNoise.kr}.plot(minval: -2, maxval: 2); // Minimo e massimo
{Pulse.ar*SinOsc.ar}.plot(bounds: 500@100);           // Abbreviazione

image not found

Possiamo notare che la visualizzazione non è per nulla precisa, soprattutto all'inizio quando SuperCollider deve effettuare numerose operazioni "nascoste", ma può tornarci utile ugualmente per farci un'idea della forma d'onda di un segnale o nella ricerca di eventuali errori.

{UGen.ar.poll()}.play

Infine per un monitoraggio numerico dei valori dei singoli samples possiamo utilizzare il metodo .poll invocato direttamente su un UGen. Questo metodo stampa semplicemente nella Post window i valori in uscita. L'argomento è il numero di campioni (snapshots) per secondo (in Hertz).

{SinOsc.ar.poll(1)}.play;
{SinOsc.ar(MouseX.kr(40,10000,1).poll(10), 0, 0.1) }.play;

Teniamo presente che il metodo .poll serve solo per monitorare, non possiamo assegnare i singoli valori ad una variabile per poi riutilizzarli all'interno del codice. Per fare questo dovremo utilizzare un'altra tecnica che vedremo in un'altra sezione di questo scritto.

Synth e segnali

Abbiamo visto nel paragrafo precedente che per ottenere segnali (sia audio che di controllo) in output da una UGen una delle possibili sintassi a nostra disposizione consiste nell'includerla tra parentesi graffe come se fosse il contenuto di una funzione. Eseguendo la riga seguente potremo leggere nella post window la scritta a Function.

{SinOsc.ar};

La valutazione della riga precedente genera dunque una funzione, non un segnale. Per generare un segnale dobbiamo invocare su di essa uno dei metodi che abbiamo già incontrato.

{SinOsc.ar}.play;
{SinOsc.ar}.scope;
{SinOsc.ar}.plot;

Questa forma infatti è un'abbreviazione sintattica: ogni volta che eseguiamo un codice di questo tipo, SuperCollider compie alcune operazioni per noi, creando temporaneamente un nuovo Synth (una nuova "istanza" di Synth), derivato dalla Classe della specifica UGen e impostata sui valori di default.

Indici e Nodi

Eseguiamo più volte la riga seguente:

{SinOsc.ar}.play;

Ad ogni valutazione vedremo comparire nella Post window alcune informazioni e sentiremo l'ampiezza del suono aumentare:

Synth("temp__1" : 1000)
Synth("temp__2" : 1001)
Synth("temp__3" : 1002)
...

Queste ci dicono che a ogni valutazione SuperCollider ha creato una nuova istanza di Synth rappresentata dall'oggetto Synth(). Come possiamo notare ogni Synth() ha un argomento che indica l'etichetta (o indirizzo o nome o indice) che gli è stato assegnato automaticamente ed è diviso in due parti:

In pratica ogni nuova istanza è un nuovo Synth che esiste nel Server fino a quando non spegnamo l'audio con 'cmd+.'. Ogni istanza si chiama Node ed è indicizzata sul Server. Possiamo visualizzare in una finestra grafica tutti i nodi presenti su un Server invocando il metodo .plotTree.

s.plotTree;
{SinOsc.ar(rrand(400,1900))*SinOsc.ar}.play;

image not found

Se volessimo eliminare tutti i Synth (Nodes) da un Server:

s.freeAll;

Notiamo che all'interno di un singolo Synth possiamo avere sia una sola UGen (un segnale audio) che più UGens (più segnali audio) collegate tra loro in diversi modi attraverso le più disoarate tecniche di sintesi e/o elaborazione del segnale espresse sotto forma di algoritmi. In questo secondo caso il segnale in ouput sarà il risultato dell'algoritmo specifico. Infine in termini musicali possiamo pensare a ogni singolo Synth (o Nodo) come a una una voce monofonica.

SynthDef e Synth

Per "far suonare" SuperCollider fino a questo punto abbiamo utilizzato l'abbreviazione sintattica: {UGen.ar}.play ma il paradigma per una corretta liuteria virtuale da realizzarsi per mezzo di qualsiasi software passato o presente (e probabilmente futuro) consiste nel seguire alcuni passi derivati dalle prassi del fare musica con strumenti acustici che sono:

  1. Progettare e creare uno o più strumenti virtuali (cosa che in SuperCollider possiamo realizzare con SynthDef e Synth). Nel caso specifico di SuperCollider la struttura interna più completa di una SynthDef dovrebbe essere:

    • Nome dello strumento (indirizzo al quale inviare i parametri dall'esterno)
    • Argomenti (nomi dei parametri di controllo da ricevere dall'esterno)
    • Variabili locali (eventuali)
    • Bus In (il bus dal quale leggere eventuali segnali in entrata)
    • Algoritmo di sintesi o elaborazione del suono
    • Bus Out (il bus sul quale scrivere il segnale in uscita)

  2. Connetterli tra loro in diverse configurazioni attraverso i Bus o una matrice (che corrisponde alla definizione di un organico acustico e all'orchestrazione un brano).

  3. Controllarli inviando valori dinamicamente agli argomenti (eseguire una partitura musicale).

In SuperCollider è possibile memorizzare sul Server un modello di strumento in una SynthDef (Synth Definition) o meglio inviare al Server le istruzioni necessarie alla costruzione di un Synth. Dopo aver compiuto questa operazione da questo modello possiamo derivare uno o più strumenti virtuali (Synth) costruiti seguendo le istruzioni memorizzate in precedenza. Sfruttiamo dunque un paradigma simile al rapporto che c'è tra Classe e Istanza nei linguaggi di programmazione OOP ma che tecnicamente non è la stessa cosa. Vediamo come passare gradualmente da come abbiamo operato finora (abbreviazione sintattica {}.play) alla programmazione di Synth attraverso le SynthDef.

A questo punto credo sia importante sottolineare alcune piccole differenze che potranno tornarci utili in seguito:

Possiamo infine visualizzare in un browser grafico tutte le SynthDef presenti su un Server in un determinato momento:

SynthDescLib.global.read.browse;

image not found

Come possiamo notare sono presenti sia quelle di default che quelle aggiunte da noi (custom).