@leafo/lml 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,8 +18,8 @@ import SongParser from "@leafo/lml"
18
18
  // Parse and compile LML to a song
19
19
  const song = SongParser.load(`
20
20
  ks0 ts4/4
21
- c5 d5 e5 f5
22
- g5.2 g5.2
21
+ c5 d e f
22
+ g.2 g.2
23
23
  `)
24
24
 
25
25
  // Access the notes
@@ -40,42 +40,94 @@ import SongParser from "@leafo/lml"
40
40
  const parser = new SongParser()
41
41
 
42
42
  // Phase 1: Parse text to AST
43
- const ast = parser.parse("c5 d5 e5")
44
- // [["note", "C5"], ["note", "D5"], ["note", "E5"]]
43
+ const ast = parser.parse("c5 d e")
44
+ // [["note", "C5"], ["note", "D"], ["note", "E"]]
45
45
 
46
46
  // Phase 2: Compile AST to song
47
47
  const song = parser.compile(ast)
48
48
  ```
49
49
 
50
+ ### Parser Options
51
+
52
+ Both `SongParser.load()` and `parser.compile()` accept an options object:
53
+
54
+ ```typescript
55
+ const song = SongParser.load("c d e f g", {
56
+ defaultOctave: 4, // Default octave for relative notes (default: 5)
57
+ })
58
+ ```
59
+
60
+ This is useful for bass clef or other instruments that typically play in different registers.
61
+
50
62
  ## LML Syntax
51
63
 
52
64
  ### Notes
53
65
 
54
- A note is written as the note name followed by the octave. Notes are placed sequentially and must be separated by whitespace.
66
+ 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).
55
67
 
56
68
  ```
57
- c5 d5 e5
69
+ c d e f g a b c # c5 d5 e5 f5 g5 a5 b5 c6
58
70
  ```
59
71
 
60
- A duration multiplier can be specified by appending a period and a number. The default duration is 1 beat.
72
+ The algorithm picks the octave that minimizes the distance in semitones:
61
73
 
62
74
  ```
63
- c5.2 d5 d5 e5.4
75
+ c d e f g a b c # Ascending: c5 c6
76
+ c6 b a g f e d c # Descending: c6 → c5
64
77
  ```
65
78
 
66
- Notes can be made sharp with `+`, flat with `-`, or natural with `=`. These modifiers appear after the note name but before the octave.
79
+ Duration can be modified with `.` (multiply) or `/` (divide). The default duration is 1 beat.
67
80
 
68
81
  ```
69
- c+5 c-5 b=4
82
+ c.2 d d e.4 # c is 2 beats, d is 1 beat each, e is 4 beats
83
+ c/2 d/2 e/4 # c and d are 0.5 beats, e is 0.25 beats
84
+ ```
85
+
86
+ An explicit start position can be specified with `@` followed by the beat number. This places notes at absolute positions rather than sequentially.
87
+
88
+ ```
89
+ c@0 d@4 e@8 # Notes at beats 0, 4, and 8
90
+ c.2@0 d.2@2 # Duration 2, at beats 0 and 2
91
+ ```
92
+
93
+ Notes can be made sharp with `+`, flat with `-`, or natural with `=`:
94
+
95
+ ```
96
+ c c+ d- e # c c# db e
97
+ ```
98
+
99
+ ### Explicit Octaves
100
+
101
+ When you need precise control, add an octave number (0-9) after the note name:
102
+
103
+ ```
104
+ c5 d5 e5 # Explicit octaves
105
+ c3 d e f # Start at c3, then continue relatively: c3 d3 e3 f3
106
+ g4 c # Jump to g4, then c5 (closest to g4)
107
+ ```
108
+
109
+ This is useful for:
110
+ - Setting the starting octave
111
+ - Jumping to a different register
112
+ - Writing music that spans multiple octaves
113
+
114
+ ```
115
+ # Two octave arpeggio
116
+ c4 e g c5 e g c6
117
+
118
+ # Jump between registers
119
+ c5 d e g3 a b
70
120
  ```
71
121
 
72
122
  ### Rests
73
123
 
74
- Insert silence using the rest command `r`, optionally with a duration multiplier.
124
+ Insert silence using the rest command `r`, optionally with a duration multiplier. Like notes, rests can also use `@` for explicit positioning.
75
125
 
76
126
  ```
77
- c5 r d5.2
78
- d5 r2 a4
127
+ c r d.2
128
+ d r2 a
129
+ r@4 # Rest at beat 4
130
+ r2@4 # Rest with duration 2 at beat 4
79
131
  ```
80
132
 
81
133
  ### Time Commands
@@ -88,13 +140,15 @@ Change the base duration using time commands. These take effect until the end of
88
140
 
89
141
  ```
90
142
  dt
91
- c5 d5 c5 d5 c5 d5 e5.2
143
+ c d c d c d e.2
92
144
  ```
93
145
 
94
- Time commands stack when repeated:
146
+ Time commands stack when repeated. You can also add a number to apply the effect multiple times:
95
147
 
96
148
  ```
97
- dt dt c5 d5 # Each note is 0.25 beats
149
+ dt dt c d # Each note is 0.25 beats
150
+ dt2 c d # Same as above
151
+ ht3 c # Note is 8 beats (2^3)
98
152
  ```
99
153
 
100
154
  ### Position Restore
@@ -102,14 +156,14 @@ dt dt c5 d5 # Each note is 0.25 beats
102
156
  Move the position back to the start using `|`. This is useful for writing chords or multiple voices.
103
157
 
104
158
  ```
105
- c5 | e5 | g5 # C major chord
159
+ c5 | e | g # C major chord (c5 e5 g5)
106
160
  ```
107
161
 
108
162
  Two voices:
109
163
 
110
164
  ```
111
- | c5 g5 e5.2
112
- | c4.2 f4.2
165
+ | c5 g e.2
166
+ | c4.2 f.2
113
167
  ```
114
168
 
115
169
  ### Blocks
@@ -123,42 +177,51 @@ Blocks are delimited with `{` and `}`. They affect how commands work:
123
177
  ```
124
178
  {
125
179
  dt
126
- c5 { dt e5 f5 } d5.2 e5 g5 a5 c6
180
+ c5 { dt e f } d.2 e g a c
127
181
  }
128
182
  |
129
- { ht g4 f4 }
183
+ { ht g4 f }
130
184
  ```
131
185
 
132
186
  ### Measures
133
187
 
134
- The `m` command moves the position to a specific measure. Commonly used with blocks:
188
+ 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:
135
189
 
136
190
  ```
137
- m0 {
138
- | c5 c5 a5 g5
191
+ m {
192
+ | c5 c a g
139
193
  | g4.4
140
194
  }
141
195
 
142
- m1 {
143
- | d5 d5 a5 e5
196
+ m {
197
+ | d5 d a e
144
198
  | f4.4
145
199
  }
146
200
  ```
147
201
 
202
+ The first `m` goes to measure 0, then each subsequent `m` increments. Explicit measure numbers also update the counter:
203
+
204
+ ```
205
+ m { c d } # measure 0
206
+ m { e f } # measure 1
207
+ m5 { g a } # measure 5
208
+ m { b c } # measure 6
209
+ ```
210
+
148
211
  ### Key Signature
149
212
 
150
213
  Set the key signature with `ks` followed by the number of sharps (positive) or flats (negative). Notes are automatically adjusted to match the key.
151
214
 
152
215
  ```
153
- ks2 # D major (2 sharps: F#, C#)
154
- c5 d5 e5 f5 # F becomes F#, C becomes C#
216
+ ks2 # D major (2 sharps: F#, C#)
217
+ c d e f # F becomes F#, C becomes C#
155
218
  ```
156
219
 
157
220
  Use `=` to override the key signature with a natural:
158
221
 
159
222
  ```
160
223
  ks-2
161
- b5 c5 b=5 # B natural
224
+ b c b= # B natural
162
225
  ```
163
226
 
164
227
  ### Time Signature
@@ -167,18 +230,69 @@ Set the time signature with `ts`:
167
230
 
168
231
  ```
169
232
  ts3/4
170
- c5 d5 e5
233
+ c d e
171
234
  ```
172
235
 
173
236
  This affects beats per measure and where measure lines appear.
174
237
 
238
+ Time signatures can change mid-song. Notes are placed sequentially regardless of time signature changes:
239
+
240
+ ```
241
+ ts4/4
242
+ c d e f # 4 beats in 4/4
243
+ ts3/4
244
+ g a b # 3 beats in 3/4
245
+ ts4/4
246
+ c d e f # 4 beats in 4/4
247
+ ```
248
+
249
+ Time signature changes are tracked and accessible via `song.timeSignatures`:
250
+
251
+ ```typescript
252
+ const song = SongParser.load("ts3/4 c d e ts4/4 f g a b")
253
+ console.log(song.timeSignatures)
254
+ // [[0, 3], [3, 4]] // [beat_position, beats_per_measure]
255
+ ```
256
+
257
+ ### Measures API
258
+
259
+ 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:
260
+
261
+ ```typescript
262
+ const song = SongParser.load("c5 d e f g a b c") // 8 beats in 4/4
263
+ const measures = song.getMeasures()
264
+ // [{ start: 0, beats: 4 }, { start: 4, beats: 4 }]
265
+ ```
266
+
267
+ This correctly handles time signature changes:
268
+
269
+ ```typescript
270
+ const song = SongParser.load(`
271
+ ts4/4 c d e f # 4 beats
272
+ ts3/4 g a b # 3 beats
273
+ ts4/4 c d e f # 4 beats
274
+ `)
275
+ const measures = song.getMeasures()
276
+ // [
277
+ // { start: 0, beats: 4 },
278
+ // { start: 4, beats: 3 },
279
+ // { start: 7, beats: 4 }
280
+ // ]
281
+ ```
282
+
283
+ Each measure object contains:
284
+ - `start`: Beat position where the measure begins
285
+ - `beats`: Number of beats in this measure
286
+
287
+ Note: 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.
288
+
175
289
  ### Chords
176
290
 
177
291
  The `$` command specifies a chord symbol for auto-chord generation:
178
292
 
179
293
  ```
180
- {$G c5.2 a5 d5}
181
- {$Dm e5 f5 g5.2}
294
+ {$G c5.2 a d}
295
+ {$Dm e f g.2}
182
296
  ```
183
297
 
184
298
  Supported chord types: `M`, `m`, `dim`, `dim7`, `dimM7`, `aug`, `augM7`, `M6`, `m6`, `M7`, `7`, `m7`, `m7b5`, `mM7`
@@ -188,8 +302,8 @@ Supported chord types: `M`, `m`, `dim`, `dim7`, `dimM7`, `aug`, `augM7`, `M6`, `
188
302
  Songs can have multiple tracks, numbered starting from 0. Use `t` to switch tracks:
189
303
 
190
304
  ```
191
- t0 c5 d5 e5
192
- t1 g3 g3 g3
305
+ t0 c5 d e
306
+ t1 g3 g g
193
307
  ```
194
308
 
195
309
  ### Clefs
@@ -197,8 +311,52 @@ t1 g3 g3 g3
197
311
  Set the clef with `/g` (treble), `/f` (bass), or `/c` (alto):
198
312
 
199
313
  ```
200
- /g c5 d5 e5
201
- /f c3 d3 e3
314
+ /g c5 d e
315
+ /f c3 d e
316
+ ```
317
+
318
+ 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:
319
+
320
+ - Notes primarily above middle C → treble staff
321
+ - Notes primarily below middle C → bass staff
322
+ - Notes spanning both ranges → grand staff (treble + bass)
323
+
324
+ Clefs are accessible via `track.clefs`:
325
+
326
+ ```typescript
327
+ const song = SongParser.load("/f c3 d e")
328
+ console.log(song.tracks[0].clefs)
329
+ // [[0, "f"]] // [position, clefType]
330
+ ```
331
+
332
+ ### Strings
333
+
334
+ 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.
335
+
336
+ ```
337
+ c "hel" d "lo" e "world"
338
+ ```
339
+
340
+ Both single and double quotes are supported. Strings can span multiple lines:
341
+
342
+ ```
343
+ c.2 'First verse
344
+ continues here' d.2
345
+ ```
346
+
347
+ Escape sequences are supported: `\"`, `\'`, `\\`, `\n`.
348
+
349
+ ```
350
+ "say \"hello\""
351
+ 'it\'s working'
352
+ ```
353
+
354
+ Strings are accessible via `song.strings`:
355
+
356
+ ```typescript
357
+ const song = SongParser.load('c "la" d "la"')
358
+ console.log(song.strings)
359
+ // [[1, "la"], [2, "la"]] // [position, text]
202
360
  ```
203
361
 
204
362
  ### Comments
@@ -206,11 +364,44 @@ Set the clef with `/g` (treble), `/f` (bass), or `/c` (alto):
206
364
  Text after `#` is ignored:
207
365
 
208
366
  ```
209
- c5 d5 # this is a comment
367
+ c d # this is a comment
210
368
  # full line comment
211
- e5 f5
369
+ e f
370
+ ```
371
+
372
+ ### Frontmatter
373
+
374
+ 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:
375
+
376
+ ```
377
+ # title: Moonlight Sonata
378
+ # author: Beethoven
379
+ # bpm: 120
380
+ # difficulty: intermediate
381
+
382
+ ts4/4 ks-3
383
+ c d e f
212
384
  ```
213
385
 
386
+ The convention is to use lowercase key names. Frontmatter is accessible via `song.metadata.frontmatter`:
387
+
388
+ ```typescript
389
+ const song = SongParser.load(`
390
+ # title: My Song
391
+ # bpm: 90
392
+ c d e
393
+ `)
394
+
395
+ console.log(song.metadata.frontmatter)
396
+ // { title: "My Song", bpm: "90" }
397
+ ```
398
+
399
+ Notes:
400
+ - Frontmatter must appear at the start of the file, before any music commands
401
+ - Keys are case-sensitive
402
+ - All values are stored as strings
403
+ - Once a non-frontmatter line is encountered, subsequent `# key: value` lines are treated as regular comments
404
+
214
405
  ## Music Theory Utilities
215
406
 
216
407
  The library also exports music theory utilities:
@@ -243,6 +434,34 @@ key.name() // "D"
243
434
  key.accidentalNotes() // ["F", "C"]
244
435
  ```
245
436
 
437
+ ## Limitations
438
+
439
+ Current limitations that may be addressed in future versions:
440
+
441
+ - **Clefs are per-track**: Each track supports only a single clef. Mid-track clef changes are not supported.
442
+
443
+ - **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()`.)
444
+
445
+ - **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):
446
+
447
+ ```
448
+ # Recommended: time signature is always applied at start of measure
449
+ m ts4/4 c d e f
450
+ m ts3/4 g a b
451
+
452
+ # Although you can also write it before the m command, if the previous measure
453
+ # accidentally pushed the cursor into the next measure then the time signature
454
+ # application will be delayed a measure:
455
+
456
+ ts3/4
457
+ m g.4 # extends 1 beat past 3-beat measure
458
+ ts4/4 # recorded at beat 4, but next measure starts at beat 3
459
+ m a b c d
460
+
461
+ ```
462
+
463
+ - **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.
464
+
246
465
  ## License
247
466
 
248
467
  MIT
@@ -1 +1 @@
1
- {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../src/grammar.js"],"names":[],"mappings":"AAg2CA,8CAEE;AA71CF;IAwCE,uDA2GC;IAlJD,oEAMC;IAJC,cAAwB;IACxB,WAAkB;IAClB,cAAwB;IAI1B,6BA6BC;CA8GF;AAED,0DAmsCC"}
1
+ {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../src/grammar.js"],"names":[],"mappings":"AAuoEA,8CAGE;AAroEF;IAwCE,uDA2GC;IAlJD,oEAMC;IAJC,cAAwB;IACxB,WAAkB;IAClB,cAAwB;IAI1B,6BA6BC;CA8GF;AAED,0DA0+DC"}