I was testing an instrument change in Battuta, a MIDI editor I’m building. I asked it to make the second note a trumpet. The command succeeded, and inspecting the file showed the change. But when I played it back, that note was still a violin.
These two clips reproduce the problem and the fix. The first note is identical in both; the difference is in the second.
The second note should be a trumpet.
Each clip plays two notes, followed by their fading sound.
A / The second note stays a violin
What the file says
Same time, on separate tracks.
Neither instruction is placed before the other.
What this player does
- Starts the note as a violin
- Changes the instrument to trumpet
The note has already started.
It stays a violin.
B / The second note becomes a trumpet
What the file says
Same time, in a stated sequence.
The arrow means order, not a delay.
What this player does
- Changes the instrument to trumpet
- Starts the note as a trumpet
The change reaches the note.
It plays as a trumpet.
Measurements, MIDI files and reproduction
The difference-signal peak is −17.1 dBFS, measured as the largest sample in A minus B. This is not a loudness difference or a comparison of players.
The check also handles edits that move or add a note against an instrument change on another track. Two tracks stating the same instrument do not conflict with each other; differing instrument values do. This compares channel state, not all possible audio behaviour.
Reconstructed on 6 September 2026 from Battuta’s issue #26 regression fixture. These are new renders, not the original debugging audio.
Two notes, C4 then E4, each 1 second, velocity 64. Channel 0; 960 ticks per quarter note; 120 BPM. Track and channel numbers are zero-based. A puts program 56 (trumpet) on track 2 at tick 1920; B puts it on track 1 before E4, at the same tick.
The fixture also has a carried-in cross-track program change at tick 0. It is preserved in both versions. On this renderer the first note uses the initial piano sound; the diagram isolates the edit at tick 1920.
FluidSynth 2.6.0, GeneralUser GS, 44.1 kHz stereo, 16-bit PCM. Shared synth gain 1.0; reverb and chorus off. No normalisation or time shift. Full release tails retained; the shorter render is padded with silence. Each clip is 4.46 seconds.
Battuta 0.1.2 refuses A unless this run explicitly allows t2:c0:s1920. B succeeds without that exception. Reproduction checks both behaviours and verifies that A still matches the unedited audio.
Input MIDI Unedited audio Edit A Edit B Measurements Refusal Reproduction script
The script requires Python with mido, FluidSynth, Battuta 0.1.2, and a soundfont supplied with --soundfont. Run it with --out pointing to a new directory. The measurement file records the soundfont checksum; the soundfont itself is not included.
The player started the note before changing the instrument
A MIDI file contains instructions for making sound, rather than a recording. “Start this note” and “change the instrument” are separate instructions. Their order matters: if the note starts first, it starts with the instrument already selected.
In this case, both instructions had the same timestamp. They were stored on different tracks, each with its own sequence of events. The file said when to execute them, but supplied no order between them across those tracks.
The fix puts the instrument change before the note in the same track. Both still happen at the same timestamp; the file now states which comes first. The MIDI format’s track structure makes that sequence expressible.
Battuta’s inspection had read the new instrument and treated it as the one the note would use. That was the mistake. I use the tool to work on music with an AI agent, which reads these reports before deciding what to try next. A report that overlooks the execution order can send the next edit in the wrong direction.
The same problem needs different treatment when reading and writing
One response would be to choose an order and make playback follow it. That works inside a particular player. The edited file can leave that player, though, and the choice would not travel with it.
Battuta instead reports the missing order. Whether it also stops depends on what it is being asked to do.
Open a file
The file already contains this problem.
Show it and keep reading
The music stays available for inspection.
Make an edit
The edit would write events with this problem.
Stop before writing
Explain where it happens and how to fix it.
Allow this location
The caller explicitly accepts it for this run.
Write with the exception
Inspection still shows the missing order.
When opening a file, refusing to read it would make the problem harder to diagnose. Inspection therefore shows where the order is missing and continues listing the music. It leaves the original events alone.
When editing, the tool can prevent itself from writing another instance of the problem. If the batch writes events with this cross-track dependency, it stops before creating an output file. The error identifies the events and suggests putting the instrument change on the notes’ track, or earlier than the notes.
Allowing the edit does not settle what it will sound like
Sometimes the caller wants to keep the arrangement: they know the intended player, or they are making a test file. They can explicitly allow that location for this run. That is how clip A was produced with the current version of Battuta, which otherwise refuses the edit.
The exception does not insert an order into the file. Inspection still reports the problem afterward. Accepting an ambiguity does not resolve it.
I do not take this check as a guarantee that a file will sound identical on every synthesiser. It addresses this particular missing relationship between instructions. The audio comparison above demonstrates its consequence on one renderer.
Moving the instrument change fixes the file. Allowing it leaves the file’s meaning dependent on the player.