I’m really happy to share the video of the single from an EP I've been working on last year, which is now finally seeing the light, thanks to the Z-Axis label.
Thanks again to @tristanmcguire for coming up with this really nice piece of work!
In 2021 I released a new album as vdof where I got back to one of my initial love, electric bass. It’s the result of few years experimenting with ambient and drone music but, at same time a return to my roots (beats and bass).
I just recently released two albums I have been working on in the last months, raw material and for the present, and I decided to document the making of them (the theory and the research part apply to both works, but the technical details described below apply to for the present only).
Both these works are born from the need to reconnect with sound and explore its pure characteristics, free from musical structures, scores, visual feedback and any other sensory input other than hearing.
When working on them I tried to take the distance from my usual setup which includes laptop (mainly running ChucK and Pure Data) and few hardware sequencers/synths, that I usually control and heavily process in the digital domain (a sort of augmentation one could say).
In this case instead I wanted to play a single instrument, something simpler in its structure, something I had to learn how to play, something unfamiliar at first.
Also, I didn’t want to use the computer (as in a standard laptop), not even to record the work.
Thus, I ended up focusing on two phenomena: feedback and modulation.
I started working on a ChucK script I then ran on Raspberry Pi 3 and at the same time I bought a small second-hand Soundcraft mixer (before I had everything going straight into the audio interface).
In ChucK I implemented a unit generator based on a particular modulation technique, a pulse generator. I then used this as a main module in a slightly more complex unit generator which uses two pulse generators in an fm fashion, with also a feedback component in it.
I mapped a MIDI controller and began playing with this new sound generator.
On the mixer side, I started experimenting with some basic no input techniques which involved a Lexicon MPX550 multi effect.
I then combined the work done in ChucK with the analog experiments conducted with the mixer and ended up with a system that looked like this:
Raspberry Pi 3 running ChucK
Novation ZeRO SL MkII MIDI controller
Soundcraft Spirit Folio lite mixer
Lexicon MPX550
Tascam DR-05 portable recorder
I spent several days playing with the system, trying to understand its strengths and weaknesses, changing approaches, working on the mappings (what controls what and how), exploring new gestures, etc. .
In doing so I realized I wanted something a bit more homogenic as I felt as if I had two separate instruments in front of me (the Rasp Pi and the mixer itself), therefore I added another feedback component to the system, looping the main output from the mixer back to the RaspPi (which already used internal feedback).
This kind of closed the circle. Now all the parts in the system played an equal role in the overall architecture, and also an unstable component was added, allowing the instrument to surprise me with ever evolving textures as well as piercing sounds.
I had to revisit once again what I learned so far. A new approach was required, because now the player and the instrument had the same importance.
It was like the instrument had its own will that the player must respect and listen to, instead of trying to impose his/her own one.
I feel pretty satisfied with the system now. There are still things I want to add/change, and I hope I’ll do that as soon as possible.
Probably I will also spend few words about the artistic decisions behind these works, since they played quite an important role, but for now I’ll share few diagrams showing what described above.
The next step would be that of releasing the ChucK code as well.
r_cycle - creative coding with Launchpad and Pure Data
I’m really happy to announce that few weeks ago r_cycle, a project I’ve been working on in the last months, has been officially announced.
It’s an open source “middleware” written in Pure Data Vanilla, a library for creative coding with Launchpad.
More info here.
A big thanks goes to Paul Mansell, Tristan McGuire, Jerome Meunier, Dave Hodder and Peter Kirn.
Also, a huge thanks goes to all the open source communities, especially the Pure Data one.
A Pure Data patch that allows to easily receive MIDI messages from the ZeRO SLMkII MIDI controller, and also to control encoders, buttons and screens from the patch.
Spoiler alert, with the encoders is possible to use whatever range from 1 to N!!!!!
Per poter utilizzare questo programma serve avere un controller MIDI connesso al computer, o una virtual MIDI keyboard. Questo va connesso (o aperto nel caso di una tastiera virtuale), prima di far partire il programma.
Conoscenza base del protocollo MIDI (https://it.wikipedia.org/wiki/Musical_Instrument_Digital_Interface - http://www.nyu.edu/classes/bello/FMT_files/8_MIDIcomms.pdf – https://www.nyu.edu/classes/bello/FMT_files/9_MIDI_code.pdf).
Conoscenza base dei filtri classici (https://en.wikipedia.org/wiki/Audio_filter).
ANALISI DEL PROGRAMMA
In questo programma viene implementata una sezione filtri che include:
filtro passa basso
filtro passa banda
filtro passa alto
In ChucK questi corrispondono ai seguenti UGens1:
LPF
BPF
HPF
I quali hanno I seguenti metodi:
.freq() - per impostare il cutoff
.Q() - per impostare la risonanza
.set() - per impostare cutoff e risonanza contemporaneamente
All’inizio del programma questi UGens vengono dichiarati e inizializzati:
/-----------------------INFO------------------------- PREMI "Add Shred" per lanciare il programma e "Remove Shred" per term_inarlo. "Replace Shred" serve a far ripartire il programma. ----------------------------------------------------/ // RTcMix Lookup table: https://en.wikipedia.org/wiki/Real-time_Cmix Gen10 oscils[13]; Phasor phasor; // utilizzato per controllare le look-up table contentute in 'oscils' 0 => int selected_oscil; // oscillatore selezionato (0. sine, 1. square, 2. tri, 3. sawtooth) -1 => int previous_oscil; // variabile utilizzata nel ciclo while ["SINE", "SQUARE", "TRIANGLE", "SAWTOOTH"] @=> string waveform_name[]; // FILTRI LPF lowpass; BPF bandpass; HPF highpass; Step _c => Envelope cutoff_interp => blackhole; _c.next(1); 0.5 => float raw_cutoff; // valore assoluto del cutoff - range 0-1 float cutoff; float Q; set_cutoff( raw_cutoff ); set_Q(0); 0 => int filter_id; // id del filtro selezionato - utilizzato per connettere gli oscillatori solo al filtro in uso 0 => int previous_filter_id; // lancia 'cutoff_update()' su uno 'shread' (thread) parallelo spork ~ cutoff_update(); // Step step => Envelope line => phasor; // usa il low-pass filter di default Envelope env => lowpass => Gain master => dac; // interpola l'amplitude del synth per evitare glitch - miglioramento rispetto alla soluzione proposta fino al tutorial nr 8 Step m_step => Envelope m_env => master; master.op(3); // i segnali inviati a 'master' vengono moltiplicati tra di loro m_env.time( 0.005 ); // 5 msec m_env.target(0);
Si puo’ notare che questa sezione viene utilizzato un interpolatore lineare (stesso sistema utilizzato precedentemente per il portamento) per cambiare la frequenza di _cutoff _senza generare glitch2.
Vengono dichiarate una serie di variabili per la gestione dei parametri dei filtri, e poi una nuova _istruzione _e’ presente:
spork ~ cutoff_update();
spork e’ un’istruzione che permette di lanciare processi paralleli, generalmente chiamati threads, in ChucK chiamati shreads.
Questo testo non pretende in alcun modo di affrontare in modo esaustivo (e non) argomenti quali il multithreading, I quali verranno semplicemente discussi in riferimento all’utilizzo che ne viene fatto all’interno del programma.
Prima di spiegare cosa significa avere processi paralleli (in questo programma), bisogna dare uno sguardo alla funzione _cutoff_update() _utilizzata con l’operatore spork.
function void cutoff_update() { /* questa funzione viene lanciata su un thread parallelo (spork) utilizzata per interpolare il cutoff del filtro in modo da evitare 'glitch' */ float _f; while( true ) { cutoff_interp.last() => _f; // al posto di 'if( _f != cutoff )' - dato che sono due numeri float!!!!! // aggiorna il 'cutoff' solo quando e' necessario if( Math.fabs( _f - cutoff ) > 0.001 ) { lowpass.freq( _f ); bandpass.freq( _f ); highpass.freq( _f ); } 5::samp => now; } }
Questa funzione contiene un ciclo while infinito, esattamente come in tutti I tutorial fin’ora (questo incluso).
All’interno del ciclo infinito, l’ultimo valore dell’UGen Envelope cutoff_interp viene assegnato alla variabile __f _utilizzando il metodo .last() (che tutti gli UGen hanno).
Questo metodo restituisce il valore dell’ultimo sample generato dall’UGen.
Quando il valore di _f e’ diverso da quello di _cutoff _la frequenza di tutti I filtri viene aggiornata.
Sapendo che cutoff_interp e’ una rampa lineare (_Step => Envelope _– stesso sitema utilizzato per il portamento), si puo’ intuire che lo scopo di questa funzione e’ quello di campionare la rampa lineare (cutoff_update) ogni 5 samples ed utilizzare il valore campionato come cutoff per I filtri.
In termini pratici, al posto di impostare il cutoff immediatamente sul valore desiderato, si utilizza un interpolatore per far si che non ci siano cambi di valore troppo bruschi.
Tornando all’operatore spork, questo fa si che la funzione cutoff_update() venga eseguita in parallelo al programma.
In questo modo la sua esecuzione non interferisce con lo svolgimento del programma.
Si puo’ pensare a cutoff_update() come ad un programma autonomo _generato _dal programma principale (09-filteri.ck) e con cui questo puo’ interagire.
Alcuni esempi utili a proposito del _multishreding _in ChucK possono essere trovati qui: https://chuck.cs.princeton.edu/doc/examples/ e https://en.flossmanuals.net/chuck/_full/#concurrency-and-shreds.
Il programma prosegue come nel precedente tutorial (parte 8).
Il ciclo while ha semplicemente qualche piccola aggiunta:
// loop infinito while( true ) { // avanza ad ogni messaggio MIDI ricevuto m_in => now; while( m_in.recv( msg_in ) ) { if( msg_in.data1 == ( NOTE_ON + MIDI_CHANNEL ) && msg_in.data3 != 0 ) { msg_in.data2 => midi_note; set_oscils( midi_note ); // seleziona la wavetable in base alla nota ricevuta e l'oscillatore selezionato Std.mtof( midi_note ) => fr; // conversione da MIDI a Freq in Hz line.target( fr ); // assegnare la frequenza a 'line' che controlla la freq dell'oscillatore msg_in.data2 => last_note; set_cutoff( raw_cutoff ); msg_in.data3 * divided_by_127 => float amplitude; m_env.target( amplitude ); env.duration( attack_time ); env.keyOn(1); } // controlla che il messaggio MIDI sia un NOTE OFF o un NOTE ON con velocity 0 // in entrambi i casi la nota MIDI deve combaciare con l'ultima suonata (playedNote) else if( ( msg_in.data1 == ( NOTE_ON + MIDI_CHANNEL ) && msg_in.data3 == 0 && msg_in.data2 == last_note ) || ( msg_in.data1 == ( NOTE_OFF + MIDI_CHANNEL ) && msg_in.data2 == last_note ) ) { env.duration( release_time ); env.keyOff(1); } // portamento else if( msg_in.data1 == ( CTRL_CHANGE+MIDI_CHANNEL ) && msg_in.data2 == knobs[0] ) { set_interp( msg_in.data3 ) => freq_interpolation; line.time( freq_interpolation ); } // seleziona la forma d'onda else if( msg_in.data1 == ( CTRL_CHANGE+MIDI_CHANNEL ) && msg_in.data2 == knobs[1] ) { msg_in.data3 * 4 => int x; x / 128 => selected_oscil; if( selected_oscil != previous_oscil ) { set_oscils( midi_note ); // seleziona la wavetable in base alla nota ricevuta e l'oscillatore selezionato <<< "waveform: " + waveform_name[ selected_oscil ] >>>; selected_oscil => previous_oscil; } } // seleziona il filtro else if( msg_in.data1 == ( CTRL_CHANGE+MIDI_CHANNEL ) && msg_in.data2 == knobs[2] ) { ( ( msg_in.data3 / 128. ) * 3 ) $ int => filter_id; // chiama 'select_filter()' solo quando 'filter_id' cambia valore if( filter_id != previous_filter_id ) { select_filter( filter_id ); filter_id => previous_filter_id; } } // cutoff else if( msg_in.data1 == ( CTRL_CHANGE+MIDI_CHANNEL ) && msg_in.data2 == knobs[3] ) { msg_in.data3 * divided_by_127 => raw_cutoff; set_cutoff( raw_cutoff ); <<< "CUTOFF: " + cutoff >>>; } // Q - resonance else if( msg_in.data1 == ( CTRL_CHANGE+MIDI_CHANNEL ) && msg_in.data2 == knobs[4] ) { set_Q( msg_in.data3 * divided_by_127 ); <<< "filter Q: " + Q >>>; } } }
Nella sezione che gestisce le note MIDI ricevute, si puo’ notare che prima di impostare l’ampiezza della nota che si vuole suonare, viene invocata la funzione set_cutoff. Questa e’ discussa nelle righe successive, per ora basti sapere che il filtro del sintetizzatore viene aggiornato in base alla frequenza della nota suonata.
Di seguito tre nuovi potenziometri (knob del controller hardware) vengono utilizzati per selezionare il tipo di filtro, impostare il _cutoff _ed il parametro Q (la risonanza del filtro).
Negli else_if relativi a questi parametri vengono chiamate 3 nuove funzioni:
function void set_cutoff( float f ) { // imposta il valore minimo di cutoff in base all'ultima nota suonata Std.mtof( midi_note ) => float min_freq; Math.max( 50, min_freq - ( min_freq * 0.5 ) ) => min_freq; Math.min( min_freq, 11950 ) => min_freq; ( f * f * f * ( 12000 - min_freq ) ) + min_freq => cutoff; // aggiorna l'UGen Envelope utilizzato per impostare il cutoff del filtro in 'modo interpolato' - vedi 'cutoff_update()' cutoff_interp.time( 0.005 ); // tempo di interpolazione in secondi cutoff_interp.target( cutoff ); } function void set_Q( float q ) { ( q * 4 ) + 1 => Q; lowpass.Q(Q); bandpass.Q(Q); highpass.Q(Q); } function void select_filter( int id ) { /* sceglie tra passa basso, passa banda e passa alto */ if( id == 0 ) { // disconnete 'env' da tutti i filtri e tutti i filtri da 'master' env =< lowpass =< master; env =< bandpass =< master; env =< highpass =< master; // connette 'env' al filtro giusto ed il filtro giusto a 'master' env => lowpass => master; <<< "FILTRO LOWPASS" >>>; } else if( id == 1 ) { // disconnete 'env' da tutti i filtri e tutti i filtri da 'master' env =< lowpass =< master; env =< bandpass =< master; env =< highpass =< master; // connette 'env' al filtro giusto ed il filtro giusto a 'master' env => bandpass => master; <<< "FILTRO BANDPASS" >>>; } else { // disconnete 'env' da tutti i filtri e tutti i filtri da 'master' env =< lowpass =< master; env =< bandpass =< master; env =< highpass =< master; // connette 'env' al filtro giusto ed il filtro giusto a 'master' env => highpass => master; <<< "FILTRO HIGHPASS" >>>; } }
Di queste, _set_Q _e select_filter sono abbastanza semplici e non contengono niente che non sia gia’ stato affrontato nei tutorial precedenti.
set_cutoff invece presenta alcuni concetti interessanti e nuovi che meritano di essere discussi.
Come prima cosa si puo’ notare che, come gia’ detto in precedenza, la frequenza di _cutoff _varia in base alla frequenza della nota suonata3.
In pratica in questo modo il valore minimo del cutoff e’ approssimativamente la frequenza della nota suonata.
Di seguito viene aggiornato l’interpolatore lineare che viene utilizzato per controllare il _cutoff _dei tre filtri, viene impostato il tempo necessario per raggiungere il valore di cutoff, ed il valore di _cutoff _stesso.
Va ricordato che questo interpolatore lineare gira su un processo parallelo, e come visto precedentemente quando si e’ analizzata la funzione cutoff_update, questo processo e’ un ciclo infinito che calcola un nuovo valore di cutoff quando un cambiamento avviene.
RIEPILOGO
filtri: low-pass, band-pass, high-pass
shreads e processi paralleli
miglioramento della sezione _amplitude _per evitare glitch.
Il codice ed il testo possono essere scaricati qui: https://bitbucket.org/mariobuoninfante/chuck_workshop/src
1Vedi https://en.flossmanuals.net/chuck/_full/#unit-generators per altri UGens.
2Anche l’ampiezza e’ ora interpolata, per evitare glitch.
Interesting article about hacking, creative coding and other cool things here (scroll down till the bottom to see a pop up dialog appear):
https://novationmusic.com/keys/sl-mkiii
Proud to be there together with some amazing people... talking about Pd (among other things)
Implementazione dei 4 oscillatori classici (limitati in banda):
sine
square
triangle
sawtooth
Utilizzo di array di UGen e perfezionamento dell'algoritmo di gestione degli oscillatori.
PREREQUISITI
Per poter utilizzare questo programma serve avere un controller MIDI connesso al computer, o una virtual MIDI keyboard. Questo va connesso (o aperto nel caso di una tastiera virtuale), prima di far partire il programma.
Conoscenza base del protocollo MIDI (https://it.wikipedia.org/wiki/Musical_Instrument_Digital_Interface - http://www.nyu.edu/classes/bello/FMT_files/8_MIDIcomms.pdf - https://www.nyu.edu/classes/bello/FMT_files/9_MIDI_code.pdf)
Conoscenza base della sintesi additiva e della serie di Fourier
ANALISI DEL PROGRAMMA
Questo programma non differisce troppo da quello presentato nel precedente tutorial.
All’oscillatore a forma d’onda quadra sono stati affiancati quelli: sinusoidale, triangolare e a dente di sega.
Il programma inizia nel seguente modo:
// RTcMix Lookup table: https://en.wikipedia.org/wiki/Real-time_Cmix Gen10 oscils[13]; Phasor phasor; // utilizzato per controllare le look-up table contentute in 'oscils' 0 => int selected_oscil; // oscillatore selezionato (0. sine, 1. square, 2. tri, 3. sawtooth) -1 => int previous_oscil; // variabile utilizzata nel ciclo while ["SINE", "SQUARE", "TRIANGLE", "SAWTOOTH"] @=> string waveform_name[];
Nella prima riga di codice si e’ utilizzato un array di UGen, contenente 13 Gen10 utilizzati come lookup table.
Come si vedra’ di seguito nel codice, l’array e’ suddiviso nel seguente modo:
oscils[0] = oscillatore sinusoidale
da oscils[1] a oscils[4] = oscillatore onda quadra (I 4 oscillatori hanno differente contenuto armonico, come nel case del tutorial precedente)
da oscils[5] a oscils[8] = oscillatore onda triangolare
da oscils[9] a oscils[12] = oscillatore onda a dente di sega
Di seguito la variabile selected_oscil viene dichiarata. Questa rappresenta l’oscillatore selezionato (sine, square, triangle, sawtooth). La variabile previous_oscil invece, rappresenta l’ultimo oscillatore selezionato e viene’ utilizzata nel ciclo while.
Un array di tipo string contenente I nomi dei vari oscillatori viene dichiarato. Questo e’ utilizzato per stampare sulla console il nome dell’oscillatore selezionato.
Il codice che segue, fini al punto in cui il ciclo while viene definito, e’ praticamente identico a quello del tutorial precedente.
Qui si ha:
// loop infinito while( true ) { // avanza ad ogni messaggio MIDI ricevuto m_in => now; while( m_in.recv( msg_in ) ) { if( msg_in.data1 == ( NOTE_ON + MIDI_CHANNEL ) && msg_in.data3 != 0 ) { // resetta il volume prima di generare una nuova nota per evitare 'glitch' env.duration( 3::ms ); env.keyOff(1); 3::ms => now; msg_in.data2 => midi_note; set_oscils( midi_note ); // seleziona la wavetable in base alla nota ricevuta e l’oscillatore selezionato Std.mtof( midi_note ) => fr; // conversione da MIDI a Freq in Hz line.target( fr ); // assegnare la frequenza a 'line' che controlla la freq dell'oscillatore msg_in.data2 => last_note; msg_in.data3 * divided_by_127 => float amplitude; env.gain( amplitude ); env.duration( attack_time ); env.keyOn(1); } // controlla che il messaggio MIDI sia un NOTE OFF o un NOTE ON con velocity 0 // in entrambi i casi la nota MIDI deve combaciare con l'ultima suonata (playedNote) else if( ( msg_in.data1 == ( NOTE_ON + MIDI_CHANNEL ) && msg_in.data3 == 0 && msg_in.data2 == last_note ) || ( msg_in.data1 == ( NOTE_OFF + MIDI_CHANNEL ) && msg_in.data2 == last_note ) ) { env.duration( release_time ); env.keyOff(1); } // portamento else if( msg_in.data1 == ( CTRL_CHANGE+MIDI_CHANNEL ) && msg_in.data2 == knobs[0] ) { set_interp( msg_in.data3 ) => freq_interpolation; line.time( freq_interpolation ); } // seleziona la forma d'onda else if( msg_in.data1 == ( CTRL_CHANGE+MIDI_CHANNEL ) && msg_in.data2 == knobs[1] ) { msg_in.data3 * 4 => int x; x / 128 => selected_oscil; if( selected_oscil != previous_oscil ) { set_oscils( midi_note ); // seleziona la wavetable in base alla nota ricevuta e l'oscillatore selezionato <<< "waveform: " + waveform_name[ selected_oscil ] >>>; selected_oscil => previous_oscil; } } } }
L’ultimo else if presente nel ciclo while e’ l’unica aggiunta al codice del tutorial 7.
Viene utilizzato il secondo knob del controller (Novation Launchkey nel caso specifico) per selezionare il tipo di oscillatore. Ogni qual volta select_oscil cambia valore, l’oscillatore selezionato cambia e viene stampato sulla console il suo nome.
La sezione FUNZIONI contiente set_interp, gia’ discussa nel tutorial precedente, e altri due funzioni: set_oscils e init_oscils.
Quest’ultima viene utilizzata per inizializzare I Gen10 con le diverse forme d’onda:
function void init_oscils() { /* Inizializza l'array di Gen10 'oscils'. Il primo elemento dell'array e' una sinusoide, a seguire si ha onda quadra, triangolare e dente di sega, le quali utilizzano 4 Gen10 ognuna. */ float coefficients[0]; // coefficienti degli UGen Gen10 // onda sinusoidale - oscils[0] coefficients.size(1); 1 => coefficients[0]; oscils[0].coefs( coefficients ); // onda quadra - formula: y = sum[(1/k)*sin(2PI*f*k*t)]; con k=1,3,5,7,9,... // quadra: 14 parziali - oscils[1] for( 1 => int c; c < 15; c++ ) { ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array 1. / odd => float amp; // 1/n amp => coefficients[ odd-1 ]; } oscils[1].coefs( coefficients ); // quadra: 7 parziali - oscils[2] for( 1 => int c; c < 8; c++ ) { ( ( c * 2 ) - 1 ) => int odd; coefficients.size( odd ); 1. / odd => float amp; amp => coefficients[ odd-1 ]; } oscils[2].coefs( coefficients ); // quadra: 3 parziali - oscils[3] for( 1 => int c; c < 4; c++ ) { ( ( c * 2 ) - 1 ) => int odd; coefficients.size( odd ); 1. / odd => float amp; amp => coefficients[ odd-1 ]; } oscils[3].coefs( coefficients ); // quadra: 1 parziale - sinusoide - oscils[4] for( 1 => int c; c < 2; c++ ) { ( ( c * 2 ) - 1 ) => int odd; coefficients.size( odd ); 1. / odd => float amp; amp => coefficients[ odd-1 ]; } oscils[4].coefs( coefficients ); // onda triangolare - formula: y = sum[(1/k^2)*sin(2PI*f*k*t + theta)]; con k=1,3,5,7,9,... e theta=0,180,0,180 in gradi // triangolare: 14 parziali - oscils[5] for( 1 => int c; c < 15; c++ ) { float theta; /* theta=0 quando c e' dispari, theta=180 quando c e' pari. La fase viene espressa attraverso l'ampiezza. Ampiezza positiva quando theta=0, ampiezza negativa quando theta=180, cioe' si ha la fase inversa. */ if( ( c % 2 ) == 1 ) { 1 => theta; } else { -1 => theta; } ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array theta * ( 1. / ( odd * odd ) ) => float amp; // 1/(n^2) * theta amp => coefficients[ odd-1 ]; } oscils[5].coefs( coefficients ); // triangolare: 7 parziali - oscils[6] for( 1 => int c; c < 8; c++ ) { float theta; /* theta=0 quando c e' dispari, theta=180 quando c e' pari. La fase viene espressa attraverso l'ampiezza. Ampiezza positiva quando theta=0, ampiezza negativa quando theta=180, cioe' si ha la fase inversa. */ if( ( c % 2 ) == 1 ) { 1 => theta; } else { -1 => theta; } ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array theta * ( 1. / ( odd * odd ) ) => float amp; // 1/(n^2) * theta amp => coefficients[ odd-1 ]; } oscils[6].coefs( coefficients ); // triangolare: 3 parziali - oscils[7] for( 1 => int c; c < 4; c++ ) { float theta; /* theta=0 quando c e' dispari, theta=180 quando c e' pari. La fase viene espressa attraverso l'ampiezza. Ampiezza positiva quando theta=0, ampiezza negativa quando theta=180, cioe' si ha la fase inversa. */ if( ( c % 2 ) == 1 ) { 1 => theta; } else { -1 => theta; } ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array theta * ( 1. / ( odd * odd ) ) => float amp; // 1/(n^2) * theta amp => coefficients[ odd-1 ]; } oscils[7].coefs( coefficients ); // triangolare: 1 parziale - sinusoide - oscils[8] for( 1 => int c; c < 2; c++ ) { float theta; /* theta=0 quando c e' dispari, theta=180 quando c e' pari. La fase viene espressa attraverso l'ampiezza. Ampiezza positiva quando theta=0, ampiezza negativa quando theta=180, cioe' si ha la fase inversa. */ if( ( c % 2 ) == 1 ) { 1 => theta; } else { -1 => theta; } ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array theta * ( 1. / ( odd * odd ) ) => float amp; // 1/(n^2) * theta amp => coefficients[ odd-1 ]; } oscils[8].coefs( coefficients ); // onda a dente di sega - formula: y = sum[(1/k)*sin(2PI*f*k*t)]; con k=1,2,3,4,5,... // dente di sega: 14 parziali - oscils[9] for( 1 => int c; c < 15; c++ ) { coefficients.size( c ); // ridimensiona l'array ( 1. / c ) => float amp; // 1/n amp => coefficients[ c-1 ]; } oscils[9].coefs( coefficients ); // dente di sega: 7 parziali - oscils[10] for( 1 => int c; c < 8; c++ ) { coefficients.size( c ); // ridimensiona l'array ( 1. / c ) => float amp; // 1/n amp => coefficients[ c-1 ]; } oscils[10].coefs( coefficients ); // dente di sega: 3 parziali - oscils[11] for( 1 => int c; c < 4; c++ ) { coefficients.size( c ); // ridimensiona l'array ( 1. / c ) => float amp; // 1/n amp => coefficients[ c-1 ]; } oscils[11].coefs( coefficients ); // dente di sega: 1 parziale - sinusoide - oscils[12] for( 1 => int c; c < 2; c++ ) { coefficients.size( c ); // ridimensiona l'array ( 1. / c ) => float amp; // 1/n amp => coefficients[ c-1 ]; } oscils[12].coefs( coefficients ); }
Mentre set_oscils seleziona il corretto Gen10 in base alla nota suonata e l’oscillatore selezionato.
Come nel tutorial 7, piu’ la nota suonata e’ alta, meno armoniche sono necessarie.
function void set_oscils( int x ) { /* Seleziona l'UGen Gen10 in base alla nota ricevuta. I diversi Gen10 hanno un diverso numero di armoniche, piu' e' alta la nota selezionata meno armoniche si avranno per evitare problemi di aliasing */ 0 => int pointer; // utilizzato come puntatore per l'array 'oscils' // quando l'oscillatore selezionato NON e' sinusoidale if( selected_oscil > 0 ) { ( ( selected_oscil - 1 ) * 4 ) + 1 => pointer; if( x <= 70 && oscils[ pointer ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == pointer ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } else if( x > 70 && x <= 90 && oscils[ pointer + 1 ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == ( pointer + 1 ) ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } else if( x > 90 && x <= 108 && oscils[ pointer + 2 ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == ( pointer + 2 ) ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } else if( x > 108 && x <= 127 && oscils[ pointer + 3 ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == ( pointer + 3 ) ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } } // quando l'oscillatore selezionato e' sinusoidale else if( selected_oscil == 0 ) { if( oscils[ pointer ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == pointer ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } } }
Nonostante il numero di righe contenute in questa funzione possa sembrare elevato, set_oscil non contiene nessun costrutto particolare ed il codice al suo interno e’ abbastanza semplice.
Per prima cosa si controlla se l’oscillatore selezionato e’ quello sinusoidale ed in tal caso viene connesso il primo Gen10 contenuto in oscils[] (oscils[0]).
In caso l’oscillatore selezionato non e’ quello sinusoidale, viene considerato il pitch della nota suonata e viene selezionato il giusto oscillatore con il giusto contenuto armonico (piu’ alta e’ la nota, meno armoniche sono necessarie).
A seguire il programma intero:
/*-----------------------INFO------------------------- PREMI "Add Shred" per lanciare il programma e "Remove Shred" per term_inarlo. "Replace Shred" serve a far ripartire il programma. ----------------------------------------------------*/ // RTcMix Lookup table: https://en.wikipedia.org/wiki/Real-time_Cmix Gen10 oscils[13]; Phasor phasor; // utilizzato per controllare le look-up table contentute in 'oscils' 0 => int selected_oscil; // oscillatore selezionato (0. sine, 1. square, 2. tri, 3. sawtooth) -1 => int previous_oscil; // variabile utilizzata nel ciclo while ["SINE", "SQUARE", "TRIANGLE", "SAWTOOTH"] @=> string waveform_name[]; Step step => Envelope line => phasor; Envelope env => Gain master => dac; step.next(1); // imposta il valore di 'step' su 1 phasor.sync(0); init_oscils(); // inizializza gli UGen Gen10 // connessioni MIDI MidiIn m_in; // crea un input MIDI MidiMsg msg_in; // crea un contenitore per i messaggi MIDI ricevuti /* seleziona la porta MIDI che corrisponde al controller in uso (CTRL+2 per controllare le porte MIDI) */ "Launchkey MK2 25 MIDI 1" => string device_name; m_in.open( device_name ); // variabili 144 => int NOTE_ON; 128 => int NOTE_OFF; 176 => int CTRL_CHANGE; 0 => int MIDI_CHANNEL; // canale MIDI - range da 0 a 15 float fr; float amp; int midi_note; int last_note; 0.007874016 => float divided_by_127; 0.05 => float freq_interpolation; // in secondi [21,22,23,24,25,26,27,28] @=> int knobs[]; // control change corrispondenti agli 8 potenziometri presenti sul controller 0.1 => amp; master.gain( amp ); // impostare l'ampiezza del Gain 'master' 10::ms => dur attack_time; // tempo di attacco 500::ms => dur release_time; // tempo di rilascio line.time( freq_interpolation ); // imposta il tempo di interpolatione della frequenza dell'oscillatore <<< "Basic Mono Synth with Sine, Square, Triangle and Sawtooth oscillators" >>>; <<< "waveform: " + waveform_name[ selected_oscil ] >>>; // loop infinito while( true ) { // avanza ad ogni messaggio MIDI ricevuto m_in => now; while( m_in.recv( msg_in ) ) { if( msg_in.data1 == ( NOTE_ON + MIDI_CHANNEL ) && msg_in.data3 != 0 ) { // resetta il volume prima di generare una nuova nota per evitare 'glitch' env.duration( 3::ms ); env.keyOff(1); 3::ms => now; msg_in.data2 => midi_note; set_oscils( midi_note ); // seleziona la wavetable in base alla nota ricevuta e l'oscillatore selezionato Std.mtof( midi_note ) => fr; // conversione da MIDI a Freq in Hz line.target( fr ); // assegnare la frequenza a 'line' che controlla la freq dell'oscillatore msg_in.data2 => last_note; msg_in.data3 * divided_by_127 => float amplitude; env.gain( amplitude ); env.duration( attack_time ); env.keyOn(1); } // controlla che il messaggio MIDI sia un NOTE OFF o un NOTE ON con velocity 0 // in entrambi i casi la nota MIDI deve combaciare con l'ultima suonata (playedNote) else if( ( msg_in.data1 == ( NOTE_ON + MIDI_CHANNEL ) && msg_in.data3 == 0 && msg_in.data2 == last_note ) || ( msg_in.data1 == ( NOTE_OFF + MIDI_CHANNEL ) && msg_in.data2 == last_note ) ) { env.duration( release_time ); env.keyOff(1); } // portamento else if( msg_in.data1 == ( CTRL_CHANGE+MIDI_CHANNEL ) && msg_in.data2 == knobs[0] ) { set_interp( msg_in.data3 ) => freq_interpolation; line.time( freq_interpolation ); } // seleziona la forma d'onda else if( msg_in.data1 == ( CTRL_CHANGE+MIDI_CHANNEL ) && msg_in.data2 == knobs[1] ) { msg_in.data3 * 4 => int x; x / 128 => selected_oscil; if( selected_oscil != previous_oscil ) { set_oscils( midi_note ); // seleziona la wavetable in base alla nota ricevuta e l'oscillatore selezionato <<< "waveform: " + waveform_name[ selected_oscil ] >>>; selected_oscil => previous_oscil; } } } } // -------------FUNZIONI-------------- function float set_interp( float x ) { x * divided_by_127 => x; // scala in un range da 0 a 1 x * x * x => x; // rendi la funzione esponenziale ( x * 0.997 ) + 0.003 => x; // scala in un range da 3 a 1000 millisecondi <<< "Freq Interpolation: " + x + " sec" >>>; return x; } function void set_oscils( int x ) { /* Seleziona l'UGen Gen10 in base alla nota ricevuta. I diversi Gen10 hanno un diverso numero di armoniche, piu' e' alta la nota selezionata meno armoniche si avranno per evitare problemi di aliasing */ 0 => int pointer; // utilizzato come puntatore per l'array 'oscils' // quando l'oscillatore selezionato NON e' sinusoidale if( selected_oscil > 0 ) { ( ( selected_oscil - 1 ) * 4 ) + 1 => pointer; if( x <= 70 && oscils[ pointer ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == pointer ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } else if( x > 70 && x <= 90 && oscils[ pointer + 1 ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == ( pointer + 1 ) ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } else if( x > 90 && x <= 108 && oscils[ pointer + 2 ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == ( pointer + 2 ) ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } else if( x > 108 && x <= 127 && oscils[ pointer + 3 ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == ( pointer + 3 ) ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } } // quando l'oscillatore selezionato e' sinusoidale else if( selected_oscil == 0 ) { if( oscils[ pointer ].isConnectedTo( env ) == 0 ) { for( 0 => int c; c < oscils.size(); c++ ) { if( c == pointer ) { phasor => oscils[c] => env; } else { phasor =< oscils[c] =< env; } } } } } function void init_oscils() { /* Inizializza l'array di Gen10 'oscils'. Il primo elemento dell'array e' una sinusoide, a seguire si ha onda quadra, triangolare e dente di sega, le quali utilizzano 4 Gen10 ognuna. */ float coefficients[0]; // coefficienti degli UGen Gen10 // onda sinusoidale - oscils[0] coefficients.size(1); 1 => coefficients[0]; oscils[0].coefs( coefficients ); // onda quadra - formula: y = sum[(1/k)*sin(2PI*f*k*t)]; con k=1,3,5,7,9,... // quadra: 14 parziali - oscils[1] for( 1 => int c; c < 15; c++ ) { ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array 1. / odd => float amp; // 1/n amp => coefficients[ odd-1 ]; } oscils[1].coefs( coefficients ); // quadra: 7 parziali - oscils[2] for( 1 => int c; c < 8; c++ ) { ( ( c * 2 ) - 1 ) => int odd; coefficients.size( odd ); 1. / odd => float amp; amp => coefficients[ odd-1 ]; } oscils[2].coefs( coefficients ); // quadra: 3 parziali - oscils[3] for( 1 => int c; c < 4; c++ ) { ( ( c * 2 ) - 1 ) => int odd; coefficients.size( odd ); 1. / odd => float amp; amp => coefficients[ odd-1 ]; } oscils[3].coefs( coefficients ); // quadra: 1 parziale - sinusoide - oscils[4] for( 1 => int c; c < 2; c++ ) { ( ( c * 2 ) - 1 ) => int odd; coefficients.size( odd ); 1. / odd => float amp; amp => coefficients[ odd-1 ]; } oscils[4].coefs( coefficients ); // onda triangolare - formula: y = sum[(1/k^2)*sin(2PI*f*k*t + theta)]; con k=1,3,5,7,9,... e theta=0,180,0,180 in gradi // triangolare: 14 parziali - oscils[5] for( 1 => int c; c < 15; c++ ) { float theta; /* theta=0 quando c e' dispari, theta=180 quando c e' pari. La fase viene espressa attraverso l'ampiezza. Ampiezza positiva quando theta=0, ampiezza negativa quando theta=180, cioe' si ha la fase inversa. */ if( ( c % 2 ) == 1 ) { 1 => theta; } else { -1 => theta; } ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array theta * ( 1. / ( odd * odd ) ) => float amp; // 1/(n^2) * theta amp => coefficients[ odd-1 ]; } oscils[5].coefs( coefficients ); // triangolare: 7 parziali - oscils[6] for( 1 => int c; c < 8; c++ ) { float theta; /* theta=0 quando c e' dispari, theta=180 quando c e' pari. La fase viene espressa attraverso l'ampiezza. Ampiezza positiva quando theta=0, ampiezza negativa quando theta=180, cioe' si ha la fase inversa. */ if( ( c % 2 ) == 1 ) { 1 => theta; } else { -1 => theta; } ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array theta * ( 1. / ( odd * odd ) ) => float amp; // 1/(n^2) * theta amp => coefficients[ odd-1 ]; } oscils[6].coefs( coefficients ); // triangolare: 3 parziali - oscils[7] for( 1 => int c; c < 4; c++ ) { float theta; /* theta=0 quando c e' dispari, theta=180 quando c e' pari. La fase viene espressa attraverso l'ampiezza. Ampiezza positiva quando theta=0, ampiezza negativa quando theta=180, cioe' si ha la fase inversa. */ if( ( c % 2 ) == 1 ) { 1 => theta; } else { -1 => theta; } ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array theta * ( 1. / ( odd * odd ) ) => float amp; // 1/(n^2) * theta amp => coefficients[ odd-1 ]; } oscils[7].coefs( coefficients ); // triangolare: 1 parziale - sinusoide - oscils[8] for( 1 => int c; c < 2; c++ ) { float theta; /* theta=0 quando c e' dispari, theta=180 quando c e' pari. La fase viene espressa attraverso l'ampiezza. Ampiezza positiva quando theta=0, ampiezza negativa quando theta=180, cioe' si ha la fase inversa. */ if( ( c % 2 ) == 1 ) { 1 => theta; } else { -1 => theta; } ( ( c * 2 ) - 1 ) => int odd; // solo numeri dispari coefficients.size( odd ); // ridimensiona l'array theta * ( 1. / ( odd * odd ) ) => float amp; // 1/(n^2) * theta amp => coefficients[ odd-1 ]; } oscils[8].coefs( coefficients ); // onda a dente di sega - formula: y = sum[(1/k)*sin(2PI*f*k*t)]; con k=1,2,3,4,5,... // dente di sega: 14 parziali - oscils[9] for( 1 => int c; c < 15; c++ ) { coefficients.size( c ); // ridimensiona l'array ( 1. / c ) => float amp; // 1/n amp => coefficients[ c-1 ]; } oscils[9].coefs( coefficients ); // dente di sega: 7 parziali - oscils[10] for( 1 => int c; c < 8; c++ ) { coefficients.size( c ); // ridimensiona l'array ( 1. / c ) => float amp; // 1/n amp => coefficients[ c-1 ]; } oscils[10].coefs( coefficients ); // dente di sega: 3 parziali - oscils[11] for( 1 => int c; c < 4; c++ ) { coefficients.size( c ); // ridimensiona l'array ( 1. / c ) => float amp; // 1/n amp => coefficients[ c-1 ]; } oscils[11].coefs( coefficients ); // dente di sega: 1 parziale - sinusoide - oscils[12] for( 1 => int c; c < 2; c++ ) { coefficients.size( c ); // ridimensiona l'array ( 1. / c ) => float amp; // 1/n amp => coefficients[ c-1 ]; } oscils[12].coefs( coefficients ); } // _FUNZIONI
RIEPILOGO
generatori d'onda triangolare, dente di sega (limitati in banda)
array di UGen
perfezionamento dell'algoritmo di gestione degli oscillatori
Il codice ed il testo possono essere scaricati qui: https://bitbucket.org/mariobuoninfante/chuck_workshop/src
Utilizzo di lookup table come generatore d’onda quadra limitata in banda al posto di un oscillatore sinusoidale.
Utilizzo della struttura di controllo “for”.
PREREQUISITI
Per poter utilizzare questo programma serve avere un controller MIDI connesso al computer, o una virtual MIDI keyboard. Questo va connesso (o aperto nel caso di una tastiera virtuale), prima di far partire il programma.
Conoscenza base del protocollo MIDI (https://it.wikipedia.org/wiki/Musical_Instrument_Digital_Interface - http://www.nyu.edu/classes/bello/FMT_files/8_MIDIcomms.pdf - https://www.nyu.edu/classes/bello/FMT_files/9_MIDI_code.pdf)
Conoscenza base della sintesi additiva e della serie di Fourier
LOOKUP TABLE E PHASOR
Nei precedenti tutorial sono stati affrontate principalmente problematiche riguardanti il controllo delle varie componenti base di un sintetizzatore (implementazione di una tastiera MIDI, controllo in frequenza di un oscillatore, portamento, etc.). Questo tutorial invece, sara’ incentrato sull’implementazione di un oscillatore ad onda quadra limitato in banda e si iniziera’ ad affrontare la parte riguardante il timbro dello strumento.
Generalmente I sintetizzatori hardware hanno una sezione chiamata VCO (voltage controlled oscillators - se analogici) o DCO (digital controlled oscillators - se digitali), la quale include vari oscillatori con forme d’onda differenti tra loro.
Le forme d’onde classiche molto spesso implementate nei synth hardware e software sono: sinusoidale, quadra, triangolare e dente di sega.
Esistono vari modi per implementare questi generatori in ChucK, il piu’ semplice prevede l’utilizzo di dedicati UGen come SqrOsc, TriOsc e SawOsc (onda quadra, triangolare e dente di sega), I quali hanno le stesse funzionalita’ dell’UGen SinOsc con cui si e’ lavorato fin’ora.
Purtroppo pero’, non e’ sempre possibile utilizzare questi UGen, in quanto e’ molto facile imbattersi in una problematica comune a tutti I sistemi digitali: l’aliasing.
Gli UGen presenti in ChucK, generano una versione perfetta delle forme d’onda classiche sopra elencate, la quale ha un alto contenuto armonico, che non varia in base alla frequenza (ie una forma d’onda a 50 Hz ha lo stesso numero di armoniche di una a 1000 Hz), e cio’ risulta essere un problema quando si lavora con strumenti musicali digitali.
Nel caso di SqrOsc si puo’ pensare ad un suono (onda quadra) con un numero infinito di armoniche. Cio’ significa che si avra’ un tot di armoniche con freqenza superiore alla frequenza di Nyquist (frequenza di campionamento / 2), al quale corrisponde il limite in banda di un sistema digitale.
Le frequenze che superano questo limite, non scompaiono dallo spettro ma vengono ribaltate.
Prendendo in considerazione un sistema con frequenza di campionamento pari a 48 kHz (48000 Hz), possono essere generate tutte le frequenze inferiori a 24 kHz (24000 Hz). Quando questa soglia viene suprata, le frequenze vengono ribaltate nel seguente modo:
y = x-SR
dove:
x >= SR/2 e rappresenta la frequenza in Hz
SR = sample rate (frequenza di campionamento)
Quindi nel caso di un sistema a 48 kHz, se si prova a generare una frequenza pari a 25000 Hz, si otterra’ come risultato una frequenza pari a -23000 Hz per la formula di cui sopra.
Il segno meno “-” suggerisce che la fase e’ stata invertita.
E’ chiaro a questo punto che questo comportamento e’ indesiderato nel caso di strumenti musicali digitali (ma non solo), in quanto il contenuto armonico e di consegueza il timbro dello strumento puo’ essere facilmente alterato. Allo stesso tempo pero’, non si vuole rinunciare all’utilizzo di forme d’onda diverse da quella sinusoidale.
Esistono varie tecniche utilizzate per aggirare questo problema. Quella presa in considerazione in questo esempio prevede l’utilizzo di lookup table, degli array contenenti forme d’onda (wavetable) utilizzati al posto dei generatori d’onda (oscillatori). L’idea e’ quella di avere piu’ lookup table contenenti la stessa forma d’onda ma con diverso numero di armonici (ie prima lookup table ha un’onda quadra con 15 armonici, la seconda lookup table una forma d’onda quadra con 8 armonici, etc.), per poi poter scegliere quella da utilizzare in base alla frequenza fondamentale della nota che si intende riprodurre. Piu’ la nota e’ alta minore sara’ il contenuto armonico della forma d’onda.
Essendo le lookup table delle tabelle (file audio), necessitano di un ulteriore UGen utilizzato per leggere I valori da esse contenuti. Generalmente si utilizza un ramp generator, che in ChucK (ed in molti altri linguaggi di programmazione audio) e’ l’UGen Phasor.
Questa tecnica risulta essere uno dei compromessi piu’ utilizzati, e come tutti I compromessi ha ovviamente pro e contro (uno dei contro e’ che non e’ facile modificare il contenuto delle lookup table in tempo reale, mentre puo’ essere relativamente facile modificare un algoritmo che genera la forma d’onda in tempo reale. Uno dei pro e’ che dal punto di vista computazionale e’ meglio avere una lookup table che singoli oscillatori quando si ha che fare con forme d’onda complesse).
In ChucK esistono degli UGen che fungono da lookup table, e sono quelli appartenenti alla famiglia dei GenX. Nell’esempio illustrato a breve, verra’ utilizzato Gen10, il quale attraverso il metodo .coeff(), genera automaticamente una forma d’onda in base all’argomento ricevuto.
Nel caso specifico, l’argomento deve essere un’array di numeri in virgola mobile, il quale rappresenta l’ampiezza delle varie sinusoidi (parziali) che compongono la forma d’onda desiderata. Le celle dell’array corrispondono alle parziali della forma d’onda (cella nr 1 = fondamentale, cella 2 = prima armonica, cella 3 = seconda armonica, etc.), per un massimo di 100 argomenti e quindi 100 parziali.
L’immagine seguente illustra il risultato, in termini di contenuto armonico, ottenuto utilizzando l’array [1., 0., 0.75, 0., 0.5, 0., 1., 0., 0., 0.5]
Si puo’ intuire dunque che Gen10 non fa altro che effettuare una sintesi additiva in base all’argomento ricevuto, e ne salva il risultato (forma d’onda) in un array.
Come accennato in precedenza il modo utilizzato per accedere ai dati contenuti in questo array, per leggere l’array, prevede l’utilizzo di una rampa collegata all’ingresso di Gen10.
L’UGen Phasor genera una rampa in un range da 0 ad 1, e puo’ essere collegato all’ingresso di un UGen Gen10 per far si che questo generi un segnale in uscita corrispondente alla forma d’onda salvata al suo interno.
I valori generati da Phasor (qualcosa di molto simile a 0, 0.001, 0.002, 0.003, 0.004, …, 0.999, 1. - per poi ripartire da 0) vengono utilizzati come indice dell’array contenuto in Gen10.
Avendo come riferimento l’immagine precedente, l’asse y di phasor corrisponde all’asse x della lookup table.
Quindi quando l’output di phasor e’ 0.5 (meta’ rampa) si stara’ leggendo il valore al centro della lookup table, quando l’output di phasor e’ 0.75, si stara’ leggendo il valore salvato nel punto x = ¾ * (lunghezza dell’array), e cosi’ via.
In questo scenario la frequenza (Hz) di Phasor influenza la velocita’ con cui la lookup table viene letta e di conseguenza la frequenza del segnale in uscita.
ANALISI DEL PROGRAMMA
Il programma come tutti quelli precedenti inizia con la dichiarazione degli UGen e delle variabili globali:
Gen10 square_1; // note da 0 a 60 Gen10 square_2; // note da 61 a 80 Gen10 square_3; // note da 81 a 105 Gen10 square_4; // note da 106 a 127 Phasor phasor; // utilizzato per controllare gli UGen Gen10 Step step => Envelope line => phasor; Envelope env => Gain master => dac; step.next(1); phasor.sync(0); float coefficients[0]; // coefficienti degli UGen Gen10 initSquareWave(); // connessioni MIDI MidiIn mIn; MidiMsg msgIn; mIn.open("Launchkey MK2 25 MIDI 1"); // variabili 144 => int NOTE_ON; 128 => int NOTE_OFF; 176 => int CTRL_CHANGE; 0 => int MIDI_CHANNEL; // canale MIDI - range da 0 a 15 float fr; float amp; int midiNote; int lastNote; 0.007874016 => float dividedBy127; 0.05 => float freqInterpolation; // in secondi [21,22,23,24,25,26,27,28] @=> int knobs[]; // control change corrispondenti agli 8 potenziometri presenti sul controller 0.1 => amp; master.gain(amp); // impostare l'ampiezza del Gain 'master' 10::ms => dur attackTime; // tempo di attacco 500::ms => dur releaseTime; // tempo di rilascio line.time(freqInterpolation); // imposta il tempo di interpolatione della frequenza dell'oscillatore
Come prima cosa vengono creati 4 Gen10 (lookup table) ed un Phasor. Quest’ultimo viene controllato in frequenza da una catena Step => Envelope. Questo, come visto negli esempi precedenti permette di avere l’effetto portamento (interpolazione del segnale che controlla la frequenza).
Viene poi chiamata la funzione initSquareWave(), che e’ dichiarata in seguito nel programma dopo ciclo while. Questa permette di inizializzare le lookup table, creando le 4 forme d’onda quadra con differenti contenuti armonici.
Il codice successivo, fino all’inizio del ciclo while non presenta niente di diverso da quello affrontato nel tutorial precedente.
A seguire si ha:
while(true) { // avanza ad ogni messaggio MIDI ricevuto mIn => now; while(mIn.recv(msgIn)) { if(msgIn.data1 == (NOTE_ON + MIDI_CHANNEL) && msgIn.data3 != 0) { // resetta il volume prima di generare una nuova nota per evitare 'glitch' env.duration(3::ms); env.keyOff(1); 3::ms => now; msgIn.data2 => midiNote; setSquareWave(midiNote); // seleziona la wavetable in base alla nota ricevuta Std.mtof(midiNote) => fr; line.target(fr); msgIn.data2 => lastNote; msgIn.data3 * dividedBy127 => float amplitude; env.gain(amplitude); env.duration(attackTime); env.keyOn(1); } else if( (msgIn.data1 == (NOTE_ON + MIDI_CHANNEL) && msgIn.data3 == 0 && msgIn.data2 == lastNote) || (msgIn.data1 == (NOTE_OFF + MIDI_CHANNEL) && msgIn.data2 == lastNote) ) { env.duration(releaseTime); env.keyOff(1); } else if(msgIn.data1 == (CTRL_CHANGE+MIDI_CHANNEL) && msgIn.data2 == knobs[0]) { setIntep(msgIn.data3) => freqInterpolation; line.time(freqInterpolation); } } }
Il main loop inizia con qualcosa di nuovo. Ogni qual volta un messaggio MIDI di tipo note on viene ricevuto il seguente codice, prima di suonare la nota ricevuta, resetta il volume dell’oscillatore (di Gen10 in questo caso) ed aspetta 3 millisecondi prima di procedere:
env.duration(3::ms); env.keyOff(1); 3::ms => now;
Questo piccolo pezzo di codice serve ad eliminare eventuali glitch, rumori indesiderati che possono essere causati dal cambio repentino di frequenza dell’oscillatore (ie si passa da una nota a 146.83 Hz ad una a 8372.01). Una situazione del genere potrebbe altrimenti creare delle discontinuita’ nel segnale, che si manifesterebbero nel dominio audio sotto forma di glitch (clicks, pops, etc.).
A seguire la nota MIDI ricevuta viene passata come argomento alla funzione setSquareWave(), anche questa dichiarata alla fine del codice nella sezione FUNZIONI. Questa funzione seleziona la lookup table da utilizzare in base alla nota MIDI. Piu’ la nota e’ alta (frequenza in Hz maggiore) meno armoniche devo essere utilizzate, e dunque una lookup table con meno armoniche viene selezionata.
Il resto del codice presente nel ciclo while e’ lo stesso utilizzato nel tutorial precedente.
A questo punto si ha la sezione dove le funzioni vengono dichiarate. La prima e’ setInterp(), spiegata nel tutorial parte 6.
A seguire le funzioni setSquareWave() e initSquareWave():
function void setSquareWave(int x) { // seleziona l'UGen Gen10 in base alla nota ricevuta. // I diversi Gen10 hanno un diverso numero di armoniche, // piu' e' alta la nota selezionata meno armoniche si avranno // per evitare problemi di aliasing if(x <= 70 && square_1.isConnectedTo(env) == 0) { phasor => square_1 => env; phasor =< square_2 =< env; phasor =< square_3 =< env; phasor =< square_4 =< env; <<< "SQUARE 1" >>>; } else if(x > 70 && x <= 90 && square_2.isConnectedTo(env) == 0) { phasor =< square_1 =< env; phasor => square_2 => env; phasor =< square_3 =< env; phasor =< square_4 =< env; <<< "SQUARE 2" >>>; } else if(x > 90 && x <= 108 && square_3.isConnectedTo(env) == 0) { phasor =< square_1 =< env; phasor =< square_2 =< env; phasor => square_3 => env; phasor =< square_4 =< env; <<< "SQUARE 3" >>>; } else if(x > 108 && x <= 127 && square_4.isConnectedTo(env) == 0) { phasor =< square_1 =< env; phasor =< square_2 =< env; phasor =< square_3 =< env; phasor => square_4 => env; <<< "SQUARE 4" >>>; } } function void initSquareWave() { // crea 4 diverse lookup table, con contenuti armonici different. // onda quadra - formula: y = sum[(1/k)*sin(2PI*f*k*t)]; con k=1,3,5,7,9,... // square_1: 14 parziali for(1=>int c; c<15; c++) { ((c * 2) - 1) => int odd; // solo numeri dispari coefficients.size(odd); // ridimensiona l'array 1. / odd => float amp; // 1/n amp => coefficients[odd-1]; } square_1.coefs(coefficients); // square_2: 7 parziali for(1=>int c; c<8; c++) { ((c * 2) - 1) => int odd; coefficients.size(odd); 1. / odd => float amp; amp => coefficients[odd-1]; } square_2.coefs(coefficients); // square_3: 3 parziali for(1=>int c; c<4; c++) { ((c * 2) - 1) => int odd; coefficients.size(odd); 1. / odd => float amp; amp => coefficients[odd-1]; } square_3.coefs(coefficients); // square_4: 1 parziale - sinusoide for(1=>int c; c<2; c++) { ((c * 2) - 1) => int odd; coefficients.size(odd); 1. / odd => float amp; amp => coefficients[odd-1]; } square_4.coefs(coefficients); }
setSquareWave(), come gia’ detto in precedenza, si occupa di scegliere la lookup table giusta in base alla nota MIDI ricevuta. Infatti quando quest’ultima viene passata come argomento, viene controllato il suo valore e che la lookup table da utilizzare (in base alla nota MIDI) non sia gia’ in uso.
Per fare cio’ viene utilizzato il metodo comune a tutti gli UGen, isConnectedTo(nomeDell’UGen):
square_1.isConnectedTo(env) == 0
Utilizzando questo metodo si puo’ capire se un UGen A e’ connesso ad un altro UGen B.
Il valore restituito da isConnectedTo() e’ 1 nel caso in cui I due UGen sono connessi, altrimenti e’ 0.
Nel caso in cui la nota MIDI e’ nel range desiderato (ie nota <= 70) ed il risultato di isConnectedTo() e’ 0, viene eseguito il codice nelle parentesi graffe {}.
I 4 if contenuti in setSquareWave, hanno tutti una struttura simile:
In termini pratici, questo codice non fa altro che connettere l’output di phasor esclusivamente alla lookup table desiderata, e di connettere solo l’output della lookup table desiderata all’inviluppo env.
Per disconnettere due UGen si utilizza l’operatore UnChucK (=<).
Il motivo per cui si effettuano tutte queste operazioni e’ semplicemente per evitare calcoli inutili. Solo un Gen10 alla volta e’ necessario per generare la forma d’onda desiderata, e lasciare phasor connesso a piu’ Gen10 sarebbe uno spreco, in quando ogni operazione ha un costo computazionale che influenza le performance del programma.
Lasciare phasor connesso a tutti i Gen10 equivale a lasciare tutte le luci accese in un appartamento, anche se si e’ da soli, chiusi in una stanza. Come risultato si avra’ un maggiore consumo di corrente.
Nelle ultime righe di codice viene dichiarata la funzione initSquareWave() che genera le 4 forme d’onda quadra, ognuna con un diverso numero di parziali.
Questa funzione non ha bisogno di argomenti e quando invocata come prima cosa crea un array di tipo float con lunghezza pari a 0, a cui successivamente aggiunge I valori corrispondenti all’ampiezza delle parziali della forma d’onda.
Questa operazione si ripete 4 volte (una volta per onda quadra, per Gen10).
Per fare cio’ viene utilizzato una nuova struttura di controllo, il ciclo for:
Questo ciclo esegue il codice presente tra le parentesi graffe {} un numero x di volte, in base alla dichiarazione presente tra le parentesi tonde ().
for necessita di una variabile con un valore noto (non nulla - il valore puo’ essere anche 0), di una condizione da valutare e di un’operazione da svolgere per far si che il codice tra le parentesi graffe {} venga eseguito.
for(variabile; condizione; operazione) { codice da eseguire un numero ‘n’ di volte } for(5 => int c; c > 0; c--) { <<< c >>>; }
Questo breve esempio puo’ essere interpretato nel seguente modo:
affinche ‘c’ (che ha valore 5) e’ maggiore di 0, decrementa ‘c’ di 1, e stampa il suo valore sulla console. Quando ‘c<=0’ interrompi il loop e prosegui nel programma.
Il risultato sara’:
5 :(int) 4 :(int) 3 :(int) 2 :(int) 1 :(int)
Quello che succede in pratica e’:
la variabile c viene creata e gli viene assegnato il valore 5
viene controllato che e’ maggiore di 0
viene eseguito il codice tra le parentesi graffe {}
a c viene assegnato il valore (c-1) – in ChucK ed altri linguaggi di programmazione il valore delle le variabili puo’ essere incrementato e/o diminuito di 1, semplicemente utilizzando nomeDellaVariabile++ oppure nomeDellaVariabile-- (http://chuck.cs.princeton.edu/doc/language/oper.html#incdec)
il codice tra le parentesi graffe {} viene ripetuto e a c viene sottratto 1, affinche’ c>0
Tornando al codice in esame, il ciclo for esegue le seguenti operazioni:
((c * 2) - 1) => int odd; // solo numeri dispari coefficients.size(odd); // ridimensiona l'array 1. / odd => float amp; // 1/n amp => coefficients[odd-1];
La variabile c dichiarata nel costrutto for viene utilizzata per inizializzare la variabile odd con solo numeri dispari:
(1*2) -1 = 1
(2*2) -1 = 3
(3*2) -1 = 5
etc.
L’array dichiarato all’esterno del ciclo for viene ridimensionato (era di lunghezza 0 al momento della sua dichiarazione) utilizzando il metodo size(). La variabile amp viene creata e gli viene assegnato un valore pari a 1/odd. Infine amp viene salvata in una cella (odd-1) dell’array coefficients.
Queste 4 righe di codice non fanno altro che calcolare l’ampiezza delle parziali di una forma d’onda quadra in accordo con la seguente formula:
y = sum[(1/k)*sin(2PI*f*k*t)]; con k=1,3,5,7,9,...
la quale dice che per ottenere una forma d’onda quadra serve sommare solo sinusoidi dispari con ampiezza uguale a 1/n con n = numero della parziale.
Utilizzando Gen10 non e’ necessario pero’ effettuare nessuna operazione se non il calcolo dell’ampiezza delle armoniche. Le operazioni trigonometriche avvengono all’interno dell’UGen stesso.
Alla fine di ognuno dei 4 cicli for si ha un rigo di codice del tipo:
square_1.coefs(coefficients);
il quale passa l’array appena generato come argomento al metodo .coefs(), che fa si che la forma d’onda desiderata venga generata e salvata in uno dei 4 UGen Gen10.
I 4 cicli for sono identici a parte per il numero delle parziali che vengono create all’interno delle parentesi graffe {}, e dunque alla durata del loop for.
Questo e’ dovuto alla differenza nel codice contenuto nelle parentesi tonde () che seguono for:
for(1=>int c; c<15; c++)
for(1=>int c; c<8; c++)
for(1=>int c; c<4; c++)
for(1=>int c; c<2; c++)
Il primo ciclo for genera 14 parziali, il secondo 7, il terzo 3 e l’ultimo 1.
RIEPILOGO
utilizzo di lookup table - UGen Gen10
generatore d'onda quadra limitata in banda
connettere e disconnettere UGen
struttura di controllo "for"
Il codice ed il testo possono essere scaricati qui: https://bitbucket.org/mariobuoninfante/chuck_workshop/src
Utilizzo di funzioni, array e di messaggi MIDI control change.
PREREQUISITI
Per poter utilizzare questo programma serve avere un controller MIDI connesso al computer, o una virtual MIDI keyboard. Questo va connesso (o aperto nel caso di una tastiera virtuale), prima di far partire il programma.
Conoscenza base del protocollo MIDI (https://it.wikipedia.org/wiki/Musical_Instrument_Digital_Interface - http://www.nyu.edu/classes/bello/FMT_files/8_MIDIcomms.pdf - https://www.nyu.edu/classes/bello/FMT_files/9_MIDI_code.pdf)
ANALISI DEL PROGRAMMA
In questo esempio viene implementato un altro modulo presente in molti sintetizzatori commerciali, il portamento, che permette di effettuare un glissando tra note successive.
Di solito, quando due note differenti vengono generate in successione (ie C4 e dopo F#4), la frequenza dell’oscillatore cambia immediatamente. Con l’utilizzo del portamento e’ possibile rendere questo passaggio graduale. Al cambiare della nota, la frequenza dell’oscillatore non passa immediatamente dal punto A (frequenza della prima nota) al punto B (frequenza della seconda nota), ma “impiega del tempo” la cui durata corrisponde al valore del portamento.
Nella figura precedente viene illustrato il comportamento della frequenza dell’oscillatore nel tempo, nel caso in cui non e’ presente alcun
portamento (linea nera) e quando invece c’e’ portamento (linea rossa). In pratica il segnale che controlla frequenza viene interpolato.
In ChucK fino ad ora la frequenza dell’UGen SinOsc e’ stata sempre controllata attraverso l’utilizzo del metodo freq(), ma esistono altri modi per svolgere lo stesso compito.
In questo esempio SinOsc utilizza il segnale inviato al suo ingresso come controllo in frequenza.
Per fare cio’ viene introdotto un nuovo UGen: Step.
Questo genera un valore costante che puo’ essere impostato utilizzando il metodo next().
Step viene collegato ad un Envelope il quale ne controlla l’ampiezza in modo lineare (interpolazione).
L’output dell’Envelope viene inviato in ingresso a SinOsc, il quale lo utilizza come controllo in frequenza.
Dopo la dichiarazione degli UGen, questi vengono inizializzati. Step genera un segnale costante pari a 1 e SinOsc attraverso l’utilizzo del metodo sync(), viene impostato in modo da utilizzare il segnale in ingresso come controllo in frequenza (vedi metodo sync in: ChucK FLOSS: SinOsc).
//connessioni MIDI MidiIn mIn; // crea un input MIDI MidiMsg msgIn; // crea un contenitore per i messaggi MIDI ricevuti mIn.open("Launchkey MK2 25 MIDI 1"); // variabili 144 => int NOTE_ON; 128 => int NOTE_OFF; 176 => int CTRL_CHANGE; 0 => int MIDI_CHANNEL; // MIDI channel da 0 a 15 float fr; float amp; int midiNote; int lastNote; 0.05 => float freqInterpolation; // in secondi [21,22,23,24,25,26,27,28] @=> int knobs[]; 0.2 => amp; osc.gain(amp); 10::ms => dur attackTime; 500::ms => dur releaseTime; line.time(freqInterpolation);
A seguire gli oggetti MidiIn e MidiMsg vengono dichiarati e il device MIDI in uso viene connesso.
Come negli esempi precedenti anche le variabili in uso nel programma vengono dichiarate e inizializzate.
Un nuovo elemento viene pero’ introdotto, la variabile knobs[], che rappresenta un array di valori.
Un array puo’ essere immaginato come una scatola in grado di contenere un numero n di dati. Un semplice esempio di array puo’ essere quello dei cruciverba, dove si hanno delle singole caselle contenti dati atomici (in questo caso lettere) che sono parte di una struttura piu’ grande (parola).
In ChucK bisogna specificare il tipo di dato che si vuole salvare nell’array, che nel caso specifico e’ di tipo int.
Esistono vari modi per dichiarare ed inizializzare un array, nel caso in cui lo si voglia solo dichiarare bisogna utilizzare la seguente sintassi:
tipo nome[numeroDiCelle];
int numeriPrimi[12];
Il tipo di dato viene specificato, e successivamente il nome dell’array seguito da due parentesi quadre [] contenenti il numero di celle dell’array, che corrisponde al numero massimo di elementi che si possono salvare.
In questo caso l’array non essendo stato inizializzato, conterra’ tutti valori nulli, pari a 0.
Ovviamente esiste un modo per poter assegnare valori alle diverse celle di un array dopo la sua dichiarazione.
Va tenuto presente che in informatica (in ChucK e quasi tutti I linguaggi di programmazione) gli elementi di un array si contano a partire da 0. Dunque nel caso di un array di 10 elementi, si avra’ la prima cella pari a array[0], la seconda array[1] e cosi via fino all’ultima che sara’ array[9].
Per assegnare un valore alla prima cella di un array, dopo che questo e’ stato dichiarato, si utilizza la seguente sintassi:
valore => nome[numeroCella];
13 => numeriPrimi[0];
Cosi’ come illustrato nel codice precedente, gli array possono anche essere inizializzati nel momento in cui vengono dichiarati, utilizzando la seguente sintassi:
valori @=> tipo nome[];
[1,2,3,5,8,13] @=> int fibonacci[];
A differenza di una normale variabile, per inizializzare un intero array (non una singola cella) viene utilizzato il simbolo @=> (invece di => - per maggiori informazioni al riguardo: ChucK FLOSS: Arrays)
A questo punto l’array fibonacci contiene I seguenti 6 numeri: 1, 2, 3, 5, 8, 13.
In modo molto simile a come un singolo valore e’ stato assegnato ad una specifica cella, si possono ottenere I valori contenuti in un array. Nell’esempio dell’array fibonacci, si ha che:
fibonacci[0] = 1
fibonacci[1] = 2
fibonacci[2] = 3
fibonacci[3] = 5
fibonacci[4] = 8
fibonacci[5] = 13
Si puo’ dunque utilizzare fibonacci[4] per ottenere il valore intero 8. ln effetti fibonacci[4] verra’ trattato esattamente come un numero intero, si possono dunque effettuare operazioni matematiche, lo puo’ utilizzare in strutture di controllo, etc. (ie fibonacci[4] * 2 restituira’ 16).
Tornando al programma in questione, si ha:
[21,22,23,24,25,26,27,28] @=> int knobs[];
che corrisponde a un array di numeri interi, I quali rappresentano i control number (vedi: MIDI protocol) degli 8 potenziometri presenti sul controller Novation Launchkey MK2 (in uso al momento della realizzazione del testo).
I potenziometri su questo (e molti altri) controller inviano messaggi MIDI di tipo control change che come I messaggi Note On e Note Off sono composti da 3 byte:
byte 1: Tipo di Messaggio
byte 2: dato 1
byte 3: dato 2
Nel caso di messaggi control change il secondo byte corrisponde al control number, cioe’ un numero identificativo utilizzato dal controllo (knob, fader, bottone, etc.). Il terzo byte invece corrisponde al valore generato dal controllo, in un range da 0 a 127.
Nel caso di un knob, generalmente, si ha un valore 0 quando il knob e impostato sul minimo e un valore 127 quando e’ impostato al massimo.
Nel programma in questione verra’ utilizzato solo uno degli 8 potenziometri disponibili sul controller, ma nonostante cio’ si e’ deciso di utilizzare un array contenente anche le informazioni riguardanti I 7 potenziometri non in uso. Questo e’ stato fatto per facilitare l’aggiunta di altro codice al programma considerando che nei prossimi tutorial probabilmente altri potenziometri verranno utilizzati per diversi scopi.
Dopo la dichiarazione di knobs[] il programma prosegue con la dichiarazione di altre variabili e infine viene impostato il tempo di portamento utilizzando il metodo time() dell’UGen Envelope.
In questo modo viene specificato il tempo necessario per passare da un valore A ad un valore B, nel caso specifico si parla della frequenza dell’oscillatore.
while(true) { // avanza ad ogni messaggio MIDI ricevuto mIn => now; while(mIn.recv(msgIn)) { if(msgIn.data1 == (NOTE_ON + MIDI_CHANNEL) && msgIn.data3 != 0) { msgIn.data2 => midiNote; Std.mtof(midiNote) => fr; line.target(fr); msgIn.data2 => lastNote; env.duration(attackTime); env.keyOn(1); <<< "KEY PRESSED" >>>; } else if( (msgIn.data1 == (NOTE_ON + MIDI_CHANNEL) &&
msgIn.data3 == 0 && msgIn.data2 == lastNote) ||
(msgIn.data1 == (NOTE_OFF + MIDI_CHANNEL) &&
msgIn.data2 == lastNote) ) { // impostare il tempo impegato dall'inviluppo per // passare da 1 a 0 (da massimo a minimo) env.duration(releaseTime); // triggerare l'inviluppo per farlo passare da ON a OFF env.keyOff(1); <<< "KEY RELEASED" >>>; } else if(msgIn.data1 == (CTRL_CHANGE+MIDI_CHANNEL) && msgIn.data2 == knobs[0]) { setIntep(msgIn.data3) => freqInterpolation; line.time(freqInterpolation); } } }
Il ciclo while risulta essere praticamente identico a quello mostrato nella Parte 5, con l’aggiunta pero’ di un ulteriore costrutto else if alla fine della struttura.
Quest’ultimo if viene utilizzato per controllare che il messagio MIDI inviato dal controller sia di tipo control change, che sia sul canale MIDI desiderato e che il control number corrisponda a quello specificato nel primo elemento salvato nell’array knobs (cioe’ il primo verso sinistra degli 8 potenziometri presenti sul controller in utilizzo).
A questo punto, se il messaggio MIDI soddisfa tutte queste condizioni il seguente codice viene eseguito:
Il terzo byte del messaggio MIDI (msgIn.data3) viene passato come argomento alla funzione setInterp, la quale genera un valore e lo assegna alla variabile freqInterpolation. Questa poi viene utilizzata come tempo di interpolazione per l’Envelope line (che controlla la frequenza dell’oscillatore). Dunque freqInterpolation rappresenta il portamento.
In queste due ultime linee di codice viene utilizzata una funzione che non appartiene a nessuna libreria di ChucK (Std e Math), che infatti e’ stata scritta ad hoc nelle ultime righe del programma, al di fuori del ciclo while.
function float setIntep(float x) { x / 127. => x; // scala in un range da 0 a 1 x*x*x => x; // rendi la funzione esponenziale (x * 0.997) + 0.003 => x; // scala in un range da 3 a 1000 millisecondi <<< "Freq Interpolation: " + x + " ms" >>>; return x; }
In ChucK, come in altri linguaggi di programmazione, e’ possibile scrivere le proprie funzioni dichiarandole nello script, all’inizio o alla fine del codice.
Una funzione non e’ altro che un pezzo di codice che puo’ essere utilizzato infinite volte nel programma, atto a svolgere un determinato compito con lo scopo di ottimizzare la leggibilita’, la manutenibilita’ e la qualita’ del codice.
Nel caso dell’esempio in questione, la funzione setInterp genera un numero utilizzato come valore di interpolazione per l’UGen Envelope.
Le funzioni in ChucK hanno la seguente struttura:
function tipoDiDato nomeDellaFunzione(tipoDiDato nomeDellArgomento)
{
codice da eseguire;
}
function float quadratoDiUnNumero(float x) { return x*x; }
La dichiarazione inizia con la parola chiave function (e’ anche possibile utilizzare la versione abbreviata fun), seguita dal tipo di dato che la funzione deve restituire, il nome della funzione, a seguire tra parentesi tonde () gli argomenti di cui la funzione necessita (possono anche non esserci argomenti ed in tal caso le parentesi tonde saranno vuote), ed infine il codice da eseguire quando la funzione viene utilizzata, tra parentesi graffe{}.
Va notato che anche per gli argomenti (nel caso in cui ce ne siano), va specificato il tipo di dato.
Come detto in precedenza, dopo la parola chiave function va specificato il tipo di dato che la funzione restituira’. Questo puo’ essere di qualsiasi tipo (int, float, etc.), ed anche di un tipo nuovo che non e’ mai stato incontrato fin’ora: void.
Quando una funzione e’ di tipo void significa che non restituisce alcun valore.
Di seguito due esempi che mostrano due diverse funzioni, la prima restituisce un dato di tipo string:
In questo caso la funzione riceve un dato string e ne restituisce uno dello stesso tipo. Per restituire un dato, le funzioni utilizzano la parola chiave return seguita dal dato da restituire tra parentesi tonde ().
La stringa myFile.wav (variabile fileName) viene passata alla funzione, la quale aggiunge un’altra stringa contenente il nome del folder in cui lo script e’ contenuto (per fare cio’ viene utilizzato me.dir()) alla stringa ricevuta. L’output di questo programma, stampato sulla console (CTRL+0) e’:
"/home/myFile.wav" : (string)
Questo secondo esempio mostra l’utilizzo di una funzione che non restituisce alcun dato:
function void printTime() { <<< "Time Elapsed in Minutes: ", now/minute >>>; } printTime();
printTime non necessita di alcun argomento, e quando utilizzata stampa sulla console la frase “Time Elapsed in Minutes:“ piu’ un numero che rappresenta il tempo (in minuti) passato da quando la Virtual Machine di ChucK e’ stata avviata a quando la funzione e’ stata chiamata:
Time Elapsed in Minutes: 5.392267
Essendo printTime una funzione che non restituisce alcun valore, non contiente la parola chiave return.
Per ulteriori inforamazioni sulle funzioni in ChucK: ChucK FLOSS: Functions.
RIEPILOGO
funzioni
array
utilizzo di UGen per controllare la frequenza dell'oscillatore
messaggi MIDI control change
Il codice ed il testo possono essere scaricati qui: https://bitbucket.org/mariobuoninfante/chuck_workshop/src
Utilizzo di un device MIDI (controller, keyboard, virtual keyboard, etc.) per controllare un oscillatore sinusoidale.
Utilizzo della struttura di controllo if/else e if/else if.
PREREQUISITI
Per poter utilizzare questo programma serve avere un controller MIDI connesso al computer, o una virtual MIDI keyboard. Bisogna connettere (o aprire nel caso di una tastiera virtuale) il controller prima di far partire il programma.
Conoscenza base del protocollo MIDI (https://it.wikipedia.org/wiki/Musical_Instrument_Digital_Interface - http://www.nyu.edu/classes/bello/FMT_files/8_MIDIcomms.pdf - https://www.nyu.edu/classes/bello/FMT_files/9_MIDI_code.pdf)
ANALISI DEL PROGRAMMA
Nella parte 4 si e’ realizzato un semplice programma che permette di controllare un oscillatore sinusoidale ed il suo inviluppo via MIDI.
Con l’intento di offrire una gentile introduzione all’argomento, non sono state fatte una serie di considerazioni importanti riguardo la gestione di messaggi MIDI, infatti il precedente programma, nonostante perfettamente funzionante, non considera alcuni possibili situazioni in cui l’utente si puo’ trovare e che possono causare comportamenti indesiderati.
In questa nuova versione vengono implementate delle restrizioni che rendono il programma piu’ efficiente.
La parte iniziale e’ praticamente identica a quella del programma spiegato nella parte 4, a parte per l’introduzione di 4 nuove variabili: NOTE_ON, NOTE_OFF, MIDI_CHANNEL e lastNote.
SinOsc sine => Envelope env => dac; //connessioni MIDI MidiIn mIn; // crea un input MIDI MidiMsg msgIn; // crea un contenitore per i messaggi MIDI ricevuti mIn.open(1); // selezionare la porta MIDI che corrisponde al controller utilizzato (CTRL+2 per controllare le porte MIDI) // variabili 144 => int NOTE_ON; 128 => int NOTE_OFF; 0 => int MIDI_CHANNEL;// canale MIDI in un range da 0 a 15 float fr; float amp; int midiNote; int lastNote; 0.2 => amp; sine.gain(amp); // impostare l'ampiezza di "sine" su "amp", cioe' 0.2 10::ms => dur attackTime; // tempo di attacco 500::ms => dur releaseTime; // tempo di rilascio
Le prime 3 variabili rappresentano I due messaggi MIDI note on e note off ed il canale MIDI utilizzato dal device in uso. In pratica quando si preme un tasto su una tastiera MIDI, il messaggio e’ composto da 3 byte, di cui il primo specifica il tipo di messaggio (ie note on, cc, real time, etc.). Nel caso di note on il valore e’ appunto 144 + canale MIDI (in un range da 0 a 15). Quindi nel caso in cui il device invia messaggi sul canale MIDI 3, il byte sara’ 146.
Lo stesso discorso vale per il messaggio note off, con il valore del primo byte del messaggio uguale a 128 + canale MIDI.
La variabile lastNote verra’ utilizzata in seguito nel codice.
Il programma prosegue con il ciclo infinto while:
while(true) { // quando un (qualsiasi) messaggio MIDI viene ricevuto // avanza nel programma mIn => now; while(mIn.recv(msgIn)) { // stampa sulla console il messaggio MIDI raw <<< "Raw MIDI msg: " + msgIn.data1, msgIn.data2, msgIn.data3 >>>; // controlla che il messaggio MIDI sia un NOTE ON con velocity maggiore di 0 if(msgIn.data1 == (NOTE_ON + MIDI_CHANNEL) && msgIn.data3 != 0) { // prendere il MIDI byte 2 (nel caso di messaggi NOTE ON/OFF e' il pitch) // ed assegnarlo alla variabile di tipo int "midiNote" msgIn.data2 => midiNote; Std.mtof(midiNote) => fr; // conversione da MIDI a Freq in Hz sine.freq(fr); // assegnare la frequenza all'oscillatore msgIn.data2 => lastNote; // impostare il tempo impegato dall'inviluppo per // passare da 0 a 1 (da minimo a massimo) env.duration(attackTime); // triggerare l'inviluppo per farlo passare da OFF a ON env.keyOn(1); <<< "KEY PRESSED" >>>; } // controlla che il messaggio MIDI sia un NOTE OFF o un NOTE ON con velocity 0 // in entrambi i casi la nota MIDI deve combaciare con l'ultima suonata (playedNote) else if( (msgIn.data1 == (NOTE_ON + MIDI_CHANNEL) && msgIn.data3 == 0 && msgIn.data2 == lastNote) || (msgIn.data1 == (NOTE_OFF + MIDI_CHANNEL) && msgIn.data2 == lastNote) ) { // impostare il tempo impegato dall'inviluppo per // passare da 1 a 0 (da massimo a minimo) env.duration(releaseTime); // triggerare l'inviluppo per farlo passare da ON a OFF env.keyOff(1); <<< "KEY RELEASED" >>>; } } }
Anche questa parte del programma e’ molto simile a quella mostrata nella nella parte 4. Detto cio’, ci sono pero’ delle piccole ma importanti differenze.
Come prima cosa ogni qual volta un messaggio MIDI viene generato, questo viene stampato sulla console (CTRL+0). Cio’ permette di visualizzare I byte che sono parte del messaggio.
In seguito il costrutto if/else risulta essere leggermente diverso da quello usato in precedenza.
In questo caso il messaggio MIDI viene utilizzato per controllare la frequenza dell’oscillatore ed il suo inviluppo solo se e’ del tipo note on, se e’ stato inviato sul canale MIDI specificato all’inizio del programma e se la sua velocity e’ diversa da zero.
La novita’ risiede nel fatto che si stanno valutando piu’ condizioni, utilizzando piu’ operatori logici (prima condizione && seconda condizione), che in ChucK sono:
&& : and
|| : or
== : uguale
!= : diverso
> : maggiore di
>= : maggiore uguale di
< : minore di
<= : minore uguale di
Per maggiori info a proposito degli operatori logici si possono trovare molte fonti in rete (https://www.c-programming-simple-steps.com/logical-operators.html).
Nel caso in cui il messaggio ricevuto rispetti tutti I criteri specificati precedentemente, la nota MIDI viene convertita in frequenza ed utilizzata per controllare la frequenza dell’oscillatore e l’inviluppo viene triggerato dopo aver impostato il suo tempo di attacco su attackTime. Infine la nota MIDI viene assegnata alla variabile lastNote, che tornera’ utile in seguito.
Nel caso in cui il messaggio ricevuto non rispetti tutti I criteri specificati nel costrutto if, il codice tra le parentesi graffe {} che seguono if verra’ ignorato.
A questo punto si ha il costrutto else che, a differenza del programma utilizzato nella parte 4, e’ seguito da un altro costrutto if:
Questo rigo di codice non fa altro che dire: nel caso in cui la condizione specificata nel precedente if non sia soddisfatta, valuta la seguente:
e’ il primo byte di tipo note on?
e’ stato inviato sul canale MIDI giusto?
se e’ di tipo note on ha la velocity uguale a 0?
e’ il secondo byte (nota) uguale alla variabile lastNote?
Nel caso nessuna delle precedenti condizioni sia soddisfatta verfica le seguenti (il simbolo || significa or)
e’ il primo byte di tipo note off?
e’ stato inviato sul canale MIDI giusto?
e’ il secondo byte (nota) uguale alla variabile lastNote?
In pratica il codice contenuto nelle seguenti parentesi graffe {} (che consiste nel rilasciare la nota) viene eseguito solo nel caso in cui si riceve un messaggio note on con velocity pari a 0, o di tipo note off (la velocity non viene presa in considerazione), se questo e’ inviato sul canale MIDI giusto ma soprattutto se e’ uguale alla variabile lastNote.
Quest’ultima parte e’ di fondamentale importanza. Va ricordato che lo scopo del programma e’ quello di controllare un (semplicissimo al momento) synth monofonico, dove quindi si puo’ suonare una sola nota alla volta.
Bisogna dunque prendere in considerazione un importante possibile scenario. Nel caso in cui si preme un tasto della tastiera e successivamente, senza rilasciare il primo tasto se ne preme un altro, poi si rilascia il primo, I seguenti messaggi vengono generati:
note on (primo tasto premuto)
note on (secondo tasto premuto)
note off (primo tasto rilasciato)
Il fatto di aver assegnato il valore dell’ultima nota premuta alla varibile lastNote, permette di poter in seguito accettare solo messaggi note off provenienti dall’ultima nota, cioe’ la nota che corrisponde a lastNote.
Infatti, nel caso in cui questa condizione non esistesse, in una situazione come quella descritta nell’esempio precedente in cui due tasti vengono premuti e solo il primo viene rilasciato, al rilascio del primo tasto premuto, il messaggio di note off verrebbe preso in considerazione e la nota verrebbe rilasciata.
Dunque, l’accettare solo messaggi note off generati dall’ultima nota suonata, permette di premere e rilasciare piu’ tasti contemporaneamente, con la consapevolezza che solo l’ultima nota suonata verra’ presa in considerazione.
RIEPILOGO
operatori logici
struttura di controllo if/else e if/else if
Il codice ed il testo possono essere scaricati qui: https://bitbucket.org/mariobuoninfante/chuck_workshop/src
Utilizzo di un device MIDI (controller, keyboard, virtual keyboard, etc.) per controllare un oscillatore sinusoidale.
Utilizzo della struttura di controllo if/else.
PREREQUISITI
Per poter utilizzare questo programma serve avere un controller MIDI connesso al computer, o una virtual MIDI keyboard. Bisogna connettere (o aprire nel caso di una tastiera virtuale) il controller prima di far partire il programma.
Conoscenza base del protocollo MIDI (https://it.wikipedia.org/wiki/Musical_Instrument_Digital_Interface - http://www.nyu.edu/classes/bello/FMT_files/8_MIDIcomms.pdf - https://www.nyu.edu/classes/bello/FMT_files/9_MIDI_code.pdf)
ANALISI DEL PROGRAMMA
Anche in questa quarta parte, il programma inizia con la dichiarazione degli UGen, seguita pero’ da due oggetti nuovi: MidiIn e MidiMsg.
//oscillatore sinusoidale collegato ad un inviluppo SinOsc sine => Envelope env => dac; //connessioni MIDI MidiIn mIn; MidiMsg msgIn;
MidiIn rappresenta la porta MIDI utilizzata da ChucK (rappresenta il controller), mentre MidiMsg puo’ essere considerato come un contenitore in cui vengono contenuti I messaggi generati dal device MIDI.
Come tutti gli oggetti, questi necessitano di un nome, che in questo caso sono mIn per l’oggetto MidiIn e msgIn per MidiMsg.
Una volta creata la porta MIDI utilizzata dal programma, questa va collegata al device MIDI che si vuole utilizzare:
mIn.open("Virtual Keyboard");
Si utilizza il metodo open() il quale necessita di un argomento che puo’ essere o un numero intero o una stringa. L’argomento rappresenta il device MIDI a cui si vuole connettere la porta mIn.
In miniAudicle si possono visualizzare I device MIDI collegati al computer premendo CTRL+2, o cliccando su Window→Device Browser.
A questo punto, nel tab MIDI, apparira’ una lista di input e output MIDI, che corrisponde alle porte MIDI disponibili.
Nel caso di questo programma, va considerata solo la lista riguardante gli input, in quanto non si vuole inviare nessun messaggio da ChucK ad un apparecchiatura esterna, ma solo ricevere messaggi da un device esterno.
Si puo’ notare che la lista contiente sia numeri (indice dei device disponibili) che stringhe di testo (nomi dei device). Come gia’ detto in precedenza, il metodo open() puo’ ricevere sia il numero che il nome del device a cui ci si vuole connettere. In questo caso e’ stato utilizzato il nome del device "Virtual Keyboard" il quale ovviamente cambia a seconda del device connesso, quindi questa riga di codice va cambiata in base al controller a disposizione.
Una volta aperta la porta MIDI, le variabili vengono dichiarate ed inizializzate:
// variabili float fr; float amp; int midiNote; 0.2 => amp; sine.gain(amp); // impostare l'ampiezza di "sine" su "amp", cioe' 0.2 10::ms => dur attackTime; // tempo di attacco 500::ms => dur releaseTime; // tempo di rilascio
A seguire il ciclo while che rappresenta un loop infinito:
while(true) { mIn => now; while(mIn.recv(msgIn)) { msgIn.data2 => midiNote; Std.mtof(midiNote) => fr; // conversione da MIDI a Freq in Hz sine.freq(fr); // assegnare la frequenza all'oscillatore if(msgIn.data3 != 0) { env.duration(attackTime); env.keyOn(1); <<< "KEY PRESSED" >>>; } else { env.duration(releaseTime); env.keyOff(1); <<< "KEY RELEASED" >>>; } } }
Questo programma risulta essere leggermente piu’ complesso dei precedenti e riserva alcune novita’.
Come prima cosa va notato il primo rigo di codice all’interno del costrutto while:
mIn => now;
Come gia’ detto in precedenza, now viene utilizzato come delay, una sorta di punto in cui il programma si ferma per un determinato lasso di tempo, prima di procedere ed eseguire il codice seguente.
La novita’ risiede nel fatto che qui now viene utilizzato in combinazione con la porta MIDI mIn, invece che con una variabile di tipo dur (ie 100::ms => now, 2::second => now).
In pratica il risultato e’ che il programma aspetta affinche’ l’oggetto mIn, che rappresenta il controller MIDI, non invia un messaggio MIDI. Nel momento in cui questo accade, il programma prosegue.
A seguire si ha un altro costrutto while, anche questo leggermente diverso da quello degli esempi precedenti.
while(mIn.recv(msgIn))
In questo caso while valuta il valore restituito dal metodo .recv() a cui viene passato come argomento l’oggetto msgIn.
Nonostante questo passaggio possa sembrare al quanto complicato, quello che realmente succede e’ relativamente semplice: il codice contenuto tra le parentesi graffe {} che seguono while, viene eseguito ogni qual volta un messaggio MIDI viene generato dal controller MIDI (ie un tasto della tastiere viene premuto).
Il procedimento e’ il seguente:
Un tasto viene premuto sul controller
il controller genera ed invia un messaggio MIDI a ChucK
il messaggio raggiunge ChucK tramite la porta mIn
il messaggio viene salvato in msgIn utilizzando il costrutto mIn.recv(msgIn) – il quale indica che il messaggio ricevuto (da qui il nome del metodo recv) viene passato a msgIn.
Il ciclo while inizia ed al suo termine, aspetta un nuovo messaggio MIDI prima di ricominciare
A questo punto il secondo byte (msgIn.data2) del messaggio MIDI, che nel caso del messaggio note on/off rappresenta la nota suonata (vedi i link riguardanti il protocollo MIDI elencati nei PREREQUISITI), viene assegnato alla variabile midiNote.
Questa viene successivamente convertita in frequenza in hertz e utilizzata per controllare l’oscillatore.
msgIn.data2 => midiNote; Std.mtof(midiNote) => fr; // conversione da MIDI a Freq in Hz sine.freq(fr); // assegnare la frequenza all'oscillatore
Di seguito nel codice viene utilizzata una nuova struttura di controllo: if/else
Questa struttura non fa altro che valutare il codice contenuto nelle parentesi tonde () che seguono if, nel caso specifico msgIn.data3 != 0 e nel caso questo sia vero eseguire il codice contenuto tra le parentesi graffe {}, altrimenti nel caso in cui sia falso, ignorare il codice tra le parentesi graffe {} e proseguire.
Il costrutto else, che puo’ essere solo utilizzato in combinazione con if, offre un’alternativa. Quando msgIn.data3 != 0 e’ falso, viene eseguito il codice contenuto nelle parentesi graffe {} che seguono else.
Il codice msgIn.data3 != 0 non fa altro che valutare se il terzo byte (msgIn.data3) contenuto nel messaggio MIDI ricevuto e’ diverso da 0. Il terzo byte, nel caso di un messaggio MIDI note on/off, rappresenta la velocity.
Dunque questo pezzo di codice fa si che quando la velocity e’ diversa da zero (cioe’ quando si preme un tasto della tastiera), l’inviluppo che controlla il volume dell’oscillatore venga attivato in un tempo pari ad attackTime. Mentre quando la velocity e’ uguale a 0 (al rilascio della nota della tastiera), l’inviluppo viene rilasciato ed il volume dell’oscillatore torna ad essere 0 in un tempo pari a releaseTime.
Una volta eseguito questa parte di codice il loop riparte da
msgIn.data2 => midiNote;
e dunque il programma rimane in attesa di un nuovo evento, messaggio MIDI.
Per ascoltare il risultato di questo programma basta eseguirlo con un device MIDI connesso al computer, e generare messaggi MIDI note on/off (di solito generati da controller a tastiera, ma non solo).
Va ricordato che il nome del device in uso va specificato nella prima parte del programma:
mIn.open(”nome del device”)
oppure il suo numero del device, per esempio:
mIn.open(2)
RIEPILOGO
"MidiIn" e "MidiMsg" per controllare l'oscillatore con un device MIDI
struttura di controllo "if/else"
Il codice ed il testo possono essere scaricati qui: https://bitbucket.org/mariobuoninfante/chuck_workshop/src
Aggiunta di un inviluppo alla catena audio utilizzata fino ad ora.
ANALISI DEL PROGRAMMA
Come di consueto il programma inizia con la dichiarazione degli UGen e delle variabili utilizzate:
// oscillatore sinusoidale collegato ad un inviluppo SinOsc sine => Envelope env => dac; // variabili float fr; float amp; int midiNote; dur t;
In questo caso si ha un oscillatore sinusoidale collegato ad un UGen Envelope la cui uscita e’ collegata al dac.
Envelope e’ un generatore di inviluppo (Wikipedia - Generatore di inviluppo) di tipo AR, cioe’ ha solo due “stati”, attack e release. In pratica si possono impostare indipendentemente il tempo di attacco (tempo che intercorre tra volume 0 e volume massimo) e di rilascio (tempo che intercorre tra volume massimo e volume 0) che influenzaranno il segnale generato dall’oscillatore sinusoidale (env controlla il volume dell’oscillatore).
A seguire vengono inizializzate le variabili ed alcune di esse vengono passate come argomenti alle apposite funzioni:
0.2 => amp; sine.gain(amp); // impostare l'ampiezza di "sine" su "amp", cioe' 0.2 100::ms => dur attackTime; // tempo di attacco 990::ms => dur releaseTime; // tempo di rilascio
A questo punto il while loop che contiene il codice seguente:
Come prima cosa si genera una nota MIDI random utilizzando la funzione random2 parte della libreria Math, questa viene poi convertita in frequenza in hertz e passata all’oscillatore sinusoidale.
Viene poi impostato il parametro duration dell’inviluppo env. Questo permette di impostare la quantita’ di tempo che l’inviluppo impiega per passare da uno stato A ad uno stato B (ie. da 0 ad 1, da 1 a 0).
Nel caso di questo programma, viene passata la variabile di tipo dur, attackTime che corrisponde a 100 millisecondi.
Successivamente l’inviluppo viene triggerato utilizzando il metodo keyOn che, come il nome suggerisce, equivale all’attivazione dell’inviluppo (passaggio da 0, volume minimo, ad 1, volume massimo – equivale a premere un tasto di una tastiera).
Questo metodo necessita di un argomento che sia un numero intero. Nel caso in cui l’argomento sia 1, l’inviluppo passa da 0 ad 1, mentre nel caso l’argomento sia 0, l’inviluppo passa da 1 a 0.
A seguire la variabile attackTime, che rappresenta il tempo di attacco, viene utilizzata come tempo di attesa prima di proseguire con il programma. Questo delay serve a dare all’inviluppo il tempo di raggiungere il volume massimo prima di eseguire una qualsiasi altra operazione.
Una volta che env completa il passaggio di stato da silenzio a volume massimo, viene in pratica ripetuto il codice precedente con l’unica differenza che al posto di attackTime, viene utilizzata la variabile releaseTime (990 millisecondi), che rappresenta il tempo di rilascio. In piu’ per far si che l’inviluppo env passi da 1 a 0, viene utilizzato il metodo keyOff (che equivale a rilasciare un tasto di una tastiera) che, come keyOn, necessita di un argomento che sia un numero intero.
keyOff e’ l’esatto opposto di keyOn, infatti necessita di un argomento pari ad 1 per passare dal volume massimo al volume minimo, e di un argomento pari a 0 per passare dal volume minimo al volume massimo.
Infine releaseTime viene utilizzata come delay, tempo di attesa, per permettere ad env di raggiungere il volume minimo prima di eseguire una qualsiasi altra operazione.
RIEPILOGO
UGen: Envelope (inviluppo)
triggerare l'inviluppo
utilizzare piu' volte "now" in un solo loop
Il codice ed il testo possono essere scaricati qui: https://bitbucket.org/mariobuoninfante/chuck_workshop/src
Utilizzo di variabili al posto di valori hard coded (https://it.wikipedia.org/wiki/Codifica_fissa), conversione da note MIDI a frequenze in Hertz e generazione di valori random da utilizzare come frequenza per un oscillatore sinusoidale.
ANALISI DEL PROGRAMMA
Questo programma, come il precedente, utilizza un oscillatore sinusoidale controllato in frequenza ed ampiezza, quindi come prima cosa si dichiara un UGen del tipo SinOsc, di nome sine, collegato (=>) ad un dac (scheda audio del computer):
//oscillatore sinusoidale SinOsc sine => dac;
Di seguito viene introdotta un’importante novita’, i valori numerici utilizzati per impostare la frequenza e l’ampiezza dell’oscillatore sinusoidale, come per altri scopi, qui vengono assegnati a delle variabili.
//variabili float fr; float amp; int midiNote; dur t; //assegnare valori alle variabili ("valore => nome della variabile") 0.2 => amp; 500::ms => t;
Una variabile non e’ altro che un contenitore di informazioni (dati) che possiede un nome specifico e unico.
In ChucK le variabili possono essere di tipo:
int: numero intero (ie 1,-35, 9999)
float: numero in virgola mobile (ie 456.789, -23.45, .5, 0.678903)
time: tempo in ChucK (ie now+5::second, 34567::samp)
complex: numero complesso in forma rettangolare (ie #(2.3, 4), #(5, -1.235) )
polar: numero complesso in forma polare (ie %(2, 0.5*pi), %(45, -1.2*pi) )
string: stringa di testo (ie “monophonic synth”, “I’m a string”, “foo”)
Sono dichiarate nel seguente modo:
tipo nome;
float freq;
Nell’esempio di cui sopra, le variabili fr, amp, midiNote e t sono state dichiarate e sono di tipo float, int e dur.
Alle variabili amp e t vengono assegnati corrispettivamente I valori 0.2 (amp) e 500::ms (t), mentre le restanti non vengono inizializzate (verra’ fatto di seguito nel codice).
Si noti come per assegnare un valore ad una variabile si utilizza il ChucK operator (=>).
valore => variabile;
23.45 => freq; -245 => myNumber;
A seguire si ha di nuovo la struttura di controllo while in modalita’ loop infinito (utilizzando il costrutto while(true) ):
Nel primo rigo di codice, all’interno della struttura while, un numero intero random viene generato ed assegnato alla variabile midiNote. Questo rappresenta una nota MIDI (https://it.wikipedia.org/wiki/Musical_Instrument_Digital_Interface).
Per fare cio’ viene utilizzata una libreria ( https://it.wikipedia.org/wiki/Libreria_(software) ) chiamata Math, la quale contiente una serie di funzioni matematiche.
Il modo in cui le librerie vengono utilizzate in ChucK e’ il seguente:
nomeDellaLibreria + . + nomeDellaFunzione + (argomenti se necessari);
Math.sin(0.5*pi);
Per argomenti si intende I valori che vengono passati ad una funzione (possono essere passati da 0 a n argomenti, a seconda dalla funzione). Nel caso di Math.random2(48,72), gli argomenti sono 48 e 72, ed indicano il range entro cui viene generato il numero random intero (numero random da 48 a 72).
Successivamente il valore assegnato alla variabile midiNote viene convertito da nota MIDI a frequenza in Hertz utilizzando una funzione parte di una diversa libreria di ChucK, Std.mtof(midiNote). Std sta per standard, mentre mtof per MIDI to frequency (in molti linguaggi di programmazione viene utilizzato questo acronimo).
L’output di questa funzione viene assegnato alla variabile fr, che e’ di tipo float.
A questo punto vengono impostate frequenza e ampiezza dell’oscillatore sinusoidale, questa volta assegnando variabili al posto di digitare numeri.
Come ultima cosa vengono stampati sulla console il valore MIDI e l’equivalente in Hertz e poi impostato il tempo di attesa (la variabile t di tipo dur) prima che il loop riparta dall’inizio del costrutto while.
Cliccando su Add Shred si avra’ come risultato un oscillatore sinusoidale di ampiezza 0.2, la cui frequenza e’ impostata in modo random ogni 500 millisecondi. Al termine di ogni ciclo (loop) viene stampata sulla console (CTRL+0) la nota MIDI e il suo equivalente in Hertz.
Per terminare bisogna cliccare su Remove Shred.
RIEPILOGO
Dichiarare ed inizializzare (asseganare un valore) variabili
Utilizzo di funzioni e librerie
Generare numeri random interi
Conversione da MIDI a frequenza in Hertz
Il codice ed il testo possono essere scaricati qui: https://bitbucket.org/mariobuoninfante/chuck_workshop/src