@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 +257 -38
- package/dist/grammar.d.ts.map +1 -1
- package/dist/grammar.js +1236 -261
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/music.d.ts +1 -0
- package/dist/music.d.ts.map +1 -1
- package/dist/music.js +7 -3
- package/dist/music.js.map +1 -1
- package/dist/noteUtils.d.ts +24 -0
- package/dist/noteUtils.d.ts.map +1 -0
- package/dist/noteUtils.js +80 -0
- package/dist/noteUtils.js.map +1 -0
- package/dist/parser.d.ts +4 -2
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +145 -23
- package/dist/parser.js.map +1 -1
- package/dist/song.d.ts +10 -1
- package/dist/song.d.ts.map +1 -1
- package/dist/song.js +39 -3
- package/dist/song.js.map +1 -1
- package/package.json +5 -3
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
|
|
22
|
-
|
|
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
|
|
44
|
-
// [["note", "C5"], ["note", "
|
|
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
|
-
|
|
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
|
-
|
|
72
|
+
The algorithm picks the octave that minimizes the distance in semitones:
|
|
61
73
|
|
|
62
74
|
```
|
|
63
|
-
c5
|
|
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
|
-
|
|
79
|
+
Duration can be modified with `.` (multiply) or `/` (divide). The default duration is 1 beat.
|
|
67
80
|
|
|
68
81
|
```
|
|
69
|
-
c
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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 |
|
|
159
|
+
c5 | e | g # C major chord (c5 e5 g5)
|
|
106
160
|
```
|
|
107
161
|
|
|
108
162
|
Two voices:
|
|
109
163
|
|
|
110
164
|
```
|
|
111
|
-
| c5
|
|
112
|
-
| c4.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
|
|
180
|
+
c5 { dt e f } d.2 e g a c
|
|
127
181
|
}
|
|
128
182
|
|
|
|
129
|
-
{ ht g4
|
|
183
|
+
{ ht g4 f }
|
|
130
184
|
```
|
|
131
185
|
|
|
132
186
|
### Measures
|
|
133
187
|
|
|
134
|
-
The `m` command moves the position to a
|
|
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
|
-
|
|
138
|
-
| c5
|
|
191
|
+
m {
|
|
192
|
+
| c5 c a g
|
|
139
193
|
| g4.4
|
|
140
194
|
}
|
|
141
195
|
|
|
142
|
-
|
|
143
|
-
| d5
|
|
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
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
{$Dm
|
|
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
|
|
192
|
-
t1 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
|
|
201
|
-
/f c3
|
|
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
|
-
|
|
367
|
+
c d # this is a comment
|
|
210
368
|
# full line comment
|
|
211
|
-
|
|
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
|
package/dist/grammar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../src/grammar.js"],"names":[],"mappings":"
|
|
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"}
|