Eigenstate
Essays / Eigenvalues

Why the instrument change didn’t change the sound

The tool reported a trumpet. Playback still gave me a violin.

State: public draft Domain: agent tools Pressure: state truth Published: 2026-09-06

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

Two instructions on separate tracksAt one second, track 1 starts the note and track 2 changes the instrument to trumpet. Both address channel 0. The file does not state which comes first.At 1 second · channel 0Track 1Start the noteTrack 2Change to trumpet

Same time, on separate tracks.
Neither instruction is placed before the other.

What this player does

  1. Starts the note as a violin
  2. 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

Both instructions in the same trackAt one second, track 1 changes channel 0 to trumpet before starting the note. The arrow represents event order, with no time between the instructions. Track 2 has no event at this instant.At 1 second · channel 0Track 1Change totrumpetStartthe noteTrack 2

Same time, in a stated sequence.
The arrow means order, not a delay.

What this player does

  1. Changes the instrument to trumpet
  2. Starts the note as a trumpet

The change reaches the note.
It plays as a trumpet.

The second note in each clip, played by FluidSynth 2.6.0. Both files request the change at one second. Only B states the order within a track.
These clips reconstruct the regression test on the same player and soundfont. A’s audio is identical to the unedited input, sample for sample.
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.

Allowing the write does not remove the condition that inspection reports.

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.