Prove a MIDI asset fits its player before the player drops notes, rejects events, or hangs a voice.
A Standard MIDI File can be perfectly parseable and still exceed the engine that must play it. Embedded synths, games, web players, karaoke systems, and older hardware differ in track, event, polyphony, channel, controller, program, metadata, and SysEx support. Playback spot checks usually find those mismatches after integration.
MidiConform turns the target's limits into a small TOML contract. It parses the actual event stream, simulates pressed and sustain-held notes, measures the file's capability envelope, and points to the exact tick, track, and event that breaks the contract. It is a real local CLI and Python package—no upload, account, MIDI device, or AI model.
Python 3.11+ and uv are required for source development.
git clone https://github.com/KanadeK/midiconform.git
cd midiconform
uv sync --locked --all-extras
# Must pass with exit 0.
uv run midiconform check examples/compatible.mid \
--profile examples/pocket-player.toml
# Must fail the contract with exit 1 and exact evidence.
uv run midiconform check examples/incompatible.mid \
--profile examples/pocket-player.tomlThe failing sample is deliberately too wide for the fictional pocket player:
MidiConform FAIL — Pocket Player Demo
FAIL examples/incompatible.mid · type 1 · 2 track(s) · 12 event(s)
peak 3 voice(s) · 3 SysEx byte(s)
ERROR MC012 tick 0 track 0 event 2: meta message lyrics is not allowed
ERROR MC008 tick 0 track 1 event 1: channel 2 is not allowed
ERROR MC009 tick 0 track 1 event 1: program 40 is not allowed
ERROR MC013 tick 0 track 1 event 2: SysEx is forbidden but the file contains 1 event(s) totaling 3 data byte(s)
ERROR MC101 tick 0 track 1 event 4: note 60 is pressed again before release on channel 2
…
The examples are original synthetic data. Their readable generator is
scripts/generate_examples.py; CI regenerates them and rejects
binary drift.
version = 1
name = "Pocket Player"
[limits]
file_bytes = 65536
tracks = 4
events = 5000
ticks_per_beat = 480
total_polyphony = 16
channel_polyphony = 8
sysex_bytes = 0
min_note_ticks = 2
[allow]
file_types = [0, 1]
channels = [1, 2, 10]
programs = [0, 24, 32]
controllers = [1, 7, 10, 64, 120, 123]
channel_messages = ["note_on", "note_off", "program_change", "control_change"]
meta_messages = ["track_name", "set_tempo", "time_signature", "end_of_track"]
sysex = falseEvery allow-list except file_types is optional, while the sysex boolean is required. An omitted
list means unconstrained; an empty list means nothing in that category is allowed. Channel numbers
are 1..16; program and controller values are the MIDI data values 0..127.
| Layer | Deterministic evidence |
|---|---|
| File envelope | SMF type, bytes, tracks, events, ticks per beat |
| Playback load | Sustain-aware total and per-channel sounding-note peaks |
| Capability use | Channels, programs, controllers, channel messages, meta messages, SysEx bytes |
| Note lifecycle | Repeated Note On, orphan Note Off, dangling notes, held sustain, zero/short notes |
| Source location | Absolute tick plus zero-based track and event indexes |
MIDI Note On with velocity zero is correctly treated as Note Off. SMF type 2 tracks are independent sequences, so their peaks are compared rather than incorrectly summed.
uv run midiconform check assets/theme.mid assets/boss.mid \
--profile targets/handheld.toml \
--format json --output artifacts/midi-conformance.json \
--fail-on warningExit codes are stable:
0: the selected threshold passed;1: a conformance finding met the threshold;2: invocation, profile, filesystem, safety-limit, or parsing failure.
JSON uses schema_version: 1, preserves input order, and sorts findings deterministically. See the
complete profile reference and rule catalog.
Download the wheel from the latest GitHub Release, then install it into a virtual environment:
python -m pip install midiconform-0.1.0-py3-none-any.whl
midiconform --versionMidiConform v0.1.0 is distributed through GitHub Releases. It is not claimed to be published on PyPI.
uv sync --locked --all-extras
uv run python scripts/check.pyThat command regenerates fixtures, checks binary drift, formatting, lint, strict typing, 90% branch coverage, wheel/sdist metadata, dependency advisories, and all three CLI exit paths. If a step fails, run that printed command alone, follow the repair table, then rerun the full gate. Detailed manual acceptance is in docs/acceptance.md.
- MidiConform proves only the declared contract. Wrong or incomplete device limits produce an incomplete conclusion.
- Polyphony is an event/sustain envelope, not a model of sample layers, envelopes, voice stealing, or device-specific synthesis.
- It does not judge musical quality, harmony, timing feel, or instrument choice.
- It never plays or repairs files. In particular, SysEx bytes remain inert data.
- Automatic repair is omitted because changing note timing, programs, or controller events can change musical intent.
The design decision, competitor comparison, and threat boundary are recorded in ADR 0001, research, and the threat model.
See CONTRIBUTING.md for the development gate. Please report parser or crafted-file security issues through the private process in SECURITY.md.
MIT. The code and generated example MIDI files are original project assets; see LICENSE.