xxxxxxxxxx
600
let jsonData, earwormData;
let dontleaveme, goingtocollege, stabyourback, whatsmyage, wigglyworld;
let earworms = [];
let reverb, delay;
let midi_files = {
["Stab Your Back"]: null,
["Wiggly World"]: null,
["What’s My Age Again"]: null,
["Going Away To College"]: null,
["Wiggly World"]: null,
["Don’t Leave Me"]: null,
};
let sound_files = {
["stabyourback"]: null,
["wigglyworld0"]: null,
["whatsmyageagain"]: null,
["goingawaytocollege"]: null,
["wigglyworld1"]: null,
["dontleaveme0"]: null,
["wigglyworld2"]: null,
["dontleaveme1"]: null,
};
let mouse_x, mouse_y;
let date_range = { from: null, to: null };
function preload() {
// midi_files.push(loadJSON('data/tunes/simplemelody.json'));
let api_key = 'dd33171cfb8a4cc4c2afbce8d695e0ed';
let user = 'aceslowman';
// last week
let from = new Date();
from.setUTCFullYear(2020, 9, 18);
// from.setHours(0);
// now
let to = new Date();
to.setUTCFullYear(2020, 9, 25);
date_range = {from: from, to: to};
let apiurl = `http://ws.audioscrobbler.com/2.0/`+
`?method=user.getrecenttracks`+
`&user=${user}`+
`&from=${Math.floor((from.getTime() - 86400000) / 1000)}`+
`&to=${Math.floor(to.getTime() / 1000)}`+
`&api_key=${api_key}`+
`&limit=200`+
`&extended=1`+
`&format=json`;
// get recent
// console.log(apiurl)
jsonData = loadJSON('data/scrobbles.json', 'json');
earwormData = loadJSON('data/earworms.json', 'json');
// MIDI FILES
midi_files["Stab Your Back"] = loadJSON('data/tunes/stabyourback16.json','json');
midi_files["What’s My Age Again"] = loadJSON('data/tunes/whatsmyageagain16.json','json');
midi_files["Going Away To College"] = loadJSON('data/tunes/goingtocollege16.json','json');
midi_files["Wiggly World"] = loadJSON('data/tunes/wigglyworld16.json','json');
midi_files["Don’t Leave Me"] = loadJSON('data/tunes/dontleaveme16.json','json');
// AUDIO FILES
sound_files["stabyourback"] = loadSound('data/soundfiles/stabyourback.mp3');
sound_files["wigglyworld0"] = loadSound('data/soundfiles/wigglyworld.mp3');
sound_files["whatsmyageagain"] = loadSound('data/soundfiles/whatsmyageagain.mp3');
sound_files["goingawaytocollege"] = loadSound('data/soundfiles/goingtocollege.mp3');
sound_files["wigglyworld1"] = loadSound('data/soundfiles/wigglyworld.mp3');
sound_files["dontleaveme0"] = loadSound('data/soundfiles/dontleaveme.mp3');
sound_files["wigglyworld2"] = loadSound('data/soundfiles/wigglyworld.mp3');
sound_files["dontleaveme1"] = loadSound('data/soundfiles/dontleaveme.mp3');
}
let ruler;
let cnv;
let ready = false;
function setup() {
cnv = createCanvas(window.innerWidth, window.innerHeight);
cnv.mousePressed(canvasPressed)
delay = new p5.Delay();
delay.process(p5.soundOut.output, 0.5,0.3, 10000)
delay.setType(1)
reverb = new p5.Reverb();
// p5.soundOut.output.disconnect();
reverb.process(delay, 3, 3)
// reverb.amp(2)
reverb.drywet(1)
}
function drawWelcome() {
textAlign(LEFT, CENTER)
cnv.background(255)
rectMode(CENTER)
textSize(20)
text('This visual looks at the melodies that I had stuck in my head from October 18th, 2020 to October 24th 2020. \nI recorded the name of the first melody that came to mind, three times a day, and if they repeated I counted it as an earworm. At the end of the week I transcribed the melodies.\n\nWith this project I was interested in understanding more about the role of earworms in my experience with OCD.\n\nThis project involves audio, using Tone.js and p5.Sound, used to play sound files synced to a MIDI file. The earworm data was collected by hand and my listening history was collected using LastFM.\n\nAudio works best in Chrome.', 10, height/2)
let string = "a week of earworms - "
string.split('').reverse().forEach((e,i) => {
push();
textAlign(CENTER,CENTER)
textSize(60)
translate(width/4,height/2)
rotate((i/string.length)*(Math.PI*2)-(-Math.PI/2)+(millis()/10000))
text(e,0,(height/4)*0.8)
pop();
})
}
function init() {
let recenttracks = jsonData.recenttracks.track;
let items = recenttracks.reverse().map((e,i) => {
let d = new Date(e.date['#text']);
d.setHours(d.getUTCHours() + 12)
let hrs = d.getHours() > 12 ? d.getHours() - 12 : d.getHours();
let mins = d.getMinutes().toString().padStart(2,0);
let m = d.getHours() > 12 ? 'pm' : 'am';
let time = `${hrs}:${mins}${m}`;
return ({
label: `"${e.name}" - ${e.artist.name}`,
time: time,
position: (d - date_range.from) / (date_range.to - date_range.from)
});
});
ruler = new Ruler(items);
let colors = {
["Stab Your Back"]: color("blue"),
["What’s My Age Again"]: color("aqua"),
["Going Away To College"]: color("green"),
["Wiggly World"]: color("orange"),
["Don’t Leave Me"]: color("purple"),
}
earwormData.entries.forEach((e,i) => {
let next_d;
if(i < earwormData.entries.length - 1) {
next_d = new Date(earwormData.entries[i+1].date);
} else {
next_d = date_range.to;
}
let d = new Date(e.date);
// d.setHours(d.getHours() + 18)
let x = ((d - date_range.from) / (date_range.to - date_range.from)) * width;
let y = height / 2;
// length of time to next earworm...
// let radius = sound_files[e.trackname].duration()*5;
let radius = (next_d - d) / 750000;
if(e.trackname) {
earworms.push(
new Earworm({
midifile: midi_files[e.trackname],
audiofile: sound_files[e.soundfile],
radius: radius,
position: {
x: x,
y: y
},
// color: colors[e.trackname]
color: 0
})
);
}
})
ready = true;
}
function draw() {
if(!ready) {
drawWelcome();
return false;
}
background(255);
earworms.forEach(e => e.draw());
// plot out earworms...
earwormData.entries.forEach((e,i) => {
let d = new Date(e.date);
// d.setHours(d.getHours() + 18)
// console.log(d,e.date)
let x = (d - date_range.from) / (date_range.to - date_range.from);
x *= width;
let y = (height/2)+(i*45) + 30;
if(e.trackname) {
fill(0)
stroke(color('red'))
strokeWeight(3)
fill(color('red'))
ellipse(x,height/2,15);
line(x,height/2,x,y);
noStroke()
if(x < width-450) {
stroke(255)
strokeWeight(5)
textAlign(LEFT, CENTER);
fill(0)
textSize(15)
text(e.trackname, x + 15, y);
textSize(12)
text(e.artistname, x + 15, y+20);
} else {
stroke(255)
strokeWeight(5)
textAlign(RIGHT, CENTER);
fill(0)
textSize(15)
text(e.trackname, x - 15, y);
textSize(12)
text(e.artistname, x - 15, y+20);
}
noStroke();
fill(color('red'))
ellipse(x,y,15);
fill(0)
}
})
ruler.draw();
drawTitle();
}
function canvasPressed() {
if(ready) return false;
userStartAudio().then(() => {
// console.log(getAudioContext())
init()
});
}
function mouseMoved(e) {
if(!ready) return false
ruler.update();
}
function drawTitle() {
textSize(40)
textAlign(LEFT, TOP)
textStyle(ITALIC)
textFont('monospace')
text('a week of earworms', 15, 15)
textSize(15);
text('October 18th 2020 - October 24th 2020', 15, 60)
textAlign(RIGHT, TOP)
text('use the mouse to move along the timeline',width-15,15);
text('to view listening history',width-15,40)
}
class Ruler {
constructor(notches) {
this.notches = notches;
this.pg = createGraphics(window.innerWidth, window.innerHeight);
this.update();
}
update() {
this.pg.clear();
this.pg.textFont('monospace');
this.pg.rectMode(CORNERS);
this.pg.textAlign(LEFT, CENTER)
this.pg.stroke(0);
this.pg.line(0,height/2,width,height/2);
this.pg.fill(color(255,255,255,240));
this.pg.noStroke();
this.pg.rect(0,height/2,width,height/2-75);
this.pg.fill(0);
this.pg.rectMode(CENTER)
this.drawWeekdays();
let skew_range = 25;
// draw skew window
if(mouseY > (height/2) - 50 && mouseY < (height/2) + 50) {
this.pg.fill(color(0,0,255,120))
this.pg.noStroke();
this.pg.rect(mouseX, height/2, skew_range * 2 , 50 )
}
this.pg.fill(0);
this.pg.stroke(0);
this.pg.fill(0);
this.pg.rectMode(CENTER);
this.pg.rect(width/2,height/2,width,10);
this.notches.forEach((e,i) => {
let x = (e.position / 1.0) * (width - 15);
let y = height / 2;
let skew = 0;
let notch_size = 5;
if(mouseX > x - skew_range && mouseX < x + skew_range && mouseY > (height / 2) - skew_range && mouseY < (height / 2) + skew_range) {
let modifier;
modifier = (x - mouseX) / skew_range;
skew = modifier * 12;
this.pg.push();
this.pg.scale(0.75 + skew/2);
this.pg.translate(x/3,y)
// this.pg.translate(x,(i%20)*30);
// this.pg.stroke(255);
// this.pg.strokeWeight(6);
this.pg.textAlign(LEFT, CENTER)
this.pg.fill(0)
this.pg.rect(275,0,550,20)
this.pg.fill(255)
// get time of day
this.pg.textStyle(ITALIC);
this.pg.text(e.time,4,0);
this.pg.textStyle(NORMAL);
this.pg.text(e.label,74,0);
this.pg.pop();
}
this.pg.fill(255)
this.pg.noStroke()
this.pg.ellipse(x+skew,y,notch_size);
})
}
drawWeekdays() {
for(let i = 0; i < 7; i++) {
let x = (width / 7) * i;
let y = height / 2 - 50;
this.pg.stroke(color('green'))
this.pg.strokeWeight(2)
this.pg.line(
x, y,
x, y + 50
);
this.pg.line(
x, y,
x + 20, y
);
let weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][(date_range.from.getUTCDay() + (i)) % 7]
let monthday = date_range.from.getUTCDate() + (i);
let month = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November"][date_range.from.getUTCMonth()]
this.pg.fill(0);
this.pg.noStroke();
this.pg.textStyle(BOLD)
this.pg.text(weekday, x + 5, y - 10)
this.pg.textStyle(ITALIC)
this.pg.text(`${month} ${monthday}`, x + 5, y + 10)
this.pg.textStyle(NORMAL)
}
}
draw() {
image(this.pg,0,0,width,height);
}
}
class Earworm {
constructor(opts = {
midifile: null,
audiofile: null,
radius: 170,
position: {
x: width / 2,
y: height / 2
},
color: color('black')
}) {
this.bpm = opts.midifile.header.tempos[0].bpm;
this.notes = opts.midifile.tracks[0].notes;
this.audio = opts.audiofile;
this.audio.setVolume(0.0);
this.audio.rate(1.0);
this.audio.loop();
this.color = opts.color;
this.position = opts.position;
this.ring = new PianoRing({
radius: opts.radius,
position: opts.position,
notes: this.notes,
bpm: this.bpm,
audiofile: this.audio,
color: this.color
})
this.ring.update();
// console.log('earmworm', this)
}
draw() {
this.ring.draw();
// adjust volume and playback rate with x position
let skew = 0;
let skew_range = 15;
let x = this.position.x;
// if(mouseX > x - (skew_range*2) && mouseX < x + (skew_range*2) && mouseY > (height / 2) - skew_range && mouseY < (height / 2) + skew_range) {
let modifier;
if(mouseX > x) {
modifier = (mouseX - x);
} else {
modifier = (x - mouseX);
}
skew = modifier;
let rate = 1.0 - skew/(width/10);
rate = constrain(rate,0,1);
let vol = 1.0 - skew/(width/6);
vol = constrain(vol,0,1);
vol = map(vol,0,1,0,0.1);
this.audio.rate(rate);
this.audio.setVolume(vol)
}
playSynth(time, data) {
this.ring.current_note = data;
}
}
class PianoRing {
constructor(opts = {
radius: 200,
position: {
x: width / 2,
y: height / 2
},
notes: [],
bpm: 120,
audiofile: null,
color: color('black')
}) {
this.radius = opts.radius;
this.position = opts.position;
this.bpm = opts.bpm;
this.pg = createGraphics(window.innerWidth, window.innerHeight);
this.audiofile = opts.audiofile;
this.color = opts.color;
/*
NOTE: this is a hack to make tonejs midi work with p5.Part.
p5.Part relies on all steps being represented (every 16th
in my case), but tonejs midi only represents the notes played.
To get around this, all midi files are 16th notes and rests
are generated by any notes in the second octave. very hacky
but it will work. I have an open issue in case it's something
worth tweaking p5.Part for.
https://github.com/processing/p5.js-sound/issues/554
*/
this.notes = opts.notes.map((e,i) => {
return e.name.slice(e.name.length - 1) <= 2 ? 0 : e
});
this.note_min = Math.min(this.notes.filter(e=>e.midi).map(e => e.midi));
this.note_max = Math.max(this.notes.filter(e=>e.midi).map(e => e.midi));
}
draw() {
push();
translate(this.position.x, this.position.y);
rotate((this.audiofile.currentTime() / this.audiofile.duration())*(Math.PI*2));
fill(color('red'))
noStroke();
ellipse(0,0,10)
translate(-width/2,-height/2);
image(this.pg, 0, 0, width, height);
pop();
}
update() {
this.pg.ellipseMode(CENTER);
this.pg.rectMode(CENTER);
this.pg.textAlign(CENTER, BOTTOM)
// this.pg.noFill();
this.pg.stroke(0);
// this.pg.ellipse(this.position.x,this.position.y,this.radius*2);
this.pg.stroke(0);
let note_range = this.note_max - this.note_min;
this.notes.forEach((e,i) => {
this.pg.push();
let vertical = ((e.midi - this.note_min) / note_range) * this.radius;
let theta = (i / this.notes.length) * (Math.PI*2) - Math.PI;
let notch_width = Math.PI / this.notes.length;
theta += notch_width;
let notch_height = ((1/note_range) * this.radius) / 2;
let base_rad = this.radius / 2;
// this.pg.translate(this.position.x,this.position.y);
this.pg.translate(width/2,height/2)
if(i === 0) {
this.pg.stroke(color('gray'));
this.pg.strokeWeight(4);
this.pg.line(0,0,0,-this.radius);
this.pg.strokeWeight(1);
this.pg.stroke(0);
}
this.pg.rotate(theta);
for(let j = 0; j <= note_range; j++) {
if(j === (e.midi - this.note_min)) {
this.pg.fill(this.color);
this.pg.stroke(this.color);
} else {
this.pg.noFill();
}
let v = base_rad + (notch_height*2) * j;
let x1 = sin(-notch_width) * (v-(notch_height));
let y1 = cos(-notch_width) * (v-(notch_height));
let x2 = sin(notch_width) * (v-(notch_height));
let y2 = cos(notch_width) * (v-(notch_height));
let x3 = sin(notch_width) * (v+(notch_height));
let y3 = cos(notch_width) * (v+(notch_height));
let x4 = sin(-notch_width) * (v+(notch_height));
let y4 = cos(-notch_width) * (v+(notch_height));
// this.pg.stroke(190)
this.pg.noStroke();
this.pg.quad(
x1, y1, // bottom left
x2, y2, // bottom right
x3, y3, // top right
x4, y4 // top left
)
}
this.pg.noStroke();
this.pg.fill(255);
// Note Name:
// this.pg.text(e.name, 0, base_rad + vertical + (notch_height/2));
this.pg.pop();
})
}
}