Mauro Graziani
Tecniche di Sintesi

L'oscillatore digitale


Prima di partire con la descrizione delle tecniche di sintesi vere e proprie, sarà utile chiarire, sia pure in breve, i principi di funzionamento di uno dei moduli maggiormente utilizzati in questo campo: l'oscillatore digitale.
In realtà si possono capire le tecniche di sintesi anche senza sapere esattamente come funziona un oscillatore digitale, assumendo fideisticamente il fatto che questo modulo è in grado di riprodurre correttamente qualsiasi forma d'onda periodica, ma la conoscenza non è mai inutile e in questo caso specifico mette in luce alcuni aspetti della tecnologia digitale, che spesso non è così precisa come si pensa.

NB: quello descritto qui è il classico "wavetable oscillator", cioè l'oscillatore basato su tabella che si trova in varie forma in Csound (oscil, oscili, oscil3, etc) e in Max/MSP (cycle~).

Descrizione

L'oscillatore digitale è un dispositivo software che legge ciclicamente una tabella numerica in cui è memorizzato un ciclo della forma d'onda da produrre. La lettura avviene mediante un incremento di fase che è calcolato in base alla frequenza da produrre e alla frequenza di campionamento.
Se, per fare un esempio, si desidera produrre una sinusoide a 440 Hz, occorre, prima di tutto, memorizzare in tabella un ciclo della forma d'onda richiesta (in questo caso, una sinusoide) e poi far partire un oscillatore indirizzandolo alla tabella suddetta e specificando la frequenza desiderata.
L'oscillatore scandirà l'intera tabella in 1/440 di secondo, procedendo ciclicamente (cioè dall'inizio alla fine e poi da capo), generando, in questo modo, un'onda sinusoidale avente un periodo pari alla frequenza richiesta.
Ecco uno schema:

schema oscillatore digitale
Il dettaglio è semplice, ma non del tutto banale.

La tabella

Innanzitutto, con il termine tabella (detta anche array unidimensionale o vettore) si intende una struttura dati formata da una serie di locazioni di memoria consecutive. Per accedere ai singoli valori, quindi, basta conoscere la locazione di partenza e sommare un numero indice che rappresenta la posizione dell'elemento desiderato.
Supponendo che la tabella si chiami T, l'ennesimo valore sarà indicato con Tn o scritture equivalenti (T[n], T(n), etc). In pratica, supponendo che la tabella sia memorizzata a partire dalla locazione 1000, il primo valore si troverà alla locazione 1000, il secondo alla locazione 1001, il terzo alla 1002, ... ,l'ennesimo alla locazione (T + n - 1). Si noti, però, che, per convenzione, in alcuni linguaggi di programmazione (e.g. C, Phyton e altri) il primo valore è definito come di indice 0 (zero: T0, quindi il valore con indice n si troverà alla locazione T + n), mentre in altri (Pascal e altri) ha indice 1 (T1, quindi il valore con indice n si troverà alla locazione T + n - 1).
Lunghezza della tabella
Nei linguaggi di sintesi, comunque, questo meccanismo di lettura è trasparente all'utente che non se ne deve preoccupare, se non per un particolare: la lunghezza della tabella. Di solito, infatti, si richiede una lunghezza multipla di 2 (128, 256, 512, 1024, 2048, 4096, 8192, etc).
Anche il lettore ingenuo può intuire che questa imposizione è collegata alla particolare numerazione utilizzata dal computer, che codifica i numeri in base 2. In dettaglio, una lunghezza pari a una potenza di 2 consente alla macchina di risparmiare un test, velocizzando il processo di lettura. Una lettura ciclica, infatti, prevede che, una volta superata l'ultima locazione, si ritorni alla prima. Per farlo, è necessario testare il valore dell'indice prima di ogni lettura con un codice tipo

if (index >= lunghezza_tabella) then index = index - lunghezza_tabella

oppure eseguire l'operazione di modulo

index = index mod lunghezza_tabella


Sia il test che il modulo, eseguiti SR volte al secondo (SR = frequenza di campionamento, e.g. 44100), consumano tempo che può essere risparmiato con un semplice AND: posto che si sia precalcolato il valore

lunghezza_tabella_meno_uno = lunghezza_tabella - 1


allora basta porre

index = index AND lunghezza_tabella_meno_uno


effettuando una operazione molto più veloce rispetto ai metodi di cui sopra. Questo artificio, però, è possibile se e solo se la lunghezza della tabella è una potenza di 2 perché solo in questo caso il valore lunghezza_tabella_meno_uno, in binario, è una serie ininterrotta di 1 che, con l'operazione di AND, elimina tutte le cifre superiori.
Esempio: sia lunghezza_tabella = 256 = 28, in binario 0000000100000000, allora lunghezza_tabella_meno_uno = 255, in binario 0000000011111111. Ora, finché l'indice è <= 255, l'AND con 255 lo lascia inalterato, ma quando l'indice è > 255, l'effetto dell'AND è quello di azzerare tutte le cifre superiori, riportando l'indice nell'ambito richiesto con il minimo consumo di tempo di calcolo.
Importanza della lunghezza della tabella
Alcuni sistemi, come Csound, lasciano all'utente l'onere di scegliere la lunghezza della tabella in cui memorizzare la forma d'onda. La scelta non è banale perché può influire sulla definizione della forma d'onda. Se quest'ultima è insufficiente si produrranno approssimazioni con un rumore di quantizzazione sensibile.
Il perché è semplice. Supponiamo che si voglia memorizzare in tabella una sinusoide, senza armonici. La tabella sarà riempita con valori di sin(x), con x che va da 0 a 2π (un ciclo) in un numero di passi pari alla lunghezza della tabella. Nel digitale, la funzione non è mai continua, ma approssimata con una serie di gradini. Ne consegue che la definizione della curva memorizzata è direttamente proporzionale alla dimensione della tabella.
A titolo di esempio, ecco come appare una sinusoide calcolata con 16, 128, 512 e 4096 punti. La differenza si vede già così, ma cliccando su una qualsiasi immagine si aprirà una pagina con ingrandimenti in cui si vede che anche la funzione calcolata in 512 punti è lungi dall'essere buona (ovviamente 16 punti è un caso assurdo, ma rende l'idea; per tornare qui usate il link "Back" a fine pagina). Per informazione, in Csound ver. 5.00 la massima lunghezza della tabella è 16777216 (224).

16 punti
128 punti
512 punti
4096 punti

Abbiamo, quindi, un primo dato di fatto: la risoluzione della forma d'onda è proporzionale alla lunghezza della tabella. Tutto ciò può avere un impatto non banale sulla qualità dell'audio generato dall'oscillatore, come vedremo.

Il meccanismo di lettura

Poco fa, nella descrizione, abbiamo detto che, per ottenere 440 Hz, "l'oscillatore scandirà l'intera tabella in 1/440 di secondo, procedendo ciclicamente". Concettualmente è vero, ma praticamente i programmi di sintesi non si comportano così. In un software come Csound o Max/MSP, il tempo non ha alcuna importanza e non viene minimamente considerato come tale. L'unico parametro collegato al tempo che questi software maneggiano, infatti, è il numero di campioni audio calcolato in base alla frequenza di campionamento (SR).
Se, per esempio, dite a Csound di generare un suono per 2 secondi, questo valore temporale sarà immediatamente trasformato in campioni moltiplicandolo per SR e il software scriverà nel file di uscita SR * 2 campioni per ogni canale 1. SR, in pratica, viene ad essere l'unica unità di misura del tempo e lo spazio di un campione è il più piccolo intervallo esistente (il "quantum" temporale) 2. Ma, attenzione: la definizione di quantum temporale non è un colorito modo di dire, perché, proprio come nell'analoga teoria fisica, il quantum non è solo l'intervallo minimo, ma anche il sottomultiplo, base di tutti gli intervalli temporali. In pratica, non esiste, in digitale, un intervallo temporale pari a 1.5 campioni (e in genere a n.k campioni). Esistono solo intervalli temporali pari a un numero intero di campioni.
Quindi, quando dite a Csound (o Max/MSP) SR = 44100, stabilite anche altre due cose:
È importante capire che il software procede nel tempo a passi di 1/SR, quindi esistono i tempi 1/SR, 2/SR, 3/SR, ma non c'è niente fra loro; il tempo 1.5/SR semplicemente non esiste: la scala dei tempi è discreta (a gradini), non continua.
Per esempio, se SR = 44100, allora 1/SR = 0.0000226757369615 secondi. Quindi il procedere del tempo in secondi è:
0 - 0.0000226757369615 - 0.000045351473923 - 0.0000680272108845 - ...
Qualsiasi intervallo fra questi, per es. 0.00005, non esiste proprio. Ne consegue che, se chiedete a Csound di fare un suono che parte all'istante 0 e termina all'istante 1.00005 (con SR = 44100), per il software sarà impossibile farlo. O termina a 1.000045351473923, o termina a 1.0000680272108845. La risoluzione temporale è 1/SR.
Così, a differenza dell'analogico in cui, almeno nella comune immagine mentale, si passa, senza soluzione di continuità, attraverso tutti i valori possibili, nel digitale non è così. Nello stesso modo, nell'analogico esiste sempre un ε (epsilon) piccolo a piacere che fa sì che il campo dei numeri reali (numeri con virgola) sia continuo (dati due numeri qualsiasi A e B, con A < B, si può sempre trovare un numero che sta in mezzo: A < K < B), mentre nel digitale a un certo punto ci si deve fermare (se non erro, l'aritmetica floating point a 32 bit, nel caso migliore, si ferma al 18° decimale).
Una conseguenza di tutto ciò è che il digitale è sempre approssimato, cioè contiene una certa quantità di rumore. Non si esaltino i sostenitori dell'analogico, perché il rumore del digitale può essere ridotto a livelli infinitamente inferiori rispetto a quello presente nei circuiti analogici. In realtà, la continuità dell'analogico è un'astrazione e mi fa pensare sempre al punto euclideo che non ha dimensioni.
Qui, però, non voglio nemmeno iniziare una discussione su pregi e difetti dei due sistemi. Tutto questo discorso è qui solo per introdurre l'idea di approssimazione del digitale.

Abbiamo già visto, infatti, come, in dipendenza dalla lunghezza della tabella, la forma d'onda venga calcolata in un numero finito di passi e questa è già una prima approssimazione. Anche se l'oscillatore leggesse tutti i valori della tabella, il risultato sarebbe approssimato.
Ma l'oscillatore non può leggere tutti i valori perché, per rispettare la frequenza richiesta, deve generare ogni ciclo in un tempo ben determinato. Per esempio, per ottenere un'onda a 100 Hz, ogni ciclo deve durare 1/100 secondi = 0.01 secondi (il periodo è l'inverso della frequenza).
Ora, dato che, come abbiamo accennato, i tempi devono essere trasformati in numero di campioni, supponendo un SR tipico = 44100, ogni ciclo della nostra forma d'onda sarà formato da 44100/100 = 441 campioni. Se l'SR fosse 48000, ogni ciclo avrebbe 480 campioni.
A questo punto, come può, l'oscillatore riprodurre un ciclo in 441 o 480 campioni se in tabella un ciclo è memorizzato con un numero di campioni diverso? (512, 1024, o altra potenza di 2). E in generale, come si può creare un ciclo in K campioni, diverso da quello con cui è stato memorizzato in tabella?
La risposta è semplice e introduce una approssimazione ulteriore: l'oscillatore non legge tutti i valori della tabella, ma la scandisce saltando o doppiando alcuni valori. In pratica, calcola un passo di incremento (SI = sampling increment) che gli permette di scandire la tabella nel numero di campioni richiesto.
Un esempio per chiarire. Sia
Con questi valori abbiamo un ciclo che consta di 441 campioni (SR/F). Di conseguenza la tabella dovrà essere letta con un passo di incremento SI = TL/441 = 1024/441 = 2.32199546485.
Quindi l'oscillatore leggerà dapprima il valore che si trova nella posizione 0 della tabella, poi quelli che si trovano nelle posizioni 2.32199546485, 4.6439909297, 6.96598639455, 9.2879818594, ... etc, incrementando sempre di 2.32199546485.
In questo modo l'oscillatore è in grado di scandire l'intera tabella (un ciclo) in 441 campioni generando un'onda il cui periodo è 1/100 di secondo, cioè 100 Hz.
In generale, quindi, la formula per calcolare SI a partire da SR, TL e F è SI = TL/(SR/F) = TL * F / SR. Infatti, con i numeri di cui sopra si ottiene il valore precedentemente calcolato: 1024 * 100 / 44100 = 2.32199546485.
Ovviamente, così facendo, si introduce un ulteriore grado di imprecisione perché saltando da un valore a un altro della tabella si creano gradini ancora più grandi di quelli che si avrebbero leggendo tutti i valori in successione. A parità di lunghezza della tabella, questa imprecisione aumenta via via che la frequenza aumenta. Tenendo fissi i valori di SR e TL di cui sopra e cambiando solo F, si vede che, se a 100 Hz si saltano 2.32199546485 campioni a ogni passo, a 400 Hz SI diventa 9.28798185941, a 800 Hz è 18.5759637188, eccetera. Ma la cosa assume proporzioni notevoli con frequenze molto alte: per es., a 5000 Hz, SI è 116.099773243, a 15000 Hz diventa 348.299319728 a e 20000 Hz (vicino alla frequenza di Nyquist) SI è 464.399092971. Praticamente, a questa frequenza, ci sono solo due campioni per ogni ciclo.

Nel migliore dei casi, a frequenze molto alte, più che una sinusoide, si genera una triangolare. Fortunatamente quello che si crea non è rumore, ma distorsione armonica con il primo armonico ben oltre la frequenza di Nyquist, così il tutto viene tagliato dal filtro sul DAC.
In effetti, una delle caratteristiche interessanti del wavetable oscillator è che il segnale è, per forza di cose, periodico: leggendo ciclicamente una tabella, anche riempita di numeri a caso, si crea forzatamente un ciclo. Di conseguenza, con un solo oscillatore tabellare è praticamente impossibile creare del rumore vero e proprio (non periodico), se non per cause esterne (imperfezioni nelle apparecchiature che generano per es. jitter). Come vedremo, dal punto di vista delle tecniche di sintesi, questa caratteristica è, nello stesso tempo, un vantaggio e un handicap.

Ora, però, dobbiamo affrontare un secondo scoglio. Un effetto del Sampling Increment è che all'oscillatore, spesso, è richiesta la lettura in tabella di campioni che non si trovano in posizioni intere. Per esempio, nel caso di cui sopra, con SI = 2.32199546485, l'oscillatore dovrebbe partire leggendo il valore che si trova nella posizione 0 della tabella, poi quelli che si trovano nelle posizioni 2.32199546485, 4.6439909297, 6.96598639455, 9.2879818594, ... etc, incrementando sempre di 2.32199546485.
Il problema è che non può farlo perché la tabella ha solo posizioni intere (0, 1, 2, 3, ...). Per risolvere questo problema, l'oscillatore può adottare varie tecniche, ognuna delle quali produce una diversa quantità di errore e richiede differenti tempi di calcolo o quantità di memoria. In generale i due sono legati da una relazione inversa: più errore = meno calcolo/memoria, meno errore = più calcoli/memoria.
Oscillatore a troncamento
Si tratta del sistema più semplice e più veloce, utilizzato, per es., dal modulo oscil di Csound.
La parte frazionaria è, semplicemente, ignorata. Sia che si debba leggere il campione 2.1 o 2.99, l'oscillatore va a leggere il campione 2. La posizione di lettura in tabella è

int(fase_istantanea)

(dove fase istantanea è la posizione attuale nel ciclo)

Questo sistema produce un errore pari, al massimo, alla più grande differenza che esiste fra un campione e il successivo. A sua volta, questo valore non è fisso, ma dipende dalla forma dell'onda e dalla posizione nel ciclo (fase istantanea).
sinusoide calcolata in 128 passiGuardando la figura a lato, relativa a una semplice sinusoide campionata in 128 passi, si vede a occhio nudo come la differenza fra un campione e il successivo non sia sempre uguale, ma aumenti dove la funzione ha pendenza maggiore (ovvero, dove i gradini sono più alti) 4.
Qui l'importanza della lunghezza della tabella è massima. Più lunga è la tabella, infatti, minore è il passo fra un campione e il successivo. Di conseguenza, anche lo scarto fra i valori e quindi il possibile errore, diminuisce.
Nella figura seguente (sotto) potete vedere un sonogramma che contiene, l'uno dopo l'altro, sette suoni, visibili nella parte superiore.
Tutti i suoni dovrebbero essere identici e contenere soltanto una sinusoide a 1000 Hz sintetizzata con il modulo oscil di Csound (oscillatore a troncamento). L'unica differenza fra loro è la lunghezza della tabella, che raddoppia sempre dal primo all'ultimo, essendo 128, 256, 512, 1024, 2048, 4096, 8192.
Il sonogramma dovrebbe mostrare una singola linea, molto forte, a 1000 Hz, mentre, come si vede chiaramente, solo gli ultimi due spettri (con tabella di 4096 e 8192 punti) sono abbastanza puliti. Gli altri evidenziano delle componenti che non fanno parte del segnale desiderato.
Si vede anche che le componenti indesiderate si attenuano via via che la lunghezza della tabella aumenta. C'è da dire che, in realtà, non spariscono mai del tutto, ma quando la loro ampiezza è 80 db sotto al segnale primario, sono praticamente ininfluenti.
analisi di 7 suoni con lunghezza
        tabella crescente
Quindi, un buon consiglio è: non usare tabelle inferiori a 4096 punti con un oscillatore audio a troncamento.
Oscillatore ad arrotondamento
Un sistema per ridurre l'errore dato dal troncamento è quello di arrotondare la fase all'intero più prossimo, invece di troncarla del tutto. Così, se la fase è 2.3, si prende il campione 2, mentre se è 2.8 si prende il 3. In termini di tempo di calcolo, basta sommare 0.5 alla fase istantanea, prima di troncarla. In tal modo, si resta all'intero inferiore se la parte frazionaria della fase è < 0.5 e si va a quello superiore se è >= 0.5.
La posizione di lettura in tabella è

int(fase_istantanea + 0.5)

Qui l'errore massimo corrisponde all'incirca alla metà della più grande differenza che esiste fra un campione e il successivo (e quindi dipende anche dalla forma d'onda).
Con questo sistema si possono utilizzare tabelle di lunghezza pari alla metà del precedente, ma il risparmio di pochi kbyte non vale il tempo perso per la somma e conviene passare a un sistema più complesso, in grado di ridurre sensibilmente l'errore, come il seguente.
Oscillatore a interpolazione
Con questo metodo si va a calcolare il valore presunto del campione mediante una qualche forma di interpolazione fra i valori del precedente e del successivo (eventualmente si possono considerare più di due campioni).
Per esempio, se la fase è 5.3, si stima quale valore avrebbe un ipotetico campione in 5.3, considerando il valore del campione 5 e del 6, oppure, per una stima migliore, si possono prendere in considerazione i valori dei campioni 4, 5, 6 e 7.
Chiaramente, con questi sistemi, il tempo di calcolo aumenta notevolmente, però il risultato è molto più pulito anche con tabelle relativamente corte.
I possibili sistemi di interpolazione sono molti. Qui tratteremo solo quelli utilizzati in Csound e Max/MSP. Per una trattazione generale, si veda la voce in Wikipedia.
In pratica, interpolare significa trovare una funzione che passi per i punti dati e poi calcolare il valore che questa funzione assume nel punto richiesto. La funzione è un polinomio, cioè un'espressione della forma:

y = K1xn + K2xn-1 + K3xn-2 + ... + Knx + K
(per es. 2x4 - 5x3 + 6x2 + 7x - 3; notare che i coefficienti Kn possono essere negativi)

Il metodo generale è quello di Lagrange che ha dimostrato che, dati N punti, si può sempre trovare un polinomio di grado N-1 (cioè il cui esponente massimo è N-1) che passa per tutti i punti dati. Di conseguenza, se si considerano solo 2 punti, la funzione interpolante è una retta (y = mx + p; grado 1) e abbiamo la cosiddetta interpolazione lineare. Se si interpola su 4 punti, la funzione è una cubica (y = K1x3 + K2x2 + K3x + K; grado 3). Via via che il numero di punti su cui si interpola aumenta, anche l'approssimazione migliora, ma aumentano anche i calcoli necessari per definire la funzione e per calcolare il valore richiesto.
Interpolazione lineare
interpolazione lineareQuesto sistema approssima il valore del campione cercato, immaginando che i punti della forma d'onda in tabella siano collegati da segmenti di retta. In pratica, supponendo che si debba stimare il valore dell'onda in 5.3 e che il campione 5 valga 10 e il campione 6 valga 20, nell'interpolazione lineare si immagina che, in questo intervallo, l'onda cambi linearmente (appunto), passando da 10 a 20. Di conseguenza, nel punto 5.1 varrà 11, in 5.2 varrà 12, in 5.3 13, in 5.4 14, ... , in 5.9 19.
Quindi, per la posizione 5.3 l'oscillatore a interpolazione lineare assumerà un valore di 13.
Il problema di questo metodo, però, è che l'interpolazione lineare è una buona approssimazione solo se la funzione (in questo caso, l'onda) non ha curve 5 molto pronunciate.
L'immagine a lato, che si ingrandisce cliccandola, mostra una sinusoide (in rosso) e la sua approssimazione con un certo numero di segmenti (in cyan).
Come si vede, in alcune parti la curva e la sua approssimazione coincidono abbastanza bene, mentre in altre, dove la funzione è più curva, divergono nettamente. Considerate che funzioni con armonici hanno curve più pronunciate rispetto alla singola sinusoide.
L'errore dell'approssimazione dipende dalla funzione in tabella e dal punto in cui ci si trova. In generale, comunque, l'errore medio dipende dalla distanza che separa i due punti, quindi, anche qui, si riduce con tabelle più lunghe. Il risultato è comunque nettamente migliore rispetto all'oscillatore a troncamento

In Csound, il modulo oscili e altri da lui derivati (e.g. oscil1i) utilizzano l'interpolazione lineare. Si noti che questi moduli necessitano di una tabella dotata di "wrap-around guard point", cioè di un punto in più, posto alla fine, in cui è duplicato il valore del primo campione, per avere un punto finale con cui interpolare quando la fase eccede di una frazione la lunghezza - 1.
In Max/MSP, cycle~ usa l'interpolazione lineare su una tabella lunga 512 campioni dotata di "wrap-around guard point" come sopra. La lunghezza della tabella è fissa e non può essere cambiata dall'utente (per tabelle di lunghezza qualsiasi si usa wave~).
Interpolazione cubica
cubicaIl modulo oscil3 di Csound utilizza, in via sperimentale, l'interpolazione cubica basata su 4 punti (2 prima e 2 dopo). Nel caso di cui sopra (fase 5.3), la funzione interpolante considera i valori dei campioni 4, 5, 6, 7.
Con adeguati coefficienti, la cubica può assumere forme come quella mostrata nella figura a destra, particolarmente adatte ad approssimare sinusoidi o altre funzioni con curve pronunciate.
L'errore è decisamente inferiore rispetto a quello generato dall'interpolazione lineare.
Considerazioni sull'interpolazione
Intanto, un confronto temporale. Nella seguente tabella trovate i tempi di calcolo per la generazione di una sinusoide a 1000 Hz con uno strumento composto da un solo oscillatore senza inviluppo con out monofonico (vedi ex02.csd).
Per ogni modulo è stato generato un singolo suono di durata = 5000 secondi (circa 83 minuti) senza scrittura su disco (opzione -n). L'operazione è stata ripetuta 5 volte, facendo, poi, la media dei tempi (che comunque differivano di poco).
I tempi sono totali, cioè comprendono l'apertura e la chiusura del job, compresa la compilazione dell'orchestra (solo 3 msec) e sono stati calcolati con il comando unix time.
Il test è stato eseguito su un dual core a 2.2 Ghz con Ubuntu Linux 10.04 e Csound versione 5.10. Con altre macchine e/o sistemi si potrebbero ottenere tempi diversi, ma, ribadisco, qui non c'entra la velocità del disco rigido perché Csound è stato lanciato senza scrittura (opzione -n). Quelli che vedete sono i tempi di puro calcolo.

modulo
media percentuale
oscil 4.35 100.00
oscili 5.46
125.52
oscil3 8.42
193.56

Come si vede, rispetto all'oscillatore a troncamento, i tempi di calcolo aumentano del 25% con l'interpolazione lineare e del 93% con l'interpolazione cubica. Considerate che questo aumento è relativo ad un solo oscillatore. Se in uno strumento ce ne sono più di uno, l'aumento va moltiplicato.

Ora, però, è il caso di fare anche un test di qualità. Nella figura seguente vedete lo spettro di una sinusoide a 1000 Hz generata con tabella di 1024 punti da oscillatore a troncamento, interpolazione lineare e cubica.

confronto qualità

Si vede chiaramente come nel primo suono (troncamento) ci siano componenti indesiderate, mentre gli altri due (interpolazione lineare e cubica, rispettivamente) siano puliti.

Quindi, quale tipo di oscillatore conviene usare? A mio avviso, con i computer attuali, conviene usare l'interpolazione lineare con tabelle lunghe o il troncamento con tabelle lunghissime. Con una tabella di 16777216 punti, la distanza fra un campione è il successivo è di soli 0.000021458 gradi (0.000000375 radianti) che corrisponde a uno scarto fra un valore e il successivo nell'ordine di 10-7, quindi l'errore medio è piccolissimo.
Considerate che l'interpolazione è stata introdotta nei primi software di sintesi per poter risparmiare memoria usando tabelle corte. Quando, negli anni '70, lavoravo al CSC di Padova usando l'antenato di Csound (il Music360 dello stesso Barry Vercoe), avevo un'area di memoria di 256 Kb (kappa). Usarne 32 per una tabella di 8192 campioni (8192 * 4 byte) era uno spreco. Oggi il problema non si pone e si possono tranquillamente utilizzare tabelle da 16 Mb senza interpolazione risparmiando in tempo, che è una cosa non recuperabile.

Altre particolarità

Frequenza negativa
Nell'oscillatore analogico una frequenza negativa non ha significato. In quello digitale, invece, sì.
L'oscillatore si limita a calcolare il SI con la formula di cui sopra e il fatto che F abbia un valore negativo significa soltanto che anche SI è < 0. Nel caso già visto di F = 100 Hz, in cui il SI era 2.32199546485, con F = -100 Hz sarà SI = -2.32199546485.
Grazie anche alla particolare codifica binaria degli interi negativi e all'operazione di AND sulla lunghezza della tabella, questo significa semplicemente che l'oscillatore scandirà la tabella all'indietro, cioè non dal primo all'ultimo valore, ma dall'ultimo al primo. Tutto ciò si traduce soltanto in un fatto: l'onda verrà generata con fase invertita rispetto alla lettura normale. Di conseguenza, l'esecuzione contemporanea di due suoni con la stessa tabella e frequenza identica, ma una negativa, produce il nulla, come si può vedere lanciando l'esempio ex04 6.
Questa particolarità è da ricordare perché, a volte, con certe tecniche di sintesi, si generano delle componenti con frequenza negativa, cioè con fase invertita.
Foldover
Come è noto, in digitale non si possono generare frequenze superiori alla frequenza di Nyquist (ovvero SR/2). Per es., con SR = 44100, non possono esistere frequenze > 22050. Ma cosa accade se si chiede a un oscillatore di generare una frequenza > SR/2, per es. 30000 o 40000?
Esaminiamo in dettaglio il meccanismo di lettura.
Se la frequenza richiesta è < 22050 (SR/2), tutto procede normalmente. Per es, con frequenza = 11000 e lunghezza tabella = 4096, abbiamo:
Con la formula già vista SI = TL * F / SR, abbiamo SI = 4096 * 11000 / 44100 = 1021.67800454.
Questo valore corrisponde a circa ¼ della lunghezza della tabella. Di conseguenza la lettura procederà come in figura seguente. Sotto (in nero) abbiamo la sinusoide memorizzata in tabella lunga 4096 campioni. Sopra (in verde), la sinusoide generata. Si vede come il meccanismo proceda regolarmente: partendo dal primo campione procede a passi di circa 1021 fino alla fine della tabella, per poi ciclare.

lettura in tabella con freq 11000
        Hz

Ora, invece, chiediamo all'oscillatore di generare una frequenza > SR/2.
L'oscillatore non si rifiuta. Come al solito calcola il suo SI e procede, ma il SI sarà così grande che l'oscillatore produrrà solo un campione per ciclo, o ancora meno, con l'effetto di non riprodurre la forma d'onda in tabella, ma di generarne una a frequenza inferiore perché troppe parti della tabella saranno saltate.
Per esempio, sia
Con la formula già vista SI = TL * F / SR, abbiamo SI = 4096 * 40000 / 44100 = 3715.19274376.
Con questo SI, il primo campione sarà a 0, il secondo a 3715.19274376 e il terzo a 7430.38548752. Ma quest'ultimo valore è già ampiamente oltre la lunghezza della tabella, per cui si esegue un ritorno a capo e si va al campione 7430.38548752 - 4096 = 3334.38548752.
Quello che accade è schematizzato in figura sotto.

lettura in tabella con freq 40000
        Hz

Come si vede, la situazione è un po' più strana rispetto alla precedente. Come è normale, si parte dal primo campione, ma il secondo (circa 3715) è così avanti da trovarsi vicino alla fine della tabella. Di conseguenza, già con il terzo, che sarebbe a 7430.38548752, si ritorna a capo, andando al campione 7430.38548752 - 4096 =3334.38548752 e così via.
Una prima conseguenza, apprezzabile anche in figura, è che la fase dell'onda è invertita, come se la tabella venisse letta all'indietro. E in effetti è così: il SI è così grande che il punto di lettura si muove, praticamente, all'indietro (un effetto analogo si può vedere nei vecchi film western in bianco e nero, in cui la frequenza dei fotogrammi era così bassa che le ruote delle diligenze sembravano ruotare all'indietro).
Ma quello della fase è il problema minore. Il problema maggiore è che, in tal modo, il ciclo non viene riprodotto correttamente. Se, infatti, sovrapponiamo le due onde così generate, mettendole nella giusta scala, vediamo (fig. sotto) che l'onda a 40000 Hz (rossa) ha un periodo ben più lungo di quella a 11000 (verde). Se ne deduce che l'onda generata ha frequenza inferiore a quella prevista.

foldover

Questo accade perché il SI dell'onda a 40000 Hz è così grande che non è in grado di campionare adeguatamente l'onda in tabella prendendo un solo campione per ogni ciclo, mentre ne sarebbero necessari almeno due.
Per spiegare la cosa con una analogia, è come se, dovendo fare un grafico della variazione di temperatura nella vostra città, voi andaste a misurarla solo una volta al giorno, mentre invece, per avere un risultato realistico, avete bisogno almeno di due misure: una nella zona di massimo (per es. alle 14) e una in quella di minimo (es. dopo mezzanotte). Se invece leggete le temperature solo di giorno (o di notte) otterrete un grafico che sovrastima (sottostima) il valore medio.
In modo analogo, anche nel caso dell'oscillatore digitale servono almeno due campioni per ciclo per poter riprodurre una forma d'onda con la frequenza corretta, altrimenti avremo un errore. Notare che il limite dei due campioni per ciclo si raggiunge proprio con frequenza pari a SR/2, infatti, dalla formula già vista, SI = TL * F / SR, abbiamo SI = TL * SR/2 / SR = TL * SR / 2 * SR = TL / 2.
Alla frequenza di SR/2, quindi, il SI è pari a metà della lunghezza della tabella. Oltre questa frequenza SI > TL/2, quindi abbiamo meno di un campione per ciclo. Di conseguenza, oltre questo muro, la frequenza prodotta differisce quella richiesta.
È facile calcolare qual'è la frequenza effettivamente prodotta: basta sottrarla da SR. Infatti, se freq > SR/2, la frequenza effettiva è SR - freq.
Es. sia SR 44100 e si chiede freq = 30000, il risultato è una frequenza = 44100 - 30000 = 14100.
Questo fenomeno è detto foldover (ripiegamento della parte che sta sopra), infatti la parte di frequenza che eccede SR/2 si ripiega intorno a SR/2. In pratica, rimbalza indietro, come se il valore SR/2 fosse un muro (come effettivamente è).
A titolo di esempio, ascoltate questo suono creato con Csound, che contiene un semplice oscillatore sinusoidale la cui frequenza glissa con continuità superando il valore di SR/2. Per non mettere a dura prova il vostro udito, qui la frequenza di campionamento è 22050 e la frequenza della sinusoide va da 5000 Hz a 20000 Hz in 20 secondi. Si dovrebbe sentire un glissato sempre ascendente, mentre, a un certo punto (a SR/2 = 11025 Hz), la frequenza comincia a scendere, come schematizzato in figura. Sono in ogni caso frequenze alte: attenzione alle orecchie. Clicca qui per ascoltare l'esempio e qui per vedere il codice Csound.

effetto del foldover

Ovviamente bisogna fare i conti con il foldover considerando non solo la fondamentale, ma anche tutte le componenti del nostro suono. Se, per esempio, stiamo lavorando con una forma d'onda che contiene una ventina di armonici, bisogna pensare che a 1000 Hz il ventesimo armonico è già a 20000 Hz e di conseguenza, con SR = 44100, si produrrà foldover già quando la fondamentale raggiunge i 1102.5 Hz.


[1] Si può pensare che Csound si comporti così perché, di solito, non viene utilizzato in tempo reale, ma scrive in un file. Ebbene, non è così. Anche i software concepiti per il real time, sebbene siano "interrupt driven" (come Max/MSP), contano solo i campioni e seguono solo in parte il tempo reale. Il loro legame con quest'ultimo è, sì, determinato dall'interrupt, ma i software lavorano in un "tempo logico" che è leggermente sfasato rispetto al tempo reale. Questo perché devono riempire di campioni audio un piccolo buffer prima che i suddetti campioni debbano essere spediti al DAC (in Max/MSP la dimensione di tale buffer è l'I/O Vector Size controllabile dal box DSP Status).
Di conseguenza sono in ritardo rispetto al tempo reale (è la cosiddetta latenza). Ciò accade perché, in questo preciso istante, il software sta, in realtà, calcolando i prossimi (per es.) 10 millisecondi di suono. Ne consegue che, se un evento audio esterno (es. l'input da un microfono) arriva adesso, il suo effetto si sentirà solo fra 10 millisecondi, cioè quando il buffer che adesso è in via di riempimento verrà mandato in output (ricordo chiaramente il piccolo shock che questa cosa mi ha provocato quando, ai tempi dell'Apple II, ho scritto in assembler il mio primo scheduler di eventi audio per le schede della Mountain Hardware).
Ma tutto questo, ormai, è accademia. Chi volesse approfondire può consultare, fra gli altri, il testo di Miller Puckette "The Theory and Technique of Electronic Music", cap. 3.2, pag. 61 segg.

[2] Incidentalmente, questa è anche la ragione per cui non si possono mixare via software segnali a SR diverso e se un software lo fa, esegue una conversione preventiva senza dirvelo. Sempre incidentalmente, questo è quello che accade ogni volta che un sistema operativo riproduce suoni con la scheda audio in dotazione: tutti i segnali vengono convertiti al SR di quella scheda (tipicamente 48000 per le schede on-board).

[3] In Max/MSP, per certi moduli, il minimo intervallo temporale dipende anche da altri fattori, come la dimensione del vettore di I/O.

[4] Ovviamente è più elegante dire dove il coefficiente angolare della derivata prima è maggiore.

[5] Modo di dire per spiegare la cosa a chi è digiuno di matematica. Ovviamente si dovrebbe guardare la derivata seconda.

[6] Qualcuno si chiederà perché l'ampiezza totale viene ad essere 23 e non 0. Il fatto è che le due onde sono, sì, in fase invertita l'una rispetto all'altra, ma hanno anche un campione di differenza. Questo perché il primo campione della tabella ha fase 0, ma l'ultimo campione non ha fase 360° (2π), bensì 360 meno un campione.



Index
Back