@k-l-lambda/lilylet 0.1.62 → 0.1.63

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.
@@ -695,13 +695,13 @@ const convertEventTerm = (eventTerm, unitLength, pendingMarks, pendingContextCha
695
695
  return undefined;
696
696
  const firstPitch = chord.pitches[0];
697
697
  // Check if rest
698
- if (firstPitch.phonet === "z" || firstPitch.phonet === "Z" || firstPitch.phonet === "x") {
698
+ if (firstPitch.phonet === "z" || firstPitch.phonet === "Z" || firstPitch.phonet === "x" || firstPitch.phonet === "y") {
699
699
  const duration = convertDuration(eventData.duration, unitLength);
700
700
  const rest = {
701
701
  type: "rest",
702
702
  duration,
703
703
  };
704
- if (firstPitch.phonet === "x") {
704
+ if (firstPitch.phonet === "x" || firstPitch.phonet === "y") {
705
705
  rest.invisible = true;
706
706
  }
707
707
  if (firstPitch.phonet === "Z") {
@@ -712,7 +712,7 @@ const convertEventTerm = (eventTerm, unitLength, pendingMarks, pendingContextCha
712
712
  return rest;
713
713
  }
714
714
  // Note or chord
715
- const pitches = chord.pitches.filter(p => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x").map(convertPitch);
715
+ const pitches = chord.pitches.filter(p => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x" && p.phonet !== "y").map(convertPitch);
716
716
  if (pitches.length === 0)
717
717
  return undefined;
718
718
  const duration = convertDuration(eventData.duration, unitLength);
@@ -781,6 +781,14 @@ const decodeTune = (tune) => {
781
781
  let tempo;
782
782
  const voiceConfigs = new Map();
783
783
  const voiceClefs = new Map();
784
+ // Pre-scan for unit length (needed for bare Q: tempo)
785
+ for (const h of headers) {
786
+ const hdr = h;
787
+ if (hdr.name === "L" && hdr.value?.numerator && hdr.value?.denominator) {
788
+ unitLength = hdr.value;
789
+ break;
790
+ }
791
+ }
784
792
  for (const h of headers) {
785
793
  if (h.comment)
786
794
  continue;
@@ -822,25 +830,45 @@ const decodeTune = (tune) => {
822
830
  tempo = { beat: beatDuration, bpm: header.value.bpm };
823
831
  }
824
832
  else if (typeof header.value === "number") {
825
- tempo = { bpm: header.value };
833
+ const beat = convertDuration({ numerator: 1, denominator: 1 }, unitLength);
834
+ tempo = { beat, bpm: header.value };
826
835
  }
827
836
  break;
828
837
  case "V": {
829
838
  const voiceValue = header.value;
830
839
  if (voiceValue) {
831
- const voiceNum = typeof voiceValue === "number" ? voiceValue :
832
- (voiceValue.name || 1);
833
- const clefStr = typeof voiceValue === "string" ? voiceValue :
834
- (voiceValue.clef || undefined);
835
- voiceConfigs.set(voiceNum, {
836
- name: voiceNum,
840
+ let voiceId;
841
+ let clefStr;
842
+ if (typeof voiceValue === "number") {
843
+ voiceId = voiceValue;
844
+ }
845
+ else if (typeof voiceValue === "string") {
846
+ voiceId = voiceValue;
847
+ }
848
+ else {
849
+ const rawClef = (voiceValue.clef || "").replace(/,+$/, "").trim();
850
+ const isKnownClef = !!convertClef(rawClef);
851
+ if (isKnownClef) {
852
+ // V:1 treble → voiceId=number, clef=treble
853
+ voiceId = voiceValue.name || 1;
854
+ clefStr = rawClef;
855
+ }
856
+ else {
857
+ // V:S clef=treble → voiceId=voiceName, clef from properties
858
+ voiceId = rawClef || voiceValue.name || 1;
859
+ const propClef = (voiceValue.properties?.clef || "").replace(/,+$/, "").trim();
860
+ clefStr = propClef || undefined;
861
+ }
862
+ }
863
+ voiceConfigs.set(voiceId, {
864
+ name: typeof voiceId === "number" ? voiceId : 1,
837
865
  clef: clefStr,
838
- properties: voiceValue.properties,
866
+ properties: voiceValue?.properties,
839
867
  });
840
868
  if (clefStr) {
841
869
  const clef = convertClef(clefStr);
842
870
  if (clef)
843
- voiceClefs.set(voiceNum, clef);
871
+ voiceClefs.set(voiceId, clef);
844
872
  }
845
873
  }
846
874
  break;
@@ -469,6 +469,9 @@ const encodeTupletEvent = (event, env, lastDuration) => {
469
469
  newEnv = ne;
470
470
  newDuration = nd;
471
471
  }
472
+ else if (subEvent.type === 'context') {
473
+ result += encodeContextChange(subEvent) + ' ';
474
+ }
472
475
  }
473
476
  result += '}';
474
477
  return { str: result, newEnv, newDuration };
@@ -538,7 +538,7 @@ const tupletHasInternalBeams = (event) => {
538
538
  return starts > 0 && starts === ends;
539
539
  };
540
540
  // Convert TupletEvent to MEI
541
- const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff, ottavaShift = 0, inParentBeam = false, measureAccidentals) => {
541
+ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff, ottavaShift = 0, inParentBeam = false, measureAccidentals, currentClef) => {
542
542
  // LilyPond \times 2/3 means "multiply duration by 2/3"
543
543
  // So 3 notes × 2/3 = 2 beats worth (3 in time of 2)
544
544
  // MEI: num = number of notes written, numbase = normal equivalent
@@ -561,6 +561,8 @@ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff
561
561
  // Handle internal beam groups: if notes have manual beam marks, respect them
562
562
  const hasInternalBeams = !inParentBeam && tupletHasInternalBeams(event);
563
563
  let beamOpen = false;
564
+ let activeClef = currentClef;
565
+ let endingClef;
564
566
  for (const e of event.events) {
565
567
  if (e.type === 'note') {
566
568
  const noteEvent = e;
@@ -607,13 +609,27 @@ const tupletEventToMEI = (event, indent, layerStaff, keyFifths = 0, currentStaff
607
609
  const restIndent = beamOpen ? baseIndent + ' ' : baseIndent;
608
610
  xml += restEventToMEI(e, restIndent, keyFifths, ottavaShift, measureAccidentals);
609
611
  }
612
+ else if (e.type === 'context') {
613
+ const ctx = e;
614
+ if (ctx.clef && ctx.clef !== activeClef) {
615
+ const layerStaffNum = layerStaff || 1;
616
+ const effectiveStaffNum = effectiveStaff ?? layerStaffNum;
617
+ if (effectiveStaffNum === layerStaffNum) {
618
+ const clefIndent = beamOpen ? baseIndent + ' ' : baseIndent;
619
+ const clefInfo = CLEF_SHAPES[ctx.clef] || CLEF_SHAPES.treble;
620
+ xml += `${clefIndent}<clef xml:id="${generateId('clef')}" shape="${clefInfo.shape}" line="${clefInfo.line}" />\n`;
621
+ }
622
+ activeClef = ctx.clef;
623
+ endingClef = ctx.clef;
624
+ }
625
+ }
610
626
  }
611
627
  // Close any unclosed beam
612
628
  if (beamOpen) {
613
629
  xml += `${baseIndent}</beam>\n`;
614
630
  }
615
631
  xml += `${indent}</tuplet>\n`;
616
- return { xml, firstNoteId, slurStarts, slurEnds, dynamics, fermatas, trills, mordents, turns, arpeggios };
632
+ return { xml, firstNoteId, slurStarts, slurEnds, dynamics, fermatas, trills, mordents, turns, arpeggios, endingClef };
617
633
  };
618
634
  // Convert TremoloEvent to MEI (fingered tremolo - alternating between two notes)
619
635
  const tremoloEventToMEI = (event, indent, keyFifths = 0, ottavaShift = 0, measureAccidentals) => {
@@ -688,7 +704,7 @@ const getEventBeamMarks = (event) => {
688
704
  const markOptions = extractMarkOptions(event.marks);
689
705
  return { beamStart: markOptions.beamStart, beamEnd: markOptions.beamEnd };
690
706
  }
691
- if (event.type === 'tuplet') {
707
+ if (event.type === 'tuplet' || event.type === 'times') {
692
708
  const tuplet = event;
693
709
  // If the tuplet has internal beam groups, don't report beam marks to the parent
694
710
  // so the parent won't wrap the tuplet in an external <beam>
@@ -923,11 +939,16 @@ const encodeLayer = (voice, layerN, indent, initialTiePitches = [], keyFifths =
923
939
  xml += restEventToMEI(event, currentIndent, keyFifths, currentOttavaShift, measureAccidentals, restCrossStaff);
924
940
  break;
925
941
  }
926
- case 'tuplet': {
942
+ case 'tuplet':
943
+ case 'times': {
927
944
  // Tuplet can be nested inside beam in MEI: <beam><tuplet>...</tuplet></beam>
928
945
  // Pass beamElementOpen to tuplet so it knows not to create its own beam
929
- const tupletResult = tupletEventToMEI(event, currentIndent, voice.staff, keyFifths, currentStaff, currentOttavaShift, beamElementOpen, measureAccidentals);
946
+ const tupletResult = tupletEventToMEI(event, currentIndent, voice.staff, keyFifths, currentStaff, currentOttavaShift, beamElementOpen, measureAccidentals, currentClef);
930
947
  xml += tupletResult.xml;
948
+ // Propagate clef change from inside the tuplet to the parent tracker
949
+ if (tupletResult.endingClef) {
950
+ currentClef = tupletResult.endingClef;
951
+ }
931
952
  // Flush any pending markups onto the first note of the tuplet
932
953
  if (tupletResult.firstNoteId) {
933
954
  flushPendingMarkups(tupletResult.firstNoteId);
@@ -1566,7 +1587,7 @@ const docHasBeamMarks = (doc) => {
1566
1587
  }
1567
1588
  }
1568
1589
  }
1569
- else if (event.type === 'tuplet') {
1590
+ else if (event.type === 'tuplet' || event.type === 'times') {
1570
1591
  const tuplet = event;
1571
1592
  for (const e of tuplet.events) {
1572
1593
  if (e.type === 'note') {
@@ -1729,7 +1750,7 @@ const applyAutoBeamToVoice = (events, beamGroups) => {
1729
1750
  flushRun();
1730
1751
  position += dur;
1731
1752
  }
1732
- else if (event.type === 'tuplet') {
1753
+ else if (event.type === 'tuplet' || event.type === 'times') {
1733
1754
  const tuplet = event;
1734
1755
  const ratio = tuplet.ratio; // LilyPond ratio: num/den
1735
1756
  // Check if all inner notes are beamable (division >= 8)
@@ -1907,7 +1928,7 @@ const encode = (doc, options = {}) => {
1907
1928
  for (const event of voice.events) {
1908
1929
  // Check for actual musical content (not just context changes or pitch resets)
1909
1930
  if (event.type === 'note' || event.type === 'rest' ||
1910
- event.type === 'tuplet' || event.type === 'tremolo') {
1931
+ event.type === 'tuplet' || event.type === 'times' || event.type === 'tremolo') {
1911
1932
  return true;
1912
1933
  }
1913
1934
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k-l-lambda/lilylet",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "description": "Lilylet is a lilyopnd-like sheet music language designed for Markdown rendering and symbolic music representation in AIGC applications.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -29,11 +29,13 @@
29
29
  "prepublishOnly": "npm run build:grammar && npm run build",
30
30
  "test": "tsx ./tests/parser.ts",
31
31
  "test:mei": "tsx ./tests/mei.ts",
32
+ "test:mei-hashes": "tsx ./tests/computeMeiHashes.ts",
32
33
  "test:unit": "tsx ./tests/unit/encodePitch.test.ts",
33
34
  "test:partial": "tsx ./tests/unit/partialWarning.test.ts",
34
35
  "test:decoder": "tsx ./tests/lilypondDecoder.ts",
35
36
  "test:abc": "tsx ./tests/abc-decoder.ts",
36
37
  "test:roundtrip": "tsx ./tests/lilypond-roundtrip.ts",
38
+ "test:abc-svg": "tsx ./tests/abc-abcjs-svg.ts",
37
39
  "build:tests": "tsc -p tsconfig.tests.json; cp source/lilylet/grammar.jison.js lib-tests/source/lilylet/ && cp source/abc/grammar.jison.js lib-tests/source/abc/ && node tools/fixEsmExtensions.cjs lib-tests && ln -sfn ../../tests/assets lib-tests/tests/assets",
38
40
  "test:roundtrip:compiled": "node lib-tests/tests/lilypond-roundtrip.js",
39
41
  "ts": "tsx"
@@ -51,14 +53,15 @@
51
53
  "devDependencies": {
52
54
  "@types/node": "^20.11.20",
53
55
  "@types/yargs": "^17.0.32",
56
+ "abcjs": "^6.6.2",
54
57
  "formidable": "^3.5.4",
55
58
  "jison": "^0.4.18",
59
+ "jsdom": "^29.0.2",
56
60
  "sha1": "^1.1.1",
57
61
  "ts-node": "^10.9.2",
58
62
  "tsx": "^4.21.0",
59
63
  "typescript": "^5.3.3",
60
64
  "verovio": "^5.7.0",
61
- "xmldom": "^0.6.0",
62
65
  "yargs": "^17.7.2"
63
66
  },
64
67
  "dependencies": {
@@ -82,18 +82,24 @@
82
82
  const {patches} = body;
83
83
  const measures = [];
84
84
  let measure = null;
85
- let lastVoice = 1;
85
+ const seenVoices = new Set();
86
+
86
87
  patches.forEach(patch => {
87
88
  const voice = patch.control.V || 1;
88
- if (voice <= lastVoice) {
89
+ if (seenVoices.has(voice)) {
89
90
  if (measure)
90
91
  measures.push(measure);
91
92
  measure = {voices: []};
93
+ seenVoices.clear();
92
94
  }
95
+ if (!measure)
96
+ measure = {voices: []};
97
+ seenVoices.add(voice);
93
98
  measure.voices.push(patch);
94
99
  });
95
100
 
96
- measures.push(measure);
101
+ if (measure)
102
+ measures.push(measure);
97
103
 
98
104
  measures.forEach((measure, index) => measure.index = index + 1);
99
105
 
@@ -151,6 +157,21 @@
151
157
 
152
158
 
153
159
  const octaveShift = shift => ({octaveShift: shift});
160
+
161
+
162
+ const parseMode = (name) => {
163
+ const n = name.toLowerCase();
164
+ if (n.startsWith("ma")) return "major";
165
+ if (n === "m" || n.startsWith("mi")) return "minor";
166
+ if (n.startsWith("dor")) return "dorian";
167
+ if (n.startsWith("phr")) return "phrygian";
168
+ if (n.startsWith("lyd")) return "lydian";
169
+ if (n.startsWith("mix")) return "mixolydian";
170
+ if (n.startsWith("aeo")) return "aeolian";
171
+ if (n.startsWith("loc")) return "locrian";
172
+ if (n === "hp") return "highland";
173
+ return n;
174
+ };
154
175
  %}
155
176
 
156
177
 
@@ -160,24 +181,27 @@
160
181
 
161
182
  %x string
162
183
  %x comment
184
+ %x spec_comment_name
163
185
  %x spec_comment
186
+ %x spec_comment_skip
164
187
  %x title_string
165
188
  %x key_signature
189
+ %x voice_header
166
190
  %x exclamation_exp
167
191
 
168
-
169
192
  H \b[A-Z](?=\:[^|])
170
- A \b[A-G](?=[\W\d\sA-Ga-g_zHJLMOPRSTuv]*\b)
193
+ A \b[A-G](?=[\W\d\sA-Ga-g_yzHJLMOPRSTuv]*\b)
171
194
  Am \b[A-G](?=[m][a][j]|[m][i][n]\b)
172
- a \b[a-g](?=[\W\d\sA-Ga-g_zHJLMOPRSTuv]*\b)
195
+ a \b[a-g](?=[\W\d\sA-Ga-g_yzHJLMOPRSTuv]*\b)
173
196
  z \b[z]
174
197
  Z \b[Z]
175
198
  x \b[x](?=[\W\d\s])
199
+ y \b[y]
176
200
  N [0-9]
177
201
  P \b[HJLMOPRSTuv](?=[A-Ga-g][A-Ga-g0-9]*\b)
178
202
  PP \b[HJLMOPRSTuv](?=[xz!\[^_=\s"])
179
203
 
180
- SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
204
+ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
181
205
 
182
206
 
183
207
  %%
@@ -193,10 +217,30 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
193
217
  <title_string>[^\n]+ return 'STR_CONTENT'
194
218
 
195
219
  ^[K][:][\s]* { this.pushState('key_signature'); return 'K:'; }
220
+ ^[V][:][ \t]* { this.pushState('voice_header'); return 'V:'; }
221
+ <voice_header>\" { this.pushState('string'); return 'STR_START'; }
222
+ <voice_header>[a-zA-Z][a-zA-Z0-9,]* return 'NAME';
223
+ <voice_header>[0-9]+ return 'N';
224
+ <voice_header>[=] return '=';
225
+ <voice_header>[+\-] return yytext;
226
+ <voice_header>[ \t]+ {}
227
+ <voice_header>\n { this.popState(); }
228
+ <voice_header>\] { this.popState(); return ']'; }
196
229
  <key_signature>"treble" return 'TREBLE';
197
230
  <key_signature>"bass" return 'BASS';
198
231
  <key_signature>"tenor" return 'TENOR';
232
+ <key_signature>"none" return 'NAME';
233
+ <key_signature>"Dor" return 'NAME';
234
+ <key_signature>"Phr" return 'NAME';
235
+ <key_signature>"Lyd" return 'NAME';
236
+ <key_signature>"Mix" return 'NAME';
237
+ <key_signature>"Aeo" return 'NAME';
238
+ <key_signature>"Loc" return 'NAME';
239
+ <key_signature>"HP" return 'NAME';
240
+ <key_signature>"Hp" return 'NAME';
241
+ <key_signature>[a-z]+[ \t]*=[^\n\]]* {}
199
242
  <key_signature>[A-G] return 'A';
243
+ <key_signature>[A-Z][a-z]+ return 'NAME';
200
244
  <key_signature>[b] return 'FLAT';
201
245
  <key_signature>[#] return 'SHARP';
202
246
  <key_signature>[m][a-z]* return 'NAME';
@@ -208,14 +252,20 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
208
252
  <key_signature>\] { this.popState(); return ']'; }
209
253
 
210
254
  ^[%] { this.pushState('comment'); }
211
- <comment>[%] { this.pushState('spec_comment'); }
255
+ <comment>[%] { this.pushState('spec_comment_name'); }
212
256
  <comment>[^\n]+ { return 'COMMENT'; }
213
- <spec_comment>\n { this.popState(); this.popState(); }
214
257
  <comment>\n { this.popState(); }
215
- <spec_comment>\s {}
216
- <spec_comment>"score" return 'SCORE'
217
- <spec_comment>[\w]+ return 'NN'
258
+ <spec_comment_name>[ \t]+ {}
259
+ <spec_comment_name>"score" { this.popState(); this.pushState('spec_comment'); return 'SCORE'; }
260
+ <spec_comment_name>"staves" { this.popState(); this.pushState('spec_comment'); return 'SCORE'; }
261
+ <spec_comment_name>[\w]+ { this.popState(); this.pushState('spec_comment_skip'); }
262
+ <spec_comment_name>\n { this.popState(); this.popState(); }
263
+ <spec_comment>[ \t]+ {}
218
264
  <spec_comment>[(){}\[\]|] return yytext
265
+ <spec_comment>[\w]+ return 'NN'
266
+ <spec_comment>\n { this.popState(); this.popState(); return 'LAYOUT_END'; }
267
+ <spec_comment_skip>[^\n]+ {}
268
+ <spec_comment_skip>\n { this.popState(); this.popState(); }
219
269
 
220
270
  [!] { this.pushState('exclamation_exp'); return '!'; }
221
271
  <exclamation_exp>[!] { this.popState(); return '!'; }
@@ -231,6 +281,7 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
231
281
  <exclamation_exp>{N} return 'N'
232
282
  <exclamation_exp>[a-zA-Z][\w-]* return 'NAME'
233
283
 
284
+ \\\n {}
234
285
  \s+ {}
235
286
 
236
287
  {SPECIAL} return yytext
@@ -250,6 +301,7 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~]
250
301
  "staff" return 'STAFF'
251
302
  "maj" return 'MAJ'
252
303
  "min" return 'MIN'
304
+ {y} return 'y'
253
305
  [a-zA-Z][\w-]* return 'NAME'
254
306
 
255
307
  <<EOF>> return 'EOF'
@@ -284,6 +336,7 @@ head_lines
284
336
  | head_lines head_line -> [...$1, $2]
285
337
  | head_lines comment -> [...$1, $2]
286
338
  | head_lines staff_layout_statement -> [...$1, $2]
339
+ | head_lines 'LAYOUT_END' -> $1
287
340
  | head_lines ']' -> $1
288
341
  | head_lines '}' -> $1
289
342
  | head_lines ')' -> $1
@@ -294,7 +347,7 @@ comment
294
347
  ;
295
348
 
296
349
  staff_layout_statement
297
- : 'SCORE' staff_layout -> $2
350
+ : 'SCORE' staff_layout 'LAYOUT_END' -> $2
298
351
  ;
299
352
 
300
353
  staff_layout
@@ -320,6 +373,7 @@ head_line
320
373
  : 'T:' string_content -> header('T', $2)
321
374
  | 'C:' string_content -> header('C', $2)
322
375
  | 'K:' key_signature -> header('K', $2)
376
+ | 'V:' header_value -> header('V', $2)
323
377
  | H ':' header_value -> header($1, $3)
324
378
  ;
325
379
 
@@ -328,6 +382,7 @@ header_value
328
382
  | number
329
383
  | frac
330
384
  | numeric_tempo
385
+ | string frac '=' number -> ({text: $1, note: $2, bpm: $4})
331
386
  | upper_phonet
332
387
  | voice_exp
333
388
  | staff_shift
@@ -342,6 +397,11 @@ staff_shift
342
397
  ;
343
398
 
344
399
  key_signature
400
+ : key_root -> $1
401
+ | NAME -> key(null, $1)
402
+ ;
403
+
404
+ key_root
345
405
  : A -> key($1, null)
346
406
  | A sharp_or_flat -> key($1 + $2, null)
347
407
  | A key_mode -> key($1, $2)
@@ -362,7 +422,7 @@ sharp_or_flat
362
422
  key_mode
363
423
  : MAJ -> 'major'
364
424
  | MIN -> 'minor'
365
- | NAME -> $1.startsWith("ma") ? "major" : "minor"
425
+ | NAME -> parseMode($1)
366
426
  ;
367
427
 
368
428
  plus_minus_number
@@ -402,6 +462,10 @@ voice_exp
402
462
  | number NAME assigns -> voice($1, $2, $3)
403
463
  | NAME -> voice(1, $1)
404
464
  | NAME assigns -> voice(1, $1, $2)
465
+ | upper_phonet number -> voice(1, $1 + String($2))
466
+ | upper_phonet number assigns -> voice(1, $1 + String($2), $3)
467
+ | upper_phonet number NAME -> voice(1, $1 + String($2))
468
+ | upper_phonet number NAME assigns -> voice(1, $1 + String($2), $4)
405
469
  ;
406
470
 
407
471
  assigns
@@ -418,6 +482,7 @@ assign_value
418
482
  | number
419
483
  | plus_minus_number
420
484
  | NAME
485
+ | upper_phonet
421
486
  ;
422
487
 
423
488
  upper_phonet
@@ -437,6 +502,9 @@ patches
437
502
  | patches tailless_patch -> [...$1, $2]
438
503
  | patches ']' -> $1
439
504
  | patches '}' -> $1
505
+ | patches head_line -> $1
506
+ | patches 'LAYOUT_END' -> $1
507
+ | patches '&' patch -> $1
440
508
  ;
441
509
 
442
510
  patch
@@ -459,10 +527,11 @@ bar
459
527
  | ':' '|' ']' -> ':|]'
460
528
  | '|' N -> '|' + $2
461
529
  | ':' '|' N -> ':|' + $2
530
+ | '&' -> '&'
462
531
  ;
463
532
 
464
533
  music
465
- : %empty
534
+ : %empty -> []
466
535
  | music expressive_mark -> $1 ? [...$1, $2] : [$2]
467
536
  | music text -> $1 ? [...$1, $2] : [$2]
468
537
  | music event -> $1 ? [...$1, $2] : [$2]
@@ -474,11 +543,13 @@ music
474
543
  | music NAME -> $1
475
544
  | music '^' NAME -> $1
476
545
  | music '^' -> $1
546
+ | music '[' N -> $1
477
547
  ;
478
548
 
479
549
  control
480
550
  : '[' H ':' header_value ']' -> ({control: header($2, $4)})
481
551
  | '[' 'K:' header_value ']' -> ({control: header("K", $3)})
552
+ | '[' 'V:' header_value ']' -> ({control: header("V", $3)})
482
553
  | '[' NAME ':' header_value ']' -> ({control: header($2, $4)})
483
554
  ;
484
555
 
@@ -510,6 +581,7 @@ articulation
510
581
  articulation_content
511
582
  : scope_articulation -> articulation($1)
512
583
  | scope_articulation parenthese -> articulation($1, $2)
584
+ | scope_articulation '=' assign_value -> articulation($1 + '=' + String($3))
513
585
  | DYNAMIC -> articulation($1)
514
586
  | a -> articulation($1)
515
587
  | "^" -> articulation($1)
@@ -613,6 +685,8 @@ accidentals
613
685
  | '^' '^' -> 2
614
686
  | '_' '_' -> -2
615
687
  | '=' '=' -> 0
688
+ | '^' '/' -> 0.5
689
+ | '_' '/' -> -0.5
616
690
  ;
617
691
 
618
692
  pitch
@@ -637,6 +711,7 @@ rest_phonet
637
711
  : z
638
712
  | Z
639
713
  | x
714
+ | y
640
715
  ;
641
716
 
642
717
  event