cgcardona / muse public
render_midi_demo.py python
2017 lines 85.8 KB
8d1c0847 fix: escape backslash-n in JS split() call inside Python template strin… Gabriel Cardona <cgcardona@gmail.com> 1d ago
1 #!/usr/bin/env python3
2 """MIDI Demo Page generator — Bach Prelude BWV 846 × Muse VCS.
3
4 Outputs: artifacts/midi-demo.html
5
6 Demonstrates Muse's 21-dimensional MIDI version control using
7 Bach's Prelude No. 1 in C Major (BWV 846).
8
9 Note data sourced from the music21 corpus (MuseScore 1.3 transcription,
10 2013-07-09, musescore.com/score/117279). Bach died 1750 — public domain.
11 Format: [pitch_midi, velocity, start_sec, duration_sec, measure, voice]
12 """
13
14 import json
15 import logging
16 import pathlib
17
18 logger = logging.getLogger(__name__)
19
20 # ──────────────────────────────────────────────────────────────────────────────
21 # Bach BWV 846 — note data extracted from music21 corpus MusicXML
22 # Voices: 1=treble arpeggios (pitch 55-81), 5=bass long notes (36-60),
23 # 6=inner voice (47-64). Tempo: 66 BPM, Duration: ~127s.
24 # ──────────────────────────────────────────────────────────────────────────────
25 _BACH_NOTES_JSON = (
26 "[[60,64,0.0,1.5833,1,5],[64,64,0.2083,0.5938,1,6],[67,80,0.4167,0.1979,1,1],"
27 "[72,80,0.625,0.1979,1,1],[76,80,0.8333,0.1979,1,1],[67,80,1.0417,0.1979,1,1],"
28 "[72,80,1.25,0.1979,1,1],[76,80,1.4583,0.1979,1,1],[60,64,1.6667,1.5833,1,5],"
29 "[64,64,1.875,0.5938,1,6],[67,80,2.0833,0.1979,1,1],[72,80,2.2917,0.1979,1,1],"
30 "[76,80,2.5,0.1979,1,1],[67,80,2.7083,0.1979,1,1],[72,80,2.9167,0.1979,1,1],"
31 "[76,80,3.125,0.1979,1,1],[60,64,3.3333,1.5833,2,5],[62,64,3.5417,0.5938,2,6],"
32 "[69,80,3.75,0.1979,2,1],[74,80,3.9583,0.1979,2,1],[77,80,4.1667,0.1979,2,1],"
33 "[69,80,4.375,0.1979,2,1],[74,80,4.5833,0.1979,2,1],[77,80,4.7917,0.1979,2,1],"
34 "[60,64,5.0,1.5833,2,5],[62,64,5.2083,0.5938,2,6],[69,80,5.4167,0.1979,2,1],"
35 "[74,80,5.625,0.1979,2,1],[77,80,5.8333,0.1979,2,1],[69,80,6.0417,0.1979,2,1],"
36 "[74,80,6.25,0.1979,2,1],[77,80,6.4583,0.1979,2,1],[59,64,6.6667,1.5833,3,5],"
37 "[62,64,6.875,0.5938,3,6],[67,80,7.0833,0.1979,3,1],[74,80,7.2917,0.1979,3,1],"
38 "[77,80,7.5,0.1979,3,1],[67,80,7.7083,0.1979,3,1],[74,80,7.9167,0.1979,3,1],"
39 "[77,80,8.125,0.1979,3,1],[59,64,8.3333,1.5833,3,5],[62,64,8.5417,0.5938,3,6],"
40 "[67,80,8.75,0.1979,3,1],[74,80,8.9583,0.1979,3,1],[77,80,9.1667,0.1979,3,1],"
41 "[67,80,9.375,0.1979,3,1],[74,80,9.5833,0.1979,3,1],[77,80,9.7917,0.1979,3,1],"
42 "[60,64,10.0,1.5833,4,5],[64,64,10.2083,0.5938,4,6],[67,80,10.4167,0.1979,4,1],"
43 "[72,80,10.625,0.1979,4,1],[76,80,10.8333,0.1979,4,1],[67,80,11.0417,0.1979,4,1],"
44 "[72,80,11.25,0.1979,4,1],[76,80,11.4583,0.1979,4,1],[60,64,11.6667,1.5833,4,5],"
45 "[64,64,11.875,0.5938,4,6],[67,80,12.0833,0.1979,4,1],[72,80,12.2917,0.1979,4,1],"
46 "[76,80,12.5,0.1979,4,1],[67,80,12.7083,0.1979,4,1],[72,80,12.9167,0.1979,4,1],"
47 "[76,80,13.125,0.1979,4,1],[60,64,13.3333,1.5833,5,5],[64,64,13.5417,0.5938,5,6],"
48 "[69,80,13.75,0.1979,5,1],[76,80,13.9583,0.1979,5,1],[81,80,14.1667,0.1979,5,1],"
49 "[69,80,14.375,0.1979,5,1],[76,80,14.5833,0.1979,5,1],[81,80,14.7917,0.1979,5,1],"
50 "[60,64,15.0,1.5833,5,5],[64,64,15.2083,0.5938,5,6],[69,80,15.4167,0.1979,5,1],"
51 "[76,80,15.625,0.1979,5,1],[81,80,15.8333,0.1979,5,1],[69,80,16.0417,0.1979,5,1],"
52 "[76,80,16.25,0.1979,5,1],[81,80,16.4583,0.1979,5,1],[60,64,16.6667,1.5833,6,5],"
53 "[62,64,16.875,0.5938,6,6],[66,80,17.0833,0.1979,6,1],[69,80,17.2917,0.1979,6,1],"
54 "[74,80,17.5,0.1979,6,1],[66,80,17.7083,0.1979,6,1],[69,80,17.9167,0.1979,6,1],"
55 "[74,80,18.125,0.1979,6,1],[60,64,18.3333,1.5833,6,5],[62,64,18.5417,0.5938,6,6],"
56 "[66,80,18.75,0.1979,6,1],[69,80,18.9583,0.1979,6,1],[74,80,19.1667,0.1979,6,1],"
57 "[66,80,19.375,0.1979,6,1],[69,80,19.5833,0.1979,6,1],[74,80,19.7917,0.1979,6,1],"
58 "[59,64,20.0,1.5833,7,5],[62,64,20.2083,0.5938,7,6],[67,80,20.4167,0.1979,7,1],"
59 "[74,80,20.625,0.1979,7,1],[79,80,20.8333,0.1979,7,1],[67,80,21.0417,0.1979,7,1],"
60 "[74,80,21.25,0.1979,7,1],[79,80,21.4583,0.1979,7,1],[59,64,21.6667,1.5833,7,5],"
61 "[62,64,21.875,0.5938,7,6],[67,80,22.0833,0.1979,7,1],[74,80,22.2917,0.1979,7,1],"
62 "[79,80,22.5,0.1979,7,1],[67,80,22.7083,0.1979,7,1],[74,80,22.9167,0.1979,7,1],"
63 "[79,80,23.125,0.1979,7,1],[59,64,23.3333,1.5833,8,5],[60,64,23.5417,0.5938,8,6],"
64 "[64,80,23.75,0.1979,8,1],[67,80,23.9583,0.1979,8,1],[72,80,24.1667,0.1979,8,1],"
65 "[64,80,24.375,0.1979,8,1],[67,80,24.5833,0.1979,8,1],[72,80,24.7917,0.1979,8,1],"
66 "[59,64,25.0,1.5833,8,5],[60,64,25.2083,0.5938,8,6],[64,80,25.4167,0.1979,8,1],"
67 "[67,80,25.625,0.1979,8,1],[72,80,25.8333,0.1979,8,1],[64,80,26.0417,0.1979,8,1],"
68 "[67,80,26.25,0.1979,8,1],[72,80,26.4583,0.1979,8,1],[57,64,26.6667,1.5833,9,5],"
69 "[60,64,26.875,0.5938,9,6],[64,80,27.0833,0.1979,9,1],[67,80,27.2917,0.1979,9,1],"
70 "[72,80,27.5,0.1979,9,1],[64,80,27.7083,0.1979,9,1],[67,80,27.9167,0.1979,9,1],"
71 "[72,80,28.125,0.1979,9,1],[57,64,28.3333,1.5833,9,5],[60,64,28.5417,0.5938,9,6],"
72 "[64,80,28.75,0.1979,9,1],[67,80,28.9583,0.1979,9,1],[72,80,29.1667,0.1979,9,1],"
73 "[64,80,29.375,0.1979,9,1],[67,80,29.5833,0.1979,9,1],[72,80,29.7917,0.1979,9,1],"
74 "[50,64,30.0,1.5833,10,5],[57,64,30.2083,0.5938,10,6],[62,80,30.4167,0.1979,10,1],"
75 "[66,80,30.625,0.1979,10,1],[72,80,30.8333,0.1979,10,1],[62,80,31.0417,0.1979,10,1],"
76 "[66,80,31.25,0.1979,10,1],[72,80,31.4583,0.1979,10,1],[50,64,31.6667,1.5833,10,5],"
77 "[57,64,31.875,0.5938,10,6],[62,80,32.0833,0.1979,10,1],[66,80,32.2917,0.1979,10,1],"
78 "[72,80,32.5,0.1979,10,1],[62,80,32.7083,0.1979,10,1],[66,80,32.9167,0.1979,10,1],"
79 "[72,80,33.125,0.1979,10,1],[55,64,33.3333,1.5833,11,5],[59,64,33.5417,0.5938,11,6],"
80 "[62,80,33.75,0.1979,11,1],[67,80,33.9583,0.1979,11,1],[71,80,34.1667,0.1979,11,1],"
81 "[62,80,34.375,0.1979,11,1],[67,80,34.5833,0.1979,11,1],[71,80,34.7917,0.1979,11,1],"
82 "[55,64,35.0,1.5833,11,5],[59,64,35.2083,0.5938,11,6],[62,80,35.4167,0.1979,11,1],"
83 "[67,80,35.625,0.1979,11,1],[71,80,35.8333,0.1979,11,1],[62,80,36.0417,0.1979,11,1],"
84 "[67,80,36.25,0.1979,11,1],[71,80,36.4583,0.1979,11,1],[55,64,36.6667,1.5833,12,5],"
85 "[58,64,36.875,0.5938,12,6],[64,80,37.0833,0.1979,12,1],[67,80,37.2917,0.1979,12,1],"
86 "[73,80,37.5,0.1979,12,1],[64,80,37.7083,0.1979,12,1],[67,80,37.9167,0.1979,12,1],"
87 "[73,80,38.125,0.1979,12,1],[55,64,38.3333,1.5833,12,5],[58,64,38.5417,0.5938,12,6],"
88 "[64,80,38.75,0.1979,12,1],[67,80,38.9583,0.1979,12,1],[73,80,39.1667,0.1979,12,1],"
89 "[64,80,39.375,0.1979,12,1],[67,80,39.5833,0.1979,12,1],[73,80,39.7917,0.1979,12,1],"
90 "[53,64,40.0,1.5833,13,5],[57,64,40.2083,0.5938,13,6],[62,80,40.4167,0.1979,13,1],"
91 "[69,80,40.625,0.1979,13,1],[74,80,40.8333,0.1979,13,1],[62,80,41.0417,0.1979,13,1],"
92 "[69,80,41.25,0.1979,13,1],[74,80,41.4583,0.1979,13,1],[53,64,41.6667,1.5833,13,5],"
93 "[57,64,41.875,0.5938,13,6],[62,80,42.0833,0.1979,13,1],[69,80,42.2917,0.1979,13,1],"
94 "[74,80,42.5,0.1979,13,1],[62,80,42.7083,0.1979,13,1],[69,80,42.9167,0.1979,13,1],"
95 "[74,80,43.125,0.1979,13,1],[53,64,43.3333,1.5833,14,5],[56,64,43.5417,0.5938,14,6],"
96 "[62,80,43.75,0.1979,14,1],[65,80,43.9583,0.1979,14,1],[71,80,44.1667,0.1979,14,1],"
97 "[62,80,44.375,0.1979,14,1],[65,80,44.5833,0.1979,14,1],[71,80,44.7917,0.1979,14,1],"
98 "[53,64,45.0,1.5833,14,5],[56,64,45.2083,0.5938,14,6],[62,80,45.4167,0.1979,14,1],"
99 "[65,80,45.625,0.1979,14,1],[71,80,45.8333,0.1979,14,1],[62,80,46.0417,0.1979,14,1],"
100 "[65,80,46.25,0.1979,14,1],[71,80,46.4583,0.1979,14,1],[52,64,46.6667,1.5833,15,5],"
101 "[55,64,46.875,0.5938,15,6],[60,80,47.0833,0.1979,15,1],[67,80,47.2917,0.1979,15,1],"
102 "[72,80,47.5,0.1979,15,1],[60,80,47.7083,0.1979,15,1],[67,80,47.9167,0.1979,15,1],"
103 "[72,80,48.125,0.1979,15,1],[52,64,48.3333,1.5833,15,5],[55,64,48.5417,0.5938,15,6],"
104 "[60,80,48.75,0.1979,15,1],[67,80,48.9583,0.1979,15,1],[72,80,49.1667,0.1979,15,1],"
105 "[60,80,49.375,0.1979,15,1],[67,80,49.5833,0.1979,15,1],[72,80,49.7917,0.1979,15,1],"
106 "[52,64,50.0,1.5833,16,5],[53,64,50.2083,0.5938,16,6],[57,80,50.4167,0.1979,16,1],"
107 "[60,80,50.625,0.1979,16,1],[65,80,50.8333,0.1979,16,1],[57,80,51.0417,0.1979,16,1],"
108 "[60,80,51.25,0.1979,16,1],[65,80,51.4583,0.1979,16,1],[52,64,51.6667,1.5833,16,5],"
109 "[53,64,51.875,0.5938,16,6],[57,80,52.0833,0.1979,16,1],[60,80,52.2917,0.1979,16,1],"
110 "[65,80,52.5,0.1979,16,1],[57,80,52.7083,0.1979,16,1],[60,80,52.9167,0.1979,16,1],"
111 "[65,80,53.125,0.1979,16,1],[50,64,53.3333,1.5833,17,5],[53,64,53.5417,0.5938,17,6],"
112 "[57,80,53.75,0.1979,17,1],[60,80,53.9583,0.1979,17,1],[65,80,54.1667,0.1979,17,1],"
113 "[57,80,54.375,0.1979,17,1],[60,80,54.5833,0.1979,17,1],[65,80,54.7917,0.1979,17,1],"
114 "[50,64,55.0,1.5833,17,5],[53,64,55.2083,0.5938,17,6],[57,80,55.4167,0.1979,17,1],"
115 "[60,80,55.625,0.1979,17,1],[65,80,55.8333,0.1979,17,1],[57,80,56.0417,0.1979,17,1],"
116 "[60,80,56.25,0.1979,17,1],[65,80,56.4583,0.1979,17,1],[43,64,56.6667,1.5833,18,5],"
117 "[50,64,56.875,0.5938,18,6],[55,80,57.0833,0.1979,18,1],[59,80,57.2917,0.1979,18,1],"
118 "[65,80,57.5,0.1979,18,1],[55,80,57.7083,0.1979,18,1],[59,80,57.9167,0.1979,18,1],"
119 "[65,80,58.125,0.1979,18,1],[43,64,58.3333,1.5833,18,5],[50,64,58.5417,0.5938,18,6],"
120 "[55,80,58.75,0.1979,18,1],[59,80,58.9583,0.1979,18,1],[65,80,59.1667,0.1979,18,1],"
121 "[55,80,59.375,0.1979,18,1],[59,80,59.5833,0.1979,18,1],[65,80,59.7917,0.1979,18,1],"
122 "[48,64,60.0,1.5833,19,5],[52,64,60.2083,0.5938,19,6],[55,80,60.4167,0.1979,19,1],"
123 "[60,80,60.625,0.1979,19,1],[64,80,60.8333,0.1979,19,1],[55,80,61.0417,0.1979,19,1],"
124 "[60,80,61.25,0.1979,19,1],[64,80,61.4583,0.1979,19,1],[48,64,61.6667,1.5833,19,5],"
125 "[52,64,61.875,0.5938,19,6],[55,80,62.0833,0.1979,19,1],[60,80,62.2917,0.1979,19,1],"
126 "[64,80,62.5,0.1979,19,1],[55,80,62.7083,0.1979,19,1],[60,80,62.9167,0.1979,19,1],"
127 "[64,80,63.125,0.1979,19,1],[48,64,63.3333,1.5833,20,5],[55,64,63.5417,0.5938,20,6],"
128 "[58,80,63.75,0.1979,20,1],[60,80,63.9583,0.1979,20,1],[64,80,64.1667,0.1979,20,1],"
129 "[58,80,64.375,0.1979,20,1],[60,80,64.5833,0.1979,20,1],[64,80,64.7917,0.1979,20,1],"
130 "[48,64,65.0,1.5833,20,5],[55,64,65.2083,0.5938,20,6],[58,80,65.4167,0.1979,20,1],"
131 "[60,80,65.625,0.1979,20,1],[64,80,65.8333,0.1979,20,1],[58,80,66.0417,0.1979,20,1],"
132 "[60,80,66.25,0.1979,20,1],[64,80,66.4583,0.1979,20,1],[41,64,66.6667,1.5833,21,5],"
133 "[53,64,66.875,0.5938,21,6],[57,80,67.0833,0.1979,21,1],[60,80,67.2917,0.1979,21,1],"
134 "[64,80,67.5,0.1979,21,1],[57,80,67.7083,0.1979,21,1],[60,80,67.9167,0.1979,21,1],"
135 "[64,80,68.125,0.1979,21,1],[41,64,68.3333,1.5833,21,5],[53,64,68.5417,0.5938,21,6],"
136 "[57,80,68.75,0.1979,21,1],[60,80,68.9583,0.1979,21,1],[64,80,69.1667,0.1979,21,1],"
137 "[57,80,69.375,0.1979,21,1],[60,80,69.5833,0.1979,21,1],[64,80,69.7917,0.1979,21,1],"
138 "[42,64,70.0,1.5833,22,5],[48,64,70.2083,0.5938,22,6],[57,80,70.4167,0.1979,22,1],"
139 "[60,80,70.625,0.1979,22,1],[63,80,70.8333,0.1979,22,1],[57,80,71.0417,0.1979,22,1],"
140 "[60,80,71.25,0.1979,22,1],[63,80,71.4583,0.1979,22,1],[42,64,71.6667,1.5833,22,5],"
141 "[48,64,71.875,0.5938,22,6],[57,80,72.0833,0.1979,22,1],[60,80,72.2917,0.1979,22,1],"
142 "[63,80,72.5,0.1979,22,1],[57,80,72.7083,0.1979,22,1],[60,80,72.9167,0.1979,22,1],"
143 "[63,80,73.125,0.1979,22,1],[44,64,73.3333,1.5833,23,5],[53,64,73.5417,0.5938,23,6],"
144 "[59,80,73.75,0.1979,23,1],[60,80,73.9583,0.1979,23,1],[62,80,74.1667,0.1979,23,1],"
145 "[59,80,74.375,0.1979,23,1],[60,80,74.5833,0.1979,23,1],[62,80,74.7917,0.1979,23,1],"
146 "[44,64,75.0,1.5833,23,5],[53,64,75.2083,0.5938,23,6],[59,80,75.4167,0.1979,23,1],"
147 "[60,80,75.625,0.1979,23,1],[62,80,75.8333,0.1979,23,1],[59,80,76.0417,0.1979,23,1],"
148 "[60,80,76.25,0.1979,23,1],[62,80,76.4583,0.1979,23,1],[43,64,76.6667,1.5833,24,5],"
149 "[53,64,76.875,0.5938,24,6],[55,80,77.0833,0.1979,24,1],[59,80,77.2917,0.1979,24,1],"
150 "[62,80,77.5,0.1979,24,1],[55,80,77.7083,0.1979,24,1],[59,80,77.9167,0.1979,24,1],"
151 "[62,80,78.125,0.1979,24,1],[43,64,78.3333,1.5833,24,5],[53,64,78.5417,0.5938,24,6],"
152 "[55,80,78.75,0.1979,24,1],[59,80,78.9583,0.1979,24,1],[62,80,79.1667,0.1979,24,1],"
153 "[55,80,79.375,0.1979,24,1],[59,80,79.5833,0.1979,24,1],[62,80,79.7917,0.1979,24,1],"
154 "[43,64,80.0,1.5833,25,5],[52,64,80.2083,0.5938,25,6],[55,80,80.4167,0.1979,25,1],"
155 "[60,80,80.625,0.1979,25,1],[64,80,80.8333,0.1979,25,1],[55,80,81.0417,0.1979,25,1],"
156 "[60,80,81.25,0.1979,25,1],[64,80,81.4583,0.1979,25,1],[43,64,81.6667,1.5833,25,5],"
157 "[52,64,81.875,0.5938,25,6],[55,80,82.0833,0.1979,25,1],[60,80,82.2917,0.1979,25,1],"
158 "[64,80,82.5,0.1979,25,1],[55,80,82.7083,0.1979,25,1],[60,80,82.9167,0.1979,25,1],"
159 "[64,80,83.125,0.1979,25,1],[43,64,83.3333,1.5833,26,5],[50,64,83.5417,0.5938,26,6],"
160 "[55,80,83.75,0.1979,26,1],[59,80,83.9583,0.1979,26,1],[65,80,84.1667,0.1979,26,1],"
161 "[55,80,84.375,0.1979,26,1],[59,80,84.5833,0.1979,26,1],[65,80,84.7917,0.1979,26,1],"
162 "[43,64,85.0,1.5833,26,5],[50,64,85.2083,0.5938,26,6],[55,80,85.4167,0.1979,26,1],"
163 "[59,80,85.625,0.1979,26,1],[65,80,85.8333,0.1979,26,1],[55,80,86.0417,0.1979,26,1],"
164 "[59,80,86.25,0.1979,26,1],[65,80,86.4583,0.1979,26,1],[43,64,86.6667,1.5833,27,5],"
165 "[51,64,86.875,0.5938,27,6],[57,80,87.0833,0.1979,27,1],[60,80,87.2917,0.1979,27,1],"
166 "[66,80,87.5,0.1979,27,1],[57,80,87.7083,0.1979,27,1],[60,80,87.9167,0.1979,27,1],"
167 "[66,80,88.125,0.1979,27,1],[43,64,88.3333,1.5833,27,5],[51,64,88.5417,0.5938,27,6],"
168 "[57,80,88.75,0.1979,27,1],[60,80,88.9583,0.1979,27,1],[66,80,89.1667,0.1979,27,1],"
169 "[57,80,89.375,0.1979,27,1],[60,80,89.5833,0.1979,27,1],[66,80,89.7917,0.1979,27,1],"
170 "[43,64,90.0,1.5833,28,5],[52,64,90.2083,0.5938,28,6],[55,80,90.4167,0.1979,28,1],"
171 "[60,80,90.625,0.1979,28,1],[67,80,90.8333,0.1979,28,1],[55,80,91.0417,0.1979,28,1],"
172 "[60,80,91.25,0.1979,28,1],[67,80,91.4583,0.1979,28,1],[43,64,91.6667,1.5833,28,5],"
173 "[52,64,91.875,0.5938,28,6],[55,80,92.0833,0.1979,28,1],[60,80,92.2917,0.1979,28,1],"
174 "[67,80,92.5,0.1979,28,1],[55,80,92.7083,0.1979,28,1],[60,80,92.9167,0.1979,28,1],"
175 "[67,80,93.125,0.1979,28,1],[43,64,93.3333,1.5833,29,5],[50,64,93.5417,0.5938,29,6],"
176 "[55,80,93.75,0.1979,29,1],[60,80,93.9583,0.1979,29,1],[65,80,94.1667,0.1979,29,1],"
177 "[55,80,94.375,0.1979,29,1],[60,80,94.5833,0.1979,29,1],[65,80,94.7917,0.1979,29,1],"
178 "[43,64,95.0,1.5833,29,5],[50,64,95.2083,0.5938,29,6],[55,80,95.4167,0.1979,29,1],"
179 "[60,80,95.625,0.1979,29,1],[65,80,95.8333,0.1979,29,1],[55,80,96.0417,0.1979,29,1],"
180 "[60,80,96.25,0.1979,29,1],[65,80,96.4583,0.1979,29,1],[43,64,96.6667,1.5833,30,5],"
181 "[50,64,96.875,0.5938,30,6],[55,80,97.0833,0.1979,30,1],[59,80,97.2917,0.1979,30,1],"
182 "[65,80,97.5,0.1979,30,1],[55,80,97.7083,0.1979,30,1],[59,80,97.9167,0.1979,30,1],"
183 "[65,80,98.125,0.1979,30,1],[43,64,98.3333,1.5833,30,5],[50,64,98.5417,0.5938,30,6],"
184 "[55,80,98.75,0.1979,30,1],[59,80,98.9583,0.1979,30,1],[65,80,99.1667,0.1979,30,1],"
185 "[55,80,99.375,0.1979,30,1],[59,80,99.5833,0.1979,30,1],[65,80,99.7917,0.1979,30,1],"
186 "[36,64,100.0,1.5833,31,5],[48,64,100.2083,0.5938,31,6],[55,80,100.4167,0.1979,31,1],"
187 "[58,80,100.625,0.1979,31,1],[64,80,100.8333,0.1979,31,1],[55,80,101.0417,0.1979,31,1],"
188 "[58,80,101.25,0.1979,31,1],[64,80,101.4583,0.1979,31,1],[36,64,101.6667,1.5833,31,5],"
189 "[48,64,101.875,0.5938,31,6],[55,80,102.0833,0.1979,31,1],[58,80,102.2917,0.1979,31,1],"
190 "[64,80,102.5,0.1979,31,1],[55,80,102.7083,0.1979,31,1],[58,80,102.9167,0.1979,31,1],"
191 "[64,80,103.125,0.1979,31,1],[36,64,103.3333,1.5833,32,5],[48,64,103.5417,0.5938,32,6],"
192 "[53,80,103.75,0.1979,32,1],[57,80,103.9583,0.1979,32,1],[60,80,104.1667,0.1979,32,1],"
193 "[65,80,104.375,0.1979,32,1],[60,80,104.5833,0.1979,32,1],[57,80,104.7917,0.1979,32,1],"
194 "[60,80,105.0,0.1979,32,1],[57,80,105.2083,0.1979,32,1],[53,80,105.4167,0.1979,32,1],"
195 "[57,80,105.625,0.1979,32,1],[53,80,105.8333,0.1979,32,1],[50,80,106.0417,0.1979,32,1],"
196 "[53,80,106.25,0.1979,32,1],[50,80,106.4583,0.1979,32,1],[36,64,116.3636,1.7273,33,5],"
197 "[47,64,116.5909,0.6477,33,6],[67,80,116.8182,0.2159,33,1],[71,80,117.0455,0.2159,33,1],"
198 "[74,80,117.2727,0.2159,33,1],[77,80,117.5,0.2159,33,1],[74,80,117.7273,0.2159,33,1],"
199 "[71,80,117.9545,0.2159,33,1],[74,80,118.1818,0.2159,33,1],[71,80,118.4091,0.2159,33,1],"
200 "[67,80,118.6364,0.2159,33,1],[71,80,118.8636,0.2159,33,1],[62,80,119.0909,0.2159,33,1],"
201 "[65,80,119.3182,0.2159,33,1],[64,80,119.5455,0.2159,33,1],[62,80,119.7727,0.2159,33,1],"
202 "[64,80,120.0,3.4545,34,1],[36,64,120.0,3.4545,34,5],[67,80,123.6364,3.4545,34,1],"
203 "[72,80,123.6364,3.4545,34,1],[48,64,123.6364,3.4545,34,5]]"
204 )
205
206 # ──────────────────────────────────────────────────────────────────────────────
207 # 21 MIDI Dimensions
208 # ──────────────────────────────────────────────────────────────────────────────
209 _DIMS_21: list[dict[str, str]] = [
210 {"id": "notes", "label": "Notes", "group": "core", "color": "#00d4ff", "desc": "note_on/note_off — the musical content itself"},
211 {"id": "pitch_bend", "label": "Pitch Bend", "group": "expr", "color": "#7c6cff", "desc": "pitchwheel — semitone-accurate pitch deviation"},
212 {"id": "channel_pressure", "label": "Ch. Pressure", "group": "expr", "color": "#9d8cff", "desc": "aftertouch — mono channel pressure"},
213 {"id": "poly_pressure", "label": "Poly Aftertouch","group": "expr", "color": "#b8a8ff", "desc": "polytouch — per-note polyphonic aftertouch"},
214 {"id": "cc_modulation", "label": "Modulation", "group": "cc", "color": "#ff6b9d", "desc": "CC 1 — modulation wheel depth"},
215 {"id": "cc_volume", "label": "Volume", "group": "cc", "color": "#ff8c42", "desc": "CC 7 — channel volume level"},
216 {"id": "cc_pan", "label": "Pan", "group": "cc", "color": "#ffd700", "desc": "CC 10 — stereo pan position"},
217 {"id": "cc_expression", "label": "Expression", "group": "cc", "color": "#00ff87", "desc": "CC 11 — expression controller"},
218 {"id": "cc_sustain", "label": "Sustain Pedal", "group": "cc", "color": "#00d4ff", "desc": "CC 64 — damper/sustain pedal"},
219 {"id": "cc_portamento", "label": "Portamento", "group": "cc", "color": "#66e0ff", "desc": "CC 65 — portamento on/off"},
220 {"id": "cc_sostenuto", "label": "Sostenuto", "group": "cc", "color": "#99eaff", "desc": "CC 66 — sostenuto pedal"},
221 {"id": "cc_soft_pedal", "label": "Soft Pedal", "group": "cc", "color": "#aaeeff", "desc": "CC 67 — soft pedal (una corda)"},
222 {"id": "cc_reverb", "label": "Reverb Send", "group": "fx", "color": "#e879f9", "desc": "CC 91 — reverb send level"},
223 {"id": "cc_chorus", "label": "Chorus Send", "group": "fx", "color": "#c084fc", "desc": "CC 93 — chorus send level"},
224 {"id": "cc_other", "label": "Other CC", "group": "fx", "color": "#a78bfa", "desc": "All remaining CC numbers"},
225 {"id": "program_change", "label": "Program/Patch", "group": "meta", "color": "#fb923c", "desc": "program_change — instrument/patch select"},
226 {"id": "tempo_map", "label": "Tempo Map", "group": "meta", "color": "#f87171", "desc": "set_tempo meta events (non-independent)"},
227 {"id": "time_signatures", "label": "Time Signatures","group": "meta", "color": "#fbbf24", "desc": "time_signature meta events (non-independent)"},
228 {"id": "key_signatures", "label": "Key Signatures", "group": "meta", "color": "#a3e635", "desc": "key_signature meta events"},
229 {"id": "markers", "label": "Markers", "group": "meta", "color": "#34d399", "desc": "marker, cue, text, lyrics, copyright events"},
230 {"id": "track_structure", "label": "Track Structure","group": "meta", "color": "#94a3b8", "desc": "track_name, sysex, unknown meta (non-independent)"},
231 ]
232
233 # ──────────────────────────────────────────────────────────────────────────────
234 # Commit graph definition
235 # dagX: column (0=lower-register, 1=main, 2=upper-register)
236 # dagY: row (0=top)
237 # filter: {minM, maxM, voices[]} or null for no notes
238 # dims: dimension IDs active/modified in this commit
239 # dimAct: activity level per dimension (0-4)
240 # ──────────────────────────────────────────────────────────────────────────────
241 _COMMITS: list[dict[str, object]] = [
242 {
243 "id": "c0", "sha": "0000000", "branch": "main",
244 "label": "muse init",
245 "message": "Initial commit — muse init --domain midi",
246 "command": "muse init --domain midi",
247 "output": "✓ Initialized Muse repository\n Domain : midi\n Dimensions: 21\n Location : .muse/\n Tracking : muse-work/",
248 "parents": [],
249 "dagX": 1, "dagY": 0,
250 "filter": None,
251 "newVoices": [],
252 "newMeasures": [],
253 "dims": [],
254 "dimAct": {},
255 "stats": "0 notes · 0 dimensions",
256 "noteCount": 0,
257 },
258 {
259 "id": "c1", "sha": "a1b2c3d", "branch": "feat/lower-register",
260 "label": "bass + inner\nbars 1–12",
261 "message": "feat: bass and inner voices, bars 1–12",
262 "command": 'muse commit -m "feat: bass and inner voices, bars 1–12"',
263 "output": "✓ [feat/lower-register a1b2c3d]\n 48 notes added\n Dimensions modified: notes, tempo_map,\n time_signatures, track_structure\n Key detected: C major",
264 "parents": ["c0"],
265 "dagX": 0, "dagY": 1,
266 "filter": {"minM": 1, "maxM": 12, "voices": [5, 6]},
267 "newVoices": [5, 6],
268 "newMeasures": [1, 12],
269 "dims": ["notes", "tempo_map", "time_signatures", "track_structure"],
270 "dimAct": {"notes": 3, "tempo_map": 2, "time_signatures": 2, "track_structure": 1},
271 "stats": "+48 notes · 4 dimensions",
272 "noteCount": 48,
273 },
274 {
275 "id": "c2", "sha": "b3c4d5e", "branch": "feat/lower-register",
276 "label": "lower voices\nbars 13–24",
277 "message": "feat: bass and inner voices extended, bars 13–24",
278 "command": 'muse commit -m "feat: bass and inner voices extended, bars 13–24"',
279 "output": "✓ [feat/lower-register b3c4d5e]\n 40 notes added\n Dimensions modified: notes, cc_sustain,\n cc_volume\n Chord progression: Fm → C7 → Dm",
280 "parents": ["c1"],
281 "dagX": 0, "dagY": 2,
282 "filter": {"minM": 1, "maxM": 24, "voices": [5, 6]},
283 "newVoices": [5, 6],
284 "newMeasures": [13, 24],
285 "dims": ["notes", "cc_sustain", "cc_volume"],
286 "dimAct": {"notes": 3, "cc_sustain": 2, "cc_volume": 1},
287 "stats": "+40 notes · 3 dimensions",
288 "noteCount": 88,
289 },
290 {
291 "id": "c3", "sha": "c4d5e6f", "branch": "feat/lower-register",
292 "label": "lower voices\nbars 25–34 + FX",
293 "message": "feat: complete lower register + reverb + expression",
294 "command": 'muse commit -m "feat: complete lower register + reverb + expression"',
295 "output": "✓ [feat/lower-register c4d5e6f]\n 42 notes added\n Dimensions modified: notes, cc_sustain,\n cc_reverb, cc_expression, markers\n Bass descends to C2 (MIDI 36) — full range",
296 "parents": ["c2"],
297 "dagX": 0, "dagY": 3,
298 "filter": {"minM": 1, "maxM": 34, "voices": [5, 6]},
299 "newVoices": [5, 6],
300 "newMeasures": [25, 34],
301 "dims": ["notes", "cc_sustain", "cc_reverb", "cc_expression", "markers"],
302 "dimAct": {"notes": 3, "cc_sustain": 2, "cc_reverb": 2, "cc_expression": 3, "markers": 1},
303 "stats": "+42 notes · 5 dimensions",
304 "noteCount": 130,
305 },
306 {
307 "id": "c4", "sha": "d5e6f7a", "branch": "feat/upper-register",
308 "label": "treble arpeggios\nbars 1–12",
309 "message": "feat: treble arpeggios, bars 1–12",
310 "command": 'muse commit -m "feat: treble arpeggios, bars 1–12"',
311 "output": "✓ [feat/upper-register d5e6f7a]\n 144 notes added\n Dimensions modified: notes, cc_volume,\n program_change\n Voice 1: soprano arpeggios reach A5 (MIDI 81)",
312 "parents": ["c0"],
313 "dagX": 2, "dagY": 1,
314 "filter": {"minM": 1, "maxM": 12, "voices": [1]},
315 "newVoices": [1],
316 "newMeasures": [1, 12],
317 "dims": ["notes", "cc_volume", "program_change"],
318 "dimAct": {"notes": 4, "cc_volume": 2, "program_change": 1},
319 "stats": "+144 notes · 3 dimensions",
320 "noteCount": 144,
321 },
322 {
323 "id": "c5", "sha": "e6f7a8b", "branch": "feat/upper-register",
324 "label": "arpeggios\nbars 13–24",
325 "message": "feat: treble arpeggios, bars 13–24 + modulation",
326 "command": 'muse commit -m "feat: treble arpeggios, bars 13–24 + modulation"',
327 "output": "✓ [feat/upper-register e6f7a8b]\n 120 notes added\n Dimensions modified: notes, cc_modulation,\n cc_expression, key_signatures\n Development section — chromatic tensions",
328 "parents": ["c4"],
329 "dagX": 2, "dagY": 2,
330 "filter": {"minM": 1, "maxM": 24, "voices": [1]},
331 "newVoices": [1],
332 "newMeasures": [13, 24],
333 "dims": ["notes", "cc_modulation", "cc_expression", "key_signatures"],
334 "dimAct": {"notes": 4, "cc_modulation": 2, "cc_expression": 3, "key_signatures": 1},
335 "stats": "+120 notes · 4 dimensions",
336 "noteCount": 264,
337 },
338 {
339 "id": "c6", "sha": "f7a8b9c", "branch": "feat/upper-register",
340 "label": "coda\nbars 25–34",
341 "message": "feat: coda arpeggios bars 25–34 + final dynamics",
342 "command": 'muse commit -m "feat: coda arpeggios bars 25–34 + final dynamics"',
343 "output": "✓ [feat/upper-register f7a8b9c]\n 139 notes added\n Dimensions modified: notes, cc_expression,\n cc_soft_pedal, markers\n Coda: bars 33–34 hold final C major chord",
344 "parents": ["c5"],
345 "dagX": 2, "dagY": 3,
346 "filter": {"minM": 1, "maxM": 34, "voices": [1]},
347 "newVoices": [1],
348 "newMeasures": [25, 34],
349 "dims": ["notes", "cc_expression", "cc_soft_pedal", "markers"],
350 "dimAct": {"notes": 4, "cc_expression": 3, "cc_soft_pedal": 2, "markers": 2},
351 "stats": "+139 notes · 4 dimensions",
352 "noteCount": 403,
353 },
354 {
355 "id": "c7", "sha": "9a0b1c2", "branch": "main",
356 "label": "muse merge\nPrelude complete ✓",
357 "message": "merge: unite lower and upper registers — Prelude BWV 846 complete",
358 "command": "muse merge feat/lower-register feat/upper-register",
359 "output": "✓ [main 9a0b1c2] merge: Prelude BWV 846 complete\n Auto-merged: notes (no pitch conflicts —\n registers non-overlapping)\n Merged dimensions: 10 / 21\n 533 notes · 2:07 duration · Key: C major",
360 "parents": ["c3", "c6"],
361 "dagX": 1, "dagY": 4,
362 "filter": {"minM": 1, "maxM": 34, "voices": [1, 5, 6]},
363 "newVoices": [1, 5, 6],
364 "newMeasures": [1, 34],
365 "dims": [
366 "notes", "tempo_map", "time_signatures", "track_structure",
367 "cc_sustain", "cc_volume", "cc_expression", "cc_reverb",
368 "cc_modulation", "cc_soft_pedal", "markers", "program_change",
369 "key_signatures",
370 ],
371 "dimAct": {
372 "notes": 4, "tempo_map": 2, "time_signatures": 2, "track_structure": 1,
373 "cc_sustain": 2, "cc_volume": 2, "cc_expression": 3, "cc_reverb": 2,
374 "cc_modulation": 2, "cc_soft_pedal": 2, "markers": 2, "program_change": 1,
375 "key_signatures": 1,
376 },
377 "stats": "533 notes · 13 dimensions · 2:07",
378 "noteCount": 533,
379 },
380 ]
381
382
383 def render_midi_demo() -> str:
384 """Generate the complete self-contained MIDI demo HTML page."""
385 notes_json = _BACH_NOTES_JSON
386 commits_json = json.dumps(_COMMITS, separators=(",", ":"))
387 dims_json = json.dumps(_DIMS_21, separators=(",", ":"))
388
389 html = _HTML_TEMPLATE
390 html = html.replace("__NOTES_JSON__", notes_json)
391 html = html.replace("__COMMITS_JSON__", commits_json)
392 html = html.replace("__DIMS_JSON__", dims_json)
393 return html
394
395
396 # ──────────────────────────────────────────────────────────────────────────────
397 # HTML template (no Python f-strings — JS braces are literal)
398 # ──────────────────────────────────────────────────────────────────────────────
399 _HTML_TEMPLATE = """<!DOCTYPE html>
400 <html lang="en">
401 <head>
402 <meta charset="UTF-8">
403 <meta name="viewport" content="width=device-width, initial-scale=1.0">
404 <title>Bach BWV 846 × Muse VCS — 21-Dimensional MIDI Demo</title>
405 <link rel="preconnect" href="https://fonts.googleapis.com">
406 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
407 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
408 <script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
409 <script src="https://cdn.jsdelivr.net/npm/tone@14.7.77/build/Tone.js"></script>
410 <style>
411 :root {
412 --bg: #07090f;
413 --bg2: #0c0f1a;
414 --bg3: #111627;
415 --surface: rgba(255,255,255,0.035);
416 --surface2: rgba(255,255,255,0.06);
417 --border: rgba(255,255,255,0.07);
418 --border2: rgba(255,255,255,0.12);
419 --text: #e2e8f0;
420 --muted: #64748b;
421 --dim: #475569;
422 --cyan: #00d4ff;
423 --purple: #7c6cff;
424 --gold: #ffd700;
425 --green: #00ff87;
426 --pink: #ff6b9d;
427 --orange: #ff8c42;
428 --font: 'Inter', sans-serif;
429 --mono: 'JetBrains Mono', monospace;
430 }
431 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
432 html { scroll-behavior: smooth; }
433 body {
434 background: var(--bg);
435 color: var(--text);
436 font-family: var(--font);
437 font-size: 14px;
438 line-height: 1.6;
439 min-height: 100vh;
440 overflow-x: hidden;
441 }
442
443 /* ── Particles canvas ── */
444 #particles-canvas {
445 position: fixed; top: 0; left: 0;
446 width: 100%; height: 100%;
447 pointer-events: none; z-index: 0;
448 opacity: 0.4;
449 }
450
451 /* ── NAV ── */
452 .nav {
453 position: sticky; top: 0; z-index: 100;
454 display: flex; align-items: center; justify-content: space-between;
455 padding: 12px 28px;
456 background: rgba(7,9,15,0.85);
457 backdrop-filter: blur(20px);
458 border-bottom: 1px solid var(--border);
459 }
460 .nav-brand {
461 display: flex; align-items: center; gap: 10px;
462 font-family: var(--mono); font-size: 13px;
463 color: var(--cyan); text-decoration: none;
464 letter-spacing: 0.02em;
465 }
466 .nav-brand .sep { color: var(--muted); }
467 .nav-links { display: flex; gap: 20px; }
468 .nav-links a {
469 color: var(--muted); text-decoration: none; font-size: 12px;
470 letter-spacing: 0.05em; text-transform: uppercase;
471 transition: color 0.2s;
472 }
473 .nav-links a:hover { color: var(--text); }
474 .badge {
475 background: rgba(0,212,255,0.1); color: var(--cyan);
476 border: 1px solid rgba(0,212,255,0.2);
477 padding: 2px 8px; border-radius: 20px;
478 font-size: 10px; font-family: var(--mono);
479 letter-spacing: 0.05em;
480 }
481
482 /* ── HERO ── */
483 .hero {
484 position: relative; z-index: 1;
485 text-align: center;
486 padding: 60px 28px 44px;
487 background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(124,108,255,0.12) 0%, transparent 70%);
488 }
489 .hero-eyebrow {
490 font-family: var(--mono); font-size: 11px; letter-spacing: 0.15em;
491 text-transform: uppercase; color: var(--cyan); margin-bottom: 14px;
492 }
493 .hero h1 {
494 font-size: clamp(28px, 5vw, 52px);
495 font-weight: 300; letter-spacing: -0.02em;
496 line-height: 1.15; margin-bottom: 16px;
497 }
498 .hero h1 em { font-style: normal; color: var(--cyan); }
499 .hero h1 strong { font-weight: 600; }
500 .hero-sub {
501 color: var(--muted); font-size: 15px; max-width: 580px;
502 margin: 0 auto 32px;
503 }
504 .hero-pills {
505 display: flex; flex-wrap: wrap; justify-content: center;
506 gap: 8px; margin-bottom: 36px;
507 }
508 .pill {
509 padding: 5px 12px; border-radius: 20px;
510 font-size: 11px; font-family: var(--mono);
511 border: 1px solid var(--border2);
512 background: var(--surface);
513 color: var(--muted);
514 }
515 .pill.cyan { border-color: rgba(0,212,255,0.3); color: var(--cyan); background: rgba(0,212,255,0.07); }
516 .pill.purple{ border-color: rgba(124,108,255,0.3); color: var(--purple); background: rgba(124,108,255,0.07); }
517 .pill.gold { border-color: rgba(255,215,0,0.3); color: var(--gold); background: rgba(255,215,0,0.07); }
518 .pill.green { border-color: rgba(0,255,135,0.3); color: var(--green); background: rgba(0,255,135,0.07); }
519 .hero-actions { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; }
520 .btn {
521 display: inline-flex; align-items: center; gap: 7px;
522 padding: 9px 20px; border-radius: 8px;
523 font-size: 13px; font-weight: 500; cursor: pointer;
524 border: none; transition: all 0.2s; text-decoration: none;
525 font-family: var(--font);
526 }
527 .btn-primary {
528 background: var(--cyan); color: #07090f;
529 }
530 .btn-primary:hover { background: #33ddff; transform: translateY(-1px); }
531 .btn-ghost {
532 background: var(--surface2); color: var(--text);
533 border: 1px solid var(--border2);
534 }
535 .btn-ghost:hover { background: rgba(255,255,255,0.09); }
536 .btn-sm { padding: 6px 14px; font-size: 12px; }
537 .btn-icon { font-size: 15px; }
538 .btn:disabled { opacity: 0.4; cursor: not-allowed; }
539
540 /* ── DEMO WRAPPER ── */
541 .demo-wrapper {
542 position: relative; z-index: 1;
543 max-width: 1400px; margin: 0 auto;
544 padding: 0 16px 40px;
545 }
546
547 /* ── SECTION HEADING ── */
548 .section-title {
549 font-size: 10px; font-family: var(--mono);
550 letter-spacing: 0.12em; text-transform: uppercase;
551 color: var(--muted); margin-bottom: 12px;
552 display: flex; align-items: center; gap: 8px;
553 }
554 .section-title::before {
555 content: ''; display: block;
556 width: 16px; height: 1px;
557 background: var(--border2);
558 }
559
560 /* ── MAIN DEMO GRID ── */
561 .main-demo {
562 display: grid;
563 grid-template-columns: 220px 1fr 220px;
564 grid-template-rows: auto;
565 gap: 12px;
566 margin-bottom: 12px;
567 }
568
569 /* ── PANEL ── */
570 .panel {
571 background: var(--bg2);
572 border: 1px solid var(--border);
573 border-radius: 12px;
574 overflow: hidden;
575 }
576 .panel-header {
577 padding: 10px 14px;
578 border-bottom: 1px solid var(--border);
579 display: flex; align-items: center; justify-content: space-between;
580 background: rgba(255,255,255,0.015);
581 }
582 .panel-title {
583 font-size: 10px; font-family: var(--mono);
584 letter-spacing: 0.1em; text-transform: uppercase;
585 color: var(--muted);
586 }
587 .panel-body { padding: 0; }
588
589 /* ── DAG PANEL ── */
590 #dag-container {
591 height: 420px;
592 overflow: hidden;
593 padding: 8px 0;
594 }
595 #dag-container svg { width: 100%; height: 100%; }
596
597 /* ── PIANO ROLL PANEL ── */
598 #piano-roll-wrap {
599 height: 420px;
600 overflow-x: auto;
601 overflow-y: hidden;
602 position: relative;
603 cursor: crosshair;
604 background: #080c16;
605 }
606 #piano-roll-wrap::-webkit-scrollbar { height: 4px; }
607 #piano-roll-wrap::-webkit-scrollbar-track { background: var(--bg2); }
608 #piano-roll-wrap::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
609 #piano-roll-svg { display: block; }
610 .note-rect { transition: opacity 0.3s; }
611 .note-new { filter: brightness(1.3); }
612
613 /* ── DIM PANEL ── */
614 .dim-list {
615 height: 420px;
616 overflow-y: auto;
617 padding: 4px 0;
618 }
619 .dim-list::-webkit-scrollbar { width: 4px; }
620 .dim-list::-webkit-scrollbar-thumb { background: var(--border2); }
621 .dim-row {
622 display: flex; align-items: center;
623 padding: 5px 12px; gap: 8px;
624 transition: background 0.2s; cursor: default;
625 }
626 .dim-row:hover { background: var(--surface); }
627 .dim-dot {
628 width: 8px; height: 8px; border-radius: 50%;
629 flex-shrink: 0;
630 background: var(--dim);
631 transition: background 0.3s, box-shadow 0.3s;
632 }
633 .dim-row.active .dim-dot {
634 box-shadow: 0 0 8px currentColor;
635 }
636 .dim-name {
637 font-family: var(--mono); font-size: 10px;
638 color: var(--muted); flex: 1;
639 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
640 transition: color 0.3s;
641 }
642 .dim-row.active .dim-name { color: var(--text); }
643 .dim-bar-wrap {
644 width: 48px; height: 4px;
645 background: rgba(255,255,255,0.05);
646 border-radius: 2px; overflow: hidden;
647 }
648 .dim-bar {
649 height: 100%; border-radius: 2px;
650 width: 0; transition: width 0.4s ease, background 0.3s;
651 }
652 .dim-group-label {
653 padding: 8px 12px 2px;
654 font-size: 9px; font-family: var(--mono);
655 text-transform: uppercase; letter-spacing: 0.12em;
656 color: var(--dim);
657 }
658
659 /* ── CONTROLS ── */
660 .controls-bar {
661 background: var(--bg2);
662 border: 1px solid var(--border);
663 border-radius: 12px;
664 padding: 12px 20px;
665 display: flex; align-items: center; gap: 16px;
666 margin-bottom: 12px;
667 flex-wrap: wrap;
668 }
669 .ctrl-group { display: flex; align-items: center; gap: 8px; }
670 .ctrl-btn {
671 width: 36px; height: 36px; border-radius: 8px;
672 background: var(--surface); border: 1px solid var(--border2);
673 color: var(--text); font-size: 14px; cursor: pointer;
674 display: flex; align-items: center; justify-content: center;
675 transition: all 0.15s;
676 }
677 .ctrl-btn:hover { background: var(--surface2); }
678 .ctrl-btn.active { background: var(--cyan); color: #07090f; border-color: var(--cyan); }
679 .ctrl-btn:disabled { opacity: 0.3; cursor: not-allowed; }
680 .ctrl-play {
681 width: 44px; height: 44px; border-radius: 50%;
682 background: var(--cyan); border: none; color: #07090f;
683 font-size: 18px; cursor: pointer;
684 display: flex; align-items: center; justify-content: center;
685 box-shadow: 0 0 20px rgba(0,212,255,0.3);
686 transition: all 0.15s;
687 }
688 .ctrl-play:hover { background: #33ddff; transform: scale(1.05); }
689 .ctrl-play.playing { background: var(--pink); box-shadow: 0 0 20px rgba(255,107,157,0.3); }
690 .ctrl-sep { width: 1px; height: 28px; background: var(--border); }
691 .ctrl-timeline {
692 flex: 1; min-width: 120px;
693 display: flex; align-items: center; gap: 10px;
694 }
695 .ctrl-time {
696 font-family: var(--mono); font-size: 11px; color: var(--muted);
697 white-space: nowrap;
698 }
699 .progress-track {
700 flex: 1; height: 4px; background: var(--surface2);
701 border-radius: 2px; cursor: pointer; position: relative;
702 }
703 .progress-fill {
704 height: 100%; background: var(--cyan); border-radius: 2px;
705 width: 0; transition: width 0.1s linear;
706 }
707 .audio-status {
708 display: flex; align-items: center; gap: 6px;
709 font-size: 11px; color: var(--muted); font-family: var(--mono);
710 }
711 .audio-dot {
712 width: 8px; height: 8px; border-radius: 50%;
713 background: var(--dim); transition: background 0.3s;
714 }
715 .audio-dot.ready { background: var(--green); box-shadow: 0 0 8px var(--green); }
716 .audio-dot.loading { background: var(--gold); animation: pulse 1s infinite; }
717 .commit-info {
718 flex: 1; min-width: 180px;
719 }
720 .commit-sha {
721 font-family: var(--mono); font-size: 10px; color: var(--cyan);
722 }
723 .commit-msg {
724 font-size: 11px; color: var(--muted);
725 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
726 max-width: 260px;
727 }
728
729 /* ── COMMAND LOG ── */
730 .cmd-log-panel {
731 background: #060810;
732 border: 1px solid var(--border);
733 border-radius: 12px;
734 margin-bottom: 12px; overflow: hidden;
735 }
736 .cmd-log-header {
737 padding: 8px 16px;
738 border-bottom: 1px solid var(--border);
739 display: flex; align-items: center; gap: 8px;
740 background: rgba(255,255,255,0.02);
741 }
742 .terminal-dots { display: flex; gap: 5px; }
743 .terminal-dots span {
744 width: 10px; height: 10px; border-radius: 50%;
745 }
746 .dot-red { background: #ff5f57; }
747 .dot-yellow{ background: #febc2e; }
748 .dot-green { background: #28c840; }
749 .cmd-log-title {
750 font-family: var(--mono); font-size: 10px;
751 color: var(--muted); letter-spacing: 0.08em;
752 flex: 1; text-align: center;
753 }
754 .cmd-log-body {
755 padding: 14px 20px;
756 font-family: var(--mono); font-size: 12px;
757 min-height: 110px; max-height: 160px;
758 overflow-y: auto;
759 }
760 .cmd-log-body::-webkit-scrollbar { width: 4px; }
761 .cmd-log-body::-webkit-scrollbar-thumb { background: var(--border2); }
762 .log-line { line-height: 1.7; }
763 .log-prompt { color: var(--green); }
764 .log-cmd { color: var(--text); }
765 .log-out { color: var(--muted); white-space: pre; }
766 .log-cursor {
767 display: inline-block; width: 8px; height: 13px;
768 background: var(--cyan); vertical-align: middle;
769 animation: blink 1s step-end infinite;
770 }
771
772 /* ── HEATMAP ── */
773 .heatmap-panel {
774 background: var(--bg2);
775 border: 1px solid var(--border);
776 border-radius: 12px;
777 margin-bottom: 40px; overflow: hidden;
778 }
779 .heatmap-body {
780 padding: 16px 20px;
781 overflow-x: auto;
782 }
783 #heatmap-svg { display: block; }
784
785 /* ── CLI REFERENCE ── */
786 .cli-section {
787 max-width: 1400px; margin: 0 auto;
788 padding: 0 16px 80px;
789 }
790 .cli-grid {
791 display: grid;
792 grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
793 gap: 12px; margin-top: 20px;
794 }
795 .cmd-card {
796 background: var(--bg2);
797 border: 1px solid var(--border);
798 border-radius: 12px; padding: 16px 18px;
799 transition: border-color 0.2s;
800 }
801 .cmd-card:hover { border-color: var(--border2); }
802 .cmd-name {
803 font-family: var(--mono); font-size: 13px;
804 color: var(--cyan); margin-bottom: 4px;
805 }
806 .cmd-desc {
807 font-size: 12px; color: var(--muted); margin-bottom: 10px;
808 }
809 .cmd-flags { display: flex; flex-direction: column; gap: 4px; }
810 .cmd-flag {
811 display: flex; gap: 8px; align-items: baseline;
812 }
813 .flag-name {
814 font-family: var(--mono); font-size: 10px;
815 color: var(--purple); white-space: nowrap;
816 }
817 .flag-desc {
818 font-size: 11px; color: var(--dim);
819 }
820 .cmd-return {
821 margin-top: 8px; padding-top: 8px;
822 border-top: 1px solid var(--border);
823 font-size: 11px; color: var(--dim);
824 }
825 .cmd-return span {
826 font-family: var(--mono); color: var(--gold);
827 }
828
829 /* ── FOOTER ── */
830 footer {
831 border-top: 1px solid var(--border);
832 padding: 24px 28px;
833 display: flex; justify-content: space-between; align-items: center;
834 flex-wrap: wrap; gap: 12px;
835 font-size: 11px; color: var(--dim);
836 }
837 footer a { color: var(--muted); text-decoration: none; }
838 footer a:hover { color: var(--text); }
839
840 /* ── ANIMATIONS ── */
841 @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
842 @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
843 @keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
844 .fade-in { animation: fadeIn 0.4s ease forwards; }
845
846 /* ── RESPONSIVE ── */
847 @media(max-width: 900px) {
848 .main-demo {
849 grid-template-columns: 1fr;
850 }
851 #dag-container, .dim-list { height: 200px; }
852 }
853 </style>
854 </head>
855 <body>
856
857 <canvas id="particles-canvas"></canvas>
858
859 <!-- ── NAV ── -->
860 <nav class="nav">
861 <a class="nav-brand" href="index.html">
862 <span>muse</span><span class="sep">/</span><span>midi-demo</span>
863 </a>
864 <div class="nav-links">
865 <a href="index.html">Registry</a>
866 <a href="demo.html">VCS Demo</a>
867 <a href="https://github.com/cgcardona/muse" target="_blank">GitHub</a>
868 </div>
869 <span class="badge">v0.1.2</span>
870 </nav>
871
872 <!-- ── HERO ── -->
873 <section class="hero">
874 <div class="hero-eyebrow">Muse VCS · MIDI Domain · 21-Dimensional Version Control</div>
875 <h1>
876 <em>Bach</em> · <strong>Prelude No. 1 in C Major</strong><br>
877 BWV 846 · Well-Tempered Clavier
878 </h1>
879 <p class="hero-sub">
880 Watch Bach's Prelude built commit-by-commit across two parallel branches —
881 then merged automatically using Muse's 21-dimensional MIDI diff engine.
882 533 authentic notes. Real piano audio. Zero conflicts.
883 </p>
884 <div class="hero-pills">
885 <span class="pill cyan">533 notes · 34 bars</span>
886 <span class="pill purple">21 MIDI dimensions</span>
887 <span class="pill gold">2 branches · 1 merge</span>
888 <span class="pill green">Salamander Grand Piano</span>
889 <span class="pill">Bach (1685–1750) · Public domain</span>
890 <span class="pill">music21 corpus</span>
891 </div>
892 <div class="hero-actions">
893 <button class="btn btn-primary" id="btn-init-audio">
894 <span class="btn-icon">🎹</span> Load Piano &amp; Begin
895 </button>
896 <a class="btn btn-ghost" href="#cli-reference">
897 <span class="btn-icon">📖</span> CLI Reference
898 </a>
899 <a class="btn btn-ghost" href="index.html">
900 <span class="btn-icon">←</span> Landing Page
901 </a>
902 </div>
903 </section>
904
905 <!-- ── MAIN DEMO ── -->
906 <div class="demo-wrapper">
907
908 <!-- info bar -->
909 <div class="section-title" style="margin-bottom:14px">
910 Interactive Demo — click any commit to hear and see that state
911 </div>
912
913 <!-- 3-column grid -->
914 <div class="main-demo">
915
916 <!-- DAG -->
917 <div class="panel">
918 <div class="panel-header">
919 <span class="panel-title">Commit DAG</span>
920 <span class="badge" style="font-size:9px" id="dag-branch-label">main</span>
921 </div>
922 <div id="dag-container"></div>
923 </div>
924
925 <!-- Piano Roll -->
926 <div class="panel">
927 <div class="panel-header">
928 <span class="panel-title">Piano Roll — 4 octaves (C2 → C6)</span>
929 <div style="display:flex;gap:8px;align-items:center">
930 <span class="pill cyan" style="font-size:9px">■ Treble arpeggios</span>
931 <span class="pill purple" style="font-size:9px">■ Bass</span>
932 <span class="pill gold" style="font-size:9px">■ Inner voice</span>
933 </div>
934 </div>
935 <div id="piano-roll-wrap">
936 <svg id="piano-roll-svg"></svg>
937 </div>
938 </div>
939
940 <!-- 21-Dim Panel -->
941 <div class="panel">
942 <div class="panel-header">
943 <span class="panel-title">21 MIDI Dimensions</span>
944 <span id="dim-active-count" style="font-family:var(--mono);font-size:10px;color:var(--muted)">0 active</span>
945 </div>
946 <div class="dim-list" id="dim-list"></div>
947 </div>
948
949 </div>
950
951 <!-- Controls -->
952 <div class="controls-bar">
953 <div class="ctrl-group">
954 <button class="ctrl-btn" id="btn-first" title="First commit">⏮</button>
955 <button class="ctrl-btn" id="btn-prev" title="Previous commit">◀</button>
956 <button class="ctrl-play" id="btn-play" title="Play / Pause">▶</button>
957 <button class="ctrl-btn" id="btn-next" title="Next commit">▶</button>
958 <button class="ctrl-btn" id="btn-last" title="Last commit">⏭</button>
959 </div>
960 <div class="ctrl-sep"></div>
961 <div class="ctrl-timeline">
962 <span class="ctrl-time" id="time-display">0:00</span>
963 <div class="progress-track" id="progress-track">
964 <div class="progress-fill" id="progress-fill"></div>
965 </div>
966 <span class="ctrl-time" id="time-total">2:07</span>
967 </div>
968 <div class="ctrl-sep"></div>
969 <div class="commit-info">
970 <div class="commit-sha" id="commit-sha-disp">0000000</div>
971 <div class="commit-msg" id="commit-msg-disp">Select a commit to begin</div>
972 </div>
973 <div class="ctrl-sep"></div>
974 <div class="audio-status">
975 <div class="audio-dot" id="audio-dot"></div>
976 <span id="audio-label">Click "Load Piano"</span>
977 </div>
978 </div>
979
980 <!-- Command Log -->
981 <div class="cmd-log-panel">
982 <div class="cmd-log-header">
983 <div class="terminal-dots">
984 <span class="dot-red"></span>
985 <span class="dot-yellow"></span>
986 <span class="dot-green"></span>
987 </div>
988 <div class="cmd-log-title">muse — MIDI repository</div>
989 </div>
990 <div class="cmd-log-body" id="cmd-log">
991 <div class="log-line">
992 <span class="log-prompt">$ </span>
993 <span class="log-cmd">muse status</span>
994 </div>
995 <div class="log-line log-out">On branch main · 0 notes · Select a commit ↑</div>
996 <div class="log-line"><span class="log-cursor"></span></div>
997 </div>
998 </div>
999
1000 <!-- Heatmap -->
1001 <div class="heatmap-panel">
1002 <div class="panel-header">
1003 <span class="panel-title">Dimension Activity Heatmap — Commits × Dimensions</span>
1004 <span style="font-size:10px;color:var(--muted);font-family:var(--mono)">
1005 darker = inactive · brighter = active
1006 </span>
1007 </div>
1008 <div class="heatmap-body">
1009 <svg id="heatmap-svg"></svg>
1010 </div>
1011 </div>
1012
1013 </div><!-- /demo-wrapper -->
1014
1015 <!-- ── CLI REFERENCE ── -->
1016 <div class="cli-section" id="cli-reference">
1017 <div class="section-title">MIDI-Domain Commands — muse CLI Reference</div>
1018 <p style="color:var(--muted);font-size:13px;margin-bottom:8px">
1019 All standard VCS commands (commit, log, branch, merge, diff, …) work on MIDI files.
1020 The commands below are MIDI-specific additions provided by MidiPlugin.
1021 </p>
1022 <div class="cli-grid" id="cli-grid"></div>
1023 </div>
1024
1025 <footer>
1026 <div>
1027 <strong style="color:var(--text)">Muse VCS</strong> · v0.1.2 ·
1028 Bach BWV 846 — public domain (Bach 1685–1750) ·
1029 Note data: <a href="https://github.com/cuthbertLab/music21" target="_blank">music21 corpus</a> (CC0)
1030 </div>
1031 <div>
1032 <a href="index.html">Landing Page</a> ·
1033 <a href="demo.html">VCS Demo</a> ·
1034 <a href="https://github.com/cgcardona/muse" target="_blank">GitHub</a>
1035 </div>
1036 </footer>
1037
1038 <script>
1039 // ═══════════════════════════════════════════════════════════════
1040 // DATA (injected by render_midi_demo.py)
1041 // ═══════════════════════════════════════════════════════════════
1042 // [pitch_midi, velocity, start_sec, duration_sec, measure, voice]
1043 const BACH_NOTES = __NOTES_JSON__;
1044 const COMMITS = __COMMITS_JSON__;
1045 const DIMS_21 = __DIMS_JSON__;
1046
1047 const TOTAL_DURATION = 127.09;
1048 const VOICE_COLOR = { 1: '#00d4ff', 5: '#7c6cff', 6: '#ffd700' };
1049 const PITCH_MIN = 36, PITCH_MAX = 84;
1050
1051 // ═══════════════════════════════════════════════════════════════
1052 // STATE
1053 // ═══════════════════════════════════════════════════════════════
1054 const state = {
1055 commitIdx: 0,
1056 isPlaying: false,
1057 audioReady: false,
1058 audioLoading: false,
1059 playheadSec: 0,
1060 playStartWallClock: 0,
1061 playStartAudioSec: 0,
1062 rafId: null,
1063 };
1064
1065 // ═══════════════════════════════════════════════════════════════
1066 // PARTICLES BACKGROUND
1067 // ═══════════════════════════════════════════════════════════════
1068 (function initParticles() {
1069 const canvas = document.getElementById('particles-canvas');
1070 const ctx = canvas.getContext('2d');
1071 let W, H, particles;
1072
1073 function resize() {
1074 W = canvas.width = window.innerWidth;
1075 H = canvas.height = window.innerHeight;
1076 }
1077
1078 function createParticles() {
1079 const count = Math.floor(W * H / 14000);
1080 particles = Array.from({ length: count }, () => ({
1081 x: Math.random() * W,
1082 y: Math.random() * H,
1083 r: Math.random() * 1.2 + 0.2,
1084 a: Math.random() * Math.PI * 2,
1085 speed: Math.random() * 0.15 + 0.03,
1086 opacity: Math.random() * 0.5 + 0.1,
1087 }));
1088 }
1089
1090 function draw() {
1091 ctx.clearRect(0, 0, W, H);
1092 for (const p of particles) {
1093 p.x += Math.cos(p.a) * p.speed;
1094 p.y += Math.sin(p.a) * p.speed;
1095 p.a += (Math.random() - 0.5) * 0.02;
1096 if (p.x < 0) p.x = W; if (p.x > W) p.x = 0;
1097 if (p.y < 0) p.y = H; if (p.y > H) p.y = 0;
1098 ctx.beginPath();
1099 ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
1100 ctx.fillStyle = `rgba(120,180,255,${p.opacity})`;
1101 ctx.fill();
1102 }
1103 requestAnimationFrame(draw);
1104 }
1105
1106 resize();
1107 createParticles();
1108 draw();
1109 window.addEventListener('resize', () => { resize(); createParticles(); });
1110 })();
1111
1112 // ═══════════════════════════════════════════════════════════════
1113 // HELPERS
1114 // ═══════════════════════════════════════════════════════════════
1115 function getCommit(idx) { return COMMITS[idx]; }
1116
1117 function getNotesForCommit(commit) {
1118 if (!commit.filter) return [];
1119 const f = commit.filter;
1120 return BACH_NOTES.filter(n =>
1121 n[4] >= f.minM && n[4] <= f.maxM &&
1122 f.voices.includes(n[5])
1123 );
1124 }
1125
1126 function isNewNote(note, commit) {
1127 if (!commit.filter || !commit.newMeasures.length) return false;
1128 return note[4] >= commit.newMeasures[0] &&
1129 note[4] <= commit.newMeasures[1] &&
1130 commit.newVoices.includes(note[5]);
1131 }
1132
1133 function fmtTime(s) {
1134 const m = Math.floor(s / 60), sec = Math.floor(s % 60);
1135 return `${m}:${String(sec).padStart(2,'0')}`;
1136 }
1137
1138 // ═══════════════════════════════════════════════════════════════
1139 // PIANO ROLL
1140 // ═══════════════════════════════════════════════════════════════
1141 const PR = (() => {
1142 const KEYBOARD_W = 44;
1143 const NOTE_H = 8;
1144 const PX_PER_SEC = 6.5;
1145 const TOTAL_W = Math.ceil(TOTAL_DURATION * PX_PER_SEC) + KEYBOARD_W + 20;
1146 const TOTAL_H = (PITCH_MAX - PITCH_MIN) * NOTE_H;
1147 const WHITE_NOTES = new Set([0,2,4,5,7,9,11]);
1148
1149 const svg = d3.select('#piano-roll-svg')
1150 .attr('width', TOTAL_W)
1151 .attr('height', TOTAL_H);
1152
1153 // Background
1154 svg.append('rect')
1155 .attr('width', TOTAL_W).attr('height', TOTAL_H)
1156 .attr('fill', '#08101c');
1157
1158 // Octave lines & background stripes
1159 for (let pitch = PITCH_MIN; pitch <= PITCH_MAX; pitch++) {
1160 const sem = pitch % 12;
1161 const y = TOTAL_H - (pitch - PITCH_MIN + 1) * NOTE_H;
1162 if (!WHITE_NOTES.has(sem)) {
1163 svg.append('rect')
1164 .attr('x', KEYBOARD_W).attr('y', y)
1165 .attr('width', TOTAL_W - KEYBOARD_W).attr('height', NOTE_H)
1166 .attr('fill', 'rgba(0,0,0,0.25)');
1167 }
1168 if (sem === 0) {
1169 svg.append('line')
1170 .attr('x1', KEYBOARD_W).attr('x2', TOTAL_W)
1171 .attr('y1', y).attr('y2', y)
1172 .attr('stroke', 'rgba(255,255,255,0.08)').attr('stroke-width', 1);
1173 }
1174 }
1175
1176 // Bar grid (every 2 bars for readability)
1177 const secPerBar = (60.0 / 66) * 4;
1178 for (let bar = 0; bar <= 34; bar += 2) {
1179 const x = KEYBOARD_W + bar * secPerBar * PX_PER_SEC;
1180 svg.append('line')
1181 .attr('x1', x).attr('x2', x)
1182 .attr('y1', 0).attr('y2', TOTAL_H)
1183 .attr('stroke', bar % 8 === 0 ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.03)')
1184 .attr('stroke-width', 1);
1185 if (bar % 4 === 0 && bar > 0) {
1186 svg.append('text')
1187 .attr('x', x + 2).attr('y', 9)
1188 .attr('font-size', 8).attr('fill', 'rgba(255,255,255,0.2)')
1189 .attr('font-family', 'JetBrains Mono, monospace')
1190 .text(`m${bar+1}`);
1191 }
1192 }
1193
1194 // Piano keyboard (left edge)
1195 for (let pitch = PITCH_MIN; pitch < PITCH_MAX; pitch++) {
1196 const sem = pitch % 12;
1197 const isWhite = WHITE_NOTES.has(sem);
1198 const y = TOTAL_H - (pitch - PITCH_MIN + 1) * NOTE_H;
1199 svg.append('rect')
1200 .attr('x', 0).attr('y', y + 0.5)
1201 .attr('width', isWhite ? KEYBOARD_W - 2 : KEYBOARD_W * 0.62)
1202 .attr('height', NOTE_H - 1)
1203 .attr('rx', 1)
1204 .attr('fill', isWhite ? 'rgba(230,235,245,0.88)' : '#1c1c2e');
1205 // C note label
1206 if (sem === 0) {
1207 const oct = Math.floor(pitch / 12) - 1;
1208 svg.append('text')
1209 .attr('x', KEYBOARD_W - 5).attr('y', y + NOTE_H - 1)
1210 .attr('font-size', 7).attr('fill', '#555')
1211 .attr('text-anchor', 'end')
1212 .attr('font-family', 'JetBrains Mono, monospace')
1213 .text(`C${oct}`);
1214 }
1215 }
1216
1217 // Notes group (rendered above keyboard)
1218 const notesG = svg.append('g').attr('transform', `translate(${KEYBOARD_W},0)`);
1219
1220 // Playhead
1221 const playhead = svg.append('line')
1222 .attr('class', 'playhead')
1223 .attr('x1', KEYBOARD_W).attr('x2', KEYBOARD_W)
1224 .attr('y1', 0).attr('y2', TOTAL_H)
1225 .attr('stroke', 'rgba(255,255,255,0.7)')
1226 .attr('stroke-width', 1.5)
1227 .attr('opacity', 0);
1228
1229 let currentCommit = null;
1230
1231 function update(commit) {
1232 currentCommit = commit;
1233 const notes = getNotesForCommit(commit);
1234
1235 notesG.selectAll('.note-rect')
1236 .data(notes, d => d[0] + '_' + d[2])
1237 .join(
1238 enter => enter.append('rect')
1239 .attr('class', 'note-rect')
1240 .attr('x', d => d[2] * PX_PER_SEC)
1241 .attr('y', d => TOTAL_H - (d[0] - PITCH_MIN + 1) * NOTE_H)
1242 .attr('width', d => Math.max(d[3] * PX_PER_SEC - 1, 2))
1243 .attr('height', NOTE_H - 1)
1244 .attr('rx', 1.5)
1245 .attr('fill', d => VOICE_COLOR[d[5]] || '#888')
1246 .attr('opacity', 0)
1247 .call(s => {
1248 s.transition().duration(350)
1249 .attr('opacity', d => isNewNote(d, commit) ? 0.95 : 0.55);
1250 }),
1251 update => update
1252 .transition().duration(250)
1253 .attr('opacity', d => isNewNote(d, commit) ? 0.95 : 0.55),
1254 exit => exit
1255 .transition().duration(200)
1256 .attr('opacity', 0)
1257 .remove()
1258 );
1259
1260 // Scroll piano roll to show new notes
1261 if (commit.newMeasures && commit.newMeasures.length) {
1262 const targetSec = (commit.newMeasures[0] - 1) * secPerBar;
1263 const scrollX = Math.max(0, targetSec * PX_PER_SEC - 40);
1264 document.getElementById('piano-roll-wrap').scrollTo({ left: scrollX, behavior: 'smooth' });
1265 }
1266 }
1267
1268 function setPlayhead(sec) {
1269 const x = KEYBOARD_W + sec * PX_PER_SEC;
1270 playhead.attr('x1', x).attr('x2', x).attr('opacity', sec > 0 ? 0.8 : 0);
1271 // Auto-scroll to follow playhead
1272 const wrap = document.getElementById('piano-roll-wrap');
1273 const wrapW = wrap.clientWidth;
1274 const relX = x - wrap.scrollLeft;
1275 if (relX > wrapW * 0.75) {
1276 wrap.scrollLeft = x - wrapW * 0.25;
1277 } else if (relX < wrapW * 0.1) {
1278 wrap.scrollLeft = Math.max(0, x - 60);
1279 }
1280 }
1281
1282 return { update, setPlayhead };
1283 })();
1284
1285 // ═══════════════════════════════════════════════════════════════
1286 // DAG
1287 // ═══════════════════════════════════════════════════════════════
1288 const DAG = (() => {
1289 const container = document.getElementById('dag-container');
1290 const W = container.clientWidth || 210;
1291 const H = 410;
1292 const NODE_R = 14;
1293 const ROWS = 5;
1294 const ROW_H = (H - 70) / ROWS;
1295
1296 // Col x positions
1297 const COL = [W * 0.18, W * 0.5, W * 0.82];
1298
1299 // Branch colors
1300 const BRANCH_COLOR = {
1301 'main': '#00ff87',
1302 'feat/lower-register': '#7c6cff',
1303 'feat/upper-register': '#00d4ff',
1304 };
1305
1306 const svg = d3.select('#dag-container').append('svg')
1307 .attr('width', W).attr('height', H);
1308
1309 // Gradient defs
1310 const defs = svg.append('defs');
1311 ['lower','upper','main'].forEach(name => {
1312 const g = defs.append('radialGradient')
1313 .attr('id', `glow-${name}`)
1314 .attr('cx','50%').attr('cy','50%').attr('r','50%');
1315 const col = name === 'main' ? '#00ff87' : name === 'lower' ? '#7c6cff' : '#00d4ff';
1316 g.append('stop').attr('offset','0%').attr('stop-color', col).attr('stop-opacity', 0.4);
1317 g.append('stop').attr('offset','100%').attr('stop-color', col).attr('stop-opacity', 0);
1318 });
1319
1320 // Position function
1321 function pos(c) {
1322 return { x: COL[c.dagX], y: 35 + c.dagY * ROW_H };
1323 }
1324
1325 // Branch label headers
1326 [
1327 {x: COL[0], label: 'feat/lower', color: '#7c6cff'},
1328 {x: COL[1], label: 'main', color: '#00ff87'},
1329 {x: COL[2], label: 'feat/upper', color: '#00d4ff'},
1330 ].forEach(({x, label, color}) => {
1331 svg.append('text')
1332 .attr('x', x).attr('y', 14)
1333 .attr('text-anchor', 'middle')
1334 .attr('font-size', 8)
1335 .attr('font-family', 'JetBrains Mono, monospace')
1336 .attr('fill', color)
1337 .attr('letter-spacing', 1)
1338 .text(label);
1339 });
1340
1341 // Draw edges
1342 COMMITS.forEach(c => {
1343 c.parents.forEach(pid => {
1344 const parent = COMMITS.find(p => p.id === pid);
1345 if (!parent) return;
1346 const p1 = pos(parent), p2 = pos(c);
1347 const sameCol = Math.abs(p1.x - p2.x) < 5;
1348 if (sameCol) {
1349 svg.append('line')
1350 .attr('x1', p1.x).attr('y1', p1.y)
1351 .attr('x2', p2.x).attr('y2', p2.y)
1352 .attr('stroke', BRANCH_COLOR[c.branch] || '#fff')
1353 .attr('stroke-width', 1.5)
1354 .attr('stroke-opacity', 0.25);
1355 } else {
1356 const my = (p1.y + p2.y) / 2;
1357 const path = `M${p2.x},${p2.y} C${p2.x},${my} ${p1.x},${my} ${p1.x},${p1.y}`;
1358 svg.append('path')
1359 .attr('d', path)
1360 .attr('fill', 'none')
1361 .attr('stroke', 'rgba(255,255,255,0.18)')
1362 .attr('stroke-width', 1.5)
1363 .attr('stroke-dasharray', '3,2');
1364 }
1365 });
1366 });
1367
1368 // Node groups
1369 const nodeGs = svg.selectAll('.dag-node')
1370 .data(COMMITS)
1371 .join('g')
1372 .attr('class', 'dag-node')
1373 .attr('transform', c => `translate(${pos(c).x},${pos(c).y})`)
1374 .attr('cursor', 'pointer')
1375 .on('click', (e, d) => selectCommit(COMMITS.indexOf(d)));
1376
1377 // Glow ring (shown when selected)
1378 nodeGs.append('circle')
1379 .attr('r', NODE_R + 8)
1380 .attr('class', 'node-glow')
1381 .attr('fill', d => {
1382 const n = d.branch === 'main' ? 'main' : d.branch.includes('lower') ? 'lower' : 'upper';
1383 return `url(#glow-${n})`;
1384 })
1385 .attr('opacity', 0);
1386
1387 // Selection ring
1388 nodeGs.append('circle')
1389 .attr('r', NODE_R + 4)
1390 .attr('class', 'node-ring')
1391 .attr('fill', 'none')
1392 .attr('stroke', d => BRANCH_COLOR[d.branch] || '#fff')
1393 .attr('stroke-width', 1.5)
1394 .attr('opacity', 0);
1395
1396 // Main circle
1397 nodeGs.append('circle')
1398 .attr('r', NODE_R)
1399 .attr('fill', d => {
1400 if (d.branch === 'main') return '#0d1f14';
1401 if (d.branch.includes('lower')) return '#12102a';
1402 return '#0a1c28';
1403 })
1404 .attr('stroke', d => BRANCH_COLOR[d.branch] || '#fff')
1405 .attr('stroke-width', 1.8);
1406
1407 // SHA label
1408 nodeGs.append('text')
1409 .attr('text-anchor', 'middle').attr('dy', '0.35em')
1410 .attr('font-size', 8).attr('fill', 'rgba(255,255,255,0.7)')
1411 .attr('font-family', 'JetBrains Mono, monospace')
1412 .text(d => d.sha.slice(0,5));
1413
1414 // Commit message (two lines, below node)
1415 nodeGs.each(function(d) {
1416 const g = d3.select(this);
1417 const lines = d.label.split('\\n');
1418 lines.forEach((line, i) => {
1419 g.append('text')
1420 .attr('text-anchor', 'middle')
1421 .attr('y', NODE_R + 10 + i * 11)
1422 .attr('font-size', 7.5)
1423 .attr('fill', 'rgba(255,255,255,0.38)')
1424 .attr('font-family', 'JetBrains Mono, monospace')
1425 .text(line);
1426 });
1427 });
1428
1429 function select(idx) {
1430 const commit = COMMITS[idx];
1431 svg.selectAll('.node-ring').attr('opacity', 0);
1432 svg.selectAll('.node-glow').attr('opacity', 0);
1433 svg.selectAll('.dag-node')
1434 .filter(d => d.id === commit.id)
1435 .select('.node-ring').attr('opacity', 1);
1436 svg.selectAll('.dag-node')
1437 .filter(d => d.id === commit.id)
1438 .select('.node-glow').attr('opacity', 1);
1439
1440 document.getElementById('dag-branch-label').textContent = commit.branch;
1441 }
1442
1443 return { select };
1444 })();
1445
1446 // ═══════════════════════════════════════════════════════════════
1447 // 21-DIMENSION PANEL
1448 // ═══════════════════════════════════════════════════════════════
1449 const DimPanel = (() => {
1450 const container = document.getElementById('dim-list');
1451
1452 // Group by category
1453 const groups = ['core','expr','cc','fx','meta'];
1454 const groupLabel = {core:'Core',expr:'Expression',cc:'Controllers (CC)',fx:'Effects',meta:'Meta / Structure'};
1455
1456 groups.forEach(grp => {
1457 const dims = DIMS_21.filter(d => d.group === grp);
1458 if (!dims.length) return;
1459
1460 const label = document.createElement('div');
1461 label.className = 'dim-group-label';
1462 label.textContent = groupLabel[grp];
1463 container.appendChild(label);
1464
1465 dims.forEach(dim => {
1466 const row = document.createElement('div');
1467 row.className = 'dim-row';
1468 row.id = `dim-row-${dim.id}`;
1469 row.title = dim.desc;
1470 row.innerHTML = `
1471 <div class="dim-dot" id="dim-dot-${dim.id}" style="background:var(--dim)"></div>
1472 <div class="dim-name">${dim.label}</div>
1473 <div class="dim-bar-wrap"><div class="dim-bar" id="dim-bar-${dim.id}"></div></div>
1474 `;
1475 container.appendChild(row);
1476 });
1477 });
1478
1479 function update(commit) {
1480 const act = commit.dimAct || {};
1481 let activeCount = 0;
1482
1483 DIMS_21.forEach(dim => {
1484 const level = act[dim.id] || 0;
1485 const row = document.getElementById(`dim-row-${dim.id}`);
1486 const dot = document.getElementById(`dim-dot-${dim.id}`);
1487 const bar = document.getElementById(`dim-bar-${dim.id}`);
1488 if (!row) return;
1489
1490 if (level > 0) {
1491 activeCount++;
1492 row.classList.add('active');
1493 dot.style.background = dim.color;
1494 dot.style.color = dim.color;
1495 dot.style.boxShadow = `0 0 6px ${dim.color}`;
1496 bar.style.background = dim.color;
1497 bar.style.width = `${Math.min(level * 25, 100)}%`;
1498 bar.style.boxShadow = `0 0 4px ${dim.color}`;
1499 } else {
1500 row.classList.remove('active');
1501 dot.style.background = '#2a3040';
1502 dot.style.boxShadow = 'none';
1503 bar.style.background = 'rgba(255,255,255,0.08)';
1504 bar.style.width = '4%';
1505 bar.style.boxShadow = 'none';
1506 }
1507 });
1508
1509 document.getElementById('dim-active-count').textContent = `${activeCount} active`;
1510 }
1511
1512 return { update };
1513 })();
1514
1515 // ═══════════════════════════════════════════════════════════════
1516 // COMMAND LOG
1517 // ═══════════════════════════════════════════════════════════════
1518 const CmdLog = (() => {
1519 const log = document.getElementById('cmd-log');
1520
1521 function show(commit) {
1522 log.innerHTML = '';
1523
1524 // Command line
1525 const cmdLine = document.createElement('div');
1526 cmdLine.className = 'log-line';
1527 cmdLine.innerHTML = `<span class="log-prompt">$ </span><span class="log-cmd"></span>`;
1528 log.appendChild(cmdLine);
1529
1530 // Output lines
1531 const outEl = document.createElement('div');
1532 outEl.className = 'log-line log-out';
1533 log.appendChild(outEl);
1534
1535 // Stats line
1536 const statsLine = document.createElement('div');
1537 statsLine.className = 'log-line';
1538 statsLine.innerHTML = `<span style="color:var(--muted);font-size:11px">[${commit.stats}]</span>`;
1539 log.appendChild(statsLine);
1540
1541 // Cursor
1542 const cursor = document.createElement('div');
1543 cursor.className = 'log-line';
1544 cursor.innerHTML = '<span class="log-cursor"></span>';
1545 log.appendChild(cursor);
1546
1547 // Typewriter effect for command
1548 const cmdSpan = cmdLine.querySelector('.log-cmd');
1549 let i = 0;
1550 const cmdText = commit.command;
1551 const timer = setInterval(() => {
1552 cmdSpan.textContent = cmdText.slice(0, ++i);
1553 if (i >= cmdText.length) {
1554 clearInterval(timer);
1555 // Show output after command types
1556 setTimeout(() => {
1557 outEl.textContent = commit.output;
1558 log.scrollTop = log.scrollHeight;
1559 }, 100);
1560 }
1561 }, 18);
1562 log.scrollTop = 0;
1563 }
1564
1565 return { show };
1566 })();
1567
1568 // ═══════════════════════════════════════════════════════════════
1569 // HEATMAP
1570 // ═══════════════════════════════════════════════════════════════
1571 (function buildHeatmap() {
1572 const container = document.getElementById('heatmap-svg');
1573 const CELL_W = 56, CELL_H = 18, LABEL_W = 120, TOP_H = 55;
1574 const SVG_W = LABEL_W + COMMITS.length * CELL_W + 20;
1575 const SVG_H = TOP_H + DIMS_21.length * CELL_H + 10;
1576
1577 const svg = d3.select('#heatmap-svg')
1578 .attr('width', SVG_W).attr('height', SVG_H);
1579
1580 // Column headers (commit shas)
1581 COMMITS.forEach((c, ci) => {
1582 const x = LABEL_W + ci * CELL_W + CELL_W / 2;
1583 const col = c.branch === 'main' ? '#00ff87' :
1584 c.branch.includes('lower') ? '#7c6cff' : '#00d4ff';
1585 svg.append('text')
1586 .attr('x', x).attr('y', TOP_H - 28)
1587 .attr('text-anchor', 'middle')
1588 .attr('font-size', 8).attr('fill', col)
1589 .attr('font-family', 'JetBrains Mono, monospace')
1590 .text(c.sha.slice(0,5));
1591
1592 // Branch dot
1593 svg.append('circle')
1594 .attr('cx', x).attr('cy', TOP_H - 16)
1595 .attr('r', 4)
1596 .attr('fill', col)
1597 .attr('opacity', 0.6);
1598
1599 // Vertical branch line
1600 svg.append('line')
1601 .attr('x1', x).attr('x2', x)
1602 .attr('y1', TOP_H - 10).attr('y2', TOP_H)
1603 .attr('stroke', col).attr('stroke-width', 1)
1604 .attr('stroke-opacity', 0.3);
1605 });
1606
1607 // Row labels + cells
1608 DIMS_21.forEach((dim, di) => {
1609 const y = TOP_H + di * CELL_H;
1610
1611 // Row background (alternating)
1612 svg.append('rect')
1613 .attr('x', 0).attr('y', y)
1614 .attr('width', SVG_W).attr('height', CELL_H)
1615 .attr('fill', di % 2 === 0 ? 'rgba(255,255,255,0.02)' : 'transparent');
1616
1617 // Dimension label
1618 svg.append('text')
1619 .attr('x', LABEL_W - 6).attr('y', y + CELL_H * 0.67)
1620 .attr('text-anchor', 'end')
1621 .attr('font-size', 9).attr('fill', 'rgba(255,255,255,0.4)')
1622 .attr('font-family', 'JetBrains Mono, monospace')
1623 .text(dim.label);
1624
1625 // Cells
1626 COMMITS.forEach((c, ci) => {
1627 const level = (c.dimAct && c.dimAct[dim.id]) || 0;
1628 const x = LABEL_W + ci * CELL_W;
1629 const alpha = level === 0 ? 0.04 : level === 1 ? 0.25 : level === 2 ? 0.5 : level === 3 ? 0.72 : 0.92;
1630
1631 const cell = svg.append('rect')
1632 .attr('x', x + 1).attr('y', y + 1)
1633 .attr('width', CELL_W - 2).attr('height', CELL_H - 2)
1634 .attr('rx', 2)
1635 .attr('fill', level > 0 ? dim.color : 'rgba(255,255,255,0.06)')
1636 .attr('opacity', alpha)
1637 .attr('cursor', 'pointer');
1638
1639 cell.on('mouseenter', function() {
1640 if (level > 0) {
1641 d3.select(this).attr('opacity', Math.min(alpha + 0.2, 1));
1642 d3.select(this).attr('stroke', dim.color).attr('stroke-width', 1);
1643 }
1644 }).on('mouseleave', function() {
1645 d3.select(this).attr('opacity', alpha).attr('stroke', 'none');
1646 });
1647
1648 // Level indicator
1649 if (level > 0) {
1650 svg.append('text')
1651 .attr('x', x + CELL_W / 2).attr('y', y + CELL_H * 0.68)
1652 .attr('text-anchor', 'middle')
1653 .attr('font-size', 7.5)
1654 .attr('fill', 'rgba(255,255,255,0.7)')
1655 .attr('font-family', 'JetBrains Mono, monospace')
1656 .text(level === 1 ? '·' : level === 2 ? '●' : level === 3 ? '★' : '★★');
1657 }
1658 });
1659 });
1660
1661 // Heatmap selected commit highlight column
1662 window._heatmapHighlight = function(idx) {
1663 svg.selectAll('.hm-col-hl').remove();
1664 const x = LABEL_W + idx * CELL_W;
1665 svg.insert('rect', ':first-child')
1666 .attr('class', 'hm-col-hl')
1667 .attr('x', x).attr('y', TOP_H - 2)
1668 .attr('width', CELL_W).attr('height', SVG_H - TOP_H + 2)
1669 .attr('fill', 'rgba(255,255,255,0.04)')
1670 .attr('stroke', 'rgba(255,255,255,0.1)')
1671 .attr('stroke-width', 1)
1672 .attr('rx', 2);
1673 };
1674 })();
1675
1676 // ═══════════════════════════════════════════════════════════════
1677 // AUDIO ENGINE (Tone.js + Salamander Grand Piano)
1678 // ═══════════════════════════════════════════════════════════════
1679 let piano = null;
1680 let reverb = null;
1681 let scheduledNoteIds = [];
1682
1683 async function initAudio() {
1684 if (state.audioLoading || state.audioReady) return;
1685 state.audioLoading = true;
1686
1687 const dot = document.getElementById('audio-dot');
1688 const lbl = document.getElementById('audio-label');
1689 dot.className = 'audio-dot loading';
1690 lbl.textContent = 'Loading piano…';
1691
1692 await Tone.start();
1693
1694 reverb = new Tone.Reverb({ decay: 2.5, wet: 0.28 }).toDestination();
1695
1696 piano = new Tone.Sampler({
1697 urls: {
1698 A0: 'A0.mp3', C1: 'C1.mp3', 'D#1': 'Ds1.mp3', 'F#1': 'Fs1.mp3',
1699 A1: 'A1.mp3', C2: 'C2.mp3', 'D#2': 'Ds2.mp3', 'F#2': 'Fs2.mp3',
1700 A2: 'A2.mp3', C3: 'C3.mp3', 'D#3': 'Ds3.mp3', 'F#3': 'Fs3.mp3',
1701 A3: 'A3.mp3', C4: 'C4.mp3', 'D#4': 'Ds4.mp3', 'F#4': 'Fs4.mp3',
1702 A4: 'A4.mp3', C5: 'C5.mp3', 'D#5': 'Ds5.mp3', 'F#5': 'Fs5.mp3',
1703 A5: 'A5.mp3', C6: 'C6.mp3', 'D#6': 'Ds6.mp3', 'F#6': 'Fs6.mp3',
1704 A6: 'A6.mp3', C7: 'C7.mp3', 'D#7': 'Ds7.mp3', 'F#7': 'Fs7.mp3',
1705 A7: 'A7.mp3', C8: 'C8.mp3',
1706 },
1707 release: 1.2,
1708 baseUrl: 'https://tonejs.github.io/audio/salamander/',
1709 onload: () => {
1710 state.audioReady = true;
1711 state.audioLoading = false;
1712 dot.className = 'audio-dot ready';
1713 lbl.textContent = 'Piano ready';
1714 document.getElementById('btn-init-audio').textContent = '🎹 Piano Ready';
1715 document.getElementById('btn-init-audio').classList.add('active');
1716 document.getElementById('btn-play').disabled = false;
1717 },
1718 }).connect(reverb);
1719 }
1720
1721 function stopPlayback() {
1722 if (state.rafId) { cancelAnimationFrame(state.rafId); state.rafId = null; }
1723 state.isPlaying = false;
1724 state.playheadSec = 0;
1725 PR.setPlayhead(0);
1726 updatePlayBtn();
1727 document.getElementById('progress-fill').style.width = '0%';
1728 document.getElementById('time-display').textContent = '0:00';
1729 // Tone.js: cancel all pending events
1730 try { Tone.getTransport().stop(); Tone.getTransport().cancel(); } catch(e) {}
1731 }
1732
1733 function playNotes(notes) {
1734 if (!piano || !state.audioReady) return;
1735 stopPlayback();
1736
1737 const minStart = notes.length ? Math.min(...notes.map(n => n[2])) : 0;
1738 const now = Tone.now() + 0.3;
1739 state.playStartWallClock = performance.now();
1740 state.playStartAudioSec = minStart;
1741 state.isPlaying = true;
1742 updatePlayBtn();
1743
1744 // Schedule all notes via Tone.js (offloads to Web Audio scheduler)
1745 notes.forEach(n => {
1746 const t = now + (n[2] - minStart);
1747 const noteName = Tone.Frequency(n[0], 'midi').toNote();
1748 const dur = Math.max(n[3], 0.06);
1749 const vel = n[1] / 127;
1750 try { piano.triggerAttackRelease(noteName, dur, t, vel); } catch(e) {}
1751 });
1752
1753 // Animate playhead
1754 const totalDur = notes.length ? Math.max(...notes.map(n => n[2] + n[3])) - minStart : 0;
1755
1756 function tick() {
1757 if (!state.isPlaying) return;
1758 const elapsed = (performance.now() - state.playStartWallClock) / 1000;
1759 const sec = state.playStartAudioSec + elapsed;
1760 PR.setPlayhead(sec);
1761 document.getElementById('time-display').textContent = fmtTime(elapsed);
1762 document.getElementById('progress-fill').style.width =
1763 totalDur > 0 ? `${Math.min(elapsed / totalDur * 100, 100)}%` : '0%';
1764 if (elapsed < totalDur + 0.5) {
1765 state.rafId = requestAnimationFrame(tick);
1766 } else {
1767 stopPlayback();
1768 }
1769 }
1770 state.rafId = requestAnimationFrame(tick);
1771 }
1772
1773 // ═══════════════════════════════════════════════════════════════
1774 // COMMIT SELECTION (main controller)
1775 // ═══════════════════════════════════════════════════════════════
1776 function selectCommit(idx) {
1777 state.commitIdx = Math.max(0, Math.min(idx, COMMITS.length - 1));
1778 const commit = getCommit(state.commitIdx);
1779
1780 // Update all panels
1781 PR.update(commit);
1782 DAG.select(state.commitIdx);
1783 DimPanel.update(commit);
1784 CmdLog.show(commit);
1785 if (window._heatmapHighlight) window._heatmapHighlight(state.commitIdx);
1786
1787 // Update control info
1788 document.getElementById('commit-sha-disp').textContent = commit.sha;
1789 document.getElementById('commit-msg-disp').textContent = commit.message;
1790
1791 // Play if audio ready
1792 if (state.audioReady && commit.filter) {
1793 const notes = getNotesForCommit(commit);
1794 playNotes(notes);
1795 }
1796
1797 updateControls();
1798 }
1799
1800 function updateControls() {
1801 document.getElementById('btn-first').disabled = state.commitIdx === 0;
1802 document.getElementById('btn-prev').disabled = state.commitIdx === 0;
1803 document.getElementById('btn-next').disabled = state.commitIdx === COMMITS.length - 1;
1804 document.getElementById('btn-last').disabled = state.commitIdx === COMMITS.length - 1;
1805 }
1806
1807 function updatePlayBtn() {
1808 const btn = document.getElementById('btn-play');
1809 btn.textContent = state.isPlaying ? '⏸' : '▶';
1810 btn.className = 'ctrl-play' + (state.isPlaying ? ' playing' : '');
1811 }
1812
1813 // ═══════════════════════════════════════════════════════════════
1814 // CLI REFERENCE DATA
1815 // ═══════════════════════════════════════════════════════════════
1816 const CLI_CMDS = [
1817 {
1818 name: 'muse notes',
1819 desc: 'Display notes as a musical notation table (bar / beat / pitch / velocity / duration).',
1820 flags: [
1821 { name: '--bar <range>', desc: 'Filter to specific bars, e.g. 1-8 or 3' },
1822 { name: '--track <path>', desc: 'Restrict to a specific MIDI track file' },
1823 { name: '--voice <n>', desc: 'Filter to a MIDI channel/voice (1-16)' },
1824 { name: '--format', desc: 'Output format: table (default), csv, json' },
1825 ],
1826 returns: 'Table rows: bar, beat, pitch (name), MIDI#, velocity, dur(ticks)',
1827 },
1828 {
1829 name: 'muse piano-roll',
1830 desc: 'Render an ASCII piano roll of committed MIDI notes.',
1831 flags: [
1832 { name: '--bars <range>', desc: 'Bars to display (default: all)' },
1833 { name: '--resolution', desc: 'Ticks per cell: 12 (16th), 6 (32nd), 3 (64th)' },
1834 { name: '--color', desc: 'Enable ANSI color output (voice-coded)' },
1835 { name: '--width <n>', desc: 'Terminal width in chars (default: 120)' },
1836 ],
1837 returns: 'ASCII roll: pitch axis (Y), time axis (X), ● for note-on',
1838 },
1839 {
1840 name: 'muse harmony',
1841 desc: 'Chord analysis, key detection (Krumhansl-Schmuckler), and pitch-class histogram.',
1842 flags: [
1843 { name: '--bar <range>', desc: 'Analyse a specific bar range' },
1844 { name: '--window <n>', desc: 'Sliding window in bars (default: 4)' },
1845 { name: '--output', desc: 'table | json | histogram' },
1846 ],
1847 returns: 'Key guess, chord per bar, pitch-class distribution',
1848 },
1849 {
1850 name: 'muse velocity-profile',
1851 desc: 'Dynamic range analysis — velocity histogram and per-bar statistics.',
1852 flags: [
1853 { name: '--bar <range>', desc: 'Restrict to a bar range' },
1854 { name: '--bins <n>', desc: 'Histogram bucket count (default: 16)' },
1855 ],
1856 returns: 'Min/max/mean velocity, per-bar average, ASCII histogram',
1857 },
1858 {
1859 name: 'muse note-log',
1860 desc: 'Note-level change history — equivalent of git log -p but for MIDI notes.',
1861 flags: [
1862 { name: '--bar <range>', desc: 'Filter by bar number' },
1863 { name: '--pitch <p>', desc: 'Filter by MIDI pitch (e.g. 60, C4)' },
1864 { name: '--oneline', desc: 'Compact one-line-per-commit format' },
1865 { name: '--since <ref>', desc: 'Start from a commit ref' },
1866 ],
1867 returns: '+note / -note rows per commit, with pitch, velocity, timing',
1868 },
1869 {
1870 name: 'muse note-blame',
1871 desc: 'Per-bar attribution — which commit last touched each bar.',
1872 flags: [
1873 { name: '--bar <range>', desc: 'Annotate specific bars only' },
1874 { name: '--track <path>', desc: 'Target MIDI file' },
1875 { name: '--porcelain', desc: 'Machine-readable output' },
1876 ],
1877 returns: 'bar# commit-sha author message (one row per bar)',
1878 },
1879 {
1880 name: 'muse note-hotspots',
1881 desc: 'Bar-level churn leaderboard — which bars changed most frequently.',
1882 flags: [
1883 { name: '--top <n>', desc: 'Show top N bars (default: 10)' },
1884 { name: '--since <ref>', desc: 'Limit history range' },
1885 ],
1886 returns: 'Ranked list: bar, change count, last modified commit',
1887 },
1888 {
1889 name: 'muse transpose',
1890 desc: 'Surgically shift pitches by semitone interval without creating a new commit.',
1891 flags: [
1892 { name: '--semitones <n>', desc: 'Number of semitones to shift (+/-)' },
1893 { name: '--bar <range>', desc: 'Restrict transposition to a bar range' },
1894 { name: '--voice <n>', desc: 'Restrict to a specific MIDI voice/channel' },
1895 { name: '--dry-run', desc: 'Preview without writing changes' },
1896 { name: '--clamp', desc: 'Clamp to valid MIDI range 0–127 instead of error' },
1897 ],
1898 returns: 'Modified MIDI file written to muse-work/; use muse diff to review',
1899 },
1900 {
1901 name: 'muse mix',
1902 desc: 'Layer two MIDI tracks into a single output track with channel remapping.',
1903 flags: [
1904 { name: '--channel-a <n>', desc: 'Output channel for first source' },
1905 { name: '--channel-b <n>', desc: 'Output channel for second source' },
1906 { name: '--out <path>', desc: 'Destination file path' },
1907 ],
1908 returns: 'Merged MIDI file written to muse-work/; commit to persist',
1909 },
1910 {
1911 name: 'muse midi-query',
1912 desc: 'Structured query against MIDI content using the Muse query DSL.',
1913 flags: [
1914 { name: '--where', desc: 'Filter expression, e.g. "pitch > 60 AND velocity > 90"' },
1915 { name: '--select', desc: 'Output columns: pitch, velocity, bar, beat, dur' },
1916 { name: '--limit <n>', desc: 'Max rows returned' },
1917 { name: '--format', desc: 'table | json | csv' },
1918 ],
1919 returns: 'Filtered note table matching the query predicate',
1920 },
1921 {
1922 name: 'muse midi-check',
1923 desc: 'Validate MIDI invariants: no stuck notes, tempo consistency, no out-of-range events.',
1924 flags: [
1925 { name: '--strict', desc: 'Error on warnings (not just errors)' },
1926 { name: '--fix', desc: 'Auto-correct recoverable violations' },
1927 ],
1928 returns: 'Pass/fail report with violation details and line references',
1929 },
1930 {
1931 name: 'muse diff',
1932 desc: 'Show structured delta between two commits or working tree vs HEAD.',
1933 flags: [
1934 { name: '<ref1>', desc: 'Base commit SHA or branch name' },
1935 { name: '<ref2>', desc: 'Target commit SHA or branch name (default: HEAD)' },
1936 { name: '--dimension <d>', desc: 'Filter to a specific MIDI dimension' },
1937 { name: '--stat', desc: 'Summary statistics only (note counts per dimension)' },
1938 ],
1939 returns: 'InsertOp / DeleteOp / MutateOp per note with full field details',
1940 },
1941 ];
1942
1943 // ═══════════════════════════════════════════════════════════════
1944 // BUILD CLI REFERENCE
1945 // ═══════════════════════════════════════════════════════════════
1946 (function buildCLI() {
1947 const grid = document.getElementById('cli-grid');
1948 CLI_CMDS.forEach(cmd => {
1949 const card = document.createElement('div');
1950 card.className = 'cmd-card';
1951 card.innerHTML = `
1952 <div class="cmd-name">${cmd.name}</div>
1953 <div class="cmd-desc">${cmd.desc}</div>
1954 <div class="cmd-flags">
1955 ${cmd.flags.map(f => `
1956 <div class="cmd-flag">
1957 <span class="flag-name">${f.name}</span>
1958 <span class="flag-desc">${f.desc}</span>
1959 </div>
1960 `).join('')}
1961 </div>
1962 <div class="cmd-return">Returns: <span>${cmd.returns}</span></div>
1963 `;
1964 grid.appendChild(card);
1965 });
1966 })();
1967
1968 // ═══════════════════════════════════════════════════════════════
1969 // WIRE UP CONTROLS
1970 // ═══════════════════════════════════════════════════════════════
1971 document.getElementById('btn-init-audio').addEventListener('click', initAudio);
1972
1973 document.getElementById('btn-play').addEventListener('click', () => {
1974 if (!state.audioReady) { initAudio(); return; }
1975 if (state.isPlaying) {
1976 stopPlayback();
1977 } else {
1978 const notes = getNotesForCommit(getCommit(state.commitIdx));
1979 playNotes(notes);
1980 }
1981 });
1982
1983 document.getElementById('btn-first').addEventListener('click', () => selectCommit(0));
1984 document.getElementById('btn-last').addEventListener('click', () => selectCommit(COMMITS.length - 1));
1985 document.getElementById('btn-prev').addEventListener('click', () => selectCommit(state.commitIdx - 1));
1986 document.getElementById('btn-next').addEventListener('click', () => selectCommit(state.commitIdx + 1));
1987
1988 // Keyboard shortcuts
1989 document.addEventListener('keydown', e => {
1990 if (e.target.tagName === 'INPUT') return;
1991 if (e.key === 'ArrowRight') selectCommit(state.commitIdx + 1);
1992 if (e.key === 'ArrowLeft') selectCommit(state.commitIdx - 1);
1993 if (e.key === ' ') { e.preventDefault(); document.getElementById('btn-play').click(); }
1994 });
1995
1996 // ═══════════════════════════════════════════════════════════════
1997 // INIT
1998 // ═══════════════════════════════════════════════════════════════
1999 document.getElementById('btn-play').disabled = true;
2000 document.getElementById('time-total').textContent = fmtTime(TOTAL_DURATION);
2001
2002 // Start with commit 0 (muse init, no notes)
2003 selectCommit(0);
2004 </script>
2005 </body>
2006 </html>
2007 """
2008
2009
2010 if __name__ == "__main__":
2011 logging.basicConfig(level=logging.INFO, format="%(message)s")
2012 out = pathlib.Path("artifacts/midi-demo.html")
2013 out.parent.mkdir(exist_ok=True)
2014 content = render_midi_demo()
2015 out.write_text(content, encoding="utf-8")
2016 kb = len(content) // 1024
2017 logger.info("✅ artifacts/midi-demo.html written (%d KB)", kb)