Metadata-Version: 2.2
Name: syrinx
Version: 0.1.0
Summary: Structured Python wrapper around abc2midi — convert ABC notation to MIDI with phase-classified diagnostics.
Keywords: abc,midi,music,notation,abc2midi
Author-Email: Robert Helewka <r@helu.ca>
License: GPL-2.0-or-later
Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
Project-URL: Homepage, https://github.com/rhelu/syrinx
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Description-Content-Type: text/markdown

# 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](https://abcmidi.sourceforge.io/), 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:

1. **Speed.** ~20–40× less to generate per piece.
2. **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.
3. **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

```bash
pip install syrinx
```

---

## Quick Start

```python
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:

```python
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:

```python
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 |

> 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](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](LICENSE).
