syrinx (0.1.0)
Installation
pip install --index-url --extra-index-url https://pypi.org/simple syrinxAbout this package
Structured Python wrapper around abc2midi — convert ABC notation to MIDI with phase-classified diagnostics.
Syrinx
In Greek mythology, Syrinx was a nymph transformed into hollow reeds — from which Pan fashioned the first pipes, giving her voice to music. ABC notation goes in; MIDI comes out.
Syrinx is a Python wrapper around abc2midi, the reference implementation of ABC-to-MIDI conversion. It bundles a lightly-patched build of abc2midi and gives it a clean, Pythonic interface with structured, phase-classified error handling — so a conversion failure tells you whether the ABC is wrong or whether the converter could not render it, with the exact line and character to look at.
Why ABC → MIDI (and not MIDI directly)
The reason to route through ABC at all is that having an LLM generate MIDI directly is painfully slow, and ABC fixes that at the root.
LLM latency is dominated by the number of output tokens (generation is sequential). A bar of music in ABC —
e2 ^d2 e2 ^d2 e2 B2 d2 c2 A4 z2
— is about 15–20 tokens. The same bar as LLM-emitted MIDI is hundreds: a model cannot emit raw bytes, so it produces text like {"type":"note_on","note":64,"velocity":95,"time":0.5} — two events per note, each carrying delta-time and velocity bookkeeping. The ratio is roughly 20–40× fewer output tokens, and therefore proportionally faster generation, for the same music.
That single fact cascades into three wins:
- Speed. ~20–40× less to generate per piece.
- Composition, not data-entry. In MIDI form the model spends most of its budget on mechanical bookkeeping (paired note-offs, delta-times, velocity numbers) where mistakes are silent and ugly — a missing note-off is a stuck note. ABC moves that mechanical expansion into deterministic C (abc2midi). The model spends tokens on pitch, rhythm, dynamics and structure. ABC encodes intent — "transpose a fifth", "staccato this bar", "second ending" — that a single directive ripples across dozens of MIDI events.
- Iteration and correctability. Revisions are small text edits — diffable and cheap — versus regenerating an opaque event stream. And because a real parser sits in the path, bad input is catchable: Syrinx reports
line 3:2 [parse] Attempt to nest chords. Direct-MIDI generation has no such feedback loop. Syrinx exists to make that loop usable.
The trade-off: ABC is more constrained than raw MIDI (no micro-timing, continuous controllers, per-note pitch bend, or non-Western tunings). For composed instrumental music — the case Syrinx and abc2midi were built for — that ceiling sits far above anything you are likely to need.
Why not music21
The Python ecosystem's usual ABC parser, music21, treats ABC's performance directives as notation metadata rather than applying them to the MIDI. Verified differences (Syrinx decodes the resulting MIDI to confirm):
| Feature | music21 | abc2midi (Syrinx) |
|---|---|---|
Dynamics !p! !f! !ff! → velocity |
parsed but never applied | real velocities (e.g. 50 / 95 / 110) |
Crescendo/diminuendo !<(! … !<)! |
ignored | interpolated velocity ramp |
Multi-voice with repeated M: |
StreamException on MIDI write |
clean format-1 MIDI, one track per voice |
Staccato . |
duration unchanged | shortens MIDI gate time |
Guitar-chord accompaniment (%%MIDI gchord) |
not supported | full bass + chord track |
abc2midi is the reference implementation; these are solved problems there. Syrinx does not reimplement any music logic — it makes abc2midi behave like a library.
Requirements
- Python 3.11+
- A C compiler + CMake (only at build time; the prebuilt wheel bundles the binary)
abc2midi itself is bundled and compiled into the wheel — there is no separate system abc2midi dependency, and a prebuilt wheel needs no compiler at install time.
Installation
pip install syrinx
Quick Start
from syrinx import convert
abc = """
X:1
T:Für Elise
C:Ludwig van Beethoven
M:3/8
L:1/16
Q:3/8=60
K:Am
e2 ^d2 e2 ^d2 e2 B2 d2 c2 A4 z2 C2 E2 A2 B4 z2 E2 c2 B2 A6
"""
result = convert(abc)
with open("fur_elise.mid", "wb") as f:
f.write(result.bytes)
Error Handling
Syrinx splits failures by the phase abc2midi was in when it detected the problem, so callers can respond correctly:
from syrinx import convert
from syrinx.exceptions import (
Abc2MidiNotFoundError, # bundled binary missing
AbcSyntaxError, # the ABC is malformed — author should fix it
ConversionError, # abc2midi could not render it — a converter limit/bug
EmptyOutputError, # exited cleanly but produced no MIDI
ConversionTimeoutError, # conversion exceeded the timeout
)
try:
result = convert(abc, timeout=10)
except AbcSyntaxError as e:
# The note text is wrong. Point the author at the exact spot.
for d in e.errors:
print(f"line {d.line}:{d.char}: {d.message}")
except ConversionError as e:
# abc2midi parsed the music but couldn't render it (e.g. MIDI channel
# exhaustion, a bad %%MIDI directive). Not a syntax mistake.
for d in e.errors:
print(f"converter limit at {d.line}:{d.char}: {d.message}")
Every diagnostic is a Diagnostic with .severity, .phase ("parse" or "generation"), .line, .char, .message, and .raw.
Warnings
Successful conversions may still produce warnings. By default (strict=True) any warning is raised. Pass strict=False to receive them alongside a successful result:
result = convert(abc, strict=False)
for warning in result.warnings:
print(f"line {warning.line}:{warning.char}: {warning.message}")
midi_bytes = result.bytes
Options
| Parameter | Type | Default | Description |
|---|---|---|---|
tempo |
int |
None |
Override tempo in BPM (abc2midi -Q) |
transpose |
int |
None |
Semitones to transpose (injects %%MIDI transpose) |
strict |
bool |
True |
Raise on warnings |
timeout |
float |
30.0 |
Seconds before timeout |
timeoutguards against a conversion that never finishes: if abc2midi runs longer thantimeoutseconds it is aborted andConversionTimeoutErroris raised. In normal use a conversion completes in milliseconds, so this only fires on a pathological input. (The test suite covers this path by simulating a slow subprocess rather than relying on real wall-clock timing, which is machine-speed-dependent.)
Dynamics and per-voice instruments are controlled from within the ABC (e.g.
!f!,%%MIDI program), not via these options — that is how abc2midi works and is the point of using it.
Relationship to Orpheus
Syrinx was extracted from Orpheus, an MCP server for piano playback. There, an LLM composes in ABC and Orpheus plays it on the piano; a conversion problem that disappears into a subprocess means a song that simply doesn't play right, with no indication why. Syrinx exists to make those failures structured and attributable, and to render expression (velocity, articulation, accompaniment) that music21 drops.
It is packaged standalone because a reliable, well-behaved Python interface to abc2midi is useful beyond any single project.
How it works
Syrinx bundles abc2midi's C sources (pinned to a specific upstream commit), patched only to add an opt-in -struct mode that emits machine-readable, phase-tagged diagnostics to stderr instead of free text on stdout. The default abc2midi behaviour is unchanged, so the patch is upstream-friendly. See syrinx/_abc2midi/PATCH.md.
The Python layer writes the ABC to a temp file, runs the binary (-struct -silent, output to a temp .mid), parses the structured diagnostics, classifies them by phase, and returns the MIDI bytes or raises the appropriate exception. Temp files are always cleaned up.
Licence
GPLv2-or-later. abc2midi is GPL and is compiled into the distributed wheel, so the combined work is GPL. See LICENSE.