Skip to content

Commit 199ee91

Browse files
authored
Merge pull request #309 from JunShern/markov_music
Markov music
2 parents 4b97ed1 + c47a21a commit 199ee91

File tree

3 files changed

+284
-0
lines changed

3 files changed

+284
-0
lines changed

examples/markov_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/markov_music/sketch.js

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/**
2+
* Example: Markov music
3+
* Demonstrates the use of Markov chains to model musical sequences.
4+
*
5+
* The user adds notes to the Markov chain by playing notes ASDFGHJKL
6+
* on the keyboard. Every note event (note on and note off) is registered
7+
* as a new node in the graph, and the graph records all transitions
8+
* between note events as edges along the graph.
9+
*
10+
* During playback mode, the algorithm simply traverses the graph randomly,
11+
* playing notes based on the nodes upon which it traverses.
12+
*
13+
* The Markov chain (graph) is visualized as a standard network of nodes,
14+
* with transitions represented by edges between nodes. The current/latest
15+
* node as well as the edges of its previously recorded transitions are
16+
* highlighted.
17+
*/
18+
19+
// Music
20+
var synth;
21+
var velocity = 0.7; // From 0-1
22+
var baseNote = 60;
23+
var keyOrder = "ASDFGHJKL";
24+
var keyScale = [0,2,4,5,7,9,11,12,14];
25+
var keyStates = [0,0,0,0,0,0,0,0,0];
26+
// Markov Chain
27+
var graph;
28+
var latestNodeId;
29+
// Playback SoundLoops
30+
var sloop;
31+
var playing = false;
32+
var secondsPerTick = 0.1;
33+
var prevEventMillis = 0;
34+
var timeQuantizationStep = 100; // Quantize to 10 milliseconds
35+
var maxDuration = 5000;
36+
var longestDurationSoFar = timeQuantizationStep;
37+
// Colors
38+
var DEFAULT_NODE_COLOR = [85, 120, 138];
39+
var ACTIVE_NODE_COLOR = [205, 0, 100];
40+
41+
function setup() {
42+
createCanvas(720, 400);
43+
frameRate(15);
44+
angleMode(DEGREES);
45+
46+
graph = new Graph();
47+
synth = new p5.PolySynth();
48+
sloop = new p5.SoundLoop(soundLoop, 0.1);
49+
50+
playPauseButton = createButton("Play / Pause");
51+
playPauseButton.position(20, height-40);
52+
playPauseButton.mousePressed(togglePlayPause);
53+
54+
prevEventMillis = millis();
55+
}
56+
57+
function draw() {
58+
background(242, 221, 164);
59+
fill(163, 196, 188);
60+
rect(0, 0, width/2, height);
61+
// Draw edges
62+
graph.drawEdges();
63+
// Draw nodes
64+
for (var i=0; i<graph.nodes.length; i++) {
65+
graph.nodes[i].update();
66+
graph.nodes[i].display();
67+
}
68+
fill(0);
69+
noStroke();
70+
textAlign(CENTER, CENTER);
71+
// If there are no nodes, tell the users to play something
72+
if (graph.nodes.length == 0) {
73+
text("Press any of ASDFGHJKL to add notes to the Markov chain.", width/2, 20);
74+
}
75+
// If we are at the end of the chain, tell the users
76+
if (latestNodeId != null && graph.edges[latestNodeId].length == 0) {
77+
text("Reached end of Markov chain. Play a new note to add to chain.", width/2, 20);
78+
sloop.stop();
79+
synth.noteRelease(); // Release all notes
80+
}
81+
82+
if (sloop.isPlaying) {
83+
text("Generating from chain...", width/2, height - 20);
84+
} else {
85+
text("PAUSED", width/2, height - 20);
86+
}
87+
}
88+
89+
function soundLoop(cycleStartTime) {
90+
// Play the sound of this node
91+
var midiNoteNumber = graph.nodes[latestNodeId].pitch;
92+
var freq = midiToFreq(midiNoteNumber);
93+
var type = graph.nodes[latestNodeId].type;
94+
if (type == 1) {
95+
synth.noteAttack(freq, velocity, cycleStartTime);
96+
} else {
97+
synth.noteRelease(freq, cycleStartTime);
98+
}
99+
// Transition to a random new node
100+
if (graph.edges[latestNodeId].length) {
101+
latestNodeId = random(graph.edges[latestNodeId]);
102+
}
103+
// Wait for the timeFromPrevEvent of the new node
104+
var timeSincePrevEvent = graph.nodes[latestNodeId].timeSincePrevEvent / 1000; // Millis to seconds
105+
this.interval = max(timeSincePrevEvent, 0.01); // Cannot have interval of exactly 0
106+
}
107+
108+
function keyPressed() {
109+
var keyIndex = keyOrder.indexOf(key);
110+
// Check if valid note key pressed
111+
if (keyIndex >= 0) {
112+
// Play synth
113+
var midiNoteNumber = baseNote + keyScale[keyIndex]; // 0-127; 60 is Middle C (C4)
114+
var freq = midiToFreq(midiNoteNumber);
115+
synth.noteAttack(freq, velocity, 0);
116+
// Update time
117+
var timeSincePrevEvent = min(millis() - prevEventMillis, maxDuration);
118+
prevEventMillis = millis();
119+
var quantizedTimeSincePrevEvent = round(timeSincePrevEvent / timeQuantizationStep) * timeQuantizationStep;
120+
// Register node
121+
graph.registerNewNode(1, midiNoteNumber, quantizedTimeSincePrevEvent);
122+
// Activate key state
123+
keyStates[keyIndex] = 1;
124+
}
125+
}
126+
127+
function keyReleased() {
128+
var keyIndex = keyOrder.indexOf(key);
129+
// Check if valid note key pressed
130+
if (keyIndex >= 0) {
131+
// Stop synth
132+
midiNoteNumber = baseNote + keyScale[keyIndex]; // 0-127; 60 is Middle C (C4)
133+
freq = midiToFreq(midiNoteNumber);
134+
synth.noteRelease(freq, 0);
135+
// Update time
136+
var timeSincePrevEvent = min(millis() - prevEventMillis, maxDuration);
137+
prevEventMillis = millis();
138+
var quantizedTimeSincePrevEvent = round(timeSincePrevEvent / timeQuantizationStep) * timeQuantizationStep;
139+
// Register node
140+
graph.registerNewNode(0, midiNoteNumber, quantizedTimeSincePrevEvent);
141+
// Reset key state
142+
keyStates[keyIndex] = 0;
143+
144+
timeSincePrevEvent = 0;
145+
}
146+
}
147+
148+
function togglePlayPause() {
149+
if (sloop.isPlaying) {
150+
sloop.stop();
151+
synth.noteRelease(); // Release all notes
152+
} else {
153+
sloop.start();
154+
}
155+
}
156+
157+
// Class for a single node
158+
// characterized by ID, pitch and timeSincePrevEvent of the note it represents
159+
function Node(id, type, pitch, timeSincePrevEvent) {
160+
this.id = id;
161+
this.type = type; // 1 (note on) or 0 (note off)
162+
this.pitch = pitch;
163+
this.timeSincePrevEvent = timeSincePrevEvent;
164+
this.oscillateCounter = 0;
165+
166+
var x = map(this.timeSincePrevEvent, 0, maxDuration/2, 0, width/2);
167+
if (type === 1) {
168+
x = width / 2 + x;
169+
} else {
170+
x = width / 2 - x;
171+
}
172+
var y = map(this.pitch, baseNote, baseNote + max(keyScale), height*0.9, height*0.1);
173+
this.center = createVector(x, y);
174+
this.position = createVector(x, y);
175+
176+
this.color = ACTIVE_NODE_COLOR;
177+
this.diameter = map(this.timeSincePrevEvent, 0, maxDuration, 2, height/20);
178+
}
179+
Node.prototype.isSimilar = function(node) {
180+
if (this.type === node.type && this.pitch === node.pitch && this.duration === node.duration) {
181+
return true;
182+
} else {
183+
return false;
184+
}
185+
};
186+
Node.prototype.update = function() {
187+
var yAmplitude = height / 150;
188+
var xAmplitude = height / 300;
189+
this.position.y = this.center.y + (yAmplitude * sin(this.oscillateCounter));
190+
this.position.x = this.center.x + (xAmplitude * cos(this.oscillateCounter));
191+
this.oscillateCounter = this.oscillateCounter + 6;
192+
};
193+
Node.prototype.display = function() {
194+
noStroke();
195+
var color = DEFAULT_NODE_COLOR;
196+
if (this.id == latestNodeId) {
197+
// Highlight latest node
198+
color = this.color;
199+
}
200+
// Fill circle if note-on, stroke circle if note-off
201+
if (this.type == 1) {
202+
noStroke();
203+
fill(color[0], color[1], color[2]);
204+
} else {
205+
noFill();
206+
strokeWeight(2);
207+
stroke(color[0], color[1], color[2]);
208+
}
209+
ellipse(this.position.x, this.position.y, this.diameter, this.diameter);
210+
};
211+
212+
// Graph data structure code adapted from
213+
// http://blog.benoitvallon.com/data-structures-in-javascript/the-graph-data-structure/
214+
function Graph() {
215+
this.nodes = [];
216+
this.nodeIds = [];
217+
this.edges = [];
218+
this.numberOfEdges = 0;
219+
}
220+
Graph.prototype.findNode = function(node) {
221+
for (var i=0; i<this.nodes.length; i++) {
222+
if (node.isSimilar(this.nodes[i])) {
223+
return i;
224+
}
225+
}
226+
return -1; // Not found
227+
};
228+
Graph.prototype.registerNewNode = function(type, midiNoteNumber, timeSincePrevEvent) {
229+
var node = new Node(0, type, midiNoteNumber, timeSincePrevEvent);
230+
var nodeId = graph.findNode(node);
231+
if (nodeId == -1) { // If necessary, create the node
232+
nodeId = this.nodes.length;
233+
this.addNode(node);
234+
}
235+
node.id = nodeId;
236+
if (latestNodeId != null) { // On initialization it will be null
237+
// Add an edge from the previous node to this one
238+
this.addEdge(latestNodeId, nodeId);
239+
}
240+
// Update the latest node ID
241+
latestNodeId = nodeId;
242+
};
243+
Graph.prototype.addNode = function(node) {
244+
var nodeId = this.nodes.length;
245+
this.nodeIds.push(nodeId);
246+
this.nodes.push(node);
247+
this.edges[nodeId] = [];
248+
};
249+
Graph.prototype.addEdge = function(nodeId1, nodeId2) {
250+
this.edges[nodeId1].push(nodeId2);
251+
this.numberOfEdges++;
252+
};
253+
Graph.prototype.drawEdges = function() {
254+
// Draw all edges leading away from this node
255+
strokeWeight(1);
256+
for (var i=0; i<graph.edges.length; i++) {
257+
var startNode = i;
258+
if (startNode == latestNodeId) { // Highlight the latest node's edges
259+
stroke(graph.nodes[startNode].color[0], graph.nodes[startNode].color[1], graph.nodes[startNode].color[2], 100);
260+
} else {
261+
stroke(DEFAULT_NODE_COLOR);
262+
}
263+
for (var j=0; j<graph.edges[i].length; j++) {
264+
var endNode = graph.edges[i][j];
265+
line(graph.nodes[startNode].position.x, graph.nodes[startNode].position.y, graph.nodes[endNode].position.x, graph.nodes[endNode].position.y);
266+
}
267+
}
268+
};

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ <h2>p5.sound
4444
<div><a href="examples/loadSound_with_Drag_and_Drop">loadSound_with_Drag_and_Drop</a></div>
4545
<div><a href="examples/loop_stepSequencer">loop_stepSequencer</a></div>
4646
<div><a href="examples/looper_simple">looper_simple</a></div>
47+
<div><a href="examples/markov_music">markov_music</a></div>
4748
<div><a href="examples/micFFT">micFFT</a></div>
4849
<div><a href="examples/micLevel">micLevel</a></div>
4950
<div><a href="examples/micLevel_on_off">micLevel_on_off</a></div>

0 commit comments

Comments
 (0)