Skip to content

Repository files navigation

Tonalis

A generic, format-agnostic music-harmony DSL. Write a chord/harmony lead sheet as plain text and get back a structured AST, a list of lint findings (errors + warnings), a canonical JSON projection of the AST, and a canonical text rendering.

Tonalis is the pure language layer — it parses and validates harmony notation and produces a neutral abstract syntax tree. It is deliberately format-agnostic: it does not emit any vendor file/URL format. Notation-format codecs (e.g. for a specific lead-sheet app) are separate adapters built on top of this core.

The same language is implemented three times — in Python, TypeScript, and Rust — and all three pass two shared, language-agnostic conformance suites: the lead-sheet suite (255 cases under conformance/tonalis/, specified in conformance/tonalis/SPEC.md) and the music-DSL scale/encode suite (429 cases under conformance/music-dsl/, specified in conformance/music-dsl/SPEC-scales.md).

What it does

For each port the public surface is the same:

  • parse DSL text → a LeadSheet AST (sections, measures, cells, repeats/endings, navigation).
  • lint → a list of findings with (code, severity, line); banned constructs produce an error-severity finding.
  • validate a single chord token (is_valid_chord / isValidChord).
  • AST ↔ JSON — a canonical, fixed-key-order JSON projection (ast_to_json / astToJson, ast_from_json / astFromJson).
  • AST → text — a canonical text printer (serialize); parse(serialize(ir)) == ir (modulo source line numbers) is gated by the conformance suite.

Syntax example

title: All The Things You Are
composer: Jerome Kern
key: Ab
time: 4/4

[A]
| F-7 | Bb7 | Eb^7 | Ab^7 |
| D-7 G7 | C^7 | C^7 | C^7 |

A header (key: value per line; required keys title, key, time), then [Section] labels and |-separated measures. Multiple chords in one measure split the bar evenly; :N sets an explicit beat count. { ... } is a repeat, 1./2. mark first/second endings, and @segno / @coda / @tocoda / @fine express navigation. Chord quality uses - (minor), ^ (major-7), o (dim), h (half-dim), + (aug), sus; extensions include b5 #5 6 b9 9 #9 11 #11 b13 13; N.C. is no-chord. The full grammar lives in tonalis/python/tonalis/GRAMMAR.md.

Documentation

Full docs — concept guides, the scale catalog, and generated per-language API references — are built with mkdocs-material from docs/ and deploy to GitHub Pages (.github/workflows/docs.yml).

Build the docs locally:

python -m venv .venv && source .venv/bin/activate
pip install -r docs/requirements.txt
pip install -e ./music-dsl/python   # build_scales.py imports the live scale catalog
python docs/build_scales.py         # regenerate the 39-scale catalog table
bash docs/build_api.sh    # optional: generate the API references (needs pdoc/typedoc/cargo)
mkdocs serve              # or: mkdocs build --strict

Test conventions

Where a new test goes, per port:

  • Python: unit tests under <pkg>/python/tests/unit/ (music-dsl further nests by domain); shared fixtures in tests/fixtures/; conformance runners live under conformance/<pkg>/runners/python.
  • TypeScript: colocated src/*.test.ts next to the module under test.
  • Rust: integration files in <pkg>/rust/tests/ (unit.rs, conformance.rs, plus focused files like two_tier.rs).

The shared conformance corpora (conformance/{music-dsl,tonalis}/cases/) gate all three ports; the seeded differential fuzzer covers the music-dsl surface (the tonalis parser is corpus-gated only — its input space is line-oriented and the 255-case corpus plus port-identical conformance keeps divergence bounded).

Ports — install & test

Python (tonalis/python/)

python -m venv .venv && source .venv/bin/activate
pip install -e "./music-dsl/python[dev]"   # theory library first: tonalis depends on
pip install -e ./tonalis/python            # music-dsl, which isn't on PyPI until publish
pytest tonalis/python/tests             # unit tests + import-boundary guard
pytest conformance/tonalis/runners/python  # the 255-case conformance suite
from tonalis import parse_dsl, lint, serialize, to_json

result = parse_dsl(open("chart.txt").read())
findings = list(result.findings) + (lint(result.chart) if result.chart else [])
chart = result.chart            # a LeadSheet AST
json_obj = to_json(chart)       # canonical JSON (a dict)
text = serialize(chart)         # canonical text rendering

A small CLI is included: python -m tonalis chart.txt prints lint findings (exit 1 if any error).

Note: with your shell at the repo root, a bare python -c "import tonalis" picks up the tonalis/ source directory (a namespace portion) instead of the installed package — run Python from any other directory (or via pytest, which is unaffected).

TypeScript (tonalis/ts/)

cd music-dsl/ts && npm ci && npm run build   # theory lib first: tonalis resolves
cd ../../tonalis/ts                          # @tonalis/music-dsl against its dist/
npm ci
npm test          # units + import-boundary guard + the conformance suite
npm run typecheck
import { parseDsl, lint, isValidChord, astToJson, serialize } from "tonalis";

const result = parseDsl(text);
const findings = [...result.findings, ...(result.chart ? lint(result.chart) : [])];

The TS port's only runtime dependency is @tonalis/music-dsl (linked in-repo via file:, so npm ci needs no registry access; the publish workflow rewrites it to the published version range).

Rust (tonalis/rust/)

cd tonalis/rust
cargo test        # units + import-boundary guard + the conformance suite
use tonalis::{parse_dsl, lint, is_valid_chord, serialize_text};
use tonalis::ast::ast_to_json;

let parsed = parse_dsl(text);
let chart = parsed.chart.unwrap();
let json = ast_to_json(&chart);
let printed = serialize_text(&chart);

The crate is rlib-only (no cdylib / wasm-bindgen target).

License

MIT — see LICENSE.

About

Tonalis — a generic, format-agnostic music-harmony DSL (parse / lint / AST / JSON / text). Python · TypeScript · Rust. MIT.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages