Delimitare lo spazio

Vi è un’espressione che negli ultimi decenni è apparsa sporadicamente nella letteratura scientifica attorno alla musica che credo si possa considerare emblematica di questa possibile relazione: geografia acustica. Dal punto di vista del musicologo, ritenendo che l’udito costituisca una forma di conoscenza significativa, e occupandomi professionalmente di suoni, penso, infatti, sia importante riflettere sul ruolo che il suono ha nel formare la nostra conoscenza del mondo e, in particolare, dei luoghi.   G.Giuriati

Fino a questo momento abbiamo generato e udito solo segnali monofonici ovvero che venivano riprodotti da un solo altoparlante. Nel corso di questo capitolo ci occuperemo delle principali tecniche di panning ovvero di come posizionare un segnale monofonico in uno spazio bi o tri-dimensionale delimitato in vari modi da due o più altoparlanti. Facciamo attenzione a non confondere le tecniche di pannning con quelle di spazializzazione in quanto le prime implicano solo lo spostamento del segnale in uno spazio neutro e fondamentalmente si ottengono con fadein e fadeout del segnale sincronizzati tra i diversi altoparlanti mentre le seconde vogliono simulare il movimento di una sorgente sonora virtuale in relazione ad un punto di ascolto preciso e implicano anche parametri come gain (più la sorgente si allontana e più il volume diminuisce), riverberazione dinamica (più la sorgente si allontana maggiore sarà l'ampiezza del riverbero rispetto a quella del suono diretto), assorbimento dell'aria, effetto doppler, etc.

Espansione multicanale

Cominciamo ad esplorare uno spazio sonoro soffermandoci su di una particolarità di SuperCollider ovvero l'espansione multicanale. Abbiamo visto che nella maggior parte dei casi possiamo specificare gli argomenti delle UGens sia come singoli valori che come singoli segnali audio o di controllo.

s.boot;
s.scope(8);
s.meter(8);

{SinOsc.ar(980,0,0.3)}.play                   // singolo valore
{SinOsc.ar(980,0,SinOsc.kr(1).unipolar)}.play // segnale		

Se però includiamo più UGens in un Array oppure sostituiamo il singolo argomento di una sola UGen con un Array (di valori o di segnali) SuperCollider crea tante istanze della UGen che li contiene quante gli items dell'Array e ogni singola istanza (segnale monofonico) viene scritta su un canale audio differente. Verifichiamo.

// Array di UGens:
(
{[SinOsc.ar(440),
  SinOsc.ar(550),        // possono anche essere diverse UGens
  SinOsc.ar(660),
  SinOsc.ar(880)]}.play; // 4 segnali che escono su 4 canali differenti e contigui
)                        

// Array di argomenti come valori:

{SinOsc.ar(440)}.play;
{SinOsc.ar([440,550,660,880])}.play;

// Array di argomenti come segnali:

{SinOsc.ar(mul: SinOsc.kr(1).unipolar)}.play;
{SinOsc.ar(mul: SinOsc.kr([1,0.5,0.2,0.4]).unipolar)}.play;	

Se non stiamo utilizzando una scheda audio multicanale udiremo solo i primi due oscillatori (nel caso delle freqenze: 440 e 550) in quanto la prima istanza viene scritta di default sul canale sinistro (bus 0) mentre tutte le altre (se presenti) sui canali attigui in ordine crescente.

image not found

image not found

Vediamo ora cosa succede quando utilizziamo Array di lunghezza diversa per più argomenti della stessa UGen:

(
{SinOsc.ar(freq:[440,550,660,880,990,999],
           mul: [0.2,0.6,0.4]
)}.play
)

image not found

In questo caso l'Array che specifica le ampiezze (mul:) ha meno items di quello che specifica le frequenze (freq:) e quando termina ricomincia dal primo item:

440 --> 0.2
550 --> 0.6
660 --> 0.4
880 --> 0.2
990 --> 0.6
999 --> 0.4

Tutte le UGen hanno questa caratteristica, anche gli Env:

(
SynthDef(\multi,
               {arg t_trig = 0;
                var freq, bpf, env, sig;
                    freq = #[200,974,1500,2465];
                    env = Env.perc([0.1,0.2,0.3,0.4],[0.4,0.3,0.2,0.1]).kr(gate:t_trig);
                    sig = SinOsc.ar(freq, mul: env);
				Out.ar(0,sig*0.5)
                }).add;

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

~synth.set(\t_trig,1);	

Visualizzazione multicanale

Fino a questo momento abbiamo visualizzato insiemi di segnali o inviluppi sotto forma di Array facendo sì che ognuno fosse posizionato in un proprio piano cartesiano:

image not found

image not found

(
{[SinOsc.ar(440),LFNoise0.ar(443),Saw.ar(447)]}
.plot(name:"Sig", bounds:300@200, minval:-1, maxval:1);

[Env.perc(0.1,2,0.7),Env.perc(0.5,2,0.7),Env.perc(0.7,2,0.7)]
.plot(name:"Env", bounds:300@200, minval:0, maxval:1)
)	

Se utilizziamo la notazione per l'espansione multicanale però inviuppi e segnali si comportano in modo differente:

Implosione multicanale

Possiamo sfruttare la caratteristica appena illustrata in molte situazioni musicali che implicano l'uso di diversi canali audio, alcune delle quali saranno sviluppate negli Esercizi 9. Spesso però nel programmare algoritmi di sintesi o elaborazione del suono abbiamo la necessità di effettuare l'operazione opposta, ovvero miscelare tra loro più segnali per ottenerne uno monofonico in uscita come ad esempio nella sintesi additiva. Questo ristultato può essere ottenuto con la Classe Mix.new().

s.boot;
s.scope(8);
s.meter(8);

{SinOsc.ar([600,800,1000,1200,1500], mul:0.1)}.scope;          // multicanale
{Mix.new(SinOsc.ar([600,800,1000,1200,1500], mul:0.1))}.scope; // mono	

Nel codice seguente un esempio riassuntivo con inviluppi differenti applicati a segnali differenti:

(
SynthDef(\envi, {arg t_trig = 0;
                 var freq,env,sig;
                     freq = #[200,974,1500,1800];
                     env  = Env.perc([0.1,0.2,0.3,0.4],[0.4,0.3,0.2,0.1]).kr(gate:t_trig);
                     sig  = SinOsc.ar(freq, mul:env); // multicanale
                     sig  = Mix(sig);                 // mono
                Out.ar(0,sig*0.3)
                }
        ).add;

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

)
~synth.set(\t_trig,1);	

Per generare un Mix monofonico da più istanze di una sola UGen o da più UGens differenti possiamo invocare anche la maggior parte dei metodi invocabili sugli Arrays come ad esempio .fill(). I suoi argomenti sono il numero di istanze da creare e i segnali da includere nell'Array:

(
{var n = rrand(1,85);   // Numero di istanze da creare e mixare
     n.postln;
 Mix.fill(n, {SinOsc.ar(rrand(20,20000),mul:1/n)})
}.play;
)	

Oppure l'abbreviazione sintattica del metodo n.dup():

{Mix({SinOsc.ar(rrand(200,1200),mul:1/8)} ! 8)}.play;

Approfondiremo queste ed altre tecniche nel paragrafo dedicato alla sintesi additiva (METTI LINK)

Tipologie di spazi sonori

Prima di affrontare le diverse tecniche di panning con i loro specifici controlli, credo sia necessario soffermarci sui tre possibili modi di pensare lo spazio nell'utilizzo di sistemi di diffusione stereofonici e multicanale, sopratutto in relazione al pensiero musicale:

  1. gli altoparlanti delimitano uno spazio acustico all'interno del quale posizionare le diverse sorgenti monofoniche. In questo caso devono assumere il più possibile una funzione neutra ovvero l'ascoltatore non dovrebbe percepire la loro presenza, come se fossero le pareti di una sala da concerto. Utilizzeremo principalmente questo tipo di panning quando le nostre esigenze musicali sono quelle di avere diversi personaggi sonori che si muovono virtualmente all'interno dello stesso ambiente percepito come luogo inclusivo/immersivo e non come strumento.

  2. gli altoparlanti sono uno strumento di diffusione del suono caratteristico e riconoscibile e la loro disposizione in sistema (tipicamente ma non necessariamente circolare come l'ottofonia oppure in orchestre di altoparlanti come gli acousimonium) assume una precisa valenza musicale che ai nostri giorni ha ormai sviluppato proprie tecniche strumentali storicizzate e codificate come il panning circolare, randomico, le traiettorie a spirale tipiche della diffusione su acousmonium, etc.) che rispondono a necessità estetiche e musicali molto precise come in passato è avvenuto per l'evoluzione delle tecniche esecutive degli strumenti acustici.

  3. gli altoparlanti assumono la stessa valenza dei singoli strumenti acustici, di personaggi o di sculture sonore. In questo caso la caratterizzazione musicale deve essere massima in quanto anche se inseriti in un sistema di diffusione più ampio, ogni altoparlante deve rappresentare se stesso, ed essere sempre riconoscibile sia musicalmente che come punto di diffusione del suono nello spazio, esattamente come il singolo strumento di un quartetto d'archi o le sezioni di un'orchestra sinfonica.

Ognuno di questi tre modi di pensare lo spazio può essere realizzato con i diversi sistemi di diffusione del suono, dalla monofonia alla multifonia e in SuperCollider ci sono diverse UGens specifiche per ognuno di essi.

Monofonia

Se pensiamo al mondo digitale un segnale monofonico è un un flusso di numeri (numeric stream) compresi tra +/-1.0 scritti su un singolo bus di uscita audio che, dopo essere stati convertiti in segnale analogico (flusso di elettroni liberi che scorrono in una direzione all'interno di un singolo cavo) viene diffuso da un solo altoparlante. Possiamo pensare anche al percorso inverso: quando colleghiamo un microfono ad una scheda audio o a un mixer tramite un cavo analogico il segnale che entra è monofonico e se colleghiamo più microfoni avremo un segnale mono per ognuno di loro.

Mono

In SuperCollider la maggior parte delle UGens genera o elabora un segnale monofonico che di default è scritto sul bus audio 0 (in un sistema di diffusione standard corrisponde al canale sinistro). Come abbiamo appena visto può essere anche il risultato del mixaggio di più canali audio:

s.boot;
s.scope(8);
s.meter(8);

{SinOsc.ar}.play;
{Mix.new(SinOsc.ar([440,578,897])*0.2)}.play;

image not found

image not found

In una SynthDef ad esempio possiamo stabilire su quale bus audio scrivere un segnale monofonico, specificandolo come primo argomento di Our.ar() (per ora se cambiamo dinamicamente il bus udiremo un click ma questo inconveniente sarà discusso e risolto in un paragrafo dedicato METTI LINK).

(
SynthDef(\mono,
    	      {arg bus=0;
               var sig;
                   sig = SinOsc.ar;
               Out.ar(bus, sig*0.2)}
         ).add;

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

~synth.set(\bus,rand(8));

(
r = Routine.new({
                 inf.do({
                         ~synth.set(\bus,rand(2));
                         0.5.wait
                         })
                 }).reset.play
)
r.stop;		

Possiamo in questo modo inviare contemporaneamente più segnali mono a diversi bus audio che saranno diffusi da più altoparlanti

Dual mono e multi mono

Se duplichiamo un segnale monofonico scrivendolo su due canali attigui e lo diffondiamo tramite due altoparlanti non otteniamo un segnale stereofonico ma dual-mono (due segnali monofonici uguali):

{SinOsc.ar(rrand(700,1200)!2,0,0.1)}.play; // esegui più volte...	

image not found

I sistemi di diffusione multi-monofonici si prestano a interessanti soluzioni artistico espressive nel terzo modo di pensare lo spazio tra quelli illustrati all'inizio di questo Paragrafo. Pensiamo per esempio a un ensemble di strumenti acustici elaborati in tempo reale ognuno da un proprio device con un uscita monofonica e un singolo altoparlante. Se disposti in posizioni fisse non univoche all'interno di uno spazio reale (a uno strumento corrisponde l'altoparlante che diffonde le elaborazioni di un'altro strumento) si possono realizzare giochi di specchi e rimandi sonori spaziali tra strumenti reali e i loro doppi elettroacustici molto accattivanti, facendo assumere di fatto al parametro spazio la stessa importanza degli altri parametri musicali come altezza, dinamica, timbro, etc. sul piano del linguaggio musicale e sonoro.

image not found

Stereofonia

Un sistema di diffusione stereofonico è invece formato da due altoparlanti frontali posizionati idealmente a un angolo di 60° rispetto al punto di ascolto ottimale:

image not found

E' un sistema monodimensionale frontale riconducibile principalmente alla prima tipologia di spazio tra quelle illustrate all'inizio di questo Paragrafo in quanto possiamo posizionare e muovere una o più sorgenti virtuali monofoniche (source) nel fronte stereofonico ovvero nello spazio compreso tra l'altoparlante sinistro e quello destro ma la distanza (la seconda dimensione) può essere simulata solo alle spalle della linea immaginaria che collega i due diffusori.

Stereo

Per ottenere un segnale stereofonico dobbiamo prendere un segnale monofonico e distribuire la sua ampiezza tra due canali attigui. Così facendo la somma delle ampiezze in uscita dai due altoparlanti sarà la stessa del segnale monofonico originale (mentre nel dual-mono ad esempio viene raddoppiata). In base al rapporto tra le due ampiezze in uscita dagli altoparlanti la sorgente risulterà posizionata in un punto preciso del fronte stereofonico:

s.boot;
s.scope(8);
s.meter(8);

(
SynthDef(\stereo,
              {arg sx=0.5, dx=0.5;
               var sig;
                   sig = SinOsc.ar(mul:[sx,dx]);
               Out.ar(0,sig)
               }
        ).add;

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

~synth.set(\sx,0.9,\dx,0.1); // sinistra
~synth.set(\sx,0.5,\dx,0.5); // centro
~synth.set(\sx,0.1,\dx,0.9); // destra	

Come risulta evidente dal codice precedente in un canale dobbiamo moltiplicare l'ampiezza del segnale monofonico per un fattore compreso tra 0.0 e 1.0 e nell'altro per il valore che resta ad arrivare a 1.0 (1.0-x). Se automatizziamo questa operazione riusciamo a ottimizare il controllo della posizione esprimendo un solo numero compreso tra 0.0 (sinistra) e 1.0 (destra):

image not found

(
SynthDef(\stereo,
              {arg pan=0.5;
               var sig;
                   sig = SinOsc.ar(Rand(400,2000),mul:[1-pan,pan]);
               Out.ar(0,sig*0.2)
               }
        ).add;

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

~synth.set(\pan,0);   // sinistra
~synth.set(\pan,0.5); // centro	
~synth.set(\pan,1);   // destra

// Interattivo muovendo il mouse...
{SinOsc.ar(mul:[1-MouseX.kr,MouseX.kr])}.play;

// Più sorgenti virtuali...
Synth(\stereo,[\pan,rand(1.0)]); // esegui più volte

Ascoltando attentamente però, possiamo notare che la percezione dell'intensità del suono di ogni singola sorgente alle posizioni specificate nell'esempio (sinistra, centro, destra) non è sempre uguale: quando è al centro risulta più debole rispetto a quando è ai lati. Per ovviare a questo problema dato dall'imperfetta percezione umana una delle soluzioni trovate dagli studiosi nel corso del tempo è stata quella di calcolare la radice quadrata del fattore di moltiplicazione delle ampiezze divise tra i canali. Così facendo la somma dei segnali quando è posizionata al centro eccede 1.0 (0.5.sqrt = 0.707*2 = 1.141) ma all'orecchio umano risulta più bilanciato.

(
SynthDef(\stereo,
              {arg pan=0.5;
               var sig;
                   sig = SinOsc.ar(Rand(400,2000),mul:[(1-pan).sqrt,pan.sqrt]);
               Out.ar(0,sig*0.2)
               }
        ).add;

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

~synth.set(\pan,0);   // sinistra
~synth.set(\pan,0.5); // centro	
~synth.set(\pan,1);   // destra

{SinOsc.ar(mul:[(1-MouseX.kr).sqrt,MouseX.kr.sqrt])}.play

LinPan2.ar e Pan2.ar

In SuperCollider possiamo realizzare il panning stereofonico appena illustrato con due UGens dedicate. La prima è LinPan2 che corrisponde al primo esempio (con il suono che risulta più flebile al centro):

LinPan2.ar(sig, pos, level)

(
SynthDef(\stereo,
              {var sig,pos,lev,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pos = MouseX.kr(-1,1).poll(10); // -1=sx, 0=centro,1=dx
                   lev = MouseY.kr(0.001,1).sqrt;  // 1=vicino,0.001=lontano
                   pan = LinPan2.ar(sig,pos,lev);
               Out.ar(0,pan)
               }
        ).add;

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

I suoi argomenti sono:

La seconda UGen è Pan2.kr() ed è identica a quella appena illustrata eccezione fatta per la correzione percettiva dell'ampiezza in posizione centrale. E' la UGen utilizzata comunemente per il panning stereofonico semplice.

(
SynthDef(\stereo,
              {var sig,pos,lev,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pos = MouseX.kr(-1,1).poll(10); // -1=sx, 0=centro,1=dx
                   lev = MouseY.kr(0.001,1).sqrt;  // 1=vicino,0.01=lontano
                   pan = Pan2.ar(sig,pos,lev);
               Out.ar(0,pan)
               }
        ).add;

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

Splay

In SuperCollider c'è anche una UGen particolare (Splay) che si comporta come Mix ovvero miscela tra loro Array di segnali monofonici, ma invece che produrre un segnale monofonico, posiziona in modo equidistante i singoli segnali nello spazio stereofonico partendo da sinistra:

(
SynthDef(\stereo,
              {arg sprd=1,ctr=0;
               var sig,lev=0.4,pan;
                   sig = SinOsc.ar([600,800,1000,1200,1500]);
                   pan = Splay.ar(sig,   // Array di canali mono
                                  sprd,  // spread 0 = tutti al centro
                                  lev,   // ampiezza globale (0. / 1.)
                                  ctr,   // shift dal centro
                                  true); // limiter...
               Out.ar(0,pan)
               }
        ).add;

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

~synth.set(\sprd,0);   // tutti al centro 
~synth.set(\sprd,0.5); // tra -0.5 e +0.5
~synth.set(\sprd,1);   // su tutto il fronte stereofonico

~synth.set(\ctr,0);    // shift
~synth.set(\ctr,0.5);
~synth.set(\ctr,1.5);
~synth.set(\ctr,2);    // tutti a destra
~synth.set(\ctr,-2);   // tutti a sinistra
~synth.set(\ctr,-1.5);	

image not found

Possiamo invocare anche un parente stretto del metodo .fill utilizzato con Mix: .arFill e utilizzare le possibilità già illustrate per Mix:

{Splay.arFill(8, {SinOsc.ar(rrand(200,2200),mul:1/8)})}.play;
{Splay.ar({SinOsc.ar(rrand(200,4200), mul:1/8)}!8)}.play;

Balance

Se la sorgente invece che essere un segnale monofonico è un segnale stereofonico possiamo favorire la sua posizione verso destra o verso sinistra così come abbiamo fatto in Splay con l'argomento shift impiegando la UGen Balance2.ar():

(
SynthDef(\balance,
              {arg pos=0,lev=1;
               var sig,pan;
                   sig = SinOsc.ar([440,550],0,[0.7,0.3]); // segnale stereo
                   pan = Balance2.ar(sig[0],               // canale sinistro
                                     sig[1],               // canale destro
                                     pos,
                                     lev);
               Out.ar(0,pan)
               }
        ).add;

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

~synth.set(\pos,-1.0); // solo canale sinistro
~synth.set(\pos,-0.5);
~synth.set(\pos, 0.0);
~synth.set(\pos, 0.5);
~synth.set(\pos, 1.0); // solo canale destro

Eseguendo l'esempio precedente risulta abbastanza evidente il compito e i controlli di questa UGen, sottolineiamo solamente l'accesso ai due canali separati del segnale stereofonico originale attraverso la sintassi propria degli items di un Array.

GUI per sistemi stereofonici

Se pensiamo il fronte stereofonico come un modello di spazio monodimensionale frontale o come strumento di diffusione, come quello descritto nel punto 1 tra quelle illustrate all'inizio del Paragrafo precedente per la visualizzazione e l'eventuale controllo interattivo di una sorgente con il mouse o con devices esterni, abbiamo a disposizione tre interfacce grafiche tradizionali che sono anche la rappresentazione virtuale di controlli hardware largamente diffusi:

  1. Slider orizzontale.

    image not found

    s.scope(2);
    s.meter(2);
    
    (
    s.waitForBoot{
    var inizio=0;                  // operazioni di init
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\stereo_gui,
                  {arg amp=1,pos=0.0,lev=1,smt=0.2;
                   var sig,pan;
                       sig = SinOsc.ar(Rand(400,2000));
                       pan = Pan2.ar(sig,
                                     pos.lag(smt), // .lag(n) funziona anche con i number (no sig)
                                     lev.lag(smt));
                   Out.ar(0,pan*amp)
                   }
            ).add;
    	
    {~synth = Synth(\stereo_gui)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("Pan", 200@100);
    w.alwaysOnTop;
    w.front;
    w.onClose_({~synth.free;w.free;e.free;c.free});
    
    b = StaticText.new(w, Rect(10, 5, 200, 15))               // Static text
     .string_("-1                   0                   1");
    c = NumberBox.new( w, Rect(10, 75, 180, 20)).value_(0.5); // NumberBox
    d = Slider.new(    w, Rect(10, 20, 180, 50)).value_(0.5); // Slider orizzontale
    
    // ------------------------------ Operazioni di basso livello
    
    ~ksynth = {arg pos=0,dur=1,defId=0.01,smth=0.02;          
               var range,nPassi,aPasso,dPasso;
                   range  = abs(inizio-pos);
                   nPassi = 1/defId;
                   aPasso = Array.interpolation(nPassi,0,1)
                                 .normalize(inizio,pos);       
                   dPasso = dur/nPassi;
    
    Routine.new({                                                
                 aPasso.do({arg item;
                            ~synth.set(\pos,item);                      // al Synth  
                            {c.value_(item);                            // al NumberBoxe
                             d.value_(item.linlin(-1,1,0,1))}.defer(0); // allo Slider             
                             inizio = item;                      
                            dPasso.wait  
                            })
                 }).play;
    };
    	
    d.action_({arg i;
    	       ~synth.set(\pos,i.value.linlin(0,1,-1,1)); // invia il valore al Synth
    	       c.value_(  i.value.linlin(0,1,-1,1));      // invia il valore al Number Box
               });
    
    MIDIIn.connectAll;                                
    MIDIdef.cc(\knob, {arg val;                  
                       ~synth.set(\pos,val.linlin(0,127,-1,1)); // al Synth 
    		           {c.value_(val.linlin(0,127,-1,1));       // al NumberBox
    		            d.value_(val.linlin(0,127,0,1))        // al Knob
    		            }.defer(0)
                       }, 16);                                 // dal cc 16
    }
    )
    
    // ------------------------------ Sequencing
    
    ~ksynth.value(pos:rand2(1.0).postln,dur:rrand(0.1,2));
    

    E' esattamente come quello illustrato per il controllo dell'ampiezza di un segnale, ad eccezione di due caratteristiche:

    • è orizzontale e possiamo programmarlo modificando i paraetri di Rect()
    • il range non è compreso tra 0.0 e 1.0 (quello di default degli Sliders) ma tra +/-1.0. Per modificarlo utilizziamo la sintassi:

      n.linlin(minIn, maxIn, minOut, maxOut)

      che riscala lineramente i valori n da un range compreso tra minIn e maxIn in uno compreso tra minOut e maxOut. Volendo possiamo effettuare anche riscalaggi non lineari con i metodi 'fratelli': n.linexp(), n.explin(), n.expexp(), n.lincurve() e n.curvelin(). Gli argomenti sono gli stessi ad eccezione degli ultimi due dove dobbiamo specificare anche l'indice di curva.
  2. Knob (o Dial)

    image not found

    (
    s.waitForBoot{
    var inizio=0;                  // operazioni di init
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\stereo_gui,
                  {arg amp=1,pos=0.0,lev=1,smt=0.2;
                   var sig,pan;
                       sig = SinOsc.ar(Rand(400,2000));
                       pan = Pan2.ar(sig,
                                     pos.lag(smt), // .lag(n) funziona anche con i number (no sig)
                                     lev.lag(smt));
                   Out.ar(0,pan*amp)
                   }
            ).add;
    	
    {~synth = Synth(\stereo_gui)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("Pan", 200@100);
    w.alwaysOnTop;
    w.front;
    w.onClose_({~synth.free;w.free;e.free;c.free});
    
    b = StaticText.new(w, Rect(10, 5, 180, 15))                              // Static text
     .string_("                       0");
    c = NumberBox.new( w, Rect(60, 75, 75, 20));                             // NumberBox
    d = Knob.new(    w, Rect(10, 20, 180, 50)).centered_(false).value_(0.5); // Knob
    e = StaticText.new(w, Rect(60, 55, 180, 15))                             // Static text
     .string_("-1              1");
    
    // ------------------------------ Operazioni di basso livello
    
    ~ksynth = {arg pos=0,dur=1,defId=0.01,smth=0.02;          
               var range,nPassi,aPasso,dPasso;
                   range  = abs(inizio-pos);
                   nPassi = 1/defId;
                   aPasso = Array.interpolation(nPassi,0,1)
                                 .normalize(inizio,pos);       
                   dPasso = dur/nPassi;
    
    Routine.new({                                                
                 aPasso.do({arg item;
                            ~synth.set(\pos,item);                      // al Synth  
                            {c.value_(item);                            // al NumberBox
                             d.value_(item.linlin(-1,1,0,1))}.defer(0); // allo Slider             
                             inizio = item;                      
                            dPasso.wait  
                            })
                 }).play;
    };
    	
    d.action_({arg i;
    	       ~synth.set(\pos,i.value.linlin(0,1,-1,1)); // invia il valore al Synth
    	       c.value_(  i.value.linlin(0,1,-1,1));      // invia il valore al Number Box
               });
    	
    MIDIIn.connectAll;                                
    MIDIdef.cc(\knob, {arg val;                  
                       ~synth.set(\pos,val.linlin(0,127,-1,1)); // al Synth 
    		           {c.value_(val.linlin(0,127,-1,1));       // al NumberBox
    		            d.value_(val.linlin(0,127,0,1))        // al Knob
    		            }.defer(0)
                       }, 16);                                 // dal cc 16
    }
    )
    
    // ------------------------------ Sequencing
    
    ~ksynth.value(pos:rand2(1.0).postln,dur:rrand(0.1,2));
    

    Il codice è esattamente come il precedente, sostituiamo il termine Slider con Knob e aggiungiamo il metodo .centered_(false) che posiziona automaticamente lo 0 al centro (e non a sinistra). Approfondiremo altre funzionalità e caratteristiche più avanti occupandoci di interazione.

  3. RangeSlider

    image not found

    (
    var min=0.25, max=0.5, dur=0.3;
    
    s.waitForBoot{
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\stereo_gui,
                  {arg amp=1,pos=0.0,lev=1,smt=0.2;
                   var sig,pan;
                       sig = SinOsc.ar(Rand(400,2000));
                       pan = Pan2.ar(sig,
                                     pos.lag(smt), // .lag(n) funziona anche con i number (no sig)
                                     lev.lag(smt));
                   Out.ar(0,pan*amp)
                   }
            ).add;
    	
    {~synth = Synth(\stereo_gui)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("Pan", 200@100);
    w.alwaysOnTop;
    w.front;
    w.onClose_({~synth.free;w.free;e.free;c.free;r.stop});
    
    b = StaticText.new(w, Rect(10, 5, 200, 15))    // Static text
     .string_("-1                    0                   1");
    c = NumberBox.new(w, Rect(10, 75, 180, 20));   // NumberBox
    d = RangeSlider.new(w, Rect(10, 20, 180, 50))  // RangeSlider
     .lo_(0.25).hi_(0.75);
    
    // ------------------------------ Operazioni di basso livello
    
    ~ksynth = {arg a= -0.5,b=0.5,c=0.3;   
    		       min = a;
    		       max = b;
    		       dur = c;
    		      {d.lo_(min.linlin(-1,1,0,1))  // invia allo Slider
    			    .hi_(max.linlin(-1,1,0,1))}.defer(0); 
    	      };
    	
    r = Routine.new({var val;
                     inf.do({
                             val = rrand(min, max);   // sceglie il valore nel range
                             ~synth.set(\pos, val);   // lo invia al Synth
                            {c.value_(val)}.defer(0); // lo invia al Number Box
                             dur.wait                 // delta tra i cambi
                             })
                     }).play;
    	
    d.action_({arg i;
               min   = i.lo.linlin(0,1,-1,1);      // recupera il valore min
               max   = i.hi.linlin(0,1,-1,1);      // recupera il valore max
    
               [min,max].round(0.01).postln  // stampa i valori nella Post window
               })
    }
    )
    
    // ------------------------------ Sequencing
    (
    var min, max, dur;
        min = 0-rand(1.0);
        max = rand(1.0);
        dur = rrand(0.01,1);
    
    ~ksynth.value(min,max,dur);
    [min,max,dur].round(0.01)
    )
    

    Questo tipo di Slider risulta molto utile in diverse situazioni, nello specifico del panning stereofonico possiamo impegarlo quando vogliamo delimitare un ambito all'interno del quale far compiere a SuperCollider delle scelte randomiche. Il pensiero musicale potrebbe essere riassunto in questo modo:

    voglio che la sorgente si muova randomicamente più o meno velocemente nella porzione dello spazio totale che ho a disposizione delimitata dai valori di minimo e massimo.

    Le novità sono commentate nel codice, vorrei solo sottolineare la separazione tra il routing dei valori eventualmente provenienti dall'Interprete (~ksynth={})), quello dei valori eventualmente provenienti dalla GUI (d.action_({})) e la scelta randomica del valore nella Routine infinita.

Se invece vogliamo simulare una bi-dimensionalità al fronte stereofonico aggiungendo il parametro lev come abbiamo visto in precedenza possiamo utilizzare uno Slider2D:

image not found

(
s.waitForBoot{
var xO=0,yO=1;                  // operazioni di init
// ------------------------------ SynthDef e Synth

SynthDef(\stereo_gui,
              {arg amp=1,pos=0.0,lev=1,smt=0.2;
               var sig,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pan = Pan2.ar(sig,
                                 pos.lag(smt), 
                                 lev.lag(smt));
               Out.ar(0,pan*amp)
               }
        ).add;
	
{~synth = Synth(\stereo_gui)}.defer(0.1);

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

w = Window.new("Pan", 200@220);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;e.free;c.free});

b = NumberBox.new(w, Rect(40,195,50,20)).value_(0.5);  // NumberBox X
c = NumberBox.new(w, Rect(120,195,50,20)).value_(0.5); // NumberBox Y
d = StaticText.new(w, Rect(25,198,180,15))             // Static text
 .string_("X:                  Y:");
e = Slider2D.new(w, Rect(10, 10, 180, 180))            // Slider2D
 .x_(0.5).y_(0.5);
	
// ------------------------------ Operazioni di basso livello

~ksynth = {arg x=0,y=1, dur=1,defId=0.01,smth=0.02;          
           var range,nPassi,xPasso,yPasso,dPasso;
               range  = abs(xO-x);
               nPassi = 1/defId;
               xPasso = Array.interpolation(nPassi,0,1) // Passaggi di x
                             .normalize(xO,x);
               yPasso = Array.interpolation(nPassi,0,1) // Passaggi di y
                             .normalize(yO,y);   
               dPasso = dur/nPassi;

Routine.new({                                                
             xPasso.do({arg item,id;
                        ~synth.set(\pos,item,\lev,(yPasso[id]).sqrt); // al Synth  
                        {b.value_(item);                              // al NumberBox X (+/-1)
                         c.value_((yPasso[id]).sqrt);                 // al NumberBox Y (1/0)
                         e.x_(item.linlin(-1,1,0,1))                  // allo Slider2D  (0/1)
                          .y_((1-yPasso[id]).sqrt)
                        }.defer(0);

                         xO = item;                                   // resetta all'ultimo valore
                         yO = yPasso[id];
                         dPasso.wait  
                        })
             }).play;
};
	
e.action_({arg i;
           ~synth.set(\pos,i.x.linlin(0,1,-1,1), //  al Synth
                      \lev, (1-i.y).sqrt); 
          {b.value_(i.x.linlin(0,1,-1,1));       // al Number Box X
           c.value_(1-i.y)}.defer(0);            // al Number Box Y
           });
	
MIDIIn.connectAll;                                
MIDIdef.cc(\knob, {arg val;                  
                   ~synth.set(\pos,val.linlin(0,127,-1,1)); // al Synth
                   {b.value_(val.linlin(0,127,-1,1));       // al NumberBox X (+/-1)
                    e.x_(val.linlin(0,127,0,1))             // allo Slider2D  (0/1)
                    }.defer(0)
                   }, 16);                                  // dal cc 16
MIDIdef.cc(\slid, {arg val;                  
                   ~synth.set(\lev,val.linlin(0,127,1,0));  // al Synth
                   {c.value_(1-(val/127));                  // al NumberBox Y (1/0)
                    e.y_(val.linlin(0,127,0,1))             // allo Slider2D  (0/1)
                    }.defer(0)
                   }, 0);                                   // dal cc 0
}
)

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

~ksynth.value(x:rand2(1.0),y:1,dur:rrand(0.1,2));
~ksynth.value(x:rand2(1.0),y:0,dur:rrand(0.1,2));
~ksynth.value(x:rand2(1.0),y:rand(1.0),dur:rrand(0.1,2));

Anche in questo caso il codice è molto simile ai precedenti e ormai dovremmo essere in grado di analizzarlo da soli. Le uniche novità sono la separazione dei valori .x e .y come nel caso di .hi e .lo di RangeSlider e il fatto che i valori x sono rimappati tra +/-1.0 mentre i valori y rimangono tra 0.0 e 1.0 in quanto il parametro lev è un moltiplicatore di ampiezza e per questo motivo calcoliamo la radice quadrata dei valori (n.sqrt) per simulare la legge della distanza inversa.

Movimenti dinamici

Client side

Il codice appena illustrato può essere utile per tutte le modalità di controllo da Interprete (dal codice, da GUI e da devices), utilizziamolo allora per illustrare alcune tecniche di sequencing:

Singolo valore

Quando specifichiamo dall'Interprete i parametri che descrivono una nuova posizione della sorgente nel fronte stereofonico dobbiamo specificare sempre tutti e tre i valori: (x,y,durata). Se non desideriamo il parametro relativo alla distanza basterà specificare y=1.

~ksynth.value(x:0.6,y:0.3,dur:0.4);    // tutti i parametri
~ksynth.value(rand2(1.0),rand(1.0),rand(2.0)); 
~ksynth.value(rand2(1.0),rand(1.0),0); // cambio immediato (in relazione al parametro di smoothing)

Con lo strumento specificato possiamo anche interagire sia con il mouse sulla GUI che con un controller MIDI esterno alternando le diverse tipologie di controllo.

Sequencing

Come ben sappiamo, possiamo automatizzare l'invio dei parametri nel tempo attraverso tecniche di sequencing realizzate con Routine, Task o Pattern:

Controlli multipli

Nel codice precedente potevamo già interagire con devices MIDI esterni o con il mouse sulla GUI, ma espoloriamo ora una sintassi ottimizzata per quando dobbiamo collegare a uno struento più controller MIDI con le relative istanze di oggetti GUI. Come esempio andiamo a programmare uno strumento con relativa GUI che preveda il panning di tre diversi Synth e che usi tre Knob per il controllo del pan e tre Slider verticali per la distanza:

image not found

Per prima cosa (come abbiamo già fatto in precedenza) dobbiamo trovare il numero dei controller MIDI (cc) che corrispondono ai tre knobs ed ai tre sliders:

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 slider
MIDIFunc.trace(false); // 3 Fermare il monitoring MIDI

Nel caso dei NanoKontrol2 della Korg gli slider corrispondono ai cc 0,1,2 mentre i knobs a 16,17,18. A questo punto la migliore strategia consiste nel includere le tre istanze di Synth in un Array invece che assegnarne ognuna a una variabile come abbiamo fatto finora. Così facendo rendiamo dinamico il numero di copie che desideriamo generare senza dover dichiarare e assegnare ad ogni cambio di numero una nuova variabile: basterà modificare il numero di items nel costruttore dell'Array:

(
SynthDef(\stereo,
              {arg pos=0,dist=1,smt=0.2,amp=0.2;
               var sig,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pan = Pan2.ar(sig,
                                 pos.lag(smt),
                                 dist.lag(smt));
               Out.ar(0,pan*amp)
               }
        ).add;

n = 3;                                                       // Numero di Synth

{a = Array.fill(n, {Synth(\stereo,[\amp,0.2])})}.defer(0.5); // Array di Synth
)

Ora che abbiamo generato l'Array e lo abbiamo assegnato a una variabile possiamo sfruttare tutte le operazioni eseguibili su questo tipo di data come accedere ad ogni singolo Synth (singola istanza - singolo item) semplicemente richiamandone l'indice corrispondente con il metodo Array.at() o la sua versione Java Array[id], oppure richiamarli tutti una alla volta in sequenza (iter) con un loop realizzato da Array.do({})

a.at(0);     // Richima la prima istanza di Synth
a[1];        // Richiama la seconda
a.clipAt(5); // Richiama la terza...

a.do({arg i; i.postln}); // iterazione sequenziale

Infine possiamo notare dal codice seguente come in questi casi non ci sia separazione tra il blocco di codice di creazione degli strumenti e GUI e quello di gestione del controllo (che è interattivo).

(
s.waitForBoot{var n,w,a,b,c,d,e; // variabili locali

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

SynthDef(\stereo,
              {arg x=0,y=0,smt=0.2,amp=0.2;
               var sig,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pan = Pan2.ar(sig,
                                 x.lag(smt),
                                 y.lag(smt));
               Out.ar(0,pan*amp)
               }
        ).add;

n = 3;                                            // Numero di Synth

{a = Array.fill(n, {Synth(\stereo)})}.defer(0.5); // Array di Synth

// ------------------------------ GUI (per interazione)

w = Window.new("Knob", 200@210);
w.alwaysOnTop;
w.front;
w.onClose_({n.do({arg i; a[i].free})});     // Iterazione per '.free'

// ------ Knobs
w.view.decorator= FlowLayout(w.view.bounds, // FlowLayout
                             10@10,         // lo spazio orizzontale e verticale dai margini della finestra
                             30@5);         // lo spazio orizzontale e verticale tra i knobs
b = Array.fill(n, {Knob.new(w, Rect.new(50@50)).centered_(false) // Array di Knob
                                               .mode_(\horiz)
                                               .valueAction_(0.5)});
// ------ Number boxes
w.view.decorator= FlowLayout(w.view.bounds,
                             10@51,           
                             30@5);            
c = Array.fill(n, {NumberBox.new(w, Rect.new(0,0,40,20))});

// ------ Sliders
w.view.decorator= FlowLayout(w.view.bounds,
                             10@80, // lo spazio orizzontale e verticale dai margini della finestra
                             30@5); // lo spazio orizzontale e verticale tra gli sliders
d = Array.fill(n, {Slider.new(w, Rect.new(0,0,40,100)).valueAction_(0)});

// ------ Number boxes
w.view.decorator= FlowLayout(w.view.bounds,
                             10@181, // lo spazio orizzontale e verticale dai margini della finestra
                             30@5);  // lo spazio orizzontale e verticale tra gli sliders
e = Array.fill(n, {NumberBox.new(w, Rect.new(0,0,40,20))});

// ------------------------------ Azioni di basso livello
	
n.do({arg synthN;                       // Iterazione per action sui Knob
      b[synthN].action_({arg x;
			             a[synthN].set(\x,x.value.linlin(0,1,-1,1)); // ai Synth
                         c[synthN].value_(x.value.linlin(0,1,-1,1))  // ai Nbox
                         });
      });
	
n.do({arg synthN;                       // Iterazione per action sugli Sliders
      d[synthN].action_({arg y;
                         a[synthN].set(\y, y.value); // ai Synth
                         e[synthN].value_(y.value)   // ai Nbox
                         })
      });
	
MIDIIn.connectAll;

n.do({arg i;                            // Iterazione per collegamenti MIDI
      MIDIdef.cc("knob"++n,             // un nome diverso per ogni controller "knob0", "knob1" e "knob2"
                  {arg val;
                   {b[i].valueAction_(val/127)}.defer(0)}, // invia all'istanza di knob definita da i
                   i+16);                                  // specifica i cc number da 16 a 18
	  });
n.do({arg i;
	  MIDIdef.cc("slider"++n+3, {arg val;{d[i].valueAction_(val/127)}.defer(0)},i); // da 0 a 2
	  })
}
)


Nel codice precedente abbiamo adottato la tecnica degli Array anche per gli oggetti nell'interfaccia grafica e per i collegamenti MIDI. I singoli passaggi sono specificati come commenti nelle righe di codice e potete capirli da una semplice analisi sintattica. Specifichiamo infine che l'interazione con il mouse sulla GUI di un Knob è possibile in tre opzioni differenti:

b.mode_(\horiz); // i valori cambiano muovendo il mouse orizzontalmente
b.mode_(\vert);  // i valori cambiano muovendo il mouse verticalmente
b.mode_(\round); // i valori cambiano muovendo il mouse seguendo il profilo

Server side

Segnali

Possiamo controlllare il panning anche attraverso segnali di controllo e questo è il motivo per il quale in precedenza abbiamo scelto di uniformare tutti i valori in un ambito compreso tra +/-1.0 in quanto come ben sappiamo l'ampiezza dei segnali audio rientra in questi valori.

Come abbiamo accennato in precedenza i parametri controllabili dinamicamente dall'interprete in questo caso sono differenti da quelli che abbiamo già incontrato e sono:

Per una visualizzazione dinamica delle posizioni su un interfaccia grafica possiamo monitorare i segnali con un'oscilloscopio o con i PPM di servizio generati all'inizio dei codici. Di seguito un codice riassuntivo di tutto quanto appena esposto.

(
s.waitForBoot{
SynthDef(\stereo_sig,
              {arg tipo=0,vel=1,sprd=1,initpos=0,dist=1,smt=0.2;
               var sig,ksigs,pos,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pos = Select.kr(tipo,[LFTri.kr(vel,initpos,sprd),
                                         SinOsc.kr(vel,initpos,sprd),
                                         LFNoise0.kr(vel,sprd),
                                         LFNoise1.kr(vel,sprd),
                                         LFNoise2.kr(vel,sprd)]);
                   pos.scope;   // visualizza il segnale di controllo in verde
                   pan = Pan2.ar(sig,pos,dist.lag(smt));
               Out.ar(0,pan)
               }
        ).add;

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

~synth.set(\tipo,0);           // cambia la forma d'onda
~synth.set(\vel, 1);           // cambia la velocità di 1 ciclo in Hz (solo andata o ritorno)
~synth.set(\sprd,1);           // cambia la larghezza del fronte stereofonico
~synth.set(\initpos,pi);       // cambia la posizione iniziale e il verso
~synth.set(\dist,0.01,\smt,5); // cambia la distanza

Infine possiamo costruire anche una GUI sia per visualizzare i movimenti della sorgente che per controllare i diversi parametri nei modi che già conosciamo:

image not found

Per farlo dobbiamo però modificare leggermente la SynthDef per permetterci di recuperare i valori dei segnali di controllo dal Server all'Interprete attraverso una combinazione di due UGens: SendReply.kr() e OSCFunc.new({}):

(
SynthDef.new("invio",{var sig;
                          sig  = SinOsc.ar(1);
                      SendReply.kr(
                                   Impulse.kr(2), // rata di campionamento (Hz)
                                   '/sig',        // indirizzo
                                   sig);          // segnale da campionare
                      Out(0,sig)}
).add;

{Synth(\invio)}.defer(0.1);

OSCFunc.new({arg msg;     // funzione da eseguire ad ogni messaggio OSC
             msg.postln}, // recupera il messaggio OSC
             '/sig',      // indirizzo dal quale leggere
             s.addr);     // Server dal quale leggere (indirizzo IP)
)

Come possiamo evincere dal codice precedente:

(
s.waitForBoot{
var w,a,b,c,d;                  // variabili locali

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

SynthDef(\stereo_sig,
              {arg tipo=0,vel=1,sprd=1,initpos=0,dist=1,smt=0.2;
               var sig,ksigs,pos,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pos = Select.kr(tipo,[LFTri.kr(vel,initpos,sprd),
                                         SinOsc.kr(vel,initpos,sprd),
                                         LFNoise0.kr(vel,sprd),
                                         LFNoise1.kr(vel,sprd),
                                         LFNoise2.kr(vel,sprd)]);
                   pos.scope;   // visualizza il segnale di controllo in verde
                   SendReply.kr(Impulse.kr(50), '/pos', pos); // Invia al Client
                   pan = Pan2.ar(sig,pos,dist.lag(smt));
               Out.ar(0,pan)
               }
        ).add;

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

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

w = Window.new("Pan", 255@130);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;e.free;c.free});

a = PopUpMenu(w,Rect.new(10,10,100,20));
a.items = ["Triangolo","Sinusoide","Random 1","Random 2","Random 3"];
b = NumberBox.new(w,Rect.new(115,10,40,20))
 .value_(1);
c = NumberBox.new(w,Rect.new(180,10,40,20));
 .value_(1);
StaticText.new(w, Rect.new(158, 12, 20, 20))
          .string_("Vel");
StaticText.new(w, Rect.new(222, 12, 20, 20))
          .string_("Dist");
StaticText.new(w, Rect(10, 40, 255, 15))
          .string_("-1                          0                         1");
d = Slider.new(w, Rect(10, 55, 230, 50)); // Slider orizzontale

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

a.action_({arg tipo;
	       ~synth.set(\tipo,tipo.value)
	       });
b.action_({arg vel;
	  	   ~synth.set(\vel, vel.value)
	       });
c.action_({arg dist;
	  	   ~synth.set(\dist, dist.value)
	       });

OSCFunc.new({arg msg; defer{d.value_(\pan.asSpec.unmap(msg[3]))}},'/pos', s.addr)
}
)

Inviluppi

Per controllare posizione e distanza dinamicamente, possiamo utilizzare anche qualsiasi tipo di segnale di controllo generato da inviluppi a condizione che i valori dei livelli siano compresi tra +/-1. In questo modo possiamo disegnare qualsiasi tipo di movimento senza dover programmare le Routines di controllo che abbiamo visto nei controlli da Interprete, e abbiamo un'accuratezza a control rate e non dipendente dallo scheduler. Quando possibile questo metodo è da preferire a quello illustrato in precedenza che sarà comunque utile in svariate situazioni.

image not found

(
s.waitForBoot{
var w,b,d;     // variabili locali

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

SynthDef(\penvi,
              {arg t_gate=0;
               var sig,envpos,envdist,pan;
                   sig     = SinOsc.ar;
                   envpos  = Env.new([0,-1,0.7,0],[0.3,0.2,2],\lin).kr(0,t_gate);
                   envdist = Env.new([1,0.01,1],[3,3],\cub).kr(0,t_gate);
                   pan     = Pan2.ar(sig,envpos,envdist);
                   SendReply.kr(Impulse.kr(50), '/pos', envpos);
                   SendReply.kr(Impulse.kr(50), '/dis', envdist); 
               Out.ar(0,pan)
               }
          ).add;


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

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

w = Window.new("Pan", 255@130);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;e.free;c.free});
	
StaticText.new(w, Rect(86, 12, 255, 15))
          .string_("Dist");
b = NumberBox.new(w,Rect.new(115,10,40,20));

StaticText.new(w, Rect(10, 40, 255, 15))
          .string_("-1                         0                         1");
d = Slider.new(w, Rect(10, 55, 230, 50)); // Slider orizzontale

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

OSCFunc.new({arg msg; defer{d.value_(\pan.asSpec.unmap(msg[3]))}},'/pos', s.addr);
OSCFunc.new({arg msg; defer{b.value_(\pan.asSpec.unmap(msg[3]))}},'/dis', s.addr);
}
)

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

~synth.set(\t_gate,1);

Mouse

Così come abbiamo fatto per l'ampiezza possiamo controllare il panning stereofonico anche con il mouse mappando l'asse x dello schermo sulla posizione e l'asse y sulla distanza:

(
SynthDef(\pmouse,
              {var sig,pos,dist,pan;
                   sig  = SinOsc.ar;
                   pos  = MouseX.kr(-1,1);
                   dist = MouseY.kr(1,0.001); // vicino in basso
                   pan  = Pan2.ar(sig,pos,dist);
               Out.ar(0,pan)
               }
          ).add;


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

In questo caso non abbiamo generato una GUI in quanto lo spazio di azione è dato dallo schermo del computer.

Quadrifonia

Oltre a sistemi sistemi di diffusione del suono monofonici o stereofonici utilizzati prevalentemente per la fruizione musicale (e multimediale) di largo consumo (radio, televisione, sistemi Hi-Fi, videogiochi, etc.) oppure per concerti e spettacoli pop, rock e jazz, nel corso del secolo scorso alcuni compositori di musica colta prima e i produttori di sistemi di diffusione per l'audio cinematografico in seguito hanno sviluppato per ragioni differenti numerose tecnologie per la diffusione multicanale ovvero attraverso più altoparlanti disposti in vari modi. La maggior parte di queste tecnologie erano (e sono in quanto la ricerca in questo ambito è ancora molto attiva) quasi tutte volte alla ricostruzione di uno spazio acustico virtuale atto a fornire un'esperienza d'ascolto di tipo immersivo sviluppata a 360° attorno all'ascoltatore. Anche in questi casi sono validi i diversi modi di pensare lo spazio esposti all'inizio del Paragrafo precedente che si differenziano ora anche nell'utilizzo di UGens dedicate. Così come abbiamo fatto per la stereofonia, nei prossimi sottoparagrafi ci occuperemo solamente delle principali tecniche di panning multicanale realizzabili in SuperCollider, tralasciando diverse tecniche di spazializzazione che abbiamo a disposizione ai nostri giorni come gli acousmonium, la registrazione e diffusione bineaurale, la wave field synthesis, vbap, i sistemi ambisonic e gli array o cupole di altoparlanti. Chi volesse approfondire queste tecniche può trovare un valido supporto introduttivo nel Capitolo dedicato scritto da Marije A. J. Baalman e Scott Wilson del "The SuperCollider book" (METTI LINK AL PDF). Il più tradizionale tra i sistemi di diffusione multicanale è la quadrifonia ovvero il delimitare la spazio acustico all'interno del quale si trova il pubblico con quattro altoparlanti posti idealmente agli angoli di un quadrato immaginario come illustrato nell'immagine seguente:

image not found

Possiamo notare come la numerazione degli altoparlanti (L1 - L4) può essere effettuata in due modi: per coppie stereofoniche (a sinistra) oppure circolare (a destra). Notiamo anche come in questo caso i bus di uscita da SupercCollider (da 0 a 3) rimangono invariati in entrambe le numerazioni. Esattamente come abbiamo fatto per il fronte stereofonico, dovremo quindi suddividere l'ampiezza di ogni sorgente (segnale monofonico) non su due ma su quattro altoparlanti. Per farlo possiamo considerare il quadrato delimitato dagli altoparlanti come un piano cartesiano sul quale andiamo a specificare ogni punto come coordinate x/y comprese tra 0.0 e 1.0 Il valore di x rappresenterà la posizione sul fronte stereofonico mentre il valore di y la profondità:

image not found

Il calcolo delle ampiezze di ogni altoparlante relative alla posizione della sorgente è illustrato nell'immagine seguente e può tornarci utile nell'impiego di softwares differenti da SuperCollider:

image not found

Pan4.ar()

Nel caso volessimo pensare lo spazio come nella tipologia descritta al punto 1 all'inizio del Paragrafo precedente potremmo utilizzare una UGen dedicata: Pan4.ar() che ha come argomenti gli stessi parametri appena esposti ed effettua automaticamente il calcolo delle ampiezze relative alla posizione:

Pan4.ar(sig, x, y, level)

Come per Pan2.ar() i valori di x e y devono essere espressi in un ambito compreso tra +/- 1.0 mentre l'argomento level anche se presente in questo caso non è utilizzato per simulare la distanza ma per fare degli eventuali bilanciamenti o compensazioni di ampiezza tra sorgenti diverse presenti nello stesso spazio acustico virtuale.

image not found

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

(
s.waitForBoot{

SynthDef(\quadri,
              {arg x=0,y=0,smt=0.2;
               var sig,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pan = Pan4.ar(sig,x.lag(smt),y.lag(smt));
               Out.ar(0,pan)
               }
        ).add;

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

~synth.set(\x,0,\y,0);   // centro
~synth.set(\x,-1,\y,1);  // L1 - bus 0
~synth.set(\x,0, \y,1);  // centro al fondo
~synth.set(\x,1, \y,1);  // L2 - bus 1
~synth.set(\x,1, \y,0);  // centro a destra
~synth.set(\x,1,\y,-1);  // L4 - bus 3
~synth.set(\x, 0, \y,-1);// centro di fronte
~synth.set(\x,-1,\y,-1); // L3 - bus 2
~synth.set(\x, -1, \y,0);// centro a sinistra

~synth.set(\x,0,\y,0);   // centro
~synth.set(\x,rand2(1.0),\y,rand2(1.0));	

Se invece vogliamo pensare lo spazio come al punto 2 tra quelli descritti all'inizio del Paragrafo precedente ovvero alla quadrifonia come specifico sistema di diffusione vale tutto quanto diremo nel paragrafo seguente riguardo pentafonia, esafonia e ottofonia anche se quattro diffusori soli non si prestano particolarmente a tecniche di panning circolare o dedicato.

GUI per la quadrifonia

Per quanto riguarda la quadrifonia realizzata con la UGen Pan4.ar() dove come abbiamo appena visto le posizioni della sorgente sono specificate sotto forma di coordinate cartesiane (x/y) entro i limiti di +/-1.0:

image not found

image not found

possiamo utilizzare come GUI Slider2D.ar()) mappando le coordinate in modo diretto (x --> x e y --> y) riscalando in modo corretto i valori.

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

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

SynthDef(\stereo_gui,
              {arg amp=1,x=0,y=0,lev=1,smt=0.2;
               var sig,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pan = Pan4.ar(sig,
                                 x.lag(smt), 
                                 y.lag(smt));
               Out.ar(0,pan*amp)
               }
        ).add;
	
{~synth = Synth(\stereo_gui)}.defer(0.1);

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

w = Window.new("Pan", 200@220);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;e.free;c.free});

b = NumberBox.new(w, Rect(40,195,50,20)).value_(0);  // NumberBox X
c = NumberBox.new(w, Rect(120,195,50,20)).value_(0); // NumberBox Y
d = StaticText.new(w,Rect(25,198,180,15))            // Static text
 .string_("X:                  Y:");
e = Slider2D.new(w,  Rect(10, 10, 180, 180))         // Slider2D
 .x_(0.5).y_(0.5);
	
// ------------------------------ Operazioni di basso livello

~ksynth = {arg x=0,y=1, dur=1,defId=0.01,smth=0.02;          
           var range,nPassi,xPasso,yPasso,dPasso;
               range  = abs(xO-x);
               nPassi = 1/defId;
               xPasso = Array.interpolation(nPassi,0,1) // Passaggi di x
                             .normalize(xO,x);
               yPasso = Array.interpolation(nPassi,0,1) // Passaggi di y
                             .normalize(yO,y);   
               dPasso = dur/nPassi;

Routine.new({                                                
             xPasso.do({arg item,id;
                        ~synth.set(\x,item,\y,yPasso[id]);  // al Synth (+/-1)  
                         {b.value_(item);                   // al NumberBox X (+/-1)
                          c.value_(yPasso[id]);             // al NumberBox Y (+/-1)
                          e.x_(item.linlin(-1,1,0,1))       // allo Slider2D  (0/1)
                           .y_(yPasso[id].linlin(-1,1,0,1))
                         }.defer(0);

                         xO = item;                         // resetta all'ultimo valore
                         yO = yPasso[id];
                         dPasso.wait  
                        })
             }).play;
};
	
e.action_({arg i;
           ~synth.set(\x,i.x.linlin(0,1,-1,1),            //  al Synth (+/-1)
                      \y,i.y.linlin(0,1,-1,1)); 
          {b.value_(i.x.linlin(0,1,-1,1));                // al Number Box X (+/-1)
           c.value_(i.y.linlin(0,1,-1,1))}.defer(0);      // al Number Box Y (+/-1)
           });
	
MIDIIn.connectAll;                                
MIDIdef.cc(\knob, {arg val;                  
                   ~synth.set(\x,val.linlin(0,127,-1,1)); // al Synth (+/-1)
                   {b.value_(val.linlin(0,127,-1,1));     // al NumberBox X (+/-1)
                    e.x_(val.linlin(0,127,0,1))           // allo Slider2D  (0/1)
                    }.defer(0)
                   }, 16);                                // dal cc 16
MIDIdef.cc(\slid, {arg val;                  
                   ~synth.set(\y,val.linlin(0,127,-1,1)); // al Synth
                   {c.value_(val.linlin(0,127,-1,1));     // al NumberBox Y (1/0)
                    e.y_(val.linlin(0,127,0,1))           // allo Slider2D  (0/1)
                    }.defer(0)
                   }, 0);                                 // dal cc 0
}
)

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

~ksynth.value(rand2(1.0),rand2(1.0),rand(2.0));

~ksynth.value(0,0,0);  // centro
~ksynth.value(-1,1,0); // L1 - bus 0     
~ksynth.value(0,1,0);  // centro al fondo
~ksynth.value(1,1,0);  // L2 - bus 1
~ksynth.value(1,0,0);  // centro a destra
~ksynth.value(1,-1,0); // L4 - bus 3
~ksynth.value(0,-1,0); // centro di fronte
~ksynth.value(-1,-1,0);// L3 - bus 2
~ksynth.value(-1,0,0); // centro a sinistra

Movimenti dinamici

Client side

Il codice appena illustrato può essere utile per tutte le modalità di controllo da Interprete (dal codice, da GUI e da devices), utilizziamolo allora per illustrare alcune tecniche di sequencing.

Singolo valore

Per quanto riguarda l'invio di singoli valori vale quanto detto nel paragrafo corrispondente riguardante il panning stereo, ovvero possiamo specificare movimenti in qualsiasi punto nello spazio attraverso tre valori (x, y, durata):

(x:tra +/- 1.0,y:tra +/- 1.0, dur:tempo)

~ksynth.value(0,0,0.5);// centro
~ksynth.value(x:-1,y:1,dur:2); // L1 - bus 0     
~ksynth.value(0,1,2);  // centro al fondo
~ksynth.value(1,1,2);  // L2 - bus 1
~ksynth.value(1,0,2);  // centro a destra
~ksynth.value(1,-1,2); // L4 - bus 3
~ksynth.value(0,-1,2); // centro di fronte
~ksynth.value(-1,-1,2);// L3 - bus 2
~ksynth.value(-1,0,2); // centro a sinistra

~ksynth.value(0,0,0.5);// centro
~ksynth.value(rand2(1.0),rand2(1.0),rrand(0.1,2))

Sequencing

Vale quanto illustrato per i singoli valori e per il panning stereofonico.

Devices OSC

Al giorno d'oggi esistono in commercio numerose interfacce touch screen attraverso le quali possiamo controllare dinamicamente con le dita la posizione (i valori x/y) di una o più sorgenti virtuali su un piano cartesiano tramite messaggi OSC. Entreremo nel dettaglio di questi argomenti nel Capitolo su segnali e controlli (METTI LINK) mentre in questo paragrafo per semplicità utilizzeremo messaggi OSC inviati da un patch di Max in quanto le procedure e la sintassi sono le stesse.

  1. Scarichiamo e lanciamo il patch di Max
  2. In SuperCollider così come abbiamo fatto per i controller MIDI monitoriamo su quale porta OSC sono trasmessi i dati in ingresso (recvPort) e annotiamo il numero di porta sul quale avviene la trasmissione.:
    OSCFunc.trace(true);  // legge tutti i messaggi OSC in ingresso e riporta i dati nella Post window
                          // muovere il cursore sul patch di Max
    OSCFunc.trace(false); // Termina il monitoraggio 
    
  3. In Max specifichiamo come come argomento di udpsend un IP: 127.0.0.1 (che significa sullo stesso computer) e come porta il numero di porta che abbiamo annotato al punto precedente (57121) che corrispnde alla porta dell'Interprete di SuperCollider.
  4. Modifichiamo gli argomenti di OSCFunc.new() specificando l'indirizzo IP e la porta sulla quale entrano i messaggi OSC da Max (l'istanza di NetAddr(127.0.0.1, 57110) che leggiamo nella Post Window dopo address facendo attenzione che cambia quando arrivano messaggi OSC:
    a = OSCFunc.new({arg msg; msg.postln},'/pos', NetAddr.new("127.0.0.1",57876));
  5. Infine sostituiamo il codice dedicato al ricevere valori MIDI con quello dedicato a ricevere valori OSC, separiamo i valori x/y, eventualmente li riscaliamo entro range corretti (in questo caso i valori in ingresso sono tra 0 e 127) e li inviamo sia all'interfaccia grafica che al Synth:
    (
    s.waitForBoot{
    var xO=0,yO=0;                  // operazioni di init
    // ------------------------------ SynthDef e Synth
    
    SynthDef(\stereo_gui,
                  {arg amp=1,x=0,y=0,lev=1,smt=0.2;
                   var sig,pan;
                       sig = SinOsc.ar(Rand(400,2000));
                       pan = Pan4.ar(sig,
                                     x.lag(smt), 
                                     y.lag(smt));
                   Out.ar(0,pan*amp)
                   }
            ).add;
    	
    {~synth = Synth(\stereo_gui)}.defer(0.1);
    
    // ------------------------------ GUI
    
    w = Window.new("Pan", 200@220);
    w.alwaysOnTop;
    w.front;
    w.onClose_({~synth.free;w.free;e.free;c.free});
    
    b = NumberBox.new(w, Rect(40,195,50,20)).value_(0);  // NumberBox X
    c = NumberBox.new(w, Rect(120,195,50,20)).value_(0); // NumberBox Y
    d = StaticText.new(w, Rect(25,198,180,15))           // Static text
     .string_("X:                  Y:");
    e = Slider2D.new(w, Rect(10, 10, 180, 180))          // Slider2D
     .x_(0.5).y_(0.5);
    	
    // ------------------------------ Operazioni di basso livello
    
    ~ksynth = {arg x=0,y=1, dur=1,defId=0.01,smth=0.02;          
               var range,nPassi,xPasso,yPasso,dPasso;
                   range  = abs(xO-x);
                   nPassi = 1/defId;
                   xPasso = Array.interpolation(nPassi,0,1) // Passaggi di x
                                 .normalize(xO,x);
                   yPasso = Array.interpolation(nPassi,0,1) // Passaggi di y
                                 .normalize(yO,y);   
                   dPasso = dur/nPassi;
    
    Routine.new({                                                
                 xPasso.do({arg item,id;
                            ~synth.set(\x,item,\y,yPasso[id]);  // al Synth (+/-1)  
                             {b.value_(item);                   // al NumberBox X (+/-1)
                              c.value_(yPasso[id]);             // al NumberBox Y (+/-1)
                              e.x_(item.linlin(-1,1,0,1))       // allo Slider2D  (0/1)
                               .y_(yPasso[id].linlin(-1,1,0,1))
                             }.defer(0);
    
                             xO = item;                         // resetta all'ultimo valore
                             yO = yPasso[id];
                             dPasso.wait  
                            })
                 }).play;
    };
    	
    e.action_({arg i;
               ~synth.set(\x,i.x.linlin(0,1,-1,1),            //  al Synth (+/-1)
                          \y,i.y.linlin(0,1,-1,1)); 
              {b.value_(i.x.linlin(0,1,-1,1));                // al Number Box X (+/-1)
               c.value_(i.y.linlin(0,1,-1,1))}.defer(0);      // al Number Box Y (+/-1)
               });
    	
    OSCFunc.new({arg msg;
                 ~synth.set(\x, msg[1].linlin(0,127,-1,1),  // al Synth
                            \y, msg[2].linlin(0,127,-1,1));
                 {e.x_(msg[1]/127);                         // all'interfaccia
                  e.y_(msg[2]/127);
    			  b.value_(msg[1].linlin(0,127,-1,1));                
                  c.value_(msg[2].linlin(0,127,-1,1))
    		}.defer(0);
    	         },'/pos', NetAddr.new("127.0.0.1",57876));
    }
    )
    

Server side

Segnali

Se vogliamo utilizzare segnali di controllo possiamo modificare il codice realizzato per il panning stereofonico, aggiungendo un secondo segnale di controllo per il parametro y con i propri argomenti specifici e sostituendo la GUI per la visualizzazione con uno Slider2D:

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

(
s.waitForBoot{
var w,d,a;                  // variabili locali

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

SynthDef(\quad_sig,
              {arg tipoX=1,velX=0.5,sprdX=1,initposX= 0,
                   tipoY=1,velY=0.5,sprdY=1,initposY= 0.5pi,
                   smt=0.2;
               var sig,ksigs,x,y,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   x   = Select.kr(tipoX,[LFTri.kr(velX,initposX,sprdX),
                                          SinOsc.kr(velX,initposX,sprdX),
                                          LFNoise0.kr(velX,sprdX),
                                          LFNoise1.kr(velX,sprdX),
                                          LFNoise2.kr(velX,sprdX),
			                              ]);
                   y   = Select.kr(tipoY,[LFTri.kr(velY,initposY,sprdY),
                                          SinOsc.kr(velY,initposY,sprdY),
                                          LFNoise0.kr(velY,sprdY),
                                          LFNoise1.kr(velY,sprdY),
                                          LFNoise2.kr(velY,sprdY),
			                              ]);
                   SendReply.kr(Impulse.kr(50), '/pos', [x,y]); // Invia Slider2D
                   pan = Pan4.ar(sig,x,y);
               Out.ar(0,pan)
               }
        ).add;

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

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

w = Window.new("Pan", 210@210);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;d.free;a.free});

d = Slider2D.new(w, Rect(5, 5, 200, 200));          // Slider2D

OSCFunc.new({arg msg;
             {d.x_(msg[3].linlin(-1,1,0,1));     // solo all'interfaccia
              d.y_(msg[4].linlin(-1,1,0,1))}.defer(0);
              },'/pos', s.addr);
}
)

~synth.run(false);

Nel codice sottostante alcune combinazioni tipiche della musica elettronica:

~synth = Synth(\quad_sig,[\tipoX, 3, \tipoY, 3]).run;        // Random
~synth.run(false);
~synth = Synth(\quad_sig,[\initposX,0,\initposY,0.5pi]).run; // Senso orario
~synth.run(false);
~synth = Synth(\quad_sig,[\initposX,0.5pi,\initposY,0]).run; // Senso anti-orario
~synth.run(false);
~synth.set(\tipoX,rand(4),\tipoY,rand(4)).run;               // Combinazioni di segnali
~synth.run(false);
~synth.set(\tipoX,1,\tipoY,1,\velX,rrand(0.2,1),\velY,rrand(0.2,1)).run; // Combinazioni di velocità

Infine un controllo misto con una GUI dove un Knob controlla la velocità di rotazione mentre uno slider la distanza dal centro:

(
s.waitForBoot{
var w,d,a;                  // variabili locali

// ------------------------------ SynthDef e Synth
	
SynthDef(\quad_sig,
              {arg tipoX=1,velX=0.5,sprdX=1,initposX= 0,
                   tipoY=1,velY=0.5,sprdY=1,initposY= 0.5pi,
                   smt=0.2;
               var sig,ksigs,x,y,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   x   = Select.kr(tipoX,[LFTri.kr(velX,initposX,sprdX),
                                          SinOsc.kr(velX,initposX,sprdX),
                                          LFNoise0.kr(velX,sprdX),
                                          LFNoise1.kr(velX,sprdX),
                                          LFNoise2.kr(velX,sprdX),
			                              ]);
                   y   = Select.kr(tipoY,[LFTri.kr(velY,initposY,sprdY),
                                          SinOsc.kr(velY,initposY,sprdY),
                                          LFNoise0.kr(velY,sprdY),
                                          LFNoise1.kr(velY,sprdY),
                                          LFNoise2.kr(velY,sprdY),
			                              ]);
                   SendReply.kr(Impulse.kr(50), '/pos', [x,y]); // Invia Slider2D
                   pan = Pan4.ar(sig,x,y);
               Out.ar(0,pan)
               }
        ).add;

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

// ------------------------------ GUI
		
w = Window.new("Pan", 270@210);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;b.free;c.free;d.free});

b = Knob.new(w,     Rect(210, 5, 50, 50));   // Knob
c = Slider.new(w,   Rect(210, 55, 50, 150)); // Slider
d = Slider2D.new(w, Rect(5, 5, 200, 200));   // Slider2D
	
// ------------------------------ Operazioni di basso livello

b.action_({arg i; ~synth.set(\velX,i.value.linlin(0,1,0.1,5),  // solo al Synth
                             \velY,i.value.linlin(0,1,0.1,5))});
c.action_({arg i; ~synth.set(\sprdX,i.value,\sprdY,i.value)});
	
OSCFunc.new({arg msg;
                    {d.x_(msg[3].linlin(-1,1,0,1));            // solo all'interfaccia
                     d.y_(msg[4].linlin(-1,1,0,1))}.defer(0);
                     },'/pos', s.addr);
	
MIDIIn.connectAll;                                
MIDIdef.cc(\knob, {arg val;                  
                   ~synth.set(\velX,val.linlin(0,127,0.1,5),  // al Synth
                              \velY,val.linlin(0,127,0.1,5)); 
                   {b.value_(val.linlin(0,127,0,1))}.defer(0) // al Knob                 
                   }, 16);                                    // dal cc 16
MIDIdef.cc(\slid, {arg val;                  
                   ~synth.set(\sprdX,val.linlin(0,127,0,1),   // al Synth
                              \sprdY,val.linlin(0,127,0,1));
                   {c.value_(val.linlin(0,127,0,1))}.defer(0) // allo Slider
                   }, 0);                                     // dal cc 0
	
}
)

Inviluppi

Anche per quanto riguarda gli inviluppi possiamo modificare il codice già illustrato per il panning stereofonico:

(
s.waitForBoot{
var w,b,d;     // variabili locali

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

SynthDef(\penvi,
              {arg t_gate=0;
               var sig,x,xbpf,xsenv,y,ybpf,ysenv,pan;
                   sig = SinOsc.ar;

                   x   = Env.newClear(4);          // Crea un inviluppo vuoto di 4 nodi
                   xbpf= \x.kr(x.asArray);         // Crea un controllo dell'inviluppo
                   xsenv= EnvGen.kr(xbpf, t_gate); // Genera l'inviluppo

                   y   = Env.newClear(4);
                   ybpf= \y.kr(y.asArray);
                   ysenv= EnvGen.kr(ybpf, t_gate);

                   pan = Pan4.ar(sig,xsenv,ysenv);
			       SendReply.kr(Impulse.kr(50), '/pos', [xsenv,ysenv]);
               Out.ar(0,pan)
               }
          ).add;


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

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

w = Window.new("Pan", 210@210);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;d.free;a.free});

d = Slider2D.new(w, Rect(5, 5, 200, 200)); // Slider2D

OSCFunc.new({arg msg;
                    {d.x_(msg[3].linlin(-1,1,0,1));            
                     d.y_(msg[4].linlin(-1,1,0,1))}.defer(0);
                     },'/pos', s.addr);
}
)

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

(
h = 2;                                     // Definiamo una durata
u = {[{rand2(1.0)}!4, {rand(0.5)}!3]}!2;   // generiamo i valori di x e y
u.postln;                                  // stampa i valori
x = Env.new(u[0][0],u[0][1]).duration_(h); 
y = Env.new(u[1][0],u[1][1]).duration_(h); 
[x,y].plot;                               // Plotter
~synth.set(\x, x, \y, y, \t_gate,1);      // li inviamo al Synth
)

Mouse

Il codice per il controllo del pan quadrifonico con il mouse è molto semplice:

(
SynthDef(\pmouse,
              {var sig,x,y,pan;
                   sig = SinOsc.ar;
                   x   = MouseX.kr(-1,1);
                   y   = MouseY.kr(-1,1); // vicino in basso
                   pan = Pan4.ar(sig,x,y);
               Out.ar(0,pan)
               }
          ).add;

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

Anche in questo caso non è necessaria una GUI in quanto lo spazio è delimitato dallo schermo del computer.

Sistemi circolari

Un sistema di diffusione del suono che rappresenta ormai uno degli standard multicanale è quello composto da un numero variabile di altoparlanti disposti in modo circolare attorno al pubblico. Può essere un particolare tipo di quadrifonia oppure se gli altoparlanti sono cinque pentafonia, sei esafonia, sette eptafonia, otto ottofonia e via dicendo fino a a 128 canali. Questi tipi di sistemi possono essere controllati in SuperCollider attraverso una sola UGen: PanAz.ar() indipendentemente dal numero di altoparlanti impiegato e sottointende principalmente il modo di pensare lo spazio descritto nel punto 2 tra quelli esposti all'inizio del Paragrafo precedente ovvero pensando il sistema di diffusione sonora come un vero e proprio strumento musicale con le sue caratteristiche e le sue tecniche strumentali. In seguito esamineremo le tipologie di diffusione più utilizzate, ma i principi espressi valgono anche per tutte le altre configurazioni da 3 a 128 canali.

Pentafonia, esafonia e ottofonia

Utilizziamo queste tre configurazioni ampiamente diffuse per descrivere nel dettaglio i parametri espressi come argomenti di PanAz.ar(). In questo caso la posizione della sorgente non è specificata attraverso coordinate cartesiane (x/y) come per Pan4.ar() ma segue un sistema derivato dalle coordinate polari dove lo spazio non è delimitato da un quadrato ma da un cerchio e sia gli altoparlanti che i bus audio non seguono una disposizione a coppie stereofoniche come nel caso della quadrifonia appena descritta ma circolare:

image not found

Nel dettaglio gli argomenti sono:

PanAz.ar(n_chans, sig, pos, level, width, orientation)

Per quanto riguarda l'ottofonia che è diventato ormai il sistema di diffusione standard per la musica elettroacustica vale tutto quanto riportato per pentafonia ed esafonia, basterà solo cambiare il primo argomento di PanAz.ar() definendo correttamente il numero di altoparlanti utilizzato. Nelle immagini seguenti alcune configurazioni tipiche con i codici più propri per controllare lo spazio:

image not found

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

(
s.waitForBoot{
SynthDef(\ottof,
              {arg pos= 0,lev=1,dist=1,orient=0.5,smt=0.2;
               var nchans=8,sig,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pan = PanAz.ar(nchans,
                                  sig,
                                  pos.lag(smt),
                                  lev.lag(smt),
                                  dist.linexp(0,1,nchans,2).lag(smt),
                                  orient);
               Out.ar(0,pan)
               }
        ).add;
{~synth = Synth(\ottof,[\orient,0.5])}.defer(0.1);
}
)

~synth.set(\pos,0);   // fondo centro (bus 0/1)
~synth.set(\pos,0.25);// fondo destra (bus 1/2)
~synth.set(\pos,0.5); // laterale destra (bus 2/3)
~synth.set(\pos,0.75);// frontale destra (bus 3/4)
~synth.set(\pos,1);   // frontale centro (bus 4/5)
~synth.set(\pos,1.25);// frontale sinistra (bus 5/6)
~synth.set(\pos,1.5); // laterale sinistra (bus 6/7)
~synth.set(\pos,1.75);// fondo sinistra (bus 7/0)
~synth.set(\pos,2);   // fondo centro (bus 0/1)	

image not found

~synth = Synth(\ottof,[\orient,0.0]);

~synth.set(\pos,0.00, \dist, 1); // bus 0
~synth.set(\pos,0.25, \dist, 1); // bus 1
~synth.set(\pos,0.50 ,\dist, 1); // bus 2 
~synth.set(\pos,0.75, \dist, 1); // bus 3	
~synth.set(\pos,1.00, \dist, 1); // bus 4
~synth.set(\pos,1.25, \dist, 1); // bus 5
~synth.set(\pos,1.50, \dist, 1); // bus 6
~synth.set(\pos,1.75, \dist, 1); // bus 7
~synth.set(\pos,2.00, \dist, 1); // bus 0

GUI per sistemi circolari

Per visualizzare la sorgente nei sistemi di diffusione circolare come quelli appena esposti possiamo utilizzare Slider2D.ar(). Partiamo da alcune considerazioni:

  1. Abbiamo visto che PanAz.ar() accetta posizioni espresse in un sistema simile a quello delle coordinate polari dove un valore indica il raggio di un ipotetico cerchio (ρ - rho) mentre un altro valore l'angolo in gradi tra 0° e 360° (θ - theta) e che in un sistema esafonico preso come esempio può essere rappresentato nel modo seguente:

    image not found

  2. Slider2D.ar() invece accetta valori espressi in coordinate cartesiane (x/y) e la differenza tra i due sistemi ci costringe a:

    • Specificare le posizioni in valori compresi tra 0 e 2 per quanto riguarda il θ (theta) e tra 0 e 1 per quanto riguarda il ρ (rho) come stabilito in precedenza.
    • Inviare questi valori a PanAz.ar() come illustrato in precedenza.
    • Convertire i valori da coordinate polari a cartesiane.
    • Inviare a Slider2D.ar() i valori convertiti.
  3. Per convertire le coordinate da polari a cartesiane dobbiamo esprimere le posizioni (θ - theta) di un punto sull'angolo giro (la circonferenza nell'immagine precedente) in valori compresi tra 0π e 2π:

    image not found

    In SuperCollider dobbiamo allora:

    • aggiungere il π ai valori di θ (theta) che già utilizziamo per PanAz.ar()
    • traslare il punto 0π di -90° sottraendo 0.5 in quanto abbiamo stabilito che la posizione 0.0 deve coincidere con il punto centrale al fondo (a metà tra bus 0 e bus 1 o coincidente con bus 0 a seconda dell'argomento orientation di PanAz.ar())

      image not found

    • rendere negativo il π per invertire il verso dell'incremento dei valori da antiorario a orario.
      (
      var theta,radianti;
      theta    = 0;   // testare con valori compresi tra 0 e 2
      radianti = theta - 0.5 * -pi
      )
      

      image not found

    • applicare le formule di conversione da coordinate polari a cartesiane:

      x = ρ*cos(θ)
      y = ρ*sin(θ)

      (
      a = {arg theta=0,rho=1;
           var x,y;
      
           x = rho * cos(theta -0.5* -pi); // tra +/- 1
           y = rho * sin(theta -0.5* -pi); // tra +/- 1
      
      ["x: "++x.round(0.01),"y: "++y.round(0.01)]     // stampa
      }
      )
      a.value(0,1);
      

      ottenendo i valori x/y che possiamo inviare a Slider2D.ar().

      image not found

Il codice per generare l'interfaccia è lo stesso usato in precedenza per la quadrifonia, l'unica differenza consiste nel fatto che in questo caso a causa delle peculiarità del tipo di panning non è possibile interagire con il mouse sullo Slider2D ma solo visualizzare la posizione della sorgente. Possiamo invece utilizzare una combinazione di 2 GUI: Knob (per il theta) e Slider (per il rho) sia per l'interazione con il mouse che per la visualizzazione dei valori provenienti da controller MIDI o OSC esterni:

image not found

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

(
s.waitForBoot{
var w,b,c,d,e,f,g,thetaO=0,rhoO=1;             // variabili locali

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

SynthDef(\esaf,
              {arg pos= 0,lev=1,dist=1,orient=0.5,smt=0.2;
               var nchans=6,sig,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pan = PanAz.ar(nchans,
                                  sig,
                                  pos.lag(smt),
                                  lev.lag(smt),
                                  dist.linexp(0,1,nchans,2).lag(smt),
                                  orient);
               Out.ar(0,pan)
               }
        ).add;

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

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

w = Window.new("Pan", 250@220);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;b.free;c.free;d.free;e.free;f.free;g.free});

b = NumberBox.new(w, Rect(48, 195, 50, 20)).value_(0);   // NumberBox theta
c = NumberBox.new(w, Rect(140, 195, 50, 20)).value_(0);  // NumberBox rho
d = StaticText.new(w, Rect(10, 198, 180, 15))            // Static text
 .string_("theta:                  rho:");
e = Slider2D.new(w, Rect(10, 10, 180, 180))              // Slider2D
 .x_(0.5).y_(1);
f = Knob.new(w, Rect.new(195,10,50,50));
g = Slider.new(w, Rect.new(195,90,50,100));

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

~ksynth = {arg theta=0, rho=1, dur=1,defId=0.01,smth=0.2;
           var range,nPassi,thetaPasso,rhoPasso,dPasso;
               range  = abs(thetaO-theta);
               nPassi = 1/defId;
               thetaPasso = Array.interpolation(nPassi,0,1) // Passaggi di theta
                                 .normalize(thetaO,theta);
               rhoPasso = Array.interpolation(nPassi,0,1)   // Passaggi di rho
                               .normalize(rhoO,rho);
               dPasso = dur/nPassi;

Routine.new({
             thetaPasso.do({arg item,id;
                            ~synth.set(\pos,item,\dist,rhoPasso[id]);  // al Synth (+/-1)
                             {b.value_(item);                          // alle diverse GUI
                              c.value_(rhoPasso[id]);                  

                              e.x_((rhoPasso[id] * cos(item -0.5* -pi)).linlin(-1,1,0,1)) 
                               .y_((rhoPasso[id] * sin(item -0.5* -pi)).linlin(-1,1,0,1));
                              f.value_(item);
                              g.value_(rhoPasso[id]);
                              }.defer(0);
				
                             thetaO = item;                         // resetta all'ultimo valore
                             rhoO   = rhoPasso[id];
                             dPasso.wait
                            })
             }).play
           };

f.action_({arg val;
           thetaO = val.value;
           ~synth.set(\pos,thetaO*2-1);                      // al Synth
           {b.value_(thetaO*2-1);                            // alle diverse GUI
            c.value_(rhoO);
            e.x_((rhoO * cos(thetaO*2-1 -0.5* -pi)).linlin(-1,1,0,1));
            e.y_((rhoO * sin(thetaO*2-1 -0.5* -pi)).linlin(-1,1,0,1))
            }.defer(0)
           });

g.action_({arg val;
          rhoO = 1-val.value;
          ~synth.set(\dist,rhoO);                           // al Synth
          {b.value_(thetaO*2-1);                            // alle diverse GUI
           c.value_(rhoO);
           e.x_((rhoO * cos(thetaO*2-1 -0.5* -pi)).linlin(-1,1,0,1));
           e.y_((rhoO * sin(thetaO*2-1 -0.5* -pi)).linlin(-1,1,0,1))
           }.defer(0)
          });

MIDIIn.connectAll;
MIDIdef.cc(\knob, {arg val;
                   thetaO = val;
                   ~synth.set(\pos,thetaO/127*2-1);         // al Synth
                   {b.value_(thetaO/127*2-1);               // alle diverse GUI
                    c.value_(rhoO/127);
                    e.x_((rhoO/127 * cos(thetaO/127*2-1 -0.5* -pi)).linlin(-1,1,0,1));
                    e.y_((rhoO/127 * sin(thetaO/127*2-1 -0.5* -pi)).linlin(-1,1,0,1));
                    f.value_(thetaO/127);
                    }.defer(0)
                   }, 16);                                  // dal cc 16

MIDIdef.cc(\slid, {arg val;
                   rhoO = val;
                   ~synth.set(\dist,rhoO/127);              // al Synth
                   {b.value_(thetaO/127*2-1);               // alle diverse GUI
                    c.value_(rhoO/127);
                    e.x_((rhoO/127 * cos(thetaO/127*2-1 -0.5* -pi)).linlin(-1,1,0,1));
                    e.y_((rhoO/127 * sin(thetaO/127*2-1 -0.5* -pi)).linlin(-1,1,0,1));
                    g.value_(rhoO/127);
                    }.defer(0)
                   }, 0);                                   // dal cc 0
}
)

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

~ksynth.value(0.00,1,1);
~ksynth.value(0.25,1,1);

a = SystemClock.sched(0, {~ksynth.value(rand2(1.0),rand(1.0),0.3); 0.3});
a.clear;

Movimenti dinamici

Client Side

Il codice appena illustrato può essere utile per tutte le modalità di controllo da Interprete (dal codice, da GUI e da devices), utilizziamolo allora per illustrare alcune tecniche di sequencing.

Singolo valore

Per quanto riguarda l'invio di singoli valori vale quanto detto nel paragrafo corrispondente riguardante il panning stereo, ovvero possiamo specificare movimenti in qualsiasi punto nello spazio attraverso tre valori (theta, rho, durata):

(theta:tra +/- 1.0,rho:tra 0 e 1, dur:tempo)

~ksynth.value(0.00,1,1);
~ksynth.value(0.25,1,1);
~ksynth.value(0.50,1,1);
~ksynth.value(0.75,1,1);
~ksynth.value(1.00,1,1);
~ksynth.value(1.25,1,1);
~ksynth.value(1.50,1,1);
~ksynth.value(1.75,1,1);
~ksynth.value(2.00,1,1);

~ksynth.value(0,0,1);
~ksynth.value(rand2(1.0),rand(1.0),rrand(0.1,2))

Sequencing

Vale quanto illustrato per i singoli valori e per il panning stereofonico e quadrifonico.

Server side

Segnali

Se vogliamo utilizzare segnali di controllo possiamo anche in questo caso modificare il codice realizzato per il panning stereofonico e quadrifonico, specificando un segnale di controllo bipolare (+/- 1.0) per il parametro pos e valori fissi o un secondo segnale di controllo riscalato in un ambito compreso tra il numero di canali e 2 per il parametro dist:

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

(
s.waitForBoot{
var w,d,a;                  // variabili locali

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

SynthDef(\circ_sig,
              {arg tipoS=0,velS=1,sprdS=1,initpoS= 0,
                   lev = 1,
                   tipoD=0,velD=1,sprdD=1,initpoD=1,
                   orient=0.5,
                   smt=0.2;
               var nchans=6,sig,pos,dist,pan;
                   sig = SinOsc.ar(Rand(400,2000));
                   pos   = Select.kr(tipoS,[LFSaw.kr(velS,initpoS,sprdS),    // orario
                                            LFSaw.kr(velS,initpoS,sprdS)* -1,// antiorario
                                            SinOsc.kr(velS,initpoS,sprdS),
                                            LFNoise1.kr(velS,sprdS),
                                            LFNoise2.kr(velS,sprdS)
                                           ]);
                   dist = Select.kr(tipoD,[initpoD.lag(smt),              // fissa
                                           LFTri.kr(velD,initpoD,sprdD),
                                           SinOsc.kr(velD,initpoD,sprdD),
                                           LFNoise1.kr(velD,sprdD),
                                           LFNoise2.kr(velD,sprdD),
			                              ]);
                   SendReply.kr(Impulse.kr(50), '/pos', [pos,dist]);
                   pan = PanAz.ar(nchans, sig, pos, lev.lag(smt),
                                  dist.linexp(0,1,nchans,2),             // riscalato
                                  orient);
               Out.ar(0,pan)
               }
        ).add;

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

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

w = Window.new("Pan", 270@210);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;b.free;c.free;d.free});

b = Knob.new(w,     Rect(210, 5, 50, 50));   // Knob
c = Slider.new(w,   Rect(210, 55, 50, 150)); // Slider
d = Slider2D.new(w, Rect(5, 5, 200, 200));   // Slider2D

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

b.action_({arg i; ~synth.set(\velS,i.value.linlin(0,1,0.1,5),  // solo al Synth
                             \velD,i.value.linlin(0,1,0.1,5))});
c.action_({arg i; ~synth.set(\tipoD,0,\initpoS,i.value,\initpoD,i.value)});

OSCFunc.new({arg msg;

           {b.value_(msg[3]);                                 // alle diverse GUI
            c.value_(msg[4]);
            d.x_((msg[4] * cos(msg[3] -0.5* -pi)).linlin(-1,1,0,1));
            d.y_((msg[4] * sin(msg[3] -0.5* -pi)).linlin(-1,1,0,1))
            }.defer(0)

            },'/pos', s.addr);

MIDIIn.connectAll;
MIDIdef.cc(\knob, {arg val;
                   ~synth.set(\velS,val.linlin(0,127,0.1,5),
                              \velD,val.linlin(0,127,0.1,5));
                   {b.value_(val.linlin(0,127,0,1))}.defer(0) // al Knob
                   }, 16);                                    // dal cc 16
MIDIdef.cc(\slid, {arg val;
                   ~synth.set(\tipoD,0,                       // al Synth
                              \initpoS,val.linlin(0,127,0,1),
                              \initpoD,val.linlin(0,127,0,1));
                   {c.value_(val.linlin(0,127,0,1))}.defer(0) // allo Slider
                   }, 0);                                     // dal cc 0

}
)

Da notare che quando interagiamo sullo slider dedicato alla distanza sia con il mouse che con un controller esterno, automaticamente viene selezionato il tipo 0 (\tipoD o singolo valore).

Inviluppi

Se vogliamo utilizzare un qualsiasi tipo di inviluppo, basterà sostituire Select.kr() dei parametri pos e dist con un'istanza di inviluppo così come già fatto per il panning stereofonico e quadrifonico:

(
s.waitForBoot{
var w,d;                  // variabili locali

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

SynthDef(\circ_sig,
              {arg t_gate,
                   lev = 1,
                   orient=0.5,
                   smt=0.2;
               var nchans=6,sig,
                   pos,posbpf,posEnv,
                   dist,distbpf,distEnv,
                   pan;

                   sig = SinOsc.ar(Rand(400,2000));

                   pos   = Env.newClear(4);           // Crea un inviluppo vuoto di 4 nodi
                   posbpf= \pos.kr(pos.asArray);      // Crea un controllo dell'inviluppo
                   posEnv= EnvGen.kr(posbpf, t_gate); // Genera l'inviluppo
  
                   dist = Env.newClear(4);
                   distbpf= \dist.kr(dist.asArray);
                   distEnv= EnvGen.kr(distbpf, t_gate);

                   SendReply.kr(Impulse.kr(50), '/pos', [posEnv,distEnv]);
                   pan = PanAz.ar(nchans,
                                  sig,
                                  posEnv,
                                  lev.lag(smt),
                                  distEnv.linexp(0,1,nchans,2),
                                  orient);
               Out.ar(0,pan)
               }
        ).add;

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

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

w = Window.new("Pan", 210@210);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;b.free;c.free;d.free});

d = Slider2D.new(w, Rect(5, 5, 200, 200));   // Slider2D

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

OSCFunc.new({arg msg;
             {d.x_((msg[4] * cos(msg[3] -0.5* -pi)).linlin(-1,1,0,1));
              d.y_((msg[4] * sin(msg[3] -0.5* -pi)).linlin(-1,1,0,1))}.defer(0)
             },'/pos', s.addr)
}
)

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

(
var pos,dist;
h = 2;                                         // Definiamo una durata
p = [{rand2(1.0)}!4, {rand(0.5)}!3];           // generiamo i valori di pos
d = [{rand2(1.0)}!4, {rand(0.5)}!3];           // generiamo i valori di dist
pos  = Env.new(p[0],p[1]).duration_(h);
dist = Env.new(d[0],d[1]).duration_(h);
[pos,dist].plot;                               // Plotter
~synth.set(\pos, pos, \dist, dist, \t_gate,1); // li inviamo al Synth
)

Mouse

Se infine vogliamo utilizzare il mouse per controllare i movimenti, possiamo mappare l'asse delle x sulle rotazioni di pos definendo un numero di giri per range e l'asse delle y sulla distanza, riscalata come sempre tra il numero di canali e 2:

(
s.waitForBoot{
var w,d;                  // variabili locali

// ------------------------------ SynthDef e Synth
SynthDef(\pmouse,
              {var nchans=6,sig,pos,dist,pan;
                   sig = SinOsc.ar;
                   pos  = MouseX.kr(-5,5);    // numero di giri sull'asse x
                   dist = MouseY.kr(0,1);
                   SendReply.kr(Impulse.kr(50), '/pos', [pos,dist]);
                   pan = PanAz.ar(nchans,sig,pos,1,dist.linexp(0,1,nchans,2));
               Out.ar(0,pan)
               }
          ).add;

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

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

w = Window.new("Pan", 210@210);
w.alwaysOnTop;
w.front;
w.onClose_({~synth.free;w.free;b.free;c.free;d.free});

d = Slider2D.new(w, Rect(5, 5, 200, 200));   // Slider2D

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

OSCFunc.new({arg msg;
             {d.x_((msg[4] * cos(msg[3] -0.5* -pi)).linlin(-1,1,0,1));
              d.y_((msg[4] * sin(msg[3] -0.5* -pi)).linlin(-1,1,0,1))}.defer(0)
             },'/pos', s.addr)
}
)

SplayAz

Infne possiamo utilizzare la SplayAz.kr() per posizionare un'Array di segnali in punti equidistanti all'interno di un sistema di diffusione multicanale. I suoi argomenti sono simili al "fratello minore" Splay e può essere utilizzato efficacemente per aprire o chiudere dinamicamente la disposizione di segnali nel sistema (suono diffuso/suono direzionale):

(
SynthDef(\stereo,
              {arg sprd,ctr=0;
               var nchans=6,sig,width,lev=1,orient=0.5,pan;
		           sig = [SinOsc.ar(800),Saw.ar(1000),WhiteNoise.ar];
		           width = MouseX.kr(2,6);
		           sprd = MouseX.kr(0,1);
                   pan = SplayAz.ar(nchans, // numero di canali del sistema
                                    sig,    // Array di canali mono
                                    sprd,   // spread 0 = tutti al centro
                                    lev,    // ampiezza globale (0. / 1.)
                                    width,  // su quanti canali i segnali sono distribuiti
                                    ctr,    // shift dal centro
                                    orient, // orientazione
                                    true);  // limiter...
               Out.ar(0,pan)
               }
        ).add;

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

Sistemi dedicati

Oltre ai sistemi di diffusione multicanale "neutri" appena esposti e ai loro simili possiamo anche progettare uno spazio acustico dedicato alle caratteristiche musicali o drammaturgiche del brano che vogliamo diffondere posizionando altoparlanti dedicati in posizioni specifiche come già illustrato per i sistemi multi-monofonici:

image not found

In una configurazione di questo tipo ad esempio potremmo avere tutti segnali monofonici indipendenti (uno per bus) oppure diffondere e muovere una o più sorgenti con Pan4.ar() su gruppi di altopalranti come L1, L2, L3 e L4 mentre il loro riverbero o altre elaborazioni degli stessi segnali su L5, L6 e L7 muovendoli con PanAz.ar() specificando tre canali e che il primo bus di uscita è il numero 4. L'imporante è che questo tipo di diffusione deve essere strettamente correlato alle esigenze espressive e musicali del brano e non un semplice artifizio applicato a qualsiasi tipo di musica solo per generare effetti speciali.