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:

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