xxxxxxxxxx
264
let synth;
let sloop;
let validNotes = [Array(128).keys()];
let validTokens = validNotes.concat([-1]);
let minValidNote, maxValidNote;
let songLength = 32; // 4 bars * 8th-note resolution
let maxPopulationSize = 70;
let numberOfSurvivors = 20;
let population = [];
let generationCount = 1;
let songIsPlaying = false;
let clickedEarwormIndex;
let notePlaybackIndex;
// Fitness rules
let desiredKeyClasses = [0, 2, 4, 5, 7, 9, 11];
let minGoodPitch = 60;
let maxGoodPitch = 84;
function setup() {
createCanvas(800, 500);
colorMode(HSB, 255);
frameRate(10);
sloop = new p5.SoundLoop(soundLoop, 0.3); // Loop plays every 0.3s
synth = new p5.PolySynth();
minValidNote = min(validNotes);
maxValidNote = max(validNotes);
for (let i = 0; i < maxPopulationSize; i++) {
let song = new Earworm(i);
song.initialize();
population.push(song);
}
//selectionButton = createButton("Best 10");
//selectionButton.mouseClicked(selectFittest);
//selectionButton.position(10, 10);
reproductionButton = createButton("Reproduce");
reproductionButton.mouseClicked(reproducePopulation);
reproductionButton.position(10, 40);
fastForwardButton = createButton("10 Generations lAter");
fastForwardButton.mouseClicked(fastForward);
fastForwardButton.position(10, 70);
resetButton = createButton("Reset");
resetButton.mouseClicked(resetPopulation);
resetButton.position(10, 100);
}
function soundLoop(cycleStartTime) {
let duration = this.interval;
let velocity = 0.7;
let midiNote = population[clickedEarwormIndex].notes[notePlaybackIndex];
let noteFreq = midiToFreq(midiNote);
synth.play(noteFreq, velocity, cycleStartTime, duration);
// Move forward the index, and stop if we've reached the end
notePlaybackIndex++;
if (notePlaybackIndex >= population[clickedEarwormIndex].notes.length) {
this.stop(cycleStartTime);
songIsPlaying = false;
}
}
function draw() {
background(0, 0, 255, 150);
for (let i = 0; i < population.length; i++) {
population[i].display();
}
fill(255);
}
function mousePressed() {
if (songIsPlaying) {
// Stop a song
sloop.stop();
songIsPlaying = false;
} else {
// Start a song
for (var i = 0; i < population.length; i++) {
var clickToEarwormDistance = dist(mouseX, mouseY, population[i].xpos, population[i].ypos);
if (clickToEarwormDistance < population[i].radius) {
clickedEarwormIndex = i;
notePlaybackIndex = 0;
songIsPlaying = true;
console.log(population[clickedEarwormIndex].notes);
sloop.start();
}
}
}
}
function selectFittest() {
// Sort in descending order of fitness
population.sort((a, b) => b.fitnessScore - a.fitnessScore);
// Keep only the N fittest
population = subset(population, 0, numberOfSurvivors);
// Re-assign ID numbers
for (let i = 0; i < population.length; i++) {
population[i].id = i;
}
}
function reproducePopulation() {
let newPopulation = [];
while (newPopulation.length < maxPopulationSize - numberOfSurvivors) {
let parentA = random(population);
let parentB = random(population);
let child = parentA.reproduceWith(parentB);
newPopulation.push(child);
}
// Add new generation to the survivors
population = population.concat(newPopulation);
// Re-assign ID numbers
for (let i = 0; i < population.length; i++) {
population[i].id = i;
}
generationCount++;
}
function fastForward() {
let fastForwardNum = 10;
for (let i = 0; i < fastForwardNum; i++) {
selectFittest();
reproducePopulation();
}
}
function resetPopulation() {
generationCount = 1;
for (let i = 0; i < maxPopulationSize; i++) {
let song = new Earworm(i);
song.initialize();
population[i] = song;
}
}
function Earworm(indexNumber) {
this.id = indexNumber;
this.length = songLength;
this.notes = [];
this.fitnessScore = 0;
// Visual properties
this.xpos = random(width);
this.ypos = random(height);
this.radius = (width + height) / 50;
}
Earworm.prototype.initialize = function() {
this.notes = [];
for (let i = 0; i < this.length; i++) {
let token = random(validTokens);
if (random(1) > 0.2) {
this.notes.push(random(validNotes));
} else {
this.notes.push(-1);
}
}
this.calculateFitness();
};
Earworm.prototype.calculateFitness = function() {
this.fitnessScore = 0;
// Key
for (let i = 0; i < this.notes.length; i++) {
let keyClass = this.notes[i] % 12;
if (desiredKeyClasses.indexOf(keyClass) >= 0) {
this.fitnessScore = this.fitnessScore + 10;
}
}
// Prefer smaller intervals
for (let i = 0; i < this.notes.length - 1; i++) {
let currentNote = this.notes[i];
let nextNote = this.notes[i + 1];
let interval = abs(nextNote - currentNote);
this.fitnessScore = this.fitnessScore - interval;
// Consonant / dissonant intervals
// https://www.howmusicreallyworks.com/Pages_Chapter_4/4_2.html#4.2.6
let consonantIntervals = [3, 4, 5, 7, 8, 9];
let dissonantIntervals = [1, 2, 6, 10, 11];
if (consonantIntervals.indexOf(interval)) {
this.fitnessScore = this.fitnessScore + 10;
} else if (dissonantIntervals.indexOf(interval)) {
this.fitnessScore = this.fitnessScore - 10;
}
}
// Pitch range
for (let i = 0; i < this.notes.length; i++) {
if (this.notes[i] > minGoodPitch) {
this.fitnessScore = this.fitnessScore + 5;
}
if (this.notes[i] < maxGoodPitch) {
this.fitnessScore = this.fitnessScore + 5;
}
}
// Good ratio of empty and non-empty notes
let empty = 0;
let targetEmptyRatio = 0.5;
for (let i = 0; i < this.notes.length; i++) {
if (this.notes[i] === -1) {
empty++;
}
}
let emptyRatio = empty / (this.notes.length - empty);
this.fitnessScore = this.fitnessScore - (targetEmptyRatio - emptyRatio) * 100;
};
Earworm.prototype.reproduceWith = function(partner) {
let partitionIndex = round(random(this.notes.length));
let partA = subset(this.notes, 0, partitionIndex);
let partB = subset(partner.notes, partitionIndex, partner.notes.length);
let child = new Earworm(0);
child.notes = partA.concat(partB);
child.mutate(); // Add some random variation
child.calculateFitness();
return child;
};
Earworm.prototype.mutate = function() {
for (let i = 0; i < this.notes.length; i++) {
if (random(100) > 80) {
// this.notes[i] = random(validTokens);
if (random(1) > 0.2) {
this.notes[i] = random(validNotes);
} else {
this.notes[i] = -1;
}
}
}
};
Earworm.prototype.display = function() {
this.xpos = constrain(this.xpos + random(-1, 1), 0, width);
this.ypos = constrain(this.ypos + random(-1, 1), 0, height);
push();
strokeWeight(0.5);
angleMode(DEGREES); // Change the mode to DEGREES
let angle = 360 / this.notes.length;
translate(this.xpos, this.ypos);
for (let i = 0; i < this.notes.length; i++) {
rotate(angle);
if (this.notes[i] === -1) {
continue;
}
let pitchClass = this.notes[i] % 12;
let color = map(pitchClass, 0, 12, 280, 120) % 255;
let length = map(this.notes[i], minValidNote, maxValidNote, this.radius / 2, this.radius);
strokeWeight(0.5);
stroke(color, 180, 250);
if (songIsPlaying) {
if (this.id == clickedEarwormIndex) {
stroke(color, 180, 250);
if (i == notePlaybackIndex) {
strokeWeight(2);
length = this.radius;
} else {
strokeWeight(1);
}
} else {
stroke(color, 255, 100);
}
}
line(0, 0, length, 0);
}
pop();
};