Questo Notebook vuole fornire le conoscenze di base necessarie alla prototipazione e al controllo dei parametri di uno strumento virtuale generico attraverso due software rappresentativi di ambienti operativi informatici differenti:
In questo scritto ho sono illustrati i concetti fondamentali legati alla prototipazione di uno strumento virtuale attraverso due software in parallelo.
Questi software sono rappresentativi di due ambienti operativi differenti impiegati dalla maggior parte dei software dedicati alla ricerca e creazione musicale e multimediale come Processing, Lilypond, Arduino, Python, Chuck, etc..
Scarica i materiali utilizzati in questo notebook.
Super Collider è un software Open Source che utilizza un linguaggio di programmazione (Smalltalk) che segue il paradigma della programmazione orientata agli oggetti (OOP - Object Oriented Programming).
In questo tipo di ambiente informatico dobbiamo scrivere i comandi che vogliamo fare eseguire al computer sotto forma di linee di codice salvate in files di testo chiamati script.
Quando lanciamo l’applicazione (doppio click sull’icona) compare l’interfaccia utente che, dalla versione 3.4 in poi è cambiata radicalmente. La figura sottostante mostra la schermata iniziale delle versioni più recenti (IDE) che è identica su tutti i sistemi operativi ed è divisa in quattro parti:
from IPython.display import HTML
HTML('<center><img src="media/sc_inizio.png" width="80%"></center>')
SuperCollider (nonostante abbia un'unica interfaccia utente) è formato da due applicazioni separate che comunicano tra loro attraverso il protocollo OSC.
Possiamo pensare l’Interprete come un foglio sul quale sono scritte le istruzioni necessarie alla costruzione di uno strumento musicale virtuale, le tecniche strumentali con le quali sarà suonato ed eventualmente una partitura con indicate le azioni da compiere nel tempo per suonarlo.
Questo foglio viene inviato via OSC al Server, che come un liutaio/strumentista virtuale interpreta le istruzioni di montaggio, in base a queste costruisce lo strumento per poi suonarlo eseguendo le azioni specificate nella partitura o quelle inviate in tempo reale da un esecutore reale o virtuale esterno al software.
Quando lanciamo l’applicazione, vengono creati automaticamente due Server di default:
New file
MAC: cmd+n, WIN: ctrl+n
Open file
MAC: cmd+o, WIN: ctrl+o
Dopo aver creato o aperto un file, se vogliamo far compiere un’azione a SuperCollider (come eseguire un’operazione matematica, scrivere una partitura, definire un algoritmo di sintesi, cambiare i parametri di un Synth, farlo suonare, stopparlo, etc.) dobbiamo:
Linee di codice
L’Interprete è suddiviso in linee numerate da 1 a n e ogni linea deve terminare con un ; (punto e virgola).
Se scriviamo su più linee ma non mettiamo punto e virgola prima di andare a capo, SuperCollider le interpreta come una sola.
Se invece scriviamo del codice sulla stessa linea dopo un punto e virgola SuperCollider lo interpreta come se fosse scritto una linea sotto.
1+2; // questa e' una riga
( // ma anche da qua...
1+
2/
3; // a qua per SC e' una sola riga...
1+2; 3*7; // Queste sono due righe...
)
Possiamo raggruppare più linee in blocchi di codice includendoli tra parentesi tonde.
(
"ciao".postln;
a = 12 +24; // Questo è un blocco di codice
a.postln;
)
Eseguire il codice (Evaluate)
Possiamo eseguire il codice scritto nelle celle di questo Notebook copiandolo e incollandolo il contenuto nell'Interprete di SuperCollider oppure riscrivendolo (consigliato come allenamento manuale/mnemonico).
Tre possibilità:
2+2; // Posta il risultato nella Post window
(
13.postln;
[12,"ciao", \miao, 45.567].postln;
(12 + 56).postln
)
Ordine di esecuzione
In queste ultime due modalità le operazioni sono eseguite consecutivamente da sinistra a destra, riga dopo riga, dall’alto verso il basso in quasi tutti i casi (vedremo le eccezioni di volta in volta nel dettaglio) alla maggiore velocità fornita dalla potenza del computer.
Messaggi di errore
Eseguiamo il seguente codice che contiene volutamente un errore (manca la virgola prima di 67):
a = [12,34,56 67]
Leggiamo nella post window cosa ci dice SuperCollider: le freccette o il pallino indicano dove si è interrotta la compilazione, quindi l’errore sarà necessariamente prima e tocca a noi scovarlo.
Cancellare il contenuto della post window
MAC: cmd+shift+p - WIN: ctrl+shift+p
Commenti
E' molto utile inserire dei commenti nel codice sia per agevolare la memoria nel caso volessimo modificare un patch a distanza di tempo, sia nel caso volessimo passarlo a qualcuno che sarebbe così aiutato nella comprensione del codice. I commenti sono colorati di rosso e ci sono due possibili sintassi:
2+2; // somma
Su più righe:
/*
...commento...
*/
Help files
Gli Help files sono i nostri migliori amici quando incontriamo qualche oggetto (parola) di SuperCollider sul quale vogliamo ottenere informazioni.
Per richiamare un help file dobbiamo selezionare la parola (Doppio click o Click and drag).
SinOsc;
MAC: cmd + d - WIN: ctrl + d.
Save file
MAC: cmd+s - WIN: alt+s.
Si apre un menù a comparsa nel quale possiamo specificare quattro formati diversi:
N.B. Prestiamo attenzione che in questi formati viene salvato il codice scritto nell’interprete sotto forma di file di testo, non l’audio eventualmente generato da esso.
Tipi di data
Come in tutti i linguaggi formali in SuperCollider ci sono diversi tipi di data (data type) che definiscono:
I principali sono.
12 // Int
12.23 // Float
\ciao // Symbol
'ciao ciao' // Symbol
$c // Char
"vado al mare" // String
3 + 5; // Restituisce 8
"3" + "5"; // Restituisce 35
Boot del Server
Se vogliamo generare o elaborare segnali audio, prima dobbiamo accendere il Server (boot) e poi valutare il codice.
Possiamo paragonare quest’azione all’accendere o spegnere la corrente che alimenta un sistema elettroacustico (player, mixer, amplificatore e altoparlanti).
Per farlo abbiamo a disposizione tre modi:
Dall'interfaccia utente:
Boot (power on): clicchiamo su 0.00% a destra della scritta ”Server” tenendo schiacciato il tasto del mouse. Compare un menù a tendina e selezioniamo ”Boot Server”. I numeri diventano verdi e alcuni cominciano a cambiare.
Quit (power off): idem, selezionando ”Quit Server”. I numeri tornano bianchi e si azzerano.
s.boot; // Power on
s.quit; // Power off
Quando effettuiamo il ”Boot” del Server, accendiamo la computazione audio in SuperCollider, se però non inviamo alcun segnale all’uscita, non udiremo alcun suono.
(
b = {PMOsc.ar(440, MouseY.kr(1, 550), MouseX.kr(1, 15))*Line.kr(0,0.3,1)};
b.play;
b.plot(0.5);
b.scope;
b.freqscope;
)
Stop dei processi in corso, audio compreso
MAC: cmd+. - WIN: alt+.
Quando invece eseguiamo il codice e fermiamo l’esecuzione come abbiamo visto, è come se schiacciassimo i pulsanti ”Play” e ”Stop” di un CD player o di un altro generatore di suono collegato al sistema elettroacustico di cui sopra.
Max è un software commerciale che utilizza un linguaggio di programmazione visuale (VPL - Visual Programming Language) che fornisce in un primo momento un approccio più intuitivo nello sviluppo di algoritmi di sintesi ed elaborazione del suono.
In questo tipo di ambiente informatico possiamo realizzare applicazioni collegando tra loro diverse tipologie di oggetti grafici all'interno di files chiamati patch dall'aspetto simile ai diagrammi di flusso.
Pure Data è un'applicazione Open source simile a Max ma prestiamo attenzione in quanto nonostante la grande somiglianza grafica i due software sono sensibilmente differenti sotto diversi aspetti.
Quando lanciamo l’applicazione (doppio click sull’icona) compare l’interfaccia utente che è anche chiamata patch window:
from IPython.display import HTML
HTML('<center><img src="media/patch_1.png" width="60%"></center>')
La patch window può assumere due stati:
Si passa da uno stato all'altro sia cliccando sul lucchetto con il mouse sia schiacciando il tasto 'e'.
Se schiacciamo cmd+m oppure cmd+shift+m apriamo o chiudiamo la Max console che corrisponde alla Post window di SuperCollider.
Quando un patch è unlocked (lucchetto aperto) possiamo creare e posizionare due tipologie di elementi:
Object box
Questo tipo di oggetto rappresenta un opcode o codice operativo ovvero un sottoprogramma che è stato creato per svolgere un compito specifico.
Per crearne uno dobbiamo schiacciare 'n' (new) sulla tastiera (oppure click sulla prima icona sulla barra in alto a sinistra).
Compare un oggetto con all'interno un cursore che lampeggia, dovremo scrivere il nome dell'opcode che vogliamo creare tra quelli disponibili e compresi in un'elenco che possiamo richiamare cliccando sulla terza icona dall'alto nella barra di sinistra.
Come esempio scriviamo '*' Questo opcode ha come compito il moltiplicare tra loro due **valori.
from IPython.display import HTML
HTML('<center><img src="media/obj-box.png" width="15%"></center>')
Oggetti GUI
Tutti gli altri oggetti che possiamo inserire in un patch sono Graphic User Interface (GUI) ovvero oggetti dedicati a svolgere due compiti:
Nell'immagine seguente sono visualizzati i principali oggetti di questo tipo che possiamo creare sia scrivendone il nome in un object box, sia schiacciando l'abbreviazione da tastiera indicata, sia cercandoli nella barra in alto:
from IPython.display import HTML
HTML('<center><img src="media/gui.png" width="50%"></center>')
Tutti gli oggetti (anche GUI) possono avere uno o più inlet(s) e/o uno o più outlet(s):
from IPython.display import HTML
HTML('<center><img src="media/inout.png" width="15%"></center>')
Questi rappresentano delle "porte" attraverso le quali fare uscire o entrare un qualche tipo di dato (o un flusso di dati) permettendo di collegare tra loro più oggetti attraverso patch chords:
from IPython.display import HTML
HTML('<center><img src="media/patch_c.png" width="30%"></center>')
Ricoridamoci che per poter interagire con una GUI dobbiamo mettere il patch in locked mode...
New file
MAC: cmd+n, WIN: ctrl+n
Open file
MAC: cmd+o, WIN: ctrl+o
Cancellare la Max console
Cliccare sull'icona.
from IPython.display import HTML
HTML('<center><img src="media/console.png" width="70%"></center>')
Commenti
E' molto utile inserire dei commenti nel codice sia per agevolare la memoria nel caso volessimo modificare un patch a distanza di tempo, sia nel caso volessimo passarlo a qualcuno che sarebbe così aiutato nella comprensione del codice. I commenti sono come gli oggetti GUI e possiamo crearli schiacciando la lettera 'c' oppure cliccando sulla terza icona da sinistra della barra in alto.
Help files
Gli Help files sono i nostri migliori amici quando vogliamo recuperare informazioni su qualche oggetto di Max.
Per richiamare un help file dobbiamo essere in unlocked mode e selezionare l'oggetto con il tasto destro. Dal menù a comparsa selezionare 'Open ... Help'.
from IPython.display import HTML
HTML('<center><img src="media/help_2.png" width="95%"></center>')
Save file
MAC: cmd+s - WIN: alt+s.
Si apre un menù a comparsa nel quale possiamo specificare diversi formati, per ora sceglieremo sempre Patcher (nome_del_file.maxpat) che salva il patch nel formato di Max. Se Max non è stato lanciato e facciamo doppio click sull’icona di un file ’.maxpat’ il software si apre automaticamente.
N.B. Prestiamo attenzione che in questi formati viene salvato il codice scritto neò patch sotto forma di file di testo, non l’audio eventualmente generato da esso.
Anche in Max per generare o elaborare segnali audio dobbiamo accenderne il motore (come il boot del Server in SuperCollider). Per farlo abbiamo due opzioni principali:
from IPython.display import HTML
HTML('<center><img src="media/dac~.png" width="37%"></center>')
Questo patch rappresenta il nostro primo algoritmo di sintesi e contiene tutti gli elementi illustrati finora, introducendo inoltre un'ulteriore differenza tra gli oggetti:
SuperCollider
Max
Apriamo il Capitolo dedicato.
Come illustrato nel paragrafo dedicato alla sua architettura, SuperCollider è formato da due applicazioni che operano in parallelo e che, se vogliamo leggere, generare o modificare segnali audio dobbiamo accenderne (boot) una di esse (il Server).
Se non specifichiamo nulla, quando eseguiamo il boot il Server si collega ai driver audio utilizzati in quel momento dal sistema operativo del computer.
Se invece vogliamo utilizzare una scheda audio esterna dobbiamo selezionarla dalle preferenze di sistema come per tutti gli altri software.
Quando effettuiamo il boot sel Server SuperCollider restituisce nella Post window alcune informazioni che possono tornare utili:
Booting server 'localhost' on address 127.0.0.1:57110.
Found 0 LADSPA plugins Number of Devices: 5 0 : "Microfono MacBook Air" 1 : "Altoparlanti MacBook Air" 2 : "Microsoft Teams Audio" 3 : "VB-Cable" 4 : "ZoomAudioD"
"Microfono MacBook Air" Input Device Streams: 1 0 channels 1 "Altoparlanti MacBook Air" Output Device Streams: 1 0 channels 2
SC_AudioDriver: sample rate = 44100.000000, driver's block size = 512
SuperCollider 3 server ready. Requested notification messages from server 'localhost' localhost: server process's maxLogins (1) matches with my options. localhost: keeping clientID (0) as confirmed by server process. Shared memory server interface initialized
In Max possiamo trovare le stesse informazioni aprendo la finestra Audio Status dal menù Options.
from IPython.display import HTML
HTML('<center><img src="media/audio_status.png" width="35%"></center>')
In questo caso il block size è chiamato I/O Vector size per distinguerlo dal Signal Vector Size che è un valore riguardante il funzionamento interno (MSP Routines) dei patch di Max.
Con l'audio spento possiamo anche modificare le impostazioni direttamente dall'interfaccia senza farlo dalle preferenze di sistema.
Predisposto l'ambiente elettroacustico e collegato correttamente tra loro i dispositivi della catena audio possiamo cominciare a generare dei suoni.
Nel paragrafo dedicato all'architettura informatica di SuperCollider abbiamo visto che ci sono due Server di default.
Possiamo scegliere quale utilizzare.
s = Server.local; // Settiamo il Server locale
La lettera s è una variabile.
La parola Server è una classe.
La parola .local è un messaggio.
Ma cos’è una variabile)?
Pensiamola come una porzione di memoria del computer destinata a contenere dei dati (numeri, caratteri, audio files, synth, tabelle, array, etc.), suscettibili di modifica nel corso dell’esecuzione di un programma.
Per distinguere la singola porzione di memoria tra le tante, dobbiamo contrassegnarla con un nome (o etichetta o indirizzo).
La linea di codice nella cella precedente chiama con il nome 's' una porzione di memoria e la assegna al Server locale.
In SuperCollider esistono tre tipi di variabili:
La parola Server è una classe ovvero uno stampino attraverso il quale possiamo costruire tante copie di un oggetto (istanze).
Agli oggetti possono essere inviati dei messaggi che assumono due funzionalità:
Possiamo inviare messaggi agli oggetti separandoli con un punto.
In questo caso abbiamo definito il Server come 'locale' inviando il messaggio .local alla classe Server e ne abbiamo assegnato una copia (istanza) alla variabile 's' secondo una delle più comuni strutture sintattiche impiegate in SuperCollider.
// Nome_variabile assegnazione Oggetto.messaggio_a_oggetto
s = Server.local;
s.boot; // Invia un nuovo messaggio a Server.local
s.quit;
s.reboot;
Possiamo ora intuire a cosa servono le variabili: rendere il codice più leggibile e meno ridondante.
Senza l'utilizzo della variabile s avremmo dovuto scrivere.
Server.local.boot;
Server.local.quit;
Server.local.reboot;
Ricordiamo che il codice sulla stessa linea viene eseguito da sinistra a destra: prima dichiara il Serve poi lo definisce come locale e poi lo accende, spegne, etc. ( richiesta di compiere un'azione ).
Attraverso la sintassi appena illustrata possiamo impostare dal codice diversi parametri del Server.
Definire quale Server utilizzare nello script corrente.
s = Server.internal;
s = Server.local;
La lettera s è assegnata di default al Server locale.
Se vogliamo modificare il Server di default che sarà assegnato alla variabile globale s in assenza di ulteriori messaggi.
Server.default = Server.internal;
Server.default = Server.local;
Principali messaggi che possiamo inviare al Server in uso.
s = Server.local;
s.boot; // Boot del Server
s.quit; // Chiude il Server
s.reboot; // Chiude e riapre il Server
s.waitForBoot({...}) // Accende il Server e terminato il booting esegue il codice successivo tra le parentesi
s.freeAll; // Libera tutti i nodi nel Server
s.volume = 0; // Setta il voume in uscita (in dB)
s.mute; // Mute del Server
s.unmute; // Unmute
In assenza di indicazioni SuperCollider riceve e invia tutti i segnali audio ai driver selezionati nel sistema operativo.
Possiamo modificare questa scelta dal codice attraverso le ServerOptions.
Otteniamo un elenco di dispositivi connessi al computer.
ServerOptions.devices;
ServerOptions.inDevices;
ServerOptions.outDevices;
Selezioniamo quello da utilizzare.
s = Server.local;
o = s.options;
o.device; // riporta il device in uso
o.device = "Soundflower (2ch)"; // seleziona il device specificando il nome
o.device_("Soundflower (2ch)"); // altra sintassi...
s.reboot; // Se già acceso dobbiamo spegnere e riaccendere...
o.device = nil; // 'nil' specifica i driver in uso dal sistema
s.reboot;
Dispositivi differenti per i segnali in entrata e quelli in uscita.
o.inDevice = "Built-in Microph";
o.outDevice = "Soundflower (2ch)";
s.reboot;
Modificare il numero di canali.
o.numOutputBusChannels.postln; // riporta l'informazione nella post window
o.numOutputBusChannels = 8; // modifica i settings
o.numOutputBusChannels_(8); // altra sintassi...
o.numInputBusChannels.postln; // riporta
o.numInputBusChannels = 8; // modifica
o.numInputBusChannels(8); // altra sintassi...
s.reboot;
Modificare rata di campionamento e block size.
o.sampleRate; // riporta
o.sampleRate = 44100; // modifica
o.sampleRate_(44100);
o.blockSize; // riporta
o.blockSize = 64; // modifica
o.blockSize_(64);
s.reboot;
Ora che sappiamo accendere il Server di SuperCollider e ricevere o inviare alle porte del computer segnali audio vediamo come generarli o modificarli utilizzando una tipologia di oggetti chiamati UGens o Unit Generator.
Le UGens sono particolari classi ottimizzate per la generazione o modifica di segnali audio.
Le UGens 'vivono' nel Server.
Come tutte le classi cominciano sempre con una lettera maiuscola e di default sono colorate di blu.
Possono ricevere solo tre messaggi che ne definiscono la rata ovvero quanti valori devono generare o modificare nel tempo (azioni da compiere).
Nella figura seguente lo stesso segnale generato ad audio rate (sopra) e a control rate (sotto)
{[SinOsc.ar,SinOsc.kr]}.plot(1/50, bounds:1000@500);
I segnali a audio rate possono sia essere collegati agli inputs (adc) o outputs (dac) che utilizzati come segnali di controllo dei parametri di altre UGens mentre i segnali a control rate non possono essere collegati agli inputs e agli outputs.
Mentre i tre messaggi precedenti definiscono quanti valori vengono generati nel tempo i nomi delle specifiche UGen indicano quale tipo di relazione intercorre fra i valori generati (forma d'onda).
Possiamo ottenere un elenco di tutte le UGens andando nella finestra degli Help files e cliccando prima su Browse e poi su UGens.
from IPython.display import HTML
HTML('<center><img src="media/broswe.png" width="65%"></center>')
Tutti e tre i metodi invocabili sulle UGen possiedono degli argomenti che specificano i valori di alcuni parametri del segnale da generare o modificare come ad esempio frequenza, ampiezza o altro.
Gli argomenti sono specificati all'interno di parentesi tonde e sono differenti per ogni UGen sulla quale sono invocati i metodi.
Per conoscere quali sono gli argomenti di una specifica UGen dobbiamo richiamarne l'Help file.
Come esempio osserviamo quelli di SinOsc che è un oscillatore sinusoidale.
from IPython.display import HTML
HTML('<center><img src="media/args.png" width="50%"></center>')
Il primo specifica la frequenza in Hertz.
(
{[SinOsc.ar(1320),
SinOsc.ar(880),
SinOsc.ar(440)]
}.plot(minval:-1,maxval:1)
)
Il secondo specifica la fase iniziale in radianti tra -1 e 1.
(
{[SinOsc.ar(440,0.0),
SinOsc.ar(440,0.5),
SinOsc.ar(440,1.0)]
}.plot(minval:-1,maxval:1)
)
Il terzo specifica un fattore di moltiplicazione dei valori in uscita (usualmente compresi tra -1.0 e + 1.0).
(
{[SinOsc.ar(440,0,1.0),
SinOsc.ar(440,0,0.5),
SinOsc.ar(440,0,0.2)]
}.plot(minval:-1,maxval:1)
)
Il quarto specifica un valore da sommare ai valori in uscita.
(
{[SinOsc.ar(440,0,1.0),
SinOsc.ar(440,0,0.5),
SinOsc.ar(440,0,0.2)]
}.plot(minval:-1,maxval:1)
)
Questi ultimi due argomenti (mul e add) sono compresi in quasi tutte le UGens e impiegati di prassi per riscalare il range in uscita.
Non è necessario specificare tutti gli argomenti.
Quelli non indicati assumeranno il valore di default che possiamo trovare nell'help file.
Possiamo utilizzare due sintassi differenti.
SinOsc.ar(440,0,0.3);
SinOsc.ar(mul:0.7, freq:500);
Nel proseguio di questo scritto ci soffermeremo sulle caratteristiche delle singole UGens, per ora possiamo curiosare richiamando l'Help file di alcune tra quelle in elenco.
I monitors che riguardano l'audio sono di tre tipi:
In SuperCollider possiamo monitorare:
Attività computazionale
Abbiamo due tipologie di monitor numerici:
1 - Leggere le informazioni nell'IDE (in basso a destra).
from IPython.display import HTML
HTML('<center><img src="media/monitor_1.png" width="45%"></center>')
Da sinistra a destra:
Se clicchiamo sui numeri appare la seguente finestra dove possiamo attivare o disattivare diversi comandi.
from IPython.display import HTML
HTML('<center><img src="media/monitor_6.png" width="40%"></center>')
2 - Ottenere i valori invocando metodi nel codice.
s.status; // Riporta il nome del Server in uso
s.avgCPU; // Riporta l'utilizzo medio della CPU
s.peakCPU; // Riporta l'utilizzo di picco della CPU
s.numUGens; // Riporta il numero di UGens
s.numSynths; // Riporta il numero di Synths
s.numGroups; // Riporta il numero di Gruppi
s.numSynthDefs; // Riporta il numero di SynthDefs
s.volume; // Riporta il volume massimo in uscita (dB)
s.mute; // Muting
s.unmute; // Toglie il Mute
Una delle ragioni di poter ottenere queste informazioni dal codice sta nel fatto che potremmo utilizzarle come valori di controllo di una qualche tecnica di sintesi o di elaborazione del segnale oppure riportarli su un'interfaccia grafica (GUI) per un monitoraggio visivo personalizzato.
Segnali in ingresso e uscita dal Server
Richiamiamo interfacce grafiche (GUI) dedicate.
s.meter; // Visualizza segnali in input e ouput
s.scope(2); // Oscilloscopio del master out (il numero di canali si accorda
// con quello specificato nelle ServerOption oppure possiamo spe-
// cificarlo come argomento)
s.freqscope; // Spettroscopio del master out (monofonico, possiamo scegliere
// quale canale (Bus) visualizzare specificandone l'ID nel box
// a destra. Possiamo inoltre specificare se la visualizzazione
// deve essere lineare o logaritmica (di default) e anche il li-
// mite inferiore in dB -(96 di default)
{Pan2.ar(Mix(SinOsc.ar(Array.rand(20,200,5000),0,0.1)),0.3)}.play // Test
Segnali in uscita dalle UGens
Uditivo
Un modo veloce per far suonare e monitorare attraverso l'ascolto una o più UGens consiste nell'includerla tra parentesi graffe e invocare il metodo .play.
E' un'abbreviazione sintattica che possiamo utilizzare esclusivamente per prototipare velocemente algoritmi di sintesi.
{SinOsc.ar}.play;
{SinOsc.ar + WhiteNoise.ar}.play;
Grafico
Per avere un monitoraggio visivo della forma d'onda (oscillogramma) del segnale in uscita di una specifica UGen (o da un network di UGens) possiamo invocare il metodo .scope che sostituisce play.
{Saw.ar}.scope;
{SinOsc.ar(LFNoise0.kr(15,1000,2000))}.scope;
Possiamo utilizzare i diversi metodi in contemporanea.
(
{
SinOsc.ar(2000 + (1000 * LFNoise0.kr(5).scope(\pitch)) // Scope segnale di controllo
).scope(\sine) // Scope segnale audio
}.play; // Monitor uditivo
s.scope; // Scope del Server
)
Due considerazioni.
La prima consiste nel fatto che l'argomento di .scope() è il nome che vogliamo dare alla finestra grafica.
La seconda riguarda il colore che differenzia lo scoping dei segnali a audio rate (gialli) da quelli a control rate (verdi).
Possiamo anche visualizzare in un plot i valori dell'output di una UGen invocando l'omonimo metodo.
Nell'esempio seguente sono illustrati i principali argomenti per questo tipo di plot.
{Pulse.ar}.plot;
{Pulse.ar + Saw.ar}.plot(duration: 0.01 ); // Durata della finestra in secondi
{Pulse.ar * Saw.ar}.plot(bounds: Rect(0,0,500,100)); // Dimensioni grafiche finestra
{Pulse.ar * PinkNoise.ar}.plot(bounds: 500@100); // Abbreviazione
{Pulse.ar + WhiteNoise.kr}.plot(minval: -2, maxval: 2); // Minimo e massimo asse y
Numerico
Infine per un monitoraggio numerico dei valori dei singoli campioni possiamo utilizzare il metodo .poll invocato direttamente su una UGen.
Questo metodo stampa semplicemente nella Post window i valori in uscita.
L'argomento è il numero di campioni per secondo da stampare (sottocampionamento).
Questo metodo serve solo per monitorare, non possiamo assegnare i singoli valori ad una variabile per poi riutilizzarli all'interno del codice.
{SinOsc.ar.poll(1)}.play;
{SinOsc.ar(MouseX.kr(40,10000,1).poll(10), 0, 0.1) }.play;
Gli Help files delle UGens sono principalmente divisi in tre sezioni.
from IPython.display import HTML
HTML('<center><img src="media/sc_help_1.png" width="60%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/sc_help_2.png" width="60%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/sc_help_3.png" width="60%"></center>')
In SuperCollider il codice viene eseguito dall'alto verso il basso, da sinistra a destra, linea dopo linea.
In Max tutti le operazioni effettuate dagli oggetti posizionati più a destra sono eseguite prima.
from IPython.display import HTML
HTML('<center><img src="media/max_ordine.png" width="25%"></center>')
Se un object box ha più di un outlet l'ordine di uscita dei valori va anch'esso da destra a sinistra.
from IPython.display import HTML
HTML('<center><img src="media/max_ordine_out.png" width="19%"></center>')
Se un object box ha più di un inlet:
il primo a sinistra è l'inlet caldo ovvero memorizza il dato in ingresso, esegue l'operazione e manda il risultato in uscita dall'outlet.
tutti quelli più a destra del primo sono inlet freddi ovvero memorizzano il il dato in ingresso in quella posizione senza triggerare il risultato in uscita.
Se un singolo patch cord entra in più inlets l'ordine è sempre da destra a sinistra.
from IPython.display import HTML
HTML('<center><img src="media/max_ordine_3.png" width="55%"></center>')
Per recuperare segnali audio in ingresso possiamo utilizzare l'oggetto adc~ che accetta come argomenti il numero di canale(i) al quale collegarsi.
Per inviare segnali audio in uscita possiamo utilizzare l'oggetto dac~ che accetta come argomenti il numero di canale(i) al quale collegarsi.
Possiamo accendere o spegnere l'audio inviando 1 (on) o 0 (off) nel loro inlet di sinistra sia attraverso message box che con un toggle.
from IPython.display import HTML
HTML('<center><img src="media/max_inout.png" width="20%"></center>')
In locked mode se eseguiamo doppio click su di essi compare l'audio status dal quale possiamo impostare i diversi dispositivi collegati nonchè la Sampling Rate e l'I/O Vector Size (block size di SuperCollider).
Possiamo anche monitorare l'attività computazionale.
from IPython.display import HTML
HTML('<center><img src="media/audio_status.png" width="37%"></center>')
Anche in Max ci sono oggetti audio che generano segnali (oscillatori) e che li modificano.
I segnali possono essere monitorati principalmente attraverso tre GUI.
Accettano i segnali da monitorare negli inlet di sinistra.
from IPython.display import HTML
HTML('<center><img src="media/max_monitors.png" width="70%"></center>')
Gli Help files di Max sono dei veri e propri patch e sono strutturalmente simili a quelli di SuperCollider.
from IPython.display import HTML
HTML('<center><img src="media/max_help_1.png" width="95%"></center>')
L’altezza (o frequenza) di un suono è il parametro legato alla sensazione di gravità/acutezza che si percepisce e dipende dalle frequenze di variazione delle onde elementari che compongono l’onda sonora.
Possiamo specificare questo parametro in tre diversi tipi di notazione simbolica:
from IPython.display import HTML
HTML('<center><img src="media/Simboli.png" width="40%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/Nomi.png" width="40%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/Midi.png" width="40%"></center>')
Oltre ai valori midi esistono altre rappresentazioni simboliche assolute impiegate:
from IPython.display import HTML
HTML('<center><img src="media/Altezze.png" width="63%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/Frequenze.png" width="40%"></center>')
Un'onda periodica è data da un fenomeno vibratorio che si ripete identico a se stesso a intervalli di tempo uguali.
Possiamo rappresentare graficamente un onda attraverso un sistema di riferimento cartesiano:
La frequenza di un suono periodico è misurata in Hertz o cps (cicli per secondo) ovvero si misura quante volte il periodo si ripete uguale in un secondo.
import os
import sys
sys.path.insert(0, os.path.abspath('moduli'))
import parametri as par
%matplotlib inline
freq = 4# Frequenza (Hz o cps)
par.sine(freq)
L'orecchio umano nelle sue condizioni ottimali percepisce suoni compresi tra 20 e 20.000 Hz.
Nella seguente tabella i rapporti tra alcune unità di misura appena esposte.
Nota | MIDI | Rapporto | Frequenza (Hz) | |
---|---|---|---|---|
Do | 60 | $1:1$ | 261.6 | |
Do# o Reb | 61 | $\sqrt[12]{2}$ | 277.2 | |
Re | 62 | $\sqrt[12]{2^2}$ | 293.7 | |
Re# o Mib | 63 | $\sqrt[12]{2^3}$ | 311.1 | |
Mi | 64 | $\sqrt[12]{2^4}$ | 329.6 | |
Fa | 65 | $\sqrt[12]{2^5}$ | 349.2 | |
Fa# o Solb | 66 | $\sqrt[12]{2^6}$ | 370.0 | |
Sol | 67 | $\sqrt[12]{2^7}$ | 390.0 | |
Sol# o Lab | 68 | $\sqrt[12]{2^8}$ | 415.3 | |
La | 69 | $\sqrt[12]{2^9}$ | 440.0 | |
La# o Sib | 70 | $\sqrt[12]{2^{10}}$ | 466.2 | |
Si | 71 | $\sqrt[12]{2^{11}}$ | 493.9 | |
Do | 72 | $2:1$ | 523.3 |
Questo parametro in termini musicali è dato dal rapporto tra i suoni più deboli (pianissimissimo) e quelli più forti che caratterizzano la dinamica di un brano.
L'intensità di un suono in fisica si definisce con il termine ampiezza in quanto descrive l’ampiezza delle variazioni dell’onda sonora rispetto allo stato di quiete e fornisce una misura dell’energia da essa trasportata.
Se prendiamo in considerazione l'aspetto fisico acustico e non la percezione umana questo parametro è indipendente dalla frequenza e per convenzione rappresentato da valori compresi tra -1.0 e +1.0 e che nell'audio digitale corrisponde al valore del singolo campione.
import os
import sys
sys.path.insert(0, os.path.abspath('moduli'))
import parametri as par
%matplotlib inline
freq = 8 # Frequenza (Hz o cps)
amp = 0.2 # Ampiezza (tra 0.0 e 1.0)
par.vsine(freq, amp)
par.sine(freq, amp)
La funzione sinusoidale può rappresentare una legge oraria chiamata moto armonico semplice che è un moto periodico lungo un asse rettilineo detto origine (O).
$x(t) = a * sin(\omega t + \varphi)$
Dal punto di vista cinematico può essere visto come:
from IPython.display import HTML
HTML('<center><video width="95%" controls loop autoplay> <source src="media/sine.mp4"></video></center>')
import os
import sys
sys.path.insert(0, os.path.abspath('moduli'))
import parametri as par
%matplotlib inline
freq = 5 # Cambia i parametri...
amp = 1
fase = 0.5
par.sinepar(freq,amp,fase)
Il timbro è quella particolare qualità del suono che permette di distinguere due suoni con uguale ampiezza e altezza e consente all'ascoltatore di identificare la fonte sonora, rendendola distinguibile da ogni altra.
Il timbro dei suoni naturali è influenzato da moltissimi parametri variabili come ad esempio le caratteristiche fisiche dei materiali della sorgente sonora e dal modo in cui è messo in vibrazione.
Tutti questi parametri concorrono a formare una rappresentazione fisica del timbro di quel suono ovvero la sua forma d'onda, che descrive come varia nel tempo la pressione atmosferica (o il voltaggio di un segnale) nel produrre quel determinato suono.
Nelle celle sottostanti sono illustrate tre diverse forme d'onda:
import os
import sys
import IPython.display as ipd
sys.path.insert(0, os.path.abspath('moduli'))
import parametri as par
%matplotlib inline
par.puro()
ipd.Audio('media/puro.mp3')
/Users/andreavigani/anaconda3/lib/python3.11/site-packages/matplotlib/cbook/__init__.py:1340: ComplexWarning: Casting complex values to real discards the imaginary part return np.asarray(x, float)
par.periodico()
ipd.Audio('media/periodico.mp3')
par.noise()
ipd.Audio('media/noise.mp3')
Gli elementi principali che contribuiscono alla caratterizzazione delle forme d'onda sono due:
I suoni presenti in natura non producono mai suoni puri (onde sinusoidali perfette) ma sono caratterizzati da forme d'onda differenti tra loro e dunque da suoni complessi.
Un suono complesso è il risultato della sovrapposizione (somma) di più suoni puri ognuno con frequenza, ampiezza e fase differenti secondo il teorema enunciato dal fisico francese J.Fourier nei primi anni dell'800:
Qualunque segnale periodico può essere scomposto nella somma di un eventuale termine costante e di segni sinusoidali, dei quali il primo, avente lo stesso periodo e quindi la stessa frequenza del segnale considerato, si chiama prima armonica o fondamentale, e gli altri, aventi periodi sottomultipli e quindi frequenze multiple, si chiamano armoniche superiori
import os
import sys
sys.path.insert(0, os.path.abspath('moduli'))
import parametri as par
%matplotlib inline
par.fourier()
Per chiarire ulteriormente possiamo pensare uno spettro sonoro come un accordo musicale le cui singole note sono eseguite da suoni sinusoidali (un singolo suono puro per ogni nota) che l'orecchio umano non percepisce come un accordo formato da più note ma come un singolo suono con un determinato timbro.
from IPython.display import HTML
HTML('<center><img src="media/Fourier.png" width="14%"></center>')
Ogni suono puro che concorre a formare uno spettro complesso può essere chiamato suono armonico o parziale.
La differenza terminologica tra queste due definizioni è sottile e implica la conoscenza del tipo di spettro a cui si riferisce.
Fondamentalmente gli spettri sonori possono essere suddivisi in due grandi famiglie la cui differenziazione è data dai rapporti frequenziali intercorrenti tra le componenti pure che li formano.
Spettri armonici. Le frequenze dei parziali seguono rapporti formati da numeri interi:
I parziali possono essere chiamati anche armonici dove il suono (usualmente) più grave che corrisponde al rapporto 1:1 è chiamato fondamentale ed è quello che generalmente caratterizza l'altezza percepita di quel suono complesso, mentre i parziali successivi sono chiamati primo armonico, secondo armonico, e via dicendo fino idealmente a infinito.
La forma d'onda di questi suoni complessi può essere solamente periodica.
In questo caso, stabilita la frequenza fondamentale in Hertz per ricavare le frequenze degli armonici basterà moltiplicarne il valore per i numeri interi.
from IPython.display import HTML
HTML('<center><img src="media/Serie_armonica.png" width="63%"></center>')
from IPython.display import Audio
Audio('media/armonico.mp3')
Spettri inarmonici. Le frequenze dei parziali non seguono alcun rapporto particolare, o meglio non sono in rapporto con un suono fondamentale che è assente.
La forma d'onda di questi suoni può essere sia periodica che aperiodica.
from IPython.display import Audio
Audio('media/inarmonico.mp3')
Possiamo affermare che tutti i suoni possibili sono formati da spettri compresi tra:
le cui caratteristiche oscillano tra suoni inarmonici, suoni quasi armonici e suoni armonici.
import os
import librosa
import IPython.display as ipd
sys.path.insert(0, os.path.abspath('moduli'))
import parametri as par
%matplotlib inline
sr = 22050 # Frequenza di campionamento
path = os.path.abspath('media/tenuto.wav') # Path file
offset = 1 # Offset in secondi
w_size = 1024 # Window size (in samples)
y, sr = librosa.load(path, sr=sr,offset=offset, duration=(1/sr)*w_size) # Assegna le ampiezze istantanee di y
par.plotFFT(y,sr,150)
ipd.Audio('media/tenuto.wav')
/Users/andreavigani/anaconda3/lib/python3.11/site-packages/matplotlib/cbook/__init__.py:1340: ComplexWarning: Casting complex values to real discards the imaginary part return np.asarray(x, float)
Ci sono delle forme d'onda ormai ritenute classiche in quanto erano le sole che potevano essere generate dai primi oscillatori che sono stati costruiti.
Nel proseguio di questo scritto impiegheremo solo queste forma d'onda classiche a spettro fisso in quanto le tematiche legate alla sintesi ed elaborazione del suono sono affrontate in un altra parte del sito.
In SuperCollider ci sono generalmente due copie degli stessi oscillatori (UGens).
Osserviamo le diverse forme d'onda e le caratteristiche spettrali attraverso i monitors più adatti ai parametri che vogliamo indagare.
{ SinOsc.ar(400).poll(10) }.play; // Sinusoidale
{ Saw.ar(400) }.play; // Dente di sega
{ Pulse.ar(400) }.play; // Onda quadra
{ VarSaw.ar(400) }.play; // Onda triangolare
{ Blip.ar(400) }.play; // Treno d'impulsi
{ WhiteNoise.ar }.play; // Rumore bianco
{ PinkNoise.ar }.play; // Rumore Rosa
{ BrownNoise.ar }.play; // Rumore Browniano
{ GrayNoise.ar }.play; // Rumore grigio
{ ClipNoise.ar }.play; // Rumore clippato
{ LFPar.ar(400).poll(10) }.play; // Sinusoidale (parabolica)
{ LFSaw.ar(400) }.play; // Dente di sega
{ LFPulse.ar(400) }.play; // Onda quadra
{ LFTri.ar(400) }.play; // Onda triangolare
{ Impulse.ar(400) }.play; // Treno d'impulsi
{ LFNoise0.ar }.scope; // Generatori random
{ LFNoise1.ar }.scope;
{ LFNoise2.ar }.scope;
{ LFClipNoise.ar }.scope;
Alcuni paragoni visivi (plot).
(
{[SinOsc.ar, // Oscillatore sinusoidale
WhiteNoise.ar, // Generatore di rumore bianco
Saw.ar, // Generatore di onde a dente di sega
Pulse.ar, // Generatore di onde quadre
Blip.ar, // Treno d'impulsi (PulseTrain)
LFPar.ar, // Parabolica (Quasi-Sine)
LFNoise0.ar, // Generatore di valori pseudo-casuali
LFSaw.ar,
LFPulse.ar,
Impulse.ar]
}.plot(1/70, minval:-1, maxval:1);
)
In Max abbiamo degli object box audio che assumono la funzione di oscillatori.
I parametri che corrispondono agli argomenti delle UGens in SuperCollider sono inviati attraverso GUI negli inlets degli oggetti oppure specificati come argomenti di default al momento della creazione degli oggetti stessi.
Per conoscere quali sono i parametri che possiamo controllare dobbiamo consultare l'Help file del singolo oggetto.
from IPython.display import HTML
HTML('<center><img src="media/classic.png" width="65%"></center>')
Abbiamo ora le conoscenze di base per cominciare a progettare e realizzare uno strumento virtuale.
La procedura da seguire.
formalizziamo il pensiero in un algoritmo di sintesi e/o elaborazione del suono definendo sia il percorso dei segnali che i parametri di controllo e le modalità attraverso le quali agiremo su di essi (tecnica strumentale).
definiamo attraverso un diagramma di flusso le istruzioni necessarie alla costruzione di più copie dello strumento in modo che si possano realizzare con qualsiasi software passato, presente e futuro.
programmiamo con il software di nostra scelta un modello (stampo) dello strumento secondo le istruzioni presenti nel diagramma di flusso.
ricaviamo dal modello definito nel punto precedente le copie dello strumento di cui abbiamo bisogno.
modifichiamo secondo le modalità definite nel primo punto (tecniche strumentali) i parametri di controllo delle singole copie.
Possiamo intuire che questa procedura non è poi così dissimile da quella adottata da un liutaio nella costruzione di uno strumento acustico reale.
Come primo strumento decidiamo di progettare un semplice sintetizzatore monofonico che produce un suono sinusoidale del quale possiamo controllare frequenza, ampiezza e fase in precisi istanti nel tempo (oneshot).
Abbiamo bisogno dei seguenti elementi:
Definiti gli elementi necessari li dobbiamo collegare tra loro per realizzare una catena audio (audio chain).
Per farlo disegnamo un diagramma di flusso.
Possiamo disegnarlo a mano oppure utilizzare draw.io sia nella versione online che in quella desktop
from IPython.display import HTML
HTML('<center><img src="media/base.png" width="15%"></center>')
Con le istruzioni rappresentate nel grafico possiamo costruire uno strumento analogico oppure programmare attraverso un un software a nostra scelta uno strumento virtuale.
Realizziamone uno in SuperCollider e uno in Max.
Memorizziamo nel Server un modello di strumento attraverso la classe SynthDef (Synth Definition).
Questa classe deve contenere le istruzioni necessarie alla costruzione di un Synth non sotto forma grafica come nei diagrammi di flusso ma in un linguaggio comprensibile dal software.
La struttura sintattica.
s = Server.local;
s.boot;
(
SynthDef.new( // Nuova istanza di SynthDef
\sine, // Nome del modello di strumento
{ // Algoritmo di sintesi
arg freq=600, amp=0 fase=0; // Argomenti
var sig; // Variabili locali
sig = SinOsc.ar(freq,fase); // Oscillatore
sig = sig * amp; // Amplificatore (variabile riassegnata)
Out.ar(0, sig); // Segnale in uscita
}
).add; // Invia le istruzioni al Server
)
Per visualizzare tutte le SynthDef presenti in un Server da un'interfaccia grafica.
SynthDescLib.global.read.browse;
from IPython.display import HTML
HTML('<center><img src="media/synthdef.png" width="80%"></center>')
Ora che il Server ha le istruzioni necessarie alla costruzione di questo modello di strumento (ricordiamoci di valutare il blocco di codice della SynthDef) ne ricaviamo tante copie quante ne vogliamo attraverso la classe Synth specificandone il nome.
Synth.new(\sine); // Copia
Ogni istanza (copia) di Synth si chiama Node.
Per monitorare quanti Nodi ci sono nel Server apriamo l'interfaccia Node Tree.
s.plotTree;
Se valutiamo più volte la linea di codice del Synth vedremo comparire tutte le copie generate una sopra l'altra.
from IPython.display import HTML
HTML('<center><img src="media/nodetree.png" width="45%"></center>')
Quando utilizziamo la sintassi {UGen.ar}.play per far suonare una o più UGens in realtà stiamo scrivendo un'abbreviazione sintattica e chiediamo a SuperCollider di generare per noi una SynthDef e un Synth temporanei.
Per quanto possa sembrare meno macchinoso utilizziamo questa sintassi solo per prototipare algoritmi oppure come monitor uditivo e definiamo sempre noi SynthDef e Synth.
Possiamo cancellare tutti i Nodi nel Server sia interrompendo la computazione con cmd + . che invocando il metodo
s.freeAll;
Realizziamo lo stesso strumento in Max.
from IPython.display import HTML
HTML('<center><img src="media/max_sine_1.png" width="25%"></center>')
Notiamo che gli object box hanno degli argomenti di default.
Realizziamo un modello di strumento attraverso un subpatch.
from IPython.display import HTML
HTML('<center><img src="media/subpatch.png" width="65%"></center>')
Con un semplice copia e incolla generiamo tante copie indipendenti del subpatch quante ne vogliamo.
from IPython.display import HTML
HTML('<center><img src="media/copie.png" width="65%"></center>')
Abbiamo la possibilità di controllare i parametri degli strumenti che abbiamo costruito in diversi modi.
Uno consiste nell'inviare nuovi valori attraverso messaggi agli strumenti sia in SuperCollider che in Max.
SuperCollider
Assegnamo il singolo Synth a una variabile per contrassegnarlo con un indirizzo al quale inviare il messaggio.
Inviamo il messaggio con il metodo .set(\nome, valore).
s = Server.local;
s.boot;
(
SynthDef.new(\sine, {arg freq=600, amp=0;
var sig;
sig = SinOsc.ar(freq);
sig = sig * amp;
Out.ar(0, sig);
}).add;
)
a = Synth.new(\sine); // Istanza e indirizzo (variabile)
a.set(\freq, 567, \amp, 0.3); // nome_argomento, valore
a.set( \amp, 0.8);
a.set(\freq, 1278 );
a.set(\freq, 893, \amp, 0.2);
a.run(false); // Interrompe la computazione senza distruggere il Node
a.run(true); // Riprende la computazione
a.free; // Distrugge il Node
Possiamo impostare gli argomenti di un Synth anche al momento della sua creazione con la seguente sintassi.
a = Synth.new(\sine, [\freq, 987, \amp, 0.5]);
Max
In Max ci sono due modi per collegare tra loro i diversi elementi:
from IPython.display import HTML
HTML('<center><img src="media/send.png" width="38%"></center>')
L'oggetto send può essere sostituito da un message box nella forma illustrata in figura dove il punto e virgola sta per send e il simbolo successivo è il nome del receive seguito dal valore o dai valori che vogliamo inviare.
Sfruttiamo questa caratteristica modificando il subpatch con l'oggetto route che separa i parametri e ci permette di indirizzarli agli inlet corretti.
In base a questa strategia definiamo una sintassi che ci permetta di inviare i diversi parametri comunicandoli alla singola copia dello strumento.
from IPython.display import HTML
HTML('<center><img src="media/messaggio.png" width="100%"></center>')
Usualmente gli oscillatori che abbiamo a disposizione nei software audio accettano valori assoluti di frequenza in hertz ma ci sono situazioni in cui potremmo volerli definire sotto forma di midinote.
In questi casi è necessario convertirli.
Formule
Da Hertz a MIDI e viceversa
$m = 12*log_{2}(f/440) + 69$
$f = 2^{(m−69)/12} * 440$
In SuperCollider
f = 440;
m = 12 * log2(f/440) + 69; // Sviluppando l'espressione
f.cpsmidi; // Con la funzione dedicata
m = 60;
f = 2**((m-69)/12) * 440; // Sviluppando l'espressione
m.midicps; // Con la funzione dedicata
In Max
from IPython.display import HTML
HTML('<center><img src="media/max_freqconv.png" width="45%"></center>')
Nel paragrafo precedente abbiamo modificato l'intensità di un segnale moltiplicando il valore di ogni campione (ampiezza tra -1.0 e +1.0) per una costante compresa tra 0.0 e 1.0 chimata anch'essa ampiezza.
0.0 genera silenzio e 1.0 mantiene l'intensità del segnale originale.
Il termine ampiezza risulta dunque troppo generico.
Per evitare confusione aggiungiamo un aggettivo per distinguere tre differenti modalità di misurazione.
import numpy as np
import os
import sys
sys.path.insert(0, os.path.abspath('moduli'))
import parametri as par
%matplotlib inline
n = 12 # Numero di valori
a = par.amp(n) # Valori ampiezza istantanea
print('Ampiezze istantanee: ' + str(a))
a = np.abs(a) # Valori assoluti
a = np.amax(a) # Restituisce il valore più alto
print('Ampiezza di picco: ' + str(a))
a = par.amp(n) # Valori ampiezza istantanea
a = a**2 # Eleva al quadrato
a = np.mean(a) # Calcola la media aritmetica
a = np.sqrt(a) # Calcola la radice quadrata
a = np.round_(a, 2) # Approssima a due decimali
print('Root Mean Square: ' + str(a))
par.img(n)
Ampiezze istantanee: [ 0. 0.86 0.65 0.34 0.59 0.34 -0.61 -0.85 -0.21 -0.04 -0.34 0.03] Ampiezza di picco: 0.86 Root Mean Square: 0.5
La principale differenza tra ampiezza di picco e RMS sta nel fatto che la prima è un valore univoco indipendente dall'andamento del segnale mentre la seconda essendo una media è strettamente legata all'andamento dell'inviluppo del segnale.
import os
import sys
sys.path.insert(0, os.path.abspath('moduli'))
import parametri as par
%matplotlib inline
par.sf('media/pizz.wav')
par.sf('media/tenuto.wav')
Ampiezza di picco: 0.88 Root Mean Square: 0.12
Ampiezza di picco: 0.91 Root Mean Square: 0.25
In musica è importante misurare non solo le ampiezze ma anche i rapporti che intercorrono tra suoni con intensità differenti.
Questo parametro è anche comunemente chiamato volume o fattore di amplificazione e possiamo rappresentarlo in due diversi modi:
from IPython.display import HTML
HTML('<center><img src="media/Dinamiche.png" width="36%"></center>')
Quartica - Unità di misura assoluta espressa in valori decimali compresi tra 0.0 e 1.0. La più vicina alla percezione umana riguardo i cambiamenti di intensità dei suoni. Per calcolare i valori corretti dobbiamo elevare l'ampiezza lineare alla quarta potenza. Essendo compresi tra 0.0 e 1.0 l'ambito (range) rimane lo stesso.
$$a^4$$
Decibels (dB) - Unità di misura relativa espressa in valori decimali compresi tra 0.0 e -inf (o +inf a seconda del tipo di misurazione). Misura la differenza di intensità tra l'ampiezza di un suono rispetto a un'ampiezza di riferimento. Un suono con ampiezza di -6.02 dB sarà sempre forte la metà rispetto a un suono la cui ampiezza è stata presa come riferimento per la misurazione. Per calcolare i valori corretti dobbiamo utilizzare la seguente formula dove $a$ è il valore dell'ampiezza lineare del suono che vogliamo misurare mntre $a0$ è l'ampiezza del suono di riferimento (1.0).
$$20*\log_{10}(\frac{a}{a0})$$
import os
import sys
sys.path.insert(0, os.path.abspath('moduli'))
import parametri as par
%matplotlib inline
par.curve()
Segno | Vel | lin | quart | dB | |
---|---|---|---|---|---|
pppp | 12 | 0.1 | 0.0001 | -20 | |
ppp | 24 | 0.2 | 0.0016 | -14 | |
pp | 44 | 0.3 | 0.0081 | -10 | |
p | 54 | 0.4 | 0.0256 | -8 | |
mp | 64 | 0.5 | 0.0625 | -6 | |
mf | 74 | 0.6 | 0.1296 | -4 | |
f | 84 | 0.7 | 0.2401 | -3 | |
ff | 94 | 0.8 | 0.4096 | -2 | |
fff | 114 | 0.9 | 0.6561 | -1 | |
ffff | 127 | 1.0 | 1.0000 | 0 |
Gli oggetti dedicati all'ampiezza e alle sue variazioni nel tempo dei diversi software musicali accettano generalmente valori di ampiezza lineare (tra 0.0 e 1.0) e/o decibels (tra -inf. e 0.0).
Potremmo anche in questo caso avere la necessità di convertire i valori da un'unità di misura ad un'altra.
Formule
Da ampiezza lineare a MIDI velocity e viceversa
$vel = amp_{lin} * 127$
$amp_{lin} = vel / 127$
Da ampiezza lineare ad ampiezza quartica e viceversa
$amp_{q} = amp_{lin}^4$
$amp_{lin} = \sqrt[4]{amp_{q}}$
Da ampiezza lineare a decibels e viceversa
$dB = 20 * log_{10}(amp_{lin})$
$amp_{lin} = 10^{(dB/20)}$
In SuperCollider
a = 0.5;
v = a * 127;
v = 64;
a = v / 127;
a = 0.5;
q = a**4;
q = 0.0625;
a = pow(q, 1/4); // radice ennesima di x = x**(1/radice ennesima)
a = 0.5;
d = 20 * log10(a);
d = -12;
a = 10**(d/20);
In Max
from IPython.display import HTML
HTML('<center><img src="media/max_ampconv.png" width="85%"></center>')
Quando controlliamo i parametri di uno strumento possiamo farlo principalmente in due modi.
Un'interpolazione ci permette di realizzare il seguente concetto.
_vai da a fino a b in n_passi_
(
a = 0.1; // Inizio
b = 1.0; // Fine
n = 10; // Numero di passi
i = Array.interpolation(n, a, b); // Interpolazione
i.postln;
i.plot.plotMode_(\points);
)
// 1 2 3 4 5 6 7 8 9 10 Passi
// [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] Valori
Una variante del concetto precedente spesso impiegata in musica nel sostituire 'a' con l'ultimo valore raggiunto (stato):
_(dal valore raggiunto) vai fino a b in n_passi_
a = 0.1; // Valore di init (fuori dal blocco di codice)
(
b = rand(50); // Vai a questo valore
n = 6; // in n passi
i = Array.interpolation(n, a, b);
i.postln;
i.plot(minval:0,maxval:50).plotMode_(\points);
a = b; // L'ultimo valore diventa quello di partenza
)
// 1 2 3 4 5 6 Passi
// [0.1, 7.88, 15.66, 23.44, 31.22, 39.0] Valori
In entrambe i casi il delta o incremento del valore a ogni passo è costante in quanto si tratta di interpolazioni lineari.
In informatica musicale usiamo un tipo di interpolazioni chiamate rampe dove il numero di passi per andare da un valore all'altro è legato al tempo e corrisponde a un numero finito di campioni.
Il concetto delle rampe è:
_vai da a fino a b in tot_tempo_
Il numero di passi corrisponde alla rata di campionamento per il tempo specificato.
Esempio: vai da 0.0 a 1.0 in 1 secondo.
Se la rata di campionamento è 44.100 Hz i passi saranno 44.100 e il valore incrementale (delta) a ogni campione di 1/44.100 mentre se è 48.000 i passi saranno 48.000 e il valore incrementale più piccolo (1/48.000).
La rampa andrà comunque da 0.0 a 1.0 in un secondo.
Avrà semplicemente più o meno definizione.
In SuperCollider possiamo realizzare interpolazioni nell'Interprete mentre e rampe nel Server.
In Max ci sono oggetti sia per generare interpolazioni (controllo) che rampe (audio).
Interpolazioni non lineari
Ci sono due tipi di interpolazioni:
from IPython.display import HTML
HTML('<center><img src="media/ramp_1.png" width="45%"></center>')
Nelle interpolazioni non lineari se i valori sono compresi tra 0.0 e 1.0 cambia la curva ma non inizio e fine, se invece il range è compreso tra altri valori inizio e/o fine vengono riscalati a seconda dell'esponente o della base logaritmica.
In SuperCollider possiamo utilizzare la UGen Line.ar() per generare rampe lineari e la UGen XLine.ar() per rampe esponenziali.
Per entrambe gli argomenti sono: Line.ar(inizio, fine, durata).
Per XLine-ar() inizio e fine non possono essere 0.0 e devono avere lo stesso segno.
(
SynthDef(\myLine, {arg a=0,b=1,dur=1
var rampa;
rampa = Line.ar(a,b,dur);
//rampa = XLine.ar(a,b,dur);
Out.ar(0, rampa)
}).add;
)
a = Synth(\myLine);
//a = Synth(\myLine. [0.001, 1, 1]); // Se XLine..
a.set(\a,1, \b,0.001, \dur,0.5); // se eseguiamo non accade nulla...
Se proviamo a modificare dinamicamente uno dei parametri invocando .set() la rampa non si ripete in quanto viene generata solo alla creazione del Synth.
Questa è una importante limitazione di queste UGens che le differenzia dagli oggetti omonimi di Max.
Uno degli argomenti comuni ad alcune UGens è doneAction:n che si rivela estremamente utile per l'allocazione dinamica delle voci (dynamic voice allocation) e per risparmiare risorse della CPU.
Possiamo assegnare a questo argomento un valore compreso tra 0 e 14.
Ognuno di questi rappresenta un'azione automatica che ha effetto sul Synth che contiene la UGen nella quale è specificato questo argomento quando questa ha terminato il suo compito (nel caso di Line al termine della rampa).
from IPython.display import HTML
HTML('<center><img src="media/done_2.png" width="60%"></center>')
Tra tutte queste possibilità quelle che sono più utilizzate sono doneAction:0
(
{[SinOsc.ar,
Line.ar(0, 1, 1, doneAction:0),
SinOsc.ar*Line.ar(0, 1, 1, doneAction:0)
]}.plot(2);
)
{SinOsc.ar*Line.ar(0, 1, 1, doneAction:0).scope}.scope
e doneAction:2
(
{[SinOsc.ar,
Line.ar(0, 1, 1, doneAction:2),
SinOsc.ar*Line.ar(0, 1, 1, doneAction:2)
]}.plot(2);
)
{SinOsc.ar*Line.ar(0, 1, 1, doneAction:2).scope}.scope
In Max possiamo realizzare rampe di diverso tipo con gli oggetti line, line~ e curve~.
from IPython.display import HTML
HTML('<center><img src="media/rampe.png" width="100%"></center>')
Per quanto riguarda il terzo argomento di curve~:
Dall'outlet destro esce un bang alla fine della rampa.
Nel paragrafo precedente abbiamo osservato rampe di un solo segmento.
Per generare rampe multisegmento possiamo definire i diversi punti su un piano cartesiano specificandone le coordinate (xy) dove le ascisse (x) corrispondono al tempo mentre le ordinate (y) ai livelli.
from IPython.display import HTML
HTML('<center><img src="media/bpf_1.png" width="50%"></center>')
Questo tipo di descrizione e visualizzazione si chiama BPM (Break Point Function) e in quasi tutti i software musicali è presente un'interfaccia grafica che ci permette di definire e visualizzare tutti i segmenti multirampa che desideriamo.
In SuperCollider la classe Env mentre in Max l'oggetto function.
La classe Env ci permette di definire tutte le tipologie di inviluppi che desideriamo sotto forma di BPF.
Il metodo .new (che ricordiamo può essere sottinteso) crea un nuovo inviluppo (istanza).
I suoi primi due argomenti sono:
Il metodo .test serve come monitor uditivo.
Il metodo .plot come monitor visivo.
s.boot; // Per il metodo .test
(
Env.new([0, 0.7, 0.1, 0], // Livelli
[ 0.1, 0.2, 1 ] // Tempi delta
).test.plot(minval: 0, maxval: 1);
)
from IPython.display import HTML
HTML('<center><img src="media/bpf_2.png" width="50%"></center>')
Possiamo anche specificare un terzo argomento che ne specifica la curva in due modi.
(
[Env([0,0.5,0.1,0], [0.1,0.2,1], \step), // Scalini
Env([0,0.5,0.1,0], [0.1,0.2,1], \lin), // Curva lineare
Env([0.0001,0.5,0.1,0.0001],[0.1,0.2,1], \exp), // Curva esponenziale
Env([0,0.5,0.1,0], [0.1,0.2,1], \sin), // Curva sinusoidale
Env([0,0.5,0.1,0], [0.1,0.2,1], \wel), // Curva welch
Env([0,0.5,0.1,0], [0.1,0.2,1], \sqr), // Curva radice
Env([0,0.5,0.1,0], [0.1,0.2,1], \cub)].plot; // Curva quartica
)
(
[Env([0,0.5,0.1,0],[0.1,0.2,1], -0.8), // Curva logaritmica_n
Env([0,0.5,0.1,0],[0.1,0.2,1], 0.0), // Curva lineare
Env([0,0.5,0.1,0],[0.1,0.2,1], 0.8)].plot // Curva esponenziale_n
)
Possiamo specificare curve diverse, una per ogni segmento sotto forma di Array.
(
[Env([0,0.5,0.1,0], [0.1,0.2,1], [ 2, 0, -8]),
Env([0,0.5,0.1,0], [0.1,0.2,1], [\lin,\exp,\sqr])].plot;
)
Una Break Point Function descrive punti su un piano cartesiano non rampe.
Dobbiamo generare i valori intermedi tra i punti attraverso interpolazioni che trasformino le BPF in segnali di controllo.
Così facendo potremo moltiplicare tra loro i valori dei singoli campioni del segnale al quale vogliamo applicare l'inviluppo con i valori delle rampe generate dall'inviluppo.
La UGen EnvGen svolge questo compito.
(
{EnvGen.kr( // Anche .ar
Env.new([0,0.5,0.1,0],[0.01,0.2,1],\cub), // BPF
1, // Gate (1 = noteon, 0 = noteoff)
doneAction:2) // doneAction
}.scope;
)
Esempio di inviluppo d'ampiezza.
from IPython.display import HTML
HTML('<center><img src="media/envi.png" width="40%"></center>')
(
SynthDef(\envi,
{arg freq=500, amp=0, gate=0, done=2;
var sig, env;
sig = SinOsc.ar(freq);
env = Env.new([0,1,0.5,0], [0.01,0.2,1], \cub);
env = EnvGen.kr(env, gate, doneAction:done);
sig = sig * env * amp;
Out.ar(0,sig)
}
).add
)
Synth(\envi,[\freq,rrand(200,2000), \amp, rand(1.0), \gate,1]);
Per la definizione grafica di un inviluppo in Max possiamo utilizzare l'oggetto function che genera una lista di valori come quella che abbiamo definito a mano nei message box nel paragrafo dedicato alle rampe.
from IPython.display import HTML
HTML('<center><img src="media/max_function.png" width="100%"></center>')
Un trigger è il comando che fa partire una qualsiasi azione o evento come l'invio della nuova frequenza di una nota, l'avvio di una sequenza di note o di un suono, l'invio di parametri di controllo a un Synth oppure la partenza di un inviluppo.
Esistono due tipologie di inviluppi che si distinguono tra loro in base al numero di triggers necessario per realizzarsi.
inviluppi senza fase di sostegno - pensiamo a un tamburo o a uno strumento a percussione: il suono (il suo inviluppo d'ampiezza) comincia nel momento in cui la bacchetta o le mani colpiscono la pelle dello strumento (trigger) dopodichè il suono termina dopo un tot di tempo (durata) con l'esaurirsi delle vibrazioni dello strumento.
E' necessario un solo trigger iniziale (gate:1) e dobbiamo specificare la durata del suono.
from IPython.display import HTML
HTML('<center><img src="media/env_nosust.png" width="50%"></center>')
inviluppi con fase di sostegno - pensiamo a una tastiera: quando premiamo un tasto (note On) facciamo partire l'inviluppo dopodichè il suono continua (fase di sostegno) fino a quando non rilasciamo il tasto (note Off) facendo partire il rilascio del suono fino all'estinzione.
In questo caso sono necessari due triggers:
gate:0 - fa partire il rilascio del suono.
e non possiamo specificare la durata del suono.
from IPython.display import HTML
HTML('<center><img src="media/env_sust.png" width="50%"></center>')
Sviscereremo le caratteristiche di entrambi in paragrafi dedicati.
from IPython.display import HTML
HTML('<center><img src="media/trig.png" width="40%"></center>')
Per definire gli inviluppi senza fase di sostegno dobbiamo stabilire:
A seconda delle caratteristiche del software di realizzazione potremmo dover effettuare una serie di operazioni per ottimizzare la manipolazione di questi parametri in modo musicale.
In SuperCollider possiamo definire qualsiasi profilo di inviluppo invocando il metodo .new() sulla classe Env oppure utilizzare altri metodi che richiamano i profili di tipologie classiche.
Invocando il metodo .new() sulla classe Env generiamo un'istanza di un inviluppo custom che possiamo disegnare a piacere definendone tutti i parametri (livelli, tempi_delta, curve).
Può essere sia con che senza fase di sostegno.
Nella sua versione senza fase di sostegno i suoi argomenti sono:
s.boot;
// livelli tempi delta curva
Env.new([0, 1, 0.65, 0.8, 0],[0.3, 0.8, 0.5, 1], \sine).test.plot;
Env.new([0, 1, 0.65, 0.8, 0],[0.3, 0.8, 0.5, 1], [0.9, 0.6, 0, -0.7]).test.plot;
Potremmo pensare che per questo tipo di inviluppi sia superfluo inviare il messaggio gate:0 (note off) in quanto terminano il loro percorso automaticamente.
Non è così.
Per convenzione infatti in quasi tutti i software dobbiamo sempre fare seguire un messaggio di note off dopo uno di note on anche quando sembra superfluo.
(
SynthDef(\envi,
{arg freq=980, amp=0, gate=0, done=0;
var sig, env;
sig = SinOsc.ar(freq);
env = Env.new([0,1,0.3,0],[0.01,0.2,0.3],\cub);
env = EnvGen.kr(env, gate, doneAction:done);
sig = sig * env * amp;
Out.ar(0, sig)
}
).add;
)
a = Synth(\envi);
a.set(\freq, 967, \amp, 0.5, \gate,1); // Se valutiamo nuovamente prima di gate 0 non va
a.set(\gate,0);
a.set(\freq, 893, \amp, 0.5, \gate,1);
a.set(\gate,0);
Questo meccanismo rende piuttosto macchinoso il controllo musicale e la programmazione di sequenze con questo tipo di inviluppi.
Se però facciamo precedere t_ al nome dell'argomento assegnato a gate, SuperCollider genera il messaggio di gate:0 (note Off) automaticamente alla fine della rampa.
(
SynthDef(\envi,
{arg freq=980, amp=0, t_gate=0, done=0;
var sig, env;
sig = SinOsc.ar(freq);
env = Env.new([0,1,0.3,0],[0.01,0.2,0.3],\cub);
env = EnvGen.kr(env, t_gate, doneAction:done);
sig = sig * env * amp;
Out.ar(0, sig)
}
).add;
)
a = Synth(\envi);
a.set(\amp, 0.5, \t_gate,1);
Livelli e tempi delta di Env.new sono specificati sotto forma di Array.
Un array è una collezione indicizzata di dati di qualsiasi tipo (numeri, UGens, Env, etc.).
In SuperCollider sono inclusi tra parentesi quadre e separati da virgole.
Ad ogni elemento corrisponde un indice sottinteso che parte da 0.
// 0 1 2 3 4 Indici sottintesi
a = [34, 45.4, "ciao", Env.new, SinOsc.ar]; // Elementi (items)
a.postln;
Gli array sono impiegati per molti propositi, possiamo modificarne il contenuto in diversi modi invocando metodi dedicati ed effettuare operazioni matemetiche ma non possiamo modificarne il numero di elementi dinamicamente.
a = [10, 20, 30, 40];
a.put(0, 23); // Sostituisci il 10 all'indice 0 con 23
b = a.reverse; // Inverte l'ordine
c = a * 100; // Moltiplica tutti gli elementi per 100
Esiste un particolare tipo di array che si chiama literal array sul quale possiamo effettuare tutte le operazioni che vogliamo ma del quale non possiamo modificarne il contenuto ed è identificato con un # prima della parentesi di apertura.
a = #[10, 20, 30, 40];
a.put(0, 23); // Dà errore
Quando un array contiene valori numerici, possiamo visualizzarli in un piano cartesiano invocando il metodo .plot().
Gli indici (sottintesi) rappresentano le ascisse (x) e i valori (elementi) le ordinate (y).
#[12,34,56,3,78,98,23,9].plot; // valori determinati
Questo metodo come molti altri può essere invocato su diversi tipi di oggetti (polimorfismo), tra i quali gli Array.
Se richiamiamo l'Help file di un metodo compare un elenco di oggetti sui quali può essere invocato.
from IPython.display import HTML
HTML('<center><img src="media/plotter_2.png" width="60%"></center>')
L’aspetto di un plot è personalizzabile attraverso gli argomenti.
Prestiamo attenzione che a seconda del tipo di data sul quale è invocato, gli argomenti di un metodo possono cambiare.
Nell’esempio seguente sono presenti quelli spendibili nella visualizzazione di Array numerici.
(
a = #[12,34,56,3,78,98,23,9];
a.plot(name: "mio plot", // Nome
bounds:540@200, // Dimensioni x@y in pixels
minval: -100, // Valore limite inferiore
maxval: 100, // Valore limite superiore
discrete:true) // True = punti, false = linee
)
E' sempre raccomandabile specificare minval e maxval perchè di default i limiti si adattano ai valori contenuti nell’array e questo potrebbe generare errori di lettura e valutazione.
Confrontiamo le seguenti visualizzazioni.
(
a = #[0.1,0.5,0.6,0.1,0.9,0.3];
b = #[1,5,2,4,8,10,5,6];
[a, b].plot;
[a, b].plot(minval:0,maxval:10);
)
Nei paragrafi precedenti la durata delll'evento sonoro è data dalla somma dei tempi delta definiti nell'array dedicato.
Musicalmente è però preferibile definire prima la durata in valori assoluti (secondi o millisecondi) per poi riscalare proporzionalmente le durate delle singole rampe dell'inviluppo.
Per compiere questa operazione dobbiamo fare in modo che la somma dei valori nell'array dei tempi delta sia 1.0 (relativa) in modo da poterli moltiplicare per il valore della durata.
s.boot;
s.plotTree;
s.scope;
(
d = 1.0; // Durata (secondi o ms)
l = [0.0, 1.0, 0.5, 0.8, 0.0]; // Livelli (tra 0.0 e 1.0)
t = [ 0.1, 0.3, 0.2, 0.4 ]; // Tempi - somma = 1.0
t = t * d; // Riscala sulla durata
Env.new(l, t, -4).test.plot;
)
Possiamo definire inviluppi con pochi segmenti effetturando il calcolo a mente mentre se vogliamo pensare in modo proporzionale dobbiamo invocare sull'array dei tempi il metodo [ ].normalizeSum che riscala i valori in modo che la somma dia 1.0.
[10, 30, 50, 90].normalizeSum;
Oltre a Env.new() con il quale possiamo realizzare qualunque tipo di inviluppo SuperCollider fornisce diversi metodi dedicati per la definizione di inviluppi classici principalmente impiegati come inviluppi d'ampiezza.
Anche in questi metodi possiamo riscalare proporzionalmente segmenti e durata.
Env.linen - inviluppo trapezoidale.
// attacco sostegno rilascio livello curva
Env.linen(0.1, 0.2, 0.1, 1, 0 ).test.plot;
Env.linen(1, 2, 3, 0.3, \sine ).test.plot;
Env.linen(1, 2, 3, 1, [[\sine, \welch, \lin, \exp]]).plot; // Array
d = 0.25; // Durata (secondi)
Env.linen(0.1*d, 0.4*d, 0.1*d).test.plot; // La somma deve dare 1.0
(
SynthDef(\trap,
{arg freq=890, t_ciao=0, dur=1, a=0.1, s=0.2, r=0.6, amp=0, done=2;
var sig,env;
sig = SinOsc.ar(freq);
env = Env.linen(a*dur,s*dur,r*dur,amp,-4);
env = EnvGen.kr(env,t_ciao,doneAction:done);
sig = sig * env;
Out.ar(0, sig)
}
).add;
)
w = Synth(\trap, [\done, 0]);
w.set(\dur,rrand(0.1,3.0),\amp,0.5, \t_ciao,1);
(
a = rand(0.5) + 0.01;
u = rand(1-a) + 0.1;
r = 1-a-u; // Somma deve essere 1.0
[a,u,r].postln;
[a,u,r].sum.postln;
w.set(\dur, rrand(0.1,3),
\a, a,
\s, u,
\r, r,
\amp, 0.3,
\t_ciao, 1)
)
Env.perc - inviluppo percussivo
// attacco rilascio livello curva
Env.perc(0.1, 0.9, 1, 0 ).test.plot;
Env.perc(1, 3, 0.3, \sine ).test.plot;
Env.perc(1, 2, 1, [[\sine, \welch]]).plot; // Array
d = 0.25; // Durata (secondi)
Env.perc(0.1*d, 0.9*d).test.plot; // La somma deve dare 1.0
(
SynthDef(\perc,
{arg freq=890, t_ciao=0, dur=1, a=0.1, r=0.6, amp=0, done=2;
var sig,env;
sig = SinOsc.ar(freq);
env = Env.perc(a*dur,r*dur,amp,-4);
env = EnvGen.kr(env,t_ciao,doneAction:done);
sig = sig * env;
Out.ar(0, sig)
}
).add;
)
w = Synth(\perc, [\done, 0]);
w.set(\dur,rrand(0.1,3.0),\amp,0.5, \t_ciao,1);
(
a = rand(0.5) + 0.01;
r = 1-a; // Somma deve essere 1.0
[a,r].postln;
[a,r].sum.postln;
w.set(\dur, rrand(0.1,3),
\a, a,
\r, r,
\amp, 0.3,
\t_ciao, 1)
)
Env.sine - inviluppo sinusoidale
// durata livello
Env.sine(0.1, 1, ).test.plot;
d = 0.25; // Durata (secondi)
Env.sine(d).test.plot;
(
SynthDef(\sine,
{arg freq=890, t_ciao=0, dur=1, amp=0, done=2;
var sig,env;
sig = SinOsc.ar(freq);
env = Env.sine(dur,amp);
env = EnvGen.kr(env,t_ciao,doneAction:done);
sig = sig * env;
Out.ar(0, sig)
}
).add;
)
w = Synth(\sine, [\done, 0]);
w.set(\dur,rrand(0.1,3.0),\amp,0.5, \t_ciao,1);
Env.triangle - inviluppo triangolare
// durata livello
Env.triangle(0.1, 1, ).test.plot;
d = 0.25; // Durata (secondi)
Env.triangle(d).test.plot;
(
SynthDef(\triangle,
{arg freq=890, t_ciao=0, dur=1, amp=0, done=2;
var sig,env;
sig = SinOsc.ar(freq);
env = Env.triangle(dur,amp);
env = EnvGen.kr(env,t_ciao,doneAction:done);
sig = sig * env;
Out.ar(0, sig)
}
).add;
)
w = Synth(\triangle, [\done, 0]);
w.set(\dur,rrand(0.1,3.0),\amp,0.5, \t_ciao,1);
In Max possiamo definire inviluppi in due modi, con message box o con l'oggetto grafico function.
La problematica principale che affronteremo in questo paragrafo riguarda la diversa sintassi rispetto a quella impiegata in SuperCollider attraverso la quale descriviamo i parametri di un inviluppo e il riscalaggio di tempi relativi sulla durata in milisecondi.
[0., 1., 0.5, 0.] - L Livelli
[ 0.1, 0.2, 0.7 ] - T Tempi
from IPython.display import HTML
HTML('<center><img src="media/liste.png" width="25%"></center>')
In SuperCollider abbiamo due array uno per i livelli e uno per i tempi mentre in Max abbiamo coppie di livello tempo come per le rampe.
Un'inversione di righe e colonne di una matrice.
In Max le collezioni di dati come gli Array di SuperCollider si chiamano list e possiamo definirle principalmente all'interno di message box.
Così come gli Array di SuperCollider gli elementi hanno indici sottintesi.
from IPython.display import HTML
HTML('<center><img src="media/liste_1.png" width="28%"></center>')
Su di esse possiamo compiere un discreto numero di operazioni con la collezione di oggetti zl.
Possiamo definire le caratteristiche di un inviluppo con valori temporali assoluti.
La durata sarà data dalla somma dei tempi delta.
from IPython.display import HTML
HTML('<center><img src="media/env_ass.png" width="45%"></center>')
Se invece vogliamo definire una durata e riscalare i tempi delta relativi su di essa come abbiamo visto in SuperCollider dobbiamo effettuare qualche operazione.
Per riscalare sia singoli valori che liste possiamo utilizzare l'oggetto scale.
from IPython.display import HTML
HTML('<center><img src="media/scale.png" width="63%"></center>')
Dopodichè dobbiamo:
separare i livelli dai tempi ottenendo due liste differenti come in SuperCollider (oggetto zl delace).
riscalare i valori proporzionali in tempi assoluti (tra 0.0 e la durata).
ricostruire la lista nella forma sintattica originale (oggetto zl lace).
from IPython.display import HTML
HTML('<center><img src="media/assoluto.png" width="70%"></center>')
Assembliamo il tutto in un patch.
Notiamo come la struttura sia la stessa delle SynthDef e del controllo dei parametri in SuperCollider.
from IPython.display import HTML
HTML('<center><img src="media/env_instr.png" width="93%"></center>')
Possiamo ottimizzare il patch formalizzandolo subpatches come in precedenza.
Possiamo anche utilizzare la GUI function modificandone dinamicamente la durata con il messaggio setdomain.
from IPython.display import HTML
HTML('<center><img src="media/func_dur.png" width="45%"></center>')
Per definire gli inviluppi senza fase di sostegno dobbiamo stabilire:
La durata totale non è definita e possiamo definire le durate dei segmenti di attacco e rilascio in valori di tempo assoluto.
Aggiungiamo un argomento (nodo di sostegno) a quelli che già conosciamo per specificare il numero del nodo di sostegno come indice dell'array dei livelli.
L'inviluppo interrompe il suo corso in questo punto fino a quando non riceve un messggio di gate:0.
from IPython.display import HTML
HTML('<center><img src="media/env_sust_1.png" width="55%"></center>')
s.boot;
s.scope;
s.plotTree;
(
a = Env.new([0,0.8,0.2,0],[0.1,0.2,3],\cub, 2);
a.plot;
a.isSustained; // Riporta se c'è un sustain o meno
)
(
SynthDef(\sust,
{arg freq=935, amp=0, sus=2, gate=0, done=0;
var sig, env;
sig = SinOsc.ar(freq);
// ID 0 1 2 (Nodi)
env = Env.new([0, 0.8, 0.2, 0],[0.1,0.2,3],\cub, sus);
env = EnvGen.kr(env,gate,doneAction:done);
sig = sig * amp * env;
Out.ar(0, sig)
}
).add;
)
a = Synth(\sust, [\amp, 0.5]);
a.set(\gate, 1); // Note on (trigger inizio e si ferma al nodo specificato)
a.set(\gate, 0); // Note off (trigger per proseguire)
Se vogliamo testare questo tipo di inviluppi invocando il metodo .test possiamo specificare un tempo di ritardo misurato dal trigger iniziale dopo il quale verrà automaticamente generato il messaggio di gate 0:
Env.new([0,0.5,0.05,0],[0.1,1,2], \cub, 1).test(5).plot; // dopo 5" --> gate:0
Se aggiungiamo un ulteriore argomento specifichiamo il nodo di loop.
Possiamo utilizzare questo nodo per realizzare un loop tra il nodo di sostegno e questo.
Il suo indice deve essere inferiore al nodo di sostegno.
Il loop prosegue all'infinito fino a quando non riceve un messaggio di gate 0.
a = Env.new([0,0.7,0.1,0.15,0.1,0.3,0], [0.1,0.2,0.15,0.15,0.2,1], \cub, 2, 4).plot;
from IPython.display import HTML
HTML('<center><img src="media/env_loop.png" width="55%"></center>')
(
SynthDef(\loop,
{arg freq=935, amp=0, startloop=2, endloop=4, gate=0, done=0;
var sig, env;
sig = SinOsc.ar(freq);
env = Env.new([0,0.7,0.1,0.15,0.1,0.3,0],
[0.1,0.2,0.15,0.15,0.2,1],
\cub,
endloop, startloop);
env = EnvGen.kr(env,gate,doneAction:done);
sig = sig * env * amp;
Out.ar(0, sig)
}
).add;
)
a = Synth(\loop, [\amp, 0.5]);
a.set(\gate,1); // Note on (trigger inizio e si ferma al nodo specificato)
a.set(\gate,0); // Note off (trigger per proseguire)
Così come per gli inviluppi senza sostegno in SuperCollider esistono alcuni metodi dedicati che possiamo invocare per realizzare inviluppi ormai classici.
Env.asr - inviluppo trapezoidale (come Env.linen ma con fase di sostegno).
from IPython.display import HTML
HTML('<center><img src="media/e.asr.png" width="55%"></center>')
// attacco sostegno rilascio curva
Env.asr(0.08, 1, 2, \sin).test(4).plot;
(
SynthDef(\asr,
{arg freq=890, gate=0, a=0.1, r=0.6, amp=0, done=2;
var sig,env;
sig = SinOsc.ar(freq);
env = Env.asr(a,amp,r,-4);
env = EnvGen.kr(env,gate,doneAction:done);
sig = sig * env;
Out.ar(0, sig)
}
).add;
)
a = Synth(\asr, [\amp,0.5]);
a.set(\gate,1);
a.set(\gate,0);
Env.adsr - il più classico degli inviluppi.
from IPython.display import HTML
HTML('<center><img src="media/e.adsr.png" width="55%"></center>')
// attacco decadimento sostegno rilascio curva
Env.adsr(0.08, 0.1, 0.5, 2, curve:\sin).test(4).plot;
(
SynthDef(\adsr,
{arg freq=890, gate=0, a=0.1, d= 0.1, s=0.5, r=0.6, amp=0, done=0;
var sig,env;
sig = SinOsc.ar(freq);
env = Env.adsr(a,d,s,r);
env = EnvGen.kr(env,gate,doneAction:done);
sig = sig * env * amp;
Out.ar(0, sig)
}
).add;
)
a = Synth(\adsr, [\amp,0.5]);
a.set(\gate,1);
a.set(\gate,0);
Env.circle - inviluppo in loop - l'array dei tempi delta deve contenere lo stesso numero di elementi di quello dei livelli.
from IPython.display import HTML
HTML('<center><img src="media/e.circle.png" width="55%"></center>')
// livelli tempi curva
Env.circle([0.0, 0.6,0.3, 0.8,0.0], [0.02,0.2,0.01,0.3,0.2], \cub).plot;
(
SynthDef(\circle,
{arg freq=890, gate=0, amp=0;
var sig,env;
sig = SinOsc.ar(freq);
env = Env.circle([0.0, 0.3, 0.1, 0.5, 0.0 ],
[ 0.02, 0.2, 0.01, 0.3,0.2],\cub);
env = EnvGen.kr(env,gate);
sig = sig * env * amp;
Out.ar(0, sig)
}
).add;
)
a = Synth(\circle, [\amp,0.5]);
a.set(\gate,1);
a.set(\gate,0);
In alcune situazioni potremmo dover generare un fade in e un fade out automatico alla creazione e/o distruzione di un'istanza di Synth per evitare clicks dovuti a eventuali discontinuità del segnale.
In SuperCollider possiamo farlo in due modi.
(
SynthDef(\linen,
{arg freq=890, gate=0, a=0.1, r=0.6, amp=0, done=2;
var sig,env;
sig = SinOsc.ar(freq);
env = Linen.kr(gate, a, amp, r, done);
sig = sig * env;
Out.ar(0, sig)
}
).add;
)
a = Synth(\linen, [\amp,0.5]);
a.set(\gate,1);
a.set(\gate,0);
(
SynthDef(\fades,
{arg freq=890, gate=1, fin=1, fout=1, amp=0, done=2; // gate=1
var sig, fdIn, fdOut;
sig = SinOsc.ar(freq);
fdIn = XLine.kr(0.001,amp, fin);
fdOut = Env.cutoff(fout, amp, \sin);
fdOut = EnvGen.kr(fdOut,gate,doneAction:done);
sig = sig * fdIn * fdOut;
Out.ar(0, sig)
}
).add;
)
a = Synth(\fades, [\amp,0.5]);
a.set(\gate,0);
In Max possiamo costruire inviluppi con fase di sostegno in tre modi.
In Max possiamo impiegare due messaggi uno per il note on che invia frequenza e ampiezza e un'altro per il note off che invia la stessa frequenza con ampiezza 0.0.
Il valore dell'ampiezza determina (oggetto sel 0.) se il trigger è indirizzato alla rampa che genera un fade in (ampiezza maggiore di 0) oppure a quello che genera un fade out (ampiezza uguale a 0).
from IPython.display import HTML
HTML('<center><img src="media/noteonoff.png" width="73%"></center>')
Se utilizziamo cmd + doppio click su un punto dell'oggetto function lo definiamo come nodo di sostegno esattamente come in SuperCollider.
Il primo trigger (bang) ha funzione di noteon il messaggio next di noteoff.
La strategia di patching è la stessa adottata per il messaggi.
from IPython.display import HTML
HTML('<center><img src="media/susfunc.png" width="85%"></center>')
L'oggetto adsr~ genera il più classico degli inviluppi d'ampiezza.
La figura ne illustra le caratteristiche e le numerose informazioni utili che può riportare.
from IPython.display import HTML
HTML('<center><img src="media/max_adsr.png" width="75%"></center>')
Panning o panpot significa posizionare un segnale monofonico in uno spazio bi o tri-dimensionale delimitato da due o più altoparlanti.
Non confondiamo il pannning con la spazializzazione in quanto il primo è dedicato alla posizione e al movimento dei segnali in uno spazio acustico neutro mentre la seconda simula posizione e movimento di una sorgente sonora virtuale in relazione a un punto di ascolto computando anche parametri come volume (più la sorgente si allontana e più diminuisce), riverberazione dinamica (più la sorgente si allontana maggiore sarà l'ampiezza del riverbero rispetto a quella del suono diretto), assorbimento dell'aria, effetto doppler, etc.
I bus audio sono i canali di ingresso e uscita dei segnali dal software ( e dal computer).
Se non utilizziamo un'interfaccia audio multicanale usualmente in un computer abbiamo a disposizione 1 canale in ingresso (microfono) e 2 canali in uscita (altoparlante sinistro e destro).
I software hanno dei bus audio che possiamo collegare a questi canali come desideriamo.
SuperCollider
Abbiamo a disposizione 128 canali audio (da 0 a 127) chiamati Bus.
Di default i primi tre sono pubblici ovvero utilizzati per i segnali in ingresso e in uscita dal software mentre gli altri sono privati ovvero disponibili per eventuali routing interni al software.
Possiamo scrivere qalsiasi tipo di segnale su ognuno di questi con la UGen Out.ar mentre per leggerli dobbiamo utilizzare la UGen In.ar.
Out.ar
Abbiamo già incontrato questa UGen all'interno delle SynthDef.
I suoi argomenti sono: _Out.ar(numero_del_bus, segnale_dacollegare) dove il segnale può anche essere un array di segnali.
Bus 0 è per convenzione il canale sinistro.
s.boot;
s.meter;
s.plotTree;
(
SynthDef(\out,{arg t_gate=0, outBus=0;
var sig, env;
sig = WhiteNoise.ar;
env = Env.perc;
env = EnvGen.kr(env,t_gate);
sig = sig * env * 0.5;
Out.ar(outBus, sig)
}).add;
)
a = Synth(\out);
a.set(\outBus,0, \t_gate,1);
a.set(\outBus,1, \t_gate,1);
a.set(\outBus,rand(2).postln, \t_gate,1);
Nel caso di segnali multicanale come primo argomento dobbiamo specificare il numero del bus più basso in quanto gli eventuali ulteriori canali saranno inviati al bus attiguo in ordine crescente.
s.scope(3);
(
SynthDef(\out2,{arg t_gate=0, outBus=0;
var siga, sigb, env;
siga = WhiteNoise.ar;
sigb = Saw.ar;
env = Env.perc;
env = EnvGen.kr(env,t_gate);
siga = siga * env * 0.2;
sigb = sigb * env * 0.2;
Out.ar(outBus, [siga, sigb]) // Array di segnali
}).add;
)
a = Synth(\out2);
a.set(\outBus,0, \t_gate,1);
a.set(\outBus,1, \t_gate,1);
SoundIn.ar
Se invece vogliamo leggere il segnale proveniente da un microfono o da un canale audio esterno possiamo utilizzare la UGen SoundIn.ar.
(
SynthDef(\in, {arg gate=0, inBus=0, outBus=0;
var sigIn, rev, env;
sigIn = SoundIn.ar(inBus);
rev = FreeVerb.ar(sigIn);
env = Linen.kr(gate,0.2,0.5);
rev = rev * env;
Out.ar(outBus, rev)
}).add;
)
a = Synth(\in);
a.set(\gate,1);
a.set(\gate,0);
Max
In Max possiamo specificare i bus in entrata e in uscita come argomenti degli oggetti adc~ e dac~.
from IPython.display import HTML
HTML('<center><img src="media/inoutbus.png" width="45%"></center>')
Possiamo anche mappare i canali in ingresso e in uscita dal pannello Open I/O Mappings nell'Audio status.
from IPython.display import HTML
HTML('<center><img src="media/io_map.png" width="35%"></center>')
Se duplichiamo un segnale monofonico scrivendolo su due canali attigui e lo diffondiamo tramite due altoparlanti non otteniamo un segnale stereofonico ma dual-mono o multi-mono (due o più segnali monofonici uguali) come se fossero due strumenti acustici che suonano la stessa parte.
(
SynthDef(\dmono,{arg gate=0;
var sig, env;
sig = Pulse.ar;
env = Linen.kr(gate);
sig = sig * env;
Out.ar(0, [sig, sig]) // Due segnali mono identici
}).add;
)
a = Synth(\dmono);
a.set(\gate,1);
a.set(\gate,0);
Lo stesso in Max.
from IPython.display import HTML
HTML('<center><img src="media/dualmono.png" width="15%"></center>')
Un sistema di diffusione stereofonico è costituito da due altoparlanti frontali posizionati con un angolo di 60° rispetto al punto di ascolto ottimale.
from IPython.display import HTML
HTML('<center><img src="media/stereo_angolo.png" width="25%"></center>')
E' un sistema monodimensionale frontale in quanto possiamo posizionare e muovere una o più sorgenti sonore monofoniche (source) nello spazio compreso tra l'altoparlante sinistro e quello destro (fronte stereofonico) ma la distanza può essere simulata solo alle spalle della linea immaginaria che collega i due diffusori.
Per ottenere un segnale stereofonico dobbiamo prendere un segnale monofonico e distribuire la sua ampiezza tra due canali attigui in modo che la somma delle ampiezze in uscita dai due altoparlanti sia la stessa del segnale monofonico originale e mai superiore a 1.0.
Il rapporto tra le ampiezze in uscita dagli altoparlanti definisce la posizione della sorgente nel fronte stereofonico.
from IPython.display import HTML
HTML('<center><img src="media/stereo_0.png" width="40%"></center>')
s.boot;
s.meter;
s.plotTree;
(
SynthDef(\stereo,
{arg pan=0.5; // 0 = sinistra, 0.5 = centro, 1 = destra
var sig, sx, dx;
sig = SinOsc.ar;
sx = 1.0 - pan; // Ampiezza altoparlante sinistro
dx = pan; // Ampiezza altoparlante destro
Out.ar(0, [sx, dx] * sig)
}).add;
)
a = Synth(\stereo);
a.set(\pan,0); // Sinistra
a.set(\pan,0.5); // Centro
a.set(\pan,1); // Destra
Abbiamo impiegato un solo valore per controllare il panning calcolando automaticamente le ampiezze del segnale in uscita.
Ascoltando attentamente notiamo che la percezione dell'intensità del suono alle diverse posizioni non è uguale.
Al centro risulta più debole rispetto a quando è ai lati.
Per ovviare a questo problema una delle possibili soluzioni consiste nel calcolare la radice quadrata del fattore di moltiplicazione delle ampiezze.
La somma dei segnali quando è posizionata al centro è maggiore di 1.0 ($\sqrt{0.5} = 0.707*2=1.141$) ma il risultato percettivo è corretto.
(
SynthDef(\stereo,
{arg pan=0.5; // 0 = sinistra, 0.5 = centro, 1 = destra
var sig, sx, dx;
sig = SinOsc.ar;
sx = (1.0 - pan).sqrt; // Ampiezza altoparlante sinistro
dx = pan.sqrt; // Ampiezza altoparlante destro
Out.ar(0, [sx, dx] * sig)
}).add;
)
a = Synth(\stereo);
a.set(\pan,0); // Sinistra
a.set(\pan,0.5); // Centro
a.set(\pan,1); // Destra
In SuperCollider realizziamo quanto appena illustrato con due UGens dedicate.
Entrambe accettano i seguenti argomenti: segnale_mono, posizione (-1.0 = sinistra, 0 = centro 1.0 = destra), level.
from IPython.display import HTML
HTML('<center><img src="media/stereo_1.png" width="40%"></center>')
Possiamo utilizzare l'argomento level per allontanare il suono diminuendone l'intensità (legge della distanza inversa).
from IPython.display import HTML
HTML('<center><img src="media/stereo_2.png" width="40%"></center>')
(
SynthDef(\pan,
{arg pos=0, lev=1;
var sig;
sig = SinOsc.ar;
//sig = LinPan2.ar(sig, pos, lev);
sig = Pan2.ar(sig, pos, lev);
Out.ar(0, sig)
}).add;
)
a = Synth(\pan);
a.set(\pan, rand2(1.0).postln);
In Max abbiamo due possibilità per realizzare un panning stereofonico.
from IPython.display import HTML
HTML('<center><img src="media/max_pan_1.png" width="35%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/max_pan2.png" width="25%"></center>')
Negli esempi di paragrafo precedente abbiamo cambiato la posizione in modo repentino.
Se vogliamo realizzare un movimento all'interno del fronte stereofonico possiamo utilizzare inviluppi oppure segnali di controllo.
Gli inviluppi non servono solo per controllare l'ampiezza ma qualsiasi parametro.
Basta impiegare il range di livelli corretto che nel caso del panning è compreso tra -1.0 e +1.0 per quanto riguarda la posizione e tra 0.0 e 1.0 per quanto riguarda la distanza.
(
SynthDef(\muovi,
{arg t_gate=0, dur=1, amp=0;
var sig, pos, dist;
sig = SinOsc.ar;
pos = Env.new([-1, 1, 0, 0.5, -0.7, 0], // Posizione
[ 1, 0.2, 2, 0.3, 1 ].normalizeSum * dur, // Tempi (relativi)
0); // Lineare
dist= Env.new([1, 0.2, 1], // Distanza
[ 0.5, 0.5 ].normalizeSum * dur,
-4); // Quartica
pos = EnvGen.kr(pos,t_gate); // Segnali di controllo
dist= EnvGen.kr(dist,t_gate);
sig = Pan2.ar(sig, pos, dist);
sig = sig * amp;
Out.ar(0, sig)
}).add;
)
a = Synth(\muovi);
a.set(\dur, 10, \amp, 0.5, \t_gate, 1);
Nel paragrafo dedicato abbiamo visto uno dei concetti che definiscono una rampa è: dal valore che hai vai a questo nuovo valore in tot tempo.
Equivale al concetto di smoothing (arrotondamento) o lag dei segnali.
Anche in questo caso lo smoothing può essere lineare o non lineare.
In SuperCollider possiamo utilizzare la UGen Lag che realizza questa operazione con una rampa non lineare (maggiormente percepibile).
(
SynthDef(\smooth,
{arg pos=0, ptime=0.5; // Posizione e tempo delta per raggiungere il nuovo valore
var sig, pan;
sig = SinOsc.ar;
pan = Lag.kr(pos, ptime); // Trasforma il float in segnale di controllo
//pan = pos.lag(ptime); // Sintassi alternativa
sig = Pan2.ar(sig, pan);
Out.ar(0, sig)
}).add;
)
a = Synth(\pan);
a.set(\smooth, rand2(1.0).postln);
In Max possiamo utilizzare sia line~ che curve~ controllandone il tempo di smoothing.
from IPython.display import HTML
HTML('<center><img src="media/max_smooth_2.png" width="50%"></center>')
Se invece vogliamo degli inviluppi possiamo specificarli come illustrato nel paragrafo dedicato.
Se vogliamo utilizzare l'oggetto function possiamo impostare il range con un messaggio.
from IPython.display import HTML
HTML('<center><img src="media/max_smooth_1.png" width="75%"></center>')
Nel paragrafo precedente abbiamo trasformato valori (int e/o float) in segnali di controllo.
Una tecnica derivata dall'audio analogico consiste nell'utilizzare direttamente segnali di controllo a frequenze subaudio per controllare il panning.
Possiamo chiamare le UGen o gli oggetti di Max utilizzati per questo compito Low Frequency Oscillator (LFO).
Dovrebbe risultare ora chiaro il motivo per il quale abbiamo riscalato i valori in un range tra -1.0 e +1.0.
I parametri che possiamo comtrollare in questo caso sono.
from IPython.display import HTML
HTML('<center><img src="media/panKsig_1.png" width="50%"></center>')
L'immagine è traslata: sull'asse verticale è rappresentato il tempo mentre sull'asse orizzontale i movimenti della sorgente tra sinistra (-1) e destra (+1).
// Qualsiasi UGen che genera un segnale compreso tra +1 e -1
s.scope;
{LFTri.ar(5)}.play;
{SinOsc.ar(5)}.play;
{LFNoise0.ar(5)}.play;
{LFNoise1.ar(5)}.play;
{LFNoise2.ar(5)}.play;
from IPython.display import HTML
HTML('<center><img src="media/panKsig_2.png" width="50%"></center>')
(
SynthDef(\vel,
{arg vel=0;
var sig, lfo;
sig = SinOsc.ar;
lfo = SinOsc.ar(vel/2); // vel/2 per segnali periodici bipolari
//lfo = LFNoise1.ar(vel); // Random
sig = Pan2.ar(sig, lfo);
Out.ar(0, sig)
}).add;
)
a = Synth(\vel);
a.set(\vel, 2);
from IPython.display import HTML
HTML('<center><img src="media/panKsig_4.png" width="50%"></center>')
(
{[SinOsc.ar(220,0), // 0 = parte dal centro e va verso 1,
SinOsc.ar(220,pi), // pi = parte dal centro e va verso -1
SinOsc.ar(220,0.5pi), // 0.5pi = parte da 1 e va verso -1
SinOsc.ar(220,-0.5pi)] // -0.5pi = parte da -1 e va verso 1
}.plot
)
from IPython.display import HTML
HTML('<center><img src="media/panKsig_3.png" width="50%"></center>')
(
SynthDef(\spread,
{arg vel=1, min= -0.3, max= 0.2; // Limiti
var sig, range, offset, lfo;
sig = SinOsc.ar;
range = (max - min) / 2;
offset = min + range;
lfo = SinOsc.ar(vel/2, mul:range, add:offset).scope;
sig = Pan2.ar(sig, lfo);
Out.ar(0, sig)
}).add;
)
a = Synth(\spread);
a.set(\min, -0.5, \max, 0.1);
In Max possiamo utilizzare l'oggetto pan2S che è anch'esso un'astrazione e accetta nel secondo inlet segnali di controllo.
from IPython.display import HTML
HTML('<center><img src="media/max_ksig_1.png" width="45%"></center>')
Abbiamo ora tutti gli elementi per prototipare due possibili modelli di strumento virtuale.
Questi modelli possono servire come schema sintattico per tutte le tecniche di sintesi ed elaborazione del suono.
from IPython.display import HTML
HTML('<center><img src="media/modello.png" width="35%"></center>')
Prima tipologia.
ServerOptions.devices;
s.options.device_("nome device"); // Imposta device
s.boot;
s.meter; // Monitor visivi
s.scope;
s.plotTree;
(
SynthDef(\notan,
{arg freq=900, amp=0, dur=1, pan=0, tpan=0, t_gate=0, done=2;
var sig, env;
sig = Saw.ar(freq);
env = Env.new([0,1,0.5,0.5,0], [0.1,0.1,0.4,1].normalizeSum*dur, -4);
env = EnvGen.kr(env, t_gate, doneAction:done);
sig = sig * env * amp;
sig = Pan2.ar(sig, pan.lag(tpan));
Out.ar(0, sig)
}).add;
SynthDef(\notas,
{arg freq=900, amp=0, pan=0, tpan=0, gate=0, done=2;
var sig, env;
sig = Saw.ar(freq);
env = Env.new([0,1,0.5,0], [0.1,0.1,1], -4, 2);
env = EnvGen.kr(env, gate, doneAction:done);
sig = sig * env * amp;
sig = Pan2.ar(sig, pan.lag(tpan));
Out.ar(0, sig)
}).add;
)
// ------------------- Esempio polifonico senza sostegno
(
~dur = rrand(0.1,4);
Synth(\notan, [\freq, rrand(80,92).midicps,
\amp, rand(127) / 127,
\dur, ~dur,
\pan, rand2(1.0),
\tpan, ~dur,
\done, 2,
\t_gate, 1])
)
// ------------------- Esempio monofonico con sostegno
a = Synth(\notas, [\done, 0]);
a.set(\freq,rrand(800,1235),\amp,rand(0.5),\pan,rand2(1.0),\tpan,3,\gate, 1)
a.set(\gate,0);
a.release(4); // Come gate:0 modificando il tempo di fadeout
a.free;
Seconda tipologia
(
SynthDef(\fades,
{arg freq=900, amp=0, atk=0.1, rel=1, pant=1, panmin= -1, panmax=1, gate=0, done=2;
var sig, env, range, offset, lfo;
sig = Saw.ar(freq);
env = Linen.kr(gate,atk,1,rel,doneAction:done);
range = (panmax - panmin) / 2;
offset = panmin + range;
lfo = SinOsc.kr(pant/2,-0.5pi,mul:range,add:offset);
sig = sig * env * amp;
sig = Pan2.ar(sig, lfo);
Out.ar(0, sig)
}).add;
)
a = Synth(\fades, [\freq,rrand(800,1235),\amp,rand(0.5),\atk,2,\rel,4,\done,2]);
a.set(\gate, 1);
a.set(\pant, 4);
a.set(\panmax, 0.1);
a.set(\panmin, -0.3);
a.set(\gate,0);
In Max dobbiamo ulteriormente formalizzare la struttura dei patch introducendo le astrazioni.
Le astrazioni sono simili ai subpatches.
Principale differenza:
Se vogliamo utilizzare un patch come astrazione in un altro patch dobbiamo salvarlo nella stessa cartella oppure includerne il path dal menù Options -> File Preferencies.
from IPython.display import HTML
HTML('<center><img src="media/file_preferencies.png" width="70%"></center>')
A questo punto possiamo adottare la seguente strategia di programmazione:
Nell'immagine seguente il patch salvato come myinstr che possiamo utilizzare come astrazione richiamandolo in altri patches.
from IPython.display import HTML
HTML('<center><img src="media/myinstr.png" width="75%"></center>')
Tre strumenti virtuali uguali (tre istanze) controllabili separatamente.
from IPython.display import HTML
HTML('<center><img src="media/astrai.png" width="100%"></center>')
La seconda tipologia in Max la affronteremo nel paragrafo seguente.
Musicalmente possiamo utilizzare i modelli strumentali appena esposti in sequenze monofoniche oppure polifoniche.
Per quanto riguarda la gestione della polifonia a livello informatico ci sono due possibilità.
Gli inviluppi e nello specifico la loro caratteristica di poter distruggere automaticamente le istanze di Synth al termine del loro percorso attraverso l'argomento doneAction: n sono le UGens più adatte a controllare la polifonia in SuperCollider, ottimizzando le prestazioni del computer.
Principalmente abbiamo a disposizione tre possibilità:
Quest'ultima da affrontare con diverse strategie di programmazione a seconda del tipo di inviluppo.
Per questa tipologia di inviluppi possiamo realizzare strumenti monofonici o polifonici con allocazione dinamica delle voci.
s.boot;
s.meter;
s.plotTree;
(
SynthDef(\mono, {arg freq=789, amp=0, dur=0.2, pan=0, t_gate=0, done=2;
var sig, env;
sig = SinOsc.ar(freq);
env = Env.perc(0.1*dur, 0.9*dur);
env = EnvGen.kr(env,t_gate,doneAction:done);
sig = sig * env * amp;
sig = Pan2.ar(sig, pan);
Out.ar(0, sig)
}).add
)
// ------------------- Monofonico
a = Synth(\mono); // Creo un'istanza
a.set(\freq,rrand(700,1200),\amp,0.5,\done,0,\t_gate,1); // done:0 invio i parametri
a.free; // La distruggo
// ------------------- Polifonico con allocazione dinamica
Synth(\mono, [\freq, rrand(700, 1200),\amp,0.1,\dur,2,\done,2,\t_gate,1]);
In SuperCollider tutto ciò che è racchiuso tra parentesi graffe è una funzione).
Una funzione è un particolare costrutto sintattico che permette di raggruppare al suo interno una sequenza di istruzioni o operazioni che a partire da determinati input restituiscono determinati output.
I valori in uscita (output) sono in funzione (dipendono) di quelli in entrata (input).
Le funzioni servono principalmente per evitare di dover riscrivere molte volte blocchi di codice che si ripetono esattamente come le variabili.
La differenza consiste nel fatto che le variabili contengono tipi di data mentre le funzioni sequenze di operazioni.
Possiamo definire gli inputs come argomenti.
Per eseguire una funzione dobbiamo invocare il metodo .value( ) (valuta).
Possiamo inviare gli argomenti alla funzione come argomenti di questo metodo.
(
a = {arg a, b; // Argomenti (input)
var somma, moltiplica; // Variabili locali
somma = a + b; // Corpo della funzione (operazioni sugli input)
moltiplica = a * b;
"somma:"+somma+ "moltiplica:"+moltiplica // Output (ultimo elemento)
}
)
a.value(12, 3);
Notiamo come gli algoritmi di sintesi e elaborazione del suono nelle SynthDef siano funzioni.
I parametri sono gli inputs il segnale l'output.
Per questa tipologia di inviluppi possiamo realizzare strumenti monofonici o polifonici con allocazione statica delle voci.
L'allocazione statica consiste nell'indicizzare i messaggi di note on (gate:1) inviati per poter poi inviare quelli di note off (gate:0) al Synth corretto.
Una strategia consiste nel mappare i valori delle altezze (midinote o frequenze) con gli indici di un'array per poi sostituirne dinamicamente gli elementi.
Se l'elemento all'indice che corrisponde all'altezza è un istanza di Synth triggeriamo il release e lo sostituiamo con 'nil'.
Se invece è 'nil' generiamo una nuova istanza di Synth e la sostituiamo a 'nil'.
Definiamo una funzione che si occupi di gestire le operazioni necessarie.
(
SynthDef(\monos, {arg freq=789, amp=0, pan=0, gate=0, done=2;
var sig, env;
sig = SinOsc.ar(freq);
env = Env.adsr(0.01,0.3,0.5,1);
env = EnvGen.kr(env, gate,doneAction:done);
sig = sig * env * amp;
sig = Pan2.ar(sig, pan);
Out.ar(0, sig)
}).add
)
// ------------------- Monofonico
a = Synth(\monos); // Creo un'istanza
a.set(\freq,rrand(700,1200),\amp,0.5,\done,0,\gate,1); // done:0 invio i parametri
a.set(\gate,0);
a.free; // La distruggo
// ------------------- Polifonico con allocazione statica
(
~note = Array.newClear(128); // Array vuoto di 128 elementi (tutte le note midi)
// Tutti gli elementi sono 'nil'
~freqs = Array.newClear(15000); // Oppure con le frequenze in Hertz...
~alloca = {arg pitch; // Funzione
var el;
el = ~note.at(pitch); // Richiama l'elemento corrispondente alla midi note
// assegnandolo alla variabile locale
if(el.notNil, // Se il contenuto della variabile non è 'nil' (sta suonando)
{el.set(\gate,0); // 1. Triggera il release di quell'istanza di Synth
~note.put(pitch, nil)}, // 2. la sostituisce con 'nil' all'indice corrispondente
// Se invece l'elemento è 'nil'
{el = Synth(\monos, // 1. Assegna alla variabile una nuova istanza di Synth
[\freq,pitch.midicps,
\amp, 0.1,
\done, 2,
\gate, 1]);
~note.put(pitch, el)} // 2. sostituisce 'nil' con la nuova istanza di Synth
)
};
)
~alloca.value(60); // Note on
~alloca.value(64);
~alloca.value(67);
~alloca.value(72);
~alloca.value(64); // Note off
~alloca.value(67);
~alloca.value(72);
~alloca.value(60);
Quasta strategia risulta molto utile quando utilizziamo controller esterni come una tastiera midi.
In Max la polifonia è gestita principalmente con l'oggetto poli~.
Questo oggetto accetta al suo interno un numero finito di astrazioni.
La principale differenza con le astrazioni normali consiste nel fatto che gli inlets e outlets devono essere definiti come object box specificando se controllo o audio. L'argomento specifica l'ordine.
Ha come caratteristica la possibilità di accendere o spegnere la computazione audio delle singole istanze risparmiando di fatto risorse della CPU inviando un messaggio all'oggetto thispoly~.
from IPython.display import HTML
HTML('<center><img src="media/poli_1.png" width="45%"></center>')
Il primo argomento è il nome dell'astrazione che vogliamo caricare nel poly~ mentre il secondo il numero di istanze.
from IPython.display import HTML
HTML('<center><img src="media/poli_2.png" width="85%"></center>')
Nell'astrazione precedente la computazione audio è accesa o spenta con un apposito messaggio (pow).
In alcuni casi possiamo automatizzare questa operazione mappandola sull'ampiezza.
Ritardiamo anche il muting della durata del fadeout per evitare clicks.
from IPython.display import HTML
HTML('<center><img src="media/poli_3.png" width="25%"></center>')
Se l'evento sonoro prevede un inviluppo d'ampiezza possiamo utilizzare il bang di line~ come trigger per il muting.
from IPython.display import HTML
HTML('<center><img src="media/poli_4.png" width="40%"></center>')
Con l'astrazione appena illustrata possiamo definire una sintassi per i messaggi che ci permette di utilizzarlo sia in modo monofonico che polifonico attraverso un'allocazione statica delle voci.
from IPython.display import HTML
HTML('<center><img src="media/poli_5.png" width="90%"></center>')
In Max possiamo mappare l'altezza per l'allocazione dinamica delle voci inviandola con il messaggio note seguito dal valore.
Modifichiamo leggermente l'astrazione.
from IPython.display import HTML
HTML('<center><img src="media/poli_6.png" width="40%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/poli_7.png" width="25%"></center>')
Per gli inviluppi con fase di sostegno possiamo utilizzare adsr~ oppure mappare i triggers sui valori di ampiezza come già visto per il muting.
from IPython.display import HTML
HTML('<center><img src="media/polimidi.png" width="40%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/polimidi_1.png" width="35%"></center>')