Home

Improvvisazione musicale informatica¶

Indice¶

  • Introduzione
  • Prerequisiti
  • Installazione
  • Ambienti operativi
    • miniAudicle
    • Linea di comando
    • On the fly
  • Configurazione audio
  • Salvare sound files
  • Codice Notebook
  • Risorse in rete

  • Sintassi
    • Commenti
    • Stringhe
    • Print
    • Variabili
    • Data type
    • Operazioni aritmetiche
    • Ordine esecuzione
    • Gestione del Tempo
    • Organizzazione del codice
    • Funzioni
    • Sessione 1
  • Sequenze elementari
    • Loop
      • Sessione 2
      • Logica e condizioni
      • Sessione 3
      • Standard library
      • Randomness
    • Sequencing
      • Arrays
      • Iterazione
      • Sessione 4
  • UGens
    • Generatori
    • Modificatori
      • Envelope
      • Adsr
      • Sessione 5
      • Delay
      • Riverbero
      • Panning
      • Pinpong delay
      • Pan 2
    • Sessione 6
  • Impro e Live Coding elementari
    • Sincronizzazione shreds
    • Sessione 7
  • Playback e sampling
    • Path
    • Playback
    • Windowing
    • Looping
    • Trasposizione
    • Direzione
    • Sessione 8
  • Sampler
  • Sporkling e polifonia
    • Filtri
    • Rampe e sporkling parametri
  • Processori di dinamica
    • Dyno
    • UGen.op
    • Processori stereofonici
  • Live sampling (LiSa UGen)
    • ADC
    • Sessione 9
  • Esempi
    • Sintesi (sound design)
    • Segmentazione e pattern
    • Unchuck operator
    • Cambio parametri
    • Ribattuti
    • Crescendo relativi

  • Scoring
    • DAW dinamica
    • Sessione 10
    • Coll style
      • Leggere
      • Sequenze
      • Partitura
      • Scrivere
      • Memorizzare
    • Sessione 11
  • Events
  • HID
    • Tastiera
    • Memorizzare sequenze
    • Mouse
  • MIDI
    • Ricevere
    • Inviare
  • OSC
    • Ricevere
    • Inviare
    • Test
  • Sessione 12
  • Segnali di controllo e modulazioni
    • Tremolo e AM
    • Vibrato e FM
    • .sync()
    • Sessione 13

  • Liuteria virtuale
    • Classi istanze e metodi
  • Subclassi ed ereditarietà
    • Gerarchia (Class tree)
    • Processori di segnale e Chugraphs
  • Esempio modulare
  • Sessione 14

Introduzione Indice

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.

Prerequisiti Indice

  • Conoscenza degli elementi comuni ai diversi linguaggi di programmazione come tipi di data, variabili e funzioni.
  • Conoscenza delle principali tecniche di sintesi, analisi ed elaborazione del suono.
  • Conoscenza dei principali protocolli di comunicazione dati per l'audio MIDI e OSC.

Installazione Indice

Scarichiamo ed installiamo:

  • Chuck e miniAudicle
  • VScode

Ambienti operativi Indice

Possiamo programmare ed eseguire ChucK in due possibili modi:

  1. Con miniAudicle - IDE dedicata che si scarica con l'installazione del software.
  2. Da linea di comando - Terminale (Mac) o Prompt dei comandi (Win) oppure dal Terminale interno a VScode.

miniAudicle Indice

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.

In [1]:
from IPython.display import HTML
HTML('<center><img src="media/editor.png" width="50%"></center>')
Out[1]:

Virtual Machine Manager - Il motore audio di ChucK.

Simile al Server di SuperCollider e agli oggetti audio di Max.

In [2]:
from IPython.display import HTML
HTML('<center><img src="media/virtual.png" width="40%"></center>')
Out[2]:

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.

In [3]:
from IPython.display import HTML
HTML('<center><img src="media/conso.png" width="50%"></center>')
Out[3]:

Per realizzare qualsiasi cosa come ad esempio generare suono:

  1. Scriviamo del codice nell'editor. (Copia e incolla dalla cella)
In [ ]:
<<<"test">>>; 

SinOsc osc => dac;

440 => osc.freq;
0.5 => osc.gain;

1::second => now;
  1. Accendiamo la Virtual Machine.
    • Cliccando sull'interfaccia oppure
    • dal menù ChucK oppure
    • cmd .
  1. Inviamo il programma alla Virtual Machine cliccando su + (Add Shred)

    Nella Console Monitor leggiamo:

[chuck](VM): sporking incoming shred: 1 (Untitled)...
"test" : (string)
  1. Salviamo il file di testo.

In miniAudicle i singoli files di testo si chiamano shred e hanno i seguenti comandi "on the fly":

  • Add shred: carica il file sulla Virtual Machine eseguendo i comandi scritti nello shred.
  • Replace Shred: Sostituisce il file precedente con quello modificato.
  • Remove Shred: Cancella lo shred speficico dalla Virtual Machine.
  • Remove Last Shred: Cancella l'ultimo shred aggiunto dalla Virtual Machine anche se non attivo.
  • Clear Virtual Machine: Cancella tutti gli shred dalla Virtual Machine.

In miniAudicle possiamo realizzare delle semplici GUI chiamate MAUI.

In [ ]:
<<<"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.

Linea di comando Indice

In Mac OS apriamo un Terminale.

In Windows apriamo un Prompt dei comandi (Command Prompt).

In [4]:
from IPython.display import HTML
HTML('<center><img src="media/terminale.png" width="60%"></center>')
Out[4]:

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.

In [ ]:
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.

In [ ]:
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.

In [ ]:
chuck 00_test.ck 01_test.ck 

Un altro comando utile per recuperare indirizzo IP privato.

In [ ]:
// Mac OS

ipconfig getifaddr en0   // se Wi-Fi
ipconfig getifaddr en1   // se Cablato

// Windows

ipconfig

Alcune abbreviazioni da tastiera.

  • Pulire il Terminale: cmd + k
  • Interrompere un processo: ctrl + c.
  • Aprire un terminale diviso: cmd + ù.

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.

  1. Lanciamo VSCode.
  2. Creiamo un nuovo file di testo ( File --> Nuovo file di testo ).
  3. Copiamo il codice seguente.
In [ ]:
SinOsc sig => dac;

440 => sig.freq;
0.5 => sig.gain;

1::second => now;
  1. Salviamo il file come 'nome.ck' in una nuova cartella chiamata Prova.
  2. Apriamo un Terminale (Terminale --> Nuovo terminale).
  3. Cambiamo la directory dal terminale.
In [ ]:
cd '../Prova'
In [5]:
from IPython.display import HTML
HTML('<center><img src="media/vs_1.png" width="90%"></center>')
Out[5]:
  1. eseguiamo lo shred.
In [ ]:
chuck nome.ck
  1. Chiudiamo VSCode.

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.

In [6]:
from IPython.display import HTML
HTML('<center><img src="media/vs_2.png" width="90%"></center>')
Out[6]:

On the fly Indice

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+ù).

In [7]:
from IPython.display import HTML
HTML('<center><img src="media/vs_3.png" width="90%"></center>')
Out[7]:
  1. Accendiamo la Virtual Machine (equivale a cliccare su Start Virtual Machine su miniAudicle) scrivendo la seguente linea nel Terminale di sinistra.
In [ ]:
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.

  1. Nel Terminale di destra scriviamo ed eseguiamo linea per linea.
In [ ]:
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.

In [ ]:
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.

In [8]:
from IPython.display import HTML
HTML('<center><img src="media/vs_5.png" width="90%"></center>')
Out[8]:

Nella precedente immagine vediamo come lo Shred aggiunto alla VM sia stato indicizzato.

In [9]:
from IPython.display import HTML
HTML('<center><img src="media/vs_4.png" width="90%"></center>')
Out[9]:

Se avessimo voluto rimuovere lo shred dalla VM senza distruggerla avremmo potuto scrivere nel terminale di destra.

In [ ]:
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.

In [ ]:
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

Configurazione audio Indice

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.

In [10]:
from IPython.display import HTML
HTML('<center><img src="media/audio_set.png" width="50%"></center>')
Out[10]:

Da linea di comando dobbiamo specificare le command-line options (flag) desiderati prima del nome del file.

In [ ]:
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.

In [ ]:
// 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.

In [ ]:
chuck -h

Di seguito quelle più utilizzate.

In [ ]:
// [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

Salvare Sound Files Indice

Se vogliamo salvare l'audio generato in un file.

In [ ]:
<<<"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.

Eseguire codice Notebook Indice

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.

Risorse in rete Indice

  • Vari - Documentazione.
  • Vari - Linguaggio.
  • Vari - Unit Generators.
  • Vari - Unit Analyzer.

Sintassi Indice

Commenti Indice

Se vogliamo scrivere commenti dobbiamo farli precedere da un doppio slash.

In [ ]:
// Questo è un commento

Stringhe Indice

  • Una stringa è una sequenza di caratteri (char) ed è un tipo di data comune a molti linguaggi informatici.
  • Le stringhe sono utilizzate principalmente per visualizzare del testo.
In [ ]:
"Ciao"

Print Indice

Possiamo stampare diversi tipi di data nella console includendoli nei simboli <<< >>>.</br> Tutte le linee di codice devono terminare con un punto e virgola.

In [ ]:
<<< "Vattelapesca" >>>;

Best pratics: cominciare il nostro programma con una descrizione di ciò che andiamo a fare sotto forma di stringa.

In [ ]:
<<< "Sine" >>>;

SinOsc sig => dac;

440 => sig.freq;
0.5 => sig.gain;

1::second => now;

Variabili Indice

Possiamo assegnare ogni tipo di data a variabili attraverso il ChucK operator.

In [ ]:
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).

Data type Indice

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.

  • int : integer (signed)
  • float : floating point number (di default double-precision)
  • time : cronometro (tempo in campioni da un onset iniziale)
  • dur : durata
  • void : (no type)
  • vec3 : 3D vector (x, y, z) - floats
  • vec4 : 4D vector (x, y, z, w) - floats
  • complex : numeri complessi in forma rettangolare a + bi
  • polar : numeri complessi in forma polare
In [ ]:
"Andrea" => string nome;

<<< nome, "Vattelapesca" >>>;

Operazioni aritmetiche Indice

Possiamo effettuare le diverse operazioni aritmetiche assegnando i risultati a variabili.

In [ ]:
<<< "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 >>>; 

Ordine esecuzione Indice

La computazione avviene dall'alto verso il basso e da sinistra verso destra.

Le variabili possono essere riassegnate.

In [ ]:
<<< "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 >>>;

Gestione del Tempo Indice

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.

In [ ]:
<<< "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).

In [ ]:
<<< "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):

In [ ]:
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.

  • Inviare (ChucKing) una dur a now fa avanzare il tempo in ChucK di quella durata. Mentre il tempo avanza il suono è generato dal sistema mentre lo scheduling è sospeso (la computazione non passa alla linea successiva).
  • now non avanzerà mai se non lo chiediamo esplicitamente. E' come se le linee di codice prima di un now fossero racchiuse in un singolo istante di tempo.
In [ ]:
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

In [ ]:
<<< "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.

In [ ]:
<<< "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;

Organizzazione del codice Indice

  • Descrizione programma sotto forma di stringa.
  • Algoritmo di sintesi o elaborazione dei segnali.
  • Definizione delle variabili ed eventuale assegnazione di parametri di default.

  • Definiamo un suono o un evento.

    1. Settiamo i parametri.
    2. Inviamo una durata dur al tempo corrente now.
    3. Chuck esegue il suono o l'evento per la durata specificata dopo di che si interrompe.
  • Prepariamo un altro suono o un altro evento.
    1. Settiamo i parametri.
    2. Inviamo una durata dur al tempo corrente now.
    3. Chuck esegue il suono o l'evento per la durata specificata dopo di che si interrompe.
  • ...
In [ ]:
<<< "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" >>>;

Funzioni Indice

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.

In [ ]:
//  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.

In [ ]:
<<< "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);

Sessione 1 Indice

  • 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).

In [ ]:
<<< "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;

Sequenze elementari Indice

Nella gestione di improvvisazioni in tempo reale ci sono due strutture musicali fondamentali che in diverse modalità sono quasi sempre impiegate.

  • Loop
  • Sequenze di eventi

Loop Indice

In ChucK i loop del codice (cicli) possono coincidere con i loop musicali (anelli).

Per programmarli possiamo utilizzare due diverse strutture di controllo:

While

Continua a ripetere le istruzioni incluse nel loop fino a quando la condizione è vera (true).

In [ ]:
<<< "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.

In [ ]:
<<< "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;
}

For

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.

In [ ]:
<<< "For" >>>;

for(1 => int i; i <= 10; i++)  // test  (i++ = i + 1)
{                              // codice da ripetere
    <<< i >>>;
    100::ms => now;
}

Realizzazioni musicali

In [ ]:
<<< "Gliss" >>>;

SinOsc osc => dac;
0.5 => osc.gain;

for(1000 => int i; i > 0; i--)
{
    i => osc.freq;
    <<< i >>>;
    2::ms => now;
}
In [ ]:
<<< "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;
}

Sessione 2 Indice

  • 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.

In [ ]:
<<< "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);
}

Logica e condizioni Indice

Un altro costrutto sintattico informatico che possiamo impiegare per organizzare sequenze musicali nel tempo è l'operatore if.

In [ ]:
<<< "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.

In [ ]:
<<< "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.

In [ ]:
<<< "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;
}

Sessione 3 Indice

  • 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.

In [ ]:
<<< "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;
    }
}

Standard library Indice

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).

In [ ]:
<<< Std.mtof(60), 60 => Std.mtof  >>>;

Altezze e Ampiezze

In [ ]:
<<< "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

In [ ]:
<<< "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

Randomness Indice

Per la generazione di valori pseudocasuali possiamo utilizzare alcune funzioni della libreria Math.

In [ ]:
<<< "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.

In [ ]:
Math.random(23);

for(1 => int i; i < 4; i++)
{
<<< "random2:", Math.random2(60,72)>>>; 
}

Esempio musicale.

In [ ]:
<<< "Random" >>>;

SinOsc sig => dac;

while(true)
{
    sig.freq(Std.mtof(Math.random2(80,92)));
    sig.gain(Math.randomf());
    100::ms => now;
}

Sequencing Indice

Possiamo definire sequenze deterministiche di eventi per poi richiamarli singolarmente nel tempo.

Arrays Indice

Gli Array sono collezioni di dati assegnati ad una sola variabile.

Dichiariamo un Array.

In [ ]:
<<< "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.

In [ ]:
<<< "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;
}

Iterazione Indice

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).

In [ ]:
<<< "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.

In [ ]:
<<< "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.

In [ ]:
<<< "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.

In [ ]:
<<< "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;
        }
    }
}
In [ ]:
<<< "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;
        }
    }
}

Sessione 4 Indice

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.

UGens Indice

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.

In [11]:
from IPython.display import HTML
HTML('<center><img src="media/modular.png" width="40%"></center>')
Out[11]:

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.

In [ ]:
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:

  • Generatori - Generano un segnale audio digitale in uscita.
  • Elaboratori - Modificano un segnale audio digitale in ingresso restituendolo modificato in uscita.
  • Analizzatori - Analizzano una o più caratteristiche di un segnale audio digitale in ingresso.

A questo link possiamo trovare la ducumentazione delle UGens e alcuni codici di esempio.

Per il momento occupiamoci solo delle prime due tipologie.

Generatori Indice

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.

In [ ]:
<<< "Oscillatori" >>>;

SinOsc sig => dac;

587 => sig.freq;
0.5 => sig.gain;

1::second => now;

In questo esempio abbiamo utilizzato un oscillatore sinusoidale (SinOsc) collegato al dac e nei prossimi paragrafi ne impiegheremo altri ma possiamo esplorare quelli a disposizione nel reference di Chuck per sperimentare le soluzioni musicali più appropriate al singolo oggetto.

Modificatori Indice

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.

Envelope Indice

La UGen Envelope genera un inviluppo trapezoidale o triangolare.

I principali parametri di controllo sono:

  • il tempo di fade.
  • un livello (target) da raggiungere.

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.

In [ ]:
<<< "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.

In [ ]:
<<< "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...
}

ADSR Indice

La Ugen ADSR genera un inviluppo d'ampiezza classico.

  • Attack (tempo - dur)
  • Decay (tempo - dur)
  • Sustain (livello - float)
  • Release (tempo - dur)

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() ).

  • 0 = Attacco.
  • 1 = Decadimento.
  • 2 = Sostegno.
  • 3 = Rilsacio.
  • 4 = Finito.

Come generatore utilizziamo TriOsc che genera una forma d'onda triangolare.

In [ ]:
<<< "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.

Sessione 5 Indice

Realizzare un sound design con le seguenti caratteristiche:

  • Scegliere uno o più generatori tra quelli che possimo trovare nella Reference delle UGen.
  • Studiamo i parametri che possiamo controllare sull'help file relativo.
  • Ricerchiamo sonorità che ci possono interessare applicando inviluppi e durate diverse ai suoni.

Delay Indice

La UGen Delay ritarda nel tempo il segnale in ingresso di un tempo definito.

I principali parametri di controllo sono:

  • Tempo massimo di ritardo che corrisponde alla quantità di memoria occupata dal buffer interno.
  • Tempo di ritardo che deve essere inferiore al valore del parametro precedente.

Come generatore utilizziamo SawOsc che genera una forma d'onda a dente di sega con duty cycle variabile.

In [ ]:
<<< "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.

In [ ]:
<<< "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.

In [ ]:
<<< "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;

Possiamo investigare le caratteristiche di questi altre UGens dedicate al Delay.

  • DelayA
  • DelayL
  • Echo

Riverbero Indice

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.

  • NRev.
  • JCRev.
  • PRCRev.

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.

In [ ]:
<<< "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

Panning Indice

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.

Pinpong Delay Indice

In [ ]:
<<< "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 Indice

La UGen Pan2 posiziona un segnale monofonico nello spazio stereofonico.

Possiamo definire la posizione con valori compresi tra -1.0 (sinistra) e +1.0 (destra).

Come generatore utilizziamo Impulse che genera un'treno di impulsi (non limitato nello spettro).

In [ ]:
<<< "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
}

Sessione 6 Indice

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

Impro e Live Coding elementari Indice

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:

  • Accendere o spegnere la Virtual Machine (VM).
  • Lanciare ogni singolo shred nella Virtual Machine.
  • Modificare ogni singolo shred e sostituirlo con la versione modificata nella Virtual Machine.
  • Rimuovere ogni singolo shred dalla Virtual Machine.
  • Rimuovere l'ultimo shred dalla Virtual Machine.
  • Eliminare tutti gli shreds dalla dalla Virtual Machine.

Queste operazioni possono essere effettuate in due ambienti informatici diversi:

  • miniAudicle
  • linea di comando

Sincronizzazione shreds Indice

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.

In [ ]:
.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).

Sessione 7 Indice

Realizzare una sessione di live coding generando più voci sovrapposte dallo shred nella cella seguente.

  • modificare dinamicamente i parametri nel corso della performance nell'esplorare diverse texture ritmico/melodiche.
  • aggiungere a piacere ulteriori elementi sonori o musicali.
In [ ]:
<<<"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();
}

Playback e sampling Indice

Per realizzare un playback o un qualche tipo di elaborazione di un soundfile dobbiamo:

  • definire il path dove è locato il soundfile.
  • caricare il soundfile in un buffer.
  • rileggere ad audiorate il contenuto del buffer.

Path Indice

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.

In [ ]:
<<< "Path" >>>;

me.dir() + "suoni/guitar.wav" => string filename;

<<< filename >>>;

Playback Indice

Possiamo realizzare il playback di un sounfile con la UGen SndBuf nel caso si files monofonici e con SndBuf2 per files stereofonici.

I principali parametri di controllo sono:

  • Posizione iniziale
  • Durata
  • Velocità e direzione di lettura
In [ ]:
<<<"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.

In [ ]:
<<<"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);

Windowing Indice

Quanto appena illustrato può essere sfruttato musicalmente realizzando il playback di frammenti del soundfile.

Dobbiamo stabilire:

  • posizione iniziale in secondi da convertire in samples.
  • durata dell'inviluppo (window) in secondi.
In [ ]:
<<<"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);
}

Looping Indice

Se vogliamo realizzare un loop di tutto il soundfile possiamo invocare .loop() sull'istanza di SndBuf.

In questo caso non possiamo applicare nessun inviluppo.

In [ ]:
<<<"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.

In [ ]:
<<<"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;
}

Trasposizione Indice

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.

In [ ]:
<<<"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;
}

Direzione Indice

Se vogliamo modificare la direzione di lettura basta indicare rate negative.

In [ ]:
<<<"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;
}

Sessione 8 Indice

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:

  • Modifica e sostituzione dello stesso shred
  • Replicazione e modifica del contenuto dello stesso shred (polifonia)

Le caratteristiche musicali devono dipendere dalla morfologia sonora del file scelto.

Sampler Indice

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.

In [ ]:
<<<"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.

In [ ]:
<<<"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);
}

Sporkling e polifonia Indice

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:

  • definire una o più funzioni contenenti le sequenze di eventi.
  • sicronizzarle attraverso il porcesso di sporkling (termine dialettale derivato dal forkling informatico).

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).

In [ ]:
<<<"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)

Filtri Indice

Per i principali tipi di filtri utilizziamo le Basic Filter UGens.

  • LPF - Low Pass Filter
  • HPF - High Pass Filter
  • BPF - Band Pass Filter.
  • BRF - Band Reject Filter.
  • ResonZ - Resonant Filter.

Sono tutti filtri Butterworth del 2° ordine ad eccezione di ResonZ.

Tutti accettano come parametri di controllo.

  • Frequenza di taglio o ccentrale a seconda del tipo di filtro.
  • Q come fattore di risonanza e non quality factor.
In [ ]:
<<<"Filtri">>>;

Noise osc => LPF filt => dac; 

0.2       => osc.gain;

1000      => filt.freq;
1         => filt.Q;     
1::second => now;

Rampe e sporkling parametri Indice

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:

  • del passo incrementale del valore.
  • del passo temporale (tempo delta) ovvero ogni quanto tempo avviene l'incremento del valore.
In [ ]:
<<<"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:

  • salita - se inizio < fine.
  • discesa - se inizio > fine.

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.

In [ ]:
<<<"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.

In [ ]:
<<<"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.

In [ ]:
<<<"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); 
}

Processori di dinamica Indice

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à.

  1. Riscalare le ampiezze di tutti i segnali atraverso un fattore di moltiplicazione.
  2. Inserire la UGen Gain nella catena audio prima del dac come volume master.
  3. Utilizzare un limiter ( Dyno ).

UGen Dyno Indice

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.

In [ ]:
<<<"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;
In [12]:
from IPython.display import HTML
HTML('<center><img src="media/clip_1.png" width="100%"></center>')
Out[12]:
In [13]:
from IPython.display import HTML
HTML('<center><img src="media/clip_2.png" width="100%"></center>')
Out[13]:

Se però inseriamo la UGen Dyno nella catena audio.

In [ ]:
<<<"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;
In [14]:
from IPython.display import HTML
HTML('<center><img src="media/clip_3.png" width="100%"></center>')
Out[14]:

Possiamo notare che:

  1. Non c'è clipping ( rosso ).
  2. L'ampiezza della prima nota non è 1.0 come specificato nello shred ma è ridotta.
  3. L'ampiezza generale è uniforme.
  4. All'inizio delle singole note c'è un fade out.

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.

In [ ]:
<<<"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;
In [15]:
from IPython.display import HTML
HTML('<center><img src="media/clip_4.png" width="100%"></center>')
Out[15]:

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.

In [16]:
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>')
Out[16]:

UGen.op Indice

Tutte le UGens che accettano suono in ingresso (modificatori) hanno una proprietà chiamata .op che modifica il modo in cui questo viene processato:

  • 1 - Processa il segnale in ingresso.
  • 0 - Produce solo 0 (mute).
  • -1 - Disattiva il processo facendo passare il segnale in ingresso (passthrough).
In [ ]:
<<<"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;
}
In [17]:
from IPython.display import HTML
HTML('<center><img src="media/pass.png" width="35%"></center>')
Out[17]:

Processori stereofonici Indice

Quasi tutte le UGens di ChucK hanno un solo canale (mono).

In [ ]:
<<<"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.

In [ ]:
<<<"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;
    }
}

Live sampling (LiSa UGen) Indice

Con la UGen LiSa e i suoi derivati multicanale possiamo registrare un segnale in ingresso e realizzare diversi tipi di elaborazione.

In [ ]:
<<<"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

In [ ]:
<<<"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.

In [ ]:
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.

ADC Indice

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.

In [ ]:
chuck --in:1 --out:2 --dac:3 --adc:4 prova.ck     
In [ ]:
<<<"adc">>>;

adc => NRev rev => Dyno dino => dac;

0.2 => rev.mix;
10::second => now;

Possiamo registrare in LiSa i segnali in ingresso dall'adc

In [ ]:
chuck --in:1 --out:2 --dac:3 --adc:4 prova.ck     
In [ ]:
<<<"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;
} 

Sessione 9 Indice

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.

Esempi Indice

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 (sound design) Indice

In [ ]:
<<< "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;
}

Segmentazione e pattern Indice

In [ ]:
<<< "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 operator Indice

In [ ]:
<<< "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);
}

Cambio parametri Indice

In [ ]:
<<< "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 Indice

In [ ]:
<<< "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 relativi Indice

In [ ]:
<<< "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);
}

Scoring Indice

Possiamo formalizzare e memorizzare parti di un brano in files di testo per poi importarli in uno shred secondo due strategie di programmazione.

  • shred - in questo caso i file(s) da importare hanno un'estensione '.ck' e sono shreds a tutti gli effetti che possono essere eseguiti anche in modo autonomo (senza importarli in un altro shred).
  • coll file - un file di testo generico ('.txt') contenente i valori dei parametri di controllo in un qualche formato di codifica temporale.

DAW dinamica Indice

La prima strategia ci consente ad esempio di realizzare una vera e propria DAW dinamica.

  • Definiamo una o più sequenze sonore o musicali ognuna in un proprio shred autonomo (child) come abbiamo fatto finora e pensiamo a ognuno di essi come se fosse un sound file da caricare nelle tracce di una DAW.
  • Salviamo gli shred in singoli files.
  • Montiamo il tutto attraverso un file partitura (parent) che include i comandi Machine per aggiungere, rimuovere o sostituire gli shreds nella virtual machine.
  • Eseguiamo solo lo shred parent.

Come esempio riscriviamo la sequenza precedente.

Primo file (kick.ck) - child.

In [ ]:
<<<"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.

In [ ]:
<<<"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.

In [ ]:
<<<"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:

  • Lavorare ed eventualmente modificare facilmente e in modo indipendente i singoli contenuti sonori che, a differenza dei sound files possono essere dinamici impiegando procedure non deterministiche.
  • Realizzare sistemi politemporali o poliritmici in quanto il tempo (beat) interno ai singoli shreads può essere diverso tra loro e da quello dello shread partitura.

Sessione 10 Indice

  1. Realizzare un sound design complesso il cui schema formale è gestito richiamando singoli shreds (files.ck - child) da una time line definita in uno shred partitura.
  1. Realizzare una sessione di live coding formalizzato richiamando dinamicamente materiali sonori e musicali precostituiti sotto forma di shred salvati in files. Possiamo:
    • Aggiungere, rimpiazzare o rimuovere gli shread da un file partitura in cui scrivere e cancellare di volta in volta il comando da eseguire.
    • Modificare dinamicamente il contenuto degli shred.

Coll style Indice

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.

Leggere Indice

Scriviamo con un qualsiasi editor di testo (VScode o altro) e salviamo il contenuto della cella seguente in un file con estensione nome.txt.

In [ ]:
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.

In [ ]:
<<<"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:

  • 1 :(int) se il file esiste
  • 0 :(int) se il file non esiste.
In [ ]:
<<<"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

In [ ]:
["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.

In [ ]:
<<<"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().

In [ ]:
<<<"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 >>>;
}

Sequenze Indice

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.

In [ ]:
0 12 5 7 -3 4 5 -1
In [ ]:
<<<"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.

In [ ]:
<<<"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.

In [ ]:
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
In [ ]:
<<<"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)
}

Partitura Indice

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.

In [ ]:
<<<"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.

  1. Stabiliamo delle regole e dei simboli come ad esempio rappresentare le pause con il simbolo R e i commenti con //.
In [ ]:
// -------- 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
  1. 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).

In [ ]:
// 012345 indici della stringa 
  "andrea" => string myString;

myString.find("dr");

// Riporta 2 ...
  1. Applichiamo questa strategia a un progetto.
In [ ]:
<<<"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

In [ ]:
<<<"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.

In [ ]:
// -------- 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

Scrivere Indice

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).

In [ ]:
<<<"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.

In [ ]:
<<<"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.

In [ ]:
<<<"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();

Memorizzare Indice

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.

In [ ]:
<<<"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.

In [ ]:
<<<"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:

  • Rieseguirlo in modo deterministico come illustrato nei parametri precedenti.
  • Modificare i singoli parametri a notro piacimento per poi rieseguirlo.

Ad esempio possiamo tagliare a mano la sequenza generata randomicamente tenendo pochi eventi per realizzare un loop ritmico melodico.

In [ ]:
<<<"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();

Sessione 11 Indice

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.

Events Indice

Un alternativa alle tecniche di scoring nella gestione di una partitura informatica è fornita dalla classe Event.

La strategia in questo caso consiste nel:

  • Definire uno o più eventi (insiemi o sequenze di controlli, sequenze di sintesi e/o elaborazione del suono, etc.) sotto forma di funzione con la classe event come primo argomento.
  • Richiamarli in sequenza uno dopo l'altro oppure tutti insieme attraverso un trigger collegando la classe event all'interno della funzione con now.
In [ ]:
<<<"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.

In [ ]:
<<<"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.

In [ ]:
<<<"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.

HID Indice

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.

  • in miniAudicle andiamo in Window -> Device Broswer e selezioniamo Human Interface Devices dal menù a tendina.
In [18]:
from IPython.display import HTML
HTML('<center><img src="media/hid.png" width="75%"></center>')
Out[18]:
  • da linea di comando digitiamo:
In [ ]:
chuck --probe
In [19]:
from IPython.display import HTML
HTML('<center><img src="media/hid2.png" width="75%"></center>')
Out[19]:

In entrambe i casi otteniamo un elenco indicizzato.

Tastiera Indice

Possiamo ricevere data da HID esterni come la tastiera del computer attraverso la classe Hid che è una sottoclasse di Event.

In [ ]:
<<<"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.

In [ ]:
<<<"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.

In [ ]:
<<<"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.

In [ ]:
<<<"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;
        }
    }
}

Memorizzare sequenze Indice

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.

  • z --> Ottava sotto
  • y --> Ottava sopra

  • da 1 a 8 --> suddivisioni del beat

  • c d e f g a b --> altezze
  • p --> pausa
  • enter --> Scrivi file
In [ ]:
<<<"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.

In [ ]:
<<<"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;
}

Mouse Indice

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à.

In [ ]:
<<<"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.

In [ ]:
<<<"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()  >>>;
}

MIDI Indice

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.

Ricevere Indice

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:

  • msg.data1 - number definisce che tipo di data arriva.
  • msg.data2 - a seconda del tipo di data contiene un valore (corpo del messaggio)
  • msg.data3 - a seconda del tipo di data contiene un eventuale secondo valore.

Ad esempio se riceve un messaggio di Note On:

  • msg.data1 - 144 (Type - noteOn)
  • msg.data2 - 67 (Note 0-127)
  • msg.data3 - 98 (Velocity 0-127)
In [ ]:
<<<"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:

  • Note On - 144
  • Note Off - 128
  • Control - 176
  • Pitch bend - 224
  • Aftertouch - 208

Esempio di strumento monofonico.

In [ ]:
<<<"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).

In [ ]:
<<<"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.

In [ ]:
<<<"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

Inviare Indice

Per inviare messaggi MIDI possiamo utilizzare la classe MidiOut.

  • Definiamo dove inviare i dati (.open(ID)).
  • Scriviamo data nel buffer (MidiMsg) secondo la sintassi già illustrata (aggiungiamo valori).
  • Inviamo il messaggio.
In [ ]:
<<<"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);  
}

OSC Indice

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.

  • TCP (Transmission Control Protocol)
    • Il Client invia un pacchetto di dati al Server
    • Il Server gli risponde che ha ricevuto tutto (come una "ricevuta di ritorno").
    • Quando il Client ha ricevuto la risposta invia un nuovo pacchetto.
    • Se il Client non riceve una risposta dal Server invia nuovamente il pacchetto.
    • Attraverso questo sistema i pacchetti di dati sono ordinati.
    • E' molto sicuro ma richiede tempo e in alcuni casi non è necessario.
  • UDP (User Data Protocol)
    • I pacchetti di dati sono più piccoli.
    • Il Server non invia alcuna risposta al Client.
    • L'ordine dei pacchetti non è garantito.
    • E' meno sicuro ma più veloce nella trasmissione.

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.

Ricevere Indice

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.

In [ ]:
<<<"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
    }
}

Inviare Indice

Per inviare messaggi OSC possiamo utilizzare OscOut.

  • Definiamo dove inviare i dati (destination) con un IP (stringa) e una porta (int).
  • Definiamo l'indirizzo al quale inviare i dati (stringa).
  • Aggiungiamo valori (argomenti) al buffer.
  • Inviamo il messaggio.
In [ ]:
<<<"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)

Test Indice

Testiamo i codici precedenti in locale.

  • In VScode apriamo due Terminali.
  • In quello di sinistra eseguiamo lo shred che riceve data OSC (Server).
  • In quello di destra eseguiamo lo shred che invia data OSC (Client).

Esempio musicale...

In [ ]:
<<<"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;
    }
}
In [ ]:
<<<"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:

  • Terminale MAC
    • ipconfig getifaddr en0 se Wi-Fi
    • ipconfig getifaddr en1 se Cablato
  • Prompt Windows
    • ipconfig

Sessione 12 Indice

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).

Segnali di controllo e modulazioni Indice

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).

In [ ]:
<<<"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.

Tremolo e AM Indice

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).

In [ ]:
<<<"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 Indice

In [ ]:
<<<"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.

In [ ]:
<<<"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;
}

.sync() Indice

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.:

  • 0 => frequenza
  • 1 => fase
  • 2 => FM - i valori in ingresso sono sommati alla frequenza dell'oscillatore.
In [ ]:
<<<"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;

Sessione 13 Indice

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.

Liuteria virtuale Indice

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.

Classi, istanze e metodi Indice

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).

In [ ]:
// 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.

In [ ]:
<<<"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.

In [ ]:
<<<"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.

In [ ]:
<<<"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:

  • private: significa che (in ChucK) possiamo utilizzarle solo all'interno del file (shred) dove sono programmate.
  • pubbliche: possiamo utilizzarle in diversi files (shreds) e per farlo dobbiamo eseguire contemporaneamante (sulla stessa linea del terminale) prima il file che contiene la classe e poi quelli che la utilizzano.

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à:

  • un Array di intervalli.
  • un valore di offset.
In [ ]:
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.

In [ ]:
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.

In [ ]:
// 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;
        }      
    }  
}
In [ ]:
// 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;
}
In [ ]:
// 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.

In [ ]:
<<< "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.

In [ ]:
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.

In [ ]:
<<<"Performance">>>;

Machine.add(me.dir() + "riserva.ck");  
Machine.add(me.dir() + "accordo.ck"); 
Machine.add(me.dir() + "score.ck"); 

Sottoclassi ed ereditarietà Indice

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:

  • definiamo le altezze come intervalli.
  • sommiamo a questi il valore di una nota perno.
  • convertiamo i valori da MIDI a Hertz in quanto la classe TriOsc accetta questa unità di misura.
In [ ]:
<<<"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).

In [ ]:
<<<"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;
    }   
}

Gerarchia (Class tree) Indice

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.

In [20]:
from IPython.display import HTML
HTML('<center><img src="media/class_tree.png" width="25%"></center>')
Out[20]:

Due esempi più complessi.

Nel primo integriamo nella classe un vibrato.

In [ ]:
<<<"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.

In [ ]:
<<<"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);
}

Processori di segnale e Chugraphs Indice

Se vogliamo progettare catene audio che prevedono processori di segnale possiamo utilizzare Chugraph che è una classe speciale che estende le funzionalità di ChucK ed è fornita di inlet e outlet per i segnali audio.

In [21]:
from IPython.display import HTML
HTML('<center><img src="media/inout.png" width="28%"></center>')
Out[21]:

Possiamo sfruttarla nella formalizzazione di schemi a blocchi grazie all'ereditarietà della classi.

In [22]:
from IPython.display import HTML
HTML('<center><img src="media/block.png" width="40%"></center>')
Out[22]:

Definiamo come esempio una classe dedicata a una catena di processori di segnale monofonici.

In [ ]:
<<<"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.

In [ ]:
<<<"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;
    }
}

Esempio modulare Indice

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.

In [23]:
from IPython.display import HTML
HTML('<center><img src="media/modular_1.png" width="80%"></center>')
Out[23]:
In [24]:
from IPython.display import HTML
HTML('<center><img src="media/modular_2.png" width="80%"></center>')
Out[24]:

Analizziamo le caratteristiche di alcuni componenti:

  • Forma d'onda fissa (Saw)
  • Low Pass Filter con frequenza di taglio dinamica controllata dall'inviluppo d'ampiezza.
  • ADSR - Ricordiamo che possiamo monitorare un inviluppo ADSR attraverso adsr.state():
  1. Attacco
  2. Decay
  3. Sustain
  4. Release
  5. Alla fine del release
  • Gli inviluppi sono triggerati in sync con ~spork.
  • Polifonico con dynamic voice allocation (crea uno shred child ad ogni nota per poi distruggerlo).
In [ ]:
<<<"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:

  • Due oscillatori con la possibilità di selezionare forme d'onda diverse.
  • Una relazione d'ottava fissa tra le loro frequenze e un detuning factor controllabile.
  • Un limiter.
In [25]:
from IPython.display import HTML
HTML('<center><img src="media/modular_11.png" width="15%"></center>')
Out[25]:
In [ ]:
<<<"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:

  • vibrato o FM mappandolo sulla frequenza degli oscillatori.
  • wa wa mappandolo sulla frequenza di taglio del filtro.
In [ ]:
<<<"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:

  • Un noise generator (filtrato assieme agli oscillatori).
  • Un riverbero
In [26]:
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>')
Out[26]:
In [ ]:
<<<"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
}

Sessione 14 Indice

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.