make-muse-midi-episode00-demo.sh bash
135 lines 3.3 KB
Raw
sha256:5472be4fece32b606c308fad9d57295ce327955fb2b87b8beec6ea1626050473 Add published YouTube URL to Episode 00 Sonnet 5 9 hours ago
1 #!/usr/bin/env bash
2 set -euo pipefail
3
4 DEMO_DIR="${1:-muse-midi-episode00}"
5
6 command -v muse >/dev/null 2>&1 || { echo "error: muse not found on PATH" >&2; exit 1; }
7 command -v python3 >/dev/null 2>&1 || { echo "error: python3 not found on PATH" >&2; exit 1; }
8
9 python3 - <<'PY' >/dev/null 2>&1 || {
10 import mido
11 PY
12 echo "error: Python package 'mido' is not importable by this python3." >&2
13 echo "Run from your Muse development environment / venv, where mido is installed." >&2
14 exit 1
15 }
16
17 rm -rf "$DEMO_DIR"
18 mkdir -p "$DEMO_DIR"
19 cd "$DEMO_DIR"
20
21 cat > make_phrase.py <<'PY'
22 #!/usr/bin/env python3
23 from pathlib import Path
24 import argparse
25 import mido
26
27 TPB = 480
28
29 BASE = [
30 (0, 60, 80, 360), # C4
31 (480, 64, 80, 360), # E4
32 (960, 67, 80, 360), # G4
33 (1440, 72, 80, 360), # C5
34 ]
35
36 SOL = [
37 (0, 60, 80, 360), # unchanged
38 (480, 65, 80, 360), # E4 -> F4: pitch
39 (1080, 67, 80, 360), # moved later: position
40 (1440, 72, 108, 360), # velocity 80 -> 108
41 ]
42
43 def build(path: Path, bpm: int, notes):
44 mid = mido.MidiFile(type=0, ticks_per_beat=TPB)
45 track = mido.MidiTrack()
46 mid.tracks.append(track)
47
48 events = [
49 (0, mido.MetaMessage("track_name", name="Episode 00 phrase", time=0)),
50 (0, mido.MetaMessage("set_tempo", tempo=mido.bpm2tempo(bpm), time=0)),
51 (0, mido.MetaMessage(
52 "time_signature",
53 numerator=4,
54 denominator=4,
55 clocks_per_click=24,
56 notated_32nd_notes_per_beat=8,
57 time=0,
58 )),
59 ]
60
61 for start, pitch, velocity, duration in notes:
62 events.append((start, mido.Message("note_on", channel=0, note=pitch, velocity=velocity, time=0)))
63 events.append((start + duration, mido.Message("note_off", channel=0, note=pitch, velocity=0, time=0)))
64
65 order = {"track_name": 0, "set_tempo": 1, "time_signature": 2, "note_off": 3, "note_on": 4}
66 events.sort(key=lambda item: (item[0], order.get(item[1].type, 99)))
67
68 previous = 0
69 for tick, msg in events:
70 track.append(msg.copy(time=tick - previous))
71 previous = tick
72
73 track.append(mido.MetaMessage("end_of_track", time=0))
74 mid.save(path)
75
76 parser = argparse.ArgumentParser()
77 parser.add_argument("variant", choices=["base", "tempo", "sol"])
78 parser.add_argument("--out", default="phrase.mid")
79 args = parser.parse_args()
80
81 if args.variant == "base":
82 build(Path(args.out), 120, BASE)
83 elif args.variant == "tempo":
84 build(Path(args.out), 100, BASE)
85 else:
86 build(Path(args.out), 120, SOL)
87 PY
88
89 echo
90 echo "==> Initializing Muse MIDI repo"
91 muse init --domain midi
92
93 echo
94 echo "==> Baseline: 4-note phrase at 120 BPM"
95 python3 make_phrase.py base
96 muse commit -m "Base: four-note phrase at 120 BPM"
97
98 echo
99 echo "==> Gabriel branch: tempo only"
100 muse checkout -b gabriel-tempo
101 python3 make_phrase.py tempo
102 muse commit -m "Gabriel: slow tempo to 100 BPM"
103
104 echo
105 echo "==> Sol branch: note semantics only"
106 muse checkout main
107 muse checkout -b sol-notes
108 python3 make_phrase.py sol
109 muse commit -m "Sol: reshape phrase notes"
110
111 echo
112 echo "==> Return to main"
113 muse checkout main
114
115 cat <<'EOF'
116
117 Ready.
118
119 Suggested shots:
120
121 muse branch
122 muse log --oneline
123
124 muse diff main gabriel-tempo
125 muse diff main sol-notes
126 muse diff main sol-notes --format json
127
128 muse checkout main
129 muse merge gabriel-tempo
130 muse merge sol-notes
131
132 muse log --oneline
133 muse status
134
135 EOF
File History 1 commit
sha256:5472be4fece32b606c308fad9d57295ce327955fb2b87b8beec6ea1626050473 Add published YouTube URL to Episode 00 Sonnet 5 9 hours ago