Questo Notebook vuole fornire le conoscenze di base del linguaggio di programmazione ChucK orientato alla on-the-fly programmation ed al live coding. ChucK è un software open source ed è basato su C++ e Java.
Scarica i materiali utilizzati in questo notebook.
Scarichiamo ed installiamo:
Possiamo programmare ed eseguire ChucK in due possibili modi:
Lanciamo miniAudicle.
L'interfaccia utente è divisa in tre finestre.
Code editor - Dove scriviamo i nostri programmi.
Simile all'Interprete di SuperCollider o alla patch window di Max.
from IPython.display import HTML
HTML('<center><img src="media/editor.png" width="50%"></center>')
Virtual Machine Manager - Il motore audio di ChucK.
Simile al Server di SuperCollider e agli oggetti audio di Max.
from IPython.display import HTML
HTML('<center><img src="media/virtual.png" width="40%"></center>')
Console monitor - Dove vengono riportate varie informazioni.
Simile alla post window di SuperCollider e alla Max window di Max.
Compare quando lanciamo la Virtual Machine.
from IPython.display import HTML
HTML('<center><img src="media/conso.png" width="50%"></center>')
Per realizzare qualsiasi cosa come ad esempio generare suono:
<<<"test">>>;
SinOsc osc => dac;
440 => osc.freq;
0.5 => osc.gain;
1::second => now;
Inviamo il programma alla Virtual Machine cliccando su + (Add Shred)
Nella Console Monitor leggiamo:
[chuck](VM): sporking incoming shred: 1 (Untitled)... "test" : (string)
In miniAudicle i singoli files di testo si chiamano shred e hanno i seguenti comandi "on the fly":
In miniAudicle possiamo realizzare delle semplici GUI chiamate MAUI.
<<<"gui">>>;
SinOsc sine => dac;
MAUI_Slider slider;
slider.range(200,2000);
slider.name("frequency");
slider.display();
while(1)
{
slider.value() => sine.freq;
slider => now;
}
Ulteriori info a questo link.
In Mac OS apriamo un Terminale.
In Windows apriamo un Prompt dei comandi (Command Prompt).
from IPython.display import HTML
HTML('<center><img src="media/terminale.png" width="60%"></center>')
In Mac OS il formato è
nome-utente@nome-computer ~ %
In Windows
C:\Users\nome-utente>
Il simbolo % (in windows >) si chiama prompt e ci dice che il computer è pronto ad accettare istruzioni.
Normalmente i comandi sono eseguiti in serie quando schiacciamo il tasto enter.
Uno dei comandi più utili è change directory (cd) attraverso il quale possiamo settare la directory di lavoro (current directory) specificandone il percorso assoluto (anche trascinando la cartella direttamente nel Terminale) come singolo argomento.
Se ci sono spazi nei nomi delle cartelle dovremo includere in percorso tra doppie virgolette.
Una volta eseguito il comando potremo effettuare operazioni su tutti i files contenuti nella cartella scrivendo solamente nomeFile.estensione oppure percorsoLocale/nomeFile.estensione.
cd /Users/andreavigani/Desktop/ChucK // path assoluto
cd // senza argomenti --> home come directory corrente
cd .. // fa salire di una directory
pwd // stampa il percorso assoluto della directory corrente
ls // stampa una lista dei files e cartelle contenute nella directory corrente
Ad esempio se vogliamo lanciare lo shred '00_test.ck' contenuto nella cartella shred da terminale dobbiamo eseguire le seguenti linee di comando.
cd ../shred // prima eseguiamo questa linea
chuck 00_test.ck // poi questa
Abbiamo udito una emozionante sinusoide per ben un secondo!
In generale quando vogliamo eseguire un file di un software che lo permetta (come python, lilypond, chuck, etc.) da linea di comando prima scriviamo il nome del software e poi il nome del file.
Possiamo anche eseguire più shreds contemporaneamente scrivendone il nome uno dopo l'altro sulla stessa linea separati da uno spazio.
chuck 00_test.ck 01_test.ck
Un altro comando utile per recuperare indirizzo IP privato.
// Mac OS
ipconfig getifaddr en0 // se Wi-Fi
ipconfig getifaddr en1 // se Cablato
// Windows
ipconfig
Alcune abbreviazioni da tastiera.
Se vogliamo lavorare da linea di comando possiamo ottimizzare il flusso di lavoro con VSCode dove il terminale è nella stessa IDE dei files indipendentemente dal sistema operativo impiegato.
SinOsc sig => dac;
440 => sig.freq;
0.5 => sig.gain;
1::second => now;
cd '../Prova'
from IPython.display import HTML
HTML('<center><img src="media/vs_1.png" width="90%"></center>')
chuck nome.ck
Per ottimizzare il lavoro in VSCode possiamo settare anche un'area di lavoro trascinando sull'icona la cartella che contiene gli shreds e tutti i files collegati al progetto che vengono visualizzati sulla sinistra nel riquadro espolora risorse
Possiamo aprirli per editarli cliccandoci sopra.
Attenzione che questa operazione non modifica automaticamente anche la current directory del Terminale.
from IPython.display import HTML
HTML('<center><img src="media/vs_2.png" width="90%"></center>')
Esiste una seconda modalità per eseguire shreds da linea di comando più performante in situazioni di live coding o se vogliamo effettuare modifiche al volo (on the fly).
Questa modalità consente di effettuare tutte le operazioni che eseguiamo miniAudicle come lanciare una Virtual Machine (VM), aggiungere, sostituire, rimuovere schreds, etc.
Apriamo un secondo Terminale (Terminale --> Terminale diviso oppure cmd+ù).
from IPython.display import HTML
HTML('<center><img src="media/vs_3.png" width="90%"></center>')
chuck --loop
--loop è una command-line option o flag che invece di dire a ChucK quale file eseguire gli dice come eseguire i files oppure gli fornisce informazioni su qualche altra operazione o configurazione.
cd '../Prova'
chuck + 00_test.ck
Notiamo il segno + tra chuck e il nome del file che differenzia questo metodo dal precedente. In questo caso infatti lo shred è aggiunto alla Virtual Machine locale che abbiamo acceso in precedenza più o meno come quando in miniAudicle clicchiamo su Add shred.
Se dopo il + scriviamo più shreds saranno aggiunti contemporaneamente.
Se vogliamo stoppare l'audio invece che utilizzare la combinazione ctrl+c possiamo scrivere nel Terminale di destra.
chuck --clear.vm
Tutti gli shreds sono rimossi e tutte le classi pubbliche eliminate dalla VM locale esattamente come quando clicchiamo Clear VM in miniAudicle.
from IPython.display import HTML
HTML('<center><img src="media/vs_5.png" width="90%"></center>')
Nella precedente immagine vediamo come lo Shred aggiunto alla VM sia stato indicizzato.
from IPython.display import HTML
HTML('<center><img src="media/vs_4.png" width="90%"></center>')
Se avessimo voluto rimuovere lo shred dalla VM senza distruggerla avremmo potuto scrivere nel terminale di destra.
chuck - 1
Dove 1 è l'indice dello shred che vogliamo rimuovere dalla VM.
Un elenco dei principali comandi che possiamo scrivere nel Terminale di destra.
chuck + 00_test.ck // Aggiunge lo shred alla VM
chuck - 1 // Rimuove lo Shred indicizzato con 1
chuck = 1 00_test.ck // Replace Shred indicizzato con 1
chuck -- // Rimuove l ultimo Shred aggiunto
chuck --status // Riporta info sul tempo di running e sugli Shreds attivi
chuck ^ // Abbreviazione sintattica per il precedente comando
chuck --time // Riporta il running time
chuck --kill // Distrugge la VM come ctrl+c nel Terminale di sinistra
Chuck di default utilizza gli audio dirver impostati nel sistema operativo ma possiamo configurarli a nostro piacere sia in miniAudicle che da linea di comando.
In miniAudicle andare in settings... e compare la seguente finestra dove possiamo configurare i parametri a nostro piacimento.
from IPython.display import HTML
HTML('<center><img src="media/audio_set.png" width="50%"></center>')
Da linea di comando dobbiamo specificare le command-line options (flag) desiderati prima del nome del file.
chuck --channels:1 00_test.ck
Nell'esempio precedente il file eseguito è stereofonico (di default in ChucK) ma siccome abbiamo configurato l'uscita audio con un solo canale uscirà solo il canale sinistro.
Utilizzando questa sintassi la configurazione audio è valida esclusivamente nel momento in cui viene eseguito lo shred che segue i flags specificati, non configura le proprietà anche per eventuali esecuzioni di shred successive.
Se vogliamo configurare le proprietà audio per una Virtual Machine locale come avviene in miniAudicle dobbiamo impiegere la sintassi On the fly e specificare i flags nel Terminale di sinistra prima di lanciare la VM con il comando --loop per poi utilizzare il Terminale di destra per aggiungere, sostituire, rimuovere, etc. gli shreds attraverso la consueta sintassi.
// Nel Termiinale di sinistra
chuck --channels:1 --loop
// Nel Terminale di destra
chuck + 00_test.ck
chuck - 1
chuck etc.
In questo caso la sintassi completa è la seguente:
chuck --[options|commands] [+-=^] [file1 [file2 [file3 [...]]]]
Se vogliamo una lista completa delle opzioni.
chuck -h
Di seguito quelle più utilizzate.
// [options]
chuck --version / -V // Riporta la versione
chuck --probe // Riporta un elenco indicizzato di devices connessi
// (Audio, MIDI, HID)
chuck --loop / -l // Accende la VM locale
chuck --srate:48000 // Setta la sample rate (Default Mac e Win: 44100, Linux: 48000)
chuck --bufsize:512 // Buffer Size
chuck --dac:1 // Configura il device a cui inviare segnali #n (--probe)
chuck --adc:1 // Configura il device da cui ricevere segnali #n (--probe)
chuck --channels:2 / -c2 // Configura il numero di canali (input e output - Default:2)
chuck --in:1 / -i1 // Configura il numero di canali in input (Default:2)
chuck --out:1 / -o2 // Configura il numero di canali in output (Default:2)
chuck --remote:127.0.0.1 / -@hostname
chuck --port:9001 / -p9001
Se vogliamo salvare l'audio generato in un file.
<<<"Rec">>>;
SinOsc osc => dac; // Invia al dac
dac => WvOut waveOut => blackhole; // Preleva dal dac
"audio_rec.wav" => waveOut.wavFilename; // Nome del file
1::second => now; // Durata della registrazione (sincrona)
Se eseguiamo con VScode il file audio è salvato nella stessa cartella del file .ck.
Se eseguiamo con miniAudicle dobbiamo cercarlo nel computer.
Fra le diverse possibilità che abbiamo per eseguire gli esempi di codice presente nelle celle di questo Notebook ritengo la più didatticamente utile sia copiarlo ed incollarlo in miniAudicle oppure in un file "template" in VScode riscrivendolo e salvandolo ogni volta con il nuovo contenuto prima di eseguirlo dal Terminale.
Probabilmente non è il modo più elegante per compiere questa operazione ma è quello che utilizzeremo sempre al di fuori di questo scritto e penso sia un modo per abituare chi sta imparando ad automatizzare alcune procedure e operazioni.
Se vogliamo scrivere commenti dobbiamo farli precedere da un doppio slash.
// Questo è un commento
"Ciao"
Possiamo stampare diversi tipi di data nella console includendoli nei simboli <<< >>>.</br> Tutte le linee di codice devono terminare con un punto e virgola.
<<< "Vattelapesca" >>>;
Best pratics: cominciare il nostro programma con una descrizione di ciò che andiamo a fare sotto forma di stringa.
<<< "Sine" >>>;
SinOsc sig => dac;
440 => sig.freq;
0.5 => sig.gain;
1::second => now;
Possiamo assegnare ogni tipo di data a variabili attraverso il ChucK operator.
23 => int nota;
La "direzione sintattica" è quella opposta ai linguaggi basati sul C dove l'operazione sarebbe scritta in questo modo:
int nota = 23;Questo perchè può risultare più intuitivo programmare e modificare "al volo" (on-the-fly programmation) un algoritmo di sintesi o elaborazione del suono dove i segnali audio sono connessi tra diversi operatori.
Violino => mic => harmonizer => panner => riverbero => out
Può facilitare pensare l'operazione di assegnazione coma all'invio di un dato ad un'etichetta.
ChucK come molti linguaggi di programmazione ha delle parole riservate (keywords) che definiscono tipi di data come float o SinOsc o dac. etc.
Non possiamo utilizzare queste parole come nomi di variabili (regola generale: se in un editor di ChucK una parola è colorata non può essere il nome di una variabile).
ChucK è un linguaggio strongly-typed (come Java e non come python) che ci obbliga a specificare il tipo di data di una variabile in modo che possa effettuare operazioni corrette per quel tipo di data.
"Andrea" => string nome;
<<< nome, "Vattelapesca" >>>;
Possiamo effettuare le diverse operazioni aritmetiche assegnando i risultati a variabili.
<<< "Matop" >>>;
220 => int myPitch; // Dichiara ed assegna
myPitch + myPitch - 110 => int anotherPitch; // Esegue e assegna
2 * myPitch => int higherPitch;
myPitch / 2 => int lowerPitch;
<<< myPitch, anotherPitch, higherPitch, lowerPitch >>>;
La computazione avviene dall'alto verso il basso e da sinistra verso destra.
Le variabili possono essere riassegnate.
<<< "Sched" >>>;
int myPitch; // Dichiarazione
220 => myPitch; // Assegnazione
<<< myPitch >>>;
2 * myPitch => myPitch; // Riassegnazione
// (da questo punto in poi myPitch
// diventa 440)
<<< myPitch >>>;
myPitch - 110 => myPitch; // Riassegnazione...
<<< myPitch >>>;
// ----------------- Abbreviazioni sintattiche...
2 *=> myPitch; // "moltiplica e riassegna"
<<< myPitch >>>;
110 -=> myPitch; // "sottrai e riassegna"
<<< myPitch >>>;
In Chuck ci sono due tipi di data che possiamo utilizzare per specificare eventi nel tempo:
Time - keyword now
Corrisponde a un preciso momento nel tempo (onset) a partire da quando abbiamo lanciato il patch.
Scriviamo il seguente codice in VSCode, salviamo il file e lanciamolo da terminale.
<<< "Start" >>>; // type Stringa
<<< now >>>; // type Time
Il comando now restituisce il tempo dal momento del lancio dello shred da terminale che in questo caso è 0.000000 e coincide con l'attivazione della Virtual Machine.
Se effettuiamo la stessa operazione in miniAudicle il risultato sarà diverso perchè il tempo 0.000000 corrisponde al momento in cui abbiamo attivato la Virtual Machine e non a quando abbiamo inviato lo shred.
Il running time della Virtual Machine è visualizzato nella forma: minuti.secondi.campioni
Duration - keyword dur
Corrisponde al tempo delta tra due eventi (come il '.wait' in una Routine di SuperCollider).
<<< "Inizio" >>>; // Stampa la stringa (type String)
<<< now >>>; // Stampa il tempo corrente (type Time)
1::second => dur beat; // Dichiara una variabile che dice di aspettare 1 secondo
// (type Duration)
beat => now; // Richiama la variabile ora (aspetta 1 secondo prima di andare oltre)
<<< now >>>; // Stampa il tempo corrente (type Time)
<<< "Fine" >>>; // Stampa la stringa (type String)
Notiamo come il tempo che è stato stampato dopo un secondo sia in samples (44.100).
Possiamo specificare la durate in diverse unità temporali (i doppi due punti dicono a chuck che si tratta di valori temporali):
1::samp => now;
1::ms => now;
1::second => now;
1::minute => now;
1::hour => now;
1::day => now;
1::week => now;
Attraverso l'impiego congiunto di questi due data types possiamo controllare sequenze di eventi nel tempo.
SinOsc sig => dac;
sig; // Non fa nulla fino a quando non chiediamo al tempo di avanzare...
1::second => now; // Il tempo avanza di una durata poi..
<<<"Fine">>>; // si ferma...
Tempi assoluti
<<< "Tempi assoluti" >>>;
SinOsc sig => dac;
"Souno" => string suono; // Eventi
"Pausa" => string pausa;
"Fine" => string fine;
<<<suono>>>;
1 => sig.gain; // Esegue il codice fino a qua poi...
1::second => now; // Avanza di 1 secondo poi...
<<<pausa>>>; // Si interrompe (ma avanza lo scheduling)
0 => sig.gain;
0.5::second => now; // Avanza di 0.5 secondi (silenzio) poi...
<<<suono>>>; // Si interrompe (ma avanza lo scheduling)
1 => sig.gain;
1::second => now; // Avanza di 1 secondo poi...
<<<fine>>>; // Si interrompe (ma avanza lo scheduling)
0 => sig.gain;
Tempi relativi
Possiamo definire un beat e poi calcolare tutti i valori in relazione a esso.
<<< "Tempi relativi" >>>;
SinOsc sig => dac;
60 => int bpm; // Definiamo un bpm
60.0/bpm => float sec; // Convertiamo in secondi
sec::second => dur beat; // Casting in dur (samples)
<<<sec, beat>>>; // Stampa i valori (secondi, samples)
"Souno" => string suono; // Eventi
"Pausa" => string pausa;
"Fine" => string fine;
<<<suono>>>;
1 => sig.gain; // Esegue il codice fino a qua poi...
beat => now; // Avanza di 1 beat poi...
<<<pausa>>>; // Si interrompe (ma avanza lo scheduling)
0 => sig.gain;
beat/2 => now; // Avanza di 1/2 beat (silenzio) poi...
<<<suono>>>; // Si interrompe (ma avanza lo scheduling)
1 => sig.gain;
beat*2 => now; // Avanza di 2 beats poi...
<<<fine>>>; // Si interrompe (ma avanza lo scheduling)
0 => sig.gain;
Definizione delle variabili ed eventuale assegnazione di parametri di default.
Definiamo un suono o un evento.
<<< "Primobrano" >>>; // Descrizione
SinOsc osc => dac; // Definiamo uno strumento
1::second => dur beat; // Definiamo una durata (che in questo caso assume il valore
// di 1 Beat)
200 => int myPitch; // Definiamo le variabili con valori di default
0.5 => float myGain; // Note On (gain 1)
<<< "Inizio" >>>; // Definiamo un evento
myPitch => osc.freq; // Inviamo dei parametri
myGain => osc.gain;
beat / 2 => now; // Quando inviamo una durata al tempo corrente now esegue
// i comandi che la precedono e dopo la durata specificata
// si interrompe
myPitch + 100 => osc.freq; // Prepariamo un altro suono (o modifichiamo i parametri)
beat / 4 => now; // Inviamo una durata al tempo corrente now (esegue il suono)
myPitch + 200 => osc.freq;
beat * 2 => now;
<<< "Fine" >>>;
Le funzioni sono un modo di riutilizzare blocchi di codice.
In Chuck la sintassi è molto simile a python.
Possiamo specificare uno o più argomenti (valori in ingresso).
Possiamo ottenere un output attraverso la parola chiave return.
// type nome argomenti
fun int moltiplica(int a, int b)
{
<<< "Moltiplica due numeri tra loro" >>>; // Stringa di documentazione
a * b => int out; // Corpo della funzione
return out; // Output della funzione
}
<<< moltiplica(3,31) >>>;
Definiamo una funzione nota() che ha come argomenti la frequenza, l'ampiezza e la durata come sudduvusione del beat.
<<< "Funzioni" >>>;
SinOsc osc => dac;
0.6 => osc.gain;
1::second => dur beat;
fun void nota(int freq, float amp, int div) // void = qualsiasi type in output
{
freq => osc.freq; // Inviamo i parametri
amp => osc.gain;
beat / div => now;
}
nota(440*1, 0.8, 4);
nota(440*2, 0.1, 4);
nota(440*3, 0.4, 8);
nota(440*4, 0.2, 3);
nota(440*5, 0.9, 3);
nota(440*6, 0.2, 3);
Scrivere uno shred che realizzi una sequenza melodica.
Aggiungere più copie dello shred alla VM facendo in modo che si sovrappongano in tempi diversi generando un unico flusso musicale.
Un esempio (da modificare impiegando funzioni ampliandolo).
<<< "Linea" >>>;
SinOsc osc => dac;
1500::ms => dur beat; // beat
300 => float fond; // frequenza fondamentale
0.2 => osc.gain; // Ampiezza
1.0 => float fMul; // moltiplicatore delle frequenze
1.0 => float dMul; // moltiplicatore delle durate
fond*(1*fMul) => osc.freq;
0.2 => osc.gain;
beat*dMul => now;
fond*(2*fMul) => osc.freq;
0.1 => osc.gain;
beat*dMul => now;
fond*(3*fMul) => osc.freq;
0.6 => osc.gain;
beat*dMul => now;
fond*(5*fMul) => osc.freq;
0.4 => osc.gain;
beat*dMul => now;
fond*(2*fMul) => osc.freq;
0.1 => osc.gain;
beat*dMul => now;
fond*(4*fMul) => osc.freq;
0.2 => osc.gain;
beat*dMul => now;
fond*(7*fMul) => osc.freq;
0.7 => osc.gain;
beat*dMul => now;
fond*(8*fMul) => osc.freq;
0.1 => osc.gain;
beat*dMul => now;
fond*(6*fMul) => osc.freq;
0.3 => osc.gain;
beat*dMul => now;
fond*(7*fMul) => osc.freq;
0.2 => osc.gain;
beat*dMul => now;
fond*(4*fMul) => osc.freq;
0.5 => osc.gain;
beat*dMul => now;
fond*(2*fMul) => osc.freq;
0.2 => osc.gain;
beat*dMul => now;
fond*(3*fMul) => osc.freq;
0.2 => osc.gain;
beat*dMul => now;
Nella gestione di improvvisazioni in tempo reale ci sono due strutture musicali fondamentali che in diverse modalità sono quasi sempre impiegate.
In ChucK i loop del codice (cicli) possono coincidere con i loop musicali (anelli).
Per programmarli possiamo utilizzare due diverse strutture di controllo:
Continua a ripetere le istruzioni incluse nel loop fino a quando la condizione è vera (true).
<<< "While" >>>;
1 => int counter; // inizializza il counter
while(counter <= 10) // Condizione
{
500::ms => now;
<<< counter >>>; // Stampa
1 +=> counter; // Aggiorna la variabile
}
L'inizializzazione della variabile utilizzata per il test è al di fuori del ciclo.
L'aggiornamento della variabile utilizzata per il test è all'interno del ciclo.
Nel caso di loop infiniti ricordiamoci di mettere una durata tra gli eventi.
<<< "While infinito" >>>;
SinOsc osc => dac;
0.5 => osc.gain;
while(true) // sempre vero...
{
440 => osc.freq;
100::ms => now;
660 => osc.freq;
100::ms => now;
880 => osc.freq;
100::ms => now;
}
Continua a ripetere le istruzioni incluse nel loop fino a quando la condizione è vera (true).
Il test è separato dal corpo della funzione da ripetere.
L'inizializzazione e l'aggiornamento della variabile utilizzata per il test sono al di fuori del ciclo.
<<< "For" >>>;
for(1 => int i; i <= 10; i++) // test (i++ = i + 1)
{ // codice da ripetere
<<< i >>>;
100::ms => now;
}
Realizzazioni musicali
<<< "Gliss" >>>;
SinOsc osc => dac;
0.5 => osc.gain;
for(1000 => int i; i > 0; i--)
{
i => osc.freq;
<<< i >>>;
2::ms => now;
}
<<< "Arpeggio" >>>;
SinOsc osc => dac;
0.5 => osc.gain;
for(100 => int i; i < 2001; 50 +=> i) // Incremento di 50
{
i => osc.freq;
<<< i >>>;
200::ms => now;
}
Scrivere uno shred che realizzi un pattern (loop) ritmico melodico a piacere che suoni all'infinito.
Aprire una nuova pagina (File -> New Tab) e Copiare lo shred.
Aggiungerli uno alla volta (anche non subito) alla VM (Add shred) per poi modificarne i parametri e sostituirli (Replace Shred) o rimuoverli dinamicamante (Remove Shred) nel tempo investigando le possibili combinazioni e caratteristiche musicali.
N.B. Per chi utilizza VSCode con gli On The Fly commands non è necessario realizzare più copie del file in quanto possiamo scegliere quale shred sostituire o rimuovere dall'indice della VM.
Un esempio.
<<< "Steve dream" >>>;
SinOsc osc => dac;
200::ms => dur dura; // tempo delta
2.5 => float fMul; // moltiplicatore delle frequenze
0.8 => float dMul; // moltiplicatore delle durate
0.1 => float amp; // Ampiezza
fun void nota(int freq, float amp)
{
freq*fMul => osc.freq;
amp => osc.gain;
dura*dMul => now;
}
while(true) // sempre vero...
{
nota(450, amp);
nota(650, amp);
nota(850, amp);
nota(750, amp);
nota(750, 0.0);
nota(750, 0.1);
nota(550, 0.1);
}
Un altro costrutto sintattico informatico che possiamo impiegare per organizzare sequenze musicali nel tempo è l'operatore if.
<<< "If" >>>;
SinOsc sig => dac;
1 => int caso; // Provare a cambiare
if(caso == 1) // Se caso = 1 Se caso != 1
{
sig; // suona per 1 secondo
1::second => now;
}
550.0 => sig.freq; // Poi suona questo Suona subito questo
1::second => now;
Nel costrutto di if possiamo aggiungere else.
<<< "Else" >>>;
SinOsc sig => dac;
1 => int caso; // Provare a cambiare
if(caso == 1) // Se caso = 1
{
sig; // suona questo
1::second => now;
}
else // altrimenti
{
550.0 => sig.freq; // suona questo
1::second => now;
}
E possiamo utilizzare anche condizioni multiple.
<<< "And OR" >>>;
SinOsc sig => dac;
2 => int caso; // Provare a cambiare
if(caso > 1 && caso < 5) // Se maggiore di 1 AND minore di 5 (tra 2 e 4)
{
sig; // suona questo
1::second => now;
}
if(caso > 10 || caso == 5) // Se maggiore di 10 OR uguale a 5
{
550.0 => sig.freq; // suona questo
1::second => now;
}
Scrivere uno shred che sfrutta le caratteristiche appena esposte per mappare sequenze musicali differenti su numeri richiamandole successivamente secondo un ordine a piacere.
Aprire una nuova pagina (File -> New Tab) e Copiare lo shred.
Aggiungerli uno alla volta (anche non subito) alla VM (Add shred) per poi modificarne i parametri e sostituirli (Replace Shred) o rimuoverli dinamicamante (Remove Shred) nel tempo investigando le possibili combinazioni e caratteristiche musicali.
N.B. Per chi utilizza VSCode con gli On The Fly commands non è necessario realizzare più copie del file in quanto possiamo scegliere quale shred sostituire o rimuovere dall'indice della VM.
Un esempio.
<<< "Mapping" >>>;
SinOsc sig => dac;
1 => int quale;
if(quale == 1)
{
while(true)
{
for(1 => int i; i <= 10; i++)
{
100 * i => sig.freq;
10 / i => sig.gain;
100::ms => now;
}
}
}
if(quale == 2)
{
while(true)
{
550.0 => sig.freq;
0.3 => sig.gain;
100::ms => now;
0 => sig.gain;
100::ms => now;
}
}
if(quale == 3)
{
while(true)
{
950.0 => sig.freq;
0.3 => sig.gain;
100::ms => now;
0 => sig.gain;
500::ms => now;
}
}
Come in molti software anche in ChucK ci sono librerie esterne che contengono metodi e funzioni di utilizzo ricorrente all'interno del codice.
Possiamo pensarle come utilità (tools) raccolte in toolboxes caratterizzati da funzionalità comuni.
Ad esempio la Standard library contiene funzioni che si occupano principalmente di conversioni.
Possiamo utilizzare due differenti sintassi (non cambia nulla).
<<< Std.mtof(60), 60 => Std.mtof >>>;
Altezze e Ampiezze
<<< "mtof:", Std.mtof(60.0)>>>;
<<< "ftom:",Std.ftom(440.0)>>>;
<<< "powtodb:",Std.powtodb(0.5)>>>; // 0.0 - 1.0
<<< "dbtopow:",Std.dbtopow(100)>>>; // 0.0 - 100.0
<<< "rmstodb:",Std.rmstodb(0.5)>>>;
<<< "mtdbtormsf:",Std.dbtorms(50.0)>>>;
Casting
<<< "abs:", Std.abs(-60)>>>; // int
<<< "fabs:", Std.fabs(-60.3)>>>; // float
<<< "atoi:", Std.atoi("7")>>>; // da ASCII in int
<<< "itoa:", Std.itoa(4)>>>; // da int a ASCII
<<< "atof:", Std.atof("74")>>>; // da ASCII in int
<<< "ftoa:", Std.ftoa(74.23, 3)>>>; // da int a ASCII secondo argomento = numero di decimali
Per la generazione di valori pseudocasuali possiamo utilizzare alcune funzioni della libreria Math.
<<< "random:", Math.random()>>>; // Tra 0 e range (int) poco usato
<<< "randomf:", Math.randomf()>>>; // Tra 0 e 1.0 (float)
<<< "random2:", Math.random2(60,72)>>>; // Tra min e max (int)
<<< "randomf2:", Math.random2f(0.5,1.0)>>>; // Tra min e max (float)
Possiamo anche specificare il seed.
Math.random(23);
for(1 => int i; i < 4; i++)
{
<<< "random2:", Math.random2(60,72)>>>;
}
Esempio musicale.
<<< "Random" >>>;
SinOsc sig => dac;
while(true)
{
sig.freq(Std.mtof(Math.random2(80,92)));
sig.gain(Math.randomf());
100::ms => now;
}
Possiamo definire sequenze deterministiche di eventi per poi richiamarli singolarmente nel tempo.
Gli Array sono collezioni di dati assegnati ad una sola variabile.
Dichiariamo un Array.
<<< "Array" >>>;
string myArr[2]; // type nome[numero_di_elementi]
"Ciao" => myArr[0]; // Scrive nel primo ID
"Miao" => myArr[1]; // Scrive nel secondo ID
<<< myArr[0], myArr[1]>>>; // Richiama gli items con gli ID
Esempio musicale.
<<< "Array musicali" >>>;
SinOsc osc => dac;
0.25 => osc.gain;
[440, 660, 880, 1320] @=> int freqs[]; // Assegna i valori all'Array freqs
for(0 => int i; i < freqs.cap(); i++) // .cap() = capacity (come len, size, etc.)
{
freqs[i] => osc.freq; // Richiama gli elementi dagli indici
200::ms => now;
}
Attraverso la procedura appena illustrata possiamo formalizzare sequenze di parametri musicali iterando nel tempo gli Array.
Utilizziamo la Standard library per convertire note MIDI in frequenze (Hz).
<<< "Iterazione" >>>;
SinOsc osc => dac;
0.25 => osc.gain;
[60,62,64,65,67,69,71,72] @=> int midinote[];
1 => int c;
while(c <= 4) // Nested loop
{
<<< c >>>;
for(0 => int i; i < midinote.cap(); i++)
{
Std.mtof(midinote[i]) => osc.freq; // Converte midi in Hz
200::ms => now;
}
1 +=> c;
}
Sequencing on the fly.
<<< "Sequencing" >>>;
SinOsc osc => dac; // Strumento
0.5 => osc.gain; // Ampiezza
[0,4,7] @=> int maggiore[]; // intervalli accordo maggiore (Array)
[0,3,7] @=> int minore[]; // intervalli accordo minore (Array)
48 => int offset; // Nota perno
int posizione; // Posizione sulla scala (offset dell'offset)
150::ms => dur ottavo; // Durata
0 => posizione;
for(0 => int i; i < 4; i++) // Ripete 4 volte...
{
for(0 => int j; j < 3; j++) // Questo loop...
{
Std.mtof(maggiore[j] + offset + posizione) => osc.freq;
ottavo => now;
}
}
Definiamo una semplice partitura copiando il loop e cambiando tipo di accordo e posizione.
<<< "Partitura" >>>;
SinOsc osc => dac; // Strumento
0.5 => osc.gain; // Ampiezza
[0,4,7] @=> int maggiore[]; // intervalli accordo maggiore (Array)
[0,3,7] @=> int minore[]; // intervalli accordo minore (Array)
48 => int offset; // Nota perno
int posizione; // Posizione sulla scala (offset dell'offset)
150::ms => dur ottavo; // Durata
while(true) // Loop infinito
{
// --------------------------------------------------
// Prima fa questo...
0 => posizione;
for(0 => int i; i < 4; i++) // Ripete 4 volte...
{
for(0 => int j; j < 3; j++) // Questo loop...
{
Std.mtof(maggiore[j] + offset + posizione) => osc.freq;
ottavo => now;
}
}
// --------------------------------------------------
// Poi questo...
-3 => posizione;
for(0 => int i; i < 4; i++) // Ripete 4 volte...
{
for(0 => int j; j < 3; j++) // Questo loop...
{
Std.mtof(minore[j] + offset + posizione) => osc.freq;
ottavo => now;
}
}
// --------------------------------------------------
// Poi questo...
5 => posizione;
for(0 => int i; i < 4; i++) // Ripete 4 volte...
{
for(0 => int j; j < 3; j++) // Questo loop...
{
Std.mtof(maggiore[j] + offset + posizione) => osc.freq;
ottavo => now;
}
}
// --------------------------------------------------
// Poi questo...
7 => posizione;
for(0 => int i; i < 4; i++) // Ripete 4 volte...
{
for(0 => int j; j < 3; j++) // Questo loop...
{
Std.mtof(maggiore[j] + offset + posizione) => osc.freq;
ottavo => now;
}
}
}
Passiamo da linguaggio musicale a linguaggio sonoro.
<<< "Sonoro" >>>;
SinOsc osc => dac; // Strumento
0.5 => osc.gain; // Ampiezza
[0,4,7] @=> int maggiore[]; // intervalli accordo maggiore (Array)
[0,3,7] @=> int minore[]; // intervalli accordo minore (Array)
48 => int offset; // Nota perno
int posizione; // Posizione sulla scala (offset dell'offset)
70::ms => dur ottavo; // Durata musicale
for(0 => int i; i < 8; i++) // Ripete n volte...
{
0 => posizione; // Resetta la posizione a ogni giro
for(0 => int j; j < 4; j++)
{
j => posizione; // Incrementa la posizione....
for(0 => int k; k < 3; k++)
{
Std.mtof(maggiore[k] + offset + posizione) => osc.freq;
ottavo => now;
}
}
}
<<< "Cicalino" >>>;
SinOsc osc => dac; // Strumento
0.5 => osc.gain; // Ampiezza
[0,4,7] @=> int maggiore[]; // intervalli accordo maggiore (Array)
[0,3,7] @=> int minore[]; // intervalli accordo minore (Array)
48 => int offset;
int posizione;
50::ms => dur ottavo; // Cambiato qua Prova a cambiare il tempo delta....
for(0 => int i; i < 3; i++)
{
0 => posizione;
for(48 => int j; j > 0; j--) // Cambiato qua
{
j => posizione;
for(0 => int k; k < 3; k++)
{
Std.mtof(maggiore[k] + offset + posizione) => osc.freq;
ottavo => now;
}
}
}
Realizziamo una sessione di live coding in solo o in gruppo, cercando di esplorare le potenzialità musicali delle tecniche di sequencing appena esposte.
Lavoriamo con altezza, dinamica e ritmo (riserve armoniche, profili melodici, pattern ritmici, etc.).
Partiamo da una sequenza molto semplice per poi modificarla 'on the fly', sovrapporla a sue versioni modificate, richiamarla, come nelle Sessioni 1 e 2.
Le Unit Generator sono oggetti che ci permettono di generare o manipoliare segnali audio in Chuck.
Possiamo pensarle come la versione digitale dei moduli di un synth analogico.
from IPython.display import HTML
HTML('<center><img src="media/modular.png" width="40%"></center>')
Come nei sistemi modulari analogici possiamo collegare tra loro attraverso cavi virtuali i segnali in uscita da un modulo (output) con gli ingressi di un altro modulo (input) nella modellazione di algoritmi di sintesi ed elaborazione del suono più o meno complessi.
SinOsc osc => dac;
Nel codice precedente il segnale audio in uscita da un oscillatore sinusoidale (SinOsc) è collegato all'entrata del dac (Digital to Audio Converter) che lo invia alla scheda audio del computer.
Il Chuck operator (=>) in questo caso assume la funzione di un cavo virtuale.
Possiamo leggere le caratteristiche che riguardano tutte le UGen a questo link.
Esistono tre tipi di UGens:
A questo link possiamo trovare la ducumentazione delle UGens e alcuni codici di esempio.
Per il momento occupiamoci solo delle prime due tipologie.
Questa tipologia di oggetti tipicamente comprende gli oscillatori che sono dedicati alla generazione di diverse forma d'onda e i players che sono un particolare tipo di oscillatori dedicato al playback e all'elaborazione di audio (sound files o live) memorizzato su di un buffer.
<<< "Oscillatori" >>>;
SinOsc sig => dac;
587 => sig.freq;
0.5 => sig.gain;
1::second => now;
A questa tipologia appartengono gli inviluppi, i panner, i filtri, i processori di segnale e tutto ciò che modifica in qualche modo uno o più segnali in ingresso producendo un nuovo segnale in uscita.
La UGen Envelope genera un inviluppo trapezoidale o triangolare.
I principali parametri di controllo sono:
Se lo impiegiamo come inviluppo d'ampiezza il livello di default è compreso tra 0 e 1 e accetta i comandi di .keyOn e .keyOff.
Possiamo impiegarlo sia con fase di sostegno (come nei tasti del pianoforte) che senza (come negli strumenti a percussione).
In entrambe i casi dobbiamo comunque specificare due triggers (keyOn e keyOff).
Tipicamente gli inviluppi con fase di sostegno sono impiegati in ambienti che prevedono un'interazione (controller midi, tastiera, mouse, etc.) mentre quelli senza fase di sostegno nelle tecniche di sequencing.
In ChucK nella programmazione degli inviluppi senza fase di sostegno per facilitare la prassi musicale è conveniente scrivere una funzione che li definisca e li controlli in relazione a una durata dell'evento sonoro espressa sia in tempo assoluto (secondi o millisecondi) che relativo (beat).
Come generatore utilizziamo PulseOsc che genera una forma d'onda rettangolare con duty cycle variabile.
<<< "Envelope" >>>;
PulseOsc osc => Envelope env => dac; // Prima di essere inviato al dac entra in Envelope
0.5 => osc.gain;
100.0 => osc.freq; // Frequenza iniziale
fun void envi(float atk, float rel, float durata) // Funzione
{
atk::second => env.duration; // Durata attacco
1 => env.keyOn; // Trigger attacco (note On)
durata::second - rel::second => now; // Sintesi attacco + sostegno
rel::second => env.duration; // Durata rilascio
1 => env.keyOff; // Trigger rilascio (note Off)
env.duration() => now; // Sintesi rilascio (recupera il tempo automaticamente)
}
while(osc.freq() < 1800)
{
envi(0.01, 0.2, 0.3); // Singola nota (funzione)
osc.freq() + 100.0 => osc.freq; // Sequenza di armonici
}
Nel caso precedente i tempi sono definiti in valori assoluti (secondi) ma possiamo ragionare anche con tempi proporzionali alla durata per quanto riguarda fadein e fadeout e tempi relativi a un beat per quanto riguarda la durata.
Notiamo come nel codice precedente abbiamo utilizzato valori float per poi effettuare il casting in dur all'interno della funzione mentre nel codice seguente la funzione dell'inviluppo accetta i valori direttamente in formato dur.
Come generatore utilizziamo SqrOsc che genera una forma d'onda quadra, notiamo inoltre l'utilizzo dell'operatore modulo per creare un ciclo (wraparound) all'interno di un range.
<<< "Tempi relativi" >>>;
SqrOsc osc => Envelope env => dac; // Prima di essere inviato al dac entra nell'adsr
0.1 => osc.gain; // Ampiezza
100.0 => osc.freq; // Frequenza iniziale
1::second => dur beat; // Durata assoluta del beat
fun void envi(dur atk, dur rel, dur durata) // Funzione
{
atk => env.duration;
1 => env.keyOn;
durata - rel => now;
rel => env.duration;
1 => env.keyOff;
rel => now; // Alternativa al codice precedente...
}
while(true) // Loop infinito..
{
beat/Math.random2(1,8) => dur sudd; // Suddivisione randomica del Beat tra 1 e 4
envi(0.1*sudd, 0.5*sudd, sudd);
(osc.freq() + 100.0) % 1000 => osc.freq; // Operatore modulo...
}
La Ugen ADSR genera un inviluppo d'ampiezza classico.
I principali parametri di controllo sono simili a quelli di Envelope ad eccezione del fatto che possiamo settare i quattro valori con un singolo messaggio.
Inoltre possiamo recuperare informazioni sullo stato dell'inviluppo nel tempo attraverso degli indici ( .state() ).
Come generatore utilizziamo TriOsc che genera una forma d'onda triangolare.
<<< "ADSR" >>>;
TriOsc osc => ADSR env => dac; // Prima di essere inviato al dac entra nell'adsr
0.5 => osc.gain;
fun void adsr(float a,float d,float s,float r,dur durata) // Funzione
{
// A D S R
(a*durata, d*durata, s, r*durata) => env.set; // Setta i parametri dell'ADSR
1 => env.keyOn; // NoteOn
durata - env.releaseTime() => now; // Sintesi di attacco decadimento e sostegno
1 => env.keyOff; // NoteOff
env.releaseTime() => now; // Sintesi del rilascio
}
[0,4,7] @=> int maggiore[];
[0,3,7] @=> int minore[];
92 => int offset;
0 => int posizione;
while(true)
{
for(0 => int i; i < 3; i++)
{
Std.mtof(minore[i] + offset + posizione) => osc.freq;
adsr(0.1,0.1,0.5,0.2,1::second);
}
-4 => posizione;
for(0 => int i; i < 3; i++)
{
Std.mtof(maggiore[i] + offset + posizione) => osc.freq;
adsr(0.7,0.3,0,0,0.25::second);
}
-2 => posizione;
for(0 => int i; i < 3; i++)
{
Std.mtof(maggiore[i] + offset + posizione) => osc.freq;
adsr(0.1,0.1,0.2,0.8,0.5::second);
}
-5 => posizione;
for(0 => int i; i < 3; i++)
{
Std.mtof(maggiore[i] + offset + posizione) => osc.freq;
adsr(0.5,0.1,0.9,0.3,1::second);
}
}
Come esercizio possiamo provare a modificare il precedente codice rendendo relativo il controllo del tempo.
Realizzare un sound design con le seguenti caratteristiche:
La UGen Delay ritarda nel tempo il segnale in ingresso di un tempo definito.
I principali parametri di controllo sono:
Come generatore utilizziamo SawOsc che genera una forma d'onda a dente di sega con duty cycle variabile.
<<< "DELAY" >>>;
SawOsc osc => ADSR env => dac; // Suono diretto
env => Delay del => dac; // Side chain...
0.5 => osc.gain;
783 => osc.freq;
(10::ms, 20::ms, 0, 1::ms) => env.set; // Inviluppo triangolare percussivo
fun void suono() // Funzione per inviluppo
{
1 => env.keyOn; // NoteOn
env.attackTime() + env.decayTime() => now; // Sintesi di attacco decadimento e sostegno
1 => env.keyOff; // NoteOff
env.releaseTime() => now; // Sintesi del rilascio
}
5::second => del.max; // Tempo di delay massimo (size del Buffer)
1::second => del.delay; // Tempo di delay (deve essere < a delay.max)
0.5 => del.gain; // Tutte le UGens hanno un gain out
// del => del; // Feedback...la quantita e data dal gain del delay
suono();
2::second => now;
Possiamo aggiungere un feedback.
La quantità di feedback dipende dal gain del Delay.
Possiamo ottenere un controllo separato di feedback e ampiezza generale del Delay aggiungendo la UGens Gain alla catena audio.
Il tempo impiegato per l'estinzione del suono dipende dall'ampiezza del segnale ritardato, dal tempo di ritardo e dalla quantità di feedback.
Come generatore utilizziamo Noise che genera un rumore bianco.
<<< "FEEDBACK" >>>;
Noise osc => ADSR env => dac; // Suono diretto
env => Delay del => Gain delG => dac; // Side chain...
1 => osc.gain;
(10::ms, 20::ms, 0, 1::ms) => env.set; // Inviluppo triangolare percussivo
fun void suono() // Funzione per inviluppo
{
1 => env.keyOn; // NoteOn
env.attackTime() + env.decayTime() => now; // Sintesi di attacco decadimento e sostegno
1 => env.keyOff; // NoteOff
env.releaseTime() => now; // Sintesi del rilascio
}
5::second => del.max;
0.3::second => del.delay;
0.7 => del.gain; // Quantità di feedback (tra 0 e 1)
del => del; // Feedback
1.0 => delG.gain; // Ampiezza globale del Delay
suono();
10::second => now;
Nella cella seguente un impiego 'metrico' di delay e feedback.
Come generatore utilizziamo Blit che genera un treno d'impulsi limitato nello spettro.
Possiamo definire il numero di armonici presenti (tutti a partire dalla fondamentale) ma non la loro frequenza, ampiezza e fase.
<<< "DEL + FBK" >>>;
Blit osc => ADSR env => dac;
env => Delay del => dac;
0.2 => osc.gain;
9 => osc.harmonics; // Numero di armonici nello spettro
0.5::second => dur beat;
beat*2 => del.max;
beat/4 => del.delay;
0.9 => del.gain;
del => del;
(1::ms, beat/8, 0, 1::ms) => env.set;
fun void suono()
{
1 => env.keyOn;
env.attackTime() + env.decayTime() => now;
1 => env.keyOff;
env.releaseTime() => now;
}
[0,4,7,12] @=> int maggiore[];
72 => int offset;
0 => int posizione;
for(0 => int i; i < 4; i++)
{
for(0 => int j; j < 4; j++)
{
Std.mtof(maggiore[j] + offset + posizione) => osc.freq;
suono();
beat => now;
}
}
10::second => now;
Reti di delay filtrati in cascata generano riverberi simulando i ritardi dovuti alle riflessioni del suono causate da ostacoli lungo il tragitto dell'onda sonora.
In Chuck abbiamo a disposizione tre diversi algoritmi che generano riverberi sotto forma di UGens.
Per tutti e tre possiamo controllare solamente il rapporto tra suono diretto e suono riverberato ( .mix ).
Come generatore utilizziamo BlitSaw che genera un'onda a dente di sega che possiamo limitare nello spettro.
<<< "RIVERBERO" >>>;
BlitSaw osc => ADSR env => NRev rev => dac; // Suono diretto riverberato...
env => Delay del => dac; // Delay non riverberato
0.25 => osc.gain;
9 => osc.harmonics; // Numero di armonici nello spettro
0.5::second => dur beat;
beat => del.max;
beat/3 => del.delay;
0.5 => del.gain;
del => del;
0.15 => rev.mix; // Dry / Wet
(1::ms, beat/8, 0, 1::ms) => env.set;
fun void suono()
{
1 => env.keyOn;
env.attackTime() + env.decayTime() => now;
1 => env.keyOff;
env.releaseTime() => now;
}
[0,3,7,12] @=> int minore[];
60 => int offset;
12 => int posizione;
for(0 => int i; i < 4; i++)
{
for(0 => int j; j < 4; j++)
{
Std.mtof(minore[j] + offset + posizione) => osc.freq;
suono();
beat => now;
}
}
5::second => now; // Per la sintesi della coda del riverbero
Chuck di default genera segnali stereofonici centrati.
Possiamo rendere monofonico il segnale e indirizzarlo ad un singolo bus.
Se stereo: dac.left e dac.right
Se multicanale: dac.chan(0), dac.chan(1), dac.chan(2) ... dac.chan(n)
Come generatore utilizziamo BlitSquare che genera un'onda quadra che possiamo limitare nello spettro.
<<< "PING PONG" >>>;
BlitSquare osc => ADSR env => dac; // Suono diretto al centro
env => Delay delay[2]; // Array di delay...
delay[0] => dac.left; // il primo esce da destra
delay[1] => dac.right; // il secondo esce da sinistra
0.25 => osc.gain;
9 => osc.harmonics; // Numero di armonici nello spettro
0.5::second => dur beat;
beat => delay[0].max => delay[1].max; // Setta entrambe i max del
0.5 => delay[0].gain => delay[1].gain; // Setta entrambe i gains
delay[0] => delay[0]; // Feedback
delay[1] => delay[1]; // Feedback
beat/4 => delay[0].delay; // Tempo delay 1
beat/2 => delay[1].delay; // Tempo delay 2
(1::ms, beat/16, 0, 1::ms) => env.set;
fun void suono()
{
1 => env.keyOn;
env.attackTime() + env.decayTime() => now;
1 => env.keyOff;
env.releaseTime() => now;
}
[0,5,9] @=> int accordo[];
60 => int offset;
0 => int posizione;
for(0 => int i; i < 4; i++)
{
for(0 => int j; j < accordo.cap(); j++)
{
Std.mtof(accordo[j] + offset + posizione) => osc.freq;
suono();
beat => now;
}
}
10::second => now; // Per la sintesi della coda dei feedback
<<< "PAN2" >>>;
Impulse i => Pan2 p => dac;
while(true)
{
1.0 => i.next; // Setta il valore del campione corrente
Math.random2f( -1, 1 ) => p.pan; // Panning randomico
100::ms => now; // Frequenza del treno di impulsi
}
Realizziamo una sessione di improvvisazione in solo o in gruppo oppure un sound design cercando di esplorare le potenzialità musicali legate alla sovrapposizione di reti di delay.
Impieghiamo riverberi differenti per differenziare diversi piani sonori.
Utilizziamo varie tecniche di panning per modificare la percezione della densità di eventi.
Se abbiamo a disposizione un sistema di diffusione multicanale esploriamo le UGen Pan4, Pan8 e Pan16
Per realizzare sessioni di live coding o di improvvisazione dobbiamo adottare una corretta strategia di programmazione (tecnica strumentale) formalizzando diverse parti di codice in più shreds.
Durante l'esecuzione possiamo effettuare le seguenti operazioni:
Queste operazioni possono essere effettuate in due ambienti informatici diversi:
Uno dei problemi legati alla strategia di live coding appare nel momento in cui volessimo sincronizzare musicalmente con precisione i lanci di diversi shreds o delle modifiche 'on the fly' dello stesso.
Una soluzione consiste nell'inserire la seguente linea di codice negli shred.
.5::second => dur beat; // Definiamo un beat (o tempo delta - T)
beat - (now % beat) => now; // Aggiorniamo now al beat successivo attraverso l'operatore modulo
In questo modo ogni qualvolta lanciamo o rimpiazziamo un nuovo shred la computazione partirà al beat successivo al lancio.
Ovviamente il tempo del beat di sincronizzazione deve essere identico in tutti gli shred (a meno di non volere poliritmie complesse che cominciano in syncro per poi proseguire indipendentemente).
Realizzare una sessione di live coding generando più voci sovrapposte dallo shred nella cella seguente.
<<<"Syncro">>>;
TriOsc osc => ADSR env => NRev rev => Pan2 pan => dac;
0.01 => rev.mix;
12 => int ottave;
60 => int offset;
1 => float sudd; // Suddivisione del beat
[0,3,7] @=> int accordo[];
0.5::second => dur beat; // Per sincronizzazione....
beat - (now % beat) => now;
(1::ms, beat/8, 0, 1::ms) => env.set;
fun void envi() {
beat / Math.random2(2,16) => env.decayTime; // Random decay dell'inviluppo
1 => env.keyOn;
beat / sudd => now;
1 => env.keyOff;
}
while(true)
{
Math.random2(0, accordo.cap()-1) => int nota; // Indice randomico
Math.random2(0,3) * ottave => int posizione; // Ottava randomica
Std.mtof(accordo[nota] + offset + posizione) => osc.freq;
Math.random2f(0.0,1.0) => float amp;
amp => osc.gain;
Math.random2f(-1.0,1.0) => float panVal;
panVal => pan.pan;
envi();
}
Per realizzare un playback o un qualche tipo di elaborazione di un soundfile dobbiamo:
Come best pratics per quanto riguarda la riproduzione e l'elaborazione di sound files conviene posizionare i sound files in una cartella nominata suoni all'interno della cartella del patch specificandone il path assoluto.
La keyword me.dir() riporta il path dello shred in cui è contenuto sotto forma di stringa e possiamo utilizzarlo come riferimento relativo.
<<< "Path" >>>;
me.dir() + "suoni/guitar.wav" => string filename;
<<< filename >>>;
<<<"Playback">>>;
me.dir() + "suoni/guitar.wav" => string nome; // Path assoluto
SndBuf chit => dac; // Crea uno strumento
nome => chit.read; // Carica il file in RAM
<<< chit.samples() >>>; // Numero di campioni del file
<<< chit.samples() / 44100.0 >>>; // Durata in secondi (per Env)
chit.samples() / 44100.0 => float dura; // Durata in secondi
dura::second => now; // Sintetizza per la durata
Applichiamo un inviluppo di ampiezza trapezoidale della durata corrispondente al sound file con fadein e fadeout di 10ms per evitare eventuali click all'inizio e alla fine.
<<<"Playback">>>;
me.dir() + "suoni/guitar.wav" => string nome; // Path assoluto
SndBuf chit => Envelope env => dac;
nome => chit.read;
fun void envi(float d, float f){
f::second => env.duration;
1 => env.keyOn;
(d - f)::second => now;
1 => env.keyOff;
f::second => now;
}
<<< chit.samples() >>>; // Numero di campioni del file
<<< chit.samples() / 44100.0 >>>; // Durata in secondi (per Env)
chit.samples() / 44100.0 => float dursec; // Durata in secondi
envi(dursec,0.01);
Quanto appena illustrato può essere sfruttato musicalmente realizzando il playback di frammenti del soundfile.
Dobbiamo stabilire:
<<<"Windowing">>>;
me.dir() + "suoni/guitar.wav" => string nome;
SndBuf chit => Envelope env => dac;
nome => chit.read;
chit.samples() => chit.pos; // Settando la posizione alla fine del
// buffer si stoppa il playback...
fun void frag(float pos, float dur, float fade){
(pos*44100) $ int => chit.pos; // Casting a int (samples)
fade::second => env.duration;
1 => env.keyOn;
(dur - fade)::second => now;
1 => env.keyOff;
fade::second => now;
}
while(true) {
0.01 => float fdt;
Math.random2f(0, chit.samples()/44100.0) - fdt => float posi;
<<<posi>>>;
frag(posi, 0.1, fdt);
}
Se vogliamo realizzare un loop di tutto il soundfile possiamo invocare .loop() sull'istanza di SndBuf.
In questo caso non possiamo applicare nessun inviluppo.
<<<"Looping">>>;
me.dir() + "suoni/guitar.wav" => string nome;
SndBuf chit => dac;
nome => chit.read;
chit.samples() => chit.pos;
chit.loop(1); // loop(1) = Start loop(0) = Stop
(chit.samples()*3)::samp => now; // Sintetizza tre ripetizioni
Se invece vogliamo mettere in loop porzioni di soundfile possiamo adottare la tecnica illustrata per il windowing.
<<<"Looping con pause">>>;
me.dir() + "suoni/guitar.wav" => string nome;
SndBuf chit => Envelope env => dac;
nome => chit.read;
10::ms => dur fdt; // Fade
1::second => dur beat; // Beat
0.7 => float pos; // Posizione
beat/3 * 2 => dur dura; // Durata
beat/3 => dur pausa; // Pausa
chit.samples() => chit.pos;
fun void loop(float p, dur du, dur fd){
(p * 44100) $ int => chit.pos; // Casting a int (samples)
fd => env.duration;
1 => env.keyOn;
du - fd => now;
1 => env.keyOff;
fd => now;
}
while(true) {
loop(pos, dura, fdt);
pausa => now;
}
Se vogliamo trasportare un sound file o una parte di esso dobbiamo modificare la rata di lettura dei campioni.
Senza alcuna trasposizione è 1.0, l'ottava sopra 2.0 e l'ottava sotto 0.5.
Possiamo calcolare la rata in termini più musicali partendo da un indicazione in semitoni.
rata = 2^(n/12)
Dove n corrisponde al numero di semitoni (sia positivo che negativo) che vogliamo trasporre.
Ricordiamo che quando modifichiamo la rata cambiano sia l'altezza che la durata e che quest'ultima è data dall'inviluppo e non dal player.
<<<"Trasposizione">>>;
me.dir() + "suoni/guitar.wav" => string nome;
SndBuf chit => Envelope env => dac;
nome => chit.read;
10::ms => dur fdt; // Fade
1::second => dur beat; // Beat
0.7 => float pos; // Posizione
beat/3 * 2 => dur dura; // Durata
beat/3 => dur pausa; // Pausa
0 => float trsp; // Trasposizione in semitoni
Math.pow(2, (trsp/12)) => trsp; // Formula semitoni -> rate
<<<trsp>>>;
chit.samples() => chit.pos;
fun void loop(float p, float tr, dur du, dur fd){
(p * 44100) $ int => chit.pos; // Casting a int (samples)
tr => chit.rate; // Trasposizione
fd => env.duration;
1 => env.keyOn;
du - fd => now;
1 => env.keyOff;
fd => now;
}
while(true) {
loop(pos, trsp, dura, fdt);
pausa => now;
}
Se vogliamo modificare la direzione di lettura basta indicare rate negative.
<<<"Direzione">>>;
me.dir() + "suoni/guitar.wav" => string nome;
SndBuf chit => Envelope env => dac;
nome => chit.read;
10::ms => dur fdt; // Fade
1::second => dur beat; // Beat
0.7 => float pos; // Posizione
beat/6 => dur dura; // Durata
beat*0 => dur pausa; // Pausa
0 => float trsp; // Trasposizione in semitoni
Math.pow(2, (trsp/12)) => trsp; // Formula semitoni -> rate
[-1.0, 1.0] @=> float dir[]; // 1 = recto -1 = verso
chit.samples() => chit.pos;
fun void loop(float p, float tr, dur du, dur fd){
(p * 44100) $ int => chit.pos; // Casting a int (samples)
Math.random2(0,1) => int id;
tr * dir[id] => chit.rate; // Trasposizione
fd => env.duration;
1 => env.keyOn;
du - fd => now;
1 => env.keyOff;
fd => now;
}
while(true) {
loop(pos, trsp, dura, fdt);
pausa => now;
}
Realizzare una breve improvvisazione sfruttandoi le caratteristiche musicali dei loop modificando dinamicamente posizione, direzione e rate di un sound file oltre ai parametri comuni a tutti i suoni come ampiezza, morfologia dell'inviluppo d'ampiezza e panning.
La modalità improvvisativa si deve basare sulle seguenti operazioni non consecutive:
Le caratteristiche musicali devono dipendere dalla morfologia sonora del file scelto.
Definiamo uno strumento basato su più soundfiles come una batteria o un set di percussioni.
Focalizziamo la nostra attenzione su come il codice sia caratterizzato dall'iterazione di Array attraverso l'utilizzo di cicli limitando il numero di linee.
<<<"Sampler">>>;
SndBuf kick => dac; // Strumenti (1 per soundfile)
SndBuf snare => dac;
SndBuf cHat => dac;
SndBuf oHat => dac;
SndBuf clap => dac;
[ kick, snare, cHat, oHat, clap ] @=> SndBuf perc[]; // Array di UGen (lo strumento globale)
["kick", "snare", "cHat", "oHat", "clap"] @=> string nomi[]; // Array nomi dei soundfiles
for(0 => int i; i < perc.size(); i++) // Carica i soundfiles negli strumenti
{
me.dir() + "suoni/drums/"+nomi[i]+".wav" => perc[i].read;
};
for(0 => int i; i < perc.size(); i++) // Inizializza i buffer
{
perc[i].samples() => perc[i].pos;
};
while(true)
{
Math.random2(0, perc.size()-1) => int id; // Indici randomici
0 => perc[id].pos;
0.1::second => now;
}
Possiamo utilizzare la stessa strategia di programmazione anche per strumenti singoli come il violino. In questo caso i singoli files possono corrispondere a tecniche strumentali diverse come pizzicato, col legno, tremolo, etc.
Nella cella seguente invece osserviamo un'altra modalità di organizzare il codice attraverso l'utilizzo delle funzioni.
Dobbiamo definirle in modo che siano aperte a più propositi diversi.
<<<"Tamarrata">>>;
SndBuf kick => dac; // Strumenti (1 per soundfile)
SndBuf snare => dac;
SndBuf cHat => dac;
SndBuf oHat => dac;
SndBuf clap => dac;
[ kick, snare, cHat, oHat, clap ] @=> SndBuf perc[]; // Array di UGen (lo strumento globale)
["kick", "snare", "cHat", "oHat", "clap"] @=> string nomi[]; // Array nomi dei soundfiles
for(0 => int i; i < perc.size(); i++) // Carica i soundfiles negli strumenti
{
me.dir() + "suoni/drums/"+nomi[i]+".wav" => perc[i].read;
};
fun void StopAll() // Funzione che inizializza i buffer
{
for(0 => int i; i < perc.size(); i++)
{
perc[i].samples() => perc[i].pos;
};
}
StopAll(); // Richiamamo la funzione
0.3::second => dur beat; // Definiamo un Beat
fun void drum(int instr, dur durata) // Funzione che definisce un evento
{
if(instr == 0)
{
0 => kick.pos;
0 => cHat.pos;
}
if(instr == 1) // Se sel è 1 suona oHat
{
0 => oHat.pos;
}
if(instr == 2) // Se sel è 2 suona snare
{
0 => snare.pos;
}
if(instr == 3) // Se sel è 2 suona snare
{
0 => clap.pos;
}
durata => now; // Durata
StopAll(); // Resetta...
}
while(true) // Pattern che si ripete
{
drum(0, beat/2); // instr dur
drum(0, beat/2);
drum(3, beat/2);
drum(2, beat/2);
for(0 => int i; i<4; i++)
{
drum(0, beat/4);
}
drum(2, beat/2);
drum(1, beat/2);
drum(0, beat/2);
drum(1, beat/2);
drum(2, beat/2);
}
Nei paragrafi precedenti abbiamo aggiunto singoli shreads alla virtual machine.
Ogni shred è un thread ovvero un singolo processo indipendente eseguito nella Virtual Machine.
Per ottenere una polifonia abbiamo aggiunto o rimosso due o più shreds sincronizzandoli o meno con un unico beat.
Se invece vogliamo realizzare sequenze polifoniche all'interno di un singolo shread dobbiamo:
Il processo di sporkling genera automaticamente un nuovo shred (child) per ogni funzione presente nello shred principale (parent).
Per verifirarlo utilizziamo come monitor visivo la virtual machine di miniAudicle.
Il tempo interno degli shred child è sincronizzato con lo shred parent.
Possiamo pensare le singole funzioni richiamate come a righi di una partitura o a traccie di una DAW).
<<<"Sporkling">>>;
0.5::second => dur beat; // Definiamo un Beat
// ------------------------ Drum
SndBuf kick => dac; // Algoritmo di sintesi
me.dir() + "suoni/drums/kick.wav" => kick.read; // Carica il file
kick.samples() => kick.pos; // Reset
fun void drum(dur durata, float amp)
{
amp => kick.gain;
0 => kick.pos;
durata => now;
kick.samples() => kick.pos; // Reset
}
fun void traccia_1() // Funzione che genera uno shred child (se in sporkling)
{
<<<"traccia 1">>>;
while(true)
{
drum(beat * 0.75, 0.7);
drum(beat * 0.25, 0.4);
}
}
// ------------------------ Arpeggio
SinOsc osc => ADSR env => NRev rev => dac; // Algoritmo di sintesi
0.15 => osc.gain;
(1::ms, beat/4, 0, 1::ms) => env.set;
0.1 => rev.mix;
60 => int offset;
0 => int pos;
[0,4,7,12] @=> int note[];
fun void arpeggio(int accordo[]){
for(0 => int i; i < accordo.size(); i++)
{
Std.mtof(accordo[i] + offset + pos) => osc.freq;
1 => env.keyOn;
beat/4 => now;
1 => env.keyOff;
}
}
fun void traccia_2() // Funzione che genera uno shred child (se in sporkling)
{
<<<"traccia 2">>>;
while(true)
{
arpeggio(note);
}
}
// ------------------------ Partitura
spork ~ traccia_1(); // Genera un thread child sincronizzato a quello parent (questo)
beat * 4 => now; // Sintetizza 4 beats (Tempo del thread parent)
spork ~ traccia_2(); // Genera un thread child sincronizzato a quello parent (questo)
beat * 6 => now; // Sintetizza per 6 beats (Tempo del thread parent)
Per i principali tipi di filtri utilizziamo le Basic Filter UGens.
Sono tutti filtri Butterworth del 2° ordine ad eccezione di ResonZ.
Tutti accettano come parametri di controllo.
<<<"Filtri">>>;
Noise osc => LPF filt => dac;
0.2 => osc.gain;
1000 => filt.freq;
1 => filt.Q;
1::second => now;
Lo sporking è utile anche quando vogliamo modificare dinamicamente nel tempo i valori di un parametro di controllo in modo continuo e dobbiamo realizzare una rampa attraverso un'iterazione che implica la definizione:
<<<"Wa Wa">>>;
SawOsc osc => LPF filt => dac;
110 => osc.freq;
0.2 => osc.gain;
0 => filt.freq;
1 => filt.Q;
for(0 => int i; i < 2000; i++) // Su - i = passo incrementale
{
i => filt.freq;
3::ms => now; // passo temporale
}
6 => filt.Q;
for(2000 => int i; i > 0; i--) // Giu
{
i => filt.freq;
3::ms => now;
}
In questo shred però non abbiamo nessun controllo sulla durata delle rampe.
Se vogliamo definire la durata dobbiamo definire una funzione che abbia come argomenti inizio, fine, durata e prevedere due casi:
Realizzaiamo la rampa sempre tra 0.0 e 1.0 per poi riscalarla tra inizio e fine.
Questo ci permette di trasformarle facilmente in rampe non lineari.
Stabiliamo 1000 passi di default (0.001 di passo incrementale) che danno una buona risoluzione delle approssimazioni.
<<<"Rampa">>>;
fun void rampa(float start, float end, float dura)
{
float val; // Variabile
Math.fabs(start-end) => float range; // Calcola range
dura / 1000 => float deltaT; // Calcola delta time
if(start < end) // Salita
{
for(0 => float i; i <= 1.001; i + 0.001 => i) // i = passo incrementale
{ // Tra 0 e 1
(i * range) + start => val; // Tra start e end
<<<val>>>;
deltaT::second => now;
}
}
else // Discesa
{
for(1 => float i; i >= -0.001; i - 0.001 => i) // Modificato...
{
(i * range) + end => val;
<<<val>>>;
deltaT::second => now;
}
}
}
rampa(987.346,1385.472,3);
Sostituiamo <<< val >>>; con il comando di invio dei parametri alla UGen.
<<<"Rampa">>>;
SawOsc osc => LPF filt => dac;
110 => osc.freq;
0.2 => osc.gain;
0 => filt.freq;
1 => filt.Q;
fun void rampa(float start, float end, float dura)
{
Math.fabs(start-end) => float range; // Calcola range
dura / 1000 => float deltaT; // Calcola delta time
if(start < end) // Salita
{
for(0 => float i; i <= 1.001; i + 0.001 => i)
{
(i * range) + start => float val;
val => filt.freq;
deltaT::second => now;
}
}
else // Discesa
{
for(1 => float i; i >= -0.001; i - 0.001 => i)
{
(i * range) + end => float val;
val => filt.freq;
deltaT::second => now;
}
}
};
rampa(250,3000,2);
6 => filt.Q;
rampa(3000,250,3);
Se vogliamo controllare le frequenze del filtro con tempi indipendenti rispetto al cambio delle frequenze dell'oscillatore applichiamo lo sporkling su una delle due funzioni.
<<<"Sporkling Wa">>>;
SawOsc osc => ADSR env => BPF filt => dac;
0.6::second => dur beat; // Beat
[36,44,48,52] @=> int note[]; // Altezze
(1::ms, beat * 4, 0, 1::ms) => env.set;
10000 => filt.freq;
8 => filt.Q;
0.5 => osc.gain;
fun void rampa(float start, float end, float dura)
{
Math.fabs(start-end) => float range; // Calcola range
dura / 1000 => float deltaT; // Calcola delta time
if(start < end) // Salita
{
for(0 => float i; i <= 1.001; i + 0.001 => i)
{
(i * range) + start => float val;
val => filt.freq;
deltaT::second => now;
}
}
else // Discesa
{
for(1 => float i; i >= -0.001; i - 0.001 => i)
{
(i * range) + end => float val;
val => filt.freq;
deltaT::second => now;
}
}
};
fun void sweepFilt() // Funzione da mettere in sporkling
{
while(true)
{
rampa(5000,250,1);
rampa(250,3000,2);
}
}
fun void nota(int sudd) // Funzione che cambia le altezze
{
note[Math.random2(0,note.size()-1)] => int rNote;
Std.mtof(rNote) => osc.freq;
1 => env.keyOn;
beat / sudd => now;
1 => env.keyOff;
}
spork ~ sweepFilt(); // Sporking (funzione per frequenza filtro)
while(true) // Funzione per controllo frequenze oscillatore
{
nota(3);
}
Durante un'improvvisazione può diventare difficile controllare l'ampiezza di tutti i processi audio in corso ed evitare fenomeni di distorsione armonica.
Principalmente abbiamo a disposizione tre possibilità.
La UGen Dyno è un processore di dinamica che può assolvere a diversi compiti.
Nel seguente shred la somma di due segnali porta l'ampiezza di picco a 1.5 generando clipping.
<<<"Clippa">>>;
SinOsc osc1 => dac;
1.0 => osc1.gain;
220 => osc1.freq;
2::second => now; // 1 Oscillatore
SinOsc osc2 => dac;
0.5 => osc2.gain; // 2 Oscillatori, ampiezza totale 1.5...
110 => osc2.freq;
2::second => now;
from IPython.display import HTML
HTML('<center><img src="media/clip_1.png" width="100%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/clip_2.png" width="100%"></center>')
Se però inseriamo la UGen Dyno nella catena audio.
<<<"Non clippa">>>;
SinOsc osc1 => Dyno dyno => dac; // Aggiunto Dyno UGen
1.0 => osc1.gain;
220 => osc1.freq;
2::second => now;
SinOsc osc2 => dyno; // Aggiunto Dyno UGen (non è necessario inviarlo al dac)
0.5 => osc2.gain;
110 => osc2.freq;
2::second => now;
from IPython.display import HTML
HTML('<center><img src="media/clip_3.png" width="100%"></center>')
Possiamo notare che:
Questo perchè abbiamo utilizzato la UGen Dyno come limiter, e quando l'ampiezza supera una certa soglia (default: 0.5) viene riscalata nel corso di un tempo di attacco (default 5 ms).
Proviamo a modificare alcuni parametri.
<<<"Parametri">>>;
SinOsc osc1 => Dyno dyno => dac;
0::ms => dyno.attackTime; // Tempo di attacco
0.8 => dyno.thresh; // Soglia di ampiezza
1.0 => osc1.gain;
220 => osc1.freq;
2::second => now;
SinOsc osc2 => dyno;
.5 => osc2.gain;
110 => osc2.freq;
2::second => now;
from IPython.display import HTML
HTML('<center><img src="media/clip_4.png" width="100%"></center>')
La UGen Dyno non è solo un limiter ma può essere impiegata come i più comuni processori di dinamica.
Nel reference sono illustrati i valori dei parametri da settare per ottenere un particolare tipo di processore.
from IPython.display import HTML
HTML('<div style = "float:left"><img src="media/dyno_1.png" width="100%"></div>'
'<div style = "float:right"><img src="media/dyno_2.png" width="100%"></div>')
Tutte le UGens che accettano suono in ingresso (modificatori) hanno una proprietà chiamata .op che modifica il modo in cui questo viene processato:
<<<"UGen op">>>;
// Compressore Amplifica Limiter
SndBuf2 player => Dyno dyno => Gain gain => Dyno limiter => dac;
me.dir() + "suoni/break1.wav" => player.read;
30 => dyno.ratio; // Ratio di compressione
0::ms => dyno.attackTime; // Tempo di attacco compressore
0.05 => dyno.thresh; // Soglia bassissima
24 => gain.gain; // Amplificazione enorme (guadagno)
0.9 => limiter.thresh; // Limiter
0::ms => limiter.attackTime;
while(true) // Loop infinito
{
-1 => dyno.op; // Passa l'originale
-1 => gain.op;
0 => player.pos;
2::second => now;
0 => player.pos;
2::second => now;
1 => dyno.op; // Processa
1 => gain.op;
0 => player.pos;
2::second => now;
0 => player.pos;
2::second => now;
}
from IPython.display import HTML
HTML('<center><img src="media/pass.png" width="35%"></center>')
Quasi tutte le UGens di ChucK hanno un solo canale (mono).
<<<"Channel">>>;
// mono mono mono stereo stereo
SinOsc osc => ADSR env => NRev rev => Pan2 pan => dac;
<<< "Sine ch: ", osc.channels()>>>; // Riporta il numero di canali
<<< "ADSR ch: ", env.channels()>>>;
<<< "Rev ch: ", rev.channels()>>>;
<<< "Pan ch: ", pan.channels()>>>;
<<< "DAC ch: ", dac.channels()>>>;
Come processare segnali stereofonici attraverso UGens monofoniche?
Creando Array di UGens.
<<<"Processori stereo">>>;
// mono mono stereo stereo stereo stereo
SinOsc osc => ADSR env => Pan2 pan => NRev rev[2] => Dyno limiter[2] => dac;
1 => limiter[0].op => limiter[1].op; // Manda a entrambe le copie del limiter
5 => limiter[0].ratio => limiter[1].ratio;
0.2 => limiter[0].thresh => limiter[1].thresh;
0::ms => limiter[0].attackTime => limiter[1].attackTime;
3 => limiter[0].gain => limiter[1].gain;
1 => rev[0].op => rev[1].op;
0.05 => rev[0].mix => rev[1].mix;
0.5::second => dur beat;
(1::ms, beat/4, 0, 1::ms) => env.set;
0.25 => osc.gain;
[0,4,7] @=> int maggiore[];
[0,3,7] @=> int minore[];
48 => int offset;
0 => int posizione;
for(0 => int i; i < 4; i++)
{
for(-1.0 => float j; j < 1.0; 0.1 +=> j)
{ beat / Math.random2(2,16) => env.decayTime; // Random decay dell'inviluppo...
Math.random2(0,3) * 12 => posizione; // Ottava randomica
Math.random2f(-1.0,1.0) => pan.pan;
Math.random2f(0.0,1.0) => osc.gain;
Math.random2(0, minore.size()-1) => int nota; // Indice randomico delle altezze
Std.mtof(minore[nota] + offset + posizione) => osc.freq;
1 => env.keyOn;
beat / 2 => now;
1 => env.keyOff;
env.releaseTime() => now;
}
}
Con la UGen LiSa e i suoi derivati multicanale possiamo registrare un segnale in ingresso e realizzare diversi tipi di elaborazione.
<<<"Live Sampling">>>;
SinOsc osc => ADSR env => NRev rev => dac;
rev => LiSa lisa => dac;
880 => osc.freq;
0.25 => osc.gain;
(1::ms, 5::ms, 0, 1::ms) => env.set; // Impulso di 6 ms
0.2 => rev.mix;
0.5::second => dur beat;
4::beat => lisa.duration; // Durata del buffer di LiSa
// ------------------- Recording
1 => lisa.record; // Start rec
1 => env.keyOn; // Trigger env
lisa.duration() - env.releaseTime() => now; // Durata - release
1 => env.keyOff;
env.releaseTime() => now;
0 => lisa.record; // Stop rec
// ------------------- Playback
[0.25,0.5,1.0,1.5,2.0] @=> float rates[]; // Array di velocità di riproduzione
0.5 => lisa.rate; // Velocità di riproduzione del buffer
1 => lisa.play; // Start play
4::beat => now; // Per 4 beats poi
while(true) // loop
{
0::beat => lisa.playPos; // Posizione di lettura
rates[Math.random2(0,4)] => lisa.rate; // Sceglie indice randomico
0.25::beat => now; // 1/4 del beat
}
Di default riproduce il buffer in loop
<<<"Recto verso">>>;
SinOsc osc => ADSR env => NRev rev => dac;
rev => LiSa lisa => dac;
880 => osc.freq;
0.25 => osc.gain;
(1::ms, 5::ms, 0, 1::ms) => env.set; // Impulso di 6 ms
0.2 => rev.mix;
0.5::second => dur beat;
4::beat => lisa.duration; // Durata buffer di LiSa
// ------------------- Recording
1 => lisa.record; // Start rec
1 => env.keyOn; // Trigger env
lisa.duration() - env.releaseTime() => now; // Durata - release
1 => env.keyOff;
env.releaseTime() => now;
0 => lisa.record; // Stop rec
// ------------------- Playback
1.0 => lisa.rate; // Velocità di riproduzione (float)
//-2.0 => lisa.rate; // Valori negativi legge al contrario (float)
1 => lisa.play;
12::beat => now; // per 12 beats
Possiamo definire loop interni e specificare un tempo di fade in e fade out ai lati del loop per evitare clicks dovuti ad eventuali discontinuità del segnale.
SinOsc osc => ADSR env => NRev rev => dac;
rev => LiSa lisa => dac;
880 => osc.freq;
0.25 => osc.gain;
(1::ms, 50::ms, 0, 1::ms) => env.set; // Impulso di 6 ms
0.2 => rev.mix;
0.5::second => dur beat;
4::beat => lisa.duration; // Durata buffer di LiSa
[0,4,7,11,12,16,19,23] @=> int notes[]; // Array di intervalli
60 => int offset; // Nota perno
while(true)
{
1 => lisa.record; // Start rec
for(0 => int i; i < notes.size(); i++) // Legge l'Array
{
Std.mtof(notes[i] + offset) => osc.freq;
1 => env.keyOn;
0.5::beat - env.releaseTime()=> now;
1 => env.keyOff;
env.releaseTime() => now;
}
0 => lisa.record; // Stop rec
1 => lisa.rate; // Setta alcuni parametri
1 => lisa.loop;
1 => lisa.bi; // Playback bidirezionale (no click)
1::ms => lisa.rampUp; // Fade In prima del taglio...sostituisce lisa.play
for(0 => float i; i < 2; i + 1 => i) // Play Loop interno...
{
3::beat => lisa.loopStart; // Comincia dal terzo beat
4::beat => lisa.loopEnd; // Finisci al quarto beat
4::beat => now; // Esegui per 4 beat
}
1::ms => lisa.rampDown; // Fade Out alla fine del taglio
1::ms => now;
// -------------------------- Ripete con offset diverso
1 => lisa.record; // Start rec
for(0 => int i; i < notes.cap(); i++) // Legge l'array
{
Std.mtof(notes[i] + offset + 5) => osc.freq;
1 => env.keyOn;
0.5::beat => now;
}
0 => lisa.record; // Stop rec
1::ms => lisa.rampUp; // Fade In prima del taglio...sostituisce lisa.play
for(0 => float i; i < 2; i + 1 => i) // Play Loop interno...
{
3::beat => lisa.loopStart; // Comincia dal terzo beat
4::beat => lisa.loopEnd; // Finisci al quarto beat
4::beat => now; // Esegui per 4 beat
}
1::ms => lisa.rampDown; // Fade Out alla fine del taglio...
1::ms => now;
}
LiSa è uno strumento piuttosto complesso con il quale possiamo realizzare molte tecniche di elaborazione del suono tra cui la sintesi granulare.
Per approfondire nel suo reference sono presenti diversi esempi da analizzare.
Negli esempi precedenti come segnale in ingresso abbiamo utilizzato un oscillatore ma possiamo anche accedere ai segnali in ingresso provenienti da micofoni con la UGen adc.
Dobbiamo specificare il device in ingresso e il numero di canali supportato tra quelli nell'elenco di --probe.
chuck --in:1 --out:2 --dac:3 --adc:4 prova.ck
<<<"adc">>>;
adc => NRev rev => Dyno dino => dac;
0.2 => rev.mix;
10::second => now;
Possiamo registrare in LiSa i segnali in ingresso dall'adc
chuck --in:1 --out:2 --dac:3 --adc:4 prova.ck
<<<"Sampler Live">>>;
adc => NRev rev => Dyno dinout => dac;
adc => Dyno dinin => LiSa lisa => rev; // Dal microfono...
0.1 => rev.mix;
0.5::second => dur beat;
4::beat => lisa.duration; // Durata del buffer di LiSa
while(true)
{
1 => lisa.record; // Start record
4::beat => now; // Per l'intero buffer
0 => lisa.record; // Interrompe la registrazione
Math.random2f(0.6,1.4) => lisa.rate; // Setta alcuni parametri
1 => lisa.loop;
1 => lisa.bi;
10::ms => lisa.rampUp; // Fade In prima del taglio
10::ms => now;
for(0 => float i; i < 2; i + 1 => i) // Play Loop interno...
{
3::beat => lisa.loopStart; // Comincia dal terzo beat
4::beat => lisa.loopEnd; // Finisci al quarto beat
4::beat => now; // Esegui per 8 beat
}
10::ms => lisa.rampDown; // Fade Out alla fine del taglio
10::ms => now;
}
Realizzare un'improvvisazione articolata utilizzando le conoscenze acquisite fino a questo punto.
I materiali sonori devono includere suoni di sintesi, suoni registrati su soundfiles e suoni che provengono da ingressi microfonici.
In questa prima parte del Notebook abbiamo affrontato le conoscenze informatiche di base necessarie alla realizzazione di un qualsiasi tipo di sound design sia in tempo reale che in tempo differito.
Abbiamo anche esposto molto brevemente alcune tecniche di sintesi del suono e di elaborazione di sound files.
Nelle celle seguenti alcuni esempi di realizzazioni sonore complesse variate con quanto affrontato finora.
<<< "Sintesi" >>>;
// ------------------------------------------- Synth
SqrOsc osc1 => ADSR env1 => BPF filt => dac; // Primo oscillatore
SawOsc osc2 => env1 => filt => dac; // Secondo oscillatore
filt => Delay d1 => dac.right; // Side chain Delay
filt => Delay d2 => dac.left; // Pan pot
0.6::second => dur beat; // Beat
// ------------------------------------------- Set Delay
2::second => d1.max => d2.max; // Max del
beat/2 => d1.delay; // Tempo dly 1
beat => d2.delay; // Tempo dly 2
0.75 => d1.gain => d2.gain; // Ampiezza dei dly
d1 => d2; // Feedback incrociato
d2 => d1;
// ------------------------------------------- Array di intervalli
[0,7,12,16,24,5,28,31] @=> int note[];
36 => int offset;
// ------------------------------------------- Set default inviluppo, Q e gain
(1::ms, beat * 4, 0, 1::ms) => env1.set;
10000 => filt.freq;
8 => filt.Q;
0.025 => osc1.gain => osc2.gain;
// ------------------------------------------- Funzione sweep filtro
fun void SweepFilt()
{
while(true) // Loop infinito
{
for(3000 => int i; i > 250; i--) // Giu
{
i => filt.freq;
0.5::ms => now;
}
for(250 => int j; j < 3000; j++) // Su
{
j => filt.freq;
0.5::ms => now;
}
}
}
// ------------------------------------------- Partitura
spork ~ SweepFilt(); // Sporking (funzione)
while(true) // Loop infinito
{
note[Math.random2(0,note.cap()-1)] => int randNote; // richiama con indice random elemento Array Note
Std.mtof(randNote + offset) => osc1.freq; // invia a oscillatori
Std.mtof(randNote + offset + 7) => osc2.freq;
1 => env1.keyOn; // Trigger
beat / 3 => now;
}
<<< "Slicing" >>>;
// ------------------------------------------- Synth
SndBuf player => Envelope env => Pan2 pan => dac;
1.4 => float MAIN_RATE;
second / (2 * MAIN_RATE) => dur beat; // Durata del beat in secondi riscalato sulla rata (1/2=0.5)
0.6 => player.gain;
me.dir() + "suoni/break1.wav" => player.read;
MAIN_RATE => player.rate;
0.02::second => env.duration;
// ------------------------------------------- Funzione tagli
function void taglio(int cue, dur dura)
{
player.samples() / 32 => int slice; // Taglia in 32 parti il soundfile e ne calcola la lungheazza di 1
slice * cue => int onset; // calcola il punto d'inizio sync con i tagli
player.pos(onset);
1 => env.keyOn;
dura - env.duration() => now;
1 => env.keyOff;
env.duration() => now;
}
// ------------------------------------------- Partitura
while(true)
{
taglio(8, 2 * beat);// Possiamo ricombinare a piacere i ritmi in patterns diversi
taglio(24,2 * beat);
taglio(2, 1 * beat);
taglio(2, 1 * beat);
taglio(8, 1.5 * beat);
taglio(8, 0.25 * beat);
taglio(8, 0.25 * beat);
}
<<< "Unchuck" >>>;
// ------------------------------------------- Synth
SndBuf player => Envelope env => Pan2 pan => dac;
env => NRev rev => dac; // Side Chain
rev =< dac; // ChucK operator al contrario...unchuck operator...scollega
1.4 => float MAIN_RATE;
second / (2 * MAIN_RATE) => dur beat; // Durata del beat in secondi riscalato sulla rata (1/2=0.5)
0.6 => player.gain;
me.dir() + "suoni/break1.wav" => player.read;
MAIN_RATE => player.rate;
0.02::second => env.duration;
// ------------------------------------------- Funzione tagli
function void taglio(int cue, dur dura)
{
player.samples() / 32 => int slice; // Taglia in 32 parti il soundfile e ne calcola la lungheazza di 1
slice * cue => int onset; // calcola il punto d'inizio sync con i tagli
player.pos(onset);
1 => env.keyOn;
dura - env.duration() => now;
1 => env.keyOff;
env.duration() => now;
}
// ------------------------------------------- Partitura
while(true)
{
taglio(8, 2 * beat);
taglio(24, 2 * beat);
taglio(2, 1 * beat);
taglio(2, 1 * beat);
taglio(8, 1.5 * beat);
taglio(8, 0.25 * beat);
taglio(8, 0.25 * beat);
taglio(8, 2 * beat);
taglio(24, 2 * beat);
rev => dac.right; // Collega il riverbero dal canale destro
taglio(2, 1 * beat);
rev =< dac.right; // Scollega il riverbero dal canale destro
rev => dac.left; // Collega il riverbero dal canale sinistro
taglio(2, 1 * beat);
rev =< dac.left; // Scollega il riverbero dal canale sinistro
taglio(8, 1.5 * beat);
taglio(8, 0.25 * beat);
taglio(8, 0.25 * beat);
}
<<< "Rate" >>>;
// ------------------------------------------- Synth
SndBuf player => Envelope env => Pan2 pan => dac;
1.4 => float MAIN_RATE;
second / (2 * MAIN_RATE) => dur beat; // Durata del beat in secondi riscalato sulla rata (1/2=0.5)
0.6 => player.gain;
me.dir() + "suoni/break1.wav" => player.read;
MAIN_RATE => player.rate;
0.02::second => env.duration;
// ------------------------------------------- Funzione tagli
function void taglio(int cue, dur dura)
{
player.samples() / 32 => int slice; // Taglia in 32 parti il soundfile e ne calcola la lungheazza di 1
slice * cue => int onset; // calcola il punto d'inizio sync con i tagli
player.pos(onset);
1 => env.keyOn;
dura - env.duration() => now;
1 => env.keyOff;
env.duration() => now;
}
while(true)
{
taglio(8, 2 * beat);
taglio(24, 2 * beat);
taglio(2, 1 * beat);
taglio(2, 1 * beat);
taglio(8, 1.5 * beat);
taglio(8, 0.25 * beat);
taglio(8, 0.25 * beat);
taglio(8, 2 * beat);
MAIN_RATE / 2 => player.rate; // Cambia rata
taglio(24, 2 * beat);
-0.5 => pan.pan; // Cambia pan
taglio(2, 1 * beat);
0.5 => pan.pan; // Cambia pan
MAIN_RATE => player.rate; // Cambia rata
taglio(2, 1 * beat);
0 => pan.pan; // Cambia pan
taglio(8, 1.5 * beat);
taglio(8, 0.25 * beat);
taglio(8, 0.25 * beat);
}
<<< "Ribattuti" >>>;
// ------------------------------------------- Synth
SndBuf player => Envelope env => Pan2 pan => dac;
1.4 => float MAIN_RATE;
second / (2 * MAIN_RATE) => dur beat; // Durata del beat in secondi riscalato sulla rata (1/2=0.5)
0.6 => player.gain;
me.dir() + "suoni/break1.wav" => player.read;
MAIN_RATE => player.rate;
0.02::second => env.duration;
// ------------------------------------------- Funzione tagli
function void taglio(int cue, dur dura)
{
player.samples() / 32 => int slice; // Taglia in 32 parti il soundfile e ne calcola la lungheazza di 1
slice * cue => int onset; // calcola il punto d'inizio sync con i tagli
player.pos(onset);
1 => env.keyOn;
dura - env.duration() => now;
1 => env.keyOff;
env.duration() => now;
}
// ------------------------------------------- Funzione riscalato
function void riscalato(int cue, dur durata, int divisore)
{
for(0 => int i; i < divisore; i++)
{
taglio(cue, durata / divisore); // riscala durata richiamando la funzione
}
}
while(true)
{
riscalato(0, 1 * beat, 8); // 1/8 della durata
riscalato(2, 1 * beat, 8);
taglio( 8, 2 * beat);
taglio( 24, 2 * beat);
taglio( 2, 1 * beat);
taglio( 2, 1 * beat);
riscalato(0, 1 * beat, 4);
riscalato(2, 0.5 * beat, 8);
riscalato(0, 0.5 * beat, 3);
taglio( 8, 1.5 * beat);
taglio( 8, 0.25 * beat);
taglio( 8, 0.25 * beat);
}
<<< "Crescendo" >>>;
// ------------------------------------------- Synth
SndBuf player => Envelope env => Pan2 pan => dac;
1.4 => float MAIN_RATE;
second / (2 * MAIN_RATE) => dur beat; // Durata del beat in secondi riscalato sulla rata (1/2=0.5)
0.6 => player.gain;
me.dir() + "suoni/break1.wav" => player.read;
MAIN_RATE => player.rate;
0.02::second => env.duration;
// ------------------------------------------- Funzione tagli
function void taglio(int cue, dur dura)
{
player.samples() / 32 => int slice; // Taglia in 32 parti il soundfile e ne calcola la lungheazza di 1
slice * cue => int onset; // calcola il punto d'inizio sync con i tagli
player.pos(onset);
1 => env.keyOn;
dura - env.duration() => now;
1 => env.keyOff;
env.duration() => now;
}
// ------------------------------------------- Funzione riscalato
function void riscalato(int cue, dur durata, int divisore)
{
for(0 => int i; i < divisore; i++)
{
taglio(cue, durata / divisore); // riscala durata richiamando la funzione
}
}
// ------------------------------------------- Funzione rampa gain
function void rampAmp(dur durata, int divisore)
{
durata / divisore => dur deltaT; // calcola il passo temporale
player.gain() - (player.gain() / 8) => float amp; // calcola incremento/decremento ampiezza
player.gain() / 8 => player.gain; // setta il gain al passo incrementale
// (partenza del crescendo)
for(0 => int i; i < divisore; i++)
{
player.gain() + (amp / divisore) => player.gain; // aggiorna valore
deltaT => now;
}
}
// ------------------------------------------- Funzione Crescendo
function void cresc(int cue, dur durata, int divisore)
{
spork ~ rampAmp(durata, divisore); // Sporkling parametri
riscalato(cue, durata, divisore);
}
while(true)
{
cresc( 0, 2 * beat, 16);
cresc( 2, 1 * beat, 16);
taglio( 8, 2 * beat);
taglio( 24, 2 * beat);
taglio( 2, 1 * beat);
taglio( 2, 1 * beat);
cresc( 0, 1 * beat, 4);
cresc( 2, 0.5 * beat, 8);
riscalato(0, 0.5 * beat, 3);
taglio( 8, 1.5 * beat);
taglio( 8, 0.25 * beat);
taglio( 8, 0.25 * beat);
}
Possiamo formalizzare e memorizzare parti di un brano in files di testo per poi importarli in uno shred secondo due strategie di programmazione.
La prima strategia ci consente ad esempio di realizzare una vera e propria DAW dinamica.
Come esempio riscriviamo la sequenza precedente.
Primo file (kick.ck) - child.
<<<"kick>>>">>>;
0.5::second => dur beat; // Stesso beat del file score (parent)
SndBuf kick => dac;
me.dir() + "suoni/drums/kick.wav" => string kickFilename;
kickFilename => kick.read;
fun void StopAll()
{
kick.samples() => kick.pos;
}
fun void Drum(int sel, dur durata, float amp)
{
if(sel == 0)
{
amp => kick.gain;
0 => kick.pos;
}
durata => now;
StopAll();
}
while(true) // Non è più in una funzione...
{
Drum(0, beat * 0.75, 0.7);
Drum(0, beat * 0.25, 0.4);
}
Secondo file (arpeggio.ck) - child.
<<<"Arpeggio">>>;
0.5::second => dur beat;
SinOsc osc => ADSR env => NRev rev => dac;
0.15 => osc.gain;
(1::ms, beat/4, 0, 1::ms) => env.set;
0.1 => rev.mix;
60 => int offset;
0 => int pos;
[0,8,10,13] @=> int note[];
fun void arpeggio(int accordo[]){
for(0 => int i; i < accordo.size(); i++)
{
Std.mtof(accordo[i] + offset + pos) => osc.freq;
1 => env.keyOn;
beat/4 => now;
1 => env.keyOff;
}
}
while(true) // Non è più in una funzione...
{
arpeggio(note);
}
File partitura (daw.ck) - parent.
<<<"Partitura">>>;
me.dir() + "kick.ck" => string kickN; // Path del file da caricare
me.dir() + "arpeggio.ck" => string arpN;
0.5::second => dur beat; // Imposta il beat
Machine.add(kickN) => int kickID; // Aggiunge
beat * 4 => now;
Machine.replace(kickID, arpN); // Rimpiazza
beat * 4 => now;
Machine.replace(kickID, kickN); // Rimpiazza
Machine.add(arpN) => int arpID; // Aggiunge
beat * 9 => now;
Machine.remove(kickID); // Rimuove
Machine.remove(arpID);
Se pensiamo il risultato sonoro di uno shred come al playback di un soundfile montato in una DAW questa procedura ci consente di:
Possiamo scrivere una partitura in un file di testo (.txt) separato per poi richiamarlo in uno shread.
Questa strategia deriva dalle schede perforate dei primi elaboratori o dai rulli utilizzati nei pianoforti meccanici ed è tipica di alcuni software dedicati alla musica come CSound e altri e in alcuni casi permette una migliore organizzazione del flusso di lavoro.
Scriviamo con un qualsiasi editor di testo (VScode o altro) e salviamo il contenuto della cella seguente in un file con estensione nome.txt.
Ciao, mi chiamo Andrea.
Ho scritto alcune parole in questo file di testo.
Ora possiamo importarlo in uno shred di ChucK con la Classe FileIO.
<<<"File IO test">>>;
FileIO io; // Dichiara una variabile che conterrà il contenuto del file
me.dir() + "testo.txt" => string nomefile; // Recupera il path assoluto
io.open(nomefile, FileIO.READ) => int successo; // Apre il file di testo e lo assegna a successo
<<< successo >>>;
Il codice precedente stampa:
<<<"Token">>>;
FileIO io;
me.dir() + "testo.txt" => string filename;
io.open(filename, FileIO.READ) => int successo;
io => string stampa; // Prima parola (token)
<<< stampa >>>;
io => stampa; // Seconda parola (token)
<<< stampa >>>;
io => stampa; // Terza parola (token)
<<< stampa >>>;
io => stampa; // ...
<<< stampa >>>;
io => stampa;
<<< stampa >>>;
Il codice precedente ha stampato una parola per riga.
Questo perchè la UGen FileIO divide il testo in token ( lessemi ) ovvero blocchi di testo categorizzati, normalmente separati da uno spazio
["Ciao,", "mi", "chiamo", "Andrea.", "Ho", "scritto", "alcune", "parole", "in", "questo", "file", "di", "testo."]
Per recuperare tutti i tokens di un file di testo dobbiamo iterarlo con un loop.
<<<"Eof">>>;
FileIO io;
me.dir() + "testo.txt" => string filename;
io.open(filename, FileIO.READ) => int successo;
string stampa; // Variabile
while(io.eof() == false) // eof = end of file
{
io => stampa;
<<< stampa >>>;
}
Se il testo è suddiviso in linee possiamo recuperare queste al posto dei tokens invocando la funzione readLine().
<<<"Linee">>>;
FileIO io;
me.dir() + "testo.txt" => string filename;
io.open(filename, FileIO.READ) => int successo;
string stampa;
while(io.eof() == false)
{
io.readLine() => stampa; // .readLine()
<<< stampa >>>;
}
I token possono anche essere dei numeri (int o float) e come tali rappresentare sequenze di parametri.
N.B. attenzione agli spazi all'inizio e alla fine del file salvato che potrebbero non essere visibili.
Se sono eventi (note) lasciamo sempre uno spazio alla fine.
0 12 5 7 -3 4 5 -1
<<<"Numeri">>>;
FileIO io;
me.dir() + "numeri.txt" => string filename;
io.open(filename, FileIO.READ) => int successo;
int stampa;
while(io.eof() == false)
{
io => stampa;
<<< stampa >>>;
}
Possiamo quindi scrivere una sequenza di valori (parametri) in un file .txt esterno per poi richiamarli nel tempo in uno shred.
<<<"Sequenza">>>;
SinOsc osc => ADSR env => dac;
(10::ms, 10::ms, 0.5, 100::ms) => env.set;
FileIO io;
me.dir() + "numeri.txt" => string filename;
io.open(filename, FileIO.READ) => int successo;
int stampa;
while(io => int val) // Fino a quando ci sono interi
{
<<< val >>>;
90 + val => Std.mtof => osc.freq;
1 => env.keyOn;
50::ms => now;
1 => env.keyOff;
100::ms => now;
}
Possiamo anche specificare più parametri per ogni evento.
Ad esempio nel codice seguente 'pitch suddivisione ampiezza' di ogni nota.
N.B. In questo caso non lasciamo alcuno spazio alla fine.
0 4 1.0
4 16 0.2
7 16 0.4
11 16 0.6
12 16 0.8
5 8 1.0
9 8 0.5
5 16 0.8
9 16 0.6
12 16 0.4
16 16 0.2
<<<"Melodia">>>;
SinOsc osc => ADSR env => dac;
(10::ms, 10::ms, 0.5, 100::ms) => env.set;
FileIO io;
me.dir() + "melo.txt" => string filename;
io.open(filename, FileIO.READ) => int successo;
2::second => dur beat;
80 => int offset;
while(true)
{
while(io.eof() == false)
{
io => int nota;
io => int div;
io => float amp;
<<< nota, div, amp >>>;
nota + offset => Std.mtof => osc.freq; // Frequenza
amp => osc.gain; // Ampiezza
1 => env.keyOn;
(beat / div) - env.releaseTime() => now; // Suddivisione beat
1 => env.keyOff;
env.releaseTime() => now;
}
0 => io.seek; // Resetta la lettura del file (per il loop)
}
In questo modo possiamo scrivere una partitura informatica ma per inserire commenti, pause o altre funzionalità dobbiamo utilizzare simboli diversi da int e stabilire delle regole e nel codice precedente tutto ciò che non è un int nel file di testo interrompe la computazione...
La soluzione a questo problema consiste nel considerare tutto (int, float, stringhe) sotto forma di stringa e utilizzare la Classe StringTokenizer per tokenizzare il contenuto di questo tipo di data.
<<<"Tokenizzami">>>;
StringTokenizer tok;
tok.set( "Tokenizzami per favore!" ); // Stringa da tokenizzare
while( tok.more() )
{
<<< tok.next(), "" >>>;
}
Vediamo una possibile procedura per ottimizzare la scrittura di una artitura di questo tipo.
// -------- Battuta 1
0 16
R 16
R 16
0 16
12 16
12
16
R 16
12 16
3 8
7 8
10 16
10 16
-2 16
-2 16
Troviamo il modo di individuare il simbolo specifico ed effettuare un parsing.
Per esempio la funzione .find("qualchecosa") se invocata sulle stringhe riporta la posizione (ID) del primo char che cerchiamo (come negli Array).
// 012345 indici della stringa
"andrea" => string myString;
myString.find("dr");
// Riporta 2 ...
<<<"Partitura">>>;
TriOsc osc => ADSR env => dac;
(10::ms, 10::ms, 0.5, 100::ms) => env.set;
FileIO io;
me.dir() + "token.txt" => string filename;
io.open(filename, FileIO.READ) => int successo;
2::second => dur beat;
60 => int offset;
StringTokenizer tok;
while(true)
{
while(io.more())
{
io.readLine() => string line; // Legge le singole linee e le considera Stringhe
if(line.find("//") == 0) // Se '//' è alla posizione 0
{
line => commento; // Considera la linea un commento (richiama la funzione)
}
else if(line.find("R") == 0) // Se invece 'R' è alla posizione 0
{
line => pausa; // Considera la linea una pausa (richiama la funzione)
}
else // Altrimenti considerala una nota (richiama la funzione)
{
line => nota;
}
}
0 => io.seek;
}
// A questo punto scriviamo le TRE FUNZIONI che richiamiamo
fun void commento(string line)
{
<<< line >>>; // Stampa
}
fun void pausa(string line)
{
tok.set(line); // Stringa da tokenizzare (la linea)
tok.next() => string rest; // recupera il primo token (simbolo R)
tok.next() => Std.atoi => int div; // recupera il secondo token (la suddivisione). Casting a int
<<< rest, div >>>; // Stampa
beat / div => now; // Avanza la computazione del tempo
}
fun void nota(string line)
{
tok.set(line); // Stringa da tokenizzare (la linea)
tok.next() => Std.atoi => int nota; // recupera il primo token (Casting a int)
tok.next() => Std.atoi => int div; // recupera il secondo token (la suddivisione). Casting a int
<<< nota, div >>>; // Stampa
nota + offset => Std.mtof => osc.freq;
1 => env.keyOn;
(beat / div) - env.releaseTime() => now; // Suddivisione beat
1 => env.keyOff;
env.releaseTime() => now;
}
Ora possiamo modificare dinamicamente nel tempo tutti i parametri di qualsiasi algoritmo di sintesi o elaborazione del suono.
Rendiamo più complesso l'esempio precedente aggiungendo un filtro e un panner con il controllo dei relativi parametri.
N.B. .substring(1) legge tutta la stringa ad eccezione de primo char
<<<"Partitura complessa">>>;
SqrOsc osc => ADSR env => LPF lpf => Pan2 pan => dac;
(10::ms, 10::ms, 0.5, 100::ms) => env.set;
FileIO io;
me.dir() + "extra.txt" => string filename;
io.open(filename, FileIO.READ) => int successo;
2::second => dur beat;
60 => int offset;
StringTokenizer tok;
while(true)
{
while(io.more())
{
io.readLine() => string line;
if(line.find("//") == 0)
{
line => commento;
}
else if(line.find("R") == 0)
{
line => pausa;
}
else
{
line => nota;
}
}
0 => io.seek;
}
fun void commento(string line)
{
<<< line >>>;
}
fun void pausa(string line)
{
tok.set(line);
tok.next() => string rest;
tok.next() => Std.atoi => int div;
<<< rest, div >>>;
beat / div => now;
}
fun void nota(string line)
{
tok.set(line);
tok.next() => Std.atoi => int nota;
tok.next() => Std.atoi => int div;
<<< nota, div >>>;
nota + offset => Std.mtof => osc.freq;
altri(tok); // Richiamiamo una nuova funzione...
1 => env.keyOn;
(beat / div) - env.releaseTime() => now;
1 => env.keyOff;
env.releaseTime() => now;
}
fun void altri(StringTokenizer tok) // Funzione che identifica più parametri sulla stessa linea
{
while(tok.more()) // Se ci sono altri token nella linea
{
tok.next() => string extra; // Prende il token e lo assegna a extra
if(extra.find("P") == 0) // Se comincia per 'P'
{
extra.substring(1) => Std.atof => pan.pan; // Salta il primo char ("P") e Setta il pan
}
if(extra.find("F") == 0) // Se comincia per 'F'
{
extra.substring(1) => Std.atof => lpf.freq; // Salta il primo char e Setta la frequenza del filtro
}
if(extra.find("Q") == 0) // Se comincia per 'Q'
{
extra.substring(1) => Std.atof => lpf.Q; // Salta il primo char e Setta la risonanza del filtro
}
}
}
Se vogliamo organizzare la score in modo più ordinato (CSound style) StringTokenizer supporta la lettura di più spazi come singolo spazio.
// -------- Inizio
-12 16 P0.0 F2000 Q1.0
R 16
R 16
0 16 Q4.0
12 16 P-1.0 F2000
12 16 P1.0 F600
R 16
12 16 F800
3 8 P0.5 Q1.0
7 8 Q2.0
-14 16 F200 Q8.0
-14 16 F400
-14 16 F600
-14 16 F800
Possiamo creare un file di testo e scriverci dentro direttamente da ChucK.
Prestiamo attenzione in quanto le linee di codice di Chuck (separate da un punto e vigola) non coincidono con le linee del file che stiamo scrivendo (e che poi potremo voler richiamare una alla volta).
<<<"Scrivi">>>;
FileIO fout;
me.dir() + "out.txt" => string filename;
fout.open( filename, FileIO.WRITE ); // Apre o crea un file se non esiste
if( !fout.good() ) // Verifica se può essere scritto
{
cherr <= "Non posso aprire il file per scrivere..." <= IO.newline();
me.exit();
}
// spazio spazio
fout <= 1 <= " " <= 2 <= " " <= "ciao"; // Scrive all'interno
fout <= 1 <= " " <= 2 <= " " <= "ciao"; // Scrive di seguito...
fout.write( 1 ); // Altro modo...
fout.write( 2 );
fout.write( "tre!" );
fout.close(); // Chiude il file
Se vogliamo andare a capo utilizziamo IO.newline() e possiamo anche utilizzare dei loop.
<<<"A capo">>>;
FileIO fout;
me.dir() + "out.txt" => string filename;
fout.open( filename, FileIO.WRITE );
for(1 => int i; i <= 1000; i++)
{
if( !fout.good() )
{
cherr <= "Non posso aprire il file per scrivere..." <= IO.newline(); // newline
me.exit();
}
fout <= i <= IO.newline(); // newline
}
fout.close();
Nel caso precedente se modifichiamo qualcosa ed eseguiamo nuovamente lo shred il file di testo viene riscritto.
Se invece vogliamo aggiungere contenuto ad un file esistente.
<<<"Aggiungi">>>;
FileIO fout;
me.dir() + "out.txt" => string filename; // Scrive un file esistente...
fout.open( filename, FileIO.APPEND ); // APPEND al posto di WRITE
for(1 => int i; i <= 10; i++)
{
if( !fout.good() )
{
cherr <= "Non posso aprire il file per scrivere..." <= IO.newline();
me.exit();
}
fout <= i <= IO.newline(); // newline
}
fout.close();
L'utilità di scrivere automaticamente partiture in un file di testo si evidenzia nel momento in cui volgiamo tenere traccia di processi randomici per poi riprodurli oppure modificarli in modo deterministico (tagliando valori, cambiadoli, estraendo loop da sequenze infinite, etc...) secondo regole ed esigenze prettamente musicali.
Ad esempio.
<<<"Random">>>;
SinOsc osc => ADSR env => Pan2 pan => NRev rev[2] => dac;
0.25 => osc.gain;
(10::ms, 10::ms, 0.5, 100::ms) => env.set;
0.1 => rev[0].mix => rev[1].mix;
[0,3,7,10,12,14] @=> int notes[];
48 => int offset;
0 => int ottava;
for(0 => int i; i < 256; i++)
{
Math.random2(0,2) * 12 => ottava; // Ottave ramdom
notes[Math.random2(0, notes.cap()-1)] + offset + ottava => int note; // Sequenze pitch random
note => Std.mtof => osc.freq;
Math.random2f(-1.0,1.0) => pan.pan; // Pan random
Math.random2(10, 40) => int decay; // Tempo di decadimento random
decay::ms => env.decayTime;
1 => env.keyOn;
50::ms => now;
1 => env.keyOff;
env.releaseTime() => now;
}
Se volessimo registrare le scelte effettuate dal computer per poi riprodurle identiche.
<<<"Registra partitura">>>;
SinOsc osc => ADSR env => Pan2 pan => NRev rev[2] => dac;
FileIO partFile;
me.dir() + "random.txt" => string filename; // Crea un file
partFile.open( filename, FileIO.WRITE );
0.25 => osc.gain;
(10::ms, 10::ms, 0.5, 100::ms) => env.set;
0.1 => rev[0].mix => rev[1].mix;
[0,3,7,10,12,14] @=> int notes[];
48 => int offset;
0 => int ottava;
for(0 => int i; i < 256; i++)
{
Math.random2(0,2) * 12 => ottava;
notes[Math.random2(0, notes.cap()-1)] + offset + ottava => int note;
note => Std.mtof => osc.freq;
Math.random2f(-1.0,1.0) => pan.pan;
Math.random2(10, 40) => int decay;
decay::ms => env.decayTime;
1 => env.keyOn;
partFile <= note <= " " <= pan.pan() <= " " <= decay <= IO.newline(); // Scrive i parametri generati
50::ms => now;
1 => env.keyOff;
env.releaseTime() => now;
}
partFile.close(); // Chiude il file
Ora possiamo:
Ad esempio possiamo tagliare a mano la sequenza generata randomicamente tenendo pochi eventi per realizzare un loop ritmico melodico.
<<<"Replay">>>;
SinOsc osc => ADSR env => Pan2 pan => NRev rev[2] => dac;
FileIO partFile;
me.dir() + "random.txt" => string filename; // Path assoluto
partFile.open( filename, FileIO.READ ); // Legge il file
0.25 => osc.gain;
(10::ms, 10::ms, 0.5, 100::ms) => env.set;
0.1 => rev[0].mix => rev[1].mix;
while(true)
{
while(partFile.eof() == false)
{
partFile => int note;
partFile => float panPos;
partFile => int decay;
note => Std.mtof => osc.freq;
panPos => pan.pan;
decay::ms => env.decayTime;
1 => env.keyOn;
50::ms => now;
1 => env.keyOff;
env.releaseTime() => now;
}
0 => partFile.seek;
}
partFile.close();
Realizzare un'improvvisazione mandando in loop una o più sequenza sonore o musicali controllate attraverso parametri memorizzati in un coll file esterno come negli esempi precedenti.
Mentre il loop procede modificare i parametri nel file di testo esterno per poi salvarlo e sostituire lo shred che sta suonando per lanciare le modifiche oppure lanciarne una nuova istanza sovrapposta alla precedente.
Un alternativa alle tecniche di scoring nella gestione di una partitura informatica è fornita dalla classe Event.
La strategia in questo caso consiste nel:
<<<"Event">>>;
fun void nota(Event aspe, int frq) // Classe Event come argomento da collegare con now all'interno della funzione
{
SinOsc osc => ADSR env => dac; // Polifonico in quanto interno alla funzione
0.25 => osc.gain;
(10::ms, 100::ms, 0.5, 300::ms) => env.set;
while(true) // Loop infinito all'interno della funzione (child)
{
<<< "On" >>>;
0.3 => osc.gain;
frq => osc.freq;
1 => env.keyOn; // La prima volta esegue fino a qua...
aspe => now; // Non prosegue fino a quando non riceverà un trigger...
<<< "Off" >>>;
1 => env.keyOff;
env.releaseTime() => now;
}
}
Event myEvt; // Istanza di Event
spork ~ nota(myEvt, 500); // Richiama la funzione e passa l'istanza come argomento
while(true)
{
500::ms => now; // Ogni 2 secondi triggera l'evento successivo con .signal()
//myEvt.signal(); // Se commentato va avanti all'infinito...
}
Possiamo definire più eventi per poi triggerarli uno dopo l'altro.
<<<"Più Events">>>;
fun void nota(Event aspe, int frq)
{
SinOsc osc => ADSR env => dac;
0.25 => osc.gain;
(10::ms, 100::ms, 0.5, 300::ms) => env.set;
while(true)
{ aspe => now; // Aspetta il trigger...
<<< "Trig" >>>;
0.3 => osc.gain;
frq => osc.freq;
1 => env.keyOn;
150::ms => now;
1 => env.keyOff;
env.releaseTime() => now;
}
}
Event myEvt;
spork ~ nota(myEvt, 500); // Primo Evento
spork ~ nota(myEvt, 1600); // Secondo Evento
spork ~ nota(myEvt, 2700); // Terzo Evento
while(true)
{
200::ms => now;
myEvt.signal();
}
Se invece vogliamo richiamarli tutti assieme.
<<<"Events sincroni">>>;
fun void nota(Event aspe, int frq)
{
SinOsc osc => ADSR env => dac;
0.25 => osc.gain;
(10::ms, 100::ms, 0.5, 300::ms) => env.set;
while(true)
{ aspe => now; // Aspetta il trigger...
<<< "Trig" >>>;
0.3 => osc.gain;
frq => osc.freq;
1 => env.keyOn;
150::ms => now;
1 => env.keyOff;
env.releaseTime() => now;
}
}
Event myEvt;
spork ~ nota(myEvt, 500); // Primo Evento
spork ~ nota(myEvt, 1400); // Secondo Evento
spork ~ nota(myEvt, 2500); // Terzo Evento
while(true)
{
1::second => now;
myEvt.broadcast(); // Tutti assieme...
}
Il trigger può essere lanciato da un altro shred utilizzato come partitura informatica anche se l'utilizzo principale di questa funzionalità è quello di mettere uno shred o parti di esso in ascolto (in attesa) di trigger provenienti dall'esterno della Virtual Machine generati tipicamente da HID, controller o tastiere MIDI, devices che possono inviare data attraverso il protocollo OSC, etc.
Se vogliamo controllare i parametri di uno shred attraverso Human Interface Devices come la tastiera del computer, il mouse, un joystick, etc. possiamo utilizzare le classi HID e HidMsg che sono sottoclassi di Event e principalmente funzionano secondo lo stesso principio.
Prima però dobbiamo ottenere un elenco dei dispositivi disponibili e riconosciuti dalla Virtual Machine.
from IPython.display import HTML
HTML('<center><img src="media/hid.png" width="75%"></center>')
chuck --probe
from IPython.display import HTML
HTML('<center><img src="media/hid2.png" width="75%"></center>')
In entrambe i casi otteniamo un elenco indicizzato.
Possiamo ricevere data da HID esterni come la tastiera del computer attraverso la classe Hid che è una sottoclasse di Event.
<<<"Tastiera">>>;
Hid hi; // Sottoclasse di Event - istanza di tastiera
HidMsg msg; // Messaggio ricevuto
// ID
if( !hi.openKeyboard(0) ) me.exit(); // Se non connesso esce
<<< "keyboard '" + hi.name() + "' pronto", "">>>; //
while(true)
{ // Rimane in attesa di un Event proveniente dalla tastiera.
hi => now; // Prosegue quando arriva
while(hi.recv(msg) && msg.which != 29) // Se NON è tasto ctrl (diverso da 29)
{
if( msg.isButtonDown() ) // Se è un'azione di tasto schiacciato
{
<<< "down:", msg.which, "(code)", msg.key, "(usb key)", msg.ascii, "(ascii)" >>>; // ...stampa
}
}
}
Come esempio recuperiamo i valori ascii dei tasti e li mappiamo come frequenze di un synth.
<<<"Tastiera monofonica">>>;
// ----------------- Catena audio
SawOsc osc => ADSR env => NRev rev => dac;
(1::ms, 98::ms, 0, 1::ms) => env.set;
0.5 => osc.gain;
0.01 => rev.mix;
// ----------------- Input dai tasti
Hid hi;
HidMsg msg;
if( !hi.openKeyboard(0) ) me.exit();
<<< "keyboard '" + hi.name() + "' pronto", "">>>;
fun void nota(int key)
{
key => Std.mtof => osc.freq;
1 => env.keyOn;
99::ms => now;
1 => env.keyOff;
env.releaseTime() => now;
}
while(true)
{
hi => now; // Tempo che avanza (rimane in attesa)
while(hi.recv(msg) && msg.which != 29) // Quando riceve il trigger
{
if( msg.isButtonDown() ) // Se è un messaggio di ButtonDown()
{
spork ~ nota(msg.ascii); // Recupera il valore ascii e lo utilizza come midinote
}
}
}
Rendiamola polifonica.
<<<"Tastiera polifonica">>>;
NRev rev => dac; // Il riverbero va fuori...
// ----------------- Input dai tasti
Hid hi;
HidMsg msg;
if( !hi.openKeyboard( 0 ) ) me.exit();
<<< "keyboard '" + hi.name() + "' pronto", "">>>;
fun void nota(int key)
{
TriOsc osc => ADSR env => rev;
(10::ms, 599::ms, 0, 1::ms) => env.set;
0.25 => osc.gain;
0.01 => rev.mix;
key => Std.mtof => osc.freq; // Altezza
1 => env.keyOn; // Trigger
609::ms => now; // Durata
1 => env.keyOff;
200::ms => now; // Sintetizza anche coda riverbero
}
while(true)
{
hi => now;
while(hi.recv( msg ) && msg.which != 29)
{
if( msg.isButtonDown() )
{
spork ~ nota(msg.ascii); // Recupera il valore ascii e lo utilizza come midinote
}
}
}
Un esempio di switch con note On e note OFF.
<<<"Switch">>>;
// ----------------- Input dai tasti
Hid hi;
HidMsg msg;
if( !hi.openKeyboard( 0 ) ) me.exit();
<<< "keyboard '" + hi.name() + "' pronto", "">>>;
// ----------------- Algoritmo di sintesi
BeeThree organ => JCRev r => Echo e => Echo e2 => dac;
r => dac;
240::ms => e.max => e.delay;
480::ms => e2.max => e2.delay;
0.6 => e.gain;
0.3 => e2.gain;
0.05 => r.mix;
0 => organ.gain;
while( true )
{
hi => now;
while(hi.recv( msg ) && msg.which != 29)
{
if( msg.isButtonDown() )
{
Std.mtof( msg.which + 45 ) => float freq;
if( freq > 20000 ) continue; // Se superiore a 20000 non esegue il codice seguente
freq => organ.freq;
0.5 => organ.gain;
1 => organ.noteOn;
80::ms => now;
}
else
{
0 => organ.noteOff;
}
}
}
Potremmo avere la necessita di registrare i valori generati da azioni effettuate su devices esterni sia in tempo reale che in tempo differito.
Come esempio osserviamo come utilizzare la tastiera del computer per scrivere in tempo differito i valori di uno step sequencer.
Definiamo le azioni che ci interessa catturare e le mappiamo su alcuni tasti.
y --> Ottava sopra
da 1 a 8 --> suddivisioni del beat
<<<"Registra tasti">>>;
// --------------------- Algoritmo di sintesi
SinOsc osc => ADSR env => dac;
0.25 => osc.gain;
(10::ms, 10::ms, 0.5, 100::ms) => env.set;
// --------------------- Crea un file di testo esterno
FileIO partFile;
me.dir() + "out_tasti.txt" => string fileName; // Path assoluto
partFile.open( fileName, FileIO.WRITE ); // Legge il file
// --------------------- HID (Tastiera)
Hid hi;
HidMsg msg;
1 => int avanza;
if( !hi.openKeyboard( 0 ) ) me.exit();
<<< "keyboard '" + hi.name() + "' pronta,", "Step sequencer in attesa di", fileName >>>;
// --------------------- Funzioni utili
fun void suona(int note)
{
note => Std.mtof => osc.freq;
1 => env.keyOn;
50::ms => now;
1 => env.keyOff;
env.releaseTime() => now;
}
int div; // Variabile globale
fun void diviso(int divisore) // Divisore del beat
{
divisore => div;
<<< "Divisore beat:", div >>>;
}
diviso(16); // Default 1/16
int offset;
fun void offs(int myOffs) // Offset pitch
{
myOffs => offset;
<<< "Offset:", offset >>>;
}
offs(60); // Default Do centrale
fun void nota(int note) // Azioni relative alla nota
{
partFile <= note <= " " <= div <= IO.newline(); // Scrive nel file di testo esterno
<<< note, div >>>; // Monitor visivo
suona(note); // Invia allla UGen
}
fun void pausa() // Azioni relative alle pause
{
partFile <= "R" <= " " <= div <= IO.newline(); // Scrive nel file di teso esterno
<<< "R", div >>>; // Monitor visivo
}
// --------------------- Gestione comandi da tastiera
fun void tasti(int quale)
{
if(quale == 12) // u
{
offs(offset - 12);
}
if(quale == 24) // i
{
offs(offset + 12);
}
// ---> Divisori del beat
if(quale == 30) // 1
{
diviso(1);
}
if(quale == 31) // 2
{
diviso(2);
}
if(quale == 32) // 3
{
diviso(3);
}
if(quale == 33) // 4
{
diviso(4);
}
if(quale == 34) // 5
{
diviso(6);
}
if(quale == 36) // 6
{
diviso(8);
}
if(quale == 36) // 7
{
diviso(16);
}
if(quale == 37) // 8
{
diviso(32);
}
// ---> Altezze
if(quale == 6) // c
{
nota(offset);
}
if(quale == 7) // d
{
nota(offset + 2);
}
if(quale == 8) // e
{
nota(offset + 4);
}
if(quale == 9) // f
{
nota(offset + 5);
}
if(quale == 10) // g
{
nota(offset + 7);
}
if(quale == 4) // a
{
nota(offset + 9);
}
if(quale == 5) // b
{
nota(offset + 11);
}
// ---> Pause
if(quale == 19) // p
{
pausa();
}
// ---> Procedi
if(quale == 40) // enter
{
0 => avanza;
}
}
while( avanza == 1 )
{
hi => now; // Quando arriva un messaggio dalla tastiera....
while( hi.recv( msg ) && msg.which != 29 ) // Ignora ctrl key
{
if( msg.isButtonDown() ) // Se è un tasto schiacciato...
{
tasti(msg.which); // Richiama la funzione
}
}
}
<<< "scritto in", fileName >>>;
partFile.close();
Per rileggere il file possiamo utilizzare strategie già illustrate per lo scoring.
<<<"Riproduci sequenza">>>;
SinOsc osc => Envelope env => dac;
10::ms => env.duration;
FileIO io;
StringTokenizer tok;
2::second => dur beat;
48 => int offset;
0.2 => osc.gain;
me.dir() + "out_tasti.txt" => string fileName;
while(true)
{
io.open(fileName, FileIO.READ);
while(io.more())
{
io.readLine() => string line;
if(io.more())
{
if (line.find("//") == 0 )
{
line => commento;
}
else if (line.find("R") == 0)
{
line => pausa;
}
else if (line.length() > 0)
{
line => nota;
}
}
}
}
fun void commento(string line)
{
<<< line >>>;
}
fun void pausa(string line)
{
tok.set(line);
tok.next() => string rest;
tok.next() => Std.atoi => int div;
<<< rest, div >>>;
beat / div => now;
}
fun void nota(string line)
{
tok.set(line);
tok.next() => Std.atoi => int note;
tok.next() => Std.atoi => int div;
<<< note, div >>>;
note => Std.mtof => osc.freq;
1 => env.keyOn;
(beat/div) - env.duration() => now;
1 => env.keyOff;
env.duration() => now;
}
Per quanto riguarda l'interazione con il mouse lo schema sintattico e i procedimenti sono pressochè identici a quelli illustrati per la tastiera.
Aumentano le possibilità.
<<<"Mouse">>>;
// ----------------- Input dal mouse
Hid hi;
HidMsg msg;
if( !hi.openMouse( 1 ) ) me.exit(); // .openMouse al posto di .openKeyboard
<<< "mouse '" + hi.name() + "' pronto", "">>>;
// ----------------- Possibili controlli
while(true)
{
hi => now;
while(hi.recv( msg ))
{
if( msg.isMouseMotion() ) // Se movimento del mouse
{
<<< "posizione normalizzata -",
"x:", msg.scaledCursorX, "y:", msg.scaledCursorY >>>;
<<< "posizione assoluta -",
"x:", msg.cursorX, " y:", msg.cursorY >>>;
<<< "velocità x:", msg.deltaX >>>;
<<< "velocità y:", msg.deltaY >>>;
}
else if( msg.isButtonDown() ) // Se click del bottone
{
<<< "Click", msg.which, "down" >>>;
}
else if( msg.isButtonUp() )
{
<<< "Click", msg.which, "up" >>>;
}
else if( msg.isWheelMotion() ) // Se rotellina
{
<<< "wheel:", msg.deltaX, "x" >>>;
<<< "wheel:", msg.deltaY, "y" >>>;
}
}
}
Esempio sonoro.
<<<"Mouse sonoro">>>;
// ----------------- Input dal mouse
Hid hi;
HidMsg msg;
if( !hi.openMouse( 1 ) ) me.exit();
<<< "mouse '" + hi.name() + "' pronto", "">>>;
// ----------------- Algoritmo di sintesi
SawOsc o => BPF f => Envelope e => Pan2 p => dac;
// ----------------- Parametri
100::ms => e.duration; // Tempo di fade
0.5 => o.gain; // Oscilla gain
0.5 => f.gain;
0 => float freqO; // Frequenza oscillatore
0 => float freqF; // Frequenza filtro
0 => float Q; // Risonanza filtro
0 => int count;
set(freqO, freqF, Q); // Inizializza
while(true)
{
hi => now;
while(hi.recv(msg))
{
if(msg.isMouseMotion()) // Movimento
{
(msg.scaledCursorX * 2) - 1.0 => p.pan; // posizione x => pan
msg.scaledCursorX => freqF; // posizione x => freq Filtro
msg.scaledCursorY => freqO; // posizione y => freq Oscill
msg.scaledCursorY => Q; // posizione y => freq Oscill
set(freqF, freqO, Q);
}
if(msg.isButtonDown()) // Click
{
count++; // Switch note ON
if(count) e.keyOn();
set(freqF, freqO, Q);
}
if(msg.isButtonUp())
{
count--; // Switch note OFF
if(!count) e.keyOff();
}
}
}
fun void set(float freqF, float freqO, float q)
{
Math.fabs(1.0-freqO) * 440 + 200 => o.freq;
freqF * 4440 + 480 => f.freq;
q * 20 + 0.01 => f.Q;
// ----------------- Monitor numerico
if(count)
<<< "pan:", p.pan(), "freq:", o.freq(), "res:", f.freq(), "q:", f.Q() >>>;
}
Il Musical Instrument Digital Interface è un protocollo che permette a diversi hardware e/o diversi software di comunicare tra loro attraverso l'invio di pacchetti di dati.
I devices sono collegati attraverso cavi MIDI.
Anche in questo caso possiamo ottenere una lista di devices MIDI nei due modi già illustrati per le HID.
In miniAudicle nel menù a tendina sceglieremo MIDI.
Per ricevere messaggi MIDI possiamo utilizzare MidiIn che è una sottoclasse di Event e contiene un receive buffer dove sono scritti i messaggi midi (MidiMsg) in ingresso.
MidiMsg contiene 3 streams di data:
Ad esempio se riceve un messaggio di Note On:
<<<"Ricevere MIDI">>>;
MidiIn midiIn; // Evento MIDI
MidiMsg midiMsg; // Buffer per memorizzare il messaggio
if ( !midiIn.open(0) ) me.exit(); // Se non è il device esce dallo shred
<<< "MIDI device:", midiIn.num(), " -> ", midiIn.name() >>>;
while(true)
{
<<< "Sto aspettando un messaggio MIDI" >>>;
midiIn => now;
while( midiIn.recv(midiMsg) ) // Quando riceve il messaggio
{
<<< midiMsg.data1, midiMsg.data2, midiMsg.data3 >>>; // Stampa i valori dello stream
}
}
I principali indici sono:
Esempio di strumento monofonico.
<<<"Strumento MIDI monofonico">>>;
// ----------------- Catena audio
TriOsc osc => Envelope env => NRev rev => dac;
20::ms => env.duration; // Fade
100::ms => dur dur; // Durata
0.5 => osc.gain;
0.1 => rev.mix;
// ----------------- Input MIDI
MidiIn midiIn;
MidiMsg midiMsg;
if( !midiIn.open(0) ) me.exit();
<<< "MIDI device:", midiIn.num(), " -> ", midiIn.name() >>>;
fun void nota(int key, int vel, dur dur)
{
key => Std.mtof => osc.freq; // Altezza
vel / 127.0 => osc.gain; // Ampiezza
1 => env.keyOn; // Note On
dur => now; // Durata
1 => env.keyOff; // Note Off
}
while(true)
{
midiIn => now;
while(midiIn.recv( midiMsg ))
{
if(midiMsg.data1 == 144) // Note On
{
spork ~ nota(midiMsg.data2, midiMsg.data3, dur);
<<< midiMsg.data1, midiMsg.data2, midiMsg.data3, dur >>>;
}
}
}
Esempio di strumento polifonico (con durata fissa).
<<<"Strumento MIDI polifonico">>>;
NRev rev => dac; // Il riverbero va fuori...
// ----------------- Input MIDI
MidiIn midiIn;
MidiMsg midiMsg;
if( !midiIn.open(0) ) me.exit();
<<< "MIDI device:", midiIn.num(), " -> ", midiIn.name() >>>;
1500::ms => dur dur; // Durata delle note
fun void nota(int key, int vel, dur dur)
{
// ----------------- Catena audio (interna alla funzione)
SinOsc osc => ADSR env => rev;
0.25 => osc.gain;
(20::ms, 20::ms, 0.5, 200::ms) => env.set;
0.03 => rev.mix;
key => Std.mtof => osc.freq; // Altezza
0.3 * (vel / 127.0) => osc.gain; // Ampiezza
1 => env.keyOn; // Note On
dur => now; // Durata
1 => env.keyOff; // Note Off
}
while(true)
{
midiIn => now;
while(midiIn.recv( midiMsg ))
{
if(midiMsg.data1 == 144) // Note On
{
spork ~ nota(midiMsg.data2, midiMsg.data3, dur);
<<< midiMsg.data1, midiMsg.data2, midiMsg.data3 >>>;
}
}
}
Aggiungiamo un controller che controlla la frequenza di taglio di un filtro passa basso.
<<<"Strumento MIDI polifonico">>>;
LPF lpf => NRev rev => dac; // Aggiungiamo il filtro fuori...
500 => lpf.freq; // init
5 => lpf.Q;
0.05 => rev.mix;
// ----------------- Input MIDI
MidiIn midiIn;
MidiMsg midiMsg;
if( !midiIn.open(0) ) me.exit();
<<< "MIDI device:", midiIn.num(), " -> ", midiIn.name() >>>;
1500::ms => dur dur; // Durata delle note
fun void nota(int key, int vel, dur dur)
{
// ----------------- Catena audio (interna alla funzione)
SinOsc osc => ADSR env => lpf;
0.25 => osc.gain;
(20::ms, 20::ms, 0.5, 200::ms) => env.set;
0.03 => rev.mix;
key => Std.mtof => osc.freq; // Altezza
0.3 * (vel / 127.0) => osc.gain; // Ampiezza
1 => env.keyOn; // Note On
dur => now; // Durata
1 => env.keyOff; // Note Off
}
while(true)
{
midiIn => now;
while(midiIn.recv( midiMsg ))
{
if(midiMsg.data1 == 144) // Note On
{
spork ~ nota(midiMsg.data2, midiMsg.data3, dur);
<<< midiMsg.data1, midiMsg.data2, midiMsg.data3 >>>;
}
if(midiMsg.data1 == 176) // Bend
{
10000 * (midiMsg.data3 / 127.0) => lpf.freq;
}
}
}
Se vogliamo costruire uno strumento polifonico MIDI controllato da un inviluppo con fase di sostegno possiamo analizzare il codice a questo link
Per inviare messaggi MIDI possiamo utilizzare la classe MidiOut.
<<<"MIDI out">>>;
MidiOut midiOut; // Evento MIDI
midiOut.open(0); // Destinazione - apre il device MIDI collegato all'ID (recuperabile con --probe)
MidiMsg midiMsg; // Buffer per il messaggio in uscita
48 => int offset;
[0,4,7,11] @=> int accordo[];
fun void invia(int note, int vel)
{
<<< note >>>;
144 => midiMsg.data1; // Note On Aggiunge un valore...
note => midiMsg.data2; // Altezza
vel => midiMsg.data3; // Velocity
midiOut.send(midiMsg); // Invia
50::ms => now; // Durata
128 => midiMsg.data1; // Note Off
note => midiMsg.data2; // Altezza
vel => midiMsg.data3; // Velocity
midiOut.send(midiMsg);
50::ms => now; // Durata
}
while(true)
{
Math.random2(0, 4) * 12 => int ottava;
Math.random2(0, accordo.cap()-1) => int note;
Math.random2(0, 4) * 12 => int vel;
invia(accordo[note] + ottava + offset, vel);
}
Open Sound Control è un protocollo per permettere a diversi hardware e/o diversi software di comunicare tra loro attraverso l'invio di pacchetti di dati.
A differenza del protocollo MIDI le comunicazioni non avvengono attraverso il collegamento di cavi ma sono wireless.
E' basato su di un'architettura Server - Client.
Per l'invio dei pacchetti di dati utilizza il protocollo UDP.
A differenza del MIDI non ci sono tipi di messaggi predefiniti come noteon o pitchbend o altro, è un sistema aperto (Open).
Un Messaggio OSC è formato da un OSC Address Pattern seguito da una OSC Type Tag String e da zero o più OSC Arguments.
Gli argomenti possono essere principalmente: string, int e float.
Se il Server riceve un pacchetto di dati dei quali non sa cosa fare lo elimina quindi quando inviamo un comando dal Client al Server dobbiamo conoscere quali sono i comandi accettati da quest'ultimo.
Per ricevere messaggi OSC possiamo utilizzare OscIn che è una sottoclasse di Event e contiene un receive buffer dove sono scritti i messaggi OSC (OscMsg) in ingresso.
Per inviare o ricevere un messaggio OSC da un Client dobbiamo definire una porta (un numero a piacere) e un indirizzo (una stringa) che dovranno essere specificati anche nel Server.
<<<"Ricevi OSC">>>;
OscIn oscIn; // L'evento OSC
OscMsg oscMsg; // I suoi messaggi
9001 => oscIn.port; // Porta dalla quale riceve
oscIn.addAddress("/myOsc/ciao"); // Indirizzo (etichetta)
while(true)
{
<<< "Sto aspettando un messaggio OSC" >>>;
oscIn => now;
while( oscIn.recv(oscMsg) !=0 ) // Quando riceve il messaggio
{
oscMsg.getInt(0) => int nota; // Prende il primo item (int) e lo assegna a una variabile
oscMsg.getFloat(1) => float amp; // Prende il secondo item (float) e lo assegna a una variabile
oscMsg.getString(2) => string strum; // Prende il terzo item (string) e lo assegna a una variabile
<<< "Ricevuto: ", strum, nota, amp >>>; // Stampa i valori dello stream
}
}
Per inviare messaggi OSC possiamo utilizzare OscOut.
<<<"Invia OSC">>>;
OscOut oscOut; // Evento OSC
oscOut.dest("localhost", 9001); // Destinazione ("IP", porta)
oscOut.start("/myOsc/Ciao"); // Indirizzo (etichetta)
72 => int nota;
nota => oscOut.add; // Aggiunge un valore (argomento) al messaggio
0.5 => float amp;
amp => oscOut.add;
"Vl."=> string istr;
istr => oscOut.add;
oscOut.send(); // Invia tre items (come in un Array)
Testiamo i codici precedenti in locale.
Esempio musicale...
<<<"Test riceve OSC (sinistra)">>>;
SinOsc sig => ADSR env => dac;
env => NRev rev => dac;
0.25 => sig.gain;
(1::ms, 100::ms, 0, 1::ms) => env.set;
0.1 => rev.mix;
OscIn oscIn;
OscMsg oscMsg;
9000 => oscIn.port;
oscIn.addAddress("/myOsc/Ciao");
while(true)
{
<<< "Sto aspettando un messaggio OSC" >>>;
oscIn => now;
while( oscIn.recv(oscMsg) !=0 )
{
oscMsg.getInt(0) => int nota;
oscMsg.getFloat(1) => float amp;
oscMsg.getString(2) => string strum;
<<< strum, nota, amp >>>;
nota => Std.mtof => sig.freq; // Converte e invia al Synth
amp => sig.gain;
1 => env.keyOn;
}
}
<<<"Test invia OSC (destra)">>>;
OscOut oscOut;
oscOut.dest("localhost", 9000);
[-12,0,4,7,11,12,14] @=> int notes[];
[0.0,0.2,1.0,0.5] @=> float amps[];
while(true)
{
oscOut.start("/myOsc/Ciao"); // Crea un messaggio
notes[Math.random2(0, notes.cap()-1)] + 60 => int nota; // Primo item (int)
amps[Math.random2(0, amps.cap()-1)] => float amp; // Secondo item (float)
"mySynth" => string instr; // Terzo item (string)
nota => oscOut.add;
amp => oscOut.add;
instr => oscOut.add;
oscOut.send(); // Invia il messaggio
150::ms => now;
}
Se vogliamo inviare messaggi OSC ad un Server che è su un altro device dobbiamo sostituire "localhost" con l'indirizzo IP del device ricevente.
Per ottenere l'indirizzo IP da linea di comando:
Realizzare un'improvvisazione musicale prototipando un ambiente esecutivo dedicato al controllo dei parametri di algoritmi di sintesi del suono e/o elaborazione di sound files attraverso una o più tra le tipologie di interfacce appena illustrate (HID, MIDI, OSC).
Un altro modo per modificare dinamicamente i parametri di un algoritmo di sintesi o elaborazione del suono è attraverso segnali di controllo ovvero dei segnali audio che non sono inviati all'ouput (dac) oppure agli ingressi di UGen modificatori ma che, debitamente riscalati sono mappati come valori di uno o più parametri.
Tipicamente se la frequenza delle UGen utilizzate per generare segnali di controllo è minore di 20Hz si definiscono Low Frequency Oscillator, se invece è maggiore possiamo parlare di modulazioni.
Tutte le UGen hanno un metodo .last( ) che riporta l'ultimo valore in uscita e che può essere utilizzato per questo scopo.
Se utilizziamo un loop possiamo ottenere questi valori alla rata di controllo che vogliamo (sottocampionamento).
Per poter utilizzare questi segnali li dobbiamo inviare a blackhole (come dac ma non invia i campioni da nessuna parte).
<<<"Last">>>;
TriOsc lfo => blackhole;
1 => lfo.freq; // Frequenza a 1 Hz
for(0 => int i; i < 10; i++) // Loop
{
<<< lfo.last(), "" >>>; // Sottocampionamento
1::ms => now;
}
Possiamo utilizzare l'output di qualsiasi UGen debitamente riscalato come segnale di controllo per qualsiasi parametro di altre UGens.
Se andiamo a modulare l'ampiezza di un segnale (valori compresi tra 0.0 e 1.0) con l'output di un oscillatore (segnale di controllo) la cui frequenza è inferiore a 20Hz possiamo parlare di tremolo, se superiore di modulazione di ammpiezza (AM).
<<<"Tremolo">>>;
SinOsc lfo => blackhole; // Segnale di controllo (o modulante)
SinOsc osc => dac; // Segnale audio
440 => osc.freq;
0.5 => osc.gain;
fun void gliss() // Frequenza della modulante
{
for(1 => int kfreq; kfreq < 80; kfreq++)
{
<<<kfreq>>>;
kfreq => lfo.freq;
500::ms => now;
}
}
spork ~ gliss();
while(true)
{
lfo.last() * 0.5 + 0.5 => osc.gain; // Riscalato
1::ms => now;
}
<<<"Vibrato e FM">>>;
SinOsc osc => dac; // Segnale audio (portante)
SinOsc lfo => blackhole; // Segnale di controllo (modulante)
440 => int oscFreq => osc.freq;
0.5 => osc.gain;
300 => int deviazione; // Deviazione della frequenza
fun void gliss() // Frequenza della modulante
{
for(1 => int kfreq; kfreq < 80; kfreq++)
{
<<<kfreq>>>;
kfreq => lfo.freq;
500::ms => now;
}
}
spork ~ gliss();
while(true)
{
oscFreq + (lfo.last() * deviazione) => osc.freq;
1::ms => now;
}
Come segnale modulante possiamo utilizzare la UGen Modulate che combina valori randomici e periodici per umanizzare la modulazione.
<<<"Modulate">>>;
Modulate lfo => blackhole; // Segnale di controllo
TriOsc osc => dac; // Segnale audio
440 => int oscFreq => osc.freq;
0.5 => float oscGain => osc.gain;
2 => lfo.vibratoRate; // Frequenza
10 => lfo.vibratoGain; // Deviazione
1 => lfo.randomGain;
while(true)
{
oscFreq + (oscFreq * lfo.last() / 40) => osc.freq;
1::ms => now;
}
In ChucK esiste un altro modo per modulare la frequenza di un oscillatore attraverso un segnale di controllo.
Gli oscillatori che sono sottoclassi di Osc hanno un parametro chiamato .sync che permette di accettare in ingresso un altro oscillatore.
I valori in ingresso possono essere considerati in diversi modi a seconda del numero che specifichiamo come argomento.:
<<<"sync()">>>;
SinOsc modulante => SinOsc portante => dac;
2 => portante.sync;
0.8 => portante.gain;
220 => portante.freq;
10 => modulante.gain; // Tra +/- 10 a partire dalla frequenza della portante (deviazione)
6 => modulante.freq; // Frequenza della modulante (segnale di controllo)
2::second => now;
Realizzare uno o più sound design evolutivi ovvero con i parametri del suono controllati da uno o più segnali di controllo che cambiano continuamente in frequenza e range.
L'obbiettivo è organizzare un suono che muta nel tempo autonomamente con continue variazioni e che nel contesto dell'improvvisazione possa assumere la stessa funzione musicale di un loop.
Sfruttare il concetto di modulazioni multiple ovvero modulazioni anche dei parametri dei segnali modulanti.
In questo paragrafo affronteremo le problematiche inerenti la prototipazione di uno strumento virtuale complesso nello stile dei sintetizzatori modulari analogici con il quale potremo realizzare improvvisazioni controllandone i parametri attraverso una delle tecniche già illustrate.
Dovendo formalizzare in modo più strutturato il codice introduciamo il paradigma della programmazione orientata agli oggetti (OOP) in Chuck.
Realizzando un'importante semplificazione questo paradigma si basa su tre tipi di oggetti.
Classi - pensiamole come degli stampini che il software utilizza per costruire diverse copie di un oggetto.
Istanze - pensiamole come i singoli oggetti costruiti attraverso le classi.
Metodi - pensiamoli come delle azioni che vogliamo far compiere alle istanze.
In ChucK tutte le parole che cominciano con una lettera maiuscola sono Classi.
Le UGen sono dei particolari tipi di classi ottimizzati per gestire segnali audio.
Quando scriviamo il codice seguente generiamo una nuova istanza (copia) dalla classe SinOsc e le assegnamo un nome (variabile).
// Classe Istanza
// Type Object
SinOsc osc => dac;
Le caratteristiche di SinOsc sono già programmate da altri ma volendo possiamo creare nuove classi (custom) che rispondono alle nostre esigenze.
All'interno delle classi possiamo definire uno o più metodi sotto forma di funzioni.
I metodi sono delle azioni che vogliamo la classe eseguirà ogni qualvolta le inviamo un messaggio corrispondente.
<<<"Classe">>>;
private class Stampa
{
fun void stampa() // Metodo (azione che deve svolgere la classe)
{
<<< "Ciao, sono il contenuto di Stampa" >>>;
}
}
Stampa ist; // Creo un'istanza
ist.stampa(); // Invio un messaggio a quell'istanza.
Oltre ai metodi possiamo definire delle variabili di istanza (istance data) che servono per inviare valori dall'esterno dell'istanza.
Se dichiarate prima delle funzioni sono pre-costruttori, accettano valori di default e vengono eseguite tutte le volte che chiamiamo un'istanza.
<<<"Variabili di istanza">>>;
private class Stampa
{
"ciao" => string testo; // Variabile di istanza (pre-costruttore)
fun void stampa() // Metodo (azione che deve svolgere la classe)
{
<<< testo >>>; // Richiama la variabile locale
}
}
Stampa ist; // Creo un'istanza
"Opperbacco!" => ist.testo; // Invio la variabile a quell'istanza.
ist.stampa(); // Invio un messaggio che invoca il metodo corrispondente.
Le variabili in questo caso sono locali e il loro nome vale solo all'interno della singola istanza (scope).
Se invece vogliamo inviare un valore a tutte le istanze contemporaneamente dobbiamo utilizzare la keyword static.
In questo caso inviandolo a una qualsiasi istanza viene ricevuto anche da quelle successive in assenza di ulteriori invii specifici.
<<<"Static keyword">>>;
private class Stampa
{
127 => static int numero; // static (comune a tutte le istanze)
fun void stampa()
{
<<< numero >>>;
}
}
Stampa ist_1; // Prima istanza
Stampa ist_2; // Seconda istanza
Stampa ist_3; // Terza istanza
1 => ist_1.numero; // Invio la proprietà ist_1.
ist_1.stampa(); // Chiedo di eseguire l'azione
2 => ist_2.numero; // Invio la variabile ist_2.
ist_2.stampa(); // Chiedo di eseguire l'azione
ist_3.stampa(); // Chiedo di eseguire l'azione senza specificare il valore della variabile
ist_1.stampa(); // E' ricevuta anche dalla prima...
Esistono due tipi di classi:
Ci può essere una sola classe pubblica per singolo file.
Esempio musicale.
Definiamo e salviamo in un file .ck una classe pubblica che ha solo due proprietà:
public class Accordo // Classe pubblica (condivisibile tra più files)
{
[0,3,7] @=> static int accordo[]; // Proprietà statiche
0 => static int posizione;
}
Definiamo e salviamo in un file .ck una classe pubblica che contiene un database di accordi sotto forma di rapporti intervallari.
public class Riserva // Classe pubblica (condivisibile tra più files)
{
[0,3,7] @=> static int minor[]; // Proprietò statiche
[0,4,7] @=> static int major[];
[0,5,7] @=> static int sus4[];
[0,2,7] @=> static int sus2[];
[0,4,7,10] @=> static int dom7[];
[0,4,7,11] @=> static int maj7[];
[0,3,7,10] @=> static int min7[];
}
Definiamo e salviamo tre shreds (tre sound design indipendenti) che utilizzano istanze delle classi pubbliche appena definite.
// Ciccio
Accordo a; // Istanza della classe pubblica
SinOsc osc => ADSR env => NRev rev => Pan2 pan => dac;
env => Delay delay => rev;
0.5::second => dur beat;
(1::ms, beat/4, 0, 1::ms) => env.set;
0.25 => osc.gain;
0.02 => rev.mix;
beat => delay.max;
beat / 4 => delay.delay;
0.25 => delay.gain;
delay => delay; // Feedback
[0,4,7] @=> int major[];
[0,3,7] @=> int minor[];
48 => int offset;
while(true){
for(0 => int i; i < 4; i++)
{
for(-1 => float j; j <= 1; 0.1 + j => j)
{
Math.random2f(-1,1) => pan.pan;
beat/Math.random2(2,16) => env.decayTime;
(Math.random2(0,4) * 12) + a.posizione => int position; // Utilizza Classe pubblica
Math.random2(0,2) => int note;
Std.mtof(a.accordo[note] + offset + position) => osc.freq; // Utilizza Classe pubblica
1 => env.keyOn;
beat / 2 => now;
1 => env.keyOff;
1::ms => now;
}
}
}
// Ciaccio
Accordo s; // Istanza della classe pubblica
SinOsc osc => LPF lpf => ADSR env => dac;
SqrOsc osc2 => lpf;
0.4 => osc.gain;
0.1 => osc2.gain;
400 => lpf.freq;
0.5::second => dur beat;
(1::ms, beat/4,0,1::ms) => env.set;
36 => int offset;
while(true)
{
beat / 4 => dur sedicesimi;
Std.mtof(s.accordo[0] + offset + s.posizione) => osc.freq; // Utilizza Classe pubblica
Std.mtof(s.accordo[0] + offset + s.posizione - 24) => osc2.freq; // Utilizza Classe pubblica
1 => env.keyOn;
sedicesimi * 3 => now; // ottavo puntato
1 => env.keyOff;
1::ms => now;
1 => env.keyOn;
sedicesimi * 3 => now; // ottavo puntato
1 => env.keyOn;
1 => env.keyOff;
1::ms => now:
sedicesimi * 2 => now; // ottavo
1 => env.keyOff;
1::ms => now;
}
// Ciuccio
Accordo t; // Istanza della classe pubblica
0.5::second => dur beat;
48 => int offset;
fun void playNote(int note, int position, dur duration)
{
// Freq di Saw
SinOsc lfo => SawOsc osc => LPF lpf => ADSR env => dac.left;
env => Delay delay => dac.right;
duration => delay.max;
duration / 4 => delay.delay;
delay => delay;
1 => delay.gain;
3 => lfo.gain;
6 => lfo.freq;
2 => osc.sync; // Segnale di controllo per frequenza
0.1 => osc.gain;
500 => lpf.freq;
Std.mtof(note + offset + position) => osc.freq;
(1::ms, duration * 2, 0, 1::ms) => env.set;
1 => env.keyOn;
duration => now;
1 => env.keyOff;
duration => now;
}
while(true)
{
for(0 => int i; i < t.accordo.size(); i++)
{
spork ~ playNote(t.accordo[i], t.posizione, beat); // Utilizza Classe pubblica
}
beat => now;
}
Definiamo e salviamo in un file uno shred partitura dove specifichiamo uno schema formale.
<<< "Score" >>>;
me.dir() + "ciccio.ck" => string ciccio; // Path files
me.dir() + "ciaccio.ck" => string ciaccio;
me.dir() + "ciuccio.ck" => string ciuccio;
Machine.add(ciccio) => int stopCiccio; // Invia i files alla VM e recuoera ID
Machine.add(ciaccio) => int stopCiaccio;
Machine.add(ciuccio) => int stopCiuccio;
Accordo cp; // Istanze delle classi
Riserva ch;
0.5::second => dur beat; // Durata Beat
beat * 4 => dur bar; // Durata Battuta
for(1 => int i; i < 5; i++)
{
<<<i>>>;
0 => cp.posizione; // Invia messaggio a tutte le Istanza (static)
ch.minor @=> cp.accordo; // Invia i parametri da una classe all'altra
bar => now; // Sintetizza una battuta
5 => cp.posizione;
ch.minor @=> cp.accordo;
bar => now;
7 => cp.posizione;
ch.sus4 @=> cp.accordo;
bar => now;
7 => cp.posizione;
ch.dom7 @=> cp.accordo;
bar => now;
}
<<<5>>>;
0 => cp.posizione;
ch.minor @=> cp.accordo;
bar * 8 => now;
Machine.remove(stopCiccio); // Rimuove gli shred
Machine.remove(stopCiaccio);
Machine.remove(stopCiuccio);
Possiamo ora lanciarli da linea di comando scivendoli uno dopo l'altro sulla stessa linea.
chuck riserva.ck accordo.ck score.ck
In alternativa definiamo e salviamo un file di esecuzione dove prima carichiamo nella Virtual Machine tutte le classi necessarie e poi la partitura che carica gli shred nel tempo.
<<<"Performance">>>;
Machine.add(me.dir() + "riserva.ck");
Machine.add(me.dir() + "accordo.ck");
Machine.add(me.dir() + "score.ck");
Nel paradigma della OOP un concetto importante è quello legato all'ereditarietà (inheritance) che consiste nell'ereditare, ed evenrualmente estendere o alterare le caratteristiche di una classe esistente
Ad esempio nel codice seguente:
<<<"TriOsc">>>;
TriOsc osc => dac;
220 => osc.freq;
0.2 => osc.gain;
[0,4,7,11] @=> int notes[];
48 => int offset;
while(true)
{
for(0 => int i; i < notes.size(); i++)
{
Std.mtof(notes[i] + offset) => osc.freq;
125::ms => now;
}
}
Se queste operazioni sono ricorrenti (ogni volta che utilizziamo TriOsc vogliamo definire le altezze sotto forma di intervalli) possiamo programmare una classe con al suo interno l'oscillatore TriOsc e tutte le operazioni.
Definiamo una nuova classe (MusicTri) che eredita attraverso la parola chiave extends tutte le proprietà da TriOsc ovvero tutti i metodi e variabili che possiamo trovare nel reference.
Alle proprietà ereditate possiamo aggiungerne altre (estendere) come in questo caso la conversione da midinote a Hz e un offset fisso.
TriOsc è la classe parent (superclasse) mentre MusicTri la classe child (sottoclasse).
<<<"MusicTri">>>;
// subclasse parent
public class MusicTri extends TriOsc
{
48 => int offset; // Variabile d'istanza con valore di default
fun void note(int intervallo) // Metodo aggiunto a quelli della Classe TriOsc
{
Std.mtof(intervallo + offset) => this.freq; // this == ogni singola istanza generata dalla classe
}
}
MusicTri osc => dac; // Nuova classe
220 => osc.freq; // Metodi ereditati
0.2 => osc.gain;
[0,4,7,11] @=> int notes[];
while(true)
{
for(0 => int i; i < notes.size(); i++)
{
notes[i] => osc.note; // Metodo aggiunto
125::ms => now;
}
}
In ChucK le UGens sono organizzate secondo una precisa gerarchia.
Tutte le UGens hanno le prime proprietà.
TriOsc in più ha quelle ereditate da Osc come freq e sync.
MusicTri in più ha note.
from IPython.display import HTML
HTML('<center><img src="media/class_tree.png" width="25%"></center>')
Due esempi più complessi.
Nel primo integriamo nella classe un vibrato.
<<<"MusicTri">>>;
public class MusicTri extends TriOsc
{
SinOsc vibrato => this; // LFO interno
2 => this.sync; // FM Synth
6 => vibrato.freq; // Frequenza del vibrato
5 => vibrato.gain; // Deviazione dalla frequenza portante
48 => int offset;
fun void note(int noteNumber)
{
Std.mtof(noteNumber + offset) => this.freq;
}
}
MusicTri osc => dac;
220 => osc.freq;
0.2 => osc.gain;
[0,4,7,11] @=> int notes[];
while(true)
{
-1 => osc.vibrato.op; // Possiamo applicare on e off (ereditati)
for(0 => int i; i < notes.size(); i++)
{
notes[i] => osc.note;
1::second / 4 => now;
}
1 => osc.vibrato.op;
for(0 => int i; i < notes.cap(); i++)
{
10 * i => osc.vibrato.freq; // Modifica la frequenza del vibrato dinamicamente
notes[i] => osc.note;
250::ms => now;
}
}
Nel secondo trasformiamo la tecnica dello slicing in una classe.
<<<"Slicing">>>;
public class Resega extends SndBuf2 // Ha tutte le proprietà di SndBuf2 alle quali aggiungiamo:
{
this => Envelope env => this;
// ------------------------------------------- Funzione tagli
function void taglio(int cue, dur dura)
{
this.samples() / 32 => int slice;
slice * cue => int onset;
this.pos(onset);
dura => now;
}
// ------------------------------------------- Funzione riscalato
function void riscalato(int cue, dur durata, int divisore)
{
for(0 => int i; i < divisore; i++)
{
taglio(cue, durata / divisore);
}
}
// ------------------------------------------- Funzione rampa gain
function void rampAmp(dur durata, int divisore)
{
durata / divisore => dur deltaT;
this.gain() - (this.gain() / 8) => float amp;
this.gain() / 8 => this.gain;
for(0 => int i; i < divisore; i++)
{
this.gain() + (amp / divisore) => this.gain;
deltaT => now;
}
}
// ------------------------------------------- Funzione Crescendo
function void cresc(int cue, dur durata, int divisore)
{
spork ~ rampAmp(durata, divisore);
riscalato(cue, durata, divisore);
}
}
Resega player => Pan2 pan => dac; // Nuova classe
1.4 => float MAIN_RATE;
second / (2 * MAIN_RATE) => dur beat; // Durata del beat in secondi riscalato sulla rata (1/2=0.5)
0.8 => player.gain;
me.dir() + "suoni/break1.wav" => player.read;
MAIN_RATE => player.rate;
while(true)
{
player.cresc(0, 2 * beat, 16);
player.cresc(2, 1 * beat, 16);
player.taglio(8, 2 * beat);
player.taglio(24, 2 * beat);
player.taglio(2, 1 * beat);
player.taglio(2, 1 * beat);
player.cresc(0, 1* beat, 4);
player.cresc(2, 0.5 * beat, 8);
player.riscalato(0, 0.5 * beat, 12);
player.taglio(8, 1.5 * beat);
player.taglio(8, 0.25 * beat);
player.taglio(8, 0.25 * beat);
}
from IPython.display import HTML
HTML('<center><img src="media/inout.png" width="28%"></center>')
Possiamo sfruttarla nella formalizzazione di schemi a blocchi grazie all'ereditarietà della classi.
from IPython.display import HTML
HTML('<center><img src="media/block.png" width="40%"></center>')
Definiamo come esempio una classe dedicata a una catena di processori di segnale monofonici.
<<<"Lowpass delay mono">>>;
class LPDelay extends Chugraph // Classe figlia di Chubgraph
{
inlet => LPF lpf => Delay delay => outlet; // Percorso del segnale...
lpf => outlet; // Side chain...
500 => lpf.freq; // Parametri dei processi
1::second => delay.max;
0.5 => delay.gain;
1::second / 3 => delay.delay;
delay => delay;
}
SawOsc osc => LPDelay graph => dac; // Utilizziamo la Classe in un'altra catena audio
0.5 => osc.gain;
220 => osc.freq;
[0,4,7,11] @=> int notes[];
48 => int offset;
while(true)
{
for(0 => int i; i < notes.cap(); i++)
{
Std.mtof(offset + notes[i]) => osc.freq;
1::second / 4 => now;
}
}
Processori stereofonici.
<<<"Lowpass delay stereo">>>;
class LPDelay extends Chugraph
{
inlet => LPF lpf => Delay delay => outlet; // Tolto side chain...
500 => lpf.freq;
1::second => delay.max;
0.5 => delay.gain;
1::second / 3 => delay.delay;
delay => delay;
}
SawOsc osc => LPDelay graph[2] => dac; // Array di Chugraph
0.5 => osc.gain;
220 => osc.freq;
1::second / 5 => graph[1].delay.delay; // Modifica solo canale destro...
[0,4,7,11] @=> int notes[];
48 => int offset;
while(true)
{
for(0 => int i; i < notes.cap(); i++)
{
Std.mtof(offset + notes[i]) => osc.freq;
1::second / 4 => now;
}
}
Progettiamo e programmiamo un algoritmo di sintesi e/o elaborazione del suono complesso e modificabile dinamicamente come ambiente di generazione sonora per l'improvvisazione.
Come esempio formalizziamo una replica digitale di alcuni componenti di un sintetizzatore modulare monofonico.
from IPython.display import HTML
HTML('<center><img src="media/modular_1.png" width="80%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/modular_2.png" width="80%"></center>')
Analizziamo le caratteristiche di alcuni componenti:
<<<"Modulare 1">>>;
public class SynthVoice extends Chugraph
{
// --------------------------------------------------- Catena audio e parametri di default
SawOsc osc => LPF lpf => ADSR env => outlet;
0.2 => osc.gain; // Saw gain
10 => float lpfreq; // Frequenza iniziale filtro (Init)
lpfreq => lpf.freq;
0 => float filtenv; // Range inviluppo filtro (Hz)
24 => int offset; // Offset MIDI
0.1 => float atk; // ADSR
0.1 => float dec;
0.5 => float sus;
0.3 => float rel;
100::ms => dur dura; // Durata suono
(atk*dura, dec*dura, sus, rel*dura) => env.set; // Setta i parametri dell'ADSR
// --------------------------------------------------- Inviluppo Ampiezza
fun void ampEnv()
{
1 => env.keyOn; // NoteOn
dura - env.releaseTime() => now; // Sintesi di attacco decadimento e sostegno
1 => env.keyOff; // NoteOff
env.releaseTime() => now; // Sintesi del rilascio
}
// --------------------------------------------------- Inviluppo Filtro
fun void filtEnv()
{
lpfreq => float startFreq; // Frequenza iniziale del Filtro
10::ms => now; // Aspetta a partire...
while((env.state() != 0 && env.value() == 0) == false) // ...quando l'inviluppo d'ampiezza e attivo...
{
(filtenv * env.value()) + startFreq => lpf.freq; // Genera una Rampa riscalata...
10::ms => now; // Sottocampionamento
}
}
// --------------------------------------------------- Trigger (midinote)
fun void trig(int nota)
{
Std.mtof(offset + nota) => osc.freq; // Note number
spork ~ ampEnv(); // Infiluppo ampiezza
spork ~ filtEnv(); // Infiluppo filtro
}
// --------------------------------------------------- Cutoff freq (fissa) tra 0 e 100
fun void cutoff(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
(amount / 100) * 5000 => lpfreq; // Riscala tra 0 e 5000
}
// --------------------------------------------------- Risonanza del Filtro (Q) tra 0 e 100
fun void ris(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
20 * (amount / 100) + 0.3 => lpf.Q; // Riscala tra 0.3 e 20.3
}
// --------------------------------------------------- Range inviluppo del Filtro tra 0 e 100
fun void cenv(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
5000 * (amount / 100) => filtenv; // Riscala tra 0 e 5000
}
}
[0,4,7,11,14,16,19,23,24] @=> int notes[];
while(true)
{
SynthVoice voice => dac; // Collega al DAC
//Math.random2(50,100) => voice.cutoff; // Cutoff fisso (0-100)
Math.random2(1,22) => voice.ris; // Risonanza (0-100)
Math.random2(1,50) => voice.cenv; // Cutoff con inviluppo (0-100)
notes[Math.random2(0, notes.cap()-1)] + 12 => voice.trig; // Pitch + note ON
200::ms => voice.dura; // Durata
150::ms => now; // Delta time
}
Aggiungiamo:
from IPython.display import HTML
HTML('<center><img src="media/modular_11.png" width="15%"></center>')
<<<"Modulare 2">>>;
public class SynthVoice extends Chugraph
{
// --------------------------------------------------- Catena audio e parametri di default
SawOsc saw1 => LPF lpf => ADSR env => Dyno limiter => outlet;
SawOsc saw2 => lpf;
TriOsc tri1, tri2;
SqrOsc sqr1, sqr2;
0.2 => saw1.gain => saw2.gain; // Saw gain
0.2 => tri1.gain => tri2.gain; // Tri gain
0.2 => sqr1.gain => sqr2.gain; // Sqr gain
10 => float lpfreq;
lpfreq => lpf.freq;
0 => float filtenv;
0::ms => limiter.attackTime; // Limiter
0.8 => limiter.thresh;
24 => int offset;
1 => float osc2Detune; // Detuning factor
0 => int oscOffset; // Offset tra oscillatori (rapporto frequenziale)
0.1 => float atk;
0.1 => float dec;
0.5 => float sus;
0.3 => float rel;
100::ms => dur dura;
(atk*dura, dec*dura, sus, rel*dura) => env.set;
// --------------------------------------------------- Setta le frequenze
fun void setOsc1Freq(float freq)
{
freq => saw1.freq => tri1.freq => sqr1.freq; // Primi oscillatori
}
fun void setOsc2Freq(float freq)
{
freq => saw2.freq => tri2.freq => sqr2.freq; // Secondi oscillatori
}
// --------------------------------------------------- Scelta forme d'onda
fun void ondaOsc1(int tipo)
{
if(tipo == 0)
{
tri1 =< lpf; // Scollega tutto
saw1 =< lpf;
sqr1 =< lpf;
}
if(tipo == 1)
{
tri1 => lpf; // Collega
saw1 =< lpf; // Scollega
sqr1 =< lpf;
}
if(tipo == 2)
{
tri1 =< lpf;
saw1 => lpf;
sqr1 =< lpf;
}
if(tipo == 3)
{
tri1 =< lpf;
saw1 =< lpf;
sqr1 => lpf;
}
}
fun void ondaOsc2(int tipo)
{
if(tipo == 0)
{
tri2 =< lpf;
saw2 =< lpf;
sqr2 =< lpf;
}
if(tipo == 1)
{
tri2 => lpf;
saw2 =< lpf;
sqr2 =< lpf;
}
if(tipo == 2)
{
tri2 =< lpf;
saw2 => lpf;
sqr2 =< lpf;
}
if(tipo == 3)
{
tri2 =< lpf;
saw2 =< lpf;
sqr2 => lpf;
}
}
// --------------------------------------------------- Inviluppo Ampiezza
fun void ampEnv()
{
1 => env.keyOn;
dura - env.releaseTime() => now;
1 => env.keyOff;
env.releaseTime() => now;
}
// --------------------------------------------------- Inviluppo Filtro
fun void filtEnv()
{
lpfreq => float startFreq;
10::ms => now;
while((env.state() != 0 && env.value() == 0) == false)
{
(filtenv * env.value()) + startFreq => lpf.freq;
10::ms => now;
}
}
// --------------------------------------------------- Trigger (midinote)
fun void trig(int nota)
{
Std.mtof(offset + nota) => setOsc1Freq;
Std.mtof(offset + nota + oscOffset) - osc2Detune => setOsc2Freq; // Offset + detuning
spork ~ ampEnv();
spork ~ filtEnv();
}
// --------------------------------------------------- Cutoff freq (fissa) tra 0 e 100
fun void cutoff(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
(amount / 100) * 5000 => lpfreq;
}
// --------------------------------------------------- Risonanza del Filtro (Q) tra 0 e 100
fun void ris(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
20 * (amount / 100) + 0.3 => lpf.Q;
}
// --------------------------------------------------- Range inviluppo del Filtro tra 0 e 100
fun void cenv(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
5000 * (amount / 100) => filtenv;
}
// --------------------------------------------------- Detuning (AGGIUNTA)
fun void detune(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) {0 => amount;}
5 * (amount / 100) => osc2Detune; // Riscala tra 0 e 5
}
}
[0,0,12,10] @=> int notes[];
SynthVoice voice => dac; // Monofonico
7 => voice.oscOffset; // Offset frequenziale
10 => voice.detune; // Detuning factor (0-100)
while(true)
{
Math.random2(1,3) => int nextOsc; // Forma onda random
nextOsc => voice.ondaOsc1;
nextOsc => voice.ondaOsc2;
//Math.random2(50,100) => voice.cutoff; // Cutoff fisso (0-100)
Math.random2(1,22) => voice.ris; // Risonanza (0-100)
Math.random2(1,50) => voice.cenv; // Cutoff con inviluppo (0-100)
notes[Math.random2(0, notes.cap()-1)] + 12 => voice.trig; // Pitch + note ON
120::ms => voice.dura; // Durata
120::ms => now; // Delta time
}
Aggiungiamo un LFO con possibilità di selezionare la forma d'onda della modulante e di essere impiegato come:
<<<"Modulare 3">>>;
public class SynthVoice extends Chugraph
{
// --------------------------------------------------- Catena audio e parametri di default
SawOsc saw1 => LPF lpf => ADSR env => Dyno limiter => outlet;
SawOsc saw2 => lpf;
TriOsc tri1, tri2;
SqrOsc sqr1, sqr2;
// --------------------------------------------------- LFO
SinOsc sinLfo; // LFO Sinusoidale
SawOsc sawLfo; // LFO Saw
SqrOsc sqrLfo; // LFO Quadra
sinLfo => Gain pitchLfo => blackhole; // Mandata (gain) in output
sinLfo => Gain filterLfo => blackhole;
fun void lfoFreq(float freq)
{
freq => sinLfo.freq => sawLfo.freq => sqrLfo.freq;
}
6.0 => lfoFreq;
0 => pitchLfo.gain;
0 => filterLfo.gain;
// Vibrato o FM tramite .sync
2 => saw1.sync => saw2.sync => tri1.sync => tri2.sync => sqr1.sync => sqr2.sync;
pitchLfo => saw1;
pitchLfo => saw2;
pitchLfo => tri1;
pitchLfo => tri2;
pitchLfo => sqr1;
pitchLfo => sqr2;
// ---------------------------------------------------
0.2 => saw1.gain => saw2.gain;
0.2 => tri1.gain => tri2.gain;
0.2 => sqr1.gain => sqr2.gain;
10 => float lpfreq;
lpfreq => lpf.freq;
0 => float filtenv;
0::ms => limiter.attackTime;
0.8 => limiter.thresh;
24 => int offset;
1 => float osc2Detune;
0 => int oscOffset;
0.1 => float atk;
0.1 => float dec;
0.5 => float sus;
0.3 => float rel;
100::ms => dur dura;
(atk*dura, dec*dura, sus, rel*dura) => env.set;
// --------------------------------------------------- Setta le frequenze
fun void setOsc1Freq(float freq)
{
freq => saw1.freq => tri1.freq => sqr1.freq;
}
fun void setOsc2Freq(float freq)
{
freq => saw2.freq => tri2.freq => sqr2.freq;
}
// --------------------------------------------------- Scelta forme d'onda Oscillatori
fun void ondaOsc1(int tipo)
{
if(tipo == 0)
{
tri1 =< lpf;
saw1 =< lpf;
sqr1 =< lpf;
}
if(tipo == 1)
{
tri1 => lpf;
saw1 =< lpf;
sqr1 =< lpf;
}
if(tipo == 2)
{
tri1 =< lpf;
saw1 => lpf;
sqr1 =< lpf;
}
if(tipo == 3)
{
tri1 =< lpf;
saw1 =< lpf;
sqr1 => lpf;
}
}
fun void ondaOsc2(int tipo)
{
if(tipo == 0)
{
tri2 =< lpf;
saw2 =< lpf;
sqr2 =< lpf;
}
if(tipo == 1)
{
tri2 => lpf;
saw2 =< lpf;
sqr2 =< lpf;
}
if(tipo == 2)
{
tri2 =< lpf;
saw2 => lpf;
sqr2 =< lpf;
}
if(tipo == 3)
{
tri2 =< lpf;
saw2 =< lpf;
sqr2 => lpf;
}
}
// --------------------------------------------------- Scelta forme d'onda LFO (AGGIUNTO)
fun void ondaLfo(int tipo)
{
if(tipo == 0)
{
sinLfo =< filterLfo;
sinLfo =< pitchLfo;
sawLfo =< filterLfo;
sawLfo =< pitchLfo;
sqrLfo =< filterLfo;
sqrLfo =< pitchLfo;
}
if(tipo == 1)
{
sinLfo => filterLfo;
sinLfo => pitchLfo;
sawLfo =< filterLfo;
sawLfo =< pitchLfo;
sqrLfo =< filterLfo;
sqrLfo =< pitchLfo;
}
if(tipo == 2)
{
sinLfo =< filterLfo;
sinLfo =< pitchLfo;
sawLfo => filterLfo;
sawLfo => pitchLfo;
sqrLfo =< filterLfo;
sqrLfo =< pitchLfo;
}
if(tipo == 3)
{
sinLfo =< filterLfo;
sinLfo =< pitchLfo;
sawLfo =< filterLfo;
sawLfo =< pitchLfo;
sqrLfo => filterLfo;
sqrLfo => pitchLfo;
}
}
// --------------------------------------------------- Inviluppo Ampiezza
fun void ampEnv()
{
1 => env.keyOn;
dura - env.releaseTime() => now;
1 => env.keyOff;
env.releaseTime() => now;
}
// --------------------------------------------------- Inviluppo Filtro
fun void filtEnv()
{
lpfreq => float startFreq;
10::ms => now;
while((env.state() != 0 && env.value() == 0) == false)
{
(filtenv * env.value()) + startFreq + filterLfo.last() => lpf.freq; // AGGIUNTO!
10::ms => now;
}
}
// --------------------------------------------------- Trigger (midinote)
fun void trig(int nota)
{
Std.mtof(offset + nota) => setOsc1Freq;
Std.mtof(offset + nota + oscOffset) - osc2Detune => setOsc2Freq;
spork ~ ampEnv();
spork ~ filtEnv();
}
// --------------------------------------------------- Cutoff freq (fissa) tra 0 e 100
fun void cutoff(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
(amount / 100) * 5000 => lpfreq;
}
// --------------------------------------------------- Risonanza del Filtro (Q) tra 0 e 100
fun void ris(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
20 * (amount / 100) + 0.3 => lpf.Q;
}
// --------------------------------------------------- Range inviluppo del Filtro tra 0 e 100
fun void cenv(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
5000 * (amount / 100) => filtenv;
}
// --------------------------------------------------- Detuning (AGGIUNTA)
fun void detune(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) {0 => amount;}
5 * (amount / 100) => osc2Detune;
}
// --------------------------------------------------- Vibrato / FM (AGGIUNTA)
fun void pitchMod(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 1) {0 => amount;}
84 * (amount / 100) => pitchLfo.gain;
}
// --------------------------------------------------- CutOff LFO (AGGIUNTA)
fun void cutoffMod(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 1) {0 => amount;}
500 * (amount / 100) => filterLfo.gain;
}
}
[0,0,12,10] @=> int notes[];
SynthVoice voice => dac; // Monofonico
10 => voice.cutoff; // Cutoff freq fissa (0-100)
7 => voice.oscOffset; // Offset frequenziale
10 => voice.detune; // Detuning factor (0-100)
1.5 => voice.pitchMod; // Deviazione vibrato (0-100)
50 => voice.cutoffMod; // Deviazione cutoff lfo (0-100)
1 => voice.ondaLfo; // Forma d'onda della modulante
5 => voice.lfoFreq; // Frequenza della modulante
while(true)
{
Math.random2(1,3) => int nextOsc; // Forma onda random
nextOsc => voice.ondaOsc1;
nextOsc => voice.ondaOsc2;
Math.random2(5,10) => voice.lfoFreq; // Frequenza della modulante
//Math.random2(50,100) => voice.cutoff; // Cutoff fisso (0-100)
Math.random2(1,22) => voice.ris; // Risonanza (0-100)
Math.random2(1,50) => voice.cenv; // Cutoff con inviluppo (0-100)
notes[Math.random2(0, notes.cap()-1)] + 12 => voice.trig; // Pitch + note ON
120::ms => voice.dura; // Durata
120::ms => now; // Delta time
}
Infine aggiungiamo:
from IPython.display import HTML
HTML('<div style = "float:left"><img src="media/noise.png" width="50%"></div>'
'<div style = "float:right"><img src="media/rev.png" width="50%"></div>')
<<<"Modulare 4">>>;
public class SynthVoice extends Chugraph
{
// --------------------------------------------------- Catena audio e parametri di default
SawOsc saw1 => LPF lpf => ADSR env => Dyno limiter => NRev rev => outlet;
SawOsc saw2 => lpf;
Noise noiz => lpf; // Noise
TriOsc tri1, tri2;
SqrOsc sqr1, sqr2;
// --------------------------------------------------- LFO
SinOsc sinLfo;
SawOsc sawLfo;
SqrOsc sqrLfo;
sinLfo => Gain pitchLfo => blackhole;
sinLfo => Gain filterLfo => blackhole;
fun void lfoFreq(float freq)
{
freq => sinLfo.freq => sawLfo.freq => sqrLfo.freq;
}
6.0 => lfoFreq;
0 => pitchLfo.gain;
0 => filterLfo.gain;
// Vibrato o FM...
2 => saw1.sync => saw2.sync => tri1.sync => tri2.sync => sqr1.sync => sqr2.sync;
pitchLfo => saw1;
pitchLfo => saw2;
pitchLfo => tri1;
pitchLfo => tri2;
pitchLfo => sqr1;
pitchLfo => sqr2;
// ---------------------------------------------------
0.2 => saw1.gain => saw2.gain;
0.2 => tri1.gain => tri2.gain;
0.2 => sqr1.gain => sqr2.gain;
0 => noiz.gain; // Noise gain
10 => float lpfreq;
lpfreq => lpf.freq;
0 => float filtenv;
0::ms => limiter.attackTime;
0.8 => limiter.thresh;
24 => int offset;
1 => float osc2Detune;
0 => int oscOffset;
0.1 => float atk;
0.1 => float dec;
0.5 => float sus;
0.3 => float rel;
100::ms => dur dura;
(atk*dura, dec*dura, sus, rel*dura) => env.set;
// --------------------------------------------------- Setta le frequenze
fun void setOsc1Freq(float freq)
{
freq => saw1.freq => tri1.freq => sqr1.freq;
}
fun void setOsc2Freq(float freq)
{
freq => saw2.freq => tri2.freq => sqr2.freq;
}
// --------------------------------------------------- Scelta forme d'onda Oscillatori
fun void ondaOsc1(int tipo)
{
if(tipo == 0)
{
tri1 =< lpf;
saw1 =< lpf;
sqr1 =< lpf;
}
if(tipo == 1)
{
tri1 => lpf;
saw1 =< lpf;
sqr1 =< lpf;
}
if(tipo == 2)
{
tri1 =< lpf;
saw1 => lpf;
sqr1 =< lpf;
}
if(tipo == 3)
{
tri1 =< lpf;
saw1 =< lpf;
sqr1 => lpf;
}
}
fun void ondaOsc2(int tipo)
{
if(tipo == 0)
{
tri2 =< lpf;
saw2 =< lpf;
sqr2 =< lpf;
}
if(tipo == 1)
{
tri2 => lpf;
saw2 =< lpf;
sqr2 =< lpf;
}
if(tipo == 2)
{
tri2 =< lpf;
saw2 => lpf;
sqr2 =< lpf;
}
if(tipo == 3)
{
tri2 =< lpf;
saw2 =< lpf;
sqr2 => lpf;
}
}
// --------------------------------------------------- Scelta forme d'onda LFO
fun void ondaLfo(int tipo)
{
if(tipo == 0)
{
sinLfo =< filterLfo;
sinLfo =< pitchLfo;
sawLfo =< filterLfo;
sawLfo =< pitchLfo;
sqrLfo =< filterLfo;
sqrLfo =< pitchLfo;
}
if(tipo == 1)
{
sinLfo => filterLfo;
sinLfo => pitchLfo;
sawLfo =< filterLfo;
sawLfo =< pitchLfo;
sqrLfo =< filterLfo;
sqrLfo =< pitchLfo;
}
if(tipo == 2)
{
sinLfo =< filterLfo;
sinLfo =< pitchLfo;
sawLfo => filterLfo;
sawLfo => pitchLfo;
sqrLfo =< filterLfo;
sqrLfo =< pitchLfo;
}
if(tipo == 3)
{
sinLfo =< filterLfo;
sinLfo =< pitchLfo;
sawLfo =< filterLfo;
sawLfo =< pitchLfo;
sqrLfo => filterLfo;
sqrLfo => pitchLfo;
}
}
// --------------------------------------------------- Inviluppo Ampiezza
fun void ampEnv()
{
1 => env.keyOn;
dura - env.releaseTime() => now;
1 => env.keyOff;
env.releaseTime() => now;
}
// --------------------------------------------------- Inviluppo Filtro
fun void filtEnv()
{
lpfreq => float startFreq;
10::ms => now;
while((env.state() != 0 && env.value() == 0) == false)
{
(filtenv * env.value()) + startFreq + filterLfo.last() => lpf.freq;
10::ms => now;
}
}
// --------------------------------------------------- Trigger (midinote)
fun void trig(int nota)
{
Std.mtof(offset + nota) => setOsc1Freq;
Std.mtof(offset + nota + oscOffset) - osc2Detune => setOsc2Freq;
spork ~ ampEnv();
spork ~ filtEnv();
}
// --------------------------------------------------- Cutoff freq (fissa) tra 0 e 100
fun void cutoff(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
(amount / 100) * 5000 => lpfreq;
}
// --------------------------------------------------- Risonanza del Filtro (Q) tra 0 e 100
fun void ris(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
20 * (amount / 100) + 0.3 => lpf.Q;
}
// --------------------------------------------------- Range inviluppo del Filtro tra 0 e 100
fun void cenv(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) { 0 => amount;}
5000 * (amount / 100) => filtenv;
}
// --------------------------------------------------- Detuning
fun void detune(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 0) {0 => amount;}
5 * (amount / 100) => osc2Detune;
}
// --------------------------------------------------- Vibrato / FM
fun void pitchMod(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 1) {0 => amount;}
84 * (amount / 100) => pitchLfo.gain;
}
// --------------------------------------------------- CutOff LFO
fun void cutoffMod(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 1) {0 => amount;}
500 * (amount / 100) => filterLfo.gain;
}
// --------------------------------------------------- Noise (AGGIUNTA)
fun void noise(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 1) {0 => amount;}
1.0 * (amount / 100) => noiz.gain;
}
// --------------------------------------------------- Riverbero (AGGIUNTA)
fun void reverb(float amount)
{
if(amount > 100) {100 => amount;}
if(amount < 1) {0 => amount;}
0.2 * (amount / 100) => rev.mix;
}
}
[0,0,12,10] @=> int notes[];
SynthVoice voice => dac; // Monofonico
10 => voice.cutoff; // Cutoff freq fissa (0-100)
7 => voice.oscOffset; // Offset frequenziale
10 => voice.detune; // Detuning factor (0-100)
1.5 => voice.pitchMod; // Deviazione vibrato (0-100)
50 => voice.cutoffMod; // Deviazione cutoff lfo (0-100)
1 => voice.ondaLfo; // Forma d'onda della modulante
5 => voice.lfoFreq; // Frequenza della modulante
15 => voice.noise; // Gain del nosie (0-100)
10 => voice.reverb; // Mix del riverbero (0-100)
while(true)
{
Math.random2(1,3) => int nextOsc; // Forma onda random
nextOsc => voice.ondaOsc1;
nextOsc => voice.ondaOsc2;
Math.random2(5,10) => voice.lfoFreq; // Frequenza della modulante
//Math.random2(50,100) => voice.cutoff; // Cutoff fisso (0-100)
Math.random2(1,22) => voice.ris; // Risonanza (0-100)
Math.random2(1,50) => voice.cenv; // Cutoff con inviluppo (0-100)
notes[Math.random2(0, notes.cap()-1)] + 12 => voice.trig; // Pitch + note ON
120::ms => voice.dura; // Durata
120::ms => now; // Delta time
}
Realizzare almeno un prototipo di strumento virtuale complesso definendolo in una classe oppure una famiglia di strumenti semplici (uno per classe) patchabili dinamicamente.
Esplorarne le potenzialità sonore e musicali nel corso di Sessioni d'improvvisazione.