Episode 11 --- Muse Understands Music
Working YouTube title:
I Broke My Own Feature On Camera (MIDI, Note-Level Diff)
Thumbnail thought:
one note. four notes. one bug.
Target runtime: ~8:30
[0:00--0:20] COLD OPEN
[CAMERA --- Episode 10's closing line, on screen: "What does 'understanding music' get you?"]
GABRIEL:
Same six methods. A file format that isn't text, isn't symbols --- it's notes, velocity, pitch bend, twenty-one of these things at once. Let's find out what "understanding" actually means here.
[TITLE CARD --- fast]
MUSE UNDERSTANDS MUSIC
[Music enters --- literally, this time.]
[0:20--1:10] FOUR NOTES, ONE FILE
[TERMINAL]
$ python3 -c "import mido; [print(m) for m in mido.MidiFile('phrase.mid').tracks[0]]"
MetaMessage('set_tempo', tempo=500000, time=0)
note_on channel=0 note=60 velocity=80 time=0 # C4
note_off channel=0 note=60 velocity=0 time=480
note_on channel=0 note=64 velocity=80 time=0 # E4
note_off channel=0 note=64 velocity=0 time=480
note_on channel=0 note=67 velocity=80 time=0 # G4
note_off channel=0 note=67 velocity=0 time=480
note_on channel=0 note=72 velocity=80 time=0 # C5
note_off channel=0 note=72 velocity=0 time=480
GABRIEL VO:
C, E, G, C-up-an-octave. A real .mid file, built with a real MIDI
library, not a mock. To Git this is 69 bytes of opaque binary.
[beat]
$ muse init --domain midi
GABRIEL:
To Muse, it's about to be something else entirely.
[1:10--1:50] TWENTY-ONE DIMENSIONS
[TERMINAL]
$ muse domain-info --json | jq '.domain_schema.dimensions | length'
21
$ muse domain-info --json | jq '.domain_schema.dimensions[].name'
"notes" "pitch_bend" "channel_pressure" "poly_pressure"
"cc_modulation" "cc_volume" "cc_pan" "cc_expression" "cc_sustain"
"cc_portamento" "cc_sostenuto" "cc_soft_pedal" "cc_reverb" "cc_chorus"
"cc_other" "program_change" "tempo_map" "time_signatures"
"key_signatures" "markers" "track_structure"
GABRIEL VO:
Episode 05's code domain has five dimensions --- structure, symbols,
imports, variables, metadata. Same six-method protocol, same
MuseDomainPlugin interface. Twenty-one dimensions here, because
music has twenty-one independent ways to change.
[beat]
Most of those are independently mergeable --- two agents editing reverb and pan don't even see each other. A couple aren't: change the tempo map and it's declared non-independent, because it affects how every other dimension reads.
[1:50--2:50] ONE NOTE CHANGES
[TERMINAL]
$ muse checkout -b feat/raise-third --intent "raise the third from E to F"
$ python3 -c "
import mido
mid = mido.MidiFile('phrase.mid')
for t in mid.tracks:
for m in t:
if m.type in ('note_on','note_off') and m.note == 64:
m.note = 65
mid.save('phrase.mid')
"
$ muse commit -m "Raise the third from E to F" --sign
GABRIEL:
One semitone. E to F. The kind of edit a composer makes fifty times in an afternoon. Now let's look at what Muse actually recorded.
$ muse read --json --manifest | jq '.structured_delta'
{
"domain": "midi",
"ops": [{
"op": "replace",
"address": "phrase.mid",
"old_content_id": "sha256:ed3b5e06...",
"new_content_id": "sha256:e3c892f5..."
}],
"summary": "1 file modified"
}
[beat, GABRIEL reading the output, visibly stopping]
GABRIEL:
That's not what I expected. op: replace. Whole file, opaque hash
to opaque hash. That's what Git would show me. That's not
"twenty-one independently mergeable dimensions" --- that's a binary
diff with extra JSON around it.
[2:50--4:10] FINDING IT LIVE
[CAMERA]
Let's not just accept that. The plugin's source says it should be better than this.
[TERMINAL --- muse code cat]
$ muse code cat "muse/plugins/midi/plugin.py::_diff_modified_file"
if path.lower().endswith(".mid") and repo_root is not None:
base_bytes = read_object(repo_root, old_hash)
target_bytes = read_object(repo_root, new_hash)
if base_bytes is not None and target_bytes is not None:
try:
child_delta = diff_midi_notes(base_bytes, target_bytes, file_path=path)
return PatchOp(op="patch", address=path, child_ops=child_delta["ops"], ...)
except Exception as exc:
logger.debug("MIDI deep diff failed: %s", exc)
return ReplaceOp(op="replace", ...) # ← what we actually got
GABRIEL VO:
There it is in the code: real note-level diffing exists, it's called
diff_midi_notes, and it silently falls back to ReplaceOp the
moment either blob can't be read from the object store --- logged at
debug level, invisible by default.
[beat]
So which blob is missing? Let's just call the exact same function directly, by hand, against the exact same two hashes.
$ python3 -c "
from muse.core.object_store import read_object
from muse.plugins.midi.midi_diff import diff_midi_notes
old = read_object(root, 'sha256:ed3b5e06...')
new = read_object(root, 'sha256:e3c892f5...')
print(diff_midi_notes(old, new, file_path='phrase.mid'))
"
{
"domain": "midi_notes",
"ops": [
{"op": "insert", "content_summary": "F4 vel=80 @beat=1.00 dur=1.00"},
{"op": "delete", "content_summary": "E4 vel=80 @beat=1.00 dur=1.00"}
],
"summary": "1 note added, 1 note removed"
}
GABRIEL:
Works perfectly. F4 in, E4 out. Same two hashes, called directly.
So the note-diff logic isn't broken --- something about when it
gets called during muse commit is.
[TERMINAL --- muse code cat commit.py, two line numbers highlighted]
commit.py:532 structured_delta = plugin.diff(base_snap, snap, repo_root=root)
commit.py:658 write_object_from_path(root, object_id, root / rel_path)
GABRIEL VO:
Line 532 runs before line 658. Muse computes the note-level diff
before it writes the new file's bytes into the object store. The
new hash doesn't exist yet when diff_midi_notes goes looking for
it. read_object returns nothing. The except Exception catches
the failure silently, and every single MIDI commit, forever, has
been falling back to the opaque replace.
[beat]
That's a real bug. In my own product. Found by making a demo for this exact episode.
[4:10--4:40] WHAT HAPPENS NEXT
[CAMERA]
I'm not fixing it on camera --- fixing it means reordering writes inside the commit path, and I want to actually think about the blast radius of that before I touch it. Filed:
$ muse hub issue read 203
#203 MIDI structured_delta always falls back to file-level ReplaceOp
assignee: gabriel label: bug
GABRIEL VO:
The commit itself is correct --- the right bytes are stored, nothing is lost. Only the observability of the change is wrong. Still a real defect, still going in the tracker, still getting fixed after this season wraps, not glossed over for a smoother episode.
[4:40--6:00] TWO BRANCHES, ONE NOTE, TWO DIFFERENT ANSWERS
[CAMERA]
The underlying diff logic works, and I can prove the merge engine still uses it correctly for conflict detection, even while commit's delta storage is broken. Watch.
[TERMINAL]
$ muse switch main
$ muse checkout -b feat/louder-melody --intent "raise velocity on the melody notes"
$ python3 -c "
import mido
mid = mido.MidiFile('phrase.mid')
for t in mid.tracks:
for m in t:
if m.type == 'note_on': m.velocity = 110
mid.save('phrase.mid')
"
$ muse commit -m "Increase melody velocity to 110" --sign
GABRIEL VO:
Second branch, off the original main --- before the pitch change. Same file, different attribute: this one only touches velocity.
$ muse switch main
$ muse merge feat/raise-third
status: fast_forward
GABRIEL:
First merge --- trivial, only one side touched anything since main last moved. Now the second one.
$ muse merge feat/louder-melody
{ "status": "conflict", "conflicts": ["phrase.mid"] }
GABRIEL VO:
A real conflict. Not staged --- this is what happens when one branch changes a note's pitch and another branch changes that same note's velocity, and both branches think they're the only one who touched it.
[6:00--7:00] RESOLVING BY EAR, NOT BY FLAG
[CAMERA]
No conflict markers to read --- it's a binary format, there's no
<<<<<<< ours you can put inside a .mid file. So I do what
Episode 08 already established: read what each side actually wanted,
by hand.
[TERMINAL]
$ muse conflicts --json
{ "conflicts": [{"path": "phrase.mid", "kind": "file"}],
"ours_commit": "...raise-third", "theirs_commit": "...louder-melody" }
GABRIEL:
Ours wanted F4. Theirs wanted velocity 110. Neither side is wrong --- they're just two true things about the same note that nobody reconciled yet. So I write the file that's actually correct: F4, velocity 110, every note.
$ python3 -c "... set velocity=110 on the F4-corrected file ..."
$ muse resolve phrase.mid
$ muse commit -m "Merge feat/louder-melody: F pitch + velocity 110" --sign
GABRIEL VO:
muse resolve, not --ours or --theirs. Neither branch alone had
the answer. The merged file does.
[7:00--7:40] THE POINT, EVEN WITH THE BUG IN IT
[CAMERA]
Here's what I want you to sit with: the conflict detection worked. The merge engine correctly noticed two branches touched the same note. The problem is scoped to one function, in one file, running in the wrong order relative to one write. That's fixable. It's fixable precisely because Muse knows what a note is in the first place --- you can't have an ordering bug in a note-level diff for a domain that never modeled notes at all.
[beat]
Git wouldn't have this bug. Git also wouldn't have shown you F4 and velocity 110 as two separate, mergeable facts about the same note. You don't get the second thing without some risk of the first.
[7:40--8:15] OUT
[TERMINAL --- fading to a blank prompt]
GABRIEL VO:
Ten episodes of "here's what Muse can tell you." Next: what happens when the thing writing commits isn't a person sitting at this terminal at all.
[CAMERA]
Agents. Real ones, with real identities, making real commits --- several of which you've already watched happen in this very season.
[CUT TO BLACK]
musehub.ai
Production Notes
Episode 11 is structurally different from every episode before it:
the demo doesn't just show a feature, it finds a real bug in that
feature, live, and keeps going. That's a harder needle to thread than
Episode 09's Harmony bug because this one is in the exact command
(muse commit) every prior episode has trusted implicitly. Don't
let the tone curdle into an apology --- it's a discovery, delivered
with the same energy as finding gravity disagree with hotspots in
Episode 10.
Why This One, Honestly, Not Worked Around
Per the season's established discipline (Episode 09's Harmony
auto-apply bug), when a real defect is found while building an
episode's own demo, the choice this time was to show it plainly
rather than route around it with a lower-level function call dressed
up as the CLI path. The one deliberate exception: the "does the
underlying diff logic even work" check does call diff_midi_notes
directly, on camera, but it's framed explicitly as debugging, not
as what muse commit actually does --- the audience should never
think that's the normal user path.
The Merge Still Has To Be Real
The Episode 11 conflict is not staged around the bug --- it's a
genuine two-branch conflict (pitch changed on one side, velocity on
the other, same note) that the merge engine correctly detects
despite commit's structured-delta bug, because conflict detection
and delta storage are different code paths. Verify this distinction
holds on whatever muse build is current at record time before
claiming it on camera again.
Ticket Discipline
staging#203 was filed the moment the root cause was confirmed
(read the exact two line numbers in commit.py, reproduced the
underlying diff function working correctly in isolation), not before
confirming it wasn't user error. Assigned to gabriel, deferred until
after the season, matching #90--#93.
Everything Here Is Real
phrase.mid was built with mido, a real MIDI library --- not a
byte-literal fake. Every commit, branch, conflict, and the bug itself
were reproduced twice against the actual current build before being
described on camera. Re-run make-midi-episode11-demo.sh at record
time and re-verify the exact hashes and line numbers still match ---
if commit.py has since been reordered, this episode's central beat
needs re-shooting, not just re-narrating.
The Seed
The viewer arrives thinking:
Okay, MIDI is a cute proof that domains aren't just "code."
They should leave thinking:
Wait --- he just found and filed a real bug in his own tool, mid-episode, and the fix was obvious once he saw it. What else is in here that nobody's looked at yet?
That question is the entire back half of the season.