Last active
February 16, 2021 03:45
-
-
Save kastnerkyle/7d93ab8db8397c76cbc610fd0ce4fac6 to your computer and use it in GitHub Desktop.
Quick and dirty example of piano roll plotting from "music JSON"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 | |
] | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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