Tonalis¶
Parse a chord chart into a structured AST — in Python, TypeScript, or Rust, with byte-for-byte identical results. Tonalis is a spec-driven, polyglot music-theory library: one normative conformance suite, three ports kept honest by a three-way differential fuzzer.
Quickstart Lead-sheet DSL View on GitHub
Write a harmony chart as plain text:
…and get back a canonical AST, a list of lint findings, a lossless JSON projection, and a canonical text rendering — the same answers whichever port you call. See it end to end in the worked example.
Under it sits the theory core's showpiece — key-relative chords. Write harmony once as roman numerals and realize it into any key:
ii = NumericChord.from_chord_string("ii-7")
ii.in_key(Notes.C) # -> D-7
ii.in_key(Notes.Eb) # -> F-7
Two layers¶
-
music_dsl— the theory core
Structured chords and, its centerpiece, numeric (key-relative) chords — a roman numeral that realizes to the right absolute chord in any key. Plus scale degrees, notes/intervals, and a 39-scale catalog as supporting tools.
-
tonalis— the lead-sheet language
A generic, format-agnostic chord-chart language. Pure language layer — it emits no vendor file or URL format; notation-app codecs are separate adapters built on this neutral core.
Why it is built this way¶
The design rule is one spec, three ports, no drift.
-
One normative conformance suite
The contract is human-readable specs (lead-sheet, scales) backed by a machine-checkable case corpus. Cases are blessed from the Python reference — that's what's authoritative, not any single port's code.
-
Three implementations, identical behavior
Python (reference), TypeScript, and Rust each implement the same public surface and pass the same 255-case lead-sheet suite plus the theory conformance cases.
-
A differential fuzzer keeps them honest
A seeded Python↔TS↔Rust fuzzer (
make fuzz) drives the full chord-suffix grammar and the scale surface, and requires 0 divergences across all three ports. -
Pick the port that fits your stack
The TS port has zero runtime dependencies and is browser- and Node-safe; the Rust crate is
rlib-only (no cdylib / wasm-bindgen target). Same answers either way.
What you get¶
- Parse DSL text → a
LeadSheetAST (sections, measures, cells, repeats/endings, navigation). - Lint → findings with
(code, severity, line); banned constructs produce anerror-severity finding. - Validate a single chord token (
is_valid_chord/isValidChord). - AST ↔ JSON — a canonical, fixed-key-order projection that round-trips losslessly.
- AST → text — a canonical printer;
parse(serialize(ir)) == ir(modulo source line numbers), gated by conformance. - Theory model — notes, intervals, chords, scale degrees, key-relative
NumericChords, and the 39-scale catalog.
Where to go next¶
- Quickstart — install and a first runnable example, in all three ports.
- Lead-sheet DSL — the chart language, chord grammar, and the chart→AST worked example.
- Theory model — chords and key-relative numeric chords (the centerpiece), plus scale degrees and notes.
- Scale catalog — all 39 scales and the two-tier diatonicity rules.
- API reference — generated per-language API docs.
License¶
MIT.