{"id":42184422,"url":"https://github.com/leafo/lml","last_synced_at":"2026-01-26T22:20:15.701Z","repository":{"id":329539530,"uuid":"1119930379","full_name":"leafo/lml","owner":"leafo","description":"leaf's music language","archived":false,"fork":false,"pushed_at":"2026-01-17T04:59:43.000Z","size":352,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-17T17:24:47.957Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leafo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-20T05:59:40.000Z","updated_at":"2026-01-17T04:59:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/leafo/lml","commit_stats":null,"previous_names":["leafo/lml"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/leafo/lml","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leafo%2Flml","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leafo%2Flml/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leafo%2Flml/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leafo%2Flml/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leafo","download_url":"https://codeload.github.com/leafo/lml/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leafo%2Flml/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28789723,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-26T21:49:50.245Z","status":"ssl_error","status_checked_at":"2026-01-26T21:48:29.455Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-26T22:20:12.441Z","updated_at":"2026-01-26T22:20:15.662Z","avatar_url":"https://github.com/leafo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @leafo/lml\n\nLML (Leaf's Music Language) is a text-based notation for writing music. It's designed to be easy to write by hand and parse programmatically.\n\nWARNING: this is work in progress, expect interfaces to change. This was created for use in \u003chttps://sightreading.training\u003e.\n\n## Installation\n\n```bash\nnpm install @leafo/lml\n```\n\n## Usage\n\n```typescript\nimport SongParser from \"@leafo/lml\"\n\n// Parse and compile LML to a song\nconst song = SongParser.load(`\n  ks0 ts4/4\n  c5 d e f\n  g*2 g*2\n`)\n\n// Access the notes\nfor (const note of song) {\n  console.log(`${note.note} at beat ${note.start} for ${note.duration} beats`)\n}\n\n// Access metadata\nconsole.log(song.metadata.keySignature)  // 0\nconsole.log(song.metadata.beatsPerMeasure)  // 4\n```\n\n### Two-phase parsing\n\n```typescript\nimport SongParser from \"@leafo/lml\"\n\nconst parser = new SongParser()\n\n// Phase 1: Parse text to AST\nconst ast = parser.parse(\"c5 d e\")\n// [[\"note\", \"C5\"], [\"note\", \"D\"], [\"note\", \"E\"]]\n\n// Phase 2: Compile AST to song\nconst song = parser.compile(ast)\n```\n\n### Parser Options\n\nBoth `SongParser.load()` and `parser.compile()` accept an options object:\n\n```typescript\nconst song = SongParser.load(\"c d e f g\", {\n  defaultOctave: 4,  // Default octave for relative notes (default: 5)\n})\n```\n\nThis is useful for bass clef or other instruments that typically play in different registers.\n\n## LML Syntax\n\n### Notes\n\nNotes are written as letter names (`a` through `g`) and placed sequentially, separated by whitespace. The octave is automatically determined by finding the closest one to the previous note. The first note defaults to octave 5 (configurable via `defaultOctave` option).\n\n```\nc d e f g a b c    # c5 d5 e5 f5 g5 a5 b5 c6\n```\n\nThe algorithm picks the octave that minimizes the distance in semitones:\n\n```\nc d e f g a b c    # Ascending: c5 → c6\nc6 b a g f e d c   # Descending: c6 → c5\n```\n\nDuration can be modified with `*` (multiply) or `/` (divide). The default duration is 1 beat.\n\n```\nc*2 d d e*4        # c is 2 beats, d is 1 beat each, e is 4 beats\nc/2 d/2 e/4        # c and d are 0.5 beats, e is 0.25 beats\n```\n\nDotted notes use `.` after the duration. A dot adds half the note's value (1.5x), double-dot adds 1.75x, etc.\n\n```\nc.                 # dotted quarter: 1.5 beats\nc..                # double-dotted quarter: 1.75 beats\nc*2.               # dotted half: 3 beats (2 * 1.5)\nc/2.               # dotted eighth: 0.75 beats (0.5 * 1.5)\n```\n\nAn explicit start position can be specified with `@` followed by the beat number. This places notes at absolute positions rather than sequentially.\n\n```\nc@0 d@4 e@8        # Notes at beats 0, 4, and 8\nc*2@0 d*2@2        # Duration 2, at beats 0 and 2\n```\n\nNotes can be made sharp with `+`, flat with `-`, or natural with `=`:\n\n```\nc c+ d- e          # c c# db e\n```\n\n### Explicit Octaves\n\nWhen you need precise control, add an octave number (0-9) after the note name:\n\n```\nc5 d5 e5           # Explicit octaves\nc3 d e f           # Start at c3, then continue relatively: c3 d3 e3 f3\ng4 c               # Jump to g4, then c5 (closest to g4)\n```\n\nThis is useful for:\n- Setting the starting octave\n- Jumping to a different register\n- Writing music that spans multiple octaves\n\n```\n# Two octave arpeggio\nc4 e g c5 e g c6\n\n# Jump between registers\nc5 d e g3 a b\n```\n\n### Rests\n\nInsert silence using the rest command `r`, with the same duration modifiers as notes. Rests can use `*N` to multiply, `/N` to divide, `.` for dotting, and `@` for explicit positioning.\n\n```\nc r d*2\nd r2 a\nr/2         # Half-beat rest (0.5 beats)\nr/4         # Quarter-beat rest (0.25 beats)\nr@4         # Rest at beat 4\nr2@4        # Rest with duration 2 at beat 4\nr.          # Dotted rest: 1.5 beats\nr2.         # Dotted rest with duration: 3 beats\nr/2.        # Dotted half-beat rest: 0.75 beats\n```\n\n### Time Commands\n\nChange the base duration using time commands. These take effect until the end of the song or block.\n\n- `dt` — Double time (notes become half as long)\n- `ht` — Half time (notes become twice as long)\n- `tt` — Triple time (notes become one-third as long)\n\n```\ndt\nc d c d c d e*2\n```\n\nTime commands stack when repeated. You can also add a number to apply the effect multiple times:\n\n```\ndt dt c d    # Each note is 0.25 beats\ndt2 c d      # Same as above\nht3 c        # Note is 8 beats (2^3)\n```\n\n### Position Restore\n\nMove the position back to the start using `|`. This is useful for writing chords or multiple voices.\n\n```\nc5 | e | g     # C major chord (c5 e5 g5)\n```\n\nTwo voices:\n\n```\n| c5 g e*2\n| c4*2 f*2\n```\n\n### Blocks\n\nBlocks are delimited with `{` and `}`. They affect how commands work:\n\n- `|` moves position back to the start of the block\n- Time commands (`dt`, `ht`, `tt`) reset after the block\n- Track selection resets after the block\n\n```\n{\n  dt\n  c5 { dt e f } d*2 e g a c\n}\n|\n{ ht g4 f }\n```\n\n### Measures\n\nThe `m` command moves the position to the start of a measure boundary, useful for aligning notes. Measure boundaries are determined by the current time signature. Use `m` alone to auto-increment to the next measure, or `m0`, `m1`, etc. for explicit positioning:\n\n```\nm {\n  | c5 c a g\n  | g4*4\n}\n\nm {\n  | d5 d a e\n  | f4*4\n}\n```\n\nThe first `m` goes to measure 0, then each subsequent `m` increments. Explicit measure numbers also update the counter:\n\n```\nm { c d }      # measure 0\nm { e f }      # measure 1\nm5 { g a }     # measure 5\nm { b c }      # measure 6\n```\n\n### Key Signature\n\nSet the key signature with `ks` followed by the number of sharps (positive) or flats (negative). Notes are automatically adjusted to match the key.\n\n```\nks2          # D major (2 sharps: F#, C#)\nc d e f      # F becomes F#, C becomes C#\n```\n\nUse `=` to override the key signature with a natural:\n\n```\nks-2\nb c b=       # B natural\n```\n\n### Time Signature\n\nSet the time signature with `ts`:\n\n```\nts3/4\nc d e\n```\n\nThis affects beats per measure and where measure lines appear.\n\nTime signatures can change mid-song. Notes are placed sequentially regardless of time signature changes:\n\n```\nts4/4\nc d e f      # 4 beats in 4/4\nts3/4\ng a b        # 3 beats in 3/4\nts4/4\nc d e f      # 4 beats in 4/4\n```\n\nTime signature changes are tracked and accessible via `song.timeSignatures`:\n\n```typescript\nconst song = SongParser.load(\"ts3/4 c d e ts4/4 f g a b\")\nconsole.log(song.timeSignatures)\n// [[0, 3], [3, 4]]  // [beat_position, beats_per_measure]\n```\n\n### Measures API\n\nMeasures are implicitly created based on the time signature and the duration of notes in the song. Use `getMeasures()` to get an array of measure boundaries, useful for drawing grid lines or measure markers:\n\n```typescript\nconst song = SongParser.load(\"c5 d e f g a b c\")  // 8 beats in 4/4\nconst measures = song.getMeasures()\n// [{ start: 0, beats: 4 }, { start: 4, beats: 4 }]\n```\n\nThis correctly handles time signature changes:\n\n```typescript\nconst song = SongParser.load(`\n  ts4/4 c d e f    # 4 beats\n  ts3/4 g a b      # 3 beats\n  ts4/4 c d e f    # 4 beats\n`)\nconst measures = song.getMeasures()\n// [\n//   { start: 0, beats: 4 },\n//   { start: 4, beats: 3 },\n//   { start: 7, beats: 4 }\n// ]\n```\n\nEach measure object contains:\n- `start`: Beat position where the measure begins\n- `beats`: Number of beats in this measure\n\nNote: The `m` command (see [Measures](#measures)) is used to align notes to measure boundaries during composition, but is not required—`getMeasures()` computes measure boundaries from the time signature regardless of whether `m` was used.\n\n### Chords\n\nThe `$` command specifies a chord symbol for auto-chord generation:\n\n```\n{$G c5*2 a d}\n{$Dm e f g*2}\n```\n\nSupported chord types: `M`, `m`, `dim`, `dim7`, `dimM7`, `aug`, `augM7`, `M6`, `m6`, `M7`, `7`, `m7`, `m7b5`, `mM7`\n\n### Tracks\n\nSongs can have multiple tracks, numbered starting from 0. Use `t` to switch tracks:\n\n```\nt0 c5 d e\nt1 g3 g g\n```\n\n### Clefs\n\nSet the clef with `/g` (treble), `/f` (bass), or `/c` (alto):\n\n```\n/g c5 d e\n/f c3 d e\n```\n\nClefs are stored as track metadata, not on individual notes. They hint to renderers which staff to use for displaying the track. Each track supports a single clef assignment. When no clef is specified, the staff is auto-detected based on the note range:\n\n- Notes primarily above middle C → treble staff\n- Notes primarily below middle C → bass staff\n- Notes spanning both ranges → grand staff (treble + bass)\n\nClefs are accessible via `track.clefs`:\n\n```typescript\nconst song = SongParser.load(\"/f c3 d e\")\nconsole.log(song.tracks[0].clefs)\n// [[0, \"f\"]]  // [position, clefType]\n```\n\n### Strings\n\nQuoted strings can be placed anywhere in a song. They are tagged with their position in beats and stored separately from notes. This is useful for lyrics or annotations.\n\n```\nc \"hel\" d \"lo\" e \"world\"\n```\n\nBoth single and double quotes are supported. Strings can span multiple lines:\n\n```\nc*2 'First verse\ncontinues here' d*2\n```\n\nEscape sequences are supported: `\\\"`, `\\'`, `\\\\`, `\\n`.\n\n```\n\"say \\\"hello\\\"\"\n'it\\'s working'\n```\n\nStrings are accessible via `song.strings`:\n\n```typescript\nconst song = SongParser.load('c \"la\" d \"la\"')\nconsole.log(song.strings)\n// [[1, \"la\"], [2, \"la\"]]  // [position, text]\n```\n\n### Comments\n\nText after `#` is ignored:\n\n```\nc d    # this is a comment\n# full line comment\ne f\n```\n\n### Frontmatter\n\nMetadata can be embedded at the start of a file using comment-style frontmatter. Lines matching `# key: value` at the very beginning (before any commands) are parsed as metadata:\n\n```\n# title: Moonlight Sonata\n# author: Beethoven\n# bpm: 120\n# difficulty: intermediate\n\nts4/4 ks-3\nc d e f\n```\n\nThe convention is to use lowercase key names. Frontmatter is accessible via `song.metadata.frontmatter`:\n\n```typescript\nconst song = SongParser.load(`\n# title: My Song\n# bpm: 90\nc d e\n`)\n\nconsole.log(song.metadata.frontmatter)\n// { title: \"My Song\", bpm: \"90\" }\n```\n\nNotes:\n- Frontmatter must appear at the start of the file, before any music commands\n- Keys are case-sensitive\n- All values are stored as strings\n- Once a non-frontmatter line is encountered, subsequent `# key: value` lines are treated as regular comments\n\n## Music Theory Utilities\n\nThe library also exports music theory utilities:\n\n```typescript\nimport {\n  parseNote,\n  noteName,\n  Chord,\n  MajorScale,\n  MinorScale,\n  KeySignature,\n} from \"@leafo/lml\"\n\n// Note conversion\nparseNote(\"C5\")  // 60 (MIDI pitch)\nnoteName(60)     // \"C5\"\n\n// Scales\nconst scale = new MajorScale(\"C\")\nscale.getRange(5, 8)  // [\"C5\", \"D5\", \"E5\", \"F5\", \"G5\", \"A5\", \"B5\", \"C6\"]\n\n// Chords\nconst chord = new Chord(\"C\", \"M\")\nchord.getRange(5, 3)  // [\"C5\", \"E5\", \"G5\"]\n\n// Key signatures\nconst key = new KeySignature(2)  // D major\nkey.name()  // \"D\"\nkey.accidentalNotes()  // [\"F\", \"C\"]\n```\n\n## Limitations\n\nCurrent limitations that may be addressed in future versions:\n\n- **Clefs are per-track**: Each track supports only a single clef. Mid-track clef changes are not supported.\n\n- **Key signature metadata is global**: While key signatures (`ks`) can change mid-song and correctly affect note parsing, the metadata only stores the final value. Renderers cannot determine where key signature changes occur within the song. (Time signature changes are tracked via `song.timeSignatures` and `song.getMeasures()`.)\n\n- **Time signature changes should be placed at measure boundaries**: Time signatures are recorded at the cursor position when parsed. When combined with measure markers (`m`) and notes that extend past measure boundaries, the recorded position may not align with where the measure actually starts. For predictable behavior, place time signature changes immediately after a measure marker (which positions the cursor at the boundary):\n\n```\n# Recommended: time signature is always applied at start of measure\nm ts4/4 c d e f\nm ts3/4 g a b\n\n# Although you can also write it before the m command, if the previous measure\n# accidentally pushed the cursor into the next measure then the time signature\n# application will be delayed a measure:\n\nts3/4\nm g*4       # extends 1 beat past 3-beat measure\nts4/4       # recorded at beat 4, but next measure starts at beat 3\nm a b c d\n\n```\n\n- **No explicit grand staff**: Grand staff is only available through auto-detection when notes span both treble and bass registers. There is no syntax to explicitly request a grand staff.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleafo%2Flml","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleafo%2Flml","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleafo%2Flml/lists"}