Summary: La produzione di fenomeni oscillatori è un aspetto frequente di molte realizzazioni interattive. Vengono descritte tecniche di riproduzione di oscillazioni di varia forma e frequenza. Inoltre, si presenta il buffer circolare come struttura di supporto alla manipolazione continua del tempo.
Note: Your browser may not currently support MathML. See our browser support page for additional details. You can always view the correct math in the PDF version.
Tra i pattern emergenti nel design dell'interazione, si nota la necessità di produrre fenomeni oscillatori, di tipo uditivo o visuale. La gestione più versatile di questi fenomeni si fa mediante array contenenti un periodo del fenomeno oscillatorio da produrre.
L'approccio più classico e versatile alla sintesi di forme
d'onda periodiche è la lettura ciclica di una tabella
contenente un periodo della forma d'onda periodica da
generare. Detto buf[] il buffer (è un array)
contenente la forma d'onda da generare, l'oscillatore
tabellare funziona per lettura ciclica della tabella, con un
passo di avanzamento
Per dare l'illusione di una onda prodotta da un gesto di
interazione e propagantesi in una certa direzione si può
fare uso dell'oscillatore tabellare. Nel codice che segue il
buffer viene precaricato con un periodo di sinusoide. Il
punto in cui l'utilizzatore clicca sulla finestra determina
la posizione iniziale della cresta, e viene generata una
oscillazione la cui frequenza è impostata tramite la
variabile f.
int B = 256; // lunghezza di tabella (risoluzione)
int R = 20; // rate
float f = 0.8; // frequenza in cicli al secondo
float[] buf = new float[B];
int HEIGHT = 128; // altezza della finestra
float amp; // ampiezza della oscillazione
int readPoint; // posizione orizzontale della cresta
float decayFactor = 0.95; // fattore di decadimento dell'onda
void setup() {
size(B,HEIGHT);
stroke(255); strokeWeight(2);
for (int i = 0; i < buf.length; i++) buf[i] = sin(2*PI*(float)i/buf.length);
}
void draw() {
background(0);
if ((mouseX >= 0) && (mouseX < B) && (mousePressed)) {
amp = 2*(HEIGHT/2 - mouseY)/float(HEIGHT);
readPoint = mouseX;
}
for (int i = 0; i < buf.length; i++) {
point(i, HEIGHT - HEIGHT/2 - HEIGHT/2*amp*buf[(i - readPoint + B/4 + B)%B]);
}
amp = decayFactor*amp;
readPoint = (readPoint + round(f*B/R))%B; // incremento dell'oscillatore
}
Si renda la frequenza di oscillazione dipendente dalla lunghezza dell'intervallo di tempo durante il quale il tasto del mouse viene tenuto premuto.
int B = 256; // lunghezza di tabella (risoluzione)
int R = 20; // rate
float f = 1.1; // frequenza in cicli al secondo
float[] buf = new float[B];
int HEIGHT = 128; // altezza della finestra
float amp; // ampiezza della oscillazione
float readPoint; // posizione orizzontale della cresta
float decayFactor = 0.92; // fattore di decadimento dell'onda
float tempo;
void setup() {
size(B,HEIGHT);
stroke(255); strokeWeight(2);
for (int i = 0; i < buf.length; i++) buf[i] = sin(2*PI*(float)i/buf.length);
}
void mousePressed() {
tempo = millis();
}
void mouseReleased() {
tempo = millis() - tempo; println(tempo);
f = 500/tempo;
}
void draw() {
background(0);
if ((mouseX >= 0) && (mouseX < B) && (mousePressed)) {
amp = 2*(HEIGHT/2 - mouseY)/float(HEIGHT);
readPoint = mouseX;
}
for (int i = 0; i < buf.length; i++) {
point(i, HEIGHT - HEIGHT/2 - HEIGHT/2*amp*buf[(i - round(readPoint) + B/4 + B)%B]);
}
amp = decayFactor*amp;
readPoint = (readPoint + (f*B/R))%B; // incremento dell'oscillatore
}
Si imponga un inviluppo di ampiezza sulla oscillazione generata, come se a vibrare fosse una corda vincolata alle estremità.
Quella che segue è la soluzione a Exercise 1 con i vincoli aggiunti agli estremi:
int B = 256; // lunghezza di tabella (risoluzione)
int R = 20; // rate
float f = 1.1; // frequenza in cicli al secondo
float[] buf = new float[B];
int HEIGHT = 128; // altezza della finestra
float amp; // ampiezza della oscillazione
float readPoint; // posizione orizzontale della cresta
float decayFactor = 0.92; // fattore di decadimento dell'onda
float tempo;
float ampli=0;
void setup() {
size(B,HEIGHT);
stroke(255); strokeWeight(2);
for (int i = 0; i < buf.length; i++) buf[i] = sin(2*PI*(float)i/buf.length);
}
void mousePressed() {
tempo = millis();
}
void mouseReleased() {
tempo = millis() - tempo; println(tempo);
f = 500/tempo;
}
void draw() {
background(0);
if ((mouseX >= 0) && (mouseX < B) && (mousePressed)) {
amp = 2*(HEIGHT/2 - mouseY)/float(HEIGHT);
readPoint = mouseX;
}
for (int i = 0; i < buf.length; i++) {
if (i < readPoint) ampli += 1/(float)readPoint;
else ampli -= 1/(float)(B - readPoint);
point(i, HEIGHT - HEIGHT/2 - HEIGHT/2*amp*ampli*buf[(i - round(readPoint) + B/4 + B)%B]);
}
ampli = 0;
amp = decayFactor*amp;
readPoint = (readPoint + (f*B/R))%B; // incremento dell'oscillatore
}
Si renda l'oscillazione più fluida sostituendo
l'arrotondamento (round()) con
l'interpolazione lineare.
L'oscillatore tabellare può essere la struttura di riferimento anche per ripetizioni cicliche, a frequenza controllabile, di frammenti video. In questo caso la tabella da leggere ciclicamente contiene una sequenza di immagini.
In questo esempio, quando si manda in esecuzione il programma un frammento di 4 secondi di video viene ripreso dalla videocamera a 16 frame al secondo. Poi tale frammento è riprodotto ciclicamente a 20 frame al secondo, con la possibilità di controllare la velocità di riproduzione (frequenza di oscillazione) cliccando in diverse posizioni orizzontali del mouse.
import processing.video.*;
Capture myCapture;
int captureRate = 16;
float f=1;
int B = 64;
int R = 20;
PImage[] sequenza = new PImage[B];
int i;
float j=0;
float inc = f*B/R;
boolean via = false;
void setup() {
for (int i=0; i<B; i++) sequenza[i]=loadImage("vetro.jpg"); // immagine dummy, per inizializzare
size(200, 200);
String s = "IIDC FireWire Video";
myCapture = new Capture(this, width, height, s, 30);
myCapture.frameRate(captureRate);
frameRate(R);
}
void mousePressed(){
f = float(mouseX)/width; inc = f*B/R;
}
void draw() {
if (!via) {
if(myCapture.available()) {
// Reads the new frame
myCapture.read();
sequenza[i].copy(myCapture,0,0,myCapture.width,myCapture.height,0,0,sequenza[i].width,sequenza[i].height);
i = (i+1)%B;
println(i);
if (i==0) via = true;
}
}
if (via) { image(sequenza[floor(j)], 0, 0,200,200);
j = (j+inc); if (j>=B) j=j-B;
}
}
Nel visual programming le oscillazioni tabellari spesso si costruiscono utilizzando un segnale sawtooth che consente di percorre ciclicamente gli indici dell'array, ad una velocità dipendente dalla frequenza del segnale stesso.
![]() |
onda1 e
onda2 di 64 elementi ciascuno. Il segnale a dente
di sega viene generato ad una frequenza di 139 cicli al
secondo. Il suo range viene moltiplicato per 64 in maniera da
occupare tutto il campo degli indici (da 0 a 63) dell'array
onda1. Ogni volta che si preme il bottone di
bang, 64 campioni del segnale prodotto dalla
lettura della tabella onda1 vengono scritti nella
tabella onda2. Il segnale prodotto dalla lettura
ciclica della tabella onda1 è udibile attraverso
conversione da digitale ad analogico (dac~). Si
noti che in Pd tutti gli operatori che hanno il simbolo di
tilde lavorano in maniera sincrona ad audio rate, e quindi la
lettura dalla tabella onda1 fornisce campioni al
sample rate di funzionamento (per default 44100 Hz). Si noti
anche la scalinatura del segnale scritto nella tabella
onda2, dovuto alla lettura non interpolata dei
campioni di onda1.
Come si può evitare la scalinatura nella forma d'onda prodotta dall'oscillatore tabellare di Figura 1?
Basta sostituire tabread~ con
tabread4~, in questo modo facendo una lettura
interpolata di ordine 3. In altri termini, l'interpolatore
fa passare un polinomio di terzo grado (una cubica) per quattro
punti contigui della tabella.
Nei sistemi interattivi c'è spesso la necessità di ritardare nel tempo un segnale, un processo, o un evento. Ad esempio, se ci sono più flussi di video o audio in streaming, ciascuno viaggiante su canali indipendenti per i quali non è possibile avere lo stesso tempo di propagazione da trasmittente a fruitore, è opportuno ritardare diversamente ciascun flusso in maniera da avere un riallineamento ed una sincronizzazione in ricezione. In campo audio, le linee di ritardo a lunghezza variabile sono alla base di molti effetti, quali sono echi e riverberazione.
La struttura di supporto per la realizzazione delle linee di
ritardo è il buffer circolare, cioè un array ai cui elementi
si accede con aritmetica circolare dei puntatori. In
particolare, c'è una variabile IN, chiamata
puntatore di ingresso, che contiene l'indice dell'elemento
dell'array nel quale si va a scrivere. Una variabile
OUT, chiamata puntatore di uscita, contiene
l'indice dell'elemento dell'array dal quale si va a
leggere. Si tratta di incrementare i puntatori di accesso in
maniera circolare, mantenendo la loro distanza relativa pari
al numero di passi temporali di cui si vuole che sia fatto il
ritardo. Ad ogni passo temporale (o istante di campionamento)
il segnale di ingresso è scritto nella locazione puntata da
IN e letto dalla locazione puntata da
OUT, D passi indietro. Quindi, i due
puntatori sono aggiornati con le operazioni
IN = (IN + 1) % B;
OUT = (OUT + 1) % B;
B è la lunghezza del buffer, scelta in
maniera da essere maggiore del più grande valore di ritardo
D che si intende usare. Il ritardo D
può variare dinamicamente, ad esempio oscillando tra un minimo
ed un massimo, ma è chiaro che se esso assume un valore non
intero bisognerà adottare una strategia di interpolazione per
la lettura del segnale ritardato. Ad esempio, si può ancora
una volta fare interpolazione lineare tra locazioni adiacenti,
oppure scegliere l'intero immediatamente inferiore a
D.
Si costruisca un programma Processing che legge caratteri dalla tastiera e ne fornisce una eco sulla finestra grafica con un ritardo crescente mano a mano che si aggiungono linee di testo.
PFont font;
int B = 1000;
int INTERLINEA = 30;
int xpos=0, ypos=INTERLINEA;
char[] buffer = new char[B];
int OUT = 0;
int IN = 0;
int D = 0;
char carattere;
char inchar = '\0';
void setup() {
size(800,800);
font = loadFont("Courier-48.vlw");
textFont(font, 32);
frameRate(30);
}
void keyReleased() {
inchar = key;
}
void draw() {
if (inchar=='\n') {
D+=10;
IN = (OUT + D)%B;
xpos = 0;
ypos += INTERLINEA;
inchar='\0';
}
else {
buffer[IN] = inchar;
carattere = buffer[OUT];
text(carattere, xpos, ypos);
xpos += textWidth(carattere);
IN = (IN + 1)%B; OUT = (OUT + 1)%B;
}
inchar = '\0';
}