Questo Notebook vuole fornire le conoscenze di base per risolvere le problematiche artistiche e tecnico/informatiche legate alla realizzazione di lavori audiovisivi sia interattivi che formalmente strutturati.
Seguiremo un percorso parallelo per affrontare gli stessi argomenti in due ambienti funzionali e percettivi differenti (tipologie di progetti):
La differenza consiste nel fatto che nel primo caso abbiamo due applicazioni differenti basate su linguaggi diversi che comunicano tra loro attraverso il protocollo OSC mentre nel secondo lavoriamo con librerie dello stesso linguaggio (JavaScript).
Nel proseguio di questo scritto queste due modalità saranno separate.
Processing è un software open source basato su Java dedicato alla realizzazione di immagini, animazioni e interazioni.
Nel corso degli anni per facilitare l'utilizzo nei diversi ambienti operativi all'interno dei quali possiamo utilizzare le funzionalità di Processing sono stati sviluppati alcuni progetti derivati che ci permettono di utilizzare altri linguaggi di programmazione.
from IPython.display import HTML
HTML('<center><img src="media/famiglia.png" width="55%"></center>')
Possiamo pensare all'insieme di questi linguaggi di programmazione come a diversi dialetti della stessa lingua.
Questo ci permette di imparare facilmente un nuovi linguaggi senza ripartire ogni volta da da zero in quanto ci sono vocaboli e sintassi simili.
Ad esempio p5.js è una libreria di JavaScript che include alcune funzionalità specifiche per la grafica e l'interazione.
from IPython.display import HTML
HTML('<center><img src="media/ling.png" width="70%"></center>')
Processing si presta alla realizzazione di progetti memorizzati all'interno di un computer.
Il risulatato grafico è renderizzato in una finestra (Display window) che può occupare tutto lo schermo (fullsize) oppure essere visualizzata su di un qualsiasi schermo o proiettore collegato al computer.
p5.js invece si presta all'integrazione in pagine HTML memorizzate su di un server ed il risultato grafico viene renderizzato in un broswer come Chrome.
Per questa tipologia di progetti utilizzeremo i due IDE (Integrated Development Environment) nativi.
from IPython.display import HTML
HTML('<center><img src="media/proc_ide1.png" width="55%"></center>')
Come nolti editor di questo tipo è suddivisio in due parti:
In alto sono presenti anche due bottoni: uno per eseguire il codice e l'altro per interrompere la computazione.
Per questa tipologia di progetti abbiamo a disposizione tre possibilità:
IDE nativo di Processing. Come vedremo nell'Intro Processing può essere programmato in tre diversi linguaggi informatici:
A seconda di quello che vogliamo utilizzare dobbiamo settare la modalità dal menù in alto a destra.
In tutti i casi per eseguire gli esempi di questo Notebook possiamo copiarli dalle celle e incollarli negli Editor scelti anche se consiglio vivamente di riscriverli da tastiera.
Processing
p5.js
I files di Processing e di p5.js si chiamano sketch (schizzi) e fondamentalmente hanno lo stesso schema sintattico.
Si aprono con la dichiarazione di eventuali variabili globali.
Nel compiere questa operazione ci sono alcune differenze in quanto Processing (basato su Java) è un linguaggio fortemente tipizzato e dobbiamo obbligatoriamente dichiarare il tipo di data mentre in p5.js (basato su JavaScript) e SuperCollider (basato su Smalltalk) no.
SuperCollider
a = 12; // int
b = 34.56; // float
c = $c; // char
d = \ciao; // Symbol
d = 'ciao ciao';
e = "ciao miao"; // string
f = true; // boleean
g = {arg ; }; // Function
h = []; // Array
// ...
Processing
int a = 12; // integer
float b = 34.45; // floating point
char d = 'c'; // character
String e = "ciao ciao"; // string
boolean = false; // boolean
void = setup(); // function
int[] f = {1,2,4,6}; // Array
// ...
p5.js
In p5 possiamo utilizzare la parola chiave let.
Sono variabili globali e/o locali (se dichiarate all'interno di un blocco di codice valgono solo la suo interno).
let ciao = 20.5;
{
let ciao = 'ciao ciao';
console.log(ciao); // al posto di print()...
}
console.log(ciao);
Per leggere il risultato in output dobbiamo cercare la Console JavaScript nei menù a tendina (in genere sotto Visualizza).
L'eventuale dichiarazione di variabili globali è seguita da due funzioni:
In entrambe i casi l'esecuzione avviene dall'alto verso il basso, linea dopo linea, da sinistra a destra.
Processing
int ciccio = 200; // Dichiarazione data type obbligatoria
void setup() {
}
void draw() {
}
p5.js
var ciaccio = 200;
function setup() {
}
function draw() {
}
In tutti e due i software le linee si chiudono con un punto e virgola
La cosa fondamentale per un software di grafica è generare immagini su di uno schermo.
from IPython.display import HTML
HTML('<center><img src="media/schermo_1.png" width="35%"></center>')
Processing renderizza il risultato della computazione nella display window.
void setup() {
size(480, 120); // crea una display window di 480x120 pixels
}
void draw() {
}
p5.js renderizza il risultato in un broswer qualsiasi come Chrome.
function setup() {
createCanvas(480, 120); // crea un 'foglio' di 480x120 pixels
background(220); // per visualizzarlo...
}
function draw() {
}
Notiamo come in entrambi i linguaggi viene utilizzata la functional notation.
Scriviamo il nome di un oggetto e gli passiamo gli argomenti all'interno delle parentesi tonde.
Ricordiamo che in SuperCollider possiamo utilizzare anche la reciver notation.
rand(12); // functional notation
12.rand; // reciver notation
Ma che argomenti (parametri) abbiamo passato a size() e createCanva()?
Lo schermo di un computer è come una griglia di elementi luminosi chiamati pixels (matrice bidimensionale) e il loro numero sull'asse orizzontale (x) e verticale (y) ne definisce la risoluzione.
from IPython.display import HTML
HTML('<center><img src="media/risoluzioni.png" width="85%"></center>')
La display window e il canvas sono esattamente come uno schermo e le loro coordinate 2D e 3D sono espresse in pixel.
from IPython.display import HTML
HTML('<center><img src="media/coordinate.png" width="70%"></center>')
Il punto in alto a sinistra alle coordinate (0, 0) è chiamato origine.
In Processing possiamo adattare allo schermo le dimensioni della diplay window (per uscire premere esc).
void setup() {
fullScreen();
}
void draw() {
}
In p5.js a causa delle restrizioni dei broswer è possibile attivare/disattivare la modalità fullscreen solo come conseguenza di un'azione da parte dell'utente.
function setup() {
createCanvas(displayWidth, displayHeight); // Crea un canvas delle dimensioni dello schermo
//createCanvas(windowWidth, windowHeight); // Crea un canvas delle dimensioni della finestra broswer
background(150);
}
function draw() {
}
function mousePressed() { // Se si clicca nell'angolo in alto a destra
// si attiva/disattiva la modalità fullscreen
if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) {
let fs = fullscreen();
fullscreen(!fs);
}
}
Disegnamo ora quattro cerchi di un pixel sullo schermo (esempio solo in Processing valido anche per p5.js).
void setup() {
size(300, 300);
background(255); // Bianco
ellipse(100,100,10,10); // x, y, larg, alt (Alto a sx)
ellipse(200,100,10,10); // x, y, larg, alt (Alto a dx)
ellipse(100,200,10,10); // x, y, larg, alt (Basso a sx)
ellipse(200,200,10,10); // x, y, larg, alt (Basso a dx)
}
void draw() {
}
Abbiamo definito le coordinate x e y in pixel assoluti.
Proviamo ora ad adattare il size alla schermo e osserviamo cosa cambia.
void setup() {
fullScreen();
background(255); // Bianco
ellipse(100,100,10,10); // x, y, larg, alt (Alto a sx)
ellipse(200,100,10,10); // x, y, larg, alt (Alto a dx)
ellipse(100,200,10,10); // x, y, larg, alt (Basso a sx)
ellipse(200,200,10,10); // x, y, larg, alt (Basso a dx)
}
void draw() {
}
Il rapporto tra la posizione dei cerchi è cambiato.
Questo perchè abbiamo utilizzato valori assoluti che sono sempre in relazione al punto d'origine (0,0) - ovvero l'angolo in alto a sinistra.
In questo caso possiamo definire le coordinate relative recuperando automaticamente le dimensioni dello schermo.
void setup() {
size(300,300);
// fullScreen();
background(255); // Bianco
ellipse(width/4 * 1, height/4 * 1, 10,10); // x, y, larg, alt (Alto a sx)
ellipse(width/4 * 3, height/4 * 1, 10,10); // x, y, larg, alt (Alto a dx)
ellipse(width/4 * 1, height/4 * 3, 10,10); // x, y, larg, alt (Basso a sx)
ellipse(width/4 * 3, height/4 * 3, 10,10); // x, y, larg, alt (Basso a dx)
}
void draw() {
}
C'è un ulteriore problema: nel primo caso le dimensioni dello schermo sono quadrate mentre nel secondo rettangolari e anche i rapporti interni al disegno vengono modificati.
Possiamo allora traslare il punto d'origine al centro e fare riferimento ad esso con valori assoluti.
void setup() {
// size(300,300);
fullScreen();
background(255); // Bianco
translate(width/2, height/2); // 0, 0 è al centro dello schermo
ellipse(-100, 100, 10,10); // x, y, larg, alt (Alto a sx)
ellipse( 100, 100, 10,10); // x, y, larg, alt (Alto a dx)
ellipse(-100, -100, 10,10); // x, y, larg, alt (Basso a sx)
ellipse( 100, -100, 10,10); // x, y, larg, alt (Basso a dx)
}
void draw() {
}
Esiste anche un altro oggetto che ruota le coordinate dello schermo in radianti (da 0 a 2PI).
void setup() {
// size(300,300);
fullScreen();
background(255); // Bianco
translate(width/2, height/2); // 0, 0 è al centro dello schermo
rotate(PI/4);
ellipse(-100, 100, 10,10); // x, y, larg, alt (Alto a sx)
ellipse( 100, 100, 10,10); // x, y, larg, alt (Alto a dx)
ellipse(-100, -100, 10,10); // x, y, larg, alt (Basso a sx)
ellipse( 100, -100, 10,10); // x, y, larg, alt (Basso a dx)
}
void draw() {
}
Per quanto riguarda p5.js possiamo utilizzare anche window.innerWidth e window.innerHeight.
Processing
Quando salviamo un progetto viene creata una nuova cartella con all'interno un file.pde. La cartella e il file devono avere lo stesso nome.
Questa cartella deve contenere anche tutti i materiali (immagini, suoni, etc.) che saranno utilizzati nel progetto.
p5.js
Quando salviamo un progetto viene creata una nuova cartella con all'interno:
from IPython.display import HTML
HTML('<center><img src="media/flusso.png" width="60%"></center>')
Vediamo alcune caratteristiche basilari di questi files.
HTML
from IPython.display import HTML
HTML('<center><img src="media/html.png" width="60%"></center>')
HTML + css
from IPython.display import HTML
HTML('<center><img src="media/duefile.png" width="60%"></center>')
HTML + css + JavaScript
from IPython.display import HTML
HTML('<center><img src="media/trefile.png" width="60%"></center>')
E' consuetudine separare le parti di codice in diversi files linkati.
from IPython.display import HTML
HTML('<center><img src="media/linka.png" width="60%"></center>')
Eseguiamo in un editor la versione p5.js dell'ultimo progetto.
function setup() {
createCanvas(windowWidth, windowHeight);
background(255);
translate(width/2, height/2);
rotate(PI/4);
ellipse(-50, 50, 10,10);
ellipse( 50, 50, 10,10);
ellipse(-50, -50, 10,10);
ellipse( 50, -50, 10,10);
}
function draw() {
}
Il file index.html che è stato generato è il seguente.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
# Titolo della pagina
<title>Test</title>
<link rel="stylesheet" type="text/css" href="style.css">
# Richiama p5 e le librerie o font o audio o video o altro...che sono online
<script src="https://cdn.jsdelivr.net/npm/p5@1.7.0/lib/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js"></script>
<script src="https://unpkg.com/tone-rhythm@0.0.2/dist/tone-rhythm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/webmidi@latest/dist/iife/webmidi.iife.js"></script>
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/tt2020-style-b" type="text/css"/>
# Richiama lo script
<script src="a0_Test.js"></script>
</head>
<body>
</body>
</html>
A questo link possiamo scaricare i files con i codici seguenti.
Come specificato nell'Introduzione per quanto riguarda la parte audio i casi di studio che seguono e gli esercizi saranno svolti sia con SuperCollider (del quale è richieta la conoscenza come prerequisito a questo scritto) che con una libreria di JavaScript che si chiama Tone.js.
Vediamone le caratteristiche di base che sono simili a quelle di SuperCollider e di molti altri software dedicati all'audio.
Nel menù a sinistra della documentazione le Classi sono suddivise per tipologia.
Segnali - corrispondono più o meno alle Ugens di SuperCollider (generano o modificano segnali audio).
Nei prossimi paragrafi sono affrontati gli argomenti principali che dovrebbero accompagnare all'autonomia per ulteriori approfondimenti personali.
A questo link troviamo numerose informazioni.
Operazioni preliminari.
Creiamo un nuovo progetto di p5.js come descritto in precedenza.
Andiamo a questo link.
Digitiamo 'tone.js'.
Copiamo il link minimizzato.
https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js
Apriamo il file index.html e sostituiamo il link che rimanda alla libreria p5.Sound con quello copiato.</br> N.B. Questa operazione va effettuata ogni volta che creiamo un nuovo progetto nel file 'index.html' corrispondente.
Salviamo il file index.html.
Come esempio significativo creiamo un algoritmo di sintesi molto semplice.
Oscillatore --> Inviluppo_ampiezza --> panner
Osserviamo che la struttura del file è la stessa di p5.js alla quale abbiamo aggiunto due nuove funzioni per poter controllare l'algoritmo di sintesi.
Una è valutata quando schiacciamo il pulsante del mouse (noteon) l'altra quando lo rilasciamo (noteoff).
La sintassi è intuitiva e commentata direttamente nello sketch.
let myOsc; // Variabili globali
let ampEnv;
let panner;
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("Clicca qua!",windowWidth/2,windowHeight/2);
// ----------------------------- Algoritmo di sintesi (definizione strumenti)
panner = new Tone.Panner().toDestination() // Agli altoparlanti (sempre per primo)
ampEnv = new Tone.AmplitudeEnvelope({
attack: 0.02, // da 0 a 2
decay: 0.05, // da 0 a 2
sustain: 0.5, // < 1
release: 0.8 // da 0 a 5
}).connect(panner); // Invia l'output al panner
myOsc = new Tone.Oscillator(440, "sine").connect(ampEnv); // Invia l'output all'inviluppo
myOsc.start(); // Accende l'oscillatore
}
function draw() {
// Non mettiamo segnali audio in queta funzione!
}
// ----------------------------- Controllo parametri (interazione o sequencing)
let toneStart = 0;
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
// Variabili locali
let freq = random(300,800); // Frequenza in Hz oppure...
let note = ["A4","C#4","E4","G#4"]; // Array con nome note
note = random(note); // Sceglie random dall'Array
let amp = random(1.0); // Ampiezza
//myOsc.type = "square"; // Modifica dinamicamente la forma d'onda
//myOsc.type = "triangle";
//myOsc.type = "sawtooth";
myOsc.type = "sine";
myOsc.partials = [0.011, 0.5,0.2,0.8]; // Ampiezze dei parziali (additiva)
myOsc.frequency.value = freq; // Setta la frequenza
// Trigger per inviluppi CON fase di sostegno
//ampEnv.triggerAttack("+0", amp); // (ritardo di attacco, ampiezza)
// Trigger per inviluppi SENZA fase di sostegno
// (tempo di sostegno, ritardo di attacco, ampiezza)
ampEnv.triggerAttackRelease(0.5,"+0", amp);
panner.pan.value = random(-1,1); // Setta il pan
}
function mouseReleased(){
//ampEnv.triggerRelease() // Per inviluppi con fase di sostegno
}
Gli oggetti di Tone.js hanno delle proprietà (Properties) che sono decritte negli Help files delle API.
Alcune di queste possono essere modificate dinamicamente.
Nel codice precedente ad esempio abbiamo modificato la forma d'onda dell'oscillatore mentre per i valori di frequenza e pan abbiamo utilizzato la seguente sintassi.
nome.parametro.value = valore;
Il valore viene inviato immediatamente come quando in SuperCollider utilizziamo il messaggio '.set(\arg, val)'.
Esattamente come in SuperCollider possiamo trasformare (.lag() e affini) il valore singolo in una rampa (segnale di controllo).
Nel codice seguente alcune possibilità offerte (decommentare le singole linee).
let myOsc;
let ampEnv;
let panner;
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("Clicca qua!",windowWidth/2,windowHeight/2);
// ----------------------------- Algoritmo di sintesi (definizione strumenti)
panner = new Tone.Panner().toDestination()
ampEnv = new Tone.AmplitudeEnvelope({
attack: 0.02,
decay: 0.05,
sustain: 0.5,
release: 0.8
}).connect(panner);
myOsc = new Tone.Oscillator(440, "sine").connect(ampEnv);
myOsc.start()
}
function draw() {
}
// ----------------------------- Controllo parametri (interazione o sequencing)
let toneStart = 0;
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
//myOsc.frequency.value = random(300,1600);
//myOsc.frequency.setValueAtTime(random(300,1600), "+1"); // Target, delay
//myOsc.frequency.rampTo(random(300,1600), 0.5); // Target, ramptime
//myOsc.frequency.linearRampToValueAtTime(random(300,1600), "+2");
//myOsc.frequency.exponentialRampToValueAtTime(random(300,1600), "+2")
//myOsc.frequency.setTargetAtTime(random(300,1600), "+2", 0.1); // Target, delay, ramptime
//myOsc.frequency.linearRampTo(random(300,1600), 0.8);
myOsc.frequency.exponentialRampTo(random(300,1600), 0.8);
panner.pan.rampTo(random(-1,1), 0.25);
ampEnv.triggerAttack("+0", 1); // CON fase di sostegno (noteon)
}
function mouseReleased(){
ampEnv.triggerRelease(); // (noteoff)
}
Tone.js fornisce una serie di strumenti con dei modelli di algoritmi di sintesi classici già programmati al loro interno.
Possiamo pensarli come delle SynthDef di SuperCollider già definite.
Come esempio utilizziamo Synth che è simile alla prima parte dell'algoritmo illustrato nel primo codice ovvero un oscillatore il cui segnale in output entra in un inviluppo d'ampiezza (per maggiori info consultiamo l'help file).
Tutti gli instrument sono monofonici.
let mySynth;
let panner;
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("Clicca qua!",windowWidth/2,windowHeight/2);
// ----------------------------- Algoritmo di sintesi (definizione strumenti)
panner = new Tone.Panner().toDestination() // Panner ESTERNO
mySynth = new Tone.Synth({
oscillator: { // Se non specifichiamo prende default
type: "triangle"
},
envelope: {
attack: 0.02,
decay: 0.05,
sustain: 0.5,
release: 0.8
}
}).connect(panner);
}
function draw() {
}
// ----------------------------- Controllo parametri (interazione o sequencing)
let toneStart = 0;
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
// ----------------------------- Oscillatore
let freq = random(900,1400); // Frequenza in Hz oppure
let note = ["A4","C#4","E4","G#4"]; // Stringa con nome note
note = random(note);
let amp = random(1.0);
//mySynth.oscillator.type = "square"; // Eventuale modifica dinamica della
// forma d'onda dell'oscillatore
// ----------------------------- Inviluppo
mySynth.envelope.attack = random(0.01,2); // Eventuale modifiche dinamiche di uno
mySynth.envelope.decay = random(0.01,2); // o più parametri dell'inviluppo
mySynth.envelope.sustain = random(0.01,1);
mySynth.envelope.release = random(0.01,5);
panner.pan.rampTo(random(-1,1), 0.25);
mySynth.triggerAttack(freq,"+0", amp); // (noteon)
}
function mouseReleased(){
mySynth.triggerRelease(); // (noteoff)
}
Per rendere polifonico un qualsiasi instrument possiamo utilizzare PoliSynth che è semplicemente un contenitore di instrument.
let myPoly;
let panner;
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("Clicca qua!",windowWidth/2,windowHeight/2);
// ----------------------------- Algoritmo di sintesi (definizione strumenti)
panner = new Tone.Panner().toDestination();
// tipo, n_voci
myPoly = new Tone.PolySynth(Tone.Synth, 56).connect(panner);
myPoly.set({ // Per settare i valori '.set({ })'
oscillator: {
type: "sine",
partials: [1, 0.5,0.2,0.8]
},
envelope: {
attack: 0.02,
decay: 0.05,
sustain: 0.5,
release: 5
}
});
}
function draw() {
}
// ----------------------------- Controllo parametri (interazione o sequencing)
let freq; // Variabili globali (devono valere sia in mousePressed() che in mouseReleased)
let note;
let amp;
let toneStart = 0;
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
freq = [random(300,1600),random(300,1600),random(300,1600),random(300,1600)];
note = ["A4","C#4","E4","G#4"];
amp = 0.2;
let durs = [0.1, 0.5, 1, 2];
//myPoly.triggerAttack(freq, "+0", amp); // Prova a modificare il delay
myPoly.triggerAttackRelease(freq, durs, "+0", amp);
panner.pan.rampTo(random(-1,1), 0.25);
}
function mouseReleased(){
//myPoly.triggerRelease(freq[3], "+0.2"); // Bisogna indicare quali note
//myPoly.triggerRelease(freq[2], "+0.6");
//myPoly.triggerRelease(freq[1], "+0.9");
//myPoly.triggerRelease(freq[0], "+0.9")
}
Possiamo definire sequenze di eventi nel tempo in diversi modi.
Una per instrument monofonici può essere definita con l'oggetto Sequence.
Accetta come primo argomento una funzione che viene valutata a tempi delta regolari (beat).
Questa funzione accetta due parametri:
Questi parametri sono passati come secondo e terzo argomento di Sequence.
let mySynth;
let panner;
let mySeq;
// Beats 1 2 3 4 5
let notes = ["C4", ["E4", "F4", "F#4", "G4"], ["E4", "E4"],["E4", null, "F#4"], ["G4","G4"]];
let amp;
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("Clicca qua!",windowWidth/2,windowHeight/2);
// ----------------------------- Algoritmo di sintesi (definizione strumenti)
panner = new Tone.Panner().toDestination();
mySynth = new Tone.Synth({
oscillator: {
type: "square"
},
envelope: {
attack: 0.02,
decay: 0.05,
sustain: 0.5,
release: 0.8
}
}).connect(panner);
// ----------------------------- Definizione sequencing
mySeq = new Tone.Sequence( // beat arg valuta la funzione ad ogni beat
function(time, note) {
amp = random(1.0);
panner.pan.rampTo(random(-1,1), 0.25);
mySynth.triggerAttackRelease(note, 0.1, time, amp)
},
notes, '4n');
// arg beat (arg della funzione)
Tone.Transport.bpm.value = 80; // Setta i BPM
Tone.Transport.start(); // Accende il Clock
}
// 16n: sedicesimi (4 per beat)
// 8n: ottavi (2 per beat)
// 4n: quarti (1 per beat)
// 2n: metà (2 beats per nota)
// 1n: intero (4 beats per nota)
// Pause: null
function draw() {
}
let toneStart = 0;
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
mySeq.start();
}
function mouseReleased(){
mySeq.stop();
}
Possiamo definire sequenze di ToneEvent.
Un ToneEvent si presenta nella seguente forma sintattica dove time corrisponde al tempo di onset e può essere definito in diversi modi:
{ time: "0:0:0", note: "C4", velocity: 0.9 }
Un Array di ToneEvent può essere letto dall'oggetto Part che accetta anche altri parametri (specificati nel codice seguente).
let mySynth;
let panner;
let mySeq;
// onsets
let score = [
{ time: "0:0:0", note: "C4", velocity: 0.9 }, // 1 ToneEvent
{ time: "0:1:0", note: "d4", velocity: 0.2 },
{ time: "0:1:2", note: "E4", velocity: 0.3 },
{ time: "0:1:3", note: "F4", velocity: 0.4 },
{ time: "0:2:0", note: "E4", velocity: 0.7 }, // Bicordo...
{ time: "0:2:0", note: "G4", velocity: 0.5 }, // Bicordo...
{ time: "0:3:0", note: "A4", velocity: 0.8 },
];
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("Clicca qua!",windowWidth/2,windowHeight/2);
// ----------------------------- Algoritmo di sintesi (definizione strumenti)
panner = new Tone.Panner().toDestination();
myPoly = new Tone.PolySynth(Tone.Synth, 8).connect(panner);
myPoly.set({
oscillator: {
type: "triangle"
},
envelope: {
attack: 0.02,
decay: 0.05,
sustain: 0.5,
release: 5
}
});
// ----------------------------- Definizione sequencing
mySeq = new Tone.Part(
// La funzione viene valutata ad ogni 'time' (onset)
// Notazione alternativa alla precedente per funzione
// beat args
(time, value) => {
myPoly.triggerAttackRelease(value.note, "8n", time, value.velocity);
panner.pan.rampTo(random(-1,1), 0.25);
},
score); // Array di ToneEvent (value)
Tone.Transport.bpm.value = 80; // Setta i BPM
Tone.Transport.start(); // Accende il Clock
}
function draw() {
}
let toneStart = 0;
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
mySeq.start();
mySeq.loop = 4; // Ripete 4 volte la sequenza
mySeq.loopStart = "0:0:0" // Inizio loop
mySeq.loopEnd = '1m' // Fine loop m = misura
mySeq.playbackRate = 1 // a questa velocità
mySeq.probability = 1; // Probabilità random tra 0 e 1
}
function mouseReleased(){
mySeq.stop();
}
Musicalmente non è particolarmente intuitivo pensare il tempo per onsets.
Possiamo allora utilizzare un'altra libreria di JavaScript (Tone-Rhythm.js) che ci consente di definire altezze, durate ed intensità sotto forma di Arrays similmente a quanto avviene ad esempio in SuperCollider iterando gli elementi con Routines.
Per utilizzare la libreria dobbiamo compiere la stessa operazione effettuata per Tone.js ovvero copiare nel file index.html il link che la richiama (verifichiamo il link alla versione più recente su GitHub).
<script src="https://unpkg.com/tone-rhythm@0.0.2/dist/tone-rhythm.min.js"></script>
L'oggetto toneRhythm.mergeMusicDataPart({ }) converte gli Array in formato JSON che può essere letto da Part.
let mySynth;
let panner;
let mySeq;
let score;
// bicordo pausa
let freq = ["Bb4", ["Ab4","C5"], "G4", null, "Eb4"];
// somma le durate
let durs = ['8n', '8n', ["4n","8n"], '8n', "4n" ];
let vels = [0.1, 0.2 , 0.5, 0, 1 ];
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("Clicca qua!",windowWidth/2,windowHeight/2);
// ----------------------------- Algoritmo di sintesi (definizione strumenti)
panner = new Tone.Panner().toDestination();
myPoly = new Tone.PolySynth(Tone.Synth, 8).connect(panner);
myPoly.set({
oscillator: {
type: "triangle"
},
envelope: {
attack: 0.02,
decay: 0.05,
sustain: 0.5,
release: 0.3
}
});
// ----------------------------- Definizione sequencing
score = toneRhythm.mergeMusicDataPart({ // Formatta gli Array in JSON
rhythms: durs,
notes: freq,
velocities: vels
});
mySeq = new Tone.Part(
(time, value) => {
myPoly.triggerAttackRelease(value.note, value.duration, time, value.velocity);
panner.pan.rampTo(random(-1,1), 0.25);
},
score);
Tone.Transport.bpm.value = 80;
Tone.Transport.start();
}
function draw() {
}
let toneStart = 0;
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
mySeq.start();
mySeq.loop = 4; // Ripete 4 volte la sequenza
mySeq.loopStart = "0:0:0" // Inizio loop
mySeq.loopEnd = '1m' // Fine loop m = misura
mySeq.playbackRate = 1 // a questa velocità
mySeq.probability = 1; // Probabilità random tra 0 e 1
}
function mouseReleased(){
mySeq.stop();
}
Per comodità riassumiamo le diverse unità di misura utilizzabili.
Tempo
Frequenze
Esistono anche librerie per definire scale, utilizzare microtoni ed altro, inoltre nei seguenti casi di studio utilizzeremo ulteriori funzionalità di Tone.js ma per gli approfondimenti viene richiesta curiosità e una ricerca personale.
Partiamo con il verificare diverse possibilità di creare un sound desing per il progetto grafico appena illustrato leggermente modificato.
void setup() {
size(300,300);
background(255);
translate(width/2, height/2);
// rotate(PI/4);
fill(0);
ellipse(-75, 75, 10,10);
ellipse( 75, 75, 10,10);
ellipse(-75, -75, 10,10);
ellipse( 75, -75, 10,10);
}
void draw() {
}
from IPython.display import HTML
HTML('<center><img src="media/caso_1.png" width="30%"></center>')
Analizziamo le caratteristiche visive.
Un obbiettivo che ci diamo consiste nel realizzare due piani percettivi indipendenti (audio / video) che descrivono la stessa idea.
La prima cosa che possiamo fare è pensare un suono che possa rappresentare al meglio a livello uditivo il singolo elemento visivo.
In questo caso il punto nero potrebbe essere ben rappresentato da un impulso di rumore bianco o simile.
s.boot;
s.scope;
s.plotTree;
(
SynthDef(\punto, {arg dur=1, t_gate=0, done=0, pos=0;
var sig, bpf, env, pan;
sig = ClipNoise.ar;
bpf = Env.perc(dur*0.1,dur*0.9);
env = EnvGen.ar(bpf,t_gate,doneAction:done);
pan = Pan2.ar(sig*env,pos);
Out.ar(0, pan)
}).add;
)
Synth(\punto,[\done,2,\dur,0.1,\pos,0,\t_gate,1]);
Il problema principale nel sonificare un'immagine statica consiste nel fatto che è priva della dimensione temporale.
Per ovviare, in questo caso possiamo sfruttare il fatto che è un pattern semplice e simmetrico, generando una pattern ritmico di quattro impulsi separati da una pausa più lunga.
(
r = Routine.new({
inf.do({
4.do({
Synth(\punto,[\done,2,\dur,0.1,\pos,0,\t_gate,1]);
1.wait;
});
2.wait;
})
}).reset.play;
)
r.stop;
La percezione del pattern a livello uditivo ora funziona ma manca la dimensione verticale (la sequenza rende l'idea di quattro punti ma tutti posizionati sulla stessa linea).
Possiamo allora sfruttare il panning posizionando i singoli impulsi in punti diversi del fronte stereofonico o, ancora meglio in altoparlanti diversi in un sistema di diffusione quadrifonico in cui gli altoparlanti stessi assumono idealmente la funzione dei punti.
(
r = Routine.new({
inf.do({
4.do({
Synth(\punto,[\done,2,\dur,0.1,\pos,[-1,-0.3,0.3,1].choose,\t_gate,1]);
1.wait;
});
2.wait;
})
}).reset.play;
)
r.stop;
La scelta randomica della posizione vuole evitare la meccanicità percettiva che potrebbe instaurarsi con patterns così elementari.
In questa versione relizziamo una piccola variazione visiva ruotando semplicemente di 90° il pattern.
function setup() {
createCanvas(windowWidth,displayHeight);
background(255);
translate(width/2, height/2);
rotate(PI/4);
fill(0);
ellipse(-75, 75, 10,10);
ellipse( 75, 75, 10,10);
ellipse(-75, -75, 10,10);
ellipse( 75, -75, 10,10);
}
function draw() {
}
from IPython.display import HTML
HTML('<center><img src="media/caso_1_2.png" width="30%"></center>')
In questo caso la sonificazione che abbiamo realizzato nell'esempio precedente potrebbe essere meno efficace in quanto la percezione del pattern visivo è leggermente diversa (la dimensione verticale assume maggiore importanza).
Possiamo sottolineare questa carateristica diversificando l'altezza dei suoni.
Per farlo dobbiamo scagliere un suono con una forma d'onda periodica e un timbro il più neutro possibile (da una semplice sinusoide a una sintesi additiva a spettro armonico fisso con pochi parziali).
Aggiungiamo un riverbero.
let rev;
let panner;
let ampEnv;
let myOsc;
let mySeq;
let notes = ["G5", "D6", "G5", "C5"]; // Sequenza pitches - Array
let pan = [-1, 0, 1, 0 ]; // panning
let amp = [0.75, 1, 0.75, 0.25]; // amp
function setup() {
// ----------------------------- Video
createCanvas(windowWidth, windowHeight);
background(255);
textAlign(CENTER, CENTER);
textSize(18);
text("Click Audio on/off",windowWidth/2,windowHeight/2);
translate(width/2, height/2);
rotate(PI/4);
fill(0);
ellipse(-75, 75, 10,10);
ellipse( 75, 75, 10,10);
ellipse(-75, -75, 10,10);
ellipse( 75, -75, 10,10);
// ----------------------------- Algoritmo di sintesi (definizione strumenti)
rev = new Tone.Reverb().toDestination(); // Riverbero
rev.decay = 1.7; // Secondi
rev.wet = 0.8; // 0 -> 1
panner = new Tone.Panner().connect(rev);
ampEnv = new Tone.AmplitudeEnvelope({attack:0.02,decay:0,Sustain:1,release:0.1}).connect(panner);
ampEnv.attackCurve = "ripple"; // Curve non lineari
ampEnv.releaseCurve = "exponential";
myOsc = new Tone.Oscillator(440, "sine").connect(ampEnv);
myOsc.partials = [0.011, 0.5,0.2,0.8];
myOsc.start();
// ----------------------------- Definizione sequencing
let i = 0; // Inizializza il contatore
let dur = '4n';
mySeq = new Tone.Sequence(
function(time, note) {
let amps;
let pans;
console.log(i); // Come print()
amps = amp[i]; // Itera gli Arrays
pans = pan[i];
myOsc.frequency.value = note;
panner.pan.rampTo(pans, dur);
ampEnv.triggerAttackRelease(0.02, "+0", amps);
i = (i+1) % notes.length; // Aggiorna il counter
// Operatore modulo
},
notes, dur);
Tone.Transport.bpm.value = 76;
Tone.Transport.start();
}
function draw() {
}
let toneStart = 0;
let i = 0; // Inizializza un contatore
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
i = (i+1) % 2; // Aggiorna un contatore che va da 0 a 1
if(i==0){
mySeq.stop();
}
else{
mySeq.start();
}
}
Possiamo iterare gli elementi di uno o più Array definendo un contatore per gli indici
String note[] = {"G5", "D6", "G5", "C5"}; // Sintassi per gli Array in Processing
float amp[] = {0.75, 1, 0.75, 0.25};
int id = 0; // Inizializza il counter FUORI dalla funzione
void draw() { // Necessario per campionare il mouse
}
void mousePressed() {
println(id, note[id], amp[id]); // Richiama gli indici
id = (id+1) % note.length; // Aggiorna il contatore
}
Possiamo includere in un disegno quattro tipi di oggetti bidimensionali (i link sono al reference di p5js che vale anche per gli oggetti corrispondenti di di Processing).
Nel primo caso di studio abbiamo disegnato dei punti (che in realtà erano delle ellissi...).
Questo oggetto fa parte di una categoria che comprende figure geometriche a due o tre dimensioni.
Molti di questi hanno delle proprietà comuni.
Per quanto riguarda i colori possiamo definire il modo attraverso il quale li componiamo con l'oggetto colorMode().
L'oggetto createColorPicker() genera una paletta dei colori che riporta i valori in entrambe i formati in modo più intuitivo.
Il codice seguente riassume tutte le principali funzionalità e vale sia per Processing che per p5.js.
int sx = 50; // Processing
int sy = 50;
//let sx = 50; // p5js
//let sy = 50;
void setup() { // Processing
//function setup() { // p5js
size(335,560); // Processing
//createCanvas(335,560); // p5js
background(215);
colorMode(RGB, 255);
smooth();
// ----------------------------- arc()
ellipseMode(CENTER);
noFill(); // Senza contenuto
strokeWeight(5); // Altezza contorno
// x y largh alt inizio fine modo
arc(sx + 25, sy + 0, 50, 50, HALF_PI, PI, OPEN);
fill(150); // Grigi (0-255)
arc(sx + 100, sy + 0, 50, 50, PI*0, PI*1.0, CHORD);
fill(80,60); // Grigio, Alpha
arc(sx + 125, sy + 0, 50, 50, PI*0, PI*1.5, PIE);
// ----------------------------- ellipse()
fill(123,234,145); // RGB
stroke(234,16,165); // Colore contorno
ellipse(sx + 200, sy + 0, 50, 50); // x y largh alt
fill(132,134,245,200); // RGBA
noStroke();
// ----------------------------- circle()
circle(sx + 215, sy + 0, 50); // x y raggio
rectMode(CENTER);
fill(0,0,255,160);
rect(sx + 25, sy + 100, 50, 30, 10); // x y largh alt curva1 curva2 etc
// ----------------------------- rect()
noFill();
stroke(100);
strokeWeight(2);
rect(sx + 110, sy + 100, 50, 50, 10); // Processing
//rect(sx + 110, sy + 100, 50, 50, 10); // p5js
// ----------------------------- square()
fill(143,219,198,189);
strokeJoin(BEVEL); // Tipo arrotondamento
square(sx + 210, sy + 100, 50);
// ----------------------------- line()
stroke(0);
strokeCap(ROUND); // Tipo arrotondamento
line(sx + 0, sy + 175, sx + 50, sy + 175); // x1 y1 x2 y2
strokeCap(SQUARE);
strokeWeight(1);
stroke(255);
line(sx + 75, sy + 185, sx + 150, sy + 165);
strokeCap(PROJECT);
strokeWeight(5);
stroke(0);
line(sx + 175, sy + 175, sx + 225, sy + 175);
// ----------------------------- triangle()
strokeWeight(1);
fill(200,0,0); // In senso orario
triangle(sx + 0, sy + 225, // x1 y1
sx + 50, sy + 275, // x2 y2
sx + 0, sy + 275); // x3 y3
stroke(20);
strokeJoin(ROUND);
strokeWeight(10);
noFill();
triangle(sx + 125, sy + 225,
sx + 150, sy + 250,
sx + 75, sy + 275);
noStroke();
fill(0,200,0, 50); // Con Alpha...
triangle(sx + 215, sy + 225,
sx + 250, sy + 275,
sx + 175, sy + 275);
// ----------------------------- quad()
strokeWeight(1);
fill(100,134,155); // In senso orario
quad(sx + 0, sy + 320,
sx + 50, sy + 335,
sx + 50, sy + 360,
sx + 0, sy + 375);
stroke(20);
strokeJoin(ROUND);
strokeWeight(2);
noFill();
quad(sx + 75, sy + 335,
sx + 150, sy + 335,
sx + 150, sy + 360,
sx + 75, sy + 360);
noStroke();
fill(167,100,0, 50); // Con Alpha...
quad(sx + 175, sy + 335,
sx + 250, sy + 320,
sx + 250, sy + 345,
sx + 175, sy + 360);
// ----------------------------- Shape()
stroke(20);
fill(167,100,112);
beginShape();
vertex(sx + 0, sy + 420); // Singolo punto
vertex(sx + 55, sy + 435);
vertex(sx + 40, sy + 460);
vertex(sx + 20, sy + 450);
vertex(sx + 15, sy + 485);
endShape(CLOSE);
// ----------------------------- Curved Shape()
strokeWeight(5);
point(sx + 80, sy + 420);
point(sx + 120, sy + 440);
point(sx + 100, sy + 460);
point(sx + 140, sy + 480);
strokeWeight(1);
noFill();
beginShape();
curveVertex(sx + 80, sy + 420);
curveVertex(sx + 80, sy + 420);
curveVertex(sx + 120, sy + 440);
curveVertex(sx + 100, sy + 460);
curveVertex(sx + 140, sy + 480);
curveVertex(sx + 140, sy + 480);
endShape();
strokeWeight(5);
point( sx + 190, sy + 420);
point( sx + 240, sy + 430);
point( sx + 190, sy + 460);
point( sx + 240, sy + 480);
// ----------------------------- bezier()
strokeWeight(1);
beginShape();
vertex( sx + 190, sy + 420); // x1 y1
bezierVertex(sx + 240, sy + 430, // x2 y2
sx + 190, sy + 460, // x3 y3
sx + 240, sy + 480); // ancora1 ancora2
endShape();
}
void draw() { // Processing
//function draw() { // p5js
}
from IPython.display import HTML
HTML('<center><img src="media/geom.png" width="30%"></center>')
Possiamo trovare ulteriori funzionalità negli Esempi.
Possiamo visualizzare immagini memorizzate in files.
I formati accettati sono: .gif, .jpg, .png e .svg.
Consiglio di mettere tutte le immagini in una cartella dedicata all'interno della cartella principale.
Carichiamo l'immagine con l'oggetto loadImage() possibilmente in una funzione preload() per essere sicuri di caricarlo prima della visualizzazione.
Quando caricato possiamo inserire nel canvas quante copie vogliamo definendone la posizione e le dimensioni.
Possiamo modificarne l'origine con l'oggetto imageMode(). Possiamo impiegare un'immagine come background.
Possiamo modificarne il colore e la trasparenza con l'oggetto tint().
Possiamo applicare dei filtri con l'oggetto filter() che agisce sugli oggetti che lo precedono nel codice (anche shapes).
Esempio Processing.
PImage img;
PImage bg;
void setup() {
img = loadImage("img/picasso.jpg"); // Carica l'immagine
bg = loadImage("img/boccioni.jpg");
size(208, 209); // Se usiamo un'immagine come background
// deve avere le stesse dimensioni
background(bg); // Immagine come background
//background(210);
imageMode(CORNER); // Default
//imageMode(CENTER);
filter(BLUR,3); // Solo su ciò che precede
image(img, 10, 10); // x y
tint(255, 150); // RGBA
image(img, 10, 20+img.height,
img.width * 0.3, // Larghezza
img.height * 0.3); // Altezza
noTint(); // Annulla tint()
image(img, 130, 30+img.height,
img.width * 0.17,
img.height * 0.1); // Deformata
}
void draw() {
}
from IPython.display import HTML
HTML('<center><img src="media/imma0.png" width="30%"></center>')
Esempio p5.js.
let img;
let bg;
function preload() {
img = loadImage('img/picasso.jpg'); // Carica l'immagine
bg = loadImage('img/boccioni.jpg');
}
function setup() {
createCanvas(windowWidth, windowHeight);
background(bg); // Immagine come background
//background(210);
imageMode(CORNER); // Default
//imageMode(CENTER);
filter(THRESHOLD,0.3); // Solo su ciò che precede
image(img, 20, 20); // x y
tint(255, 150); // RGBA
image(img, 20+img.width, 20+img.height,
img.width / 2, // Larghezza
img.height / 2); // Altezza
noTint(); // Annulla tint()
image(img, 20, 215,
img.width,
img.height * 0.22); // Deformata
}
function draw() {
}
Possiamo trovare ulteriori funzionalità negli Esempi.
Per le caratteristiche proprie dei due diversi ambienti riguardo questo argomento dobbiamo separare le strategie di programmazione.
Processing
Possiamo utilizzare i fonts installati nel computer sul quale viene eseguito lo sketch (system fonts).
Possiamo anche includerli sotto forma di file (.ttf o .otf) in una cartella dedicata come abbiamo fatto per le immagini, in questo modo saremo sicuri che se condividiamo la cartella tutto ciò che è necessario per la corretta esecuzione dello sketch è presente.
Otteniamone una lista eseguendo il codice seguente (anche in static mode ovvero senza le funzioni setup e void).
String[] fontList = PFont.list();
printArray(fontList);
Convertiamo i fonts dai formati TrueType (.ttf) o OpenType (.otf) in modo da poterli impiegare in uno sketch.
Li assegnamo a un particolare tipo di variabile: PFont.
Definiamo il punto del font che assume l'origine (x, y).
Definiamo il colore.
Assegnamo il font da utilizzare.
Eventualmente modifichiamo il size.
Visualizziamo il testo in una posizione (x, y).
PFont trattarello;
PFont ptSerif;
PFont strambo;
String a = "Ciao";
String b = "Miao";
String c = "Uao!";
void setup() {
size(200,200);
background(235);
colorMode(RGB, 255);
smooth();
//trattarello = loadFont("AlexBrush-Regular",12); // Se salvati in locale
trattarello = createFont("Trattatello",12); // Converte i font
ptSerif = createFont("PTSerif-Regular",12);
strambo = createFont("Mshtakan",12);
textAlign(LEFT, CENTER); // Punto del font che specifica la posizione (x y)
fill(150);
textFont(trattarello); // Assegna il font
textSize(27); // Modifica il size
text(a, 10, 30); // Visualizza
fill(0);
textFont(ptSerif);
textSize(47);
text(b, 50, 90);
fill(123,165,254,150);
textFont(strambo);
textSize(35);
text(c, 100,170);
}
void draw() {
}
from IPython.display import HTML
HTML('<center><img src="media/textP.png" width="20%"></center>')
p5.js
Possiamo utilizzare i fonts installati nel computer sul quale viene eseguito lo sketch (system fonts).
Teniamo però presente che a differenza di Processing ciò che disegnamo non sarà eseguito in locale ma su computers collegati attraverso il Web e non tutti potrebbero avere i font utilizzati.
I fonts più comuni che non dovrebbero dare problemi sono: “Arial”, “Courier” “Courier New”, “Georgia”, “Helvetica”, “Palatino”, “Times New Roman” “Trebuchet MS” e “Verdana”.
Per avere la sicurezza che siano disponibili per la corretta esecuzione dello sketch li dobbiamo includere sotto forma di file (.ttf o .otf) in una cartella dedicata come abbiamo fatto per le immagini.
In alternativa possiamo utilizzare Webfont come Googlefont o Open font library specificando il link di riferimento nel file index.html dello sketch.
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/tt2020-style-b" type="text/css"/>
let tratterello; // System fonts
let alex; // Font locale
let averia; // Web font
let a = "Ciao";
let b = "Miao";
let c = "Uao!";
function preload() {
alex = loadFont('font/AlexBrush-Regular.ttf'); // Carica i fonts locali
}
function setup() {
createCanvas(windowWidth, windowHeight);
background(235);
colorMode(RGB, 255);
smooth();
textAlign(LEFT, CENTER); // Punto del font che specifica la posizione (x y)
fill(150);
textFont('trattarello'); // Assegna il font di sistema
textSize(20); // Modifica il size
text(a, windowWidth/6, 30); // Visualizza
fill(0);
textFont(alex); // Font locale
textSize(47);
text(b, windowWidth/2.5, windowHeight/2.5);
stroke(0);
strokeWeight(3);
fill(123,165,254,150);
textFont('TT2020StyleBRegular'); // Web font
textSize(45);
text(c, windowWidth/1.3,windowHeight/1.5);
}
function draw() {
}
Possiamo trovare ulteriori funzionalità negli Esempi.
Disegnamo una composizione visiva statica formata da quattro elementi.
L'immagine è ri-composta randomicamente ogni volta che riceve un trigger.
Il trigger in questo caso è generato da un'inter-azione con la tastiera del computer (premere un tasto qualsiasi) oppure sul mouse (click) ma potrebbe essere generato da qualsiasi device esterno come una tastiera o controller MIDI, un sensore di Arduino, un interazione con smartphone, etc.
PImage img;
int pos;
float scale = 0.8; // Fattore di riscalaggio immagine
int nrect = 10; // Numero di elementi
int nline = 4;
int npoint = 25;
void setup() {
img = loadImage("img/picasso.jpg");
//fullScreen();
size(600,600);
noCursor(); // Fa sparire il cursore
background(255);
smooth();
};
void draw() { // Per campionare gli istanti in cui
// premiamo un tasto o clicchiamo col mouse
};
void disegno() { // Funzione che genera il disegno
println("Trigger"); // Monitor
background(255); // Prima pulisce lo schermo...
// ----------------------------- Immagine
tint(random(150,255));
imageMode(CENTER);
image(img, width/2,height/2, img.width * scale, img.height * scale);
// ----------------------------- Rettangoli
for (int i = 0; i < nrect; i = i+1) {
if(random(-1, 1) > 0) {
stroke(0); // Bordo
strokeWeight(random(1,3));
} else {
noStroke(); // No bordo
}
if(random(-1, 1) > 0) {
fill(random(255),random(255)); // Grigio
} else {
fill(random(255),random(255), // Colori
random(255),random(255));
}
rect(random(width-100),random(height-100),
random(100),random(100));
};
// ----------------------------- Linee
for (int i = 0; i < int(nline/2); i = i+1) {
stroke(random(255)); // Grigi
strokeWeight(1);
line(random(width), // Verticali
0,
random(width),
height);
line(0, // Orizzontali
random(height),
width,
random(height));
};
// ----------------------------- Punti
for (int i = 0; i < npoint; i = i+1) {
stroke(0); // Nero
strokeWeight(random(20));
point(width /2 + random(-100,100),
height/2 + random(-100,100));
}
}
void keyPressed() { // Tasto
disegno(); // Valuta la funzione...
}
void mousePressed() { // Click mouse
disegno(); // Valuta la funzione...
}
from IPython.display import HTML
HTML('<center><img src="media/caso2.png" width="60%"></center>')
Nuovi argomenti:
Per la generazione di numeri pseudocasuali abbiamo a disposizione l'oggetto random() che restituisce sempre float.
float a = random(100); // Tra 0 e n
float b = random(-50,50); // Tra min e max
int c = int(random(100)); // Casting in int
println(a);
println(b);
println(c);
Possiamo definire un disegno all'interno di una funzione per poi farlo comparire o modificarlo alla ricezione di un trigger.
void setup() {
size(400,400);
background(255);
};
void draw() {
};
void disegno() {
background(255); // Pulisce ad ogni click
ellipse(mouseX, // Recupera la posizione del mouse
mouseY,
int(random(1,100)),
int(random(1,100)));
}
void keyPressed() {
disegno();
}
void mousePressed() {
disegno();
}
size(400, 400);
for (int i = 0; i < 420; i = i+20) {
rect(i, i, random(20), 20);
}
// -----------------------------
int i = 0; // Fuori dal loop
while (i < 420) {
int size = int(map(i,0,420,0,20)); // Riscala
ellipse(i,
420 - i, // Inverso...
size, size);
i = i + 20;
}
void setup() {
size(300,300);
background(255);
smooth();
};
void draw(){
};
void mousePressed(){ // Quando clicchiamo
if (mouseX > 150) { // se il mouse è nella metà destra
fill(0); // ...nero
} else { // altrimenti
fill(200); // Grigio
};
ellipse(width/2, height/2, 80, 80);
};
void keyPressed() {
char lettera = key; // Recupera il char del tasto che abbiamo premuto
switch(lettera) {
case 'r': // Rosso
case 'R':
fill(255,0,0);
break;
case 'b': // Blu
case 'B':
fill(0,0,255);
break;
}
ellipse(width/2, height/2, 80, 80);
}
Per quanto riguarda la sonorizzazione possiamo:
Mappare ognuno dei quattro elementi visivi con altrettante sequenze musicali che ne sottolineino le caratteristiche percettive.
Per poi scegliere tra due diverse impostazioni formali.
La prima la realizziamo con Processing e SuperCollider mentre la seconda con p5.js.
Scegliamo elementi minimi anche per quanto riguarda il suono.
La patritura alterna questi elementi in modo continuamente cangiante ma non cambia al modificarsi del pattern visivo.
s = Server.local;
s.waitForBoot{
s.meter;
s.scope;
s.plotTree;
// ----------------------------- Strumenti
~snd = Buffer.read(s, "voce.aif".resolveRelative); // Carica il soundfile
SynthDef(\rev, {arg busIn=7,mix=0.5,room=0.5,damp=0.5; // Riverbero
var in, rev;
in = In.ar(busIn, 2);
rev = FreeVerb2.ar(in[0],in[1],mix,room,damp);
Out.ar(0,rev)
}).add;
SynthDef(\voce, {arg buf=0,pos=0,dur=0.2,amp=0,dir=1,pan=0,t_gate=0;
var sig,env;
sig = PlayBuf.ar(1, buf,
BufRateScale.kr(buf) * dir, t_gate,
BufSampleRate.kr(buf)* pos);
env = Env.linen(0.01,dur-0.02,0.01);
env = EnvGen.kr(env,t_gate,doneAction:2);
sig = Pan2.ar(sig*env*amp,pan);
Out.ar(7,sig) // Al riverbero
}).add;
SynthDef(\punto, {arg freq=400,amp=0,dur=1,atk=0.2,pan=0;
var sig, env;
sig = SinOsc.ar(freq);
env = Env.perc(atk, dur - atk);
env = EnvGen.ar(env, 1, doneAction:2);
sig = sig * env * amp;
sig = Pan2.ar(sig, pan);
Out.ar(7, sig) // Al riverbero
}).add;
SynthDef(\linea, {arg amp=0,dur=1,pan=0;
var sig, env;
sig = ClipNoise.ar;
env = Env.perc(dur*0.1,dur*0.9);
env = EnvGen.ar(env, 1, doneAction:2);
sig = sig * env * amp;
sig = Pan2.ar(sig, pan);
Out.ar(0, sig)
}).add;
SynthDef(\quad, {arg cutoff=400,qf=1,amp=0,dur=1,pan=0;
var sig,env,atk;
sig = WhiteNoise.ar;
sig = BPF.ar(sig, cutoff, qf);
atk = Decay.ar(Impulse.ar(0), // Bordo
dur * Rand(0.1,0.5)) * Rand(0.1,0.3);
env = Env.perc(0.01, dur - 0.01);
env = EnvGen.ar(env, 1, doneAction:2);
sig = sig * env * amp + atk;
sig = Pan2.ar(sig, pan);
Out.ar(0, sig)
}).add;
// ----------------------------- Partitura
{
Synth(\rev,[\mix,0.3,\room,0.8,\damp,0.5]);
~voci = Routine.new({
inf.do({
rrand(1,4).do({
Synth(\voce, [\buf,~snd,
\pos, rand(~snd.duration),
\dur,rrand(0.1,0.3),
\amp,exprand(1.0,4),
\dir,[1,-1].choose,
\pan,rand2(1),
\t_gate,1]);
rrand(0.01,0.2).wait;
});
[0.5,2,6].choose.wait;
})
}).play;
~punti = Routine.new({
inf.do({
rrand(4,7).do({
Synth(\punto, [\freq,rrand(600,2500),
\amp,[0.2,0.1,0.4,0.01].choose,
\dur,0.1,
\atk,0.01,
\pan,rand2(1.0)]);
rand(0.1,3).wait;
});
1.5.wait;
})
}).play;
~linee = Routine.new({
inf.do({
4.do({
Synth(\linea,[\amp,rand(0.2),
\dur,rrand(0.01,0.05),
\pan,rand2(1.0)]);
rrand(0.02,0.1).wait;
});
2.5.wait;
})
}).play;
~rettangoli = Routine.new({
inf.do({
9.do({
Synth(\quad, [\amp,30,
\cutoff,rrand(1500,4000),
\qf, 0.001,
\dur,rrand(0.1,1)]);
rrand(0.2,2.0).wait;
});
0.5.wait;
})
}).play;
}.defer(3); // Ritarda l'esecuzione di 3 secondi'
}
Prima di approcciare la seconda versione del secondo caso di studio con p5.js eploriamo le principali potenzialità di Tone.js per quanto riguarda la riproduzione e l'elaborazione di sound files.
Per riprodurre sound files dobbiamo:
Possiamo:
Esempio monofonico.
let pth = "suoni/voce.mp3"; // Path al sound file
let buffer;
let player;
function preload() {
buffer = new Tone.ToneAudioBuffer(pth); // Buffer
};
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("Clicca qua!",windowWidth/2,windowHeight/2);
player = new Tone.Player(buffer).toDestination(); // Player
player.autostart = false;
player.playbackRate = 1;
player.reverse = false;
player.volume.value = 0; // in dB
player.fadeIn = 0.3;
player.fadeOut = 0.02;
player.loopStart = 0.5;
player.loopEnd = 0.8;
player.loop = true;
};
function draw() {
};
let toneStart = 0;
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
// delay offset dur
player.start("+0", 0.6, 1.5); // Trigger
//player.start(); // Tutto il file...
console.log("dur: " + buffer.duration); // Info utili...
console.log("samples: " + buffer.length);
console.log("sr: " + buffer.sampleRate);
console.log("canali: " + buffer.numberOfChannels);
}
function mouseReleased() {
//player.stop();
//player.loop = false;
}
Esempio polifonico.
Ad ogni trigger creiamo un nuovo player e lo facciamo partire (eventualmente con un ritardo).
let pth = "suoni/voce.mp3"; // Path al sound file
let buffer;
let player;
function preload() {
buffer = new Tone.ToneAudioBuffer(pth); // Buffer
};
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("Clicca qua!",windowWidth/2,windowHeight/2);
};
function draw() {
};
let toneStart = 0;
function mousePressed(){
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
player = new Tone.Player(buffer).toDestination(); // Player
player.fadeIn = 0.02;
player.fadeOut = 0.02;
player.start("+0", // Trigger
random(0, buffer.duration-2.7),
random(0.05, 2.7));
}
Sound files multipli
Se vogliamo invece triggerare più soundfiles diversi possiamo farlo con l'oggetto Players() che è semplicemente un contenitore di Player().
Possiamo accedere alle proprietà ed ai metodi di ogni singola istanza come specificato nel codice seguente.
Anche la gestione della polifonia (della singola istanza) segue la logica di Player().
let pths = {voce: "suoni/voce.mp3",
bach: "suoni/bach.mp3",
crot: "suoni/crotal.mp3"};
let multiplayer;
function preload() {
multiplayer = new Tone.Players(pths).toDestination();
};
function setup() {
createCanvas(windowWidth, windowHeight);
background(220);
textAlign(CENTER, CENTER);
textSize(24);
text("a = Voce, s = Bach, d = Crotali, spazio = Stop",windowWidth/2,windowHeight/2);
};
function draw() {
};
let toneStart = 0;
function keyTyped() {
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
if (key === 'a') {
multiplayer.player("voce").reverse = true;
multiplayer.player("voce").start();
} else if (key === 's') {
multiplayer.player("bach").start("+0",0.5,0.5);
} else if (key === 'd') {
multiplayer.player("crot").playbackRate = 0.5;
multiplayer.player("crot").start();
}
if (key === ' ') { // Barra spazio
multiplayer.stopAll()
}
}
Realizziamo un mapping asincrono impiegando le nuove conoscenze.
Generiamo dei pattern ritmici che richiamano il pattern ritmico visivo e che cambiano sfasati nel tempo rispetto a quest'ultimo.
Siccome le morfologie visive presenti sono quattro generiamo quattro istanze di un player di sounfiles (mapping).
let img;
let pos;
let scale = 1.5; // Fattore di riscalaggio immagine
let nrect = 10; // Numero di elementi
let nline = 4;
let npoint = 25;
let buffer;
let rev;
function preload() {
img = loadImage("img/picasso.jpg"); // Immagine
buffer = new Tone.ToneAudioBuffer("suoni/voce.mp3"); // Suono
};
function setup() {
createCanvas(windowWidth, windowHeight);
noCursor();
background(220);
smooth();
rev = new Tone.Reverb().toDestination(); // Un solo riverbero
rev.decay = 1.3;
rev.wet = 0.4;
};
function draw() {};
function disegno() { // Funzione che genera il disegno
console.log("Trigger"); // Monitor
background(255); // Prima pulisce lo schermo...
// ----------------------------- Immagine
tint(random(150,255));
imageMode(CENTER);
image(img, width/2,height/2, img.width * scale, img.height * scale);
// ----------------------------- Rettangoli
for (let i = 0; i < nrect; i = i+1) {
if(random(-1, 1) > 0) {
stroke(0); // Bordo
strokeWeight(random(1,3));
} else {
noStroke(); // No bordo
}
if(random(-1, 1) > 0) {
fill(random(255),random(255)); // Grigio
} else {
fill(random(255),random(255), // Colori
random(255),random(255));
}
rect(random(width-100),random(height-100),
random(100),random(100));
};
// ----------------------------- Linee
for (let i = 0; i < int(nline/2); i = i+1) {
stroke(random(255)); // Grigi
strokeWeight(1);
line(random(width), // Verticali
0,
random(width),
height);
line(0, // Orizzontali
random(height),
width,
random(height));
};
// ----------------------------- Punti
for (let i = 0; i < npoint; i = i+1) {
stroke(0); // Nero
strokeWeight(random(20));
point(width /2 + random(-200,200),
height/2 + random(-200,200));
}
}
function suono() { // Funzione che genera il suono
for (let i = 0; i < 3; i = i+1) { // Quattro istanze
let start = random(0, buffer.duration-1.3);
let dur = random(0.05, 1.3);
let panner;
let player;
panner = new Tone.Panner().connect(rev); // 4 panners
panner.toDestination();
panner.pan.rampTo(random(-1,1), dur);
player = new Tone.Player(buffer).connect(panner); // 4 players
player.fadeIn = random(0.2,5);
player.fadeOut = random(0.2,5);
player.loopStart = start;
player.loopEnd = start + dur;
player.loop = true;
player.start("+0" + random(1, 5), start,random(5, 20));
}
}
let toneStart = 0;
function mousePressed() { // Click mouse
if (toneStart = 0){
Tone.start();
toneStart = 1;
}
disegno(); // Valuta le funzioni
suono();
}
Nei codici precedenti abbiamo utilizzato alcune funzioni per triggerare eventi recuperando le inter-azioni dell'utente su devices esterni come mouse e tastiera del computer senza troppe spiegazioni.
In questo paragrafo le esploreremo in modo più approfondito.
Indipendentemente dal tipo di device e dal protocollo di comunicazione utilizzato possiamo generare tre tipi di data.
Utilizzando il mouse abbiamo a disposizione tutte e tre le tipologie di interazione appena descritte.
Possiamo ottenere diverse informazioni (valori) in due modalità:
In entrambe i casi i valori sono memorizzati in specifiche variabili di sistema.
Sia in Processing che in p5.js ci sono le seguenti variabili di sistema che restituiscono valori corrispondenti alle specifiche azioni.
mouseY - contiene le coordinate Y del mouse all'interno della window.
pmouseX - contiene le coordinate Y del mouse all'interno della window nel frame precedente.
pmouseY - contiene le coordinate Y del mouse all'interno della window nel frame precedente.
mousePressed (mouseIsPressed per p5.js) - restituisce true ogni volta che premiamo un bottone del mouse.
mouseButton - restituisce LEFT, RIGHT o CENTER a seconda del bottone che premiamo.
Se le utilizziamo all'interno di draw otteniamo valori continui ad una frame rate che possiamo definire con la funzione frameRate().
void setup() {
size(200,200);
frameRate(10); // Abbassa la framerate (60 fps di default)
}
void draw() {
println(mouseX + " : " + mouseY);
}
Vediamo alcuni possibili utilizzi riguardo la posizione X Y.
Muovere un oggetto.
void setup() {
size(200,200);
smooth();
stroke(0,102);
}
void draw() {int x = mouseX;
int y = mouseY;
background(255); // Proviamo a commentare questa linea...
ellipse(x,y,20,20);
}
Se vogliamo una latenza (easing - smoothing) tra l'azione del mouse e l'effetto.
float x;
float y;
float px;
float py;
float easing = 0.05; // Fattore di smoothing
void setup() {
size(200, 200);
smooth();
stroke(0, 102);
}
void draw() {
x += (mouseX - x) * easing;
y += (mouseY - y) * easing;
float weight = dist(x, y, px, py); // dist() calcola l'intervallo
strokeWeight(weight); // Mappato dullo spessore
line(x, y, px, py);
py = y;
px = x;
}
Disegnare linee continue.
void setup() {
size(400,400);
smooth();
stroke(0,102);
}
void draw() {int x = mouseX;
int px = pmouseX;
int y = mouseY;
int py = pmouseY;
int largh = abs(px-x); // + veloce + alto...
//