render_midi_demo.py
python
| 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 & 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) |