Sintesi per funzione d'onda (WTL)¶
Funzioni d'onda ¶
Possiamo definire il timbro di un suono disegnandone la forma d'onda attraverso il calcolo ogni singolo valore di ampiezza istantanea (y) attraverso una funzione.
Una funzione restituisce uno o più valori in uscita che variano in funzione dei valori in ingresso.
t = {arg a, b; a + b}; // Funzione
t.value(2,4); // Argomenti = INPUT risultato = OUTPUT
// ---------------> Funzione Sinusoide
// x[n] = a * sin(w * n + O)
// x = OUTPUT
// [n] = INPUT
// Per ogni n in INGRESSO calcola un valore corrispondente in USCITA
// che corrisponde al valore di ampiezza istantanea:
t = {arg a,w,n; a * sin(w * 1 * n + 0)};
t.value(1, 0.01, 0);
t.value(1, 0.01, 1);
t.value(1, 0.01, 2);
t.value(1, 0.01, 3);
//...
// Automatizziamo il calcolo e collezioniamo i risultati in un Array
(
~punti = 32; // Numero di punti (size della tavola)
~freq = 1; // Numero di cicli per size della tavola
~amp = 1.0; // Ampiezza di picco
~w = 2pi/(~punti/~freq); // Calcolo della velocità angolare (rad/sec)
p = ~punti.collect({arg n; ~amp * sin(~w * n + 2pi)}); // n assume ad ogni valutazione
// l'indice dell'Array
// partendo da 0
p.plot(discrete:true); // Visualizza
)
In SuperCollider possiamo realizzare il processo appena esposto in due modi:
- Impiegando la Classe Signal che genera un FloatArray (32 bit float) e supporta operazioni matematiche (Client side).
- Invocando alcuni metodi d'istanza dedicati direttamente su un Buffer (Server side).
Signal ¶
La classe Signal ci permette di disegnare diverse forme d'onda utili per l'analisi, sintesi ed elaborazione del suono. Analizziamone due casi.
Spettri armonici
~punti = 32;
~amp = [1.0]; // Array...
~amp = [0.2,0.5,0.3];
// (Numero di punti, [Ampiezze di picco], [fasi])
p = Signal.sineFill(~punti, ~amp, [2pi]);
p.plot(discrete:true);
Funzioni custom
~punti = 32;
p = Signal.newClear(~punti); // Crea un FloatArray di n 0
p.waveFill(
{arg i;
i.postln},
-1, 1 // valori di 'i' tra min e max
);
// Esempio --> funzione della sinusoide
// sin(x) dove x è compresa tra 0 e 2pi (angolo giro)
(
p.waveFill({arg i;
sin(i)}, // operazioni matematiche su i
0, 2pi); // min, max
)
p.plot(discrete:true);
Generati i valori y dell'Array con Signal (Client side) dobbiamo caricarli in un Buffer (Server side) per poterli rileggere attraverso un oscillatore tabellare.
s.boot;
s.scope;
s.freqscope;
Buffer.freeAll;
~punti = 32;
p = Signal.newClear(~punti);
p.waveFill({arg i; sin(i)}, 0, 2pi);
b = Buffer.loadCollection(s, p);
b.plot;
Wavetable format ¶
Possiamo trasformare un FloatArray (quelli generati da Signal) in un formato ottimizzato per la rilettura da parte di oscillatori tabellari dedicati come Osc.ar() che realizzano una interpolazione lineare.
Questo formato si chiama waveteble format e il numero di campioni deve essere un multiplo di due.
~punti = 32;
p = Signal.newClear(~punti);
p.waveFill({arg i; sin(i)}, 0, 2pi); // FloatArray
p.postln;
w = p.asWavetable; // Trasforma il FloatArray in formato wavetable
w.plot;
Se trasformato in wavetable format il numero di campioni è raddoppiato.
p.size; // FloatArray
w.size; // Wavetable format (il doppio...)
// Per questo motivo dovremo utilizzare un Buffer size doppio
Buffer.freeAll;
b = Buffer.alloc(s, ~punti * 2);
b.loadCollection(w);
b.plot;
Se invochiamo il metodo direttamente sul Buffer il resize è automatico.
Buffer.freeAll;
b = Buffer.loadCollection(s, w); // Resize automatico...
b.write("suono_1.aiff"); // Possiamo salvare il Buffer come sound file
Buffer ¶
Possiamo disegnare alcune forme d'onda classiche invocando metodi dedicati direttamente su un Buffer.
Un esempio con i metodi .sine1 .sine2 e .sine3.
(
~punti = 1024;
Buffer.freeAll;
b = Buffer.alloc(s,~punti);
b.sine1([0.3,0.7,1.2], // Ampiezze dei parziali
true, // Normalizza
true, // asWavetable
true); // Cancella prima
{b.plot}.defer(0.1)
)
x = {Osc.ar(b, 200, 0, 0.2)}.play;
x.free;
(
~punti = 1024;
Buffer.freeAll;
b = Buffer.alloc(s,~punti);
b.sine2([1, 2, 3], // Rapporti di frequenza dei parziali
// (solo spettri armonici - numeri interi)
[0.3,0.7,1.2], // Ampiezze dei parziali
true, // Normalizza
true, // asWavetable
true); // Cancella prima
{b.plot}.defer(0.1)
)
x = {Osc.ar(b, 200, 0, 0.2)}.play;
x.free;
(
~punti = 1024;
Buffer.freeAll;
b = Buffer.alloc(s,~punti);
b.sine3([1, 2, 3], // Rapporti di frequenza dei parziali
[0.5,0.3,0.2], // Ampiezze dei parziali
[2pi,pi,0.3pi], // Fasi dei parziali
true, // Normalizza
true, // asWavetable
true); // Cancella prima
{b.plot}.defer(0.1)
)
x = {Osc.ar(b, 200, 0, 0.2)}.play;
x.free;
Non interpolating wavetable lookup synthesis¶
Utilizziamo i valori y di un fasore per richiamare gli indici (x) di una wavetable all'interno della quale abbiamo memorizzato un periodo di una qualsiasi forma d'onda.
I valori del fasore sono generalmente compresi tra -1 e +1 nel caso di un'onda bipolare oppure tra 0 e 1 nel caso di un onda unipolare.
(
~size = 100; // Size del Buffer
~unipolare = Signal.newClear(~size);
~bipolare = Signal.newClear(~size);
~unipolare.waveFill({arg i; i}, 0, 1);
~bipolare.waveFill({arg i; i}, -1, 1);
[~unipolare,~bipolare].plot(minval:-1,maxval:1,discrete:true);
)
Per poter impiegare i valori y del fasore come indici x della wavetable dobbiamo riscalarli in un ambito commpreso tra 0 e il size-1 della wavetable da rileggere.
Moltiplichiamo il fasore unipolare per il size (nel caso fosse bipolare dobbiamo prima renderlo unipolare).
(
~fasore = ~unipolare * ~size; // Fasore tra 0 e size-1
~wave = ~size.collect({arg n; (1 * sin(2pi/p * 1 * n + 0)) + // Primo armonico
(1 * sin(2pi/p * 2 * n + 0)) + // Secondo armonico
(1 * sin(2pi/p * 3 * n + 0)) // Terzo armonico
}); // 1 periodo wavetable
~wave = ~wave.normalize(-1,1); // Tra +/-1
[~fasore++~fasore++~fasore++~fasore,~wave++~wave++~wave++~wave].plot(discrete:true);
)
Un periodo del fasore coincide con un periodo della wavetable.
La frequenza del fasore (in Hz) determina anche la frequenza dell'oscillatore tabellare.
Verifichiamolo.
Disegnamo una forma d'onda
s.boot;
s.scope;
s.freqscope;
// Disegnamo una forma d'onda
(
Buffer.freeAll;
~size = 1024;
b = Buffer.alloc(s,~size);
b.sine1([0.3,0.7,1.2], asWavetable:false); // In formato normale...
{b.plot}.defer(0.1)
)
Realizziamo un fasore (due tecniche).
- Oscillatore a dente di sega (riscalato)
// LFSaw.ar()
(
{[
LFSaw.kr(2), // Bipolare
LFSaw.kr(2,-1), // Fuori fase
LFSaw.kr(2,-1)*0.5+0.5, // Unipolare
]}.plot(1)
)
{LFSaw.kr(2,-1)*0.5+0.5*~size}.plot(1) // Riscalato tra 0 e n-1
Contatore a sample rate (wrap around)
Incrementa di uno step (che può essere 1 o altro) alla velocità della rata di campionamento.
C'è un wrap around tra un valore minimo e un valore massimo.
// Phasor.ar()
// incr start end
{Phasor.ar(0, 1, 0, 100)}.plot(0.01)
Se vogliamo ottenere una frequenza in Hz dobbiamo calcolare il fattore di incremento:
size del Buffer * freq(Hz) / sample rate
100 * 440 / 48000 = 0.91666666666667
(
{Phasor.ar(0,
// recupera il size recupera la sample rate attuale
BufFrames.kr(b) * 20 / SampleRate.ir,
0, // Frame inizo lettura
BufFrames.kr(b))}.plot(0.1) // Frame fine lettura
)
Rileggiamo il Buffer con un oscillatore.
// BufRd.ar()
// Dente di sega
(
SynthDef(\wave1, {arg buf=b, freq=500, amp=0, dur=1, atk=0.1, t_gate=0, pan=0, done=0, int=1;
var punta,sig,env;
punta = LFSaw.ar(freq,-1).range(0, BufFrames.kr(buf)); // .range...
sig = BufRd.ar(1, // Numero canali
buf, // Buffer da leggere
punta, // Segnale puntatore (fasore)
1, // Loop
int); // Interpolazione: 1, 2, 4
env = Env.perc(atk*dur, (1.0-atk)*dur);
env = EnvGen.ar(env,t_gate,doneAction:done);
sig = Pan2.ar(sig*amp*env,pan);
Out.ar(0,sig)}
).add;
)
// Interpolazione: osservare le differenze nello spettroscopio...
Synth(\wave2, [\buf,b,\freq,rrand(70,82).midicps,\amp,1,\int,1,\done,2,\t_gate,1]); // No interp.
Synth(\wave2, [\buf,b,\freq,rrand(70,82).midicps,\amp,1,\int,2,\done,2,\t_gate,1]); // Interp. lin
Synth(\wave2, [\buf,b,\freq,rrand(70,82).midicps,\amp,1,\int,4,\done,2,\t_gate,1]); // Interp. cub
// Fasore
(
SynthDef(\wave2, {arg buf=b, freq=500, amp=0, dur=1, atk=0.1,t_gate=0,pan=0,done=0,int=1;
var punta,sig,env;
punta = Phasor.ar(0, BufFrames.kr(buf) * freq / SampleRate.ir, 0, BufFrames.kr(buf));
sig = BufRd.ar(1, buf, punta, 1, int);
env = Env.perc(atk*dur, (1.0-atk)*dur);
env = EnvGen.ar(env,t_gate,doneAction:done);
sig = Pan2.ar(sig*amp*env,pan);
Out.ar(0,sig)}
).add;
)
Synth(\wave2, [\buf,b,\freq,rrand(70,82).midicps,\amp,1,\int,1,\done,2,\t_gate,1]); // No interp.
Synth(\wave2, [\buf,b,\freq,rrand(70,82).midicps,\amp,1,\int,2,\done,2,\t_gate,1]); // Interp. lin
Synth(\wave2, [\buf,b,\freq,rrand(70,82).midicps,\amp,1,\int,4,\done,2,\t_gate,1]); // Interp. cub
Oscillatori dedicati¶
BufRD.ar() è un oscillatore generico che può essere utilizzato in diverse tecniche di rilettura di un Buffer.
Per la wavetable synthesis possiamo utilizzare oscillatori dedicati.
OscN.ar()¶
Oscillatore tabellare che non interpola.
Il Buffer deve obbligatoriamente contenere un numero di campioni pari a una potenza di 2 e non essere in wavetable format.
(
Buffer.freeAll;
~size = 1024;
b = Buffer.alloc(s,~size);
b.sine1([0.3,0.7,1.2], asWavetable:false); // In formato normale...
SynthDef(\oscN, {arg buf=b, freq=500, amp=0, dur=1,t_gate=0,pan=0,done=0;
var sig,env;
sig = OscN.ar(buf,freq);
env = Env.triangle(dur);
env = EnvGen.ar(env,t_gate,doneAction:done);
sig = Pan2.ar(sig*amp*env,pan);
Out.ar(0,sig)}
).add;
)
Synth(\oscN, [\buf,b,\freq,rrand(70,82).midicps,\amp,1,\dur,0.3,\done,2,\t_gate,1]);
Osc.ar()¶
Oscillatore tabellare con interpolazione lineare.
Il Buffer deve obbligatoriamente contenere un numero di campioni pari a una potenza di 2 e essere in wavetable format.
(
Buffer.freeAll;
~size = 1024;
b = Buffer.alloc(s,~size);
b.sine1([0.3,0.7,1.2], asWavetable:true); // In wavetable format
// La sintassi è identica a OscN
SynthDef(\osc, {arg buf=b, freq=500, amp=0, dur=1,t_gate=0,pan=0,done=0;
var sig,env;
sig = Osc.ar(buf,freq);
env = Env.triangle(dur);
env = EnvGen.ar(env,t_gate,doneAction:done);
sig = Pan2.ar(sig*amp*env,pan);
Out.ar(0,sig)}
).add;
)
Synth(\osc, [\buf,b,\freq,rrand(70,82).midicps,\amp,1,\dur,0.3,\done,2,\t_gate,1]);
SinOsc.ar() è come quest'ultimo Synth che legge un Buffer interno di 8192 campioni.
Sintesi additiva a spettro fisso armonico ¶
Tecnica di sintesi che ci permette di definire spettri sonori fissi ovvero con un unico inviluppo d'ampiezza per tutti i parziali.
Possiamo disegnare una forma d'onda definendo i seguenti parametri attraverso una delle sintassi illustrate nel paragrafo dedicato.
- rapporti di frequenza - numeri interi che definiscono il fattore di moltiplicazione (numero del parziale).
- ampiezze - relative di ogni parziale.
- fasi - iniziali di ogni parziale.
Definiamo un prototipo di strumento virtuale dedicato alla WTS.
s.boot;
s.scope;
s.freqscope;
//-------------------------------------------------- Buffer
(
~punti = 2048;
Buffer.freeAll;
b = Buffer.alloc(s,~punti);
b.sine3([1, 2, 3], // Rapporti di frequenza dei parziali
[0.5,0.3,0.2], // Ampiezze dei parziali
[2pi,2pi,2pi], // Fasi dei parziali
true, // Normalizza
true, // asWavetable
);
{b.plot(bounds:600@400, minval: -1, maxval:1)}.defer(0.1)
)
//-------------------------------------------------- Strumento
(
SynthDef(\wave, {arg buf=b, freq=500, amp=0, dur=1, fade=0.1,t_gate=0,pan=0,done=0;
var sig,env;
sig = Osc.ar(buf,freq);
env = Env.linen(fade, dur-(fade*2),fade,curve:\cub);
env = EnvGen.ar(env,t_gate,doneAction:done);
sig = Pan2.ar(sig*amp*env,pan);
Out.ar(0,sig)
}).add;
)
Esploriamo le potenzialità espressive di questa tecnica.
Tre esempi diversi tra loro.
- Organo - simulazione di un organo.
Synth(\wave, [\buf,b,\amp,0.4,\freq,rrand(60,72).midicps,\done,2,\t_gate,1]);
- Run - ad ogni nota cambiano ampiezze e fasi dello spettro.
(
r = Routine.new({
inf.do({
~amps = 10.collect({rand(1.0)}).normalizeSum;
~phas = 10.collect({rand(2pi)});
p = Signal.sineFill(~punti/2, ~amps, ~phas);
p = p.asWavetable;
b.loadCollection(p);
Synth(\wave, [\buf, b,
\freq, 500,
\amp, rand(1.0),
\dur, rrand(0.05,0.08),
\fade, 0.01,
\pan, [-1,1].choose,
\done, 2,
\t_gate,1]);
0.1.wait;
})
}).play
)
r.reset.play; // Suona
r.stop; // Stoppa
- Tappeto sonoro
(
~amps = 10.collect({rand(1.0)}).normalizeSum;
~phas = 10.collect({rand(2pi)});
p = Signal.sineFill(~punti/2, ~amps, ~phas);
b = Buffer.loadCollection(s, p.asWavetable);
v = Routine.new({
inf.do({
Synth(\wave, [\buf,b,
\freq,rrand(50,100).midicps, // Atonale
// \freq, [68,72,75,80].midicps.choose, // Triade
\amp,rand(1.0),
\dur,rrand(4,8),
\fade,rrand(0.2,8),
\pan, rand2(1.0),
\done,2,
\t_gate,1]);
rrand(0.1,2).wait;
})
}).play
)
v.reset.play; // Suona
v.stop; // Stoppa
Sintesi forme d'onda classiche ¶
Esistono alcune forme d'onda orami classiche con spettri ricchi di parziali.
Se computate attraverso funzioni d'onda non sono limitate in banda di frequenza e producono fenomeni di aliasing/foldover.
Per ovviare possiamo disegnarle a partire dalle loro caratteristiche spettrali con metodi dedicati alla sintesi additiva a spettro fisso.
s.boot;
s.scope;
s.freqscope;
(
SynthDef(\wave_s, {arg buf=b, freq=500, amp=0, dur=1, t_gate=0, pan=0, done=0;
var sig,env;
sig = Osc.ar(buf,freq);
env = Env.sine(dur);
env = EnvGen.ar(env,t_gate,doneAction:done);
sig = Pan2.ar(sig*amp*env,pan);
Out.ar(0,sig)}
).add;
)
Onda a dente di sega
Sono presenti tutti i parziali con ampiezze decrescenti 1/num_harm.
(
Buffer.freeAll;
~punti = 1024; // Numero di punti (size della tavola)
~npar = 45; // Numero di parziali
~harm = ~npar.collect({arg n; n}); // Tutti gli armonici (da 0)
~sign = -1**~harm; // Segno alternato
~amps = 1.0/(1+~harm*~sign); // Ampiezze decrescenti
b = Buffer.alloc(s, ~punti);
b.sine1(~amps);
b.plot;
)
Synth(\wave_s, [\buf,b,\amp,1,\freq,rrand(70,82).midicps,\done,2,\t_gate,1]);
{Saw.ar(MouseX.kr(60,3000))}.play; // UGen dedicata...
{LFSaw.ar(MouseX.kr(60,3000))}.play; // Con aliasing...
Onda quadra
Sono presenti i parziali dispari con ampiezze decrescenti 1/num_harm.
(
Buffer.freeAll;
~punti = 512; // Numero di punti (size della tavola)
~npar = (45/2).asInteger; // Numero di parziali
~harm = ~npar.collect({arg n; 1+n+n}); // Armonici dispari
~amps = 1/~harm; // Ampiezze decrescenti
c = Buffer.alloc(s, ~punti);
c.sine2(~harm,~amps);
c.plot;
)
Synth(\wave_s, [\buf,c,\amp,1,\freq,rrand(70,82).midicps,\done,2,\t_gate,1]);
{Pulse.ar(MouseX.kr(68,72).midicps)}.play; // UGen dedicata...
{LFPulse.ar(MouseX.kr(60,3000))}.play; // Con aliasing..
Onda triangolare
Sono presenti i parziali dispari con ampiezze decrescenti (1/num_harm)2** a segno alternato.
(
Buffer.freeAll;
~punti = 512; // Numero di punti (size della tavola)
~npar = (45/2).asInteger; // Numero di parziali
~harm = ~npar.collect({arg n; 1+n+n}); // Armonici dispari
~sign = ~npar.collect({arg n; -1**n}); // Segno alternato
~amps = ~sign * (1/~harm**2); // Ampiezze decrescenti
e = Buffer.alloc(s, ~punti);
e.sine2(~harm,~amps);
e.plot;
)
Synth(\wave_s, [\buf,e,\amp,1,\freq,rrand(70,82).midicps,\done,2,\t_gate,1]);
{VarSaw.ar(MouseX.kr(60,300),0,0.5)}.play; // UGen dedicata...
{LFTri.ar(MouseX.kr(60,3000))}.play; // Con aliasing...
Noise periodico con distribuzione lineare
Rumore colorato.
(
Buffer.freeAll;
~punti = 512; // Numero di punti (size della tavola)
~amp = 1.0; // Ampiezza di picco
g = ~punti.collect({~amp * rand2(1.0)});
i = Buffer.alloc(s, ~punti);
i.loadCollection(g); // Carica FloatArray nel Buffer (sine)
i.plot;
)
Synth(\wave_s, [\buf,i,\amp,1,\freq,rrand(70,82).midicps,\done,2,\t_gate,1]);
IO Buffers ¶
Possiamo salvare il contenuto di un Buffer indipendentemente da come lo abbiamo realizzato sotto forma di soundfiles per poi ricaricarlo dinamicamente.
b = Buffer.alloc(s, 2048);
b.sine1([1,0.3,0.5,0.7]);
b.plot;
b.write("/Users/andreavigani/Desktop/addi.aiff");
b.read("/Users/andreavigani/Desktop/addi.aiff");
// Nella cartella wavetables ad esempio sono memorizzate numerose
// forme d'onda che possiamo caricare in un Array di buffers per poi
// richiamarle dinamicamente attraverso gli indici.
(
Buffer.freeAll;
~wt = SoundFile.collectIntoBuffers("wavetables/*".resolveRelative,s);
~wt.size; // Numero di Buffer (wavetables) caricati nel Server
)
y = ~wt[rand(~wt.size).postln]
Synth(\wave_s, [\buf,y,\amp,0.3,\freq,rrand(70,82).midicps,\done,2,\t_gate,1]);
A questo link possiamo scaricare una cartelle contenente numerose wavetables sotto forma di sound files.