Skip to content

Commit 4b97ed1

Browse files
authored
Merge pull request #311 from JunShern/genetic_music
Genetic music
2 parents e850a8d + 6095d0f commit 4b97ed1

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

examples/genetic_music/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<head>
2+
<script language="javascript" type="text/javascript" src="../../lib/p5.js"></script>
3+
4+
<script language="javascript" type="text/javascript" src="../../lib/addons/p5.dom.js"></script>
5+
6+
<script language="javascript" type="text/javascript" src="../../lib/p5.sound.js"></script>
7+
8+
<script language="javascript" type="text/javascript" src="sketch.js"></script>
9+
10+
<style>
11+
body {
12+
margin: 0;
13+
}
14+
</style>
15+
</head>

examples/genetic_music/sketch.js

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/**
2+
* Example: Genetic music
3+
*/
4+
5+
var synth;
6+
var sloop;
7+
8+
var validNotes = [...Array(128).keys()];
9+
var minValidNote, maxValidNote;
10+
var songLength = 32; // 4 bars * 8th-note resolution
11+
12+
var maxPopulationSize = 40;
13+
var numberOfSurvivors = 10;
14+
var population = [];
15+
var generationCount = 1;
16+
17+
var songIsPlaying = false;
18+
var clickedEarwormIndex;
19+
var notePlaybackIndex;
20+
// Fitness rules
21+
var desiredKeyClasses = [0,2,4,5,7,9,11];
22+
var minGoodPitch = 48;
23+
var maxGoodPitch = 72;
24+
25+
function setup() {
26+
createCanvas(window.innerWidth, window.innerHeight);
27+
colorMode(HSB, 255);
28+
textAlign(CENTER, CENTER);
29+
textSize(16);
30+
frameRate(10);
31+
32+
sloop = new p5.SoundLoop(soundLoop, 0.3); // Loop plays every 0.3s
33+
synth = new p5.PolySynth();
34+
35+
minValidNote = min(validNotes);
36+
maxValidNote = max(validNotes);
37+
for (var i=0; i<maxPopulationSize; i++) {
38+
var song = new Earworm(i);
39+
song.initialize();
40+
population.push(song);
41+
}
42+
43+
selectionButton = createButton("Select 10 fittest");
44+
selectionButton.mouseClicked(selectFittest);
45+
selectionButton.position(10, 10);
46+
reproductionButton = createButton("Reproduce");
47+
reproductionButton.mouseClicked(reproducePopulation);
48+
reproductionButton.position(10, 40);
49+
fastForwardButton = createButton("Fast-forward 10 generations");
50+
fastForwardButton.mouseClicked(fastForward);
51+
fastForwardButton.position(10, 70);
52+
resetButton = createButton("Reset population");
53+
resetButton.mouseClicked(resetPopulation);
54+
resetButton.position(10, 100);
55+
}
56+
57+
function soundLoop(cycleStartTime) {
58+
var duration = this.interval;
59+
var velocity = 0.7;
60+
var midiNote = population[clickedEarwormIndex].notes[notePlaybackIndex];
61+
var noteFreq = midiToFreq(midiNote);
62+
synth.play(noteFreq, velocity, 0, duration);
63+
// Move forward the index, and stop if we've reached the end
64+
notePlaybackIndex++;
65+
if (notePlaybackIndex >= population[clickedEarwormIndex].notes.length) {
66+
this.stop();
67+
songIsPlaying = false;
68+
}
69+
}
70+
71+
function draw() {
72+
background(30);
73+
for (var i=0; i<population.length; i++) {
74+
population[i].display();
75+
}
76+
fill(255);
77+
if (songIsPlaying) {
78+
text("Song playing... Click to stop.", width/2, height/2);
79+
} else {
80+
text("Click on an Earworm to hear it sing!", width/2, height/2);
81+
}
82+
text("Generation: " + generationCount, width/2, height/6);
83+
}
84+
85+
function mousePressed() {
86+
if (songIsPlaying) {
87+
// Stop a song
88+
sloop.stop();
89+
songIsPlaying = false;
90+
} else {
91+
// Start a song
92+
for (var i=0; i<population.length; i++) {
93+
var clickToEarwormDistance = dist(mouseX, mouseY, population[i].xpos, population[i].ypos);
94+
if (clickToEarwormDistance < population[i].radius) {
95+
clickedEarwormIndex = i;
96+
notePlaybackIndex = 0;
97+
songIsPlaying = true;
98+
sloop.start();
99+
}
100+
}
101+
}
102+
}
103+
104+
function selectFittest() {
105+
// Sort in descending order of fitness
106+
population.sort((a, b) => b.fitnessScore - a.fitnessScore);
107+
// Keep only the N fittest
108+
population = subset(population, 0, numberOfSurvivors);
109+
// Re-assign ID numbers
110+
for (var i=0; i<population.length; i++) {
111+
population[i].id = i;
112+
}
113+
}
114+
115+
function reproducePopulation() {
116+
var newPopulation = [];
117+
while (newPopulation.length < maxPopulationSize - numberOfSurvivors) {
118+
var parentA = random(population);
119+
var parentB = random(population);
120+
var child = parentA.reproduceWith(parentB);
121+
newPopulation.push(child);
122+
}
123+
// Add new generation to the survivors
124+
population = population.concat(newPopulation);
125+
// Re-assign ID numbers
126+
for (var i=0; i<population.length; i++) {
127+
population[i].id = i;
128+
}
129+
generationCount++;
130+
}
131+
132+
function fastForward() {
133+
var fastForwardNum = 10;
134+
for (var i=0; i<fastForwardNum; i++) {
135+
selectFittest();
136+
reproducePopulation();
137+
}
138+
}
139+
140+
function resetPopulation() {
141+
generationCount = 1;
142+
for (var i=0; i<maxPopulationSize; i++) {
143+
var song = new Earworm(i);
144+
song.initialize();
145+
population[i] = song;
146+
}
147+
}
148+
149+
function Earworm(indexNumber) {
150+
this.id = indexNumber;
151+
this.length = songLength;
152+
this.notes = [];
153+
// Visual properties
154+
this.xpos = random(width);
155+
this.ypos = random(height);
156+
this.radius = (width + height) / 50;
157+
this.fitnessScore = 0;
158+
}
159+
Earworm.prototype.initialize = function() {
160+
this.notes = [];
161+
for (var i=0; i<this.length; i++) {
162+
this.notes.push(random(validNotes));
163+
}
164+
this.calculateFitness();
165+
};
166+
Earworm.prototype.display = function() {
167+
this.xpos = constrain(this.xpos + random(-1, 1), 0, width);
168+
this.ypos = constrain(this.ypos + random(-1, 1), 0, height);
169+
170+
push();
171+
strokeWeight(1);
172+
angleMode(DEGREES); // Change the mode to DEGREES
173+
var angle = 360 / this.notes.length;
174+
translate(this.xpos, this.ypos);
175+
for (var i=0; i<this.notes.length; i++) {
176+
var color = map(this.notes[i], minValidNote, maxValidNote, 280, 120) % 255;
177+
var length = map(this.notes[i], minValidNote, maxValidNote, this.radius/2, this.radius);
178+
strokeWeight(1);
179+
stroke(color, 180, 250);
180+
if (songIsPlaying) {
181+
if (this.id == clickedEarwormIndex) {
182+
stroke(color, 180, 250);
183+
if (i == notePlaybackIndex) {
184+
strokeWeight(2);
185+
length = this.radius;
186+
} else {
187+
strokeWeight(1);
188+
}
189+
} else {
190+
stroke(color, 100, 100);
191+
}
192+
}
193+
rotate(angle);
194+
line(0, 0, length, 0);
195+
}
196+
pop();
197+
};
198+
Earworm.prototype.calculateFitness = function() {
199+
this.fitnessScore = 0;
200+
// Key
201+
for (var i=0; i<this.notes.length; i++) {
202+
var keyClass = this.notes[i] % 12;
203+
if (desiredKeyClasses.indexOf(keyClass) >= 0) {
204+
this.fitnessScore = this.fitnessScore + 10;
205+
}
206+
}
207+
// Prefer smaller intervals
208+
for (var i=0; i<this.notes.length-1; i++) {
209+
var currentNote = this.notes[i];
210+
var nextNote = this.notes[i+1];
211+
var interval = abs(nextNote - currentNote);
212+
this.fitnessScore = this.fitnessScore - interval;
213+
}
214+
// Pitch range
215+
for (var i=0; i<this.notes.length; i++) {
216+
if (this.notes[i] > minGoodPitch) {
217+
this.fitnessScore = this.fitnessScore + 5;
218+
}
219+
if (this.notes[i] < maxGoodPitch) {
220+
this.fitnessScore = this.fitnessScore + 5;
221+
}
222+
}
223+
};
224+
Earworm.prototype.reproduceWith = function(partner) {
225+
var partitionIndex = round(random(this.notes.length));
226+
var partA = subset(this.notes, 0, partitionIndex);
227+
var partB = subset(partner.notes, partitionIndex, partner.notes.length);
228+
var child = new Earworm(0);
229+
child.notes = partA.concat(partB);
230+
child.mutate(); // Add some random variation
231+
child.calculateFitness();
232+
return child;
233+
};
234+
Earworm.prototype.mutate = function() {
235+
for (var i=0; i<this.notes.length; i++) {
236+
if (random(100) > 80) {
237+
this.notes[i] = random(validNotes);
238+
}
239+
}
240+
};

index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ <h2>p5.sound
3434
<div><a href="examples/FFT_scaleOneThirdOctave">FFT_scaleOneThirdOctave</a></div>
3535
<div><a href="examples/Filter_BandPass">Filter_BandPass</a></div>
3636
<div><a href="examples/Filter_LowPass">Filter_LowPass</a></div>
37+
<div><a href="examples/genetic_music">genetic_music</a></div>
3738
<div><a href="examples/granular_sampler">granular_sampler</a></div>
3839
<div><a href="examples/granular_sampler_psynth">granular_sampler_psynth</a></div>
3940
<div><a href="examples/graphical_eq">graphical_eq</a></div>
@@ -72,6 +73,7 @@ <h2>p5.sound
7273
<div><a href="examples/Reverb_convolve">Reverb_convolve</a></div>
7374
<div><a href="examples/Reverb_convolve_FFT">Reverb_convolve_FFT</a></div>
7475
<div><a href="examples/soundfile_playMode">soundfile_playMode</a></div>
76+
<div><a href="examples/soundfile_remove_cue">soundfile_remove_cue</a></div>
7577
<div><a href="examples/soundfileMod_AM">soundfileMod_AM</a></div>
7678
<div><a href="examples/soundFormats">soundFormats</a></div>
7779
<div><a href="examples/spatial_panning">spatial_panning</a></div>

0 commit comments

Comments
 (0)