Crossfading

La tecnica del crossfading consiste nel sostituire gradualmente un segnale audio con un altro attraverso una dissolvenza incrociata controllata da un singolo parametro durante la scrittura di un Bus. Principalmente è impiegata in tre situazioni:

Dissolvenze incrociate

In SuperCollider possiamo realizzare dissolvenze incrociate tra due o più segnali in tre modi differenti. Il primo (Buffer fade) corrisponde alla tecnica teorica che può essere realizzata con qualsiasi software mentre gli altri due sono specifici di SuperCollider. La principale limitazione comune consiste nel fatto che i segnali da miscelare devono essere continuamente generati anche quando non scritti sull'output utilizzando risorse della CPU che potrebbero essere risparmiate.

Buffer fade

Questa tecnica consiste nel generare un Buffer contenente i valori d'ampiezza che descrivono un fade in e un fade out preceduti da un numero variabile di zero (silenzio). Il Buffer viene riletto da un segnale puntatore che opportunamente sfasato fa cominciare la rilettura da un punto diverso per ogni generatore di segnale.

image not found

Il Buffer da generare deve essere suddiviso in un numero variabile di finestre aventi tutte lo stesso numero di campioni. Maggiore sarà questo numero più accurata sarà la definizione dei fades (256 o 512 campioni possono essere una buona scelta di base).

Il numero di queste finestre corrisponde al numero di segnali che vogliamo miscelare in output (nell'immagine di esempio sono quattro).

Due di esse definiscono delle rampe che corrispondono a fade in e fade out mentre le rimanenti contengono tanti zero quanto il loro size. Per definire i valori possiamo utilizzare Signal.fill() che genera un Float Array facilmente caricabile in un Buffer.

s.boot;
s.plotTree;
s.scope;
s.freqscope;

(
a = Signal.fill(512*2, {0})++             // Finestre con valore 0
    Signal.fill(512, {arg i; i/512})++    // Fade In
    Signal.fill(512, {arg i; 1-(i/512)}); // Fade Out
)

(
Buffer.freeAll;
b = Buffer.loadCollection(s, a); // Carica nel Buffer
)

b.numFrames; // Size totale del Buffer
b.plot;      // Visualizzazione

Definiamo un segnale puntatore che ci permetta di "navigare" a piacere tra gli indici del Buffer. I valori di questo segnale puntatore devono essere compresi tra zero e il size del Buffer meno il size dell'ultima finestra. Nell'esempio abbiamo impiegato MouseX.kr() ma possiamo utilizzare qualsiasi tipo di segnale di controllo opportunamente riscalato.

Utilizziamo la UGen WrapIndex.kr() per ottenere i valori di ampiezza istantanea memorizzati nel Buffer richiamandone gli indici debitamente sfasati per ogni segnale da miscelare. In questo modo quando il segnale puntatore genera ad esempio il valore zero che corrisponde all'indice zero, per il primo segnale corrisponde ad un'ampiezza istantanea di uno in quanto all'indice attuale è sommato un offset pari a tre volte la lunghezza della finestra, mentre per gli altri segnali con offset differenti l'ampiezza è zero.

(
SynthDef(\cross_1, {arg gate=0;
                    var punta,bufF=0,sig1,sig2,sig3,sig4,env,out;
                  
                        punta = MouseX.kr(0, BufFrames.kr(bufF)-512); // Segnale controllo crossfade
                      
                        sig1  = WhiteNoise.ar   * WrapIndex.kr(b, 512*3+punta);
                        sig2  = SinOsc.ar       * WrapIndex.kr(b, 512*2+punta);
                        sig3  = Saw.ar          * WrapIndex.kr(b, 512*1+punta);
                        sig4  = Impulse.ar(440) * WrapIndex.kr(b, 512*0+punta);
                      
                        env   = Linen.kr(gate);
                        out   = sig1+sig2+sig3+sig4;
                    Out.ar(0,out*env)
         }).add;
)

a = Synth(\cross,[\gate,1]);
a.set(\gate,0);

Possiamo utilizzare anche la UGen Index.kr() al posto di WrapIndex.kr(). La prima effettua un clipping quando i valori del segnale puntatore superano il size del Buffer (restituisce sempre l'ultimo valore) mentre la seconda fa ricominciare la lettura dall'inizio in modo circolare.

LinSelectX.ar()

Questa UGen implementa nello specifico di SuperCollider quanto appena illustrato. Fadein e fadeout sono lineari. I segnali da miscelare devono essere inclusi in un Array e il range del segnale puntatore deve essere compreso tra zero e il numero di segnali da miscelare.

(
SynthDef(\cross_2, {arg gate=0;
                    var sigs,punta,env,out;
                        sigs  = [WhiteNoise.ar,SinOsc.ar,Saw.ar,Impulse.ar(440)]; 
                        punta = MouseX.kr(0, sigs.size); // Segnale controllo crossfade
                        env   = Linen.kr(gate);
                        out   = LinSelectX.ar(punta,sigs);
                    Out.ar(0,out*env)
         }).add;
)

a = Synth(\cross_2,[\gate,1]);
a.set(\gate,0);

SelectX.ar()

Come la precedente con la differenza che il crossfade non è lineare ma equal power ovvero viene calcolata la radice quadrata dei valori di ampiezza lineare compresi tra zero e uno. Questa particolarità rende i crossfades più netti ed è particolarmente adatta al panning.

(
SynthDef(\cross_3, {arg gate=0;
                    var sigs,punta,env,out;
                        sigs  = [WhiteNoise.ar,SinOsc.ar,Saw.ar,Impulse.ar(440)]; 
                        punta = MouseX.kr(0, sigs.size); // Segnale controllo crossfade
                        env   = Linen.kr(gate);
                        out   = SelectX.ar(punta,sigs);
                    Out.ar(0,out*env)
         }).add;
)

a = Synth(\cross_3,[\gate,1]);
a.set(\gate,0);

Sintesi vettoriale

La sintesi vettoriale consiste nell'applicare le tecniche di crossfading sopra esposte al segnale generato da un oscillatore tabellare e ottenere spettri variabili nel tempo. In questo caso l'oscillatore non legge i valori di una singola forma d'onda memorizzata in un Buffer ma realizza una lettura dinamica che si muove tra più wavetables sulle quali sono memorizzzate forme d'onda differenti.

Buffer.allocConsecutive()

Per ottimizzare la lettura dinamica di più wavetables da parte di oscillatori tabellari dedicati dobbiamo generare un Array di Buffers vuoti aventi tutti lo stesso numero di campioni attraverso il metodo .allocConsecutive() per poi definire le diverse forme d'onda richiamandone l'indice. Devono obbligatoriamente essere difiniti in wavetable format.

(
Buffer.freeAll;
b = Buffer.allocConsecutive(4,      // Numero di Buffer
                            s,      // Server
                            1024);  // Size di ogni singolo Buffer (multiplo di 2)
)

(
b[0].sine1([1,0.3,0.5,0.7],true,true);  // asWavetable !!!
b[1].sine1([0.2,0.4,1.3,0.6],true,true);
b[2].sine1([1,0.2,0.5,0.6,1.3,0.5],true,true);
b[3].sine1([1.3,0.3,0.5,0.7,0.8,0.9,0.2,0.3],true,true);
)

b[0].plot;
b.do(_.free); // libera l'Array di Buffer

Un'altra sintassi che possiamo utilizzare è la seguente che invia i comandi direttamente al Server (Server Command) sotto forma di 32 bit integer invece che stringhe OSC.

(
4.do({arg i;
      var n, a;
          s.sendMsg(\b_alloc, i, 1024); // Crea n Buffers vuoti di 1024 campioni
          n = (i+1)**2;                 // Seria armonica
          a = Array.fill(n, {arg j; ((n-j)/n).squared.round(0.001)}); // Genera i valori 
          s.performList(\sendMsg, // nome del metodo
                        \b_gen,   // invia al Buffer
                        i,        // con indic i 
                        \sine1,   // metodo sine1 
                        7,        // flag che indica normalize+wavetable+clear
                        a);       // ampiezze dei parziali
});
)

Con entrambe le sintassi possiamo modificare dinamicamente le forme d'onda mentre sono rilette dall'oscillatore tabellare.

VOsc.ar()

La UGen VOsc.ar() è dedicata alla sintesi vettoriale e legge dinamicamente due o più forme d'onda memorizzate in un Array di Buffer come appena descritto.

(
SynthDef(\vec_1, {arg buf=0,bufn=4,freq=400,amp=0,pan=0,gate=0,done=2;
                  var pos,sig,env,pann;
                      pos   = MouseX.kr(buf,bufn.asFloat).poll;
                      sig   = VOsc.ar(pos, freq);
                      env   = Linen.kr(gate,doneAction:done);
                      pann  = Pan2.ar(sig*amp.lag(2)*env);
                  Out.ar(0, pann)
        }).add;
)

a = Synth(\vec_1, [\buf,b[0],\bufn,b.size-1,\freq,400,\amp,1,\pan,0,\gate,1]);
a.set(\gate,0);

Una tecnica classica per questo tipo di sintesi consiste nel mappare i valori del segnale puntatore su quelli dell'ampiezza (debitamente riscalati) rendendo in questo modo automatico l'invilppo spettrale di ogni nota.

(
SynthDef(\vec_2, {arg buf=0,bufn=4,freq=400,dur=1,amp=0,pan=0,t_gate=0,done=2;
                  var sig,bpf,env,pann;
                      bpf   = Env.perc(0.01,dur-0.01);
                      env   = EnvGen.ar(bpf,doneAction:done);
                      sig   = VOsc.ar(env.linlin(0,1,buf,bufn-1), freq);
                      pann  = Pan2.ar(sig*amp.lag(2)*env);
                  Out.ar(0, pann)
        }).add;
)

(
Synth(\vec_2, [\buf,b,\bufn,4,
               \freq,rrand(70, 96).midicps,
               \dur,rrand(0.1,2),
               \amp,rand(1.0),
               \pan,rand2(1.0),
               \t_gate,1]);
)

VOsc3.ar()

Simile al precedente ma una singola UGen contiene tre oscillatori con controllo della frequenza indipendente. Utile per effetti di chorusing. Nell'esempio abbiamo utilizzato un segnale di controllo continuo per ottenere un soundscape timbrico continuamente cangiante.

(
SynthDef(\vec_3, {arg buf=0,bufn=4,freq=#[440,459,678],amp=0,pan=0,gate=0,done=2;
                  var pos,sig,env,pann;
                      pos   = LFNoise1.kr(0.2).range(0.0,bufn-1);
                      sig   = VOsc3.ar(pos, freq[0],freq[1],freq[2]);
                      env   = Linen.kr(gate,doneAction:done);
                      pann  = Pan2.ar(sig*amp.lag(2)*env);
                  Out.ar(0, pann)
        }).add;
)

(
a = Synth(\vec_3, [\buf,b[0],
                   \bufn,b.size-1,
                   \freq, 3.collect({rrand(200,1000)}).postln,
                   \amp,1,
                   \pan,0,
                   \gate,1]);
)
a.set(\gate,0);