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.options.numOutputBusChannels_(68;
s.reboot;
s.plotTree;
s.scope;      
s.meter;

(
SynthDef(\ottof,
              {arg pos= 0,lev=1,dist=1,orient=0.5,dur=0.2;
               var nchans=8,sig,env,spos,slev,sdist,pan;
                   sig = SinOsc.ar(Rand(400,2000));
		   env = EnvGen.kr(Env.perc(0.01,0.19),Impulse.kr(5));
		   spos= VarLag.kr(pos,dur);
		   slev= Lag.kr(lev,dur);
		   sdist=VarLag.kr(dist.linexp(0,1,nchans,2),dur);
                   pan = PanAz.ar(nchans, sig*env, spos, slev, sdist, 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 che ritornano via OSC 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

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

SynthDef(\ottof,
              {arg pos= 0,lev=1,dist=1,orient=0.5,dur=0.2;
               var nchans=8,sig,env,spos,slev,sdist,pan;
                   sig = SinOsc.ar(Rand(400,2000));
		   env = EnvGen.kr(Env.perc(0.01,0.19),Impulse.kr(5));
		   spos= VarLag.kr(pos,dur);
		   slev= Lag.kr(lev,dur);
		   sdist=VarLag.kr(dist.linexp(0,1,nchans,2),dur);
                   pan = PanAz.ar(nchans, sig*env, spos, slev, sdist, orient);
				   
	       SendReply.kr(Impulse.kr(50), '/pos', [spos,1-sdist.linlin(2,nchans,0,1)]); // theta (0-2) e rho (0-1)
               Out.ar(0,pan)
               }
        ).add;

{~synth = Synth(\ottof)}.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));

OSCdef.new(\vedi, {arg msg;
             var theta,rho,x,y;
	         theta = msg[3];
	         rho = msg[4];
	         x = rho * cos(theta -0.5* -pi); // tra +/- 1
	         y = rho * sin(theta -0.5* -pi); // tra +/- 1
				 
             {b.value_(theta);                          // alle diverse GUI
              c.value_(rho);                  

              e.x_((rho * cos(theta -0.5* -pi)).linlin(-1,1,0,1)); 
              e.y_((rho * sin(theta -0.5* -pi)).linlin(-1,1,0,1));
              f.value_(theta.linlin(-1,1,0,1));
              g.value_(rho);
              }.defer(0);			 
				 
             },'/pos', s.addr);

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

f.action_({arg val; ~synth.set(\pos, val.value * 2 - 1, \dur, 0.02)});
g.action_({arg val; ~synth.set(\dist, val.value, \dur, 0.02)});

MIDIIn.disconnectAll;
MIDIIn.connectAll;

MIDIdef.cc(\knob, {arg val; ~synth.set(\pos, val/127 * 2 - 1, \dur, 0.02)}, 16);                                 
MIDIdef.cc(\slid, {arg val; ~synth.set(\dist, val/127, \dur, 0.02)}, 0);                    
)

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

a = SystemClock.sched(0, {~synth.set(\pos,rand2(1.0),\dist,rand(1.0),\dur,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)

~synth.set(\pos,0.00,\dist,1,\dur,1);
~synth.set(\pos,0.25,\dist,1,\dur,1);
~synth.set(\pos,0.50,\dist,1,\dur,1);
~synth.set(\pos,0.75,\dist,1,\dur,1);
~synth.set(\pos,1.00,\dist,1,\dur,1);
~synth.set(\pos,1.25,\dist,1,\dur,1);
~synth.set(\pos,1.50,\dist,1,\dur,1);
~synth.set(\pos,1.75,\dist,1,\dur,1);
~synth.set(\pos,2.00,\dist,1,\dur,1);

~synth.set(\pos,0,\dist,0,\dur,1);
~synth.set(\pos,rand2(1.0),\dist,rand(1.0),\dur,rrand(0.1,2))

Traiettorie.

(
var pos;
    pos = [[0,1,1],[-0.4,0.4,0.3],[-0.6,0.3,2],[0,0.4,1],[1,0,3],[-1,1,5]]; // [theta,rho,time]

h = Routine.new({
                 pos.do({arg i;
                         ~synth.set(\pos,i[0],\dist,i[1],\dur,i[2]);
                         i[2].wait
                         })
                 }).reset.play;
)

h.stop;

Random.

(
var theta,rho,dur;

h = Routine.new({
                 inf.do({theta = rand2(1.0);  
                         rho = rand(1.0);
                         dur = rrand(0.01,3);
                         ~synth.set(\pos,theta,\dist,rho,\dur,dur);
                         dur.wait                
                         })
                  }).reset.play
)

h.stop;

Rota. In questo caso rappresenta una rotazione sequenziale tra gli altoparlanti che può avvenire sia in senso orario che antiorario e rappresenta una delle prime tecniche di spazializzazione utilizzate nella musica elettronica:

// Senso orario
(
var dur, valO;
    dur  = 1;   // tempo di giro in secondi
    valO = 0;   // posizione iniziale   
h = Routine.new({
                 inf.do({
		         valO = valO+2;
		         ~synth.set(\pos,valO,\dist,1,\dur,dur);
		         dur.wait;
                         })

                 }).reset.play;
)

h.stop;
h.reset.play;

// Senso antiorario
(
var dur, valO;
    dur  = 1;   // tempo di giro in secondi
    valO = 0;   // posizione iniziale   
h = Routine.new({
                 inf.do({
		         valO = valO-2;
		         ~synth.set(\pos,valO,\dist,1,\dur,dur);
		         dur.wait;
                         })

                 }).reset.play;
)

h.stop;
h.reset.play;

Spreading. Anche in questo caso basta aggiungere la coppia di valori che definisce l'asse delle x.

(
var rth,rrh,dur,smt;
    rth = [0,0.5];
    rrh = [1,0.5];
    dur = 0.1;
    smt = 0.1;  // prova a cambiare

~ksynth.value(smt:smt);

h = Routine.new({
                 inf.do({var theta,rho;
                         theta = rrand(rth[0],rth[1]);
                         rho = rrand(rrh[0],rrh[1]);
                         ~synth.set(\pos,theta,\dist,rho,\dur,dur);
                         dur.wait
                         })
                 }).reset.play
)

h.stop;

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:

(
// ------------------------------ 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=8,sig,env,pos,dist,pan;
                   sig = SinOsc.ar(Rand(400,2000));
		   env = EnvGen.kr(Env.perc(0.01,0.19),Impulse.kr(5));
                   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*env, 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)});

OSCdef.new(\vedi, {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.disconnectAll;
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));
                   }, 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));
                   }, 0);                                     // dal cc 0
)

~synth.set(\tipoS,3,\tipoD,3);

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:

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

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

                   sig = SinOsc.ar(Rand(400,2000));
		   env = EnvGen.kr(Env.perc(0.01,0.19),Impulse.kr(5));
                   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*env,
                                  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;d.free;h.free;p.free;u.free});

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

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

OSCdef.new(\vedi, {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
u = [{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(u[0],u[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:

(
// ------------------------------ SynthDef e Synth
SynthDef(\pmouse,
              {var nchans=8,sig,env,pos,dist,pan;
                   sig = SinOsc.ar;
		   env = EnvGen.kr(Env.perc(0.01,0.19),Impulse.kr(5));
                   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*env,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;d.free});

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

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

OSCdef.new(\vedi, {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=8,sig,env,width,lev=1,orient=0.5,pan;
		           sig = [SinOsc.ar(800),Saw.ar(1000),WhiteNoise.ar];
		           env = EnvGen.kr(Env.perc(0.01,0.19),Impulse.kr(5));
		           width = MouseX.kr(2,6);
		           sprd = MouseY.kr(0,1);
                   pan = SplayAz.ar(nchans, // numero di canali del sistema
                                    sig*env,    // 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);
)