• Non ci sono risultati.

Altri segnali e altri algoritm

Nel documento Introduzione a SuperCollider (pagine 100-112)

Introduzione a SuperCollider

3. conversione digitale-analogica: per essere ascoltabile, la sequenza numeri ca che compone il segnale viene riconvertita in segnale analogico attraverso

4.5 Altri segnali e altri algoritm

Altri segnali periodici possono essere generati in forma trigonometrica/pro- cedurale, attraverso una somma di sinusoidi calibrata in ampiezza. È il caso ad esempio dell’onda triangolare. Per quanto descritto in forma discreta, il metodo ha una sua fondatezza di derivazione continua. Tuttavia nel dominio discreto sono anche possibili metodi diversi, definiti tipicamente come “non standard”. Ad esempio, si può assumere un approccio di tipo geometrico. Il periodo di un’onda triangolare può essere pensato come costituito di quattro segmenti: il

primo nell’intervallo[0.0, 1.0], il secondo in quello [1.0, 0.0], il terzo in quello

[0.0, −1.0] e il quarto in quello [−1.0, 0]. 1 // Onda triangolare per segmenti

3 var first, second, third, fourth, total ;

4 var size = 50 , step; 6 step = 1.0/size ;

7 first = Signal.series(size, 0, step) ;

8 second = (first+step).reverse ;

9 third = second-1 ;

10 fourth = first-1 ;

11 total = (first++second++third++fourth) ;

13 total.plot;

Nell’esempio la variabile size rappresenta la dimensione degli array che contengono i quattro segmenti, mentre step è l’incremento del valore di am- piezza. Il primo segmento è allora un array di tipo Signal riempito da un nu-

mero𝑠𝑡𝑒𝑝 di valori con incremento 𝑠𝑡𝑒𝑝: contiene valori da 0 a 1 − 𝑠𝑡𝑒𝑝 (7). Il

secondo segmento segue il percorso contrario: il metodo reverse restituisce un array leggendo dall’ultimo al primo elemento l’array sui cui è chiamato. Prima, viene aggiunto uno step ad ognuno degli elementi dell’array: second contiene

valori da1 a 0 + 𝑠𝑡𝑒𝑝. I segmenti successivi sono ottenuti generando due array

third e fourth che sottraggono1 rispettivamente a second e first, cioè li “tra-

slano in basso” (9, 10). Infine l’array total è ottenuto concatenando i quattro segmenti. Si noti che le operazioni di addizione (come le altre operazioni sugli array) restituiscono un array in cui ognuno degli elementi risulta dall’applica- zione dell’operazione sul rispettivo elemento dell’array di partenza. Ovvero:

1 [1, 2, 3, 4]*2 2 [ 2, 4, 6, 8 ]

Il metodo di sintesi geometrico permette in effetti di ottenere risultati mi- gliori di una somma di sinusoidi che può soltanto ad approssimare l’onda trian- golare. Sommando sinusoidi sarebbero necessarie infinite componenti per ge- nerare un’onda triangolare: gli spigoli risultanti sono sempre un po’ “smussati”. Rispetto all’esempio è possibile un’osservazione analoga a quella preceden- te relativa a tempo reale/differito. Un approccio a segmenti deve prima calcola- re i segmenti per intero e poi connetterli: non è dunque una tecnica ottimale per il tempo reale. Un approccio alternativo è presentato nell’esempio seguente:

1 // Onda triangolare per serie e cambio segno 3 var size = 100, freq = 2 ;

4 var step, val, start = 0, sign = 1 ;

5 var sig = Signal.newClear(size) ; 6 step = 1.0/size*(freq*4);

7 val = 0 ;

8 sig.size.do{ arg x;

9 if((val >= 1) || (val<=1.neg)) { 10 sign = sign.neg

11 } ;

12 val = val+(step*sign); 13 sig[x] = val ;

14 } ;

16 sig.plot ;

L’idea è in questo caso nasce dall’osservazione che la forma d’onda è una retta la cui pendenza viene invertita quando un limite viene raggiunto, ovvero i due picchi simmetrici per valore, positivo e negativo ([−1, +1]). La variabile step (6) determina la pendenza della retta. Quattro sono i segmenti in gioco,

e freq li moltiplica per il numero delle loro ripetizioni (in questo caso,2). Se

un’altra di frequenza doppia, diventa chiaro che la pendenza della seconda è il doppio (sale/scende a “doppia velocità”) della prima (Figura 4.8).

0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1 0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1

Fig. 4.8 Onde triangolari con frequenza𝑓 e 2𝑓

Definito perciò il significato di step, si inizializza val, il valore del primo cam- pione (7). Il ciclo itera sui campioni di sig (8-14). Verifica che val non sia mag- giore del picco positivo o minore del negativo, e in quel caso aggiunge a val un incremento step (12). L’incremento però è moltiplicato per sign.

1 0.216.sign 2 1

4 -0.216.sign 5 -1

Inizialmente sign vale1, dunque non sposta il valore dell’incremento. Se

però un picco è superato, allora sign è invertito con sign.neg (9-11). In altri

termini, l’onda cresce fino a1 e da lì decresce (incremento negativo) fino a −1.

Tornando alla sintesi attraverso componenti sinusoidali, l’esempio già visto di somma di sinusoidi dimostra la rilevanza della fase. Infatti, le 50 componenti in fase crescono tutte insieme a inizio ciclo producendo il picco iniziale per poi

“neutralizzarsi” fino al picco negativo di fase𝜋4. In Figura 4.9, in alto, sono

riportate le fasi delle componenti (sempre0) e il segnale risultante.

0 10 20 30 40 50 0 1.26 2.51 3.77 5.03 0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 0 10 20 30 40 50 0 1.26 2.51 3.77 5.03 6.28 0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1

Fig. 4.9 Somma di sinusoidi: in alto da sinistra, fase 0 e segnale; in basso da sinistra, fase casuale e segnale.

Nell’esempio seguente la variabile harmonics contiene il numero delle armoni- che. L’array phases è invece creato attraverso il metodo fill (3) che prevede di specificare una dimensione (qui harmonics) e una funzione che viene applicata per il calcolo di ognuno degli elementi. Qui la funzione è {2pi.rand}, che spe-

cifica di scegliere un valore casuale tra[0, 2𝜋], cioè una fase casuale nell’intero

ciclo. Le ampiezze delle componenti sono tutte pari a1 (4) e stoccate in ampli-

tudes. Infine, sig è generato come somma di sinusoidi armoniche con la stessa ampiezza ma fase casuale.

1 var sig, harmonics = 50 ;

2 //var phases = Array.fill(harmonics, {0});

3 var phases = Array.fill(harmonics, {2pi.rand});

4 var amplitudes = Array.fill(harmonics, {1}) ;

5 sig = Signal.sineFill(1024, amplitudes, phases) ; 6 sig.plot ;

In questo caso, il valore d’ampiezza di ognuna delle componente è pari a 1 ma generato dalla funzione {1.0.rand} ed è un valore pseudo-casuale com-

codice più volte si noterà che il suono cambia, poiché l’apporto delle compo- nenti dipende dalla funzione del metodo fill. Se si incrementa o decrementa il valore di partials il segnale rispettivamente si arricchisce o si impoverisce di componenti elevate. La riga 7 permette di visualizzare l’array delle ampiez- ze come una spezzata che unisce i valori discreti che lo compongono: con plot si possono specificare i valori d’escursione massimi e minimi ([0, 1]). Si tratta, come si vedrà, di una forma di sintesi additiva. In Figura 4.9, in basso, sono

riportate le fasi delle componenti (casuali nell’escursione[0, 2𝜋]) e il segnale ri-

sultante. Si noti che il contenuto armonico è uguale ma la forma d’onda è radi- calmente diversa. Se si esegue il codice più volte si noterà che la forma d’onda cambia, poiché l’apporto delle componenti dipende dalla funzione stocastica definita nel metodo fill per le fasi.

L’introduzione dei numeri pseudo-casuali permette di avvicinare anche i segnali non periodici. Un rumore bianco è un segnale il cui comportamento non è predicibile se non in termini statistici. Questo comportamento si traduce in una distribuzione uniforme dell’energia su tutto lo spettro del segnale. Un rumore bianco può essere descritto come una variazione totalmente aperiodica nel tempo: in altre parole, il valore di ogni campione è del tutto indipendente

da quelli precedenti e da quelli successivi5. Dunque, il valore di un campione𝑥

è indipendente dal valore del campione precedente𝑥 − 1: 𝑥 può avere qualsiasi

valore, sempre, evidentemente, all’interno dello spazio finito di rappresentazio- ne dell’ampiezza. Intuibilmente, l’algoritmo di generazione è molto semplice. In pseudo-codice:

Per ogni x in A: y = a*rand(-1, 1) A[x] = y

Nell’esempio seguente, il metodo fill (6) valuta per ogni campione x una funzione che restituisce un valore casuale all’interno dell’escursione normaliz-

zata[−1, 1] (rrand(-1.0,1.0)). Il codice specifica una frequenza di campiona-

mento (sr) e una durata in secondi (dur). Il risultato (un secondo di rumore

bianco) viene scalato per amp, e dunque, dato un valore di0.75 per amp, si otter-

rà un’oscillazione (pseudo-)casuale nell’escursione[−0.75, 0.75]. Si noti come la

funzione (nella definizione di partenza𝑓[𝑥]) in realtà sia calcolata indipenden-

te da𝑥: essendo totalmente aperiodica, il suo comportamento non dipende dal

tempo.

1 var sig, amp = 0.75, dur = 1, sr = 44100 ;

2 sig = Signal.fill(dur*sr, { amp*rrand(-1.0,1.0) }) ;

3 sig.plot(minval:-1, maxval:1) ;

I software rappresentano tipicamente il segnale audio attraverso una curva continua che connette i valori d’ampiezza. Nel caso del rumore, la rappresen- tazione continua è decisamente meno icastica di una rappresentazione discreta (Figura 4.10), in cui chiaramente emerge l’idea di una sorta di nuvola di punti distribuiti pseudo-casualmente. 0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1 0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1

Fig. 4.10 Rumore bianco: curva continua e dispersione dei valori.

È un’ovvietà ma è bene ricordarsi che un segnale digitale è appunto semplice- mente una sequenza di valori, sulla quale è possibile svolgere operazioni ma- tematiche. Tutta l’elaborazione digitale del segnale nasce da quest’assunto. I

prossimi sono due esempi tratti da Puckette6. Una funzione periodica, ad esem-

pio al solito la funzione seno, oscilla periodicamente tra[−1, 1]. Calcolando il

suo valore assoluto semplicemente si ribalta la parte negativa sul positivo: se si osserva la curva si nota come i due emicicli siano identici: ne consegue un segna- le con frequenza doppia di quello di partenza. In generale l’applicazione della funzione del valore assoluto è una tecnica rapida per far saltare d’ottava un se-

gnale. Poiché la curva occupa soltanto valori positivi (tra[0, 1] in caso di valori

normalizzati), è possibile traslarla in modo da evitare un offset rispetto allo ze-

ro (il segnale da unipolare ritorna bipolare): decrementando di0.5 l’escursione

del segnale diventa[−0.5, 0.5] ed è simmetrica tra positivo e negativo (Figura

6 M. Puckette, The Theory and Technique of Electronic Music, accessibile on line:

4.11). Nell’esempio seguente, il metodo abs invocato su un oggetto Signal re- stituisce un altro oggetto Signal in cui ogni elemento è il risultato del valore assoluto dell’elemento del segnale di partenza (è come chiamare abs per ogni elemento). Lo stesso vale ovviamente per la sottrazione e per la moltiplicazione: si applicano a tutti gli elementi dell’array. La sinusoide viene dunque moltipli-

cata per𝑎𝑚𝑝 e traslata verso il negativo di𝑎𝑚𝑝2 , qui implementato con amp*0.57.

La situazione è rappresentata in Figura 4.11. 1 var sig, amp = 0.75 ;

2 sig = (Signal.sineFill(100, [1]).abs)*amp-(amp*0.5) ;

3 sig.plot(minval:-1, maxval: 1) ;

0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1 0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1

Fig. 4.11 Valore assoluto della funzione seno e traslazione (𝐴 = 0.75).

0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1 0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1 Fig. 4.12 𝑦 = 𝑠𝑖𝑛(2𝜋 × 𝑥)2, 𝑦 = 𝑠𝑖𝑛(2𝜋 × 𝑥)2− 0.5

7 È una tipica prassi informatica. La moltiplicazione costa, in termini computa-

segnale asimmetrico rispetto all’ampiezza, e la presenza di uno spigolo mol- to acuto (il punto di ribaltamento) che introduce molte componenti armoniche superiori. L’utilizzo dell’elevamento al quadrato evita questa caratteristica ot- tenendo un risultato analogo: infatti il quadrato dei valori negativi del segnale del segnale è positivo, e si ottiene un ribaltamento della curva sostanzialmente analogo a quanto avviene con il valore assoluto (Figura 4.12).

1 var sig, amp = 0.75 ;

2 sig = (Signal.sineFill(100, [1]).squared)*amp-(amp*0.5) ;

3 sig.plot(minval:-1, maxval: 1) ;

Come ulteriore esempio di elaborazione del segnale si può considerare l’ope- razione di “clippping”: in caso di clipping tutti i valori superiori ad una certa

soglia𝑠 (o inferiori al negativo della stessa) vengono riportati a ±𝑠. Il clipping

prende anche il nome di “distorsione digitale” perché è quanto avviene quando il segnale digitalizzato ha un’ampiezza superiore a quella rappresentabile dalla quantizzazione e viene perciò “tagliato” agli estremi. Il clipping è una sorta di limiter (limita infatti l’ampiezza del segnale) radicale e può essere usato come “effetto” di tipo distorcente. In SC è definito il metodo clip2(t) che “taglia” un

valore fuori dell’escursione[−𝑡, 𝑡] a ±𝑡. Si consideri l’esempio seguente, in cui

𝑡 = 3. 1 1.clip2(3) 2 1 4 -1.clip2(3) 5 -1 7 4.clip2(3) 8 3 10 -4.clip2(3) 11 -3

Anche se clip2 è definito per Signal, come esercizio può essere interessante implementare un modulo “clipper”.

1 var sig, sig2, sig3, clipFunc ; 2 sig = Signal.sineFill(100, [1]) ;

4 clipFunc = { arg signal, threshold = 0.5 ;

5 var clipSig = Signal.newClear(signal.size) ;

6 signal.do({ arg item, index;

7 var val ;

8 val = if (item.abs < threshold, { item.abs }, 9 { threshold}) ;

10 val = val*item.sign ; 11 clipSig.put(index, val) ; 12 }) ;

13 clipSig ;

14 } ;

16 sig2 = clipFunc.value( sig ) ;

17 sig3 = clipFunc.value( sig, threshold: 0.75 ) ;

19 [sig, sig2, sig3].flop.flat.plot

20 (minval:-1, maxval: 1, numChannels:3) ;

Si è detto che una funzione esegue un comportamento quando riceve il mes- saggio value in funzione degli argomenti in entrata. Nell’esempio è appunto definita una funzione che implementa il clipping, clipFunc: gli argomenti in entrata prevedono un segnale (un oggetto Signal) e un valore di soglia (thre- shold). Ciò che clipFunc restituisce è un altro oggetto Signal. La riga 5 dichiara la variabile a cui viene assegnato il segnale e gli assegna subito un oggetto Si-

gnal di dimensione pari a quella del segnale in entrata, riempito di0. L’idea

è quella di valutare ognuno degli elementi (che rappresentano campioni au- dio) di signal: si tratta cioè di ciclare su signal (6). Nel ciclo la variabile val rappresenta il valore che dovrà essere scritto nell’array clipSig. Il valore da assegnare a val dipende da una condizione. Se il campione valutato è all’inter-

no dell’escursione[−𝑡ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑, 𝑡ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑] allora non ci sarà clipping: val ha lo

stesso valore del valore di signal in entrata. Se invece il valore in entrata ca- de all’esterno dell’intervallo, allora in uscita si avrà il valore della soglia stessa. Il condizionale è contenuto nelle righe 8-9. La valutazione avviene sul valore

assoluto di item (item.abs). Avviene cioè nell’intervallo[0, 1]. In uscita si ha lo

stesso valore assoluto di item oppure threshold. Se il valore in entrata è nega- tivo, verrebbe così restituito un valore positivo. Il segno del valore in entrata

segno di item: infatti, item.sign restituisce±1. Se item = -0.4 si ha:

• item.abs = 0.4

• è inferiore a threshold = 0.5? Sì, allora val = item.abs = 0.4 • item.sign = -1 (item = -0.4: è negativo)

• val = 0.4 * -1 = -0.4

Seguono due esempi, uno che sfrutta il valore predefinito di threshold (0.5), l’altro in cui threshold vale 0.75. Il clipping tende a “squadrare” la forma d’onda ed in effetti il segnale risultante tende ad assomigliare –anche all’ascol- to– ad un’onda quadra. Si notino anche le righe 19-20. Il metodo plot è definito per un array, ma prevede l’argomento numChannels, che tratta la sequenza di valori come se fosse un insieme interallacciato di valori di più canali audio. Quindi, con numChannels = 3 vengono disegnate tre curve, prendendo in suc- cessione tre campioni come fossero valori della prima, della seconda, della ter- za. Si tratta di costruire un array opportuno perché possano essere disegnati i tre array di partenza. Con il metodo flop un array di array viene riconfigurato così: nel nostro caso, si passa da tre array di cento valori a cento array di tre va- lori. Il metodo flat appiattisce l’array che a questo punto può essere disegnato specificando che i canali sono tre. Si inseriscano gli opportuni messaggi postln per capire analiticamente la situazione. Il risultato è in Figura 4.13.

0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1 0 10 20 30 40 50 60 70 80 90 100 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1

Fig. 4.13 Clipping di una sinusoide, threshold = 0.5, threshold = 0.75. Esistono altri modi ben più eleganti di implementare clipFunc. L’esempio suc- cessivo utilizza il metodo collect, che, insieme ad altri metodi analoghi, è ere- ditato dalla classe Collection. Questi metodi, chiamati per un oggetto di tipo collezione, restituiscono una nuova collezione applicando in modo diverso per ogni metodo, una funzione per ogni elemento. Se l’approccio precedente era tipico del paradigma imperativo, questi metodi invece hanno una vocazione

funzionale (e sono molto eleganti, cioè chiari e sintetici). Nell’esempio seguen- te, collect applica ad ogni elemento la funzione definita al suo interno.

1 clipFunc = { arg signal, threshold = 0.5 ;

2 signal.collect({ arg item;

3 var val, sign ; 4 sign = item.sign ;

5 val = if (item.abs < threshold, { item.abs }, 6 { threshold}) ;

7 val*sign ; 8 }) ;

9 } ;

Una funzione restituisce il valore dell’ultima espressione che la compone. Qui c’è un’unica espressione, che restituisce un Signal con l’applicazione della funzione (analoga all’implementazione precedente) ad ogni elemento. Si noti che interessava mantenere la modularità di clipFunc ma si potrebbe diretta- mente chiamare collect qui definito sull’oggetto Signal di partenza.

L’approccio modulare dell’esempio precedente permette di riscrivere i casi relativi al valore assoluto e al quadrato. In particolare si potrebbero esplicita- mente definire due funzioni di trasferimento:

𝑊𝑎𝑏𝑠: 𝑓(𝑥) = 𝑥2 𝑊𝑠𝑞𝑢𝑎𝑟𝑒: 𝑓(𝑥) = |𝑥|

Queste funzioni si comportano come veri moduli di elaborazione del se- gnale in entrata. Una versione modulare della funzione valore assoluto, scritta in maniera molto compatta (si noti l’uso di |) è la seguente:

1 absFunc = {|sig| sig.collect{|i| i.abs} - (sig.peak*0.5) } ;

Nel codice l’unica cosa di rilievo è l’eliminazione automatica dell’offset. Il metodo peak restituisce l’elemento di Signal che ha il valore assoluto più alto (il massimo d’ampiezza del segnale). Assumendo che il segnale sia simmetrico

𝑝𝑒𝑎𝑘 = 0.7: il segnale trasformato sarà compreso in [0.0, 0.7] e viene traslato di 0.7/2 = 0.35, così che la sua escursione in ampiezza risulti simmetricamente

intorno allo0 in [−0.35, 0.35].

Nel documento Introduzione a SuperCollider (pagine 100-112)