Wavetables e Buffers
In SuperCollider (e in molti altri software) un Buffer può essere utilizzato come wavetable ovvero contenere i valori che descrivono un ciclo di una forma d'onda qualsiasi in un numero limitato di campioni (usualmente da 64 a 8096). Questo poi sarà letto ciclicamente da un oscillatore tabellare realizzando quella che si chiama wavetable synthesis.
Per prima cosa programmiamo due diversi Synth che leggono ciclicamente wavetables memorizzate su Buffers in modo leggermente differente. Il primo illustra una tecnica comune a tutti i software musicali mentre il secondo sfrutta una UGen dedicata di SuperCollider (per approfondire l'argomento si faccia riferimento al paragrafo dedicato).
( s.boot; s.scope; s.meter; s.plotTree; ) ( SynthDef(\buftab, {arg buf=0,bufe=1; var punta,sig; punta = LFSaw.ar(MouseX.kr(60,300)); // mouse x controlla l'altezza sig = BufRd.ar(1, buf, punta.range(0,BufFrames.kr(buf))); Out.ar(0, sig) }).add; SynthDef(\osctab,{arg buf=0; var sig; sig = Osc.ar(buf, MouseX.kr(60,300)); // mouse x controlla l'altezza Out.ar(0,sig) }).add; )
Dopodichè dobbiamo:
- Creare un Buffer vuoto di n campioni attraverso il metodo .alloc() e assegnarlo a una variabile.
- Definire un ciclo della forma d'onda desiderata attraverso una funzione d'onda e assegnarne i valori ai campioni del Buffer. Per farlo abbiamo a disposizione quattro diverse modalità:
- Metodi dedicati. Invocati direttamente su un'istanza di Buffer.
- Float Array. I valori di ampiezza istantanea sono scritti direttamente nel Buffer (raw) secondo funzioni definite da noi.
- Inviluppi. I valori di ampiezza istantanea sono generati attraverso oggetti dedicati alla creazione di inviluppi.
- Tabling. Il ciclo è definito dal taglio di piccole porzioni di soundfile debitamente inviluppate.
Metodi dedicati
Possiamo invocare direttamente su un'istanza di Buffer alcuni metodi che generano forme d'onda dedicate alla sintesi additiva a spettro fisso. Uno di questi è .sine1 che accetta tre argomenti principali. Il primo è un Array che specifica le ampiezze dei parziali dello spettro (le frequenze sono in rapporto armonico di default), il secondo specifica se devono essere normalizzate (true) o meno (false) mentre il terzo specifica quale dei due formati possibili utilizzare per la scrittura.
Wavetable Format (.asWavetable). Ovvero un formato ottimizzato attraverso interpolazione dei valori adatto alla lettura del Buffer da parte di un oscillatore tabellare dedicato (Osc.ar()).
Buffer.freeAll; b = Buffer.alloc(s, 512); // Buffer vuoto mono di 512 campioni b.sine1([1,0.3,0.5,0.7],true,true); // Ampiezze, normalizza, asWavetable b.plot; // Visualizza Wavetable format c = Synth(\osctab, [\buf,b]); // Suona c.free;
Possiamo notare dall'immagine illustrata nel plot come la forma d'onda sia ottimizzata per evitare effetti di aliasing.
Float Array Ovvero un formato non ottimizzato (raw) da utilizzare nel caso si voglia utilizzare un oscillatore non dedicato come BufRd.ar() per la lettura del Buffer.
Buffer.freeAll; b = Buffer.alloc(s, 512); b.sine1([1,0.3,0.5,0.7],true,false); // Ampiezze, normalizza, asWavetable b.plot; // Visualizza Raw format c = Synth(\buftab, [\buf,b]); // Suona c.free;
Se leggiamo con BufRd.ar() un Buffer ottimizzato attraverso il metodo .asWavetable oppure un Buffer non ottimizzato con Osc.ar() il suono risulterà meno definito e con effetti di aliasing che però in alcuni casi potrebbero risultare interessanti.
Esistono altre due versioni di questo metodo attraverso le quali possiamo specificare anche frequenze (in cicli per Buffer) e fasi (in radianti) dei singoli parziali.
b.sine1([1,0.3,0.5,0.7]); // Ampiezze b.plot; b.sine2([1,3,5,9],[1,0.3,0.5,0.7]); // Frequenze, Ampiezze, b.plot; b.sine3([1,3,5,9],[1,0.3,0.5,0.7],[0,2pi,0.5pi,1pi,1.3pi]); // Frequenze, Ampiezze, fasi b.plot; c = Synth(\osctab, [\buf,b]); // Suona c.free;
Un'altra tecnica classica consiste nell'uso dei polinomi di Chebyshev che possono essere definiti come: cheby(n) = amp * cos(n * acos(x)) e sono utilizzati nella sintesi per distorsione non lineare.
b.cheby([1,0,1,1,0,1]); // Ampiezze b.plot; b.sine1([1,0,1,1,0,1]); // Confronto b.plot; c = Synth(\osctab, [\buf,b]); // Suona c.free;
Vediamo come possiamo calcolare alcune forme d'onda classiche della musica elettroacustica nelle versioni senza aliasing.
Onda a dente di sega
. Somma di tutti gli armonici con ampiezze decrescenti secondo la seguente relazione: 1/numero dell'armonico a segno alternato.Buffer.freeAll; b = Buffer.alloc(s, 512, 1); ( b.sine1(1.0/((-1**Array.series(45,0,1))* // segno alternato Array.series(90,1,1))) // parziali ) b.getToFloatArray(action: {arg i; {i[0, 2..].plot}.defer}); // per una migliore visualizzazione c = Synth(\osctab, [\buf,b]); {Saw.ar(MouseX.kr(60,300))}.play; // UGen dedicata...
Onda quadra
. Somma degli armonici dispari con ampiezze decrescenti secondo la seguente relazione: 1/numero dell'armonicob.sine2(Array.series(45,1,2),1.0/Array.series(45,1,2)); b.getToFloatArray(action: {arg i; {i[0, 2..].plot}.defer}); c = Synth(\osctab, [\buf,b]); {Pulse.ar(MouseX.kr(60,300))}.play; // UGen dedicata...
Onda triangolare
. Somma degli armonici dispari con ampiezze decrescenti secondo la seguente relazione: (1/numero dell'armonico)**2 a segno alternato.( b.sine2(Array.series(45,1,2), // frequenze (-1**Array.series(45,0,1))* // segno alternato (1.0/Array.series(45,1,2)**2) // ampiezze ) ); b.getToFloatArray(action: {arg i; {i[0, 2..].plot}.defer}); c = Synth(\osctab, [\buf,b]); {LFTri.ar(MouseX.kr(60,300))}.play; // UGen dedicata...
Rumore periodico
. Parziali con ampiezze randomiche.b.sine1(Array.fill(90, {1.0.rand})); b.getToFloatArray(action: {arg i; {i[0, 2..].plot}.defer}); c = Synth(\osctab, [\buf,b]);
Timbri custom
. Timbri derivati da modelli.b.sine2([0.5,1,1.19,1.56,2,2.51,2.66,3.01,4.1],[0.25,1,0.8,0.5,0.9,0.4,0.3,0.6,0.1]); b.getToFloatArray(action: {arg i; {i[0, 2..].plot}.defer}); c = Synth(\osctab, [\buf,b]);
Float Array
Possiamo generare i valori di forme d'onda anche con la Classe Signal() che è un FloatArray ottimizzato per contenere i valori di un segnale. Questa Classe è molto versatile in quanto oltre poter invocare su di essa i metodi dedicati appena illustrati accetta anche operazioni matemetiche di diverso tipo.
-
( s.boot; s.scope; s.meter; s.plotTree; ) ( [Signal.sineFill(1024, 1.0/[1, 2, 3, 4, 5, 6]), // Numero di campioni, ampiezze, fasi Signal.chebyFill(1024, [0, 1]), // Numero di campioni, ampiezze Signal.hanningWindow(1024), Signal.hanningWindow(1024, 512), // Numero di campioni, zero Pad Signal.hammingWindow(1024), Signal.welchWindow(1024), Signal.rectWindow(1024,512)].plot )
-
x = Signal.sineFill(512, [0,0,0,1]); [x, x.neg, x.abs, x.sign, x.squared, x.cubed, x.asin.normalize, x.exp.normalize, x.distort].plot;
-
( a = Signal.newClear(512); b = Signal.newClear(512); c = Signal.newClear(512); a.waveFill({arg x, old, id; sin(x)}, 0, 3pi); b.waveFill({arg x, old, id; sin(x) * sin(11 * x + 0.3) }, 0, 3pi); c.waveFill({rand2(1.0)}, 0, 512); [a,b,c].plot; )
Dopo aver generato la forma d'onda attraverso una qualsiasi tra le tecniche appena illustrate, dobbiamo scriverne i valori in un Buffer.
- Se la rilettura sarà effettuata con BufRd.ar()
Buffer.freeAll; a = Signal.sineFill(512, 1.0/[1, 2, 3, 4, 5, 6]); // Forma d'onda a = a.normalize; // Normalizzazione (eventuale) b = Buffer.loadCollection(s, a); // Carica in un Buffer b.plot; // Visualizza d = Synth(\buftab, [\buf,b]); // Suona d.free;
- Se la rilettura sarà effettuata con Osc.ar()
Buffer.freeAll; a = Signal.sineFill(512, 1.0/[1, 2, 3, 4, 5, 6]); // Forma d'onda a = a.normalize.asWavetable; // Normalizzazione (eventuale) b = Buffer.loadCollection(s, a); // Carica in un Buffer b.plot; // Visualizza d = Synth(\osctab, [\buf,b]); // Suona d.free;
Inviluppi
Possiamo generare una forma d'onda anche attraverso inviluppi debitamente convertiti in Signal() (eventualmente anche in wavetable format) e poi caricati in un Buffer.
Buffer.freeAll; a = Env.new([0, 1, 0.2, 0.3, -1, 0.3, 0], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1], \sin); a = a.asSignal(512); // Trasforma in Signal w = a.asWavetable b = Buffer.loadCollection(s, a); // Carica in un Buffer (per BufRd.ar) c = Buffer.loadCollection(s, w); // Carica in un Buffer (per Osc.ar) d = Synth(\buftab, [\buf,b]); // Suona d.free; d = Synth(\osctab, [\buf,c]); // Suona d.free;
Tabling
Possiamo caricare in un Buffer piccole porzioni di un soundfiles per poi utilizzarle come wavetables.
b = Buffer.read(s,"bach.wav".resolveRelative, 500, 512); // 512 campioni a partire da id 500 b.normalize; // Normalizzazione b.plot;
Come possiamo notare dall'immagine all'inizio e alla fine della wavetable ci sono delle discontinuità che dobbiamo rimuovere. Per farlo dobbiamo:
- Trasformare il Buffer in un Float Array (Signal).
- Generare un inviluppo d'ampiezza anch'esso sotto forma di Float array (Signal).
- Moltiplicare tra loro i valori dei due Arrays (windowing).
- Scrivere i valori del nuovo Array nel Buffer.
In questo caso abbiamo utilizzato un inviluppo triangolare ma possiamo utilizzarne uno di qualsiasi tipo.
b = Buffer.read(s,"bach.wav".resolveRelative, 500, 512); // Frammento b.normalize; // Normalizza b.loadToFloatArray(action:{arg array; a = array}); // Buffer --> Float Array e = Env.triangle.asSignal(512); // Env --> Float Array a = a*e; // Windowing b.free; // Distrugge il Buffer b = Buffer.loadCollection(s,a); // Ne crea uno nuovo b.plot; d = Synth(\buftab, [\buf,b]); // Suona d.free;
N.B. Buffers con pochi campioni (64/1024) generano timbri meno ricchi rispetto a Buffers con un numero più alto di campioni.
Salvare come soundfiles
Possiamo salvare il contenuto di un Buffer indipendentemente da come lo abbiamo realizzato sotto forma di soundfiles per poi ricaricarlo dinamicamente.
b.sine1([1,0.3,0.5,0.7]); b.write("/Users/andreavigani/Desktop/addi.aiff"); b.cheby([1,0,1,1,0,1]); c = Synth(\osctab, [\buf,b]); // Suona b.read("/Users/andreavigani/Desktop/addi.aiff"); c.free;
In questa cartella ad esempio sono memorizzate numerose forme d'onda che possiamo caricare in un Array di buffers per poi richiamarle dinamicamente attraverso gli indici.
~wt = SoundFile.collectIntoBuffers("wavetables/*".resolveRelative,s); ~wt.size; ~wt[0].plot; c = Synth(\osctab, [\buf,rand(~wt.size).postln]);