@k-l-lambda/lilylet 0.1.67 → 0.1.69
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/lib/abc/grammar.jison.js +155 -135
- package/lib/lilylet/abcDecoder.d.ts +12 -6
- package/lib/lilylet/abcDecoder.js +161 -39
- package/lib/lilylet/grammar.jison.js +195 -185
- package/lib/lilylet/meiEncoder.js +31 -6
- package/lib/lilylet/serializer.js +76 -31
- package/lib/lilylet/types.d.ts +5 -1
- package/package.json +1 -2
- package/source/abc/abc.jison +38 -14
- package/source/abc/grammar.jison.js +155 -135
- package/source/lilylet/abcDecoder.ts +173 -33
- package/source/lilylet/grammar.jison.js +195 -185
- package/source/lilylet/lilylet.jison +8 -0
- package/source/lilylet/meiEncoder.ts +36 -8
- package/source/lilylet/serializer.ts +82 -30
- package/source/lilylet/types.ts +6 -1
|
@@ -4,22 +4,28 @@
|
|
|
4
4
|
* Converts ABC notation files to Lilylet's internal LilyletDoc format.
|
|
5
5
|
*/
|
|
6
6
|
import { LilyletDoc } from "./types";
|
|
7
|
+
/**
|
|
8
|
+
* Decode an ABC tune into a LilyletDoc
|
|
9
|
+
*/
|
|
10
|
+
export interface DecodeOptions {
|
|
11
|
+
catalogComments?: boolean;
|
|
12
|
+
}
|
|
7
13
|
/**
|
|
8
14
|
* Decode ABC notation string to LilyletDoc.
|
|
9
15
|
* If the ABC contains multiple tunes, only the first is decoded.
|
|
10
16
|
*/
|
|
11
|
-
export declare const decode: (abcString: string) => LilyletDoc;
|
|
17
|
+
export declare const decode: (abcString: string, options?: DecodeOptions) => LilyletDoc;
|
|
12
18
|
/**
|
|
13
19
|
* Decode ABC notation string to multiple LilyletDocs (one per tune).
|
|
14
20
|
*/
|
|
15
|
-
export declare const decodeAll: (abcString: string) => LilyletDoc[];
|
|
21
|
+
export declare const decodeAll: (abcString: string, options?: DecodeOptions) => LilyletDoc[];
|
|
16
22
|
/**
|
|
17
23
|
* Decode an ABC file to LilyletDoc
|
|
18
24
|
*/
|
|
19
|
-
export declare const decodeFile: (filePath: string) => Promise<LilyletDoc>;
|
|
25
|
+
export declare const decodeFile: (filePath: string, options?: DecodeOptions) => Promise<LilyletDoc>;
|
|
20
26
|
declare const _default: {
|
|
21
|
-
decode: (abcString: string) => LilyletDoc;
|
|
22
|
-
decodeAll: (abcString: string) => LilyletDoc[];
|
|
23
|
-
decodeFile: (filePath: string) => Promise<LilyletDoc>;
|
|
27
|
+
decode: (abcString: string, options?: DecodeOptions) => LilyletDoc;
|
|
28
|
+
decodeAll: (abcString: string, options?: DecodeOptions) => LilyletDoc[];
|
|
29
|
+
decodeFile: (filePath: string, options?: DecodeOptions) => Promise<LilyletDoc>;
|
|
24
30
|
};
|
|
25
31
|
export default _default;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Converts ABC notation files to Lilylet's internal LilyletDoc format.
|
|
5
5
|
*/
|
|
6
6
|
import parse from "../abc/parser.js";
|
|
7
|
-
import { Phonet, Accidental, ArticulationType, OrnamentType, DynamicType, HairpinType, PedalType, NavigationMarkType, } from "./types.js";
|
|
7
|
+
import { Phonet, Accidental, ArticulationType, OrnamentType, DynamicType, HairpinType, PedalType, NavigationMarkType, Placement, } from "./types.js";
|
|
8
8
|
// ============ Constants ============
|
|
9
9
|
// NotaGen catalog tags appear as leading single-% comments in ABC files,
|
|
10
10
|
// in the order: period, composer, instrumentation. They are mapped to
|
|
@@ -91,7 +91,7 @@ const convertAccidental = (acc) => {
|
|
|
91
91
|
* Uppercase C-B = octave 0, lowercase c-b = octave 1
|
|
92
92
|
* quotes (from ' and ,) add/subtract octaves
|
|
93
93
|
*/
|
|
94
|
-
const convertPitch = (abcPitch) => {
|
|
94
|
+
const convertPitch = (abcPitch, ctx) => {
|
|
95
95
|
const phonet = ABC_PHONET_MAP[abcPitch.phonet];
|
|
96
96
|
if (!phonet) {
|
|
97
97
|
throw new Error(`Unknown ABC phonet: ${abcPitch.phonet}`);
|
|
@@ -101,9 +101,38 @@ const convertPitch = (abcPitch) => {
|
|
|
101
101
|
const baseOctave = isLower ? 1 : 0;
|
|
102
102
|
const octave = baseOctave + (abcPitch.quotes || 0);
|
|
103
103
|
const pitch = { phonet, octave };
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Resolve the effective accidental.
|
|
105
|
+
const explicit = convertAccidental(abcPitch.acc); // accidental written on this note
|
|
106
|
+
const hasExplicit = abcPitch.acc !== null && abcPitch.acc !== undefined;
|
|
107
|
+
if (ctx) {
|
|
108
|
+
const memKey = `${phonet}:${octave}`;
|
|
109
|
+
if (hasExplicit) {
|
|
110
|
+
// Explicit accidental (incl. natural) overrides and is remembered for the measure.
|
|
111
|
+
if (abcPitch.acc === 0) {
|
|
112
|
+
ctx.measureAccidentals.set(memKey, "natural");
|
|
113
|
+
// natural cancels the key alteration → no accidental on the lilylet pitch
|
|
114
|
+
}
|
|
115
|
+
else if (explicit) {
|
|
116
|
+
ctx.measureAccidentals.set(memKey, explicit);
|
|
117
|
+
pitch.accidental = explicit;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// No explicit accidental: inherit from in-measure memory, else the key signature.
|
|
122
|
+
const remembered = ctx.measureAccidentals.get(memKey);
|
|
123
|
+
if (remembered !== undefined) {
|
|
124
|
+
if (remembered !== "natural")
|
|
125
|
+
pitch.accidental = remembered;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const fromKey = ctx.keyAlterations.get(phonet);
|
|
129
|
+
if (fromKey)
|
|
130
|
+
pitch.accidental = fromKey;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (explicit) {
|
|
135
|
+
pitch.accidental = explicit;
|
|
107
136
|
}
|
|
108
137
|
return pitch;
|
|
109
138
|
};
|
|
@@ -260,6 +289,45 @@ const convertKeySignature = (abcKey) => {
|
|
|
260
289
|
mode: (abcKey.mode === "minor" || abcKey.mode === "min") ? "minor" : "major",
|
|
261
290
|
};
|
|
262
291
|
};
|
|
292
|
+
// Circle-of-fifths: a lilylet key (pitch+accidental+mode) → set of letters the key
|
|
293
|
+
// signature alters, and the direction (sharp/flat). ABC note letters inherit these
|
|
294
|
+
// alterations implicitly (e.g. K:Eb makes B sound as Bb); lilylet pitches are absolute,
|
|
295
|
+
// so the decoder must bake the alteration into each pitch's accidental.
|
|
296
|
+
const SHARP_ORDER = [Phonet.f, Phonet.c, Phonet.g, Phonet.d, Phonet.a, Phonet.e, Phonet.b];
|
|
297
|
+
const FLAT_ORDER = [Phonet.b, Phonet.e, Phonet.a, Phonet.d, Phonet.g, Phonet.c, Phonet.f];
|
|
298
|
+
const KEY_NAME_TO_FIFTHS = {
|
|
299
|
+
c: 0, g: 1, d: 2, a: 3, e: 4, b: 5, "f#": 6, "c#": 7,
|
|
300
|
+
f: -1, bb: -2, eb: -3, ab: -4, db: -5, gb: -6, cb: -7,
|
|
301
|
+
};
|
|
302
|
+
// Compute the number of sharps/flats (fifths) for a resolved KeySignature.
|
|
303
|
+
const keySignatureFifths = (key) => {
|
|
304
|
+
let letter = key.pitch;
|
|
305
|
+
if (key.accidental === Accidental.sharp)
|
|
306
|
+
letter += "#";
|
|
307
|
+
else if (key.accidental === Accidental.flat)
|
|
308
|
+
letter += "b";
|
|
309
|
+
let fifths = KEY_NAME_TO_FIFTHS[letter];
|
|
310
|
+
if (fifths === undefined)
|
|
311
|
+
fifths = 0;
|
|
312
|
+
// Minor keys share the fifths of their relative major (3 fifths up).
|
|
313
|
+
if (key.mode === "minor")
|
|
314
|
+
fifths += 3;
|
|
315
|
+
return fifths;
|
|
316
|
+
};
|
|
317
|
+
// Map of phonet → accidental implied by the key signature (only altered letters present).
|
|
318
|
+
const keySignatureAlterations = (key) => {
|
|
319
|
+
const fifths = keySignatureFifths(key);
|
|
320
|
+
const map = new Map();
|
|
321
|
+
if (fifths > 0) {
|
|
322
|
+
for (let i = 0; i < fifths && i < SHARP_ORDER.length; i++)
|
|
323
|
+
map.set(SHARP_ORDER[i], Accidental.sharp);
|
|
324
|
+
}
|
|
325
|
+
else if (fifths < 0) {
|
|
326
|
+
for (let i = 0; i < -fifths && i < FLAT_ORDER.length; i++)
|
|
327
|
+
map.set(FLAT_ORDER[i], Accidental.flat);
|
|
328
|
+
}
|
|
329
|
+
return map;
|
|
330
|
+
};
|
|
263
331
|
/**
|
|
264
332
|
* Convert ABC clef string to Lilylet Clef
|
|
265
333
|
*/
|
|
@@ -492,13 +560,17 @@ const processExpressiveTerm = (term, pendingMarks, pendingContextChanges, slurDe
|
|
|
492
560
|
/**
|
|
493
561
|
* Process a single ABC BarPatch (one voice's content for one measure) into events.
|
|
494
562
|
*/
|
|
495
|
-
const processBarPatch = (patch, unitLength, slurDepth) => {
|
|
563
|
+
const processBarPatch = (patch, unitLength, slurDepth, pitchCtx) => {
|
|
496
564
|
const events = [];
|
|
497
565
|
const terms = patch.terms || [];
|
|
498
566
|
const pendingMarks = [];
|
|
499
567
|
const pendingContextChanges = [];
|
|
500
568
|
// Collect all events first, then handle broken rhythms and tuplets
|
|
501
569
|
const rawNoteRests = [];
|
|
570
|
+
// A broken-rhythm marker (>, <) belongs to the LEFT note but modifies the pair
|
|
571
|
+
// (left, right). We can only apply it once the right note has been pushed, so we
|
|
572
|
+
// stash it here and resolve it when the next note/rest arrives.
|
|
573
|
+
let pendingBroken = null;
|
|
502
574
|
let i = 0;
|
|
503
575
|
while (i < terms.length) {
|
|
504
576
|
const term = terms[i];
|
|
@@ -513,10 +585,19 @@ const processBarPatch = (patch, unitLength, slurDepth) => {
|
|
|
513
585
|
}
|
|
514
586
|
}
|
|
515
587
|
else if (ctrl.value?.root) {
|
|
588
|
+
const newKey = convertKeySignature(ctrl.value);
|
|
516
589
|
events.push({
|
|
517
590
|
type: "context",
|
|
518
|
-
key:
|
|
591
|
+
key: newKey,
|
|
519
592
|
});
|
|
593
|
+
// Update the active key alterations and clear in-measure accidentals.
|
|
594
|
+
if (pitchCtx) {
|
|
595
|
+
const alt = keySignatureAlterations(newKey);
|
|
596
|
+
pitchCtx.keyAlterations.clear();
|
|
597
|
+
for (const [k, v] of alt)
|
|
598
|
+
pitchCtx.keyAlterations.set(k, v);
|
|
599
|
+
pitchCtx.measureAccidentals.clear();
|
|
600
|
+
}
|
|
520
601
|
}
|
|
521
602
|
}
|
|
522
603
|
else if (ctrl.name === "M") {
|
|
@@ -555,7 +636,7 @@ const processBarPatch = (patch, unitLength, slurDepth) => {
|
|
|
555
636
|
while (j < terms.length && collected < r) {
|
|
556
637
|
const nextTerm = terms[j];
|
|
557
638
|
if (nextTerm.event) {
|
|
558
|
-
const evt = convertEventTerm(nextTerm, unitLength, pendingMarks, pendingContextChanges);
|
|
639
|
+
const evt = convertEventTerm(nextTerm, unitLength, pendingMarks, pendingContextChanges, pitchCtx);
|
|
559
640
|
if (evt) {
|
|
560
641
|
// Push any pending context changes before tuplet
|
|
561
642
|
for (const ctx of pendingContextChanges.splice(0)) {
|
|
@@ -600,7 +681,7 @@ const processBarPatch = (patch, unitLength, slurDepth) => {
|
|
|
600
681
|
}
|
|
601
682
|
// Grace notes
|
|
602
683
|
if (term.grace) {
|
|
603
|
-
const graceEvents = convertGraceEvents(term, unitLength);
|
|
684
|
+
const graceEvents = convertGraceEvents(term, unitLength, pitchCtx);
|
|
604
685
|
events.push(...graceEvents);
|
|
605
686
|
i++;
|
|
606
687
|
continue;
|
|
@@ -614,10 +695,25 @@ const processBarPatch = (patch, unitLength, slurDepth) => {
|
|
|
614
695
|
// Text
|
|
615
696
|
if (term.text !== undefined) {
|
|
616
697
|
const text = term.text;
|
|
617
|
-
//
|
|
698
|
+
// ABC chord/annotation text: a leading ^ / _ marks placement above / below;
|
|
699
|
+
// other position prefixes (<,>,@) just denote placement we don't model, so strip.
|
|
700
|
+
let placement;
|
|
701
|
+
let content = text;
|
|
618
702
|
if (text.startsWith("^")) {
|
|
619
|
-
|
|
703
|
+
placement = Placement.above;
|
|
704
|
+
content = text.slice(1);
|
|
705
|
+
}
|
|
706
|
+
else if (text.startsWith("_")) {
|
|
707
|
+
placement = Placement.below;
|
|
708
|
+
content = text.slice(1);
|
|
709
|
+
}
|
|
710
|
+
else if (/^[<>@]/.test(text)) {
|
|
711
|
+
content = text.slice(1);
|
|
620
712
|
}
|
|
713
|
+
const markup = { type: "markup", content };
|
|
714
|
+
if (placement)
|
|
715
|
+
markup.placement = placement;
|
|
716
|
+
events.push(markup);
|
|
621
717
|
i++;
|
|
622
718
|
continue;
|
|
623
719
|
}
|
|
@@ -628,7 +724,7 @@ const processBarPatch = (patch, unitLength, slurDepth) => {
|
|
|
628
724
|
for (const ctx of pendingContextChanges.splice(0)) {
|
|
629
725
|
events.push(ctx);
|
|
630
726
|
}
|
|
631
|
-
const evt = convertEventTerm(eventTerm, unitLength, pendingMarks, pendingContextChanges);
|
|
727
|
+
const evt = convertEventTerm(eventTerm, unitLength, pendingMarks, pendingContextChanges, pitchCtx);
|
|
632
728
|
if (evt) {
|
|
633
729
|
if (Array.isArray(evt)) {
|
|
634
730
|
events.push(...evt);
|
|
@@ -636,12 +732,18 @@ const processBarPatch = (patch, unitLength, slurDepth) => {
|
|
|
636
732
|
else {
|
|
637
733
|
events.push(evt);
|
|
638
734
|
}
|
|
639
|
-
//
|
|
640
|
-
|
|
735
|
+
// Resolve a broken-rhythm marker carried by the PREVIOUS note: it modifies
|
|
736
|
+
// the pair (previous note, this note). Apply now that this (the right) note exists.
|
|
737
|
+
if (pendingBroken !== null) {
|
|
641
738
|
const noteRestEvents = events.filter(e => e.type === "note" || e.type === "rest");
|
|
642
739
|
if (noteRestEvents.length >= 2) {
|
|
643
|
-
applyBrokenRhythm(noteRestEvents, noteRestEvents.length - 2,
|
|
740
|
+
applyBrokenRhythm(noteRestEvents, noteRestEvents.length - 2, pendingBroken);
|
|
644
741
|
}
|
|
742
|
+
pendingBroken = null;
|
|
743
|
+
}
|
|
744
|
+
// Stash this note's own broken marker (the right note is not parsed yet).
|
|
745
|
+
if (eventTerm.broken) {
|
|
746
|
+
pendingBroken = eventTerm.broken;
|
|
645
747
|
}
|
|
646
748
|
}
|
|
647
749
|
i++;
|
|
@@ -687,7 +789,7 @@ const getDefaultTupletMultiplier = (p) => {
|
|
|
687
789
|
/**
|
|
688
790
|
* Convert a single ABC EventTerm to Lilylet event(s)
|
|
689
791
|
*/
|
|
690
|
-
const convertEventTerm = (eventTerm, unitLength, pendingMarks, pendingContextChanges) => {
|
|
792
|
+
const convertEventTerm = (eventTerm, unitLength, pendingMarks, pendingContextChanges, pitchCtx) => {
|
|
691
793
|
const eventData = eventTerm.event;
|
|
692
794
|
if (!eventData)
|
|
693
795
|
return undefined;
|
|
@@ -708,12 +810,20 @@ const convertEventTerm = (eventTerm, unitLength, pendingMarks, pendingContextCha
|
|
|
708
810
|
if (firstPitch.phonet === "Z") {
|
|
709
811
|
rest.fullMeasure = true;
|
|
710
812
|
}
|
|
711
|
-
//
|
|
813
|
+
// A dynamic that precedes a rest (e.g. ABC "!p! z") has no note to attach to.
|
|
814
|
+
// Emit it as a standalone leading DynamicEvent before the rest; lilylet attaches
|
|
815
|
+
// it to the following sounding event. Other marks on a rest are dropped.
|
|
816
|
+
const leadingDynamics = pendingMarks
|
|
817
|
+
.filter(m => m.markType === "dynamic")
|
|
818
|
+
.map(m => ({ type: "dynamic", dynamicType: m.type }));
|
|
712
819
|
pendingMarks.length = 0;
|
|
820
|
+
if (leadingDynamics.length > 0) {
|
|
821
|
+
return [...leadingDynamics, rest];
|
|
822
|
+
}
|
|
713
823
|
return rest;
|
|
714
824
|
}
|
|
715
825
|
// Note or chord
|
|
716
|
-
const pitches = chord.pitches.filter(p => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x" && p.phonet !== "y").map(convertPitch);
|
|
826
|
+
const pitches = chord.pitches.filter(p => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x" && p.phonet !== "y").map(p => convertPitch(p, pitchCtx));
|
|
717
827
|
if (pitches.length === 0)
|
|
718
828
|
return undefined;
|
|
719
829
|
const duration = convertDuration(eventData.duration, unitLength);
|
|
@@ -742,7 +852,7 @@ const convertEventTerm = (eventTerm, unitLength, pendingMarks, pendingContextCha
|
|
|
742
852
|
/**
|
|
743
853
|
* Convert grace notes to NoteEvents with grace flag
|
|
744
854
|
*/
|
|
745
|
-
const convertGraceEvents = (graceTerm, unitLength) => {
|
|
855
|
+
const convertGraceEvents = (graceTerm, unitLength, pitchCtx) => {
|
|
746
856
|
const events = [];
|
|
747
857
|
if (!graceTerm.events)
|
|
748
858
|
return events;
|
|
@@ -752,7 +862,7 @@ const convertGraceEvents = (graceTerm, unitLength) => {
|
|
|
752
862
|
const chord = eventData.chord;
|
|
753
863
|
if (!chord || !chord.pitches)
|
|
754
864
|
continue;
|
|
755
|
-
const pitches = chord.pitches.filter((p) => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x").map(convertPitch);
|
|
865
|
+
const pitches = chord.pitches.filter((p) => p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x").map((p) => convertPitch(p, pitchCtx));
|
|
756
866
|
if (pitches.length === 0)
|
|
757
867
|
continue;
|
|
758
868
|
const duration = convertDuration(eventData.duration, unitLength);
|
|
@@ -767,11 +877,7 @@ const convertGraceEvents = (graceTerm, unitLength) => {
|
|
|
767
877
|
}
|
|
768
878
|
return events;
|
|
769
879
|
};
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Decode an ABC tune into a LilyletDoc
|
|
773
|
-
*/
|
|
774
|
-
const decodeTune = (tune) => {
|
|
880
|
+
const decodeTune = (tune, options = {}) => {
|
|
775
881
|
const headers = tune.header;
|
|
776
882
|
const body = tune.body;
|
|
777
883
|
// Extract header fields
|
|
@@ -793,13 +899,15 @@ const decodeTune = (tune) => {
|
|
|
793
899
|
for (const h of headers) {
|
|
794
900
|
const comment = h.comment;
|
|
795
901
|
if (typeof comment === "string") {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
metadata.genre
|
|
799
|
-
|
|
800
|
-
metadata.instrument
|
|
801
|
-
|
|
802
|
-
metadata.composer
|
|
902
|
+
if (options.catalogComments) {
|
|
903
|
+
const value = comment.trim();
|
|
904
|
+
if (!metadata.genre && NOTAGEN_PERIOD_SET.has(value))
|
|
905
|
+
metadata.genre = value;
|
|
906
|
+
else if (!metadata.instrument && NOTAGEN_INSTRUMENTATION_SET.has(value))
|
|
907
|
+
metadata.instrument = value;
|
|
908
|
+
else if (!metadata.composer && NOTAGEN_COMPOSER_SET.has(value))
|
|
909
|
+
metadata.composer = value;
|
|
910
|
+
}
|
|
803
911
|
continue;
|
|
804
912
|
}
|
|
805
913
|
if (h.staffLayout)
|
|
@@ -891,6 +999,11 @@ const decodeTune = (tune) => {
|
|
|
891
999
|
// ABC measures contain BarPatches, each with a voice control V:n
|
|
892
1000
|
const measures = body.measures;
|
|
893
1001
|
const voiceSlurDepths = new Map();
|
|
1002
|
+
// Per-voice key alterations (persist across measures; mutated by inline [K:] changes).
|
|
1003
|
+
// Initialized from the tune's header key signature so bare ABC letters pick up the
|
|
1004
|
+
// key's sharps/flats, which lilylet pitches must carry explicitly.
|
|
1005
|
+
const initialKeyAlterations = keySig ? keySignatureAlterations(keySig) : new Map();
|
|
1006
|
+
const voiceKeyAlterations = new Map();
|
|
894
1007
|
// Process each ABC measure into Lilylet Measure
|
|
895
1008
|
const lilyletMeasures = [];
|
|
896
1009
|
for (let mi = 0; mi < measures.length; mi++) {
|
|
@@ -909,11 +1022,20 @@ const decodeTune = (tune) => {
|
|
|
909
1022
|
for (const [voiceNum, patches] of voicePatches) {
|
|
910
1023
|
const slurDepth = voiceSlurDepths.get(voiceNum) || { count: 0 };
|
|
911
1024
|
voiceSlurDepths.set(voiceNum, slurDepth);
|
|
1025
|
+
// Resolve this voice's persistent key alterations (seeded from the header key).
|
|
1026
|
+
if (!voiceKeyAlterations.has(voiceNum)) {
|
|
1027
|
+
voiceKeyAlterations.set(voiceNum, new Map(initialKeyAlterations));
|
|
1028
|
+
}
|
|
1029
|
+
// Fresh in-measure accidental memory for each measure (standard notation rule).
|
|
1030
|
+
const pitchCtx = {
|
|
1031
|
+
keyAlterations: voiceKeyAlterations.get(voiceNum),
|
|
1032
|
+
measureAccidentals: new Map(),
|
|
1033
|
+
};
|
|
912
1034
|
// Merge all patches for this voice in this measure
|
|
913
1035
|
const allEvents = [];
|
|
914
1036
|
let barline;
|
|
915
1037
|
for (const patch of patches) {
|
|
916
|
-
const result = processBarPatch(patch, unitLength, slurDepth);
|
|
1038
|
+
const result = processBarPatch(patch, unitLength, slurDepth, pitchCtx);
|
|
917
1039
|
allEvents.push(...result.events);
|
|
918
1040
|
if (result.barline && result.barline !== "|") {
|
|
919
1041
|
barline = result.barline;
|
|
@@ -1013,30 +1135,30 @@ const decodeTune = (tune) => {
|
|
|
1013
1135
|
* Decode ABC notation string to LilyletDoc.
|
|
1014
1136
|
* If the ABC contains multiple tunes, only the first is decoded.
|
|
1015
1137
|
*/
|
|
1016
|
-
export const decode = (abcString) => {
|
|
1138
|
+
export const decode = (abcString, options = {}) => {
|
|
1017
1139
|
const tunes = parse(abcString);
|
|
1018
1140
|
if (!tunes || tunes.length === 0) {
|
|
1019
1141
|
throw new Error("No tunes found in ABC notation");
|
|
1020
1142
|
}
|
|
1021
|
-
return decodeTune(tunes[0]);
|
|
1143
|
+
return decodeTune(tunes[0], options);
|
|
1022
1144
|
};
|
|
1023
1145
|
/**
|
|
1024
1146
|
* Decode ABC notation string to multiple LilyletDocs (one per tune).
|
|
1025
1147
|
*/
|
|
1026
|
-
export const decodeAll = (abcString) => {
|
|
1148
|
+
export const decodeAll = (abcString, options = {}) => {
|
|
1027
1149
|
const tunes = parse(abcString);
|
|
1028
1150
|
if (!tunes || tunes.length === 0) {
|
|
1029
1151
|
throw new Error("No tunes found in ABC notation");
|
|
1030
1152
|
}
|
|
1031
|
-
return tunes.map(decodeTune);
|
|
1153
|
+
return tunes.map(t => decodeTune(t, options));
|
|
1032
1154
|
};
|
|
1033
1155
|
/**
|
|
1034
1156
|
* Decode an ABC file to LilyletDoc
|
|
1035
1157
|
*/
|
|
1036
|
-
export const decodeFile = async (filePath) => {
|
|
1158
|
+
export const decodeFile = async (filePath, options = {}) => {
|
|
1037
1159
|
const fs = await import("fs/promises");
|
|
1038
1160
|
const content = await fs.readFile(filePath, "utf-8");
|
|
1039
|
-
return decode(content);
|
|
1161
|
+
return decode(content, options);
|
|
1040
1162
|
};
|
|
1041
1163
|
export default {
|
|
1042
1164
|
decode,
|