npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@leafo/lml

v0.3.0

Published

LML (Leaf's Music Language) parser and music theory library

Readme

@leafo/lml

LML (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.

WARNING: this is work in progress, expect interfaces to change. This was created for use in https://sightreading.training.

Installation

npm install @leafo/lml

Usage

import SongParser from "@leafo/lml"

// Parse and compile LML to a song
const song = SongParser.load(`
  ks0 ts4/4
  c5 d e f
  g*2 g*2
`)

// Access the notes
for (const note of song) {
  console.log(`${note.note} at beat ${note.start} for ${note.duration} beats`)
}

// Access metadata
console.log(song.metadata.keySignature)  // 0
console.log(song.metadata.beatsPerMeasure)  // 4

Two-phase parsing

import SongParser from "@leafo/lml"

const parser = new SongParser()

// Phase 1: Parse text to AST
const ast = parser.parse("c5 d e")
// [["note", "C5"], ["note", "D"], ["note", "E"]]

// Phase 2: Compile AST to song
const song = parser.compile(ast)

Parser Options

Both SongParser.load() and parser.compile() accept an options object:

const song = SongParser.load("c d e f g", {
  defaultOctave: 4,  // Default octave for relative notes (default: 5)
})

This is useful for bass clef or other instruments that typically play in different registers.

LML Syntax

Notes

Notes 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).

c d e f g a b c    # c5 d5 e5 f5 g5 a5 b5 c6

The algorithm picks the octave that minimizes the distance in semitones:

c d e f g a b c    # Ascending: c5 → c6
c6 b a g f e d c   # Descending: c6 → c5

Duration can be modified with * (multiply) or / (divide). The default duration is 1 beat.

c*2 d d e*4        # c is 2 beats, d is 1 beat each, e is 4 beats
c/2 d/2 e/4        # c and d are 0.5 beats, e is 0.25 beats

Dotted notes use . after the duration. A dot adds half the note's value (1.5x), double-dot adds 1.75x, etc.

c.                 # dotted quarter: 1.5 beats
c..                # double-dotted quarter: 1.75 beats
c*2.               # dotted half: 3 beats (2 * 1.5)
c/2.               # dotted eighth: 0.75 beats (0.5 * 1.5)

An explicit start position can be specified with @ followed by the beat number. This places notes at absolute positions rather than sequentially.

c@0 d@4 e@8        # Notes at beats 0, 4, and 8
c*2@0 d*2@2        # Duration 2, at beats 0 and 2

Notes can be made sharp with +, flat with -, or natural with =:

c c+ d- e          # c c# db e

Explicit Octaves

When you need precise control, add an octave number (0-9) after the note name:

c5 d5 e5           # Explicit octaves
c3 d e f           # Start at c3, then continue relatively: c3 d3 e3 f3
g4 c               # Jump to g4, then c5 (closest to g4)

This is useful for:

  • Setting the starting octave
  • Jumping to a different register
  • Writing music that spans multiple octaves
# Two octave arpeggio
c4 e g c5 e g c6

# Jump between registers
c5 d e g3 a b

Rests

Insert 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.

c r d*2
d r2 a
r/2         # Half-beat rest (0.5 beats)
r/4         # Quarter-beat rest (0.25 beats)
r@4         # Rest at beat 4
r2@4        # Rest with duration 2 at beat 4
r.          # Dotted rest: 1.5 beats
r2.         # Dotted rest with duration: 3 beats
r/2.        # Dotted half-beat rest: 0.75 beats

Time Commands

Change the base duration using time commands. These take effect until the end of the song or block.

  • dt — Double time (notes become half as long)
  • ht — Half time (notes become twice as long)
  • tt — Triple time (notes become one-third as long)
dt
c d c d c d e*2

Time commands stack when repeated. You can also add a number to apply the effect multiple times:

dt dt c d    # Each note is 0.25 beats
dt2 c d      # Same as above
ht3 c        # Note is 8 beats (2^3)

Position Restore

Move the position back to the start using |. This is useful for writing chords or multiple voices.

c5 | e | g     # C major chord (c5 e5 g5)

Two voices:

| c5 g e*2
| c4*2 f*2

Blocks

Blocks are delimited with { and }. They affect how commands work:

  • | moves position back to the start of the block
  • Time commands (dt, ht, tt) reset after the block
  • Track selection resets after the block
{
  dt
  c5 { dt e f } d*2 e g a c
}
|
{ ht g4 f }

Measures

The 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:

m {
  | c5 c a g
  | g4*4
}

m {
  | d5 d a e
  | f4*4
}

The first m goes to measure 0, then each subsequent m increments. Explicit measure numbers also update the counter:

m { c d }      # measure 0
m { e f }      # measure 1
m5 { g a }     # measure 5
m { b c }      # measure 6

Key Signature

Set the key signature with ks followed by the number of sharps (positive) or flats (negative). Notes are automatically adjusted to match the key.

ks2          # D major (2 sharps: F#, C#)
c d e f      # F becomes F#, C becomes C#

Use = to override the key signature with a natural:

ks-2
b c b=       # B natural

Time Signature

Set the time signature with ts:

ts3/4
c d e

This affects beats per measure and where measure lines appear.

Time signatures can change mid-song. Notes are placed sequentially regardless of time signature changes:

ts4/4
c d e f      # 4 beats in 4/4
ts3/4
g a b        # 3 beats in 3/4
ts4/4
c d e f      # 4 beats in 4/4

Time signature changes are tracked and accessible via song.timeSignatures:

const song = SongParser.load("ts3/4 c d e ts4/4 f g a b")
console.log(song.timeSignatures)
// [[0, 3], [3, 4]]  // [beat_position, beats_per_measure]

Measures API

Measures 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:

const song = SongParser.load("c5 d e f g a b c")  // 8 beats in 4/4
const measures = song.getMeasures()
// [{ start: 0, beats: 4 }, { start: 4, beats: 4 }]

This correctly handles time signature changes:

const song = SongParser.load(`
  ts4/4 c d e f    # 4 beats
  ts3/4 g a b      # 3 beats
  ts4/4 c d e f    # 4 beats
`)
const measures = song.getMeasures()
// [
//   { start: 0, beats: 4 },
//   { start: 4, beats: 3 },
//   { start: 7, beats: 4 }
// ]

Each measure object contains:

  • start: Beat position where the measure begins
  • beats: Number of beats in this measure

Note: The m command (see 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.

Chords

The $ command specifies a chord symbol for auto-chord generation:

{$G c5*2 a d}
{$Dm e f g*2}

Supported chord types: M, m, dim, dim7, dimM7, aug, augM7, M6, m6, M7, 7, m7, m7b5, mM7

Tracks

Songs can have multiple tracks, numbered starting from 0. Use t to switch tracks:

t0 c5 d e
t1 g3 g g

Clefs

Set the clef with /g (treble), /f (bass), or /c (alto):

/g c5 d e
/f c3 d e

Clefs 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:

  • Notes primarily above middle C → treble staff
  • Notes primarily below middle C → bass staff
  • Notes spanning both ranges → grand staff (treble + bass)

Clefs are accessible via track.clefs:

const song = SongParser.load("/f c3 d e")
console.log(song.tracks[0].clefs)
// [[0, "f"]]  // [position, clefType]

Strings

Quoted 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.

c "hel" d "lo" e "world"

Both single and double quotes are supported. Strings can span multiple lines:

c*2 'First verse
continues here' d*2

Escape sequences are supported: \", \', \\, \n.

"say \"hello\""
'it\'s working'

Strings are accessible via song.strings:

const song = SongParser.load('c "la" d "la"')
console.log(song.strings)
// [[1, "la"], [2, "la"]]  // [position, text]

Comments

Text after # is ignored:

c d    # this is a comment
# full line comment
e f

Frontmatter

Metadata 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:

# title: Moonlight Sonata
# author: Beethoven
# bpm: 120
# difficulty: intermediate

ts4/4 ks-3
c d e f

The convention is to use lowercase key names. Frontmatter is accessible via song.metadata.frontmatter:

const song = SongParser.load(`
# title: My Song
# bpm: 90
c d e
`)

console.log(song.metadata.frontmatter)
// { title: "My Song", bpm: "90" }

Notes:

  • Frontmatter must appear at the start of the file, before any music commands
  • Keys are case-sensitive
  • All values are stored as strings
  • Once a non-frontmatter line is encountered, subsequent # key: value lines are treated as regular comments

Music Theory Utilities

The library also exports music theory utilities:

import {
  parseNote,
  noteName,
  Chord,
  MajorScale,
  MinorScale,
  KeySignature,
} from "@leafo/lml"

// Note conversion
parseNote("C5")  // 60 (MIDI pitch)
noteName(60)     // "C5"

// Scales
const scale = new MajorScale("C")
scale.getRange(5, 8)  // ["C5", "D5", "E5", "F5", "G5", "A5", "B5", "C6"]

// Chords
const chord = new Chord("C", "M")
chord.getRange(5, 3)  // ["C5", "E5", "G5"]

// Key signatures
const key = new KeySignature(2)  // D major
key.name()  // "D"
key.accidentalNotes()  // ["F", "C"]

Limitations

Current limitations that may be addressed in future versions:

  • Clefs are per-track: Each track supports only a single clef. Mid-track clef changes are not supported.

  • 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().)

  • 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):

# Recommended: time signature is always applied at start of measure
m ts4/4 c d e f
m ts3/4 g a b

# Although you can also write it before the m command, if the previous measure
# accidentally pushed the cursor into the next measure then the time signature
# application will be delayed a measure:

ts3/4
m g*4       # extends 1 beat past 3-beat measure
ts4/4       # recorded at beat 4, but next measure starts at beat 3
m a b c d
  • 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.

License

MIT