musicxml-io 0.3.2 → 0.3.3
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/dist/index.d.mts +49 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +2874 -1
- package/dist/index.mjs +2872 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -201,6 +201,7 @@ __export(src_exports, {
|
|
|
201
201
|
modifyTempo: () => modifyTempo,
|
|
202
202
|
moveNoteToStaff: () => moveNoteToStaff,
|
|
203
203
|
parse: () => parse,
|
|
204
|
+
parseAbc: () => parseAbc,
|
|
204
205
|
parseAuto: () => parseAuto,
|
|
205
206
|
parseCompressed: () => parseCompressed,
|
|
206
207
|
parseFile: () => parseFile,
|
|
@@ -236,6 +237,7 @@ __export(src_exports, {
|
|
|
236
237
|
removeWedge: () => removeWedge,
|
|
237
238
|
scoresEqual: () => scoresEqual,
|
|
238
239
|
serialize: () => serialize,
|
|
240
|
+
serializeAbc: () => serializeAbc,
|
|
239
241
|
serializeCompressed: () => serializeCompressed,
|
|
240
242
|
serializeToFile: () => serializeToFile,
|
|
241
243
|
setBarline: () => setBarline,
|
|
@@ -2730,6 +2732,1750 @@ function parseAuto(data) {
|
|
|
2730
2732
|
return parse(xmlString);
|
|
2731
2733
|
}
|
|
2732
2734
|
|
|
2735
|
+
// src/importers/abc.ts
|
|
2736
|
+
var DIVISIONS = 960;
|
|
2737
|
+
var NOTE_TYPE_MAP = {
|
|
2738
|
+
// duration in terms of quarter notes
|
|
2739
|
+
16: "long",
|
|
2740
|
+
8: "breve",
|
|
2741
|
+
4: "whole",
|
|
2742
|
+
2: "half",
|
|
2743
|
+
1: "quarter",
|
|
2744
|
+
0.5: "eighth",
|
|
2745
|
+
0.25: "16th",
|
|
2746
|
+
0.125: "32nd",
|
|
2747
|
+
0.0625: "64th"
|
|
2748
|
+
};
|
|
2749
|
+
var KEY_FIFTHS = {
|
|
2750
|
+
"Cb": -7,
|
|
2751
|
+
"Gb": -6,
|
|
2752
|
+
"Db": -5,
|
|
2753
|
+
"Ab": -4,
|
|
2754
|
+
"Eb": -3,
|
|
2755
|
+
"Bb": -2,
|
|
2756
|
+
"F": -1,
|
|
2757
|
+
"C": 0,
|
|
2758
|
+
"G": 1,
|
|
2759
|
+
"D": 2,
|
|
2760
|
+
"A": 3,
|
|
2761
|
+
"E": 4,
|
|
2762
|
+
"B": 5,
|
|
2763
|
+
"F#": 6,
|
|
2764
|
+
"C#": 7
|
|
2765
|
+
};
|
|
2766
|
+
var MODE_OFFSET = {
|
|
2767
|
+
"major": 0,
|
|
2768
|
+
"maj": 0,
|
|
2769
|
+
"ion": 0,
|
|
2770
|
+
"ionian": 0,
|
|
2771
|
+
"": 0,
|
|
2772
|
+
"minor": -3,
|
|
2773
|
+
"min": -3,
|
|
2774
|
+
"m": -3,
|
|
2775
|
+
"dorian": -2,
|
|
2776
|
+
"dor": -2,
|
|
2777
|
+
"phrygian": -4,
|
|
2778
|
+
"phr": -4,
|
|
2779
|
+
"lydian": 1,
|
|
2780
|
+
"lyd": 1,
|
|
2781
|
+
"mixolydian": -1,
|
|
2782
|
+
"mix": -1,
|
|
2783
|
+
"aeolian": -3,
|
|
2784
|
+
"aeo": -3,
|
|
2785
|
+
"locrian": -5,
|
|
2786
|
+
"loc": -5
|
|
2787
|
+
};
|
|
2788
|
+
var MODE_NAME = {
|
|
2789
|
+
"major": "major",
|
|
2790
|
+
"maj": "major",
|
|
2791
|
+
"ion": "ionian",
|
|
2792
|
+
"ionian": "ionian",
|
|
2793
|
+
"": "major",
|
|
2794
|
+
"minor": "minor",
|
|
2795
|
+
"min": "minor",
|
|
2796
|
+
"m": "minor",
|
|
2797
|
+
"dorian": "dorian",
|
|
2798
|
+
"dor": "dorian",
|
|
2799
|
+
"phrygian": "phrygian",
|
|
2800
|
+
"phr": "phrygian",
|
|
2801
|
+
"lydian": "lydian",
|
|
2802
|
+
"lyd": "lydian",
|
|
2803
|
+
"mixolydian": "mixolydian",
|
|
2804
|
+
"mix": "mixolydian",
|
|
2805
|
+
"aeolian": "aeolian",
|
|
2806
|
+
"aeo": "aeolian",
|
|
2807
|
+
"locrian": "locrian",
|
|
2808
|
+
"loc": "locrian"
|
|
2809
|
+
};
|
|
2810
|
+
var DYNAMICS_VALUES = /* @__PURE__ */ new Set([
|
|
2811
|
+
"pppppp",
|
|
2812
|
+
"ppppp",
|
|
2813
|
+
"pppp",
|
|
2814
|
+
"ppp",
|
|
2815
|
+
"pp",
|
|
2816
|
+
"p",
|
|
2817
|
+
"mp",
|
|
2818
|
+
"mf",
|
|
2819
|
+
"f",
|
|
2820
|
+
"ff",
|
|
2821
|
+
"fff",
|
|
2822
|
+
"ffff",
|
|
2823
|
+
"fffff",
|
|
2824
|
+
"ffffff",
|
|
2825
|
+
"sf",
|
|
2826
|
+
"sfz",
|
|
2827
|
+
"sffz",
|
|
2828
|
+
"sfp",
|
|
2829
|
+
"sfpp",
|
|
2830
|
+
"fp",
|
|
2831
|
+
"rf",
|
|
2832
|
+
"rfz",
|
|
2833
|
+
"fz",
|
|
2834
|
+
"pf"
|
|
2835
|
+
]);
|
|
2836
|
+
function parseHeader(lines) {
|
|
2837
|
+
const header = { voices: [], extraFields: [], directives: [] };
|
|
2838
|
+
let bodyStartIndex = 0;
|
|
2839
|
+
let foundKey = false;
|
|
2840
|
+
let postKHeaderDone = false;
|
|
2841
|
+
const headerFieldOrder = [];
|
|
2842
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2843
|
+
const line = lines[i].trim();
|
|
2844
|
+
if (line === "") continue;
|
|
2845
|
+
if (line.startsWith("%%")) {
|
|
2846
|
+
if (!foundKey) {
|
|
2847
|
+
header.directives.push(line);
|
|
2848
|
+
headerFieldOrder.push(line);
|
|
2849
|
+
} else if (!postKHeaderDone) {
|
|
2850
|
+
header.directives.push(line);
|
|
2851
|
+
headerFieldOrder.push(line);
|
|
2852
|
+
bodyStartIndex = i + 1;
|
|
2853
|
+
}
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
if (line.startsWith("%")) {
|
|
2857
|
+
if (!foundKey) {
|
|
2858
|
+
headerFieldOrder.push(lines[i]);
|
|
2859
|
+
} else if (!postKHeaderDone) {
|
|
2860
|
+
headerFieldOrder.push(lines[i]);
|
|
2861
|
+
bodyStartIndex = i + 1;
|
|
2862
|
+
}
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
const fieldMatch = line.match(/^([A-Za-z]):\s*(.*)/);
|
|
2866
|
+
const postKFields = /* @__PURE__ */ new Set(["I", "N"]);
|
|
2867
|
+
if (fieldMatch && (!foundKey || fieldMatch[1] === "V" || foundKey && postKFields.has(fieldMatch[1]))) {
|
|
2868
|
+
const [, field, value] = fieldMatch;
|
|
2869
|
+
if (foundKey && field !== "V") {
|
|
2870
|
+
if (!postKHeaderDone) {
|
|
2871
|
+
header.extraFields.push({ field, value: value.trim() });
|
|
2872
|
+
headerFieldOrder.push(line);
|
|
2873
|
+
bodyStartIndex = i + 1;
|
|
2874
|
+
}
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
switch (field) {
|
|
2878
|
+
case "X":
|
|
2879
|
+
header.referenceNumber = parseInt(value, 10);
|
|
2880
|
+
headerFieldOrder.push(line);
|
|
2881
|
+
break;
|
|
2882
|
+
case "T":
|
|
2883
|
+
header.title = value.trim();
|
|
2884
|
+
headerFieldOrder.push(line);
|
|
2885
|
+
break;
|
|
2886
|
+
case "C":
|
|
2887
|
+
header.composer = value.trim();
|
|
2888
|
+
headerFieldOrder.push(line);
|
|
2889
|
+
break;
|
|
2890
|
+
case "M":
|
|
2891
|
+
header.meter = value.trim();
|
|
2892
|
+
headerFieldOrder.push(line);
|
|
2893
|
+
break;
|
|
2894
|
+
case "L":
|
|
2895
|
+
header.unitNoteLength = value.trim();
|
|
2896
|
+
headerFieldOrder.push(line);
|
|
2897
|
+
break;
|
|
2898
|
+
case "Q":
|
|
2899
|
+
header.tempo = value.trim();
|
|
2900
|
+
headerFieldOrder.push(line);
|
|
2901
|
+
break;
|
|
2902
|
+
case "K":
|
|
2903
|
+
header.key = value.trim();
|
|
2904
|
+
foundKey = true;
|
|
2905
|
+
bodyStartIndex = i + 1;
|
|
2906
|
+
headerFieldOrder.push(line);
|
|
2907
|
+
break;
|
|
2908
|
+
case "V": {
|
|
2909
|
+
const voiceValue = value.trim();
|
|
2910
|
+
const voiceId = voiceValue.split(/\s+/)[0];
|
|
2911
|
+
const nameMatch = voiceValue.match(/name=["']?([^"'\s]+)["']?/i);
|
|
2912
|
+
const nmMatch = voiceValue.match(/nm=["']([^"']*)["']/i);
|
|
2913
|
+
const clefMatch = voiceValue.match(/clef=(\S+)/i);
|
|
2914
|
+
const displayName = nameMatch ? nameMatch[1] : nmMatch ? nmMatch[1] : voiceId;
|
|
2915
|
+
const hasParams = clefMatch || nameMatch || nmMatch || /\b(Program|merge|up|down|bass|treble|alto|tenor|soprano|octave|snm|stem)\b/i.test(voiceValue);
|
|
2916
|
+
const isBodyVoiceSwitch = foundKey && !hasParams;
|
|
2917
|
+
if (isBodyVoiceSwitch) {
|
|
2918
|
+
postKHeaderDone = true;
|
|
2919
|
+
}
|
|
2920
|
+
const existingVoice = header.voices.find((v) => v.id === voiceId);
|
|
2921
|
+
if (existingVoice) {
|
|
2922
|
+
if (nameMatch || nmMatch) existingVoice.name = displayName;
|
|
2923
|
+
if (clefMatch) existingVoice.clef = clefMatch[1];
|
|
2924
|
+
if (foundKey && !isBodyVoiceSwitch) existingVoice.fullLine = lines[i];
|
|
2925
|
+
} else {
|
|
2926
|
+
header.voices.push({
|
|
2927
|
+
id: voiceId,
|
|
2928
|
+
name: displayName,
|
|
2929
|
+
clef: clefMatch ? clefMatch[1] : void 0,
|
|
2930
|
+
fullLine: lines[i]
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
if (!foundKey) {
|
|
2934
|
+
headerFieldOrder.push(lines[i]);
|
|
2935
|
+
} else if (!isBodyVoiceSwitch && !postKHeaderDone) {
|
|
2936
|
+
headerFieldOrder.push(lines[i]);
|
|
2937
|
+
bodyStartIndex = i + 1;
|
|
2938
|
+
}
|
|
2939
|
+
break;
|
|
2940
|
+
}
|
|
2941
|
+
default:
|
|
2942
|
+
header.extraFields.push({ field, value: value.trim() });
|
|
2943
|
+
headerFieldOrder.push(line);
|
|
2944
|
+
break;
|
|
2945
|
+
}
|
|
2946
|
+
} else if (!foundKey) {
|
|
2947
|
+
continue;
|
|
2948
|
+
} else {
|
|
2949
|
+
postKHeaderDone = true;
|
|
2950
|
+
break;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
return { header, bodyStartIndex, headerFieldOrder };
|
|
2954
|
+
}
|
|
2955
|
+
function parseKeySignature2(keyStr) {
|
|
2956
|
+
if (!keyStr || keyStr.trim() === "" || keyStr.trim().toLowerCase() === "none") {
|
|
2957
|
+
return { fifths: 0, mode: "major" };
|
|
2958
|
+
}
|
|
2959
|
+
const trimmed = keyStr.trim();
|
|
2960
|
+
const keyMatch = trimmed.match(/^([A-Ga-g])(#|b)?/);
|
|
2961
|
+
if (!keyMatch) {
|
|
2962
|
+
return { fifths: 0, mode: "major" };
|
|
2963
|
+
}
|
|
2964
|
+
const keyNote = keyMatch[1].toUpperCase();
|
|
2965
|
+
const keyAccidental = keyMatch[2] || "";
|
|
2966
|
+
const keyName = keyNote + keyAccidental;
|
|
2967
|
+
const remainder = trimmed.slice(keyMatch[0].length).trim().toLowerCase();
|
|
2968
|
+
let mode = "";
|
|
2969
|
+
for (const m of Object.keys(MODE_OFFSET)) {
|
|
2970
|
+
if (m && remainder.startsWith(m)) {
|
|
2971
|
+
mode = m;
|
|
2972
|
+
break;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
const baseFifths = KEY_FIFTHS[keyName];
|
|
2976
|
+
if (baseFifths === void 0) {
|
|
2977
|
+
return { fifths: 0, mode: "major" };
|
|
2978
|
+
}
|
|
2979
|
+
const modeOffset = MODE_OFFSET[mode] ?? 0;
|
|
2980
|
+
const fifths = baseFifths + modeOffset;
|
|
2981
|
+
const modeValue = MODE_NAME[mode] ?? "major";
|
|
2982
|
+
return { fifths, mode: modeValue };
|
|
2983
|
+
}
|
|
2984
|
+
function parseTimeSignature2(meterStr) {
|
|
2985
|
+
if (!meterStr || meterStr.trim() === "") {
|
|
2986
|
+
return { beats: "4", beatType: 4 };
|
|
2987
|
+
}
|
|
2988
|
+
const trimmed = meterStr.trim();
|
|
2989
|
+
if (trimmed === "C" || trimmed.toLowerCase() === "common") {
|
|
2990
|
+
return { beats: "4", beatType: 4, symbol: "common" };
|
|
2991
|
+
}
|
|
2992
|
+
if (trimmed === "C|" || trimmed.toLowerCase() === "cut") {
|
|
2993
|
+
return { beats: "2", beatType: 2, symbol: "cut" };
|
|
2994
|
+
}
|
|
2995
|
+
const match = trimmed.match(/^(\d+)\/(\d+)$/);
|
|
2996
|
+
if (match) {
|
|
2997
|
+
return { beats: match[1], beatType: parseInt(match[2], 10) };
|
|
2998
|
+
}
|
|
2999
|
+
return { beats: "4", beatType: 4 };
|
|
3000
|
+
}
|
|
3001
|
+
function parseUnitNoteLength(lengthStr, meterStr) {
|
|
3002
|
+
if (lengthStr) {
|
|
3003
|
+
const match = lengthStr.trim().match(/^(\d+)\/(\d+)$/);
|
|
3004
|
+
if (match) {
|
|
3005
|
+
return { num: parseInt(match[1], 10), den: parseInt(match[2], 10) };
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
if (meterStr) {
|
|
3009
|
+
const mMatch = meterStr.trim().match(/^(\d+)\/(\d+)$/);
|
|
3010
|
+
if (mMatch) {
|
|
3011
|
+
const ratio = parseInt(mMatch[1], 10) / parseInt(mMatch[2], 10);
|
|
3012
|
+
return ratio >= 0.75 ? { num: 1, den: 8 } : { num: 1, den: 16 };
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
return { num: 1, den: 8 };
|
|
3016
|
+
}
|
|
3017
|
+
function lengthToDuration(num, den, unitNote) {
|
|
3018
|
+
const fractionOfWhole = num * unitNote.num / (den * unitNote.den);
|
|
3019
|
+
return Math.round(fractionOfWhole * 4 * DIVISIONS);
|
|
3020
|
+
}
|
|
3021
|
+
function durationToNoteType(duration) {
|
|
3022
|
+
const quarterNotes = duration / DIVISIONS;
|
|
3023
|
+
for (const [qnStr, type] of Object.entries(NOTE_TYPE_MAP)) {
|
|
3024
|
+
const qn = parseFloat(qnStr);
|
|
3025
|
+
if (Math.abs(quarterNotes - qn) < 1e-3) {
|
|
3026
|
+
return { noteType: type, dots: 0 };
|
|
3027
|
+
}
|
|
3028
|
+
if (Math.abs(quarterNotes - qn * 1.5) < 1e-3) {
|
|
3029
|
+
return { noteType: type, dots: 1 };
|
|
3030
|
+
}
|
|
3031
|
+
if (Math.abs(quarterNotes - qn * 1.75) < 1e-3) {
|
|
3032
|
+
return { noteType: type, dots: 2 };
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
let bestType = "quarter";
|
|
3036
|
+
let bestDiff = Infinity;
|
|
3037
|
+
for (const [qnStr, type] of Object.entries(NOTE_TYPE_MAP)) {
|
|
3038
|
+
const diff = Math.abs(quarterNotes - parseFloat(qnStr));
|
|
3039
|
+
if (diff < bestDiff) {
|
|
3040
|
+
bestDiff = diff;
|
|
3041
|
+
bestType = type;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
return { noteType: bestType, dots: 0 };
|
|
3045
|
+
}
|
|
3046
|
+
function tokenizeBody(bodyLines) {
|
|
3047
|
+
const voiceTokens = /* @__PURE__ */ new Map();
|
|
3048
|
+
let currentVoice = "1";
|
|
3049
|
+
voiceTokens.set(currentVoice, []);
|
|
3050
|
+
let isContinuation = false;
|
|
3051
|
+
const inlineVoiceMarkers = /* @__PURE__ */ new Map();
|
|
3052
|
+
const voiceDeclarationLines = [];
|
|
3053
|
+
const bodyComments = [];
|
|
3054
|
+
const bodyDirectives = [];
|
|
3055
|
+
const wFields = [];
|
|
3056
|
+
const voiceInterleavePattern = [];
|
|
3057
|
+
let currentGroup = [];
|
|
3058
|
+
let lastVoiceInGroup = null;
|
|
3059
|
+
const groupBarCounts = [];
|
|
3060
|
+
let currentGroupBarCounts = [];
|
|
3061
|
+
let currentVoiceBarCount = 0;
|
|
3062
|
+
const voiceBarCounts = /* @__PURE__ */ new Map();
|
|
3063
|
+
const voiceCommentsMap = /* @__PURE__ */ new Map();
|
|
3064
|
+
const preVoiceComments = [];
|
|
3065
|
+
let pendingComments = [];
|
|
3066
|
+
function flushPendingToVoice(voiceId) {
|
|
3067
|
+
if (pendingComments.length === 0) return;
|
|
3068
|
+
if (!voiceCommentsMap.has(voiceId)) {
|
|
3069
|
+
voiceCommentsMap.set(voiceId, []);
|
|
3070
|
+
}
|
|
3071
|
+
const barCount = voiceBarCounts.get(voiceId) || 0;
|
|
3072
|
+
for (const comment of pendingComments) {
|
|
3073
|
+
voiceCommentsMap.get(voiceId).push({ barIndex: barCount, comment });
|
|
3074
|
+
}
|
|
3075
|
+
pendingComments = [];
|
|
3076
|
+
}
|
|
3077
|
+
for (const rawLine of bodyLines) {
|
|
3078
|
+
const trimmedEnd = rawLine.trimEnd();
|
|
3079
|
+
const hasLineContinuation = trimmedEnd.endsWith("\\");
|
|
3080
|
+
const lineContent = hasLineContinuation ? trimmedEnd.slice(0, -1) : rawLine;
|
|
3081
|
+
const line = lineContent.trim();
|
|
3082
|
+
const wFieldMatch = line.match(/^W:(.*)/);
|
|
3083
|
+
if (wFieldMatch) {
|
|
3084
|
+
wFields.push(rawLine);
|
|
3085
|
+
isContinuation = false;
|
|
3086
|
+
continue;
|
|
3087
|
+
}
|
|
3088
|
+
if (line === "") {
|
|
3089
|
+
isContinuation = false;
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
3092
|
+
const isRealDirective = line.startsWith("%%") && /^%%[A-Za-z]/.test(line);
|
|
3093
|
+
const isComment = line.startsWith("%") && !isRealDirective;
|
|
3094
|
+
if (isComment) {
|
|
3095
|
+
pendingComments.push(rawLine);
|
|
3096
|
+
isContinuation = false;
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
if (isRealDirective) {
|
|
3100
|
+
bodyDirectives.push(rawLine);
|
|
3101
|
+
isContinuation = false;
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
const voiceMatch = line.match(/^V:\s*(\S+)/);
|
|
3105
|
+
if (voiceMatch) {
|
|
3106
|
+
const newVoice = voiceMatch[1];
|
|
3107
|
+
if (currentGroup.includes(newVoice)) {
|
|
3108
|
+
if (lastVoiceInGroup !== null) {
|
|
3109
|
+
currentGroupBarCounts.push(currentVoiceBarCount);
|
|
3110
|
+
currentVoiceBarCount = 0;
|
|
3111
|
+
}
|
|
3112
|
+
groupBarCounts.push(currentGroupBarCounts);
|
|
3113
|
+
currentGroupBarCounts = [];
|
|
3114
|
+
voiceInterleavePattern.push(currentGroup);
|
|
3115
|
+
currentGroup = [];
|
|
3116
|
+
lastVoiceInGroup = null;
|
|
3117
|
+
for (const c of pendingComments) {
|
|
3118
|
+
bodyComments.push(c);
|
|
3119
|
+
}
|
|
3120
|
+
pendingComments = [];
|
|
3121
|
+
}
|
|
3122
|
+
preVoiceComments.push([...pendingComments]);
|
|
3123
|
+
pendingComments = [];
|
|
3124
|
+
currentVoice = newVoice;
|
|
3125
|
+
if (!voiceTokens.has(currentVoice)) {
|
|
3126
|
+
voiceTokens.set(currentVoice, []);
|
|
3127
|
+
}
|
|
3128
|
+
voiceDeclarationLines.push(rawLine);
|
|
3129
|
+
if (lastVoiceInGroup !== currentVoice) {
|
|
3130
|
+
if (lastVoiceInGroup !== null) {
|
|
3131
|
+
currentGroupBarCounts.push(currentVoiceBarCount);
|
|
3132
|
+
currentVoiceBarCount = 0;
|
|
3133
|
+
}
|
|
3134
|
+
currentGroup.push(currentVoice);
|
|
3135
|
+
lastVoiceInGroup = currentVoice;
|
|
3136
|
+
}
|
|
3137
|
+
isContinuation = false;
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
const lyricsMatch = line.match(/^w:\s*(.*)/);
|
|
3141
|
+
if (lyricsMatch) {
|
|
3142
|
+
const syllables = parseLyricLine(lyricsMatch[1]);
|
|
3143
|
+
voiceTokens.get(currentVoice).push({
|
|
3144
|
+
type: "lyrics",
|
|
3145
|
+
value: lyricsMatch[1],
|
|
3146
|
+
syllables
|
|
3147
|
+
});
|
|
3148
|
+
isContinuation = false;
|
|
3149
|
+
continue;
|
|
3150
|
+
}
|
|
3151
|
+
const bodyKeyMatch = line.match(/^K:\s*(.*)/);
|
|
3152
|
+
if (bodyKeyMatch) {
|
|
3153
|
+
const currentTokens2 = voiceTokens.get(currentVoice);
|
|
3154
|
+
if (currentTokens2.length > 0) {
|
|
3155
|
+
const lastToken = currentTokens2[currentTokens2.length - 1];
|
|
3156
|
+
if (lastToken.type !== "line_break") {
|
|
3157
|
+
currentTokens2.push({ type: "line_break", value: "\n" });
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
currentTokens2.push({ type: "inline_field", value: `K:${bodyKeyMatch[1]}` });
|
|
3161
|
+
currentTokens2.push({ type: "line_break", value: "\n" });
|
|
3162
|
+
isContinuation = false;
|
|
3163
|
+
continue;
|
|
3164
|
+
}
|
|
3165
|
+
if (/^[A-Za-z]:\s*/.test(line) && !/^\[/.test(line)) {
|
|
3166
|
+
isContinuation = false;
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
flushPendingToVoice(currentVoice);
|
|
3170
|
+
if (!currentGroup.includes(currentVoice)) {
|
|
3171
|
+
if (lastVoiceInGroup !== null) {
|
|
3172
|
+
currentGroupBarCounts.push(currentVoiceBarCount);
|
|
3173
|
+
currentVoiceBarCount = 0;
|
|
3174
|
+
}
|
|
3175
|
+
currentGroup.push(currentVoice);
|
|
3176
|
+
lastVoiceInGroup = currentVoice;
|
|
3177
|
+
}
|
|
3178
|
+
const tokens = tokenizeMusicLine(lineContent);
|
|
3179
|
+
const currentTokens = voiceTokens.get(currentVoice);
|
|
3180
|
+
if (currentTokens.length > 0 && !isContinuation) {
|
|
3181
|
+
const lastToken = currentTokens[currentTokens.length - 1];
|
|
3182
|
+
if (lastToken.type !== "line_break") {
|
|
3183
|
+
currentTokens.push({ type: "line_break", value: "\n" });
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
for (const token of tokens) {
|
|
3187
|
+
if (token.type === "inline_field") {
|
|
3188
|
+
const fieldMatch = token.value.match(/^V:\s*(.+)/);
|
|
3189
|
+
if (fieldMatch) {
|
|
3190
|
+
const voiceId = fieldMatch[1].trim().split(/\s+/)[0];
|
|
3191
|
+
currentVoice = voiceId;
|
|
3192
|
+
if (!voiceTokens.has(currentVoice)) {
|
|
3193
|
+
voiceTokens.set(currentVoice, []);
|
|
3194
|
+
}
|
|
3195
|
+
if (!inlineVoiceMarkers.has(voiceId)) {
|
|
3196
|
+
inlineVoiceMarkers.set(voiceId, `[${token.value}]`);
|
|
3197
|
+
}
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
voiceTokens.get(currentVoice).push(token);
|
|
3202
|
+
if (token.type === "bar") {
|
|
3203
|
+
currentVoiceBarCount++;
|
|
3204
|
+
voiceBarCounts.set(currentVoice, (voiceBarCounts.get(currentVoice) || 0) + 1);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
if (hasLineContinuation) {
|
|
3208
|
+
voiceTokens.get(currentVoice).push({ type: "line_break", value: "\\\n" });
|
|
3209
|
+
isContinuation = true;
|
|
3210
|
+
} else {
|
|
3211
|
+
isContinuation = false;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
const result = [];
|
|
3215
|
+
const voiceIds = [];
|
|
3216
|
+
for (const [voiceId, tokens] of voiceTokens) {
|
|
3217
|
+
if (tokens.length > 0) {
|
|
3218
|
+
result.push(tokens);
|
|
3219
|
+
voiceIds.push(voiceId);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
const trailingComments = [...pendingComments];
|
|
3223
|
+
pendingComments = [];
|
|
3224
|
+
if (currentGroup.length > 0) {
|
|
3225
|
+
currentGroupBarCounts.push(currentVoiceBarCount);
|
|
3226
|
+
groupBarCounts.push(currentGroupBarCounts);
|
|
3227
|
+
voiceInterleavePattern.push(currentGroup);
|
|
3228
|
+
}
|
|
3229
|
+
const voiceComments = {};
|
|
3230
|
+
for (const [voiceId, comments] of voiceCommentsMap) {
|
|
3231
|
+
voiceComments[voiceId] = comments;
|
|
3232
|
+
}
|
|
3233
|
+
return { tokens: result.length > 0 ? result : [[]], voiceIds, inlineVoiceMarkers, voiceDeclarationLines, bodyComments, bodyDirectives, wFields, voiceInterleavePattern, groupBarCounts, voiceComments, preVoiceComments, trailingComments };
|
|
3234
|
+
}
|
|
3235
|
+
function parseLyricLine(text) {
|
|
3236
|
+
const parts = [];
|
|
3237
|
+
const tokens = text.split(/\s+/);
|
|
3238
|
+
for (const token of tokens) {
|
|
3239
|
+
if (token === "") continue;
|
|
3240
|
+
const syllables = token.split("-");
|
|
3241
|
+
for (let i = 0; i < syllables.length; i++) {
|
|
3242
|
+
if (syllables[i] === "" && i > 0) continue;
|
|
3243
|
+
parts.push(syllables[i] + (i < syllables.length - 1 ? "-" : ""));
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
return parts;
|
|
3247
|
+
}
|
|
3248
|
+
function tokenizeMusicLine(line) {
|
|
3249
|
+
const tokens = [];
|
|
3250
|
+
let i = 0;
|
|
3251
|
+
while (i < line.length) {
|
|
3252
|
+
const ch = line[i];
|
|
3253
|
+
if (ch === " " || ch === " ") {
|
|
3254
|
+
tokens.push({ type: "space", value: " " });
|
|
3255
|
+
while (i < line.length && (line[i] === " " || line[i] === " ")) i++;
|
|
3256
|
+
continue;
|
|
3257
|
+
}
|
|
3258
|
+
if (ch === "%") break;
|
|
3259
|
+
if (ch === '"') {
|
|
3260
|
+
const end = line.indexOf('"', i + 1);
|
|
3261
|
+
if (end !== -1) {
|
|
3262
|
+
tokens.push({ type: "chord_symbol", value: line.slice(i + 1, end) });
|
|
3263
|
+
i = end + 1;
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
if (ch === "!") {
|
|
3268
|
+
const end = line.indexOf("!", i + 1);
|
|
3269
|
+
if (end !== -1) {
|
|
3270
|
+
tokens.push({ type: "decoration", value: line.slice(i + 1, end) });
|
|
3271
|
+
i = end + 1;
|
|
3272
|
+
continue;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
if (ch === "{") {
|
|
3276
|
+
tokens.push({ type: "grace_start", value: "{" });
|
|
3277
|
+
i++;
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
if (ch === "}") {
|
|
3281
|
+
tokens.push({ type: "grace_end", value: "}" });
|
|
3282
|
+
i++;
|
|
3283
|
+
continue;
|
|
3284
|
+
}
|
|
3285
|
+
if (ch === "(" && i + 1 < line.length && /\d/.test(line[i + 1])) {
|
|
3286
|
+
const tupletResult = parseTuplet(line, i);
|
|
3287
|
+
if (tupletResult) {
|
|
3288
|
+
tokens.push(tupletResult.token);
|
|
3289
|
+
i = tupletResult.nextIndex;
|
|
3290
|
+
continue;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
if (ch === "(") {
|
|
3294
|
+
tokens.push({ type: "slur_start", value: "(" });
|
|
3295
|
+
i++;
|
|
3296
|
+
continue;
|
|
3297
|
+
}
|
|
3298
|
+
if (ch === ")") {
|
|
3299
|
+
tokens.push({ type: "slur_end", value: ")" });
|
|
3300
|
+
i++;
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
if (ch === "-") {
|
|
3304
|
+
tokens.push({ type: "tie", value: "-" });
|
|
3305
|
+
i++;
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
if (ch === "|" || ch === ":" || ch === "[" && (i + 1 < line.length && line[i + 1] === "|")) {
|
|
3309
|
+
const barResult = parseBarLine(line, i);
|
|
3310
|
+
if (barResult) {
|
|
3311
|
+
tokens.push(barResult.token);
|
|
3312
|
+
i = barResult.nextIndex;
|
|
3313
|
+
if (i < line.length && /\d/.test(line[i])) {
|
|
3314
|
+
const numMatch = line.slice(i).match(/^(\d+)/);
|
|
3315
|
+
if (numMatch) {
|
|
3316
|
+
tokens.push({ type: "ending", value: numMatch[1] });
|
|
3317
|
+
i += numMatch[1].length;
|
|
3318
|
+
if (i < line.length && line[i] === " ") i++;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
continue;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
if (ch === "[" && i + 1 < line.length && /\d/.test(line[i + 1])) {
|
|
3325
|
+
const numMatch = line.slice(i + 1).match(/^(\d+)/);
|
|
3326
|
+
if (numMatch) {
|
|
3327
|
+
tokens.push({ type: "ending", value: numMatch[1], bracket: true });
|
|
3328
|
+
i += 1 + numMatch[1].length;
|
|
3329
|
+
if (i < line.length && line[i] === " ") i++;
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
if (ch === "[" && i + 1 < line.length && /[A-Za-z]/.test(line[i + 1])) {
|
|
3334
|
+
const colonIdx = line.indexOf(":", i + 2);
|
|
3335
|
+
if (colonIdx !== -1 && colonIdx <= i + 3) {
|
|
3336
|
+
const end = line.indexOf("]", colonIdx);
|
|
3337
|
+
if (end !== -1) {
|
|
3338
|
+
tokens.push({ type: "inline_field", value: line.slice(i + 1, end) });
|
|
3339
|
+
i = end + 1;
|
|
3340
|
+
continue;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
if (ch === ">" || ch === "<") {
|
|
3345
|
+
let val = ch;
|
|
3346
|
+
let j = i + 1;
|
|
3347
|
+
while (j < line.length && line[j] === ch) {
|
|
3348
|
+
val += line[j];
|
|
3349
|
+
j++;
|
|
3350
|
+
}
|
|
3351
|
+
tokens.push({ type: "broken_rhythm", value: val });
|
|
3352
|
+
i = j;
|
|
3353
|
+
continue;
|
|
3354
|
+
}
|
|
3355
|
+
if (ch === "&") {
|
|
3356
|
+
tokens.push({ type: "overlay", value: "&" });
|
|
3357
|
+
i++;
|
|
3358
|
+
continue;
|
|
3359
|
+
}
|
|
3360
|
+
if (ch === "[") {
|
|
3361
|
+
tokens.push({ type: "chord_start", value: "[" });
|
|
3362
|
+
i++;
|
|
3363
|
+
continue;
|
|
3364
|
+
}
|
|
3365
|
+
if (ch === "]") {
|
|
3366
|
+
i++;
|
|
3367
|
+
const dur = parseDuration(line, i);
|
|
3368
|
+
i = dur.nextIndex;
|
|
3369
|
+
tokens.push({ type: "chord_end", value: "]", durationNum: dur.num, durationDen: dur.den });
|
|
3370
|
+
continue;
|
|
3371
|
+
}
|
|
3372
|
+
if ((ch === "v" || ch === "u" || ch === "T" || ch === "M") && i + 1 < line.length) {
|
|
3373
|
+
const nextCh = line[i + 1];
|
|
3374
|
+
if (isNoteStart(nextCh) || nextCh === "[" || nextCh === "z" || nextCh === "x" || nextCh === "v" || nextCh === "u" || nextCh === "T" || nextCh === "M" || nextCh === "(" || nextCh === "!" || nextCh === '"') {
|
|
3375
|
+
tokens.push({ type: "decoration", value: ch });
|
|
3376
|
+
i++;
|
|
3377
|
+
continue;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
if (isNoteStart(ch) || ch === "z" || ch === "Z" || ch === "x" || ch === "X") {
|
|
3381
|
+
const noteResult = parseNoteToken(line, i);
|
|
3382
|
+
if (noteResult) {
|
|
3383
|
+
tokens.push(noteResult.token);
|
|
3384
|
+
i = noteResult.nextIndex;
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
i++;
|
|
3389
|
+
}
|
|
3390
|
+
return tokens;
|
|
3391
|
+
}
|
|
3392
|
+
function isNoteStart(ch) {
|
|
3393
|
+
return /[A-Ga-g^_=]/.test(ch);
|
|
3394
|
+
}
|
|
3395
|
+
function parseTuplet(line, i) {
|
|
3396
|
+
const match = line.slice(i).match(/^\((\d+)(?::(\d*)(?::(\d*))?)?/);
|
|
3397
|
+
if (!match) return null;
|
|
3398
|
+
const p = parseInt(match[1], 10);
|
|
3399
|
+
let q;
|
|
3400
|
+
let r;
|
|
3401
|
+
if (match[2] !== void 0 && match[2] !== "") {
|
|
3402
|
+
q = parseInt(match[2], 10);
|
|
3403
|
+
}
|
|
3404
|
+
if (match[3] !== void 0 && match[3] !== "") {
|
|
3405
|
+
r = parseInt(match[3], 10);
|
|
3406
|
+
}
|
|
3407
|
+
if (q === void 0) {
|
|
3408
|
+
if (p === 2) q = 3;
|
|
3409
|
+
else if (p === 3) q = 2;
|
|
3410
|
+
else if (p === 4) q = 3;
|
|
3411
|
+
else if (p === 5 || p === 6) q = 2;
|
|
3412
|
+
else if (p === 7 || p === 8 || p === 9) q = 2;
|
|
3413
|
+
else q = 2;
|
|
3414
|
+
}
|
|
3415
|
+
if (r === void 0) {
|
|
3416
|
+
r = p;
|
|
3417
|
+
}
|
|
3418
|
+
return {
|
|
3419
|
+
token: { type: "tuplet", value: match[0], tupletP: p, tupletQ: q, tupletR: r },
|
|
3420
|
+
nextIndex: i + match[0].length
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
function parseBarLine(line, i) {
|
|
3424
|
+
const patterns = [
|
|
3425
|
+
[":|]", "end-repeat-final"],
|
|
3426
|
+
[":||:", "double-repeat"],
|
|
3427
|
+
["::", "double-repeat"],
|
|
3428
|
+
[":|:", "double-repeat"],
|
|
3429
|
+
["|>|", "thick-thin"],
|
|
3430
|
+
["|:", "start-repeat"],
|
|
3431
|
+
[":|", "end-repeat"],
|
|
3432
|
+
["||", "double"],
|
|
3433
|
+
["|]", "final"],
|
|
3434
|
+
["[|", "heavy-light"],
|
|
3435
|
+
["|", "regular"]
|
|
3436
|
+
];
|
|
3437
|
+
for (const [pat, type] of patterns) {
|
|
3438
|
+
if (line.slice(i).startsWith(pat)) {
|
|
3439
|
+
return {
|
|
3440
|
+
token: { type: "bar", value: pat, barType: type },
|
|
3441
|
+
nextIndex: i + pat.length
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
if (line[i] === ":" && i + 1 < line.length && line[i + 1] === "|") {
|
|
3446
|
+
return {
|
|
3447
|
+
token: { type: "bar", value: ":|", barType: "end-repeat" },
|
|
3448
|
+
nextIndex: i + 2
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
return null;
|
|
3452
|
+
}
|
|
3453
|
+
function parseNoteToken(line, i) {
|
|
3454
|
+
const start = i;
|
|
3455
|
+
if (line[i] === "z" || line[i] === "Z" || line[i] === "x" || line[i] === "X") {
|
|
3456
|
+
i++;
|
|
3457
|
+
const dur2 = parseDuration(line, i);
|
|
3458
|
+
i = dur2.nextIndex;
|
|
3459
|
+
return {
|
|
3460
|
+
token: {
|
|
3461
|
+
type: "rest",
|
|
3462
|
+
value: line.slice(start, i),
|
|
3463
|
+
durationNum: dur2.num,
|
|
3464
|
+
durationDen: dur2.den
|
|
3465
|
+
},
|
|
3466
|
+
nextIndex: i
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
let accidental = 0;
|
|
3470
|
+
let explicitNatural = false;
|
|
3471
|
+
if (line[i] === "^") {
|
|
3472
|
+
accidental = 1;
|
|
3473
|
+
i++;
|
|
3474
|
+
if (i < line.length && line[i] === "^") {
|
|
3475
|
+
accidental = 2;
|
|
3476
|
+
i++;
|
|
3477
|
+
}
|
|
3478
|
+
} else if (line[i] === "_") {
|
|
3479
|
+
accidental = -1;
|
|
3480
|
+
i++;
|
|
3481
|
+
if (i < line.length && line[i] === "_") {
|
|
3482
|
+
accidental = -2;
|
|
3483
|
+
i++;
|
|
3484
|
+
}
|
|
3485
|
+
} else if (line[i] === "=") {
|
|
3486
|
+
accidental = 0;
|
|
3487
|
+
explicitNatural = true;
|
|
3488
|
+
i++;
|
|
3489
|
+
}
|
|
3490
|
+
if (i >= line.length || !/[A-Ga-g]/.test(line[i])) {
|
|
3491
|
+
return null;
|
|
3492
|
+
}
|
|
3493
|
+
const noteLetter = line[i];
|
|
3494
|
+
i++;
|
|
3495
|
+
const pitch = abcNoteToPitch(noteLetter, accidental);
|
|
3496
|
+
while (i < line.length && (line[i] === "'" || line[i] === ",")) {
|
|
3497
|
+
if (line[i] === "'") {
|
|
3498
|
+
pitch.octave++;
|
|
3499
|
+
} else {
|
|
3500
|
+
pitch.octave--;
|
|
3501
|
+
}
|
|
3502
|
+
i++;
|
|
3503
|
+
}
|
|
3504
|
+
const dur = parseDuration(line, i);
|
|
3505
|
+
i = dur.nextIndex;
|
|
3506
|
+
const token = {
|
|
3507
|
+
type: "note",
|
|
3508
|
+
value: line.slice(start, i),
|
|
3509
|
+
pitch,
|
|
3510
|
+
durationNum: dur.num,
|
|
3511
|
+
durationDen: dur.den,
|
|
3512
|
+
accidental: accidental !== 0 || explicitNatural ? accidental : void 0
|
|
3513
|
+
};
|
|
3514
|
+
if (explicitNatural) {
|
|
3515
|
+
token.explicitNatural = true;
|
|
3516
|
+
}
|
|
3517
|
+
return {
|
|
3518
|
+
token,
|
|
3519
|
+
nextIndex: i
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
function abcNoteToPitch(letter, accidental) {
|
|
3523
|
+
const isLower = letter === letter.toLowerCase();
|
|
3524
|
+
const step = letter.toUpperCase();
|
|
3525
|
+
const octave = isLower ? 5 : 4;
|
|
3526
|
+
const pitch = { step, octave };
|
|
3527
|
+
if (accidental !== 0) {
|
|
3528
|
+
pitch.alter = accidental;
|
|
3529
|
+
}
|
|
3530
|
+
return pitch;
|
|
3531
|
+
}
|
|
3532
|
+
function parseDuration(line, i) {
|
|
3533
|
+
let num = 1;
|
|
3534
|
+
let den = 1;
|
|
3535
|
+
const numMatch = line.slice(i).match(/^(\d+)/);
|
|
3536
|
+
if (numMatch) {
|
|
3537
|
+
num = parseInt(numMatch[1], 10);
|
|
3538
|
+
i += numMatch[1].length;
|
|
3539
|
+
}
|
|
3540
|
+
if (i < line.length && line[i] === "/") {
|
|
3541
|
+
i++;
|
|
3542
|
+
const denMatch = line.slice(i).match(/^(\d+)/);
|
|
3543
|
+
if (denMatch) {
|
|
3544
|
+
den = parseInt(denMatch[1], 10);
|
|
3545
|
+
i += denMatch[1].length;
|
|
3546
|
+
} else {
|
|
3547
|
+
den = 2;
|
|
3548
|
+
while (i < line.length && line[i] === "/") {
|
|
3549
|
+
den *= 2;
|
|
3550
|
+
i++;
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
return { num, den, nextIndex: i };
|
|
3555
|
+
}
|
|
3556
|
+
function buildScore(header, voiceTokensList, voiceIds, headerFieldOrder, inlineVoiceMarkers = /* @__PURE__ */ new Map(), voiceDeclarationLines = [], bodyComments = [], bodyDirectives = [], wFields = [], voiceInterleavePattern = [], groupBarCounts = [], voiceComments = {}, preVoiceComments = [], trailingComments = []) {
|
|
3557
|
+
const unitNote = parseUnitNoteLength(header.unitNoteLength, header.meter);
|
|
3558
|
+
const timeSignature = parseTimeSignature2(header.meter || "4/4");
|
|
3559
|
+
const keySignature = parseKeySignature2(header.key || "C");
|
|
3560
|
+
const beatsNum = parseInt(timeSignature.beats, 10);
|
|
3561
|
+
const beatType = timeSignature.beatType;
|
|
3562
|
+
const measureDuration = Math.round(beatsNum / beatType * 4 * DIVISIONS);
|
|
3563
|
+
const parts = [];
|
|
3564
|
+
const partListEntries = [];
|
|
3565
|
+
const miscellaneous = [];
|
|
3566
|
+
if (header.referenceNumber !== void 0) {
|
|
3567
|
+
miscellaneous.push({ name: "abc-reference-number", value: String(header.referenceNumber) });
|
|
3568
|
+
}
|
|
3569
|
+
if (header.unitNoteLength) {
|
|
3570
|
+
miscellaneous.push({ name: "abc-unit-note-length", value: header.unitNoteLength });
|
|
3571
|
+
}
|
|
3572
|
+
if (header.tempo) {
|
|
3573
|
+
miscellaneous.push({ name: "abc-tempo", value: header.tempo });
|
|
3574
|
+
}
|
|
3575
|
+
const extraCreators = [];
|
|
3576
|
+
let sourceValue;
|
|
3577
|
+
const encoderValues = [];
|
|
3578
|
+
if (header.extraFields && header.extraFields.length > 0) {
|
|
3579
|
+
for (const ef of header.extraFields) {
|
|
3580
|
+
switch (ef.field) {
|
|
3581
|
+
case "S":
|
|
3582
|
+
sourceValue = ef.value;
|
|
3583
|
+
break;
|
|
3584
|
+
case "Z":
|
|
3585
|
+
encoderValues.push(ef.value);
|
|
3586
|
+
break;
|
|
3587
|
+
case "O":
|
|
3588
|
+
extraCreators.push({ type: "origin", value: ef.value });
|
|
3589
|
+
break;
|
|
3590
|
+
default:
|
|
3591
|
+
miscellaneous.push({ name: `abc-${ef.field}`, value: ef.value });
|
|
3592
|
+
break;
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
if (header.directives && header.directives.length > 0) {
|
|
3597
|
+
miscellaneous.push({ name: "abc-directives", value: JSON.stringify(header.directives) });
|
|
3598
|
+
}
|
|
3599
|
+
if (headerFieldOrder.length > 0) {
|
|
3600
|
+
miscellaneous.push({ name: "abc-header-order", value: JSON.stringify(headerFieldOrder) });
|
|
3601
|
+
}
|
|
3602
|
+
if (voiceIds.length > 0) {
|
|
3603
|
+
miscellaneous.push({ name: "abc-voice-ids", value: JSON.stringify(voiceIds) });
|
|
3604
|
+
}
|
|
3605
|
+
if (inlineVoiceMarkers.size > 0) {
|
|
3606
|
+
const markersObj = {};
|
|
3607
|
+
for (const [id, marker] of inlineVoiceMarkers) {
|
|
3608
|
+
markersObj[id] = marker;
|
|
3609
|
+
}
|
|
3610
|
+
miscellaneous.push({ name: "abc-inline-voice-markers", value: JSON.stringify(markersObj) });
|
|
3611
|
+
}
|
|
3612
|
+
if (voiceDeclarationLines.length > 0 && inlineVoiceMarkers.size > 0) {
|
|
3613
|
+
miscellaneous.push({ name: "abc-voice-declaration-lines", value: JSON.stringify(voiceDeclarationLines) });
|
|
3614
|
+
}
|
|
3615
|
+
if (voiceDeclarationLines.length > 0) {
|
|
3616
|
+
miscellaneous.push({ name: "abc-body-voice-lines", value: JSON.stringify(voiceDeclarationLines) });
|
|
3617
|
+
}
|
|
3618
|
+
if (bodyComments.length > 0) {
|
|
3619
|
+
miscellaneous.push({ name: "abc-body-comments", value: JSON.stringify(bodyComments) });
|
|
3620
|
+
}
|
|
3621
|
+
if (bodyDirectives.length > 0) {
|
|
3622
|
+
miscellaneous.push({ name: "abc-body-directives", value: JSON.stringify(bodyDirectives) });
|
|
3623
|
+
}
|
|
3624
|
+
if (wFields.length > 0) {
|
|
3625
|
+
miscellaneous.push({ name: "abc-w-fields", value: JSON.stringify(wFields) });
|
|
3626
|
+
}
|
|
3627
|
+
if (voiceInterleavePattern.length > 0) {
|
|
3628
|
+
miscellaneous.push({ name: "abc-voice-interleave", value: JSON.stringify(voiceInterleavePattern) });
|
|
3629
|
+
}
|
|
3630
|
+
if (groupBarCounts.length > 0) {
|
|
3631
|
+
miscellaneous.push({ name: "abc-group-bar-counts", value: JSON.stringify(groupBarCounts) });
|
|
3632
|
+
}
|
|
3633
|
+
if (Object.keys(voiceComments).length > 0) {
|
|
3634
|
+
miscellaneous.push({ name: "abc-voice-comments", value: JSON.stringify(voiceComments) });
|
|
3635
|
+
}
|
|
3636
|
+
if (preVoiceComments.length > 0) {
|
|
3637
|
+
miscellaneous.push({ name: "abc-pre-voice-comments", value: JSON.stringify(preVoiceComments) });
|
|
3638
|
+
}
|
|
3639
|
+
if (trailingComments.length > 0) {
|
|
3640
|
+
miscellaneous.push({ name: "abc-trailing-comments", value: JSON.stringify(trailingComments) });
|
|
3641
|
+
}
|
|
3642
|
+
const voiceFullLines = {};
|
|
3643
|
+
for (const voice of header.voices || []) {
|
|
3644
|
+
if (voice.fullLine) {
|
|
3645
|
+
voiceFullLines[voice.id] = voice.fullLine;
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
if (Object.keys(voiceFullLines).length > 0) {
|
|
3649
|
+
miscellaneous.push({ name: "abc-voice-full-lines", value: JSON.stringify(voiceFullLines) });
|
|
3650
|
+
}
|
|
3651
|
+
for (let voiceIndex = 0; voiceIndex < voiceTokensList.length; voiceIndex++) {
|
|
3652
|
+
const tokens = voiceTokensList[voiceIndex];
|
|
3653
|
+
const partId = `P${voiceIndex + 1}`;
|
|
3654
|
+
const voiceId = voiceIds[voiceIndex];
|
|
3655
|
+
const headerVoice = header.voices?.find((v) => v.id === voiceId) || header.voices && header.voices[voiceIndex];
|
|
3656
|
+
const voiceName = headerVoice ? headerVoice.name || `Voice ${voiceIndex + 1}` : voiceTokensList.length > 1 ? `Voice ${voiceIndex + 1}` : "Music";
|
|
3657
|
+
partListEntries.push({
|
|
3658
|
+
_id: generateId(),
|
|
3659
|
+
type: "score-part",
|
|
3660
|
+
id: partId,
|
|
3661
|
+
name: voiceName
|
|
3662
|
+
});
|
|
3663
|
+
const voiceClef = headerVoice ? abcClefToMusicXml(headerVoice.clef) : void 0;
|
|
3664
|
+
const buildResult = buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDuration, voiceClef);
|
|
3665
|
+
parts.push({
|
|
3666
|
+
_id: generateId(),
|
|
3667
|
+
id: partId,
|
|
3668
|
+
measures: buildResult.measures
|
|
3669
|
+
});
|
|
3670
|
+
if (buildResult.lineBreaks.length > 0) {
|
|
3671
|
+
if (voiceIndex === 0) {
|
|
3672
|
+
miscellaneous.push({ name: "abc-line-breaks", value: JSON.stringify(buildResult.lineBreaks) });
|
|
3673
|
+
}
|
|
3674
|
+
miscellaneous.push({ name: `abc-line-breaks-${voiceIndex}`, value: JSON.stringify(buildResult.lineBreaks) });
|
|
3675
|
+
}
|
|
3676
|
+
if (buildResult.hasIndividualChordDurations) {
|
|
3677
|
+
miscellaneous.push({ name: "abc-chord-individual-durations", value: "true" });
|
|
3678
|
+
}
|
|
3679
|
+
if (voiceIndex === 0) {
|
|
3680
|
+
let hasLyrics2 = false;
|
|
3681
|
+
let lyricsAfterAll = true;
|
|
3682
|
+
let seenLyrics = false;
|
|
3683
|
+
const lyricLineCounts = [];
|
|
3684
|
+
for (const token of tokens) {
|
|
3685
|
+
if (token.type === "lyrics") {
|
|
3686
|
+
hasLyrics2 = true;
|
|
3687
|
+
seenLyrics = true;
|
|
3688
|
+
lyricLineCounts.push(token.syllables?.length || 0);
|
|
3689
|
+
} else if (seenLyrics && (token.type === "note" || token.type === "rest" || token.type === "bar")) {
|
|
3690
|
+
lyricsAfterAll = false;
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
if (hasLyrics2 && lyricsAfterAll) {
|
|
3694
|
+
miscellaneous.push({ name: "abc-lyrics-after-all", value: "true" });
|
|
3695
|
+
}
|
|
3696
|
+
if (hasLyrics2 && lyricLineCounts.length > 0) {
|
|
3697
|
+
miscellaneous.push({ name: "abc-lyrics-line-counts", value: JSON.stringify(lyricLineCounts) });
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
if (header.tempo && parts.length > 0 && parts[0].measures.length > 0) {
|
|
3702
|
+
const tempoDirection = parseTempoToDirection(header.tempo);
|
|
3703
|
+
if (tempoDirection) {
|
|
3704
|
+
parts[0].measures[0].entries.unshift(tempoDirection);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
const creators = [];
|
|
3708
|
+
if (header.composer) {
|
|
3709
|
+
creators.push({ type: "composer", value: header.composer });
|
|
3710
|
+
}
|
|
3711
|
+
creators.push(...extraCreators);
|
|
3712
|
+
const encoding = {
|
|
3713
|
+
software: ["musicxml-io (ABC import)"]
|
|
3714
|
+
};
|
|
3715
|
+
if (encoderValues.length > 0) {
|
|
3716
|
+
encoding.encoder = encoderValues;
|
|
3717
|
+
}
|
|
3718
|
+
return {
|
|
3719
|
+
_id: generateId(),
|
|
3720
|
+
metadata: {
|
|
3721
|
+
movementTitle: header.title,
|
|
3722
|
+
creators: creators.length > 0 ? creators : void 0,
|
|
3723
|
+
source: sourceValue,
|
|
3724
|
+
encoding,
|
|
3725
|
+
miscellaneous: miscellaneous.length > 0 ? miscellaneous : void 0
|
|
3726
|
+
},
|
|
3727
|
+
partList: partListEntries,
|
|
3728
|
+
parts,
|
|
3729
|
+
version: "4.0"
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
function parseTempoToDirection(tempoStr) {
|
|
3733
|
+
const match = tempoStr.match(/(?:(\d+)\/(\d+)\s*=\s*)?(\d+)/);
|
|
3734
|
+
if (!match) return null;
|
|
3735
|
+
const perMinute = parseInt(match[3], 10);
|
|
3736
|
+
let beatUnit = "quarter";
|
|
3737
|
+
if (match[1] && match[2]) {
|
|
3738
|
+
const num = parseInt(match[1], 10);
|
|
3739
|
+
const den = parseInt(match[2], 10);
|
|
3740
|
+
const quarterNotes = num / den * 4;
|
|
3741
|
+
const found = NOTE_TYPE_MAP[quarterNotes];
|
|
3742
|
+
if (found) beatUnit = found;
|
|
3743
|
+
}
|
|
3744
|
+
return {
|
|
3745
|
+
_id: generateId(),
|
|
3746
|
+
type: "direction",
|
|
3747
|
+
directionTypes: [{ kind: "metronome", beatUnit, perMinute }],
|
|
3748
|
+
placement: "above",
|
|
3749
|
+
sound: { tempo: perMinute }
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
function abcClefToMusicXml(abcClef) {
|
|
3753
|
+
if (!abcClef) return { sign: "G", line: 2 };
|
|
3754
|
+
const c = abcClef.toLowerCase();
|
|
3755
|
+
if (c === "treble" || c === "treble-8va" || c === "treble+8") return { sign: "G", line: 2 };
|
|
3756
|
+
if (c === "bass" || c === "bass3") return { sign: "F", line: 4 };
|
|
3757
|
+
if (c === "alto") return { sign: "C", line: 3 };
|
|
3758
|
+
if (c === "tenor") return { sign: "C", line: 4 };
|
|
3759
|
+
if (c === "soprano") return { sign: "C", line: 1 };
|
|
3760
|
+
if (c === "mezzo" || c === "mezzo-soprano") return { sign: "C", line: 2 };
|
|
3761
|
+
if (c === "baritone") return { sign: "C", line: 5 };
|
|
3762
|
+
if (c === "perc" || c === "percussion") return { sign: "percussion" };
|
|
3763
|
+
return { sign: "G", line: 2 };
|
|
3764
|
+
}
|
|
3765
|
+
function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDuration, clef) {
|
|
3766
|
+
const measures = [];
|
|
3767
|
+
let currentEntries = [];
|
|
3768
|
+
let hasIndividualChordDurations = false;
|
|
3769
|
+
let currentBarlines = [];
|
|
3770
|
+
let currentPosition = 0;
|
|
3771
|
+
let measureNumber = 1;
|
|
3772
|
+
let isFirstMeasure = true;
|
|
3773
|
+
let pendingTie = false;
|
|
3774
|
+
let slurDepth = 0;
|
|
3775
|
+
let pendingSlurStarts = 0;
|
|
3776
|
+
let slurStartNotes = [];
|
|
3777
|
+
let inGrace = false;
|
|
3778
|
+
let pendingBrokenRhythm = null;
|
|
3779
|
+
let tupletState = null;
|
|
3780
|
+
const pendingPreNoteItems = [];
|
|
3781
|
+
let pendingEndingNumber = null;
|
|
3782
|
+
let currentLyrics = [];
|
|
3783
|
+
let noteCountForLyrics = 0;
|
|
3784
|
+
let inChord = false;
|
|
3785
|
+
let chordNotes = [];
|
|
3786
|
+
let chordNoteTies = [];
|
|
3787
|
+
let currentUnitNote = { ...unitNote };
|
|
3788
|
+
let pendingTupletStart = false;
|
|
3789
|
+
let pendingKeyChange = null;
|
|
3790
|
+
const lineBreaks = [];
|
|
3791
|
+
function flushPendingPreNoteItems() {
|
|
3792
|
+
for (const item of pendingPreNoteItems) {
|
|
3793
|
+
if (item.kind === "harmony") {
|
|
3794
|
+
const harmony = createHarmonyEntry(item.value);
|
|
3795
|
+
if (harmony) currentEntries.push(harmony);
|
|
3796
|
+
} else if (item.kind === "dynamic") {
|
|
3797
|
+
const dynDir = createDynamicsDirection(item.value);
|
|
3798
|
+
if (dynDir) currentEntries.push(dynDir);
|
|
3799
|
+
} else if (item.kind === "decoration") {
|
|
3800
|
+
const decoDir = {
|
|
3801
|
+
_id: generateId(),
|
|
3802
|
+
type: "direction",
|
|
3803
|
+
directionTypes: [{ kind: "words", text: item.value }]
|
|
3804
|
+
};
|
|
3805
|
+
currentEntries.push(decoDir);
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
pendingPreNoteItems.length = 0;
|
|
3809
|
+
}
|
|
3810
|
+
function finalizeMeasure(endBarType) {
|
|
3811
|
+
const measure = {
|
|
3812
|
+
_id: generateId(),
|
|
3813
|
+
number: String(measureNumber),
|
|
3814
|
+
entries: currentEntries
|
|
3815
|
+
};
|
|
3816
|
+
if (isFirstMeasure) {
|
|
3817
|
+
measure.attributes = {
|
|
3818
|
+
divisions: DIVISIONS,
|
|
3819
|
+
time: timeSignature,
|
|
3820
|
+
key: keySignature,
|
|
3821
|
+
clef: [clef || { sign: "G", line: 2 }]
|
|
3822
|
+
};
|
|
3823
|
+
isFirstMeasure = false;
|
|
3824
|
+
}
|
|
3825
|
+
if (pendingKeyChange) {
|
|
3826
|
+
if (!measure.attributes) {
|
|
3827
|
+
measure.attributes = {};
|
|
3828
|
+
}
|
|
3829
|
+
const kValue = pendingKeyChange.replace(/^K:\s*/, "");
|
|
3830
|
+
measure.attributes.key = parseKeySignature2(kValue);
|
|
3831
|
+
pendingKeyChange = null;
|
|
3832
|
+
}
|
|
3833
|
+
if (endBarType || currentBarlines.length > 0) {
|
|
3834
|
+
measure.barlines = [...currentBarlines];
|
|
3835
|
+
if (endBarType) {
|
|
3836
|
+
const barline = createBarline(endBarType, "right", pendingEndingNumber);
|
|
3837
|
+
if (barline) {
|
|
3838
|
+
measure.barlines.push(barline);
|
|
3839
|
+
}
|
|
3840
|
+
pendingEndingNumber = null;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
measures.push(measure);
|
|
3844
|
+
currentEntries = [];
|
|
3845
|
+
currentBarlines = [];
|
|
3846
|
+
currentPosition = 0;
|
|
3847
|
+
measureNumber++;
|
|
3848
|
+
}
|
|
3849
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
3850
|
+
const token = tokens[ti];
|
|
3851
|
+
switch (token.type) {
|
|
3852
|
+
case "note": {
|
|
3853
|
+
if (inChord) {
|
|
3854
|
+
chordNotes.push(token);
|
|
3855
|
+
break;
|
|
3856
|
+
}
|
|
3857
|
+
const entry = createNoteEntry(token, currentUnitNote, pendingTie, inGrace, tupletState);
|
|
3858
|
+
pendingTie = false;
|
|
3859
|
+
if (pendingBrokenRhythm) {
|
|
3860
|
+
const n = pendingBrokenRhythm.length;
|
|
3861
|
+
const divisor = Math.pow(2, n);
|
|
3862
|
+
if (pendingBrokenRhythm[0] === ">") {
|
|
3863
|
+
entry.duration = Math.round(entry.duration / divisor);
|
|
3864
|
+
} else {
|
|
3865
|
+
entry.duration = Math.round(entry.duration * (2 * divisor - 1) / divisor);
|
|
3866
|
+
}
|
|
3867
|
+
pendingBrokenRhythm = null;
|
|
3868
|
+
}
|
|
3869
|
+
if (pendingTupletStart && !inGrace) {
|
|
3870
|
+
pendingTupletStart = false;
|
|
3871
|
+
}
|
|
3872
|
+
flushPendingPreNoteItems();
|
|
3873
|
+
while (pendingSlurStarts > 0) {
|
|
3874
|
+
if (!entry.notations) entry.notations = [];
|
|
3875
|
+
entry.notations.push({ type: "slur", slurType: "start", number: slurDepth - pendingSlurStarts + 1 });
|
|
3876
|
+
slurStartNotes.push(entry);
|
|
3877
|
+
pendingSlurStarts--;
|
|
3878
|
+
}
|
|
3879
|
+
if (currentLyrics.length > noteCountForLyrics && !entry.rest && !entry.grace) {
|
|
3880
|
+
const syllable = currentLyrics[noteCountForLyrics];
|
|
3881
|
+
if (syllable && syllable !== "" && syllable !== "*") {
|
|
3882
|
+
const isHyphenated = syllable.endsWith("-");
|
|
3883
|
+
const text = isHyphenated ? syllable.slice(0, -1) : syllable;
|
|
3884
|
+
entry.lyrics = [{
|
|
3885
|
+
number: 1,
|
|
3886
|
+
text,
|
|
3887
|
+
syllabic: isHyphenated ? "begin" : "single"
|
|
3888
|
+
}];
|
|
3889
|
+
if (isHyphenated && noteCountForLyrics + 1 < currentLyrics.length) {
|
|
3890
|
+
const nextSyllable = currentLyrics[noteCountForLyrics + 1];
|
|
3891
|
+
if (nextSyllable && !nextSyllable.endsWith("-")) {
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
if (noteCountForLyrics > 0) {
|
|
3895
|
+
const prevSyllable = currentLyrics[noteCountForLyrics - 1];
|
|
3896
|
+
if (prevSyllable && prevSyllable.endsWith("-")) {
|
|
3897
|
+
entry.lyrics[0].syllabic = isHyphenated ? "middle" : "end";
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
noteCountForLyrics++;
|
|
3902
|
+
}
|
|
3903
|
+
if (!inGrace) {
|
|
3904
|
+
currentEntries.push(entry);
|
|
3905
|
+
currentPosition += entry.duration;
|
|
3906
|
+
if (tupletState) {
|
|
3907
|
+
tupletState.remaining--;
|
|
3908
|
+
if (tupletState.remaining <= 0) {
|
|
3909
|
+
tupletState = null;
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
} else {
|
|
3913
|
+
currentEntries.push(entry);
|
|
3914
|
+
}
|
|
3915
|
+
break;
|
|
3916
|
+
}
|
|
3917
|
+
case "rest": {
|
|
3918
|
+
if (inChord) break;
|
|
3919
|
+
const restEntry = createRestEntry(token, currentUnitNote, tupletState, measureDuration);
|
|
3920
|
+
flushPendingPreNoteItems();
|
|
3921
|
+
currentEntries.push(restEntry);
|
|
3922
|
+
currentPosition += restEntry.duration;
|
|
3923
|
+
if (tupletState) {
|
|
3924
|
+
tupletState.remaining--;
|
|
3925
|
+
if (tupletState.remaining <= 0) {
|
|
3926
|
+
tupletState = null;
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
break;
|
|
3930
|
+
}
|
|
3931
|
+
case "chord_start":
|
|
3932
|
+
inChord = true;
|
|
3933
|
+
chordNotes = [];
|
|
3934
|
+
chordNoteTies = [];
|
|
3935
|
+
break;
|
|
3936
|
+
case "chord_end": {
|
|
3937
|
+
inChord = false;
|
|
3938
|
+
if (chordNotes.length > 0) {
|
|
3939
|
+
for (const item of pendingPreNoteItems) {
|
|
3940
|
+
if (item.kind === "harmony") {
|
|
3941
|
+
const harmony = createHarmonyEntry(item.value);
|
|
3942
|
+
if (harmony) currentEntries.push(harmony);
|
|
3943
|
+
} else if (item.kind === "dynamic") {
|
|
3944
|
+
const dynDir = createDynamicsDirection(item.value);
|
|
3945
|
+
if (dynDir) currentEntries.push(dynDir);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
pendingPreNoteItems.length = 0;
|
|
3949
|
+
const chordDurNum = token.durationNum || 1;
|
|
3950
|
+
const chordDurDen = token.durationDen || 1;
|
|
3951
|
+
const hasIndividualDurations = chordNotes.some(
|
|
3952
|
+
(cn) => cn.durationNum !== void 0 && cn.durationNum !== 1 || cn.durationDen !== void 0 && cn.durationDen !== 1
|
|
3953
|
+
);
|
|
3954
|
+
const useIndividualDurations = hasIndividualDurations && chordDurNum === 1 && chordDurDen === 1;
|
|
3955
|
+
if (useIndividualDurations) hasIndividualChordDurations = true;
|
|
3956
|
+
for (let ci = 0; ci < chordNotes.length; ci++) {
|
|
3957
|
+
const chordToken = chordNotes[ci];
|
|
3958
|
+
const originalNum = chordToken.durationNum;
|
|
3959
|
+
const originalDen = chordToken.durationDen;
|
|
3960
|
+
if (!useIndividualDurations) {
|
|
3961
|
+
chordToken.durationNum = chordDurNum;
|
|
3962
|
+
chordToken.durationDen = chordDurDen;
|
|
3963
|
+
}
|
|
3964
|
+
const entry = createNoteEntry(chordToken, currentUnitNote, false, inGrace, tupletState);
|
|
3965
|
+
chordToken.durationNum = originalNum;
|
|
3966
|
+
chordToken.durationDen = originalDen;
|
|
3967
|
+
if (ci === 0) {
|
|
3968
|
+
while (pendingSlurStarts > 0) {
|
|
3969
|
+
if (!entry.notations) entry.notations = [];
|
|
3970
|
+
entry.notations.push({ type: "slur", slurType: "start", number: slurDepth - pendingSlurStarts + 1 });
|
|
3971
|
+
slurStartNotes.push(entry);
|
|
3972
|
+
pendingSlurStarts--;
|
|
3973
|
+
}
|
|
3974
|
+
if (useIndividualDurations) {
|
|
3975
|
+
entry._abcIndividualChordDurations = true;
|
|
3976
|
+
}
|
|
3977
|
+
} else {
|
|
3978
|
+
entry.chord = true;
|
|
3979
|
+
}
|
|
3980
|
+
if (chordNoteTies[ci]) {
|
|
3981
|
+
entry.tie = { type: "start" };
|
|
3982
|
+
entry.ties = [{ type: "start" }];
|
|
3983
|
+
if (!entry.notations) entry.notations = [];
|
|
3984
|
+
entry.notations.push({ type: "tied", tiedType: "start" });
|
|
3985
|
+
}
|
|
3986
|
+
currentEntries.push(entry);
|
|
3987
|
+
if (ci === 0) {
|
|
3988
|
+
currentPosition += entry.duration;
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
chordNotes = [];
|
|
3993
|
+
break;
|
|
3994
|
+
}
|
|
3995
|
+
case "bar": {
|
|
3996
|
+
flushPendingPreNoteItems();
|
|
3997
|
+
const barType = token.barType || "regular";
|
|
3998
|
+
if (barType === "double-repeat") {
|
|
3999
|
+
finalizeMeasure("end-repeat");
|
|
4000
|
+
currentBarlines.push(createBarline("start-repeat", "left", null));
|
|
4001
|
+
} else if (barType === "end-repeat") {
|
|
4002
|
+
finalizeMeasure("end-repeat");
|
|
4003
|
+
} else if (barType === "start-repeat") {
|
|
4004
|
+
if (currentEntries.length > 0) {
|
|
4005
|
+
finalizeMeasure("regular");
|
|
4006
|
+
}
|
|
4007
|
+
currentBarlines.push(createBarline("start-repeat", "left", null));
|
|
4008
|
+
} else if (barType === "final") {
|
|
4009
|
+
finalizeMeasure("final");
|
|
4010
|
+
} else if (barType === "end-repeat-final") {
|
|
4011
|
+
finalizeMeasure("end-repeat");
|
|
4012
|
+
} else {
|
|
4013
|
+
if (currentEntries.length > 0 || currentBarlines.length > 0) {
|
|
4014
|
+
finalizeMeasure(barType !== "regular" ? barType : void 0);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
break;
|
|
4018
|
+
}
|
|
4019
|
+
case "ending": {
|
|
4020
|
+
const prevTok = ti > 0 ? tokens[ti - 1] : null;
|
|
4021
|
+
const prevBarType = prevTok?.barType || "regular";
|
|
4022
|
+
if (!token.bracket && prevTok?.type === "bar" && prevBarType !== "regular" && measures.length > 0) {
|
|
4023
|
+
const lastMeasure = measures[measures.length - 1];
|
|
4024
|
+
const rightBl = lastMeasure.barlines?.find((b) => b.location === "right");
|
|
4025
|
+
if (rightBl) {
|
|
4026
|
+
const endingBarline2 = {
|
|
4027
|
+
_id: generateId(),
|
|
4028
|
+
location: "left",
|
|
4029
|
+
barStyle: rightBl.barStyle,
|
|
4030
|
+
repeat: rightBl.repeat,
|
|
4031
|
+
ending: { number: token.value, type: "start" }
|
|
4032
|
+
};
|
|
4033
|
+
lastMeasure.barlines = lastMeasure.barlines.filter((b) => b !== rightBl);
|
|
4034
|
+
currentBarlines.push(endingBarline2);
|
|
4035
|
+
break;
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
const endingBarline = {
|
|
4039
|
+
_id: generateId(),
|
|
4040
|
+
location: "left",
|
|
4041
|
+
ending: {
|
|
4042
|
+
number: token.value,
|
|
4043
|
+
type: "start"
|
|
4044
|
+
}
|
|
4045
|
+
};
|
|
4046
|
+
if (ti > 0 && tokens[ti - 1].type === "bar" && (!tokens[ti - 1].barType || tokens[ti - 1].barType === "regular") && currentEntries.length === 0) {
|
|
4047
|
+
endingBarline.barStyle = "regular";
|
|
4048
|
+
}
|
|
4049
|
+
currentBarlines.push(endingBarline);
|
|
4050
|
+
break;
|
|
4051
|
+
}
|
|
4052
|
+
case "tie":
|
|
4053
|
+
if (inChord) {
|
|
4054
|
+
if (chordNotes.length > 0) {
|
|
4055
|
+
chordNoteTies[chordNotes.length - 1] = true;
|
|
4056
|
+
}
|
|
4057
|
+
} else {
|
|
4058
|
+
pendingTie = true;
|
|
4059
|
+
for (let ei = currentEntries.length - 1; ei >= 0; ei--) {
|
|
4060
|
+
const e = currentEntries[ei];
|
|
4061
|
+
if (e.type === "note" && !e.rest) {
|
|
4062
|
+
e.tie = { type: "start" };
|
|
4063
|
+
e.ties = [{ type: "start" }];
|
|
4064
|
+
if (!e.notations) e.notations = [];
|
|
4065
|
+
e.notations.push({ type: "tied", tiedType: "start" });
|
|
4066
|
+
break;
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
break;
|
|
4071
|
+
case "slur_start":
|
|
4072
|
+
slurDepth++;
|
|
4073
|
+
pendingSlurStarts++;
|
|
4074
|
+
break;
|
|
4075
|
+
case "slur_end":
|
|
4076
|
+
if (slurDepth > 0) {
|
|
4077
|
+
slurDepth--;
|
|
4078
|
+
for (let ei = currentEntries.length - 1; ei >= 0; ei--) {
|
|
4079
|
+
const e = currentEntries[ei];
|
|
4080
|
+
if (e.type === "note") {
|
|
4081
|
+
if (!e.notations) e.notations = [];
|
|
4082
|
+
e.notations.push({ type: "slur", slurType: "stop", number: slurDepth + 1 });
|
|
4083
|
+
break;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
break;
|
|
4088
|
+
case "grace_start":
|
|
4089
|
+
inGrace = true;
|
|
4090
|
+
break;
|
|
4091
|
+
case "grace_end":
|
|
4092
|
+
inGrace = false;
|
|
4093
|
+
break;
|
|
4094
|
+
case "broken_rhythm": {
|
|
4095
|
+
const brokenN = token.value.length;
|
|
4096
|
+
const brokenDivisor = Math.pow(2, brokenN);
|
|
4097
|
+
for (let ei = currentEntries.length - 1; ei >= 0; ei--) {
|
|
4098
|
+
const e = currentEntries[ei];
|
|
4099
|
+
if (e.type === "note") {
|
|
4100
|
+
if (token.value[0] === ">") {
|
|
4101
|
+
e.duration = Math.round(e.duration * (2 * brokenDivisor - 1) / brokenDivisor);
|
|
4102
|
+
} else {
|
|
4103
|
+
e.duration = Math.round(e.duration / brokenDivisor);
|
|
4104
|
+
}
|
|
4105
|
+
break;
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
pendingBrokenRhythm = token.value;
|
|
4109
|
+
break;
|
|
4110
|
+
}
|
|
4111
|
+
case "tuplet":
|
|
4112
|
+
tupletState = {
|
|
4113
|
+
p: token.tupletP,
|
|
4114
|
+
q: token.tupletQ,
|
|
4115
|
+
remaining: token.tupletR || token.tupletP
|
|
4116
|
+
};
|
|
4117
|
+
pendingTupletStart = true;
|
|
4118
|
+
break;
|
|
4119
|
+
case "chord_symbol":
|
|
4120
|
+
pendingPreNoteItems.push({ kind: "harmony", value: token.value });
|
|
4121
|
+
break;
|
|
4122
|
+
case "decoration":
|
|
4123
|
+
if (DYNAMICS_VALUES.has(token.value)) {
|
|
4124
|
+
pendingPreNoteItems.push({ kind: "dynamic", value: token.value });
|
|
4125
|
+
} else {
|
|
4126
|
+
const isShorthand = token.value.length === 1 && /^[vuTM]$/.test(token.value);
|
|
4127
|
+
const decoText = isShorthand ? token.value : `!${token.value}!`;
|
|
4128
|
+
pendingPreNoteItems.push({ kind: "decoration", value: decoText });
|
|
4129
|
+
}
|
|
4130
|
+
break;
|
|
4131
|
+
case "lyrics": {
|
|
4132
|
+
const syllables = token.syllables || [];
|
|
4133
|
+
applyLyricsToExistingNotes(measures, currentEntries, syllables);
|
|
4134
|
+
break;
|
|
4135
|
+
}
|
|
4136
|
+
case "overlay": {
|
|
4137
|
+
if (currentPosition > 0) {
|
|
4138
|
+
const backupEntry = {
|
|
4139
|
+
_id: generateId(),
|
|
4140
|
+
type: "backup",
|
|
4141
|
+
duration: currentPosition
|
|
4142
|
+
};
|
|
4143
|
+
currentEntries.push(backupEntry);
|
|
4144
|
+
currentPosition = 0;
|
|
4145
|
+
}
|
|
4146
|
+
break;
|
|
4147
|
+
}
|
|
4148
|
+
case "inline_field": {
|
|
4149
|
+
const lMatch = token.value.match(/^L:\s*(\d+)\/(\d+)/);
|
|
4150
|
+
if (lMatch) {
|
|
4151
|
+
currentUnitNote = { num: parseInt(lMatch[1], 10), den: parseInt(lMatch[2], 10) };
|
|
4152
|
+
const inlineEntry = {
|
|
4153
|
+
_id: generateId(),
|
|
4154
|
+
type: "direction",
|
|
4155
|
+
directionTypes: [{ kind: "words", text: `[L:${lMatch[1]}/${lMatch[2]}]` }]
|
|
4156
|
+
};
|
|
4157
|
+
currentEntries.push(inlineEntry);
|
|
4158
|
+
}
|
|
4159
|
+
const kMatch = token.value.match(/^K:\s*(.*)/);
|
|
4160
|
+
if (kMatch) {
|
|
4161
|
+
pendingKeyChange = `K:${kMatch[1]}`;
|
|
4162
|
+
}
|
|
4163
|
+
break;
|
|
4164
|
+
}
|
|
4165
|
+
case "space":
|
|
4166
|
+
break;
|
|
4167
|
+
case "line_break":
|
|
4168
|
+
flushPendingPreNoteItems();
|
|
4169
|
+
if (currentEntries.length > 0) {
|
|
4170
|
+
const breakText = token.value === "\\\n" ? "__abc_line_cont__" : "__abc_line_break__";
|
|
4171
|
+
const lineBreakDir = {
|
|
4172
|
+
_id: generateId(),
|
|
4173
|
+
type: "direction",
|
|
4174
|
+
directionTypes: [{ kind: "words", text: breakText }]
|
|
4175
|
+
};
|
|
4176
|
+
currentEntries.push(lineBreakDir);
|
|
4177
|
+
}
|
|
4178
|
+
if (measures.length > 0) {
|
|
4179
|
+
if (token.value === "\\\n") {
|
|
4180
|
+
lineBreaks.push(-measures.length);
|
|
4181
|
+
} else {
|
|
4182
|
+
lineBreaks.push(measures.length);
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
break;
|
|
4186
|
+
default:
|
|
4187
|
+
break;
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
if (currentEntries.length > 0) {
|
|
4191
|
+
finalizeMeasure();
|
|
4192
|
+
}
|
|
4193
|
+
applyTieStops(measures);
|
|
4194
|
+
return { measures, lineBreaks, hasIndividualChordDurations };
|
|
4195
|
+
}
|
|
4196
|
+
function applyLyricsToExistingNotes(finalizedMeasures, currentEntries, syllables) {
|
|
4197
|
+
const allNotes = [];
|
|
4198
|
+
for (const measure of finalizedMeasures) {
|
|
4199
|
+
for (const entry of measure.entries) {
|
|
4200
|
+
if (entry.type === "note" && !entry.rest && !entry.grace && !entry.chord) {
|
|
4201
|
+
allNotes.push(entry);
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
for (const entry of currentEntries) {
|
|
4206
|
+
if (entry.type === "note" && !entry.rest && !entry.grace && !entry.chord) {
|
|
4207
|
+
allNotes.push(entry);
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
const unlyricedNotes = allNotes.filter((n) => !n.lyrics || n.lyrics.length === 0);
|
|
4211
|
+
const targetNotes = unlyricedNotes.slice(0, syllables.length);
|
|
4212
|
+
for (let si = 0; si < syllables.length && si < targetNotes.length; si++) {
|
|
4213
|
+
const syllable = syllables[si];
|
|
4214
|
+
if (!syllable || syllable === "" || syllable === "*") continue;
|
|
4215
|
+
const note = targetNotes[si];
|
|
4216
|
+
const isHyphenated = syllable.endsWith("-");
|
|
4217
|
+
const text = isHyphenated ? syllable.slice(0, -1) : syllable;
|
|
4218
|
+
let syllabic = isHyphenated ? "begin" : "single";
|
|
4219
|
+
if (si > 0) {
|
|
4220
|
+
const prevSyllable = syllables[si - 1];
|
|
4221
|
+
if (prevSyllable && prevSyllable.endsWith("-")) {
|
|
4222
|
+
syllabic = isHyphenated ? "middle" : "end";
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
note.lyrics = [{
|
|
4226
|
+
number: 1,
|
|
4227
|
+
text,
|
|
4228
|
+
syllabic
|
|
4229
|
+
}];
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
function applyTieStops(measures) {
|
|
4233
|
+
const allNotes = [];
|
|
4234
|
+
for (const measure of measures) {
|
|
4235
|
+
for (const entry of measure.entries) {
|
|
4236
|
+
if (entry.type === "note" && !entry.grace) {
|
|
4237
|
+
allNotes.push(entry);
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
for (let i = 0; i < allNotes.length - 1; i++) {
|
|
4242
|
+
const note = allNotes[i];
|
|
4243
|
+
if (note.tie?.type === "start" && note.pitch) {
|
|
4244
|
+
for (let j = i + 1; j < allNotes.length; j++) {
|
|
4245
|
+
const next = allNotes[j];
|
|
4246
|
+
if (next.pitch && next.pitch.step === note.pitch.step && next.pitch.octave === note.pitch.octave) {
|
|
4247
|
+
if (next.tie?.type === "start") {
|
|
4248
|
+
next.tie = { type: "stop" };
|
|
4249
|
+
next.ties = [{ type: "stop" }, { type: "start" }];
|
|
4250
|
+
} else {
|
|
4251
|
+
next.tie = { type: "stop" };
|
|
4252
|
+
next.ties = [{ type: "stop" }];
|
|
4253
|
+
}
|
|
4254
|
+
if (!next.notations) next.notations = [];
|
|
4255
|
+
next.notations.push({ type: "tied", tiedType: "stop" });
|
|
4256
|
+
break;
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
function createNoteEntry(token, unitNote, _hasTieStop, isGrace, tupletState) {
|
|
4263
|
+
const num = token.durationNum || 1;
|
|
4264
|
+
const den = token.durationDen || 1;
|
|
4265
|
+
let duration;
|
|
4266
|
+
if (isGrace) {
|
|
4267
|
+
duration = 0;
|
|
4268
|
+
} else {
|
|
4269
|
+
duration = lengthToDuration(num, den, unitNote);
|
|
4270
|
+
if (tupletState) {
|
|
4271
|
+
duration = Math.round(duration * tupletState.q / tupletState.p);
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
const { noteType, dots } = durationToNoteType(isGrace ? lengthToDuration(num, den, unitNote) : duration);
|
|
4275
|
+
const entry = {
|
|
4276
|
+
_id: generateId(),
|
|
4277
|
+
type: "note",
|
|
4278
|
+
pitch: token.pitch,
|
|
4279
|
+
duration,
|
|
4280
|
+
voice: 1,
|
|
4281
|
+
noteType,
|
|
4282
|
+
dots: dots > 0 ? dots : void 0
|
|
4283
|
+
};
|
|
4284
|
+
if (isGrace) {
|
|
4285
|
+
entry.grace = { slash: true };
|
|
4286
|
+
entry.noteType = "eighth";
|
|
4287
|
+
}
|
|
4288
|
+
if (tupletState && !isGrace) {
|
|
4289
|
+
entry.timeModification = {
|
|
4290
|
+
actualNotes: tupletState.p,
|
|
4291
|
+
normalNotes: tupletState.q
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
if (token.explicitNatural) {
|
|
4295
|
+
entry.accidental = { value: "natural" };
|
|
4296
|
+
} else if (token.accidental !== void 0 && token.accidental !== 0) {
|
|
4297
|
+
switch (token.accidental) {
|
|
4298
|
+
case 1:
|
|
4299
|
+
entry.accidental = { value: "sharp" };
|
|
4300
|
+
break;
|
|
4301
|
+
case 2:
|
|
4302
|
+
entry.accidental = { value: "double-sharp" };
|
|
4303
|
+
break;
|
|
4304
|
+
case -1:
|
|
4305
|
+
entry.accidental = { value: "flat" };
|
|
4306
|
+
break;
|
|
4307
|
+
case -2:
|
|
4308
|
+
entry.accidental = { value: "double-flat" };
|
|
4309
|
+
break;
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
return entry;
|
|
4313
|
+
}
|
|
4314
|
+
function createRestEntry(token, unitNote, tupletState, measureDuration) {
|
|
4315
|
+
const num = token.durationNum || 1;
|
|
4316
|
+
const den = token.durationDen || 1;
|
|
4317
|
+
const isWholeMeasure = token.value.startsWith("Z");
|
|
4318
|
+
const isInvisible = token.value.startsWith("x") || token.value.startsWith("X");
|
|
4319
|
+
let duration;
|
|
4320
|
+
if (isWholeMeasure) {
|
|
4321
|
+
duration = measureDuration;
|
|
4322
|
+
} else {
|
|
4323
|
+
duration = lengthToDuration(num, den, unitNote);
|
|
4324
|
+
if (tupletState) {
|
|
4325
|
+
duration = Math.round(duration * tupletState.q / tupletState.p);
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
const { noteType, dots } = durationToNoteType(duration);
|
|
4329
|
+
const entry = {
|
|
4330
|
+
_id: generateId(),
|
|
4331
|
+
type: "note",
|
|
4332
|
+
rest: isWholeMeasure ? { measure: true } : {},
|
|
4333
|
+
duration,
|
|
4334
|
+
voice: 1,
|
|
4335
|
+
noteType,
|
|
4336
|
+
dots: dots > 0 ? dots : void 0
|
|
4337
|
+
};
|
|
4338
|
+
if (isInvisible) {
|
|
4339
|
+
entry.printObject = false;
|
|
4340
|
+
}
|
|
4341
|
+
return entry;
|
|
4342
|
+
}
|
|
4343
|
+
function createBarline(barType, location, endingNumber) {
|
|
4344
|
+
const barline = {
|
|
4345
|
+
_id: generateId(),
|
|
4346
|
+
location
|
|
4347
|
+
};
|
|
4348
|
+
switch (barType) {
|
|
4349
|
+
case "start-repeat":
|
|
4350
|
+
barline.barStyle = "heavy-light";
|
|
4351
|
+
barline.repeat = { direction: "forward" };
|
|
4352
|
+
break;
|
|
4353
|
+
case "end-repeat":
|
|
4354
|
+
barline.barStyle = "light-heavy";
|
|
4355
|
+
barline.repeat = { direction: "backward" };
|
|
4356
|
+
break;
|
|
4357
|
+
case "final":
|
|
4358
|
+
barline.barStyle = "light-heavy";
|
|
4359
|
+
break;
|
|
4360
|
+
case "double":
|
|
4361
|
+
barline.barStyle = "light-light";
|
|
4362
|
+
break;
|
|
4363
|
+
case "heavy-light":
|
|
4364
|
+
barline.barStyle = "heavy-light";
|
|
4365
|
+
break;
|
|
4366
|
+
case "thick-thin":
|
|
4367
|
+
barline.barStyle = "heavy-light";
|
|
4368
|
+
break;
|
|
4369
|
+
default:
|
|
4370
|
+
return null;
|
|
4371
|
+
}
|
|
4372
|
+
if (endingNumber) {
|
|
4373
|
+
barline.ending = {
|
|
4374
|
+
number: endingNumber,
|
|
4375
|
+
type: location === "left" ? "start" : "stop"
|
|
4376
|
+
};
|
|
4377
|
+
}
|
|
4378
|
+
return barline;
|
|
4379
|
+
}
|
|
4380
|
+
function createHarmonyEntry(chordStr) {
|
|
4381
|
+
const match = chordStr.match(/^([A-G])(#|b)?(min7|m7|maj7|M7|dim7|aug7|m6|m9|min|maj|dim|aug|sus4|sus2|add9|add11|add|7|9|11|13|6|m)?(\/([A-G](#|b)?))?/);
|
|
4382
|
+
if (!match) return null;
|
|
4383
|
+
const rootStep = match[1];
|
|
4384
|
+
const rootAlter = match[2] === "#" ? 1 : match[2] === "b" ? -1 : void 0;
|
|
4385
|
+
const quality = match[3] || "";
|
|
4386
|
+
const bassNote = match[5];
|
|
4387
|
+
let kind = "major";
|
|
4388
|
+
switch (quality) {
|
|
4389
|
+
case "m":
|
|
4390
|
+
case "min":
|
|
4391
|
+
kind = "minor";
|
|
4392
|
+
break;
|
|
4393
|
+
case "7":
|
|
4394
|
+
kind = "dominant";
|
|
4395
|
+
break;
|
|
4396
|
+
case "maj7":
|
|
4397
|
+
case "M7":
|
|
4398
|
+
kind = "major-seventh";
|
|
4399
|
+
break;
|
|
4400
|
+
case "m7":
|
|
4401
|
+
case "min7":
|
|
4402
|
+
kind = "minor-seventh";
|
|
4403
|
+
break;
|
|
4404
|
+
case "dim":
|
|
4405
|
+
kind = "diminished";
|
|
4406
|
+
break;
|
|
4407
|
+
case "dim7":
|
|
4408
|
+
kind = "diminished-seventh";
|
|
4409
|
+
break;
|
|
4410
|
+
case "aug":
|
|
4411
|
+
kind = "augmented";
|
|
4412
|
+
break;
|
|
4413
|
+
case "aug7":
|
|
4414
|
+
kind = "augmented-seventh";
|
|
4415
|
+
break;
|
|
4416
|
+
case "6":
|
|
4417
|
+
kind = "major-sixth";
|
|
4418
|
+
break;
|
|
4419
|
+
case "m6":
|
|
4420
|
+
kind = "minor-sixth";
|
|
4421
|
+
break;
|
|
4422
|
+
case "9":
|
|
4423
|
+
kind = "dominant-ninth";
|
|
4424
|
+
break;
|
|
4425
|
+
case "m9":
|
|
4426
|
+
kind = "minor-ninth";
|
|
4427
|
+
break;
|
|
4428
|
+
case "sus4":
|
|
4429
|
+
kind = "suspended-fourth";
|
|
4430
|
+
break;
|
|
4431
|
+
case "sus2":
|
|
4432
|
+
kind = "suspended-second";
|
|
4433
|
+
break;
|
|
4434
|
+
}
|
|
4435
|
+
const entry = {
|
|
4436
|
+
_id: generateId(),
|
|
4437
|
+
type: "harmony",
|
|
4438
|
+
root: { rootStep, rootAlter: rootAlter !== void 0 ? rootAlter : void 0 },
|
|
4439
|
+
kind
|
|
4440
|
+
};
|
|
4441
|
+
if (bassNote) {
|
|
4442
|
+
const bassMatch = bassNote.match(/^([A-G])(#|b)?/);
|
|
4443
|
+
if (bassMatch) {
|
|
4444
|
+
entry.bass = {
|
|
4445
|
+
bassStep: bassMatch[1],
|
|
4446
|
+
bassAlter: bassMatch[2] === "#" ? 1 : bassMatch[2] === "b" ? -1 : void 0
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
return entry;
|
|
4451
|
+
}
|
|
4452
|
+
function createDynamicsDirection(dynamic) {
|
|
4453
|
+
if (!DYNAMICS_VALUES.has(dynamic)) return null;
|
|
4454
|
+
return {
|
|
4455
|
+
_id: generateId(),
|
|
4456
|
+
type: "direction",
|
|
4457
|
+
directionTypes: [{
|
|
4458
|
+
kind: "dynamics",
|
|
4459
|
+
value: dynamic
|
|
4460
|
+
}],
|
|
4461
|
+
placement: "below"
|
|
4462
|
+
};
|
|
4463
|
+
}
|
|
4464
|
+
function parseAbc(abcString) {
|
|
4465
|
+
const lines = abcString.split("\n");
|
|
4466
|
+
const { header, bodyStartIndex, headerFieldOrder } = parseHeader(lines);
|
|
4467
|
+
const bodyLines = lines.slice(bodyStartIndex);
|
|
4468
|
+
const { tokens: voiceTokensList, voiceIds, inlineVoiceMarkers, voiceDeclarationLines, bodyComments, bodyDirectives, wFields, voiceInterleavePattern, groupBarCounts, voiceComments, preVoiceComments, trailingComments } = tokenizeBody(bodyLines);
|
|
4469
|
+
const score = buildScore(header, voiceTokensList, voiceIds, headerFieldOrder, inlineVoiceMarkers, voiceDeclarationLines, bodyComments, bodyDirectives, wFields, voiceInterleavePattern, groupBarCounts, voiceComments, preVoiceComments, trailingComments);
|
|
4470
|
+
const bodyText = bodyLines.join("\n");
|
|
4471
|
+
const hasExplicitHalf = /[A-Ga-gzx]\/2/.test(bodyText);
|
|
4472
|
+
if (hasExplicitHalf) {
|
|
4473
|
+
if (!score.metadata.miscellaneous) score.metadata.miscellaneous = [];
|
|
4474
|
+
score.metadata.miscellaneous.push({ name: "abc-explicit-half", value: "true" });
|
|
4475
|
+
}
|
|
4476
|
+
return score;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
2733
4479
|
// src/validator/index.ts
|
|
2734
4480
|
var DEFAULT_OPTIONS = {
|
|
2735
4481
|
checkDivisions: true,
|
|
@@ -6030,6 +7776,1123 @@ function buildMidiFile(tracks, ticksPerQuarterNote) {
|
|
|
6030
7776
|
return new Uint8Array(chunks);
|
|
6031
7777
|
}
|
|
6032
7778
|
|
|
7779
|
+
// src/exporters/abc.ts
|
|
7780
|
+
var DIVISIONS_PER_QUARTER = 960;
|
|
7781
|
+
var useExplicitHalf = false;
|
|
7782
|
+
var useIndividualChordDurations = false;
|
|
7783
|
+
var FIFTHS_TO_KEY_MAJOR = {
|
|
7784
|
+
[-7]: "Cb",
|
|
7785
|
+
[-6]: "Gb",
|
|
7786
|
+
[-5]: "Db",
|
|
7787
|
+
[-4]: "Ab",
|
|
7788
|
+
[-3]: "Eb",
|
|
7789
|
+
[-2]: "Bb",
|
|
7790
|
+
[-1]: "F",
|
|
7791
|
+
0: "C",
|
|
7792
|
+
1: "G",
|
|
7793
|
+
2: "D",
|
|
7794
|
+
3: "A",
|
|
7795
|
+
4: "E",
|
|
7796
|
+
5: "B",
|
|
7797
|
+
6: "F#",
|
|
7798
|
+
7: "C#"
|
|
7799
|
+
};
|
|
7800
|
+
var FIFTHS_TO_KEY_MINOR = {
|
|
7801
|
+
[-7]: "Ab",
|
|
7802
|
+
[-6]: "Eb",
|
|
7803
|
+
[-5]: "Bb",
|
|
7804
|
+
[-4]: "F",
|
|
7805
|
+
[-3]: "C",
|
|
7806
|
+
[-2]: "G",
|
|
7807
|
+
[-1]: "D",
|
|
7808
|
+
0: "A",
|
|
7809
|
+
1: "E",
|
|
7810
|
+
2: "B",
|
|
7811
|
+
3: "F#",
|
|
7812
|
+
4: "C#",
|
|
7813
|
+
5: "G#",
|
|
7814
|
+
6: "D#",
|
|
7815
|
+
7: "A#"
|
|
7816
|
+
};
|
|
7817
|
+
var MODE_FIFTHS_OFFSET = {
|
|
7818
|
+
"major": 0,
|
|
7819
|
+
"ionian": 0,
|
|
7820
|
+
"minor": 3,
|
|
7821
|
+
"aeolian": 3,
|
|
7822
|
+
"dorian": 2,
|
|
7823
|
+
"phrygian": 4,
|
|
7824
|
+
"lydian": -1,
|
|
7825
|
+
"mixolydian": 1,
|
|
7826
|
+
"locrian": 5
|
|
7827
|
+
};
|
|
7828
|
+
var NOTE_TYPE_TO_QUARTER_LENGTH = {
|
|
7829
|
+
"long": 16,
|
|
7830
|
+
"breve": 8,
|
|
7831
|
+
"whole": 4,
|
|
7832
|
+
"half": 2,
|
|
7833
|
+
"quarter": 1,
|
|
7834
|
+
"eighth": 0.5,
|
|
7835
|
+
"16th": 0.25,
|
|
7836
|
+
"32nd": 0.125,
|
|
7837
|
+
"64th": 0.0625,
|
|
7838
|
+
"128th": 0.03125
|
|
7839
|
+
};
|
|
7840
|
+
function musicXmlClefToAbc(clef) {
|
|
7841
|
+
if (clef.sign === "G" && (clef.line === 2 || clef.line === void 0)) return "treble";
|
|
7842
|
+
if (clef.sign === "F" && (clef.line === 4 || clef.line === void 0)) return "bass";
|
|
7843
|
+
if (clef.sign === "C" && clef.line === 3) return "alto";
|
|
7844
|
+
if (clef.sign === "C" && clef.line === 4) return "tenor";
|
|
7845
|
+
if (clef.sign === "C" && clef.line === 1) return "soprano";
|
|
7846
|
+
if (clef.sign === "C" && clef.line === 2) return "mezzo-soprano";
|
|
7847
|
+
if (clef.sign === "C" && clef.line === 5) return "baritone";
|
|
7848
|
+
if (clef.sign === "percussion") return "perc";
|
|
7849
|
+
return null;
|
|
7850
|
+
}
|
|
7851
|
+
function serializeKey2(key) {
|
|
7852
|
+
const mode = key.mode || "major";
|
|
7853
|
+
const modeOffset = MODE_FIFTHS_OFFSET[mode] ?? 0;
|
|
7854
|
+
const majorFifths = key.fifths + modeOffset;
|
|
7855
|
+
let keyNote;
|
|
7856
|
+
if (mode === "minor" || mode === "aeolian") {
|
|
7857
|
+
keyNote = FIFTHS_TO_KEY_MINOR[key.fifths] || "A";
|
|
7858
|
+
return keyNote + "m";
|
|
7859
|
+
}
|
|
7860
|
+
keyNote = FIFTHS_TO_KEY_MAJOR[majorFifths] || "C";
|
|
7861
|
+
const modeStr = modeToAbcString(mode);
|
|
7862
|
+
return keyNote + modeStr;
|
|
7863
|
+
}
|
|
7864
|
+
function modeToAbcString(mode) {
|
|
7865
|
+
switch (mode) {
|
|
7866
|
+
case "major":
|
|
7867
|
+
case "ionian":
|
|
7868
|
+
return "";
|
|
7869
|
+
case "minor":
|
|
7870
|
+
case "aeolian":
|
|
7871
|
+
return "m";
|
|
7872
|
+
case "dorian":
|
|
7873
|
+
return "dor";
|
|
7874
|
+
case "phrygian":
|
|
7875
|
+
return "phr";
|
|
7876
|
+
case "lydian":
|
|
7877
|
+
return "lyd";
|
|
7878
|
+
case "mixolydian":
|
|
7879
|
+
return "mix";
|
|
7880
|
+
case "locrian":
|
|
7881
|
+
return "loc";
|
|
7882
|
+
default:
|
|
7883
|
+
return "";
|
|
7884
|
+
}
|
|
7885
|
+
}
|
|
7886
|
+
function serializeTimeSignature(time) {
|
|
7887
|
+
if (time.symbol === "common") return "C";
|
|
7888
|
+
if (time.symbol === "cut") return "C|";
|
|
7889
|
+
return `${time.beats}/${time.beatType}`;
|
|
7890
|
+
}
|
|
7891
|
+
function computeUnitNoteLength(score) {
|
|
7892
|
+
const firstMeasure = score.parts[0]?.measures[0];
|
|
7893
|
+
const time = firstMeasure?.attributes?.time;
|
|
7894
|
+
if (time) {
|
|
7895
|
+
const beats = parseInt(time.beats, 10);
|
|
7896
|
+
const beatType = time.beatType;
|
|
7897
|
+
const ratio = beats / beatType;
|
|
7898
|
+
if (ratio < 0.75) {
|
|
7899
|
+
return { num: 1, den: 16 };
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7902
|
+
return { num: 1, den: 8 };
|
|
7903
|
+
}
|
|
7904
|
+
function durationToAbcFraction(duration, divisions, unitNote) {
|
|
7905
|
+
const abcNum = duration * unitNote.den;
|
|
7906
|
+
const abcDen = divisions * 4 * unitNote.num;
|
|
7907
|
+
const g = gcd(abcNum, abcDen);
|
|
7908
|
+
return { num: abcNum / g, den: abcDen / g };
|
|
7909
|
+
}
|
|
7910
|
+
function gcd(a, b) {
|
|
7911
|
+
a = Math.abs(a);
|
|
7912
|
+
b = Math.abs(b);
|
|
7913
|
+
while (b) {
|
|
7914
|
+
[a, b] = [b, a % b];
|
|
7915
|
+
}
|
|
7916
|
+
return a;
|
|
7917
|
+
}
|
|
7918
|
+
function formatAbcDuration(num, den) {
|
|
7919
|
+
if (num === 1 && den === 1) return "";
|
|
7920
|
+
if (den === 1) return String(num);
|
|
7921
|
+
if (num === 1) {
|
|
7922
|
+
if (den === 2 && !useExplicitHalf) return "/";
|
|
7923
|
+
return `/${den}`;
|
|
7924
|
+
}
|
|
7925
|
+
return `${num}/${den}`;
|
|
7926
|
+
}
|
|
7927
|
+
function serializePitch2(pitch, explicitNatural) {
|
|
7928
|
+
let result = "";
|
|
7929
|
+
if (explicitNatural) {
|
|
7930
|
+
result += "=";
|
|
7931
|
+
} else if (pitch.alter !== void 0 && pitch.alter !== 0) {
|
|
7932
|
+
if (pitch.alter === 1) result += "^";
|
|
7933
|
+
else if (pitch.alter === 2) result += "^^";
|
|
7934
|
+
else if (pitch.alter === -1) result += "_";
|
|
7935
|
+
else if (pitch.alter === -2) result += "__";
|
|
7936
|
+
}
|
|
7937
|
+
const step = pitch.step;
|
|
7938
|
+
const octave = pitch.octave;
|
|
7939
|
+
if (octave >= 5) {
|
|
7940
|
+
result += step.toLowerCase();
|
|
7941
|
+
for (let o = 6; o <= octave; o++) {
|
|
7942
|
+
result += "'";
|
|
7943
|
+
}
|
|
7944
|
+
} else {
|
|
7945
|
+
result += step;
|
|
7946
|
+
for (let o = 3; o >= octave; o--) {
|
|
7947
|
+
result += ",";
|
|
7948
|
+
}
|
|
7949
|
+
}
|
|
7950
|
+
return result;
|
|
7951
|
+
}
|
|
7952
|
+
function serializeHarmony2(harmony) {
|
|
7953
|
+
let result = harmony.root.rootStep;
|
|
7954
|
+
if (harmony.root.rootAlter === 1) result += "#";
|
|
7955
|
+
else if (harmony.root.rootAlter === -1) result += "b";
|
|
7956
|
+
switch (harmony.kind) {
|
|
7957
|
+
case "major":
|
|
7958
|
+
break;
|
|
7959
|
+
// no suffix
|
|
7960
|
+
case "minor":
|
|
7961
|
+
result += "m";
|
|
7962
|
+
break;
|
|
7963
|
+
case "dominant":
|
|
7964
|
+
result += "7";
|
|
7965
|
+
break;
|
|
7966
|
+
case "major-seventh":
|
|
7967
|
+
result += "maj7";
|
|
7968
|
+
break;
|
|
7969
|
+
case "minor-seventh":
|
|
7970
|
+
result += "m7";
|
|
7971
|
+
break;
|
|
7972
|
+
case "diminished":
|
|
7973
|
+
result += "dim";
|
|
7974
|
+
break;
|
|
7975
|
+
case "diminished-seventh":
|
|
7976
|
+
result += "dim7";
|
|
7977
|
+
break;
|
|
7978
|
+
case "augmented":
|
|
7979
|
+
result += "aug";
|
|
7980
|
+
break;
|
|
7981
|
+
case "augmented-seventh":
|
|
7982
|
+
result += "aug7";
|
|
7983
|
+
break;
|
|
7984
|
+
case "major-sixth":
|
|
7985
|
+
result += "6";
|
|
7986
|
+
break;
|
|
7987
|
+
case "minor-sixth":
|
|
7988
|
+
result += "m6";
|
|
7989
|
+
break;
|
|
7990
|
+
case "dominant-ninth":
|
|
7991
|
+
result += "9";
|
|
7992
|
+
break;
|
|
7993
|
+
case "minor-ninth":
|
|
7994
|
+
result += "m9";
|
|
7995
|
+
break;
|
|
7996
|
+
case "suspended-fourth":
|
|
7997
|
+
result += "sus4";
|
|
7998
|
+
break;
|
|
7999
|
+
case "suspended-second":
|
|
8000
|
+
result += "sus2";
|
|
8001
|
+
break;
|
|
8002
|
+
default:
|
|
8003
|
+
if (harmony.kindText) result += harmony.kindText;
|
|
8004
|
+
break;
|
|
8005
|
+
}
|
|
8006
|
+
if (harmony.bass) {
|
|
8007
|
+
result += "/" + harmony.bass.bassStep;
|
|
8008
|
+
if (harmony.bass.bassAlter === 1) result += "#";
|
|
8009
|
+
else if (harmony.bass.bassAlter === -1) result += "b";
|
|
8010
|
+
}
|
|
8011
|
+
return '"' + result + '"';
|
|
8012
|
+
}
|
|
8013
|
+
function serializeDynamics(direction) {
|
|
8014
|
+
for (const dt of direction.directionTypes) {
|
|
8015
|
+
if (dt.kind === "dynamics" && dt.value) {
|
|
8016
|
+
return `!${dt.value}!`;
|
|
8017
|
+
}
|
|
8018
|
+
}
|
|
8019
|
+
return null;
|
|
8020
|
+
}
|
|
8021
|
+
function serializeTempo(direction) {
|
|
8022
|
+
for (const dt of direction.directionTypes) {
|
|
8023
|
+
if (dt.kind === "metronome") {
|
|
8024
|
+
const beatUnit = dt.beatUnit;
|
|
8025
|
+
const perMinute = dt.perMinute;
|
|
8026
|
+
if (!perMinute) return null;
|
|
8027
|
+
const quarterLen = NOTE_TYPE_TO_QUARTER_LENGTH[beatUnit] ?? 1;
|
|
8028
|
+
const den = Math.round(4 / quarterLen);
|
|
8029
|
+
return `1/${den}=${perMinute}`;
|
|
8030
|
+
}
|
|
8031
|
+
}
|
|
8032
|
+
return null;
|
|
8033
|
+
}
|
|
8034
|
+
function serializeAbc(score, options) {
|
|
8035
|
+
const opts = {
|
|
8036
|
+
referenceNumber: options?.referenceNumber ?? 1,
|
|
8037
|
+
notesPerLine: options?.notesPerLine ?? 0,
|
|
8038
|
+
includeChordSymbols: options?.includeChordSymbols ?? true,
|
|
8039
|
+
includeDynamics: options?.includeDynamics ?? true,
|
|
8040
|
+
includeLyrics: options?.includeLyrics ?? true
|
|
8041
|
+
};
|
|
8042
|
+
useExplicitHalf = score.metadata.miscellaneous?.find((m) => m.name === "abc-explicit-half")?.value === "true";
|
|
8043
|
+
useIndividualChordDurations = score.metadata.miscellaneous?.some((m) => m.name === "abc-chord-individual-durations" && m.value === "true") ?? false;
|
|
8044
|
+
const storedUnitNote = score.metadata.miscellaneous?.find((m) => m.name === "abc-unit-note-length")?.value;
|
|
8045
|
+
let unitNote;
|
|
8046
|
+
if (storedUnitNote) {
|
|
8047
|
+
const match = storedUnitNote.match(/^(\d+)\/(\d+)$/);
|
|
8048
|
+
if (match) {
|
|
8049
|
+
unitNote = { num: parseInt(match[1], 10), den: parseInt(match[2], 10) };
|
|
8050
|
+
} else {
|
|
8051
|
+
unitNote = computeUnitNoteLength(score);
|
|
8052
|
+
}
|
|
8053
|
+
} else {
|
|
8054
|
+
unitNote = computeUnitNoteLength(score);
|
|
8055
|
+
}
|
|
8056
|
+
const lines = [];
|
|
8057
|
+
const firstPart = score.parts[0];
|
|
8058
|
+
const firstMeasure = firstPart?.measures[0];
|
|
8059
|
+
const attrs = firstMeasure?.attributes;
|
|
8060
|
+
const storedHeaderOrder = score.metadata.miscellaneous?.find((m) => m.name === "abc-header-order")?.value;
|
|
8061
|
+
if (storedHeaderOrder) {
|
|
8062
|
+
const headerOrder = JSON.parse(storedHeaderOrder);
|
|
8063
|
+
for (const field of headerOrder) {
|
|
8064
|
+
lines.push(field);
|
|
8065
|
+
}
|
|
8066
|
+
} else {
|
|
8067
|
+
const storedRefNum = score.metadata.miscellaneous?.find((m) => m.name === "abc-reference-number")?.value;
|
|
8068
|
+
const refNum = storedRefNum ? parseInt(storedRefNum, 10) : opts.referenceNumber;
|
|
8069
|
+
lines.push(`X:${refNum}`);
|
|
8070
|
+
if (score.metadata.movementTitle) {
|
|
8071
|
+
lines.push(`T:${score.metadata.movementTitle}`);
|
|
8072
|
+
}
|
|
8073
|
+
const composer = score.metadata.creators?.find((c) => c.type === "composer");
|
|
8074
|
+
if (composer) {
|
|
8075
|
+
lines.push(`C:${composer.value}`);
|
|
8076
|
+
}
|
|
8077
|
+
const rhythmFields = score.metadata.miscellaneous?.filter((m) => m.name === "abc-R");
|
|
8078
|
+
if (rhythmFields) {
|
|
8079
|
+
for (const rf of rhythmFields) lines.push(`R:${rf.value}`);
|
|
8080
|
+
}
|
|
8081
|
+
const originCreators = score.metadata.creators?.filter((c) => c.type === "origin");
|
|
8082
|
+
if (originCreators) {
|
|
8083
|
+
for (const oc of originCreators) lines.push(`O:${oc.value}`);
|
|
8084
|
+
}
|
|
8085
|
+
if (score.metadata.source) {
|
|
8086
|
+
lines.push(`S:${score.metadata.source}`);
|
|
8087
|
+
}
|
|
8088
|
+
const noteFields = score.metadata.miscellaneous?.filter((m) => m.name === "abc-N");
|
|
8089
|
+
if (noteFields) {
|
|
8090
|
+
for (const nf of noteFields) lines.push(`N:${nf.value}`);
|
|
8091
|
+
}
|
|
8092
|
+
if (score.metadata.encoding?.encoder) {
|
|
8093
|
+
for (const enc of score.metadata.encoding.encoder) lines.push(`Z:${enc}`);
|
|
8094
|
+
}
|
|
8095
|
+
const instrFields = score.metadata.miscellaneous?.filter((m) => m.name === "abc-I");
|
|
8096
|
+
if (instrFields) {
|
|
8097
|
+
for (const inf of instrFields) lines.push(`I:${inf.value}`);
|
|
8098
|
+
}
|
|
8099
|
+
const fileFields = score.metadata.miscellaneous?.filter((m) => m.name === "abc-F");
|
|
8100
|
+
if (fileFields) {
|
|
8101
|
+
for (const ff of fileFields) lines.push(`F:${ff.value}`);
|
|
8102
|
+
}
|
|
8103
|
+
if (attrs?.time) {
|
|
8104
|
+
lines.push(`M:${serializeTimeSignature(attrs.time)}`);
|
|
8105
|
+
}
|
|
8106
|
+
lines.push(`L:${unitNote.num}/${unitNote.den}`);
|
|
8107
|
+
const storedTempo = score.metadata.miscellaneous?.find((m) => m.name === "abc-tempo")?.value;
|
|
8108
|
+
if (storedTempo) {
|
|
8109
|
+
lines.push(`Q:${storedTempo}`);
|
|
8110
|
+
} else {
|
|
8111
|
+
const tempoStr = findTempoInMeasure(firstMeasure);
|
|
8112
|
+
if (tempoStr) {
|
|
8113
|
+
lines.push(`Q:${tempoStr}`);
|
|
8114
|
+
}
|
|
8115
|
+
}
|
|
8116
|
+
if (attrs?.key) {
|
|
8117
|
+
lines.push(`K:${serializeKey2(attrs.key)}`);
|
|
8118
|
+
} else {
|
|
8119
|
+
lines.push("K:C");
|
|
8120
|
+
}
|
|
8121
|
+
}
|
|
8122
|
+
const lineBreaksStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-line-breaks")?.value;
|
|
8123
|
+
const lineBreaks = lineBreaksStr ? JSON.parse(lineBreaksStr) : [];
|
|
8124
|
+
const storedVoiceIdsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-ids")?.value;
|
|
8125
|
+
const storedVoiceIds = storedVoiceIdsStr ? JSON.parse(storedVoiceIdsStr) : [];
|
|
8126
|
+
const lyricsAfterAll = score.metadata.miscellaneous?.find((m) => m.name === "abc-lyrics-after-all")?.value === "true";
|
|
8127
|
+
const lyricsLineCountsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-lyrics-line-counts")?.value;
|
|
8128
|
+
const lyricsLineCounts = lyricsLineCountsStr ? JSON.parse(lyricsLineCountsStr) : [];
|
|
8129
|
+
const inlineVoiceMarkersStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-inline-voice-markers")?.value;
|
|
8130
|
+
const inlineVoiceMarkers = inlineVoiceMarkersStr ? JSON.parse(inlineVoiceMarkersStr) : {};
|
|
8131
|
+
const useInlineVoiceMarkers = Object.keys(inlineVoiceMarkers).length > 0;
|
|
8132
|
+
if (useInlineVoiceMarkers) {
|
|
8133
|
+
const voiceDeclStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-declaration-lines")?.value;
|
|
8134
|
+
if (voiceDeclStr) {
|
|
8135
|
+
const voiceDeclLines = JSON.parse(voiceDeclStr);
|
|
8136
|
+
for (const vl of voiceDeclLines) {
|
|
8137
|
+
lines.push(vl);
|
|
8138
|
+
}
|
|
8139
|
+
}
|
|
8140
|
+
}
|
|
8141
|
+
const bodyCommentsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-body-comments")?.value;
|
|
8142
|
+
const bodyComments = bodyCommentsStr ? JSON.parse(bodyCommentsStr) : [];
|
|
8143
|
+
const bodyDirectivesStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-body-directives")?.value;
|
|
8144
|
+
const bodyDirectives = bodyDirectivesStr ? JSON.parse(bodyDirectivesStr) : [];
|
|
8145
|
+
const wFieldsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-w-fields")?.value;
|
|
8146
|
+
const wFieldsList = wFieldsStr ? JSON.parse(wFieldsStr) : [];
|
|
8147
|
+
const voiceInterleaveStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-interleave")?.value;
|
|
8148
|
+
const voiceInterleavePattern = voiceInterleaveStr ? JSON.parse(voiceInterleaveStr) : [];
|
|
8149
|
+
const groupBarCountsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-group-bar-counts")?.value;
|
|
8150
|
+
const groupBarCounts = groupBarCountsStr ? JSON.parse(groupBarCountsStr) : [];
|
|
8151
|
+
const bodyVoiceLinesStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-body-voice-lines")?.value;
|
|
8152
|
+
const bodyVoiceLines = bodyVoiceLinesStr ? JSON.parse(bodyVoiceLinesStr) : [];
|
|
8153
|
+
const voiceFullLinesStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-full-lines")?.value;
|
|
8154
|
+
const voiceFullLines = voiceFullLinesStr ? JSON.parse(voiceFullLinesStr) : {};
|
|
8155
|
+
const voiceCommentsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-comments")?.value;
|
|
8156
|
+
const voiceCommentsData = voiceCommentsStr ? JSON.parse(voiceCommentsStr) : {};
|
|
8157
|
+
const preVoiceCommentsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-pre-voice-comments")?.value;
|
|
8158
|
+
const preVoiceComments = preVoiceCommentsStr ? JSON.parse(preVoiceCommentsStr) : [];
|
|
8159
|
+
const multiVoice = score.parts.length > 1;
|
|
8160
|
+
const partMeasureStrings = [];
|
|
8161
|
+
const partDivisions = [];
|
|
8162
|
+
for (let partIdx = 0; partIdx < score.parts.length; partIdx++) {
|
|
8163
|
+
const part = score.parts[partIdx];
|
|
8164
|
+
const divisions = getPartDivisions(part);
|
|
8165
|
+
partDivisions.push(divisions);
|
|
8166
|
+
const measStrings = [];
|
|
8167
|
+
let currentUnitNote = { ...unitNote };
|
|
8168
|
+
let currentKey = part.measures[0]?.attributes?.key;
|
|
8169
|
+
for (let mi = 0; mi < part.measures.length; mi++) {
|
|
8170
|
+
const measure = part.measures[mi];
|
|
8171
|
+
const measDivisions = measure.attributes?.divisions ?? divisions;
|
|
8172
|
+
let measureStr = "";
|
|
8173
|
+
if (mi > 0 && measure.attributes?.key) {
|
|
8174
|
+
const newKey = measure.attributes.key;
|
|
8175
|
+
if (currentKey === void 0 || newKey.fifths !== currentKey.fifths || (newKey.mode || "major") !== (currentKey.mode || "major")) {
|
|
8176
|
+
measureStr += "\nK:" + serializeKey2(newKey) + "\n";
|
|
8177
|
+
currentKey = newKey;
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
8180
|
+
const leftBarline = measure.barlines?.find((b) => b.location === "left");
|
|
8181
|
+
if (leftBarline) {
|
|
8182
|
+
measureStr += serializeBarline2(leftBarline);
|
|
8183
|
+
}
|
|
8184
|
+
const { noteStr, updatedUnitNote } = serializeMeasureEntries(measure, measDivisions, currentUnitNote, opts);
|
|
8185
|
+
if (updatedUnitNote) currentUnitNote = updatedUnitNote;
|
|
8186
|
+
measureStr += noteStr;
|
|
8187
|
+
const rightBarline = measure.barlines?.find((b) => b.location === "right");
|
|
8188
|
+
if (rightBarline) {
|
|
8189
|
+
measureStr += serializeBarline2(rightBarline);
|
|
8190
|
+
} else if (mi < part.measures.length - 1) {
|
|
8191
|
+
const nextMeasure = part.measures[mi + 1];
|
|
8192
|
+
const nextLeftBarline = nextMeasure?.barlines?.find((b) => b.location === "left" && (b.barStyle || b.repeat));
|
|
8193
|
+
if (!nextLeftBarline) {
|
|
8194
|
+
measureStr += "|";
|
|
8195
|
+
}
|
|
8196
|
+
} else {
|
|
8197
|
+
const prevMeas = mi > 0 ? part.measures[mi - 1] : null;
|
|
8198
|
+
const prevRightBl = prevMeas?.barlines?.find((b) => b.location === "right");
|
|
8199
|
+
const prevWasFinal = prevRightBl?.barStyle === "light-heavy" && !prevRightBl.repeat;
|
|
8200
|
+
if (!prevWasFinal) {
|
|
8201
|
+
measureStr += "|";
|
|
8202
|
+
}
|
|
8203
|
+
}
|
|
8204
|
+
measStrings.push(measureStr);
|
|
8205
|
+
}
|
|
8206
|
+
partMeasureStrings.push(measStrings);
|
|
8207
|
+
}
|
|
8208
|
+
const hasInterleave = voiceInterleavePattern.length > 0 && multiVoice;
|
|
8209
|
+
if (hasInterleave && !useInlineVoiceMarkers) {
|
|
8210
|
+
const voiceMeasureCursor = {};
|
|
8211
|
+
for (const voiceId of storedVoiceIds) {
|
|
8212
|
+
voiceMeasureCursor[voiceId] = 0;
|
|
8213
|
+
}
|
|
8214
|
+
const voiceIdToPartIdx = {};
|
|
8215
|
+
for (let i = 0; i < storedVoiceIds.length; i++) {
|
|
8216
|
+
voiceIdToPartIdx[storedVoiceIds[i]] = i;
|
|
8217
|
+
}
|
|
8218
|
+
let voiceDeclIdx = 0;
|
|
8219
|
+
let commentIdx = 0;
|
|
8220
|
+
for (let gi = 0; gi < voiceInterleavePattern.length; gi++) {
|
|
8221
|
+
const group = voiceInterleavePattern[gi];
|
|
8222
|
+
for (const voiceId of group) {
|
|
8223
|
+
const partIdx = voiceIdToPartIdx[voiceId];
|
|
8224
|
+
if (partIdx === void 0) continue;
|
|
8225
|
+
const measStrings = partMeasureStrings[partIdx];
|
|
8226
|
+
const bodyVoiceLineIdx = bodyVoiceLines.findIndex((l, idx) => {
|
|
8227
|
+
const m = l.match(/^V:\s*(\S+)/);
|
|
8228
|
+
return m && m[1] === voiceId && idx >= voiceDeclIdx;
|
|
8229
|
+
});
|
|
8230
|
+
if (bodyVoiceLineIdx >= 0) {
|
|
8231
|
+
if (bodyVoiceLineIdx < preVoiceComments.length) {
|
|
8232
|
+
for (const c of preVoiceComments[bodyVoiceLineIdx]) {
|
|
8233
|
+
lines.push(c);
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
lines.push(bodyVoiceLines[bodyVoiceLineIdx]);
|
|
8237
|
+
voiceDeclIdx = bodyVoiceLineIdx + 1;
|
|
8238
|
+
} else if (voiceFullLines[voiceId]) {
|
|
8239
|
+
} else {
|
|
8240
|
+
let voiceLine = `V:${voiceId}`;
|
|
8241
|
+
const partClef = score.parts[partIdx]?.measures[0]?.attributes?.clef?.[0];
|
|
8242
|
+
if (partClef) {
|
|
8243
|
+
const clefName = musicXmlClefToAbc(partClef);
|
|
8244
|
+
if (clefName && clefName !== "treble") {
|
|
8245
|
+
voiceLine += ` clef=${clefName}`;
|
|
8246
|
+
}
|
|
8247
|
+
}
|
|
8248
|
+
lines.push(voiceLine);
|
|
8249
|
+
}
|
|
8250
|
+
const voiceIdxInGroup = group.indexOf(voiceId);
|
|
8251
|
+
const barCount = groupBarCounts[gi]?.[voiceIdxInGroup] ?? 0;
|
|
8252
|
+
const measuresInGroup = barCount > 0 ? barCount : Math.ceil((partMeasureStrings[0]?.length || 0) / voiceInterleavePattern.length);
|
|
8253
|
+
const startMeasure = voiceMeasureCursor[voiceId] || 0;
|
|
8254
|
+
const endMeasure = Math.min(startMeasure + measuresInGroup, measStrings.length);
|
|
8255
|
+
const voiceLineBreaksStr = score.metadata.miscellaneous?.find((m) => m.name === `abc-line-breaks-${partIdx}`)?.value;
|
|
8256
|
+
const voiceLineBreaks = voiceLineBreaksStr ? JSON.parse(voiceLineBreaksStr) : lineBreaks;
|
|
8257
|
+
const voiceLineBreakSet = new Set(voiceLineBreaks.map((v) => Math.abs(v)));
|
|
8258
|
+
const voiceLineContinuationSet = new Set(voiceLineBreaks.filter((v) => v < 0).map((v) => Math.abs(v)));
|
|
8259
|
+
const thisVoiceComments = voiceCommentsData[voiceId] || [];
|
|
8260
|
+
let groupMusic = "";
|
|
8261
|
+
for (let mi = startMeasure; mi < endMeasure; mi++) {
|
|
8262
|
+
for (const vc of thisVoiceComments) {
|
|
8263
|
+
if (vc.barIndex === mi) {
|
|
8264
|
+
groupMusic += vc.comment + "\n";
|
|
8265
|
+
}
|
|
8266
|
+
}
|
|
8267
|
+
groupMusic += measStrings[mi];
|
|
8268
|
+
if (mi < endMeasure - 1) {
|
|
8269
|
+
const absMeasureNum = mi + 1;
|
|
8270
|
+
if (voiceLineContinuationSet.has(absMeasureNum)) {
|
|
8271
|
+
groupMusic += "\\\n";
|
|
8272
|
+
} else if (voiceLineBreakSet.has(absMeasureNum)) {
|
|
8273
|
+
groupMusic += "\n";
|
|
8274
|
+
}
|
|
8275
|
+
}
|
|
8276
|
+
}
|
|
8277
|
+
voiceMeasureCursor[voiceId] = endMeasure;
|
|
8278
|
+
lines.push(groupMusic);
|
|
8279
|
+
}
|
|
8280
|
+
if (gi < voiceInterleavePattern.length - 1 && commentIdx < bodyComments.length) {
|
|
8281
|
+
lines.push(bodyComments[commentIdx]);
|
|
8282
|
+
commentIdx++;
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8285
|
+
} else {
|
|
8286
|
+
for (let partIdx = 0; partIdx < score.parts.length; partIdx++) {
|
|
8287
|
+
const part = score.parts[partIdx];
|
|
8288
|
+
if (multiVoice && !useInlineVoiceMarkers) {
|
|
8289
|
+
const voiceId = storedVoiceIds[partIdx] || String(partIdx + 1);
|
|
8290
|
+
if (voiceFullLines[voiceId]) {
|
|
8291
|
+
lines.push(voiceFullLines[voiceId]);
|
|
8292
|
+
} else {
|
|
8293
|
+
let voiceLine = `V:${voiceId}`;
|
|
8294
|
+
const partClef = part.measures[0]?.attributes?.clef?.[0];
|
|
8295
|
+
if (partClef) {
|
|
8296
|
+
const clefName = musicXmlClefToAbc(partClef);
|
|
8297
|
+
if (clefName && clefName !== "treble") {
|
|
8298
|
+
voiceLine += ` clef=${clefName}`;
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
8301
|
+
lines.push(voiceLine);
|
|
8302
|
+
}
|
|
8303
|
+
}
|
|
8304
|
+
const divisions = partDivisions[partIdx];
|
|
8305
|
+
const bodyResult = serializePartBody(part, divisions, unitNote, opts, lineBreaks, lyricsAfterAll, lyricsLineCounts);
|
|
8306
|
+
let musicLine = bodyResult.music;
|
|
8307
|
+
if (multiVoice && useInlineVoiceMarkers) {
|
|
8308
|
+
const voiceId = storedVoiceIds[partIdx] || String(partIdx + 1);
|
|
8309
|
+
const marker = inlineVoiceMarkers[voiceId] || `[V:${voiceId}]`;
|
|
8310
|
+
musicLine = marker + musicLine;
|
|
8311
|
+
}
|
|
8312
|
+
lines.push(musicLine);
|
|
8313
|
+
if (opts.includeLyrics && bodyResult.lyrics) {
|
|
8314
|
+
lines.push(bodyResult.lyrics);
|
|
8315
|
+
}
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
for (const directive of bodyDirectives) {
|
|
8319
|
+
lines.push(directive);
|
|
8320
|
+
}
|
|
8321
|
+
for (const wField of wFieldsList) {
|
|
8322
|
+
lines.push(wField);
|
|
8323
|
+
}
|
|
8324
|
+
const trailingCommentsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-trailing-comments")?.value;
|
|
8325
|
+
const trailingComments = trailingCommentsStr ? JSON.parse(trailingCommentsStr) : [];
|
|
8326
|
+
for (const comment of trailingComments) {
|
|
8327
|
+
lines.push(comment);
|
|
8328
|
+
}
|
|
8329
|
+
return lines.join("\n") + "\n";
|
|
8330
|
+
}
|
|
8331
|
+
function findTempoInMeasure(measure) {
|
|
8332
|
+
if (!measure) return null;
|
|
8333
|
+
for (const entry of measure.entries) {
|
|
8334
|
+
if (entry.type === "direction") {
|
|
8335
|
+
const tempo = serializeTempo(entry);
|
|
8336
|
+
if (tempo) return tempo;
|
|
8337
|
+
}
|
|
8338
|
+
}
|
|
8339
|
+
return null;
|
|
8340
|
+
}
|
|
8341
|
+
function getPartDivisions(part) {
|
|
8342
|
+
for (const measure of part.measures) {
|
|
8343
|
+
if (measure.attributes?.divisions) {
|
|
8344
|
+
return measure.attributes.divisions;
|
|
8345
|
+
}
|
|
8346
|
+
for (const entry of measure.entries) {
|
|
8347
|
+
if (entry.type === "attributes" && entry.attributes.divisions) {
|
|
8348
|
+
return entry.attributes.divisions;
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
8351
|
+
}
|
|
8352
|
+
return DIVISIONS_PER_QUARTER;
|
|
8353
|
+
}
|
|
8354
|
+
function serializePartBody(part, divisions, initialUnitNote, opts, lineBreaks = [], lyricsAfterAll = false, lyricsLineCounts = []) {
|
|
8355
|
+
let unitNote = { ...initialUnitNote };
|
|
8356
|
+
const musicParts = [];
|
|
8357
|
+
const allLyrics = /* @__PURE__ */ new Map();
|
|
8358
|
+
let currentKey = part.measures[0]?.attributes?.key;
|
|
8359
|
+
for (let mi = 0; mi < part.measures.length; mi++) {
|
|
8360
|
+
const measure = part.measures[mi];
|
|
8361
|
+
const measDivisions = measure.attributes?.divisions ?? divisions;
|
|
8362
|
+
if (mi > 0 && measure.attributes?.key) {
|
|
8363
|
+
const newKey = measure.attributes.key;
|
|
8364
|
+
if (currentKey === void 0 || newKey.fifths !== currentKey.fifths || (newKey.mode || "major") !== (currentKey.mode || "major")) {
|
|
8365
|
+
musicParts.push("\nK:" + serializeKey2(newKey) + "\n");
|
|
8366
|
+
currentKey = newKey;
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8369
|
+
const leftBarline = measure.barlines?.find((b) => b.location === "left");
|
|
8370
|
+
if (leftBarline) {
|
|
8371
|
+
musicParts.push(serializeBarline2(leftBarline));
|
|
8372
|
+
}
|
|
8373
|
+
const { noteStr, lyrics, updatedUnitNote } = serializeMeasureEntries(
|
|
8374
|
+
measure,
|
|
8375
|
+
measDivisions,
|
|
8376
|
+
unitNote,
|
|
8377
|
+
opts
|
|
8378
|
+
);
|
|
8379
|
+
if (updatedUnitNote) {
|
|
8380
|
+
unitNote = updatedUnitNote;
|
|
8381
|
+
}
|
|
8382
|
+
musicParts.push(noteStr);
|
|
8383
|
+
if (lyrics.length > 0) {
|
|
8384
|
+
allLyrics.set(mi, lyrics);
|
|
8385
|
+
}
|
|
8386
|
+
const rightBarline = measure.barlines?.find((b) => b.location === "right");
|
|
8387
|
+
if (rightBarline) {
|
|
8388
|
+
musicParts.push(serializeBarline2(rightBarline));
|
|
8389
|
+
} else if (mi < part.measures.length - 1) {
|
|
8390
|
+
const nextMeas = part.measures[mi + 1];
|
|
8391
|
+
const nextLeftBar = nextMeas?.barlines?.find((b) => b.location === "left" && (b.barStyle || b.repeat));
|
|
8392
|
+
if (!nextLeftBar) {
|
|
8393
|
+
musicParts.push("|");
|
|
8394
|
+
}
|
|
8395
|
+
} else {
|
|
8396
|
+
const prevMeas = mi > 0 ? part.measures[mi - 1] : null;
|
|
8397
|
+
const prevRightBl = prevMeas?.barlines?.find((b) => b.location === "right");
|
|
8398
|
+
const prevWasFinal = prevRightBl?.barStyle === "light-heavy" && !prevRightBl.repeat;
|
|
8399
|
+
if (!prevWasFinal) {
|
|
8400
|
+
musicParts.push("|");
|
|
8401
|
+
}
|
|
8402
|
+
}
|
|
8403
|
+
if (mi < part.measures.length - 1) {
|
|
8404
|
+
if (lineBreaks.includes(mi + 1)) {
|
|
8405
|
+
musicParts.push("\n");
|
|
8406
|
+
} else if (lineBreaks.includes(-(mi + 1))) {
|
|
8407
|
+
musicParts.push("\\\n");
|
|
8408
|
+
}
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8411
|
+
const musicStr = musicParts.join("");
|
|
8412
|
+
if (allLyrics.size === 0) {
|
|
8413
|
+
return { music: musicStr, lyrics: null };
|
|
8414
|
+
}
|
|
8415
|
+
const lineBreakSet = new Set(lineBreaks.map((v) => Math.abs(v)));
|
|
8416
|
+
const lyricsByLine = [];
|
|
8417
|
+
let lineStart = 0;
|
|
8418
|
+
for (let mi = 0; mi < part.measures.length; mi++) {
|
|
8419
|
+
if (lineBreakSet.has(mi + 1) || mi === part.measures.length - 1) {
|
|
8420
|
+
const syllables = [];
|
|
8421
|
+
for (let m = lineStart; m <= mi; m++) {
|
|
8422
|
+
const ls = allLyrics.get(m);
|
|
8423
|
+
if (ls) syllables.push(...ls);
|
|
8424
|
+
}
|
|
8425
|
+
if (syllables.length > 0) {
|
|
8426
|
+
lyricsByLine.push({ startMeasure: lineStart, endMeasure: mi, syllables });
|
|
8427
|
+
}
|
|
8428
|
+
lineStart = mi + 1;
|
|
8429
|
+
}
|
|
8430
|
+
}
|
|
8431
|
+
function formatLyrics(syllables) {
|
|
8432
|
+
let result = "w:";
|
|
8433
|
+
for (let i = 0; i < syllables.length; i++) {
|
|
8434
|
+
const syllable = syllables[i];
|
|
8435
|
+
if (i > 0) {
|
|
8436
|
+
const prev = syllables[i - 1];
|
|
8437
|
+
if (prev.endsWith("-")) {
|
|
8438
|
+
} else {
|
|
8439
|
+
result += " ";
|
|
8440
|
+
}
|
|
8441
|
+
}
|
|
8442
|
+
result += syllable;
|
|
8443
|
+
}
|
|
8444
|
+
return result;
|
|
8445
|
+
}
|
|
8446
|
+
if (lyricsAfterAll) {
|
|
8447
|
+
const allSyllables2 = [];
|
|
8448
|
+
const sortedMeasures2 = Array.from(allLyrics.keys()).sort((a, b) => a - b);
|
|
8449
|
+
for (const mi of sortedMeasures2) {
|
|
8450
|
+
allSyllables2.push(...allLyrics.get(mi));
|
|
8451
|
+
}
|
|
8452
|
+
if (lyricsLineCounts.length > 0) {
|
|
8453
|
+
const lyricsLines = [];
|
|
8454
|
+
let offset = 0;
|
|
8455
|
+
for (const count of lyricsLineCounts) {
|
|
8456
|
+
const chunk = allSyllables2.slice(offset, offset + count);
|
|
8457
|
+
if (chunk.length > 0) {
|
|
8458
|
+
lyricsLines.push(formatLyrics(chunk));
|
|
8459
|
+
}
|
|
8460
|
+
offset += count;
|
|
8461
|
+
}
|
|
8462
|
+
if (offset < allSyllables2.length) {
|
|
8463
|
+
lyricsLines.push(formatLyrics(allSyllables2.slice(offset)));
|
|
8464
|
+
}
|
|
8465
|
+
return { music: musicStr, lyrics: lyricsLines.join("\n") };
|
|
8466
|
+
}
|
|
8467
|
+
return { music: musicStr, lyrics: formatLyrics(allSyllables2) };
|
|
8468
|
+
}
|
|
8469
|
+
if (lyricsByLine.length > 0 && lineBreaks.length > 0) {
|
|
8470
|
+
const musicLines = musicStr.split("\n");
|
|
8471
|
+
const resultLines = [];
|
|
8472
|
+
let lyricIdx = 0;
|
|
8473
|
+
for (let li = 0; li < musicLines.length; li++) {
|
|
8474
|
+
resultLines.push(musicLines[li]);
|
|
8475
|
+
if (lyricIdx < lyricsByLine.length) {
|
|
8476
|
+
const lyricGroup = lyricsByLine[lyricIdx];
|
|
8477
|
+
resultLines.push(formatLyrics(lyricGroup.syllables));
|
|
8478
|
+
lyricIdx++;
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
while (lyricIdx < lyricsByLine.length) {
|
|
8482
|
+
resultLines.push(formatLyrics(lyricsByLine[lyricIdx].syllables));
|
|
8483
|
+
lyricIdx++;
|
|
8484
|
+
}
|
|
8485
|
+
return { music: resultLines.join("\n"), lyrics: null };
|
|
8486
|
+
}
|
|
8487
|
+
const allSyllables = [];
|
|
8488
|
+
const sortedMeasures = Array.from(allLyrics.keys()).sort((a, b) => a - b);
|
|
8489
|
+
for (const mi of sortedMeasures) {
|
|
8490
|
+
allSyllables.push(...allLyrics.get(mi));
|
|
8491
|
+
}
|
|
8492
|
+
return { music: musicStr, lyrics: formatLyrics(allSyllables) };
|
|
8493
|
+
}
|
|
8494
|
+
function serializeMeasureEntries(measure, divisions, unitNote, opts) {
|
|
8495
|
+
const parts = [];
|
|
8496
|
+
const lyrics = [];
|
|
8497
|
+
let currentUnitNote = { ...unitNote };
|
|
8498
|
+
let chordPitches = [];
|
|
8499
|
+
let chordDurationStr = "";
|
|
8500
|
+
let chordTieStr = "";
|
|
8501
|
+
let chordSlurStart = "";
|
|
8502
|
+
let chordSlurEnd = "";
|
|
8503
|
+
let inChord = false;
|
|
8504
|
+
let chordHasIndividualDurations = false;
|
|
8505
|
+
let tupletRemaining = 0;
|
|
8506
|
+
for (let ei = 0; ei < measure.entries.length; ei++) {
|
|
8507
|
+
const entry = measure.entries[ei];
|
|
8508
|
+
switch (entry.type) {
|
|
8509
|
+
case "note": {
|
|
8510
|
+
const note = entry;
|
|
8511
|
+
if (note.grace) {
|
|
8512
|
+
const graceNotes = [];
|
|
8513
|
+
let gi = ei;
|
|
8514
|
+
while (gi < measure.entries.length) {
|
|
8515
|
+
const ge = measure.entries[gi];
|
|
8516
|
+
if (ge.type === "note" && ge.grace) {
|
|
8517
|
+
const gs = serializeNote2(ge, divisions, currentUnitNote, false);
|
|
8518
|
+
graceNotes.push(gs.pitch);
|
|
8519
|
+
gi++;
|
|
8520
|
+
} else {
|
|
8521
|
+
break;
|
|
8522
|
+
}
|
|
8523
|
+
}
|
|
8524
|
+
parts.push("{" + graceNotes.join("") + "}");
|
|
8525
|
+
ei = gi - 1;
|
|
8526
|
+
break;
|
|
8527
|
+
}
|
|
8528
|
+
const serialized = serializeNote2(note, divisions, currentUnitNote, false);
|
|
8529
|
+
if (note.chord) {
|
|
8530
|
+
if (!inChord) {
|
|
8531
|
+
inChord = true;
|
|
8532
|
+
}
|
|
8533
|
+
let chordNoteStr = serialized.pitch;
|
|
8534
|
+
if (chordHasIndividualDurations) {
|
|
8535
|
+
chordNoteStr += serialized.duration;
|
|
8536
|
+
}
|
|
8537
|
+
if (note.tie?.type === "start" || note.ties?.some((t) => t.type === "start")) {
|
|
8538
|
+
chordNoteStr += "-";
|
|
8539
|
+
}
|
|
8540
|
+
chordPitches.push(chordNoteStr);
|
|
8541
|
+
break;
|
|
8542
|
+
}
|
|
8543
|
+
if (inChord) {
|
|
8544
|
+
parts.push("[" + chordPitches.join("") + "]" + chordDurationStr + chordTieStr + chordSlurEnd);
|
|
8545
|
+
inChord = false;
|
|
8546
|
+
chordHasIndividualDurations = false;
|
|
8547
|
+
chordPitches = [];
|
|
8548
|
+
chordDurationStr = "";
|
|
8549
|
+
chordTieStr = "";
|
|
8550
|
+
chordSlurStart = "";
|
|
8551
|
+
chordSlurEnd = "";
|
|
8552
|
+
}
|
|
8553
|
+
if (note.lyrics && note.lyrics.length > 0 && opts.includeLyrics) {
|
|
8554
|
+
const lyric = note.lyrics[0];
|
|
8555
|
+
if (lyric.text) {
|
|
8556
|
+
const syllabic = lyric.syllabic || "single";
|
|
8557
|
+
const suffix = syllabic === "begin" || syllabic === "middle" ? "-" : "";
|
|
8558
|
+
lyrics.push(lyric.text + suffix);
|
|
8559
|
+
}
|
|
8560
|
+
}
|
|
8561
|
+
let tupletPrefix = "";
|
|
8562
|
+
if (note.timeModification && !note.chord && !note.grace && tupletRemaining <= 0) {
|
|
8563
|
+
const p = note.timeModification.actualNotes;
|
|
8564
|
+
tupletRemaining = p;
|
|
8565
|
+
tupletPrefix = `(${p}`;
|
|
8566
|
+
}
|
|
8567
|
+
if (note.timeModification && !note.chord && !note.grace) {
|
|
8568
|
+
tupletRemaining--;
|
|
8569
|
+
}
|
|
8570
|
+
let effectiveSerialized = serialized;
|
|
8571
|
+
if (note.timeModification && note.pitch) {
|
|
8572
|
+
const baseDuration = Math.round(note.duration * note.timeModification.actualNotes / note.timeModification.normalNotes);
|
|
8573
|
+
const { num, den } = durationToAbcFraction(baseDuration, divisions, currentUnitNote);
|
|
8574
|
+
const baseDurationStr = formatAbcDuration(num, den);
|
|
8575
|
+
const pitchStr = serializePitch2(note.pitch, note.accidental?.value === "natural");
|
|
8576
|
+
let tieStr = "";
|
|
8577
|
+
if (note.tie?.type === "start" || note.ties?.some((t) => t.type === "start")) {
|
|
8578
|
+
tieStr = "-";
|
|
8579
|
+
}
|
|
8580
|
+
let slurStart = "";
|
|
8581
|
+
let slurEnd = "";
|
|
8582
|
+
if (note.notations) {
|
|
8583
|
+
for (const notation of note.notations) {
|
|
8584
|
+
if (notation.type === "slur") {
|
|
8585
|
+
if (notation.slurType === "start") slurStart += "(";
|
|
8586
|
+
if (notation.slurType === "stop") slurEnd += ")";
|
|
8587
|
+
}
|
|
8588
|
+
}
|
|
8589
|
+
}
|
|
8590
|
+
effectiveSerialized = {
|
|
8591
|
+
full: slurStart + pitchStr + baseDurationStr + tieStr + slurEnd,
|
|
8592
|
+
pitch: pitchStr,
|
|
8593
|
+
duration: baseDurationStr,
|
|
8594
|
+
slurStart,
|
|
8595
|
+
slurEnd,
|
|
8596
|
+
tieStr
|
|
8597
|
+
};
|
|
8598
|
+
}
|
|
8599
|
+
const nextEntry = ei + 1 < measure.entries.length ? measure.entries[ei + 1] : null;
|
|
8600
|
+
if (nextEntry && nextEntry.type === "note" && nextEntry.chord) {
|
|
8601
|
+
inChord = true;
|
|
8602
|
+
const hasIndividualDur = detectChordIndividualDurations(measure.entries, ei);
|
|
8603
|
+
chordHasIndividualDurations = hasIndividualDur;
|
|
8604
|
+
const hasPerNoteTies = detectChordPerNoteTies(measure.entries, ei);
|
|
8605
|
+
const firstNoteTie = note.tie?.type === "start" || note.ties?.some((t) => t.type === "start");
|
|
8606
|
+
let firstNoteStr = hasIndividualDur ? effectiveSerialized.pitch + effectiveSerialized.duration : effectiveSerialized.pitch;
|
|
8607
|
+
if (hasPerNoteTies && firstNoteTie) {
|
|
8608
|
+
firstNoteStr += "-";
|
|
8609
|
+
}
|
|
8610
|
+
chordPitches = [firstNoteStr];
|
|
8611
|
+
chordDurationStr = hasIndividualDur ? "" : effectiveSerialized.duration;
|
|
8612
|
+
chordTieStr = "";
|
|
8613
|
+
chordSlurStart = "";
|
|
8614
|
+
chordSlurEnd = "";
|
|
8615
|
+
if (firstNoteTie && !hasPerNoteTies) {
|
|
8616
|
+
chordTieStr = "-";
|
|
8617
|
+
}
|
|
8618
|
+
if (note.notations) {
|
|
8619
|
+
for (const notation of note.notations) {
|
|
8620
|
+
if (notation.type === "slur") {
|
|
8621
|
+
if (notation.slurType === "start") chordSlurStart = "(";
|
|
8622
|
+
if (notation.slurType === "stop") chordSlurEnd = ")";
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
if (chordSlurStart) {
|
|
8627
|
+
let insertIdx = parts.length;
|
|
8628
|
+
for (let pi = parts.length - 1; pi >= 0; pi--) {
|
|
8629
|
+
const p = parts[pi];
|
|
8630
|
+
if (/^[vuTM]$/.test(p) || /^![^!]+!$/.test(p) || /^"[^"]*"$/.test(p)) {
|
|
8631
|
+
insertIdx = pi;
|
|
8632
|
+
} else {
|
|
8633
|
+
break;
|
|
8634
|
+
}
|
|
8635
|
+
}
|
|
8636
|
+
if (insertIdx < parts.length) {
|
|
8637
|
+
parts.splice(insertIdx, 0, chordSlurStart);
|
|
8638
|
+
parts.push(tupletPrefix);
|
|
8639
|
+
break;
|
|
8640
|
+
}
|
|
8641
|
+
}
|
|
8642
|
+
parts.push(tupletPrefix + chordSlurStart);
|
|
8643
|
+
break;
|
|
8644
|
+
}
|
|
8645
|
+
if (effectiveSerialized.slurStart) {
|
|
8646
|
+
let insertIdx = parts.length;
|
|
8647
|
+
for (let pi = parts.length - 1; pi >= 0; pi--) {
|
|
8648
|
+
const p = parts[pi];
|
|
8649
|
+
if (/^[vuTM]$/.test(p) || /^![^!]+!$/.test(p) || /^"[^"]*"$/.test(p)) {
|
|
8650
|
+
insertIdx = pi;
|
|
8651
|
+
} else {
|
|
8652
|
+
break;
|
|
8653
|
+
}
|
|
8654
|
+
}
|
|
8655
|
+
if (insertIdx < parts.length) {
|
|
8656
|
+
parts.splice(insertIdx, 0, effectiveSerialized.slurStart);
|
|
8657
|
+
const noteOnly = effectiveSerialized.pitch + effectiveSerialized.duration + effectiveSerialized.tieStr + effectiveSerialized.slurEnd;
|
|
8658
|
+
parts.push(tupletPrefix + noteOnly);
|
|
8659
|
+
break;
|
|
8660
|
+
}
|
|
8661
|
+
}
|
|
8662
|
+
const brokenResult = detectBrokenRhythm(measure.entries, ei, note, divisions, currentUnitNote);
|
|
8663
|
+
if (brokenResult && !note.chord && !note.grace && !note.timeModification) {
|
|
8664
|
+
const baseDurStr1 = formatAbcDuration(brokenResult.baseFrac.num, brokenResult.baseFrac.den);
|
|
8665
|
+
const pitchStr1 = effectiveSerialized.pitch;
|
|
8666
|
+
const tie1 = effectiveSerialized.tieStr;
|
|
8667
|
+
const slurS1 = effectiveSerialized.slurStart;
|
|
8668
|
+
const slurE1 = effectiveSerialized.slurEnd;
|
|
8669
|
+
parts.push(tupletPrefix + slurS1 + pitchStr1 + baseDurStr1 + tie1 + brokenResult.marker);
|
|
8670
|
+
const note2 = brokenResult.nextNote;
|
|
8671
|
+
const pitchStr2 = serializePitch2(note2.pitch, note2.accidental?.value === "natural");
|
|
8672
|
+
let tieStr2 = "";
|
|
8673
|
+
if (note2.tie?.type === "start" || note2.ties?.some((t) => t.type === "start")) tieStr2 = "-";
|
|
8674
|
+
let slurEnd2 = "";
|
|
8675
|
+
if (note2.notations) {
|
|
8676
|
+
for (const notation of note2.notations) {
|
|
8677
|
+
if (notation.type === "slur" && notation.slurType === "stop") slurEnd2 += ")";
|
|
8678
|
+
}
|
|
8679
|
+
}
|
|
8680
|
+
parts.push(pitchStr2 + baseDurStr1 + tieStr2 + slurEnd2 + slurE1);
|
|
8681
|
+
ei = brokenResult.nextIndex;
|
|
8682
|
+
break;
|
|
8683
|
+
}
|
|
8684
|
+
parts.push(tupletPrefix + effectiveSerialized.full);
|
|
8685
|
+
break;
|
|
8686
|
+
}
|
|
8687
|
+
case "harmony": {
|
|
8688
|
+
if (opts.includeChordSymbols) {
|
|
8689
|
+
parts.push(serializeHarmony2(entry));
|
|
8690
|
+
}
|
|
8691
|
+
break;
|
|
8692
|
+
}
|
|
8693
|
+
case "direction": {
|
|
8694
|
+
let handledAsSpecial = false;
|
|
8695
|
+
for (const dt of entry.directionTypes) {
|
|
8696
|
+
if (dt.kind === "words") {
|
|
8697
|
+
if (dt.text === "__abc_line_cont__") {
|
|
8698
|
+
parts.push("\\\n");
|
|
8699
|
+
handledAsSpecial = true;
|
|
8700
|
+
break;
|
|
8701
|
+
}
|
|
8702
|
+
if (dt.text === "__abc_line_break__") {
|
|
8703
|
+
parts.push("\n");
|
|
8704
|
+
handledAsSpecial = true;
|
|
8705
|
+
break;
|
|
8706
|
+
}
|
|
8707
|
+
const lMatch = dt.text.match(/^\[L:\s*(\d+)\/(\d+)\]$/);
|
|
8708
|
+
if (lMatch) {
|
|
8709
|
+
parts.push(dt.text);
|
|
8710
|
+
currentUnitNote = { num: parseInt(lMatch[1], 10), den: parseInt(lMatch[2], 10) };
|
|
8711
|
+
handledAsSpecial = true;
|
|
8712
|
+
break;
|
|
8713
|
+
}
|
|
8714
|
+
const decoMatch = dt.text.match(/^!([^!]+)!$/);
|
|
8715
|
+
if (decoMatch) {
|
|
8716
|
+
parts.push(dt.text);
|
|
8717
|
+
handledAsSpecial = true;
|
|
8718
|
+
break;
|
|
8719
|
+
}
|
|
8720
|
+
if (dt.text.length === 1 && /^[vuTM]$/.test(dt.text)) {
|
|
8721
|
+
parts.push(dt.text);
|
|
8722
|
+
handledAsSpecial = true;
|
|
8723
|
+
break;
|
|
8724
|
+
}
|
|
8725
|
+
}
|
|
8726
|
+
}
|
|
8727
|
+
if (handledAsSpecial) break;
|
|
8728
|
+
if (opts.includeDynamics) {
|
|
8729
|
+
const dynStr = serializeDynamics(entry);
|
|
8730
|
+
if (dynStr) {
|
|
8731
|
+
parts.push(dynStr);
|
|
8732
|
+
}
|
|
8733
|
+
}
|
|
8734
|
+
break;
|
|
8735
|
+
}
|
|
8736
|
+
case "backup":
|
|
8737
|
+
parts.push(" & ");
|
|
8738
|
+
break;
|
|
8739
|
+
case "forward":
|
|
8740
|
+
break;
|
|
8741
|
+
default:
|
|
8742
|
+
break;
|
|
8743
|
+
}
|
|
8744
|
+
}
|
|
8745
|
+
if (inChord && chordPitches.length > 0) {
|
|
8746
|
+
parts.push("[" + chordPitches.join("") + "]" + chordDurationStr + chordTieStr + chordSlurEnd);
|
|
8747
|
+
}
|
|
8748
|
+
const unitNoteChanged = currentUnitNote.num !== unitNote.num || currentUnitNote.den !== unitNote.den;
|
|
8749
|
+
return { noteStr: parts.join(""), lyrics, updatedUnitNote: unitNoteChanged ? currentUnitNote : void 0 };
|
|
8750
|
+
}
|
|
8751
|
+
function detectBrokenRhythm(entries, currentIdx, currentNote, divisions, unitNote) {
|
|
8752
|
+
if (currentNote.chord || currentNote.grace || currentNote.rest || currentNote.timeModification) return null;
|
|
8753
|
+
let nextIdx = currentIdx + 1;
|
|
8754
|
+
while (nextIdx < entries.length && entries[nextIdx].type === "note" && entries[nextIdx].chord) {
|
|
8755
|
+
nextIdx++;
|
|
8756
|
+
}
|
|
8757
|
+
while (nextIdx < entries.length && entries[nextIdx].type !== "note") {
|
|
8758
|
+
nextIdx++;
|
|
8759
|
+
}
|
|
8760
|
+
if (nextIdx >= entries.length) return null;
|
|
8761
|
+
const nextEntry = entries[nextIdx];
|
|
8762
|
+
if (nextEntry.type !== "note") return null;
|
|
8763
|
+
const nextNote = nextEntry;
|
|
8764
|
+
if (nextNote.chord || nextNote.grace || nextNote.rest || nextNote.timeModification) return null;
|
|
8765
|
+
const d1 = currentNote.duration;
|
|
8766
|
+
const d2 = nextNote.duration;
|
|
8767
|
+
if (d1 <= 0 || d2 <= 0) return null;
|
|
8768
|
+
const sum = d1 + d2;
|
|
8769
|
+
if (sum % 2 !== 0) return null;
|
|
8770
|
+
const base = sum / 2;
|
|
8771
|
+
const baseFrac = durationToAbcFraction(base, divisions, unitNote);
|
|
8772
|
+
if (baseFrac.den > 16 || baseFrac.num > 16) return null;
|
|
8773
|
+
const frac1 = durationToAbcFraction(d1, divisions, unitNote);
|
|
8774
|
+
const frac2 = durationToAbcFraction(d2, divisions, unitNote);
|
|
8775
|
+
if (frac1.den === 1 && frac2.den === 1) return null;
|
|
8776
|
+
let marker = null;
|
|
8777
|
+
if (d1 * 1 === d2 * 3) {
|
|
8778
|
+
marker = ">";
|
|
8779
|
+
} else if (d1 * 3 === d2 * 1) {
|
|
8780
|
+
marker = "<";
|
|
8781
|
+
} else if (d1 * 1 === d2 * 7) {
|
|
8782
|
+
marker = ">>";
|
|
8783
|
+
} else if (d1 * 7 === d2 * 1) {
|
|
8784
|
+
marker = "<<";
|
|
8785
|
+
}
|
|
8786
|
+
if (!marker) return null;
|
|
8787
|
+
return { marker, nextNote, nextIndex: nextIdx, baseFrac };
|
|
8788
|
+
}
|
|
8789
|
+
function detectChordPerNoteTies(entries, startIdx) {
|
|
8790
|
+
let tiedCount = 0;
|
|
8791
|
+
let totalCount = 1;
|
|
8792
|
+
const firstNote = entries[startIdx];
|
|
8793
|
+
if (firstNote.type === "note") {
|
|
8794
|
+
const hasTieStart2 = firstNote.tie?.type === "start" || firstNote.ties?.some((t) => t.type === "start");
|
|
8795
|
+
if (hasTieStart2) tiedCount++;
|
|
8796
|
+
}
|
|
8797
|
+
for (let i = startIdx + 1; i < entries.length; i++) {
|
|
8798
|
+
const e = entries[i];
|
|
8799
|
+
if (e.type !== "note" || !e.chord) break;
|
|
8800
|
+
totalCount++;
|
|
8801
|
+
const hasTieStart2 = e.tie?.type === "start" || e.ties?.some((t) => t.type === "start");
|
|
8802
|
+
if (hasTieStart2) tiedCount++;
|
|
8803
|
+
}
|
|
8804
|
+
return tiedCount > 1 || tiedCount > 0 && totalCount > 1;
|
|
8805
|
+
}
|
|
8806
|
+
function detectChordIndividualDurations(entries, startIdx) {
|
|
8807
|
+
const firstNote = entries[startIdx];
|
|
8808
|
+
if (firstNote.type !== "note") return false;
|
|
8809
|
+
if (firstNote._abcIndividualChordDurations) return true;
|
|
8810
|
+
if (useIndividualChordDurations) return true;
|
|
8811
|
+
const baseDuration = firstNote.duration;
|
|
8812
|
+
for (let i = startIdx + 1; i < entries.length; i++) {
|
|
8813
|
+
const e = entries[i];
|
|
8814
|
+
if (e.type !== "note" || !e.chord) break;
|
|
8815
|
+
if (e.duration !== baseDuration) return true;
|
|
8816
|
+
}
|
|
8817
|
+
return false;
|
|
8818
|
+
}
|
|
8819
|
+
function serializeNote2(note, divisions, unitNote, _inChord) {
|
|
8820
|
+
let pitchStr = "";
|
|
8821
|
+
let durationStr = "";
|
|
8822
|
+
if (note.rest) {
|
|
8823
|
+
if (note.rest.measure) {
|
|
8824
|
+
pitchStr = "Z";
|
|
8825
|
+
durationStr = "";
|
|
8826
|
+
} else {
|
|
8827
|
+
const isInvisible = note.printObject === false;
|
|
8828
|
+
pitchStr = isInvisible ? "x" : "z";
|
|
8829
|
+
const { num, den } = durationToAbcFraction(note.duration, divisions, unitNote);
|
|
8830
|
+
durationStr = formatAbcDuration(num, den);
|
|
8831
|
+
}
|
|
8832
|
+
} else if (note.grace) {
|
|
8833
|
+
if (note.pitch) {
|
|
8834
|
+
pitchStr = serializePitch2(note.pitch, note.accidental?.value === "natural");
|
|
8835
|
+
}
|
|
8836
|
+
durationStr = "";
|
|
8837
|
+
} else if (note.pitch) {
|
|
8838
|
+
pitchStr = serializePitch2(note.pitch, note.accidental?.value === "natural");
|
|
8839
|
+
const effectiveDuration = note.duration;
|
|
8840
|
+
const { num, den } = durationToAbcFraction(effectiveDuration, divisions, unitNote);
|
|
8841
|
+
durationStr = formatAbcDuration(num, den);
|
|
8842
|
+
}
|
|
8843
|
+
let tieStr = "";
|
|
8844
|
+
if (note.tie?.type === "start" || note.ties?.some((t) => t.type === "start")) {
|
|
8845
|
+
tieStr = "-";
|
|
8846
|
+
}
|
|
8847
|
+
let slurStart = "";
|
|
8848
|
+
let slurEnd = "";
|
|
8849
|
+
if (note.notations) {
|
|
8850
|
+
for (const notation of note.notations) {
|
|
8851
|
+
if (notation.type === "slur") {
|
|
8852
|
+
if (notation.slurType === "start") slurStart += "(";
|
|
8853
|
+
if (notation.slurType === "stop") slurEnd += ")";
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8856
|
+
}
|
|
8857
|
+
const full = slurStart + pitchStr + durationStr + tieStr + slurEnd;
|
|
8858
|
+
return { full, pitch: pitchStr, duration: durationStr, slurStart, slurEnd, tieStr };
|
|
8859
|
+
}
|
|
8860
|
+
function serializeBarline2(barline) {
|
|
8861
|
+
const hasRepeatForward = barline.repeat?.direction === "forward";
|
|
8862
|
+
const hasRepeatBackward = barline.repeat?.direction === "backward";
|
|
8863
|
+
const hasEnding = barline.ending;
|
|
8864
|
+
let result = "";
|
|
8865
|
+
if (hasRepeatForward) {
|
|
8866
|
+
result += "|:";
|
|
8867
|
+
} else if (hasRepeatBackward) {
|
|
8868
|
+
result += ":|";
|
|
8869
|
+
} else if (hasEnding && !barline.barStyle) {
|
|
8870
|
+
} else {
|
|
8871
|
+
switch (barline.barStyle) {
|
|
8872
|
+
case "light-light":
|
|
8873
|
+
result += "||";
|
|
8874
|
+
break;
|
|
8875
|
+
case "light-heavy":
|
|
8876
|
+
result += "|]";
|
|
8877
|
+
break;
|
|
8878
|
+
case "heavy-light":
|
|
8879
|
+
result += "[|";
|
|
8880
|
+
break;
|
|
8881
|
+
default:
|
|
8882
|
+
result += "|";
|
|
8883
|
+
break;
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8886
|
+
if (hasEnding && hasEnding.type === "start") {
|
|
8887
|
+
if (result) {
|
|
8888
|
+
result += hasEnding.number;
|
|
8889
|
+
} else {
|
|
8890
|
+
result += `[${hasEnding.number} `;
|
|
8891
|
+
}
|
|
8892
|
+
}
|
|
8893
|
+
return result;
|
|
8894
|
+
}
|
|
8895
|
+
|
|
6033
8896
|
// src/utils/index.ts
|
|
6034
8897
|
var STEPS = ["C", "D", "E", "F", "G", "A", "B"];
|
|
6035
8898
|
var STEP_SEMITONES = {
|
|
@@ -11523,6 +14386,11 @@ var removeRepeat = removeRepeatBarline;
|
|
|
11523
14386
|
// src/file.ts
|
|
11524
14387
|
var import_promises = require("fs/promises");
|
|
11525
14388
|
async function parseFile(filePath) {
|
|
14389
|
+
const lowerPath = filePath.toLowerCase();
|
|
14390
|
+
if (lowerPath.endsWith(".abc")) {
|
|
14391
|
+
const data2 = await (0, import_promises.readFile)(filePath, "utf-8");
|
|
14392
|
+
return parseAbc(data2);
|
|
14393
|
+
}
|
|
11526
14394
|
const data = await (0, import_promises.readFile)(filePath);
|
|
11527
14395
|
if (isCompressed(data)) {
|
|
11528
14396
|
return parseCompressed(data);
|
|
@@ -11544,7 +14412,10 @@ function decodeBuffer(buffer) {
|
|
|
11544
14412
|
}
|
|
11545
14413
|
async function serializeToFile(score, filePath, options = {}) {
|
|
11546
14414
|
const lowerPath = filePath.toLowerCase();
|
|
11547
|
-
if (lowerPath.endsWith(".
|
|
14415
|
+
if (lowerPath.endsWith(".abc")) {
|
|
14416
|
+
const abcString = serializeAbc(score);
|
|
14417
|
+
await (0, import_promises.writeFile)(filePath, abcString, "utf-8");
|
|
14418
|
+
} else if (lowerPath.endsWith(".mxl")) {
|
|
11548
14419
|
const data = serializeCompressed(score, options);
|
|
11549
14420
|
await (0, import_promises.writeFile)(filePath, data);
|
|
11550
14421
|
} else if (lowerPath.endsWith(".mid") || lowerPath.endsWith(".midi")) {
|
|
@@ -11738,6 +14609,7 @@ async function serializeToFile(score, filePath, options = {}) {
|
|
|
11738
14609
|
modifyTempo,
|
|
11739
14610
|
moveNoteToStaff,
|
|
11740
14611
|
parse,
|
|
14612
|
+
parseAbc,
|
|
11741
14613
|
parseAuto,
|
|
11742
14614
|
parseCompressed,
|
|
11743
14615
|
parseFile,
|
|
@@ -11773,6 +14645,7 @@ async function serializeToFile(score, filePath, options = {}) {
|
|
|
11773
14645
|
removeWedge,
|
|
11774
14646
|
scoresEqual,
|
|
11775
14647
|
serialize,
|
|
14648
|
+
serializeAbc,
|
|
11776
14649
|
serializeCompressed,
|
|
11777
14650
|
serializeToFile,
|
|
11778
14651
|
setBarline,
|