Skip to content

Instantly share code, notes, and snippets.

@kastnerkyle
Last active February 16, 2021 03:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kastnerkyle/7d93ab8db8397c76cbc610fd0ce4fac6 to your computer and use it in GitHub Desktop.
Save kastnerkyle/7d93ab8db8397c76cbc610fd0ce4fac6 to your computer and use it in GitHub Desktop.
Quick and dirty example of piano roll plotting from "music JSON"
{
"seconds_per_quarter": 0.5,
"parts_names": [
"Soprano",
"Alto",
"Tenor",
"Bass"
],
"parts_cumulative_times": [
[
1.0,
2.0,
3.0,
4.0,
5.0,
6.0,
7.0,
8.0,
9.0,
10.0,
11.0,
12.0,
13.0,
14.0,
15.0,
16.0,
17.0,
18.0,
18.5,
19.0,
20.0,
21.0,
22.0,
23.0,
24.0,
25.0,
26.0,
27.0,
28.0,
29.0,
30.0,
31.0,
32.0,
33.0,
34.0,
35.0,
36.0,
37.0,
38.0,
39.0,
40.0,
41.0,
42.0,
42.5,
43.0,
44.0,
45.0,
46.0,
47.0,
48.0
],
[
1.0,
2.0,
3.0,
4.0,
5.0,
6.0,
7.0,
8.0,
9.0,
10.0,
10.5,
11.0,
12.0,
13.0,
14.0,
15.0,
16.0,
17.0,
18.0,
19.0,
20.0,
21.0,
21.5,
22.0,
23.0,
24.0,
25.0,
26.0,
26.5,
27.0,
28.0,
29.0,
29.5,
30.0,
30.5,
31.0,
32.0,
33.0,
34.0,
35.0,
36.0,
37.0,
38.0,
39.0,
40.0,
41.0,
42.0,
43.0,
44.0,
45.0,
45.5,
46.5,
47.0,
48.0
],
[
1.0,
2.0,
3.0,
4.0,
5.0,
6.0,
7.0,
8.0,
9.0,
10.0,
11.0,
12.0,
13.0,
14.0,
15.0,
16.0,
17.0,
18.0,
19.0,
20.0,
20.5,
21.0,
22.0,
23.0,
24.0,
25.0,
25.5,
26.0,
27.0,
28.0,
29.0,
30.0,
31.0,
32.0,
33.0,
34.0,
35.0,
36.0,
37.0,
38.0,
39.0,
40.0,
41.0,
41.5,
42.0,
43.0,
45.0,
45.5,
46.0,
46.5,
47.0,
48.0
],
[
1.0,
2.0,
3.0,
4.0,
5.0,
6.0,
7.0,
8.0,
8.5,
9.0,
10.0,
11.0,
12.0,
13.0,
14.0,
15.0,
16.0,
17.0,
18.0,
19.0,
20.0,
20.5,
21.0,
22.0,
23.0,
24.0,
25.0,
26.0,
26.5,
27.0,
28.0,
29.0,
30.0,
31.0,
32.0,
33.0,
34.0,
35.0,
36.0,
37.0,
38.0,
39.0,
40.0,
41.0,
42.0,
43.0,
44.0,
44.5,
45.0,
46.0,
47.0,
48.0
]
],
"quarter_beats_per_minute": 120.0,
"parts": [
[
67,
67,
63,
65,
67,
63,
62,
60,
67,
67,
65,
70,
67,
63,
65,
67,
67,
70,
72,
74,
75,
74,
72,
71,
72,
72,
74,
72,
70,
69,
67,
69,
67,
72,
70,
69,
70,
67,
67,
65,
63,
67,
68,
67,
65,
63,
65,
63,
62,
60
],
[
63,
62,
60,
60,
60,
60,
59,
55,
63,
63,
65,
63,
62,
63,
60,
60,
59,
60,
62,
65,
67,
65,
63,
65,
67,
67,
65,
65,
64,
66,
67,
62,
62,
67,
67,
66,
62,
60,
62,
63,
65,
63,
63,
62,
58,
60,
60,
59,
60,
62,
59,
60,
59,
55
],
[
60,
55,
55,
56,
55,
56,
50,
51,
60,
58,
58,
58,
58,
56,
48,
50,
51,
55,
56,
58,
56,
58,
60,
62,
63,
57,
58,
57,
55,
60,
60,
58,
60,
58,
53,
53,
60,
58,
58,
60,
53,
55,
55,
53,
51,
50,
56,
56,
55,
53,
55,
52
],
[
48,
47,
48,
56,
51,
53,
55,
48,
48,
50,
51,
50,
55,
51,
56,
44,
43,
48,
55,
53,
51,
53,
55,
56,
55,
48,
53,
46,
48,
50,
52,
54,
55,
50,
43,
45,
46,
48,
50,
51,
44,
46,
39,
40,
41,
43,
44,
43,
41,
43,
43,
48
]
],
"pulses_per_quarter": 220,
"parts_times": [
[
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
1.0
],
[
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
0.5,
0.5,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
1.0,
0.5,
1.0
],
[
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
2.0,
0.5,
0.5,
0.5,
0.5,
1.0
],
[
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.5,
0.5,
1.0,
1.0,
1.0
]
]
}
# Author: Kyle Kastner
# webpage parts based heavily on examples from indirajhenny
# http://bl.ocks.org/indirajhenny/15d7218cadf4fa96e407f87f034b1ff7
pre = '''
var notes = [
'''
pre_chunk = '''{
'''
chunk = ''' "name": "{}",
"midi": {},
"time": {},
"velocity": {},
"duration": {}
'''
post_chunk = '''},
'''
post = '''
];
'''
# need to auto-generate this...
code_stub_pre = '''
var midiOnly = [];
notes.forEach(function(item) {
//get value of name
var val = item.midi;
//push duration value into array
midiOnly.push(val);
});
//console.log(midiOnly);
'''
# create empty lists, as many as we have note range (start with 21?)
# create notes function and final function
N_LANES = 108 - 20
midi_to_name_lookup = {}
note_bases = {0: "C",
1: "C#",
2: "D",
3: "Eb",
4: "E",
5: "F",
6: "F#",
7: "G",
8: "Ab",
9: "A",
10: "Bb",
11: "B"}
for i in range(20, 108):
b = note_bases[i % 12]
o = (i // 12) - 1
midi_to_name_lookup[i] = b + str(o)
"""
LANES_LOOKUP= {20: "C3",
19: "D3",
18: "E3",
17: "F3",
16: "G3",
15: "A3",
14: "B3",
13: "C4",
12: "D4",
11: "E4",
10: "F4",
9: "G4",
8: "A4",
7: "B4",
6: "C5",
5: "D5",
4: "E5",
3: "F5",
2: "G5",
1: "A5",
0: "B5",
}
"""
LANES_LOOKUP = {}
for _n, i in enumerate(range(20, 108)):
rev = list(range(20, 108))[::-1]
LANES_LOOKUP[i - 20] = midi_to_name_lookup[rev[_n]]
def _create_code_stub_init():
s = "\n"
template = "minimalNotes{} = [];\n"
for i in range(N_LANES):
s = s + template.format(i)
s = s + "minimalNotesFinal = [];\n"
return s
code_stub_init = _create_code_stub_init()
def _create_code_stub_functions():
# use simple subs because the string has both {} and ()
from string import Template
s = "\n"
template = '''
minimalNotes$index = notes.filter(function (el){
return el.name == "$note_name";
});
var minimalNotesFinal$index = minimalNotes$index.map(function(el){
var addKey = Object.assign({}, el);
addKey.lane = $lane_index;
return addKey;
});
'''
t = Template(template)
for i in range(N_LANES):
s = s + t.substitute(index=i, lane_index=i, note_name=LANES_LOOKUP[i])
return s
code_stub_functions = _create_code_stub_functions()
def _create_code_stub_combine():
s = "\n"
template = "minimalNotesFinal = minimalNotesFinal.concat(minimalNotesFinal{})\n"
for i in range(N_LANES):
s = s + template.format(i)
s + "console.log(minimalNotesFinal)\n"
return s
code_stub_combine = _create_code_stub_combine()
def make_chunk(note_tuple):
midi = note_tuple[0]
name = midi_to_name_lookup[midi]
start_time = note_tuple[1]
velocity = 1.0
duration = note_tuple[2]
s = pre_chunk + chunk.format(name, midi, start_time, velocity, duration) + post_chunk
return s
code_stub = code_stub_pre + code_stub_init + code_stub_functions + code_stub_combine
def make_plot_json(list_of_notes):
cur = pre
# voice track?
for note in list_of_notes:
c = make_chunk(note)
cur = cur + c
return cur[:-2] + post + code_stub
def make_website_string(javascript_note_data_string, page_name="Piano Roll Plot", end_time=60):
from string import Template
with open("report_template.html", "r") as f:
l = f.read()
t = Template(l)
# if we reverse the list, we reverse the axis
return t.substitute(PAGE_NAME=page_name, JAVASCRIPT_NOTE_DATA=javascript_note_data_string, LANE_NAMES=str([LANES_LOOKUP[i] for i in range(N_LANES)]), LANE_TIME_END=end_time)
qppm = 220
#qpps = qppm / 60.
quarter_time_const_s = .5
# notes should be note value, start time, duration
# we add the name internally to be consistent
import json
with open("bwv101.7.C-minor-transposed.json", "r") as f:
music_json_data = json.load(f)
l = []
for _p in range(len(music_json_data["parts"])):
parts = music_json_data["parts"][_p]
parts_times = music_json_data["parts_times"][_p]
parts_cumulative_times = music_json_data["parts_cumulative_times"][_p]
assert len(parts) == len(parts_times)
assert len(parts_times) == len(parts_cumulative_times)
for _s in range(len(parts)):
d = parts_times[_s]
l.append((parts[_s], parts_cumulative_times[_s] - d, d))
last_step = max([t[1] for t in l])
last_step_dur = max([t[2] for t in l if t[1] == last_step])
end_time = last_step + last_step_dur
r = make_plot_json(l)
"""
# write out the json + javascript
with open("notesOnlyJSON.js", "w") as f:
f.write(r)
"""
# write out the index.html with minor modifications for lane names
w = make_website_string(javascript_note_data_string=r, end_time=end_time)
with open("report1.html", "w") as f:
f.write(w)
# test example, every note
l = []
start = 0.
for i in range(20, 108):
l.append((i, start, 1.))
start += 1.
last_step = max([t[1] for t in l])
last_step_dur = max([t[2] for t in l if t[1] == last_step])
end_time = last_step + last_step_dur
r = make_plot_json(l)
w = make_website_string(javascript_note_data_string=r, end_time=end_time)
with open("report2.html", "w") as f:
f.write(w)
def make_index_html_string(list_of_report_file_base_names):
from string import Template
with open("index_template.html", "r") as f:
l = f.read()
report_div_template = '<div id="{}"></div>\n'
report_load_template = " $('#{}').load('{}.html');\n"
index_chunk = "\n"
# do divs
for name in list_of_report_file_base_names:
index_chunk = index_chunk + report_div_template.format(name)
# do script tag
index_chunk = index_chunk + "<script>\n"
# do loads
for name in list_of_report_file_base_names:
index_chunk = index_chunk + report_load_template.format(name, name)
# end script
index_chunk = index_chunk + "</script>\n"
# unused example of what it should look like
"""
example_index_chunk = '''
<div id="report1"></div>
<div id="report2"></div>
<script>
$('#report1').load('report1.html');
$('#report2').load('report2.html');
</script>
'''
"""
t = Template(l)
return t.substitute(INDEX_BODY=index_chunk)
w = make_index_html_string(["report1","report2"])
#w = make_index_html_string(["report2"])
with open("index.html", "w") as f:
f.write(w)
from IPython import embed; embed(); raise ValueError()
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>load demo</title>
<style>
body {
font-size: 12px;
font-family: Arial;
}
</style>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.js"></script>
<script src="https://code.jquery.com/jquery-3.5.0.js"></script>
</head>
<body>
$INDEX_BODY
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title> $PAGE_NAME </title>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.js"></script>
<script type="text/javascript">
$JAVASCRIPT_NOTE_DATA
</script>
<style type="text/css">
.chart {
shape-rendering: crispEdges;
}
.mini text {
font: 9px sans-serif;
}
.main text {
font: 12px sans-serif;
}
.miniItem {
fill: slategray;
fill-opacity: .7;
stroke: black;
stroke-width: 2;
}
.brush .extent {
stroke: gray;
fill: dodgerblue;
fill-opacity: .365;
}
</style>
</head>
<body>
<script type="text/javascript">
//data
var lanes = $LANE_NAMES
laneLength = lanes.length,
timeBegin = 0,
timeEnd = $LANE_TIME_END ;
</script>
<script type="text/javascript">
var m = [20, 15, 15, 120], //top right bottom left
w = 960 - m[1] - m[3],
h = 600 - m[0] - m[2],
miniHeight = laneLength * 6 //* 18, //removed *12
mainHeight = h - miniHeight - 50;
//scales
var x = d3.scale.linear() //where is this positioned? at the top or bottom? this might be for main
.domain([timeBegin, timeEnd])
.range([0, w]);
var y2 = d3.scale.linear()
.domain([0, laneLength])
.range([0, miniHeight]);
var zoom = d3.behavior.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var //xAxis = d3.svg.axis().scale(x1).orient("bottom"),
xAxis2 = d3.svg.axis().scale(x).orient("bottom"),
drag = d3.behavior.drag() //when moving rect from left side, other components of rectangle need to be resized //so we must include this...i think
.on("drag", dragmove);
chart = d3.select("body")
.append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.attr("class", "chart")
.call(zoom);
var mini = chart.append("g")
.attr("transform", "translate(" + m[3] + "," + (mainHeight + m[0]) + ")")
.attr("width", w)
.attr("height", miniHeight)
.attr("class", "mini");
//mini lanes and texts
mini.append("g").selectAll(".laneLines")
.data(minimalNotesFinal)
//.data(notes)
.enter().append("line")
.attr("x1", 0)
.attr("y1", function(d) {return y2(d.lane);})
.attr("x2", w)
.attr("y2", function(d) {return y2(d.lane);})
.attr("stroke", "lightgray");
mini.append("g").selectAll(".laneText")
.data(lanes)
.enter().append("text")
.text(function(d) {return d;})
.attr("x", -m[1])
.attr("y", function(d, i) {return y2(i + .5);})
.attr("dy", ".5ex")
.attr("text-anchor", "end")
.attr("class", "laneText");
mini.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + miniHeight + ")")
.call(xAxis2);
//mini item rects soon to be brushes
mini.append("g").selectAll("miniItems")
.data(minimalNotesFinal)
.enter().append("rect") //"brush"
.attr("class", function(d) {return "miniItem";}) //midi
.attr("x", function(d) {return x(d.time);}) //time
.attr("y", function(d) {return y2(d.lane + .5) - 2.5;}) //midi
.attr("width", function(d) {
return x(d.duration);})
.attr("height", 5)
.on('click', noteClicked)
.call(drag);
mini.append("g")
.attr("class", "x brush")
//.call(brush)
.selectAll("rect")
.attr("y", 1)
.attr("height", miniHeight - 1);
function noteClicked(d, i) { //
if (d3.event.defaultPrevented) return; // dragged
}
function dragmove(d) {
d3.select(this)
.attr("x", d.x = Math.max(0, d3.event.x));
}
function zoomed() {
mini.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment