Formati e codec

Come risultato del processo di campionamento di un segnale audio analogico otteniamo una sequenza di numeri binari (numeric streams) che può essere scritta in particolari tipi di files (audio files o sound files) memorizzati su svariati tipi di supporti digitali (CD, DVD, HD o altro). Questi files possono avere tre diversi formati di codifica:

Queste diverse possibilità nascono infatti dall'esigenza che nel momento in cui vogliamo memorizzare su un supporto digitale tutte le informazioni riguardanti un segnale potremmo avere la necessità di ridurre lo spazio di memorizzazione occupato a vantaggio della portabilità o della trasmissività del flusso codificato e per farlo dobbiamo ricorrere alla compressione delle informazioni stesse in un modo tale che permetta anche l'operazione inversa.

Questa operazione è svolta dai codec che sono dei programmi (o dispositivi) che si occupano sia della digitalizzazione dei segnali (tipicamente audio o video) che della loro codifica e/o decodifica digitale.

Esistono vari tipi di codec, differenti tra loro per il tipo di segnale su cui devono operare e per l’algoritmo di codifica/compressione in essi implementato. Ogni formato di codifica può essere ottenuto da più codec differenti. Questi infatti permettono di ascoltare formati proprietari aperti da qualunque lettore di file, mantenendo separati il livello fisico del formato da quello logico della sua rappresentazione.

I vantaggi della compressione sono:

Il costo (svantaggi) è l’aumento dei tempi di lettura/scrittura legati ai tempi di decompressione/compressione e, nel caso di audio files anche in termini di qualità audio.

Bitrate

Prima di approfondire i diversi tipi di formati audio soffermiamoci su concetti legati alla velocità di trasmissione dei dati in quanto i file audio sono per loro natura legati al tempo che scorre: ad ogni secondo è associato un certo contenuto informativo e quindi una certa sottosequenza di cifre binarie. Il numero di cifre binarie che compongono queste sottosequenze è detto bitrate.

Il bitrate è il numero di cifre binarie impiegate per immagazzinare un secondo di informazione.

I cd musicali ad esempio hanno come standard una frequenza di campionamento pari a 44.100Hz che genera dunque 44.100 valori al secondo per ogni canale. Nel caso di un file stereofonico vanno poi moltiplicati per 2 e siccome il campionamento avviene a 16 bit (pari appunto a 2 byte) vanno ulteriormente moltiplicati per 2:

44.100 * 2 * 2 * 60 (secondi) = 10.584.000 bytes (~10 MB) ogni minuto

Il bitrate si esprime in kilobit per secondo (kbps) e può variare da 32 a 320kbps. Se volessimo ad esempio calcolare il bitrate del file precedente dovremmo calcolare:

44.100 * 2 * 2 * 8 (da bytes a bit) = 1.411.200 bit/secondo (1.411 kbs)

I calcoli appena effettuati fanno riferimento a un formato non compresso mentre nel caso dei formati compressi, al diminuire della lunghezza globale del file diminuisce anche la lunghezza media delle sottosequenze e di conseguenza il bitrate medio che corrisponderà al fattore di compressione.

Infatti se un file con un bitrate di 1411 Kbps come quello dell'esempio precedente fosse compresso fino ad ottenere un bitrate medio di 320 Kbps, avremmo ridotto le dimensioni del file originale di un fattore pari a circa 4.5 (1411/320).

Attualmente nei codec più evoluti esistono tre tipologie di implementazione del bitrate:

Formati non compressi

Esistono formati audio che non hanno compressione e che in fatto di qualità sonora sono i migliori. Per contro occupano molto più spazio in memoria ed una minore velocità di trasmissione rispetto ai formati compressi. Con software professionali come Pro Tools, SuperCollider o Max generalmente si lavora con file di questo tipo. I due principali formati sono:

Compressione lossy

Permette compressioni maggiori, ma a scapito della qualità sonora. I metodi di compressione lossy in generale tendono a scartare le informazioni ritenute inutili, mantenendo solo quelle essenziali e nascono dall’idea che non tutte le frequenze contenute in uno spettro sonoro vengono percepite dall’orecchio umano. Si vanno allora a tagliare le alte frequenze, che si ritiene siano quelle meno distinte dal nostro orecchio. Ovviamente più frequenze si tagliano più lo spazio occupato dalla traccia audio diminuisce e con questo anche la qualità del risultato in quanto il processo di riconversione non permette il completo ripristino delle frequenze tagliate. Vediamo quali sono i principali formati audio di questo tipo:

Compressione Lossless

Questi metodi di compressione cercano di diminuire lo spazio occupato dalla traccia senza andare a toccare il suono. La percentuale di compressione è decisamente inferiore rispetto ai metodi lossy, ma non si verifica perdita di qualità e in fase di riconversione il suono è identico all'originale. Vediamo quali sono i principali formati audio di questo tipo:

Arrays, Buffers e Wavetables

I Buffers sono come degli Arrays (o List) ottimizzati per memorizzare temporaneamente non int, float, UGens, string o altri tipi di data ma campioni audio (samples) e come già accennato non vivono nell'Interprete ma nel Server. I Buffers possono essere utilizzati anche come Wavetables ma quali sono le differenze principali tra i due termini?

Usualmente quella appena descritta è solo una differenza terminologica che descrive l'utilizzo che facciamo dei dati contenuti in un Buffer, mentre nella programmazione dei software più comuni esiste solo un oggetto chiamato Buffer che possiamo utilizzare all'occorrenza in entrambi i casi.

Possiamo scrivere dati all'interno di un Buffer in tre modi:

Sound files e buffers

Path

Nel momento in cui decidiamo di caricare un audio file in un Buffer dobbiamo dire a SuperCollider dove andare a cercarlo fornendogli l'indirizzo e il percorso (path) da compiere per trovarlo sotto forma di stringa. Per ottenere facilmente questa informazione possiamo selezionare e trascinare (click and drop) con il mouse il file desiderato nello spazio che delimita l'Interprete ottenendo automaticamente l'intero percorso che si chiama path assoluto:

"/Users/andreavigani/Desktop/21_giugno/samples/archi.wav"

Questa operazione è valida per tutti i tipi di file, non solo quelli audio e come facilmente intuibile separa le cartelle e sottocartelle con il simbolo '/' (slash) seguendo una direzione d'inclusione che va da sinistra a destra.

Specificando un percorso in queso modo (path assoluto) possiamo però incorrere in una problematica: se dovessimo spostare il file o la cartella all'interno del computer oppure eseguire il codice su di un altro computer dovremmo ogni volta sostituire il path precedente con quello nuovo. Fortunatamente esiste un metodo che ci permette di specificare un path relativo, ovvero fa aggiungere autmaticamente a SuperCollider il percorso prima della cartella o del file che specifichiamo fino al punto in cui si trova il file su quale lo richiediamo. In questo caso l'unica limitazione consiste nel fatto che il file deve essere stato salvato sull'Hard disk:

"samples".resolveRelative;

E' buona pratica posizionare gli audio files che vogliamo utilizzare in uno script di SuperCollider all'interno di una cartella che è essa stessa all'interno di un'altra cartella contenente anche il file con le istruzioni necessarie a caricarli in un buffer. In questo modo scrivendo:

"samples/archi.wav".resolveRelative; // "sample" è il nome della cartella contenente gli audio files

non dovremo ogni volta ridefinire il path assoluto anche nel caso di spostamento della cartella principale su di un altro computer.

Caricare un sound file in un Buffer

Possiamo caricare un solo audio files in un singolo buffer invocando il metodo .read sulla Classe Buffer. Il file può essere monofonico, stereofonico o multicanale in quanto il buffer si adatta automaticamente al suo numero di canali:

(
b = Buffer.read(s,                                   // Server
                "samples/archi.wav".resolveRelative, // Path del file da caricare
                action:{("File "++"samples/archi.wav".basename++" loaded in Buffer "
                         ++b.bufnum).postln});       // Un azione che compie quando il file è stato caricato
)                                                     

Eseguendo il codice precedente SuperCollider crea un nuovo buffer nel Server, gli assegna automaticamente un numero partendo da 0 (indicizzazione) e carica i dati dell'audio file al suo interno. Se ad esempio eseguiamo due o più volte il codice precedente, a ogni esecuzione sarà creato un nuovo buffer contenente sempre lo stesso audio file ma con numero d'indice crescente anche se assegnato sempre alla stessa variabile (che sarà di volta in volta sovrascritta). Per sapere quale numero è assegnato ad un buffer possiamo invocare sulla singola istanza il metodo .bufnum:

b.bufnum;

Nel blocco di codice di esempio abbiamo anche programmato un monitor visivo che stampa nella Post window questa informazione unita al nome dell'audio file caricato. Per farlo abbiamo recuperato il nome dal path invocando su di esso il metodo .basename che riporta solo questa informazione tagliando il resto del path e concatenato tra loro valori e stringhe attraverso la sintassi ++:

"File "++"samples/archi.wav".basename++" loaded in Buffer "++b.bufnum;

Possiamo anche caricare un sound file in un Buffer attraverso una finestra di dialogo invocando il metodo .loadDialog:

b = Buffer.loadDialog(s, action:{("File loaded in Buffer "++b.bufnum).postln})

Quando carichiamo un sound file in un Buffer potremmo aver bisogno di alcune informazioni sia sul sound file originale sia sul buffer sul quale lo caricheremo. Queste potranno tornarci utili quando andremo a rileggerne il contenuto.

SoundFile e Buffer info

Il modo più semplice per ottenere informazioni su di un sound file consiste nel generare una finestra grafica nella quale queste sono visualizzate automaticamente:

(
f = SoundFile.new;
f.openRead(Platform.resourceDir +/+ "sounds/a11wlk01.wav");
f.inspect;
f.close;
)

image not found

Eseguendo il codice precedente abbiamo:

In alternativa possiamo recuperare le singole informazioni invocando su un'istanza di SoundFlie diversi metodi ma prima dobbiamo riaprire il file:

f = SoundFile.new;
f.openRead(Platform.resourceDir +/+ "sounds/a11wlk01.wav");

Ricordiamo che il numero di samples (o frames) di un sound file non ci fornisce alcuna indicazione riguardo la sua durata se non lo mettiamo in relazione con la rata di campionamento: un suono registrato e riletto con una rata di campionamento di 44100 Hz dura un secondo, ma se registrato a 44100 e riletto a 88200 Hz dura mezzo secondo (lo stesso numero di campioni ha due durate differenti).

Per ottenere informazioni su di un Buffer possiamo invocare gli stessi metodi appena incontrati con l'aggiunta di altri riassunti nel codice seguente:

b = Buffer.read(s,Platform.resourceDir +/+ "sounds/a11wlk01.wav"); // carica un sound file

b.bufnum;        // numero (indice) del buffer
b.plot;          // visualizza il contenuto in un Plotter
b.play;          // 'suona' il buffer (solo per monitorarne il contenuto)
b.path;          // riporta il path assoluto del file caricato

b.numFrames;     // riporta il numero di Frames
b.numChannels;   // riporta il numero di canali
b.sampleRate;    // riporta la rata di campionamento (che portebbe essere diversa da quella del file caricato)
b.duration;      // riporta la durata in secondi

b.query;         // riporta le informazioni precedenti attraverso un solo comando

Pulire e liberare

Una volta che abbiamo creato un Buffer (allocato la memoria) possiamo cancellarne il contenuto invocando su di esso il metodo .zero:

b.zero;
b.plot;

In questo caso il Buffer assegnato alla variabile b continua a esistere nel Server e occupa una memoria di tanti frames quanti quelli del file che conteneva. Se invece vogliamo eliminare il Buffer dal Server e liberare la variabile alla quale lo avevamo associato possiamo invocare il metodo .free:

b.free;
b.plot; // WARNING: Buffer not allocated, can't plot data

Se invece vogliamo eliminare tutti i Buffers allocati su un Server possiamo invocare il metodo .freeAll:

Buffer.freeAll;

Questo comando ritorna utile in quanto c'è un limite nel numero di buffers che possono essere creati in un Server:

s.options.numBuffers;        // Per ottenerne il numero
s.options.numBuffers = 2000; // Si può cambiare ma bisogna fare il reboot del server

Caricare un solo canale

Se vogliamo caricare solo un numero parziale di canali di un file multicanale lo possiamo fare invocando il metodo .readChannel. Se non specifichiamo argomenti carica tutti i canali:

b = Buffer.readChannel(s, "samples/celesta.wav".resolveRelative); // il file è stereo
b.plot;
b.play;

Se invece specifichiamo come argomento (channels:) quale canale o quali canali vogliamo caricare sotto forma di Array carica solo quelli:

b = Buffer.readChannel(s, "samples/celesta.wav".resolveRelative,channels:[0]); // solo canale sinistro
b.plot;
b.play;

Caricare una porzione di sound file

Se vogliamo caricare solamente una porzione di file possiamo specificare inizio e fine in frames come secondo e terzo argomento (in questo caso il numero di frames corrisponde ai sample anche nel caso di files multicanale):

Buffer.freeAll;

(
b = Buffer.read(s,
	            "samples/celesta.wav".resolveRelative,
	              3456,   // primo frame (onset)
	             12345)   // numero di frames da leggere (durata)
)

b.numFrames;
b.plot;
b.play;

// Se vogliamo specificare la durata in secondi:

(
b = Buffer.read(s, "samples/celesta.wav".resolveRelative,
                0.0,                // inizio
                s.sampleRate * 0.1) // durata (sample_rate * secondi)
)

b.numFrames;
b.plot;
b.play;

// Se vogliamo specificare inizio e durata in secondi:

(
b = Buffer.read(s, "samples/archi.wav".resolveRelative,
                s.sampleRate * 0.1,    // 4410
                s.sampleRate * 0.3)    // 13230
)

b.numFrames;
b.plot;
b.play;

// Se vogliamo specificare inizio e fine in secondi:

(
var inizio = 0.3, fine = 0.6;
b = Buffer.read(s, "samples/archi.wav".resolveRelative,
                s.sampleRate * inizio,
                s.sampleRate * abs(fine-inizio))  // calcolo la durata
)

b.numFrames;
b.plot;
b.play;

Caricare una cartella di sound files

Abbiamo suggerito in precedenza di posizionare tutti gli audio files in una cartella. Vediamo allora come caricarli tutti automaticamente in diversi buffers (uno per ogni file).

  1. Definiamo il path assoluto della cartella e selezioniamo tutti i files con estensione .wav ivi contenuti invocando il metodo .pathMatch. Se volessimo solo files con altre estensioni basterà cambiare il suffisso .wav con l'estensione desiderata:

    ~paths = ("samples".resolveRelative ++ "/*.wav").pathMatch;

    Eseguendo il codice precedente, assegnamo alla variabile globale ~paths un'Array di path assoluti dei file con estensione .wav contenuti nella cartella specificata.

  2. Generiamo un'Array di buffers con il metodo ..collect invocato sull'Array ~paths:

    ~bufs  = ~paths.collect{arg i; Buffer.read(s, i)};
    
  3. Richiamiamo i singoli buffer invocando sull'Array il metodo .at come di consueto:

    ~bufs.at(0).play;
    ~bufs.at(1).play;
    ~bufs.at(2).play;
    ~bufs.at(3).play;
    ~bufs.at(4).play;
    ~bufs.at(rand(~files.size-1)).play;
    
    Buffer.freeAll;
    

Menu popup

Possiamo creare un Pop-up menu con i nomi dei file contenuti nei buffers in modo da poter interagire con una GUI.

  1. Generiamo un Array contenente solo i nomi dei sound files ricavandoli automaticamente dall'Array di path generato in precedenza:

    ~nomi = ~paths.collect{arg i; PathName(i).fileName};
  2. Verifichiamo che i sound files corrispondono agli indici dei buffers:

    [~nomi, ~bufs].flop.do{arg i; i.postln}; "" // "" Per stampare tutto nella post window

    Il metodo .flop inverte righe e colonne di un Array bidimensionale. O meglio prende l'indice 0 di due Array diversi e ne restituisce uno nuovo con i due items collocati a quell'indice, poi passa all'indice 1, e così via.

  3. Generiamo un Popup menu popolando automaticamente i nomi con l'Array nomi:
    (
    w = Window.new("popup", Rect(800, 200, 200, 30))
              .alwaysOnTop_(true)
              .front;
    
    p = PopUpMenu.new(w, Rect(2, 2, 196, 20));
    p.items_(~nomi); 
    
    // Quando interagiamo con il pop-up, restituisce l'indice del menu, partendo da 0.
    
    p.action_({arg view; view.value.postln});
    )
    
  4. Per selezionare ed eseguire i diversi files interagendo con la GUI possiamo usare il metodo p.value per ottenere lo stato del PopUpMenu e mapparlo come indice di selezione dei buffers contenuti nell'Array ~bufs:

    • p.value == 0 --> ~bufs.at(0)
    • p.value == 1 --> ~bufs.at(1)
    • p.value == n --> ~bufs.at(n)

    Separando di fatto l'azione di selezione del file da quella di esecuzione:

    ~bufs.at(p.value).play;

    Se vogliamo invece triggerare il playback del buffer contestualmente alla sua scelta sul PopUpMenu possiamo utilizzare il valore riportato da view.value:

    (
    w = Window.new("popup", Rect(800, 200, 200, 40)).alwaysOnTop_(true).front;
    p = PopUpMenu.new(w, Rect(2, 2, 196, 20))
                 .items_(~nomi)                                   
                 .action_({arg view; ~bufs.at(view.value).play}); 
    )
    

In entrambi i casi tutti i buffers saranno eseguiti fino alla fine.

Visualizzare sound files

Come per altri oggetti di SuperCollider (Array, Env, etc.) abbiamo a disposizione due modi per visualizzare un sound file:

Plot

Nel primo caso possiamo creare un Plotter per ogni Buffer che contiene un sound file invocando su di esso il metodo .plot:

(
b = Buffer.read(s, "samples/celesta.wav".resolveRelative);

{b.plot(name: "buffer "++b.bufnum++" - "++"samples/archi.wav".basename, 
	    bounds:540@200,   
	    minval: -1,       
	    maxval: 1)}.defer(0.2)
)

image not found

Realizzando un Plotter possiamo solo visualizzare il contenuto di un Buffer ma non possiamo interagire in alcun modo con esso.

SoundFileView.new()

Possiamo trovare info su come visualizzare dinamicamente sound files a questo link

Segnali e Buffers

In diverse tecniche di elaborazione del segnale in tempo reale è necessario registrare frammenti di suono più o meno lunghi in uno o più Buffer.

Per alcune di queste come quelle legate all'analisi e ri-sintesi FFT basta un Buffer di pochi campioni (512, 1024, 2048) mentre per altre come la sintesi granulare dovremo utilizzare Buffers nell'ordine di millisecondi o secondi.

Allocazione della memoria

La prima cosa che dobbiamo fare è riservare uno spazio di memoria vuoto su di un Server che riempiremo dinamicamente in seguito con i valori di un segnale. Possiamo farlo invocando il metodo .alloc() su un'istanza di Buffer:

(
s.boot;
s.scope;
s.meter;
s.plotTree;
)

(
Buffer.freeAll;                      // Prima pulisce tutto...
f = Buffer.alloc(s,                  // server
                 s.sampleRate * 0.5, // lunghezza in frames
                 1);                 // numero di canali
				 
g = Buffer.alloc(s, s.sampleRate * 0.5, 2);
)

f.plot;
g.plot;

Scrittura segnali

Possiamo scrivere i valori di uno o più segnali audio in un Buffer con la UGen RecordBuf.ar() che attraverso settaggi combinati dei suoi argomenti ci permette di sfruttare diverse possibilità.

Come prima cosa però creiamo due Synths differenti che scrivono il loro output su due Buses diversi (in questo caso 9 e 10).

Il primo sintetizza un segnale audio direttamente in SuperCollider mentre il secondo legge un segnale dagli input audio del computer o della soundcard con la UGen SoundIn.ar() che accetta come primo argomento il Bus (o un Array di Buses nel caso di più canali in ingresso) dal quale leggere i valori.

(
SynthDef(\noise,   {arg busOut=9, gain=0;
	                Out.ar(busOut, PinkNoise.ar*gain.lag(0.2))
}).add;

SynthDef(\microf,  {arg busIn=0,busOut=10,gain=0;
	                Out.ar(busOut, SoundIn.ar(busIn)*gain.lag(0.2))
}).add;
)	

Possiamo anche fermare la registrazione con l'argomento run:0 oppure specificare un offset in samples per non cominciare a scrivere nel Buffer dall'inizio con l'argomento offset:n.

Array di Buffers

Se vogliamo utilizzare più di un Buffer anche in questo caso conviene collezionarli in un Array per poi richiamarli con gli indici come abbiamo fatto per caricare cartelle di soundfiles

~durs  = [2,5,0.1,0.02322]*s.sampleRate;
~bufs  = ~durs.collect{arg i; Buffer.alloc(s, i.round, 1)};

Synth.tail(s,\recBmono1, [\buf,~bufs[0].bufnum,\bus,10,\gain,1]);
Synth.tail(s,\recBmono1, [\buf,~bufs[1].bufnum,\bus,10,\gain,1]);
Synth.tail(s,\recBmono1, [\buf,~bufs[2].bufnum,\bus,10,\gain,1]);
Synth.tail(s,\recBmono1, [\buf,~bufs[3].bufnum,\bus,10,\gain,1]);

(
~bufs[0].plot; 
~bufs[1].plot; 
~bufs[2].plot; 
~bufs[3].plot;
)

Wavetables e Buffers

Abbiamo visto in questo paragrafo che 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 (da 64 a 2048). Questo poi sarà letto ciclicamente da un oscillatore tabellare realizzando quella che si chiama wavetable synthesis.

Per prima cosa programmiamo un Synth per leggere ciclicamente le wavetables che andreamo a creare nel Buffer.

(
s.boot;
s.scope;
s.meter;
s.plotTree;
)

(
SynthDef(\osctab,{arg buf;
                  var sig;
                      sig = Osc.ar(buf, MouseX.kr(60,300)); // mouse x controlla l'altezza
                  Out.ar(0,sig)
}).add
)	

Abbiamo a disposizione tre diverse modalità per disegnare un ciclo di forma d'onda in un Buffer:

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 i cui argomenti principali sono specificati nel codice.

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;

image not found

L'immagine che vediamo nel plot visualizza la forma d'onda ottimizzata per la lettura da parte di un oscillatore tabellare in quanto abbiamo specificato true come terzo argomento. Se volessimùo una visualizzazione più chiara dovremmo specificare false, per contro il suono risulterà molto meno definito e con effetti di aliasing.

b.sine1([1,0.3,0.5,0.7],true,false); // Ampiezze, normalizza, asWavetable 
b.plot;                              // Visualizza Wavetable format

c = Synth(\osctab, [\buf,b]);       // Suona
c.free;

image not found

Possiamo anche lasciare il Buffer ottimizzato per la lettura e visualizzare in modo più chiaro la forma d'onda con il codice seguente.

b.getToFloatArray(action: {arg i; {i[0, 2..].plot}.defer});

Il secondo argomento specifica se le ampiezza devono essere normalizzate (true) o meno (false). Esistono altre due versioni di questo metodo nelle 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 si possono calcolare alcune forme d'onda classiche della musica elettroacustica nelle versioni senza aliasing.

Signal()

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.

Per poter essere letti da oscillatori tabellari attraverso la tecnica del wavetable look-up dobbiamo però scrivere i valori all'interno di un Buffer avendo cura di convertirli prima in Wavetable Format per evitare artefatti sonori e fenomeni di aliasing.

(
SynthDef(\osctab,{arg buf;
                  var sig;
                      sig = Osc.ar(buf, MouseX.kr(60,300)); // mouse x controlla l'altezza
                  Out.ar(0,sig)
}).add
)

Buffer.freeAll;

x = Signal.sineFill(512,  1.0/[1, 2, 3, 4, 5, 6]);
x.size;

c = x.normalize.asWavetable;     // Trasforma in Wavetable Format
c.size;                          // Il size della Wavetable è il doppio di Signal

b = Buffer.loadCollection(s, c); // Carica in un Buffer
b.plot;                          // Visualizza
b.numFrames;                     // Il size del Buffer è lo stesso della Wavetable

d = Synth(\osctab, [\buf,b]);    // Suona
d.free;

Inviluppi

Possiamo generare una forma d'onda anche attraverso inviluppi debitamente convertiti prima in wavetable format poi caricati in un Buffer.

(
SynthDef(\osctab,{arg buf;
                  var sig;
                      sig = Osc.ar(buf, MouseX.kr(60,300)); // mouse x controlla l'altezza
                  Out.ar(0,sig)
}).add
)

Buffer.freeAll;

x = Env([0, 1, 0.2, 0.3, -1, 0.3, 0], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1], \sin);
x.plot;
           
c = x.asSignal(512).asWavetable; // Trasforma prima in Signal e poi in Wavetable Format
c.size;                          // Il size della Wavetable è il doppio di Signal

b = Buffer.loadCollection(s, c); // Carica in un Buffer
b.plot;                          // Visualizza
b.numFrames;

d = Synth(\osctab, [\buf,b]);    // Suona
d.free;	

Salvare come soundfiles

Possiamo salvare il contenuto di un Buffer indipendentemente do 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]);