@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
|
@@ -34,6 +34,9 @@ import {
|
|
|
34
34
|
NavigationMarkType,
|
|
35
35
|
Tempo,
|
|
36
36
|
BarlineEvent,
|
|
37
|
+
MarkupEvent,
|
|
38
|
+
DynamicEvent,
|
|
39
|
+
Placement,
|
|
37
40
|
} from "./types";
|
|
38
41
|
|
|
39
42
|
|
|
@@ -125,12 +128,25 @@ const convertAccidental = (acc: number | null): Accidental | undefined => {
|
|
|
125
128
|
}
|
|
126
129
|
};
|
|
127
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Pitch-conversion context for resolving ABC's implicit accidentals into lilylet's
|
|
133
|
+
* absolute pitch model. ABC applies the key signature and in-measure accidentals
|
|
134
|
+
* implicitly to bare note letters; lilylet note names carry their own accidental, so
|
|
135
|
+
* we resolve the effective accidental here.
|
|
136
|
+
*/
|
|
137
|
+
interface PitchContext {
|
|
138
|
+
keyAlterations: Map<Phonet, Accidental>; // letters altered by the active key signature
|
|
139
|
+
// In-measure accidental memory, keyed by "phonet:octave". An explicit accidental
|
|
140
|
+
// persists for the rest of the measure at that pitch/octave (standard notation rule).
|
|
141
|
+
measureAccidentals: Map<string, Accidental | "natural">;
|
|
142
|
+
}
|
|
143
|
+
|
|
128
144
|
/**
|
|
129
145
|
* Convert ABC pitch to Lilylet Pitch
|
|
130
146
|
* Uppercase C-B = octave 0, lowercase c-b = octave 1
|
|
131
147
|
* quotes (from ' and ,) add/subtract octaves
|
|
132
148
|
*/
|
|
133
|
-
const convertPitch = (abcPitch: ABC.Pitch): Pitch => {
|
|
149
|
+
const convertPitch = (abcPitch: ABC.Pitch, ctx?: PitchContext): Pitch => {
|
|
134
150
|
const phonet = ABC_PHONET_MAP[abcPitch.phonet];
|
|
135
151
|
if (!phonet) {
|
|
136
152
|
throw new Error(`Unknown ABC phonet: ${abcPitch.phonet}`);
|
|
@@ -142,10 +158,36 @@ const convertPitch = (abcPitch: ABC.Pitch): Pitch => {
|
|
|
142
158
|
const octave = baseOctave + (abcPitch.quotes || 0);
|
|
143
159
|
|
|
144
160
|
const pitch: Pitch = { phonet, octave };
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
161
|
+
|
|
162
|
+
// Resolve the effective accidental.
|
|
163
|
+
const explicit = convertAccidental(abcPitch.acc); // accidental written on this note
|
|
164
|
+
const hasExplicit = abcPitch.acc !== null && abcPitch.acc !== undefined;
|
|
165
|
+
|
|
166
|
+
if (ctx) {
|
|
167
|
+
const memKey = `${phonet}:${octave}`;
|
|
168
|
+
if (hasExplicit) {
|
|
169
|
+
// Explicit accidental (incl. natural) overrides and is remembered for the measure.
|
|
170
|
+
if (abcPitch.acc === 0) {
|
|
171
|
+
ctx.measureAccidentals.set(memKey, "natural");
|
|
172
|
+
// natural cancels the key alteration → no accidental on the lilylet pitch
|
|
173
|
+
} else if (explicit) {
|
|
174
|
+
ctx.measureAccidentals.set(memKey, explicit);
|
|
175
|
+
pitch.accidental = explicit;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// No explicit accidental: inherit from in-measure memory, else the key signature.
|
|
179
|
+
const remembered = ctx.measureAccidentals.get(memKey);
|
|
180
|
+
if (remembered !== undefined) {
|
|
181
|
+
if (remembered !== "natural") pitch.accidental = remembered;
|
|
182
|
+
} else {
|
|
183
|
+
const fromKey = ctx.keyAlterations.get(phonet);
|
|
184
|
+
if (fromKey) pitch.accidental = fromKey;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else if (explicit) {
|
|
188
|
+
pitch.accidental = explicit;
|
|
148
189
|
}
|
|
190
|
+
|
|
149
191
|
return pitch;
|
|
150
192
|
};
|
|
151
193
|
|
|
@@ -310,6 +352,42 @@ const convertKeySignature = (abcKey: ABC.KeySignature): KeySignature => {
|
|
|
310
352
|
};
|
|
311
353
|
};
|
|
312
354
|
|
|
355
|
+
// Circle-of-fifths: a lilylet key (pitch+accidental+mode) → set of letters the key
|
|
356
|
+
// signature alters, and the direction (sharp/flat). ABC note letters inherit these
|
|
357
|
+
// alterations implicitly (e.g. K:Eb makes B sound as Bb); lilylet pitches are absolute,
|
|
358
|
+
// so the decoder must bake the alteration into each pitch's accidental.
|
|
359
|
+
const SHARP_ORDER: Phonet[] = [Phonet.f, Phonet.c, Phonet.g, Phonet.d, Phonet.a, Phonet.e, Phonet.b];
|
|
360
|
+
const FLAT_ORDER: Phonet[] = [Phonet.b, Phonet.e, Phonet.a, Phonet.d, Phonet.g, Phonet.c, Phonet.f];
|
|
361
|
+
|
|
362
|
+
const KEY_NAME_TO_FIFTHS: Record<string, number> = {
|
|
363
|
+
c: 0, g: 1, d: 2, a: 3, e: 4, b: 5, "f#": 6, "c#": 7,
|
|
364
|
+
f: -1, bb: -2, eb: -3, ab: -4, db: -5, gb: -6, cb: -7,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// Compute the number of sharps/flats (fifths) for a resolved KeySignature.
|
|
368
|
+
const keySignatureFifths = (key: KeySignature): number => {
|
|
369
|
+
let letter = key.pitch as string;
|
|
370
|
+
if (key.accidental === Accidental.sharp) letter += "#";
|
|
371
|
+
else if (key.accidental === Accidental.flat) letter += "b";
|
|
372
|
+
let fifths = KEY_NAME_TO_FIFTHS[letter];
|
|
373
|
+
if (fifths === undefined) fifths = 0;
|
|
374
|
+
// Minor keys share the fifths of their relative major (3 fifths up).
|
|
375
|
+
if (key.mode === "minor") fifths += 3;
|
|
376
|
+
return fifths;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Map of phonet → accidental implied by the key signature (only altered letters present).
|
|
380
|
+
const keySignatureAlterations = (key: KeySignature): Map<Phonet, Accidental> => {
|
|
381
|
+
const fifths = keySignatureFifths(key);
|
|
382
|
+
const map = new Map<Phonet, Accidental>();
|
|
383
|
+
if (fifths > 0) {
|
|
384
|
+
for (let i = 0; i < fifths && i < SHARP_ORDER.length; i++) map.set(SHARP_ORDER[i], Accidental.sharp);
|
|
385
|
+
} else if (fifths < 0) {
|
|
386
|
+
for (let i = 0; i < -fifths && i < FLAT_ORDER.length; i++) map.set(FLAT_ORDER[i], Accidental.flat);
|
|
387
|
+
}
|
|
388
|
+
return map;
|
|
389
|
+
};
|
|
390
|
+
|
|
313
391
|
/**
|
|
314
392
|
* Convert ABC clef string to Lilylet Clef
|
|
315
393
|
*/
|
|
@@ -567,7 +645,8 @@ interface VoiceConfig {
|
|
|
567
645
|
const processBarPatch = (
|
|
568
646
|
patch: ABC.BarPatch,
|
|
569
647
|
unitLength: { numerator: number; denominator: number },
|
|
570
|
-
slurDepth: { count: number }
|
|
648
|
+
slurDepth: { count: number },
|
|
649
|
+
pitchCtx?: PitchContext
|
|
571
650
|
): { events: Event[]; barline?: string } => {
|
|
572
651
|
const events: Event[] = [];
|
|
573
652
|
const terms = patch.terms || [];
|
|
@@ -577,6 +656,11 @@ const processBarPatch = (
|
|
|
577
656
|
// Collect all events first, then handle broken rhythms and tuplets
|
|
578
657
|
const rawNoteRests: { event: NoteEvent | RestEvent; index: number; broken?: number }[] = [];
|
|
579
658
|
|
|
659
|
+
// A broken-rhythm marker (>, <) belongs to the LEFT note but modifies the pair
|
|
660
|
+
// (left, right). We can only apply it once the right note has been pushed, so we
|
|
661
|
+
// stash it here and resolve it when the next note/rest arrives.
|
|
662
|
+
let pendingBroken: number | null = null;
|
|
663
|
+
|
|
580
664
|
let i = 0;
|
|
581
665
|
while (i < terms.length) {
|
|
582
666
|
const term = terms[i];
|
|
@@ -591,10 +675,18 @@ const processBarPatch = (
|
|
|
591
675
|
events.push({ type: "context", clef } as ContextChange);
|
|
592
676
|
}
|
|
593
677
|
} else if (ctrl.value?.root) {
|
|
678
|
+
const newKey = convertKeySignature(ctrl.value);
|
|
594
679
|
events.push({
|
|
595
680
|
type: "context",
|
|
596
|
-
key:
|
|
681
|
+
key: newKey,
|
|
597
682
|
} as ContextChange);
|
|
683
|
+
// Update the active key alterations and clear in-measure accidentals.
|
|
684
|
+
if (pitchCtx) {
|
|
685
|
+
const alt = keySignatureAlterations(newKey);
|
|
686
|
+
pitchCtx.keyAlterations.clear();
|
|
687
|
+
for (const [k, v] of alt) pitchCtx.keyAlterations.set(k, v);
|
|
688
|
+
pitchCtx.measureAccidentals.clear();
|
|
689
|
+
}
|
|
598
690
|
}
|
|
599
691
|
} else if (ctrl.name === "M") {
|
|
600
692
|
if (ctrl.value?.numerator && ctrl.value?.denominator) {
|
|
@@ -632,7 +724,7 @@ const processBarPatch = (
|
|
|
632
724
|
while (j < terms.length && collected < r) {
|
|
633
725
|
const nextTerm = terms[j];
|
|
634
726
|
if ((nextTerm as ABC.EventTerm).event) {
|
|
635
|
-
const evt = convertEventTerm(nextTerm as ABC.EventTerm, unitLength, pendingMarks, pendingContextChanges);
|
|
727
|
+
const evt = convertEventTerm(nextTerm as ABC.EventTerm, unitLength, pendingMarks, pendingContextChanges, pitchCtx);
|
|
636
728
|
if (evt) {
|
|
637
729
|
// Push any pending context changes before tuplet
|
|
638
730
|
for (const ctx of pendingContextChanges.splice(0)) {
|
|
@@ -676,7 +768,7 @@ const processBarPatch = (
|
|
|
676
768
|
|
|
677
769
|
// Grace notes
|
|
678
770
|
if ((term as any).grace) {
|
|
679
|
-
const graceEvents = convertGraceEvents(term as any, unitLength);
|
|
771
|
+
const graceEvents = convertGraceEvents(term as any, unitLength, pitchCtx);
|
|
680
772
|
events.push(...graceEvents);
|
|
681
773
|
i++;
|
|
682
774
|
continue;
|
|
@@ -692,10 +784,16 @@ const processBarPatch = (
|
|
|
692
784
|
// Text
|
|
693
785
|
if ((term as ABC.TextTerm).text !== undefined) {
|
|
694
786
|
const text = (term as ABC.TextTerm).text;
|
|
695
|
-
//
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
787
|
+
// ABC chord/annotation text: a leading ^ / _ marks placement above / below;
|
|
788
|
+
// other position prefixes (<,>,@) just denote placement we don't model, so strip.
|
|
789
|
+
let placement: Placement | undefined;
|
|
790
|
+
let content = text;
|
|
791
|
+
if (text.startsWith("^")) { placement = Placement.above; content = text.slice(1); }
|
|
792
|
+
else if (text.startsWith("_")) { placement = Placement.below; content = text.slice(1); }
|
|
793
|
+
else if (/^[<>@]/.test(text)) { content = text.slice(1); }
|
|
794
|
+
const markup: MarkupEvent = { type: "markup", content };
|
|
795
|
+
if (placement) markup.placement = placement;
|
|
796
|
+
events.push(markup);
|
|
699
797
|
i++;
|
|
700
798
|
continue;
|
|
701
799
|
}
|
|
@@ -709,7 +807,7 @@ const processBarPatch = (
|
|
|
709
807
|
events.push(ctx);
|
|
710
808
|
}
|
|
711
809
|
|
|
712
|
-
const evt = convertEventTerm(eventTerm, unitLength, pendingMarks, pendingContextChanges);
|
|
810
|
+
const evt = convertEventTerm(eventTerm, unitLength, pendingMarks, pendingContextChanges, pitchCtx);
|
|
713
811
|
if (evt) {
|
|
714
812
|
if (Array.isArray(evt)) {
|
|
715
813
|
events.push(...evt);
|
|
@@ -717,12 +815,19 @@ const processBarPatch = (
|
|
|
717
815
|
events.push(evt);
|
|
718
816
|
}
|
|
719
817
|
|
|
720
|
-
//
|
|
721
|
-
|
|
818
|
+
// Resolve a broken-rhythm marker carried by the PREVIOUS note: it modifies
|
|
819
|
+
// the pair (previous note, this note). Apply now that this (the right) note exists.
|
|
820
|
+
if (pendingBroken !== null) {
|
|
722
821
|
const noteRestEvents = events.filter(e => e.type === "note" || e.type === "rest") as (NoteEvent | RestEvent)[];
|
|
723
822
|
if (noteRestEvents.length >= 2) {
|
|
724
|
-
applyBrokenRhythm(noteRestEvents, noteRestEvents.length - 2,
|
|
823
|
+
applyBrokenRhythm(noteRestEvents, noteRestEvents.length - 2, pendingBroken);
|
|
725
824
|
}
|
|
825
|
+
pendingBroken = null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Stash this note's own broken marker (the right note is not parsed yet).
|
|
829
|
+
if (eventTerm.broken) {
|
|
830
|
+
pendingBroken = eventTerm.broken;
|
|
726
831
|
}
|
|
727
832
|
}
|
|
728
833
|
i++;
|
|
@@ -770,7 +875,8 @@ const convertEventTerm = (
|
|
|
770
875
|
eventTerm: ABC.EventTerm,
|
|
771
876
|
unitLength: { numerator: number; denominator: number },
|
|
772
877
|
pendingMarks: Mark[],
|
|
773
|
-
pendingContextChanges: ContextChange[]
|
|
878
|
+
pendingContextChanges: ContextChange[],
|
|
879
|
+
pitchCtx?: PitchContext
|
|
774
880
|
): Event | Event[] | undefined => {
|
|
775
881
|
const eventData = eventTerm.event;
|
|
776
882
|
if (!eventData) return undefined;
|
|
@@ -794,16 +900,24 @@ const convertEventTerm = (
|
|
|
794
900
|
rest.fullMeasure = true;
|
|
795
901
|
}
|
|
796
902
|
|
|
797
|
-
//
|
|
903
|
+
// A dynamic that precedes a rest (e.g. ABC "!p! z") has no note to attach to.
|
|
904
|
+
// Emit it as a standalone leading DynamicEvent before the rest; lilylet attaches
|
|
905
|
+
// it to the following sounding event. Other marks on a rest are dropped.
|
|
906
|
+
const leadingDynamics: DynamicEvent[] = pendingMarks
|
|
907
|
+
.filter(m => m.markType === "dynamic")
|
|
908
|
+
.map(m => ({ type: "dynamic", dynamicType: (m as { type: DynamicType }).type }));
|
|
798
909
|
pendingMarks.length = 0;
|
|
799
910
|
|
|
911
|
+
if (leadingDynamics.length > 0) {
|
|
912
|
+
return [...leadingDynamics, rest];
|
|
913
|
+
}
|
|
800
914
|
return rest;
|
|
801
915
|
}
|
|
802
916
|
|
|
803
917
|
// Note or chord
|
|
804
918
|
const pitches = chord.pitches.filter(p =>
|
|
805
919
|
p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x" && p.phonet !== "y"
|
|
806
|
-
).map(convertPitch);
|
|
920
|
+
).map(p => convertPitch(p, pitchCtx));
|
|
807
921
|
|
|
808
922
|
if (pitches.length === 0) return undefined;
|
|
809
923
|
|
|
@@ -841,7 +955,8 @@ const convertEventTerm = (
|
|
|
841
955
|
*/
|
|
842
956
|
const convertGraceEvents = (
|
|
843
957
|
graceTerm: any,
|
|
844
|
-
unitLength: { numerator: number; denominator: number }
|
|
958
|
+
unitLength: { numerator: number; denominator: number },
|
|
959
|
+
pitchCtx?: PitchContext
|
|
845
960
|
): NoteEvent[] => {
|
|
846
961
|
const events: NoteEvent[] = [];
|
|
847
962
|
if (!graceTerm.events) return events;
|
|
@@ -854,7 +969,7 @@ const convertGraceEvents = (
|
|
|
854
969
|
|
|
855
970
|
const pitches = chord.pitches.filter((p: ABC.Pitch) =>
|
|
856
971
|
p.phonet !== "z" && p.phonet !== "Z" && p.phonet !== "x"
|
|
857
|
-
).map(convertPitch);
|
|
972
|
+
).map((p: ABC.Pitch) => convertPitch(p, pitchCtx));
|
|
858
973
|
|
|
859
974
|
if (pitches.length === 0) continue;
|
|
860
975
|
|
|
@@ -878,7 +993,14 @@ const convertGraceEvents = (
|
|
|
878
993
|
/**
|
|
879
994
|
* Decode an ABC tune into a LilyletDoc
|
|
880
995
|
*/
|
|
881
|
-
|
|
996
|
+
export interface DecodeOptions {
|
|
997
|
+
// Extract NotaGen catalog metadata from the leading
|
|
998
|
+
// %period/%composer/%instrumentation comments. These are NotaGen's own
|
|
999
|
+
// convention, not standard ABC, so this is off by default.
|
|
1000
|
+
catalogComments?: boolean;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const decodeTune = (tune: ABC.Tune, options: DecodeOptions = {}): LilyletDoc => {
|
|
882
1004
|
const headers = tune.header;
|
|
883
1005
|
const body = tune.body;
|
|
884
1006
|
|
|
@@ -903,10 +1025,12 @@ const decodeTune = (tune: ABC.Tune): LilyletDoc => {
|
|
|
903
1025
|
for (const h of headers) {
|
|
904
1026
|
const comment = (h as any).comment;
|
|
905
1027
|
if (typeof comment === "string") {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1028
|
+
if (options.catalogComments) {
|
|
1029
|
+
const value = comment.trim();
|
|
1030
|
+
if (!metadata.genre && NOTAGEN_PERIOD_SET.has(value)) metadata.genre = value;
|
|
1031
|
+
else if (!metadata.instrument && NOTAGEN_INSTRUMENTATION_SET.has(value)) metadata.instrument = value;
|
|
1032
|
+
else if (!metadata.composer && NOTAGEN_COMPOSER_SET.has(value)) metadata.composer = value;
|
|
1033
|
+
}
|
|
910
1034
|
continue;
|
|
911
1035
|
}
|
|
912
1036
|
if ((h as any).staffLayout) continue;
|
|
@@ -996,6 +1120,12 @@ const decodeTune = (tune: ABC.Tune): LilyletDoc => {
|
|
|
996
1120
|
const measures = body.measures;
|
|
997
1121
|
const voiceSlurDepths = new Map<number, { count: number }>();
|
|
998
1122
|
|
|
1123
|
+
// Per-voice key alterations (persist across measures; mutated by inline [K:] changes).
|
|
1124
|
+
// Initialized from the tune's header key signature so bare ABC letters pick up the
|
|
1125
|
+
// key's sharps/flats, which lilylet pitches must carry explicitly.
|
|
1126
|
+
const initialKeyAlterations = keySig ? keySignatureAlterations(keySig) : new Map<Phonet, Accidental>();
|
|
1127
|
+
const voiceKeyAlterations = new Map<number, Map<Phonet, Accidental>>();
|
|
1128
|
+
|
|
999
1129
|
// Process each ABC measure into Lilylet Measure
|
|
1000
1130
|
const lilyletMeasures: Measure[] = [];
|
|
1001
1131
|
|
|
@@ -1019,12 +1149,22 @@ const decodeTune = (tune: ABC.Tune): LilyletDoc => {
|
|
|
1019
1149
|
const slurDepth = voiceSlurDepths.get(voiceNum) || { count: 0 };
|
|
1020
1150
|
voiceSlurDepths.set(voiceNum, slurDepth);
|
|
1021
1151
|
|
|
1152
|
+
// Resolve this voice's persistent key alterations (seeded from the header key).
|
|
1153
|
+
if (!voiceKeyAlterations.has(voiceNum)) {
|
|
1154
|
+
voiceKeyAlterations.set(voiceNum, new Map(initialKeyAlterations));
|
|
1155
|
+
}
|
|
1156
|
+
// Fresh in-measure accidental memory for each measure (standard notation rule).
|
|
1157
|
+
const pitchCtx: PitchContext = {
|
|
1158
|
+
keyAlterations: voiceKeyAlterations.get(voiceNum)!,
|
|
1159
|
+
measureAccidentals: new Map(),
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1022
1162
|
// Merge all patches for this voice in this measure
|
|
1023
1163
|
const allEvents: Event[] = [];
|
|
1024
1164
|
let barline: string | undefined;
|
|
1025
1165
|
|
|
1026
1166
|
for (const patch of patches) {
|
|
1027
|
-
const result = processBarPatch(patch, unitLength, slurDepth);
|
|
1167
|
+
const result = processBarPatch(patch, unitLength, slurDepth, pitchCtx);
|
|
1028
1168
|
allEvents.push(...result.events);
|
|
1029
1169
|
if (result.barline && result.barline !== "|") {
|
|
1030
1170
|
barline = result.barline;
|
|
@@ -1144,32 +1284,32 @@ const decodeTune = (tune: ABC.Tune): LilyletDoc => {
|
|
|
1144
1284
|
* Decode ABC notation string to LilyletDoc.
|
|
1145
1285
|
* If the ABC contains multiple tunes, only the first is decoded.
|
|
1146
1286
|
*/
|
|
1147
|
-
export const decode = (abcString: string): LilyletDoc => {
|
|
1287
|
+
export const decode = (abcString: string, options: DecodeOptions = {}): LilyletDoc => {
|
|
1148
1288
|
const tunes = parse(abcString);
|
|
1149
1289
|
if (!tunes || tunes.length === 0) {
|
|
1150
1290
|
throw new Error("No tunes found in ABC notation");
|
|
1151
1291
|
}
|
|
1152
|
-
return decodeTune(tunes[0]);
|
|
1292
|
+
return decodeTune(tunes[0], options);
|
|
1153
1293
|
};
|
|
1154
1294
|
|
|
1155
1295
|
/**
|
|
1156
1296
|
* Decode ABC notation string to multiple LilyletDocs (one per tune).
|
|
1157
1297
|
*/
|
|
1158
|
-
export const decodeAll = (abcString: string): LilyletDoc[] => {
|
|
1298
|
+
export const decodeAll = (abcString: string, options: DecodeOptions = {}): LilyletDoc[] => {
|
|
1159
1299
|
const tunes = parse(abcString);
|
|
1160
1300
|
if (!tunes || tunes.length === 0) {
|
|
1161
1301
|
throw new Error("No tunes found in ABC notation");
|
|
1162
1302
|
}
|
|
1163
|
-
return tunes.map(decodeTune);
|
|
1303
|
+
return tunes.map(t => decodeTune(t, options));
|
|
1164
1304
|
};
|
|
1165
1305
|
|
|
1166
1306
|
/**
|
|
1167
1307
|
* Decode an ABC file to LilyletDoc
|
|
1168
1308
|
*/
|
|
1169
|
-
export const decodeFile = async (filePath: string): Promise<LilyletDoc> => {
|
|
1309
|
+
export const decodeFile = async (filePath: string, options: DecodeOptions = {}): Promise<LilyletDoc> => {
|
|
1170
1310
|
const fs = await import("fs/promises");
|
|
1171
1311
|
const content = await fs.readFile(filePath, "utf-8");
|
|
1172
|
-
return decode(content);
|
|
1312
|
+
return decode(content, options);
|
|
1173
1313
|
};
|
|
1174
1314
|
|
|
1175
1315
|
export default {
|