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:

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.

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.

image not found

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.

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.

Dopo aver generato la forma d'onda attraverso una qualsiasi tra le tecniche appena illustrate, dobbiamo scriverne i valori in un Buffer.

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;

image not found

Come possiamo notare dall'immagine all'inizio e alla fine della wavetable ci sono delle discontinuità che dobbiamo rimuovere. Per farlo dobbiamo:

image not found

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]);