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.mjs CHANGED
@@ -2459,6 +2459,1750 @@ function parseAuto(data) {
2459
2459
  return parse(xmlString);
2460
2460
  }
2461
2461
 
2462
+ // src/importers/abc.ts
2463
+ var DIVISIONS = 960;
2464
+ var NOTE_TYPE_MAP = {
2465
+ // duration in terms of quarter notes
2466
+ 16: "long",
2467
+ 8: "breve",
2468
+ 4: "whole",
2469
+ 2: "half",
2470
+ 1: "quarter",
2471
+ 0.5: "eighth",
2472
+ 0.25: "16th",
2473
+ 0.125: "32nd",
2474
+ 0.0625: "64th"
2475
+ };
2476
+ var KEY_FIFTHS = {
2477
+ "Cb": -7,
2478
+ "Gb": -6,
2479
+ "Db": -5,
2480
+ "Ab": -4,
2481
+ "Eb": -3,
2482
+ "Bb": -2,
2483
+ "F": -1,
2484
+ "C": 0,
2485
+ "G": 1,
2486
+ "D": 2,
2487
+ "A": 3,
2488
+ "E": 4,
2489
+ "B": 5,
2490
+ "F#": 6,
2491
+ "C#": 7
2492
+ };
2493
+ var MODE_OFFSET = {
2494
+ "major": 0,
2495
+ "maj": 0,
2496
+ "ion": 0,
2497
+ "ionian": 0,
2498
+ "": 0,
2499
+ "minor": -3,
2500
+ "min": -3,
2501
+ "m": -3,
2502
+ "dorian": -2,
2503
+ "dor": -2,
2504
+ "phrygian": -4,
2505
+ "phr": -4,
2506
+ "lydian": 1,
2507
+ "lyd": 1,
2508
+ "mixolydian": -1,
2509
+ "mix": -1,
2510
+ "aeolian": -3,
2511
+ "aeo": -3,
2512
+ "locrian": -5,
2513
+ "loc": -5
2514
+ };
2515
+ var MODE_NAME = {
2516
+ "major": "major",
2517
+ "maj": "major",
2518
+ "ion": "ionian",
2519
+ "ionian": "ionian",
2520
+ "": "major",
2521
+ "minor": "minor",
2522
+ "min": "minor",
2523
+ "m": "minor",
2524
+ "dorian": "dorian",
2525
+ "dor": "dorian",
2526
+ "phrygian": "phrygian",
2527
+ "phr": "phrygian",
2528
+ "lydian": "lydian",
2529
+ "lyd": "lydian",
2530
+ "mixolydian": "mixolydian",
2531
+ "mix": "mixolydian",
2532
+ "aeolian": "aeolian",
2533
+ "aeo": "aeolian",
2534
+ "locrian": "locrian",
2535
+ "loc": "locrian"
2536
+ };
2537
+ var DYNAMICS_VALUES = /* @__PURE__ */ new Set([
2538
+ "pppppp",
2539
+ "ppppp",
2540
+ "pppp",
2541
+ "ppp",
2542
+ "pp",
2543
+ "p",
2544
+ "mp",
2545
+ "mf",
2546
+ "f",
2547
+ "ff",
2548
+ "fff",
2549
+ "ffff",
2550
+ "fffff",
2551
+ "ffffff",
2552
+ "sf",
2553
+ "sfz",
2554
+ "sffz",
2555
+ "sfp",
2556
+ "sfpp",
2557
+ "fp",
2558
+ "rf",
2559
+ "rfz",
2560
+ "fz",
2561
+ "pf"
2562
+ ]);
2563
+ function parseHeader(lines) {
2564
+ const header = { voices: [], extraFields: [], directives: [] };
2565
+ let bodyStartIndex = 0;
2566
+ let foundKey = false;
2567
+ let postKHeaderDone = false;
2568
+ const headerFieldOrder = [];
2569
+ for (let i = 0; i < lines.length; i++) {
2570
+ const line = lines[i].trim();
2571
+ if (line === "") continue;
2572
+ if (line.startsWith("%%")) {
2573
+ if (!foundKey) {
2574
+ header.directives.push(line);
2575
+ headerFieldOrder.push(line);
2576
+ } else if (!postKHeaderDone) {
2577
+ header.directives.push(line);
2578
+ headerFieldOrder.push(line);
2579
+ bodyStartIndex = i + 1;
2580
+ }
2581
+ continue;
2582
+ }
2583
+ if (line.startsWith("%")) {
2584
+ if (!foundKey) {
2585
+ headerFieldOrder.push(lines[i]);
2586
+ } else if (!postKHeaderDone) {
2587
+ headerFieldOrder.push(lines[i]);
2588
+ bodyStartIndex = i + 1;
2589
+ }
2590
+ continue;
2591
+ }
2592
+ const fieldMatch = line.match(/^([A-Za-z]):\s*(.*)/);
2593
+ const postKFields = /* @__PURE__ */ new Set(["I", "N"]);
2594
+ if (fieldMatch && (!foundKey || fieldMatch[1] === "V" || foundKey && postKFields.has(fieldMatch[1]))) {
2595
+ const [, field, value] = fieldMatch;
2596
+ if (foundKey && field !== "V") {
2597
+ if (!postKHeaderDone) {
2598
+ header.extraFields.push({ field, value: value.trim() });
2599
+ headerFieldOrder.push(line);
2600
+ bodyStartIndex = i + 1;
2601
+ }
2602
+ continue;
2603
+ }
2604
+ switch (field) {
2605
+ case "X":
2606
+ header.referenceNumber = parseInt(value, 10);
2607
+ headerFieldOrder.push(line);
2608
+ break;
2609
+ case "T":
2610
+ header.title = value.trim();
2611
+ headerFieldOrder.push(line);
2612
+ break;
2613
+ case "C":
2614
+ header.composer = value.trim();
2615
+ headerFieldOrder.push(line);
2616
+ break;
2617
+ case "M":
2618
+ header.meter = value.trim();
2619
+ headerFieldOrder.push(line);
2620
+ break;
2621
+ case "L":
2622
+ header.unitNoteLength = value.trim();
2623
+ headerFieldOrder.push(line);
2624
+ break;
2625
+ case "Q":
2626
+ header.tempo = value.trim();
2627
+ headerFieldOrder.push(line);
2628
+ break;
2629
+ case "K":
2630
+ header.key = value.trim();
2631
+ foundKey = true;
2632
+ bodyStartIndex = i + 1;
2633
+ headerFieldOrder.push(line);
2634
+ break;
2635
+ case "V": {
2636
+ const voiceValue = value.trim();
2637
+ const voiceId = voiceValue.split(/\s+/)[0];
2638
+ const nameMatch = voiceValue.match(/name=["']?([^"'\s]+)["']?/i);
2639
+ const nmMatch = voiceValue.match(/nm=["']([^"']*)["']/i);
2640
+ const clefMatch = voiceValue.match(/clef=(\S+)/i);
2641
+ const displayName = nameMatch ? nameMatch[1] : nmMatch ? nmMatch[1] : voiceId;
2642
+ const hasParams = clefMatch || nameMatch || nmMatch || /\b(Program|merge|up|down|bass|treble|alto|tenor|soprano|octave|snm|stem)\b/i.test(voiceValue);
2643
+ const isBodyVoiceSwitch = foundKey && !hasParams;
2644
+ if (isBodyVoiceSwitch) {
2645
+ postKHeaderDone = true;
2646
+ }
2647
+ const existingVoice = header.voices.find((v) => v.id === voiceId);
2648
+ if (existingVoice) {
2649
+ if (nameMatch || nmMatch) existingVoice.name = displayName;
2650
+ if (clefMatch) existingVoice.clef = clefMatch[1];
2651
+ if (foundKey && !isBodyVoiceSwitch) existingVoice.fullLine = lines[i];
2652
+ } else {
2653
+ header.voices.push({
2654
+ id: voiceId,
2655
+ name: displayName,
2656
+ clef: clefMatch ? clefMatch[1] : void 0,
2657
+ fullLine: lines[i]
2658
+ });
2659
+ }
2660
+ if (!foundKey) {
2661
+ headerFieldOrder.push(lines[i]);
2662
+ } else if (!isBodyVoiceSwitch && !postKHeaderDone) {
2663
+ headerFieldOrder.push(lines[i]);
2664
+ bodyStartIndex = i + 1;
2665
+ }
2666
+ break;
2667
+ }
2668
+ default:
2669
+ header.extraFields.push({ field, value: value.trim() });
2670
+ headerFieldOrder.push(line);
2671
+ break;
2672
+ }
2673
+ } else if (!foundKey) {
2674
+ continue;
2675
+ } else {
2676
+ postKHeaderDone = true;
2677
+ break;
2678
+ }
2679
+ }
2680
+ return { header, bodyStartIndex, headerFieldOrder };
2681
+ }
2682
+ function parseKeySignature2(keyStr) {
2683
+ if (!keyStr || keyStr.trim() === "" || keyStr.trim().toLowerCase() === "none") {
2684
+ return { fifths: 0, mode: "major" };
2685
+ }
2686
+ const trimmed = keyStr.trim();
2687
+ const keyMatch = trimmed.match(/^([A-Ga-g])(#|b)?/);
2688
+ if (!keyMatch) {
2689
+ return { fifths: 0, mode: "major" };
2690
+ }
2691
+ const keyNote = keyMatch[1].toUpperCase();
2692
+ const keyAccidental = keyMatch[2] || "";
2693
+ const keyName = keyNote + keyAccidental;
2694
+ const remainder = trimmed.slice(keyMatch[0].length).trim().toLowerCase();
2695
+ let mode = "";
2696
+ for (const m of Object.keys(MODE_OFFSET)) {
2697
+ if (m && remainder.startsWith(m)) {
2698
+ mode = m;
2699
+ break;
2700
+ }
2701
+ }
2702
+ const baseFifths = KEY_FIFTHS[keyName];
2703
+ if (baseFifths === void 0) {
2704
+ return { fifths: 0, mode: "major" };
2705
+ }
2706
+ const modeOffset = MODE_OFFSET[mode] ?? 0;
2707
+ const fifths = baseFifths + modeOffset;
2708
+ const modeValue = MODE_NAME[mode] ?? "major";
2709
+ return { fifths, mode: modeValue };
2710
+ }
2711
+ function parseTimeSignature2(meterStr) {
2712
+ if (!meterStr || meterStr.trim() === "") {
2713
+ return { beats: "4", beatType: 4 };
2714
+ }
2715
+ const trimmed = meterStr.trim();
2716
+ if (trimmed === "C" || trimmed.toLowerCase() === "common") {
2717
+ return { beats: "4", beatType: 4, symbol: "common" };
2718
+ }
2719
+ if (trimmed === "C|" || trimmed.toLowerCase() === "cut") {
2720
+ return { beats: "2", beatType: 2, symbol: "cut" };
2721
+ }
2722
+ const match = trimmed.match(/^(\d+)\/(\d+)$/);
2723
+ if (match) {
2724
+ return { beats: match[1], beatType: parseInt(match[2], 10) };
2725
+ }
2726
+ return { beats: "4", beatType: 4 };
2727
+ }
2728
+ function parseUnitNoteLength(lengthStr, meterStr) {
2729
+ if (lengthStr) {
2730
+ const match = lengthStr.trim().match(/^(\d+)\/(\d+)$/);
2731
+ if (match) {
2732
+ return { num: parseInt(match[1], 10), den: parseInt(match[2], 10) };
2733
+ }
2734
+ }
2735
+ if (meterStr) {
2736
+ const mMatch = meterStr.trim().match(/^(\d+)\/(\d+)$/);
2737
+ if (mMatch) {
2738
+ const ratio = parseInt(mMatch[1], 10) / parseInt(mMatch[2], 10);
2739
+ return ratio >= 0.75 ? { num: 1, den: 8 } : { num: 1, den: 16 };
2740
+ }
2741
+ }
2742
+ return { num: 1, den: 8 };
2743
+ }
2744
+ function lengthToDuration(num, den, unitNote) {
2745
+ const fractionOfWhole = num * unitNote.num / (den * unitNote.den);
2746
+ return Math.round(fractionOfWhole * 4 * DIVISIONS);
2747
+ }
2748
+ function durationToNoteType(duration) {
2749
+ const quarterNotes = duration / DIVISIONS;
2750
+ for (const [qnStr, type] of Object.entries(NOTE_TYPE_MAP)) {
2751
+ const qn = parseFloat(qnStr);
2752
+ if (Math.abs(quarterNotes - qn) < 1e-3) {
2753
+ return { noteType: type, dots: 0 };
2754
+ }
2755
+ if (Math.abs(quarterNotes - qn * 1.5) < 1e-3) {
2756
+ return { noteType: type, dots: 1 };
2757
+ }
2758
+ if (Math.abs(quarterNotes - qn * 1.75) < 1e-3) {
2759
+ return { noteType: type, dots: 2 };
2760
+ }
2761
+ }
2762
+ let bestType = "quarter";
2763
+ let bestDiff = Infinity;
2764
+ for (const [qnStr, type] of Object.entries(NOTE_TYPE_MAP)) {
2765
+ const diff = Math.abs(quarterNotes - parseFloat(qnStr));
2766
+ if (diff < bestDiff) {
2767
+ bestDiff = diff;
2768
+ bestType = type;
2769
+ }
2770
+ }
2771
+ return { noteType: bestType, dots: 0 };
2772
+ }
2773
+ function tokenizeBody(bodyLines) {
2774
+ const voiceTokens = /* @__PURE__ */ new Map();
2775
+ let currentVoice = "1";
2776
+ voiceTokens.set(currentVoice, []);
2777
+ let isContinuation = false;
2778
+ const inlineVoiceMarkers = /* @__PURE__ */ new Map();
2779
+ const voiceDeclarationLines = [];
2780
+ const bodyComments = [];
2781
+ const bodyDirectives = [];
2782
+ const wFields = [];
2783
+ const voiceInterleavePattern = [];
2784
+ let currentGroup = [];
2785
+ let lastVoiceInGroup = null;
2786
+ const groupBarCounts = [];
2787
+ let currentGroupBarCounts = [];
2788
+ let currentVoiceBarCount = 0;
2789
+ const voiceBarCounts = /* @__PURE__ */ new Map();
2790
+ const voiceCommentsMap = /* @__PURE__ */ new Map();
2791
+ const preVoiceComments = [];
2792
+ let pendingComments = [];
2793
+ function flushPendingToVoice(voiceId) {
2794
+ if (pendingComments.length === 0) return;
2795
+ if (!voiceCommentsMap.has(voiceId)) {
2796
+ voiceCommentsMap.set(voiceId, []);
2797
+ }
2798
+ const barCount = voiceBarCounts.get(voiceId) || 0;
2799
+ for (const comment of pendingComments) {
2800
+ voiceCommentsMap.get(voiceId).push({ barIndex: barCount, comment });
2801
+ }
2802
+ pendingComments = [];
2803
+ }
2804
+ for (const rawLine of bodyLines) {
2805
+ const trimmedEnd = rawLine.trimEnd();
2806
+ const hasLineContinuation = trimmedEnd.endsWith("\\");
2807
+ const lineContent = hasLineContinuation ? trimmedEnd.slice(0, -1) : rawLine;
2808
+ const line = lineContent.trim();
2809
+ const wFieldMatch = line.match(/^W:(.*)/);
2810
+ if (wFieldMatch) {
2811
+ wFields.push(rawLine);
2812
+ isContinuation = false;
2813
+ continue;
2814
+ }
2815
+ if (line === "") {
2816
+ isContinuation = false;
2817
+ continue;
2818
+ }
2819
+ const isRealDirective = line.startsWith("%%") && /^%%[A-Za-z]/.test(line);
2820
+ const isComment = line.startsWith("%") && !isRealDirective;
2821
+ if (isComment) {
2822
+ pendingComments.push(rawLine);
2823
+ isContinuation = false;
2824
+ continue;
2825
+ }
2826
+ if (isRealDirective) {
2827
+ bodyDirectives.push(rawLine);
2828
+ isContinuation = false;
2829
+ continue;
2830
+ }
2831
+ const voiceMatch = line.match(/^V:\s*(\S+)/);
2832
+ if (voiceMatch) {
2833
+ const newVoice = voiceMatch[1];
2834
+ if (currentGroup.includes(newVoice)) {
2835
+ if (lastVoiceInGroup !== null) {
2836
+ currentGroupBarCounts.push(currentVoiceBarCount);
2837
+ currentVoiceBarCount = 0;
2838
+ }
2839
+ groupBarCounts.push(currentGroupBarCounts);
2840
+ currentGroupBarCounts = [];
2841
+ voiceInterleavePattern.push(currentGroup);
2842
+ currentGroup = [];
2843
+ lastVoiceInGroup = null;
2844
+ for (const c of pendingComments) {
2845
+ bodyComments.push(c);
2846
+ }
2847
+ pendingComments = [];
2848
+ }
2849
+ preVoiceComments.push([...pendingComments]);
2850
+ pendingComments = [];
2851
+ currentVoice = newVoice;
2852
+ if (!voiceTokens.has(currentVoice)) {
2853
+ voiceTokens.set(currentVoice, []);
2854
+ }
2855
+ voiceDeclarationLines.push(rawLine);
2856
+ if (lastVoiceInGroup !== currentVoice) {
2857
+ if (lastVoiceInGroup !== null) {
2858
+ currentGroupBarCounts.push(currentVoiceBarCount);
2859
+ currentVoiceBarCount = 0;
2860
+ }
2861
+ currentGroup.push(currentVoice);
2862
+ lastVoiceInGroup = currentVoice;
2863
+ }
2864
+ isContinuation = false;
2865
+ continue;
2866
+ }
2867
+ const lyricsMatch = line.match(/^w:\s*(.*)/);
2868
+ if (lyricsMatch) {
2869
+ const syllables = parseLyricLine(lyricsMatch[1]);
2870
+ voiceTokens.get(currentVoice).push({
2871
+ type: "lyrics",
2872
+ value: lyricsMatch[1],
2873
+ syllables
2874
+ });
2875
+ isContinuation = false;
2876
+ continue;
2877
+ }
2878
+ const bodyKeyMatch = line.match(/^K:\s*(.*)/);
2879
+ if (bodyKeyMatch) {
2880
+ const currentTokens2 = voiceTokens.get(currentVoice);
2881
+ if (currentTokens2.length > 0) {
2882
+ const lastToken = currentTokens2[currentTokens2.length - 1];
2883
+ if (lastToken.type !== "line_break") {
2884
+ currentTokens2.push({ type: "line_break", value: "\n" });
2885
+ }
2886
+ }
2887
+ currentTokens2.push({ type: "inline_field", value: `K:${bodyKeyMatch[1]}` });
2888
+ currentTokens2.push({ type: "line_break", value: "\n" });
2889
+ isContinuation = false;
2890
+ continue;
2891
+ }
2892
+ if (/^[A-Za-z]:\s*/.test(line) && !/^\[/.test(line)) {
2893
+ isContinuation = false;
2894
+ continue;
2895
+ }
2896
+ flushPendingToVoice(currentVoice);
2897
+ if (!currentGroup.includes(currentVoice)) {
2898
+ if (lastVoiceInGroup !== null) {
2899
+ currentGroupBarCounts.push(currentVoiceBarCount);
2900
+ currentVoiceBarCount = 0;
2901
+ }
2902
+ currentGroup.push(currentVoice);
2903
+ lastVoiceInGroup = currentVoice;
2904
+ }
2905
+ const tokens = tokenizeMusicLine(lineContent);
2906
+ const currentTokens = voiceTokens.get(currentVoice);
2907
+ if (currentTokens.length > 0 && !isContinuation) {
2908
+ const lastToken = currentTokens[currentTokens.length - 1];
2909
+ if (lastToken.type !== "line_break") {
2910
+ currentTokens.push({ type: "line_break", value: "\n" });
2911
+ }
2912
+ }
2913
+ for (const token of tokens) {
2914
+ if (token.type === "inline_field") {
2915
+ const fieldMatch = token.value.match(/^V:\s*(.+)/);
2916
+ if (fieldMatch) {
2917
+ const voiceId = fieldMatch[1].trim().split(/\s+/)[0];
2918
+ currentVoice = voiceId;
2919
+ if (!voiceTokens.has(currentVoice)) {
2920
+ voiceTokens.set(currentVoice, []);
2921
+ }
2922
+ if (!inlineVoiceMarkers.has(voiceId)) {
2923
+ inlineVoiceMarkers.set(voiceId, `[${token.value}]`);
2924
+ }
2925
+ continue;
2926
+ }
2927
+ }
2928
+ voiceTokens.get(currentVoice).push(token);
2929
+ if (token.type === "bar") {
2930
+ currentVoiceBarCount++;
2931
+ voiceBarCounts.set(currentVoice, (voiceBarCounts.get(currentVoice) || 0) + 1);
2932
+ }
2933
+ }
2934
+ if (hasLineContinuation) {
2935
+ voiceTokens.get(currentVoice).push({ type: "line_break", value: "\\\n" });
2936
+ isContinuation = true;
2937
+ } else {
2938
+ isContinuation = false;
2939
+ }
2940
+ }
2941
+ const result = [];
2942
+ const voiceIds = [];
2943
+ for (const [voiceId, tokens] of voiceTokens) {
2944
+ if (tokens.length > 0) {
2945
+ result.push(tokens);
2946
+ voiceIds.push(voiceId);
2947
+ }
2948
+ }
2949
+ const trailingComments = [...pendingComments];
2950
+ pendingComments = [];
2951
+ if (currentGroup.length > 0) {
2952
+ currentGroupBarCounts.push(currentVoiceBarCount);
2953
+ groupBarCounts.push(currentGroupBarCounts);
2954
+ voiceInterleavePattern.push(currentGroup);
2955
+ }
2956
+ const voiceComments = {};
2957
+ for (const [voiceId, comments] of voiceCommentsMap) {
2958
+ voiceComments[voiceId] = comments;
2959
+ }
2960
+ return { tokens: result.length > 0 ? result : [[]], voiceIds, inlineVoiceMarkers, voiceDeclarationLines, bodyComments, bodyDirectives, wFields, voiceInterleavePattern, groupBarCounts, voiceComments, preVoiceComments, trailingComments };
2961
+ }
2962
+ function parseLyricLine(text) {
2963
+ const parts = [];
2964
+ const tokens = text.split(/\s+/);
2965
+ for (const token of tokens) {
2966
+ if (token === "") continue;
2967
+ const syllables = token.split("-");
2968
+ for (let i = 0; i < syllables.length; i++) {
2969
+ if (syllables[i] === "" && i > 0) continue;
2970
+ parts.push(syllables[i] + (i < syllables.length - 1 ? "-" : ""));
2971
+ }
2972
+ }
2973
+ return parts;
2974
+ }
2975
+ function tokenizeMusicLine(line) {
2976
+ const tokens = [];
2977
+ let i = 0;
2978
+ while (i < line.length) {
2979
+ const ch = line[i];
2980
+ if (ch === " " || ch === " ") {
2981
+ tokens.push({ type: "space", value: " " });
2982
+ while (i < line.length && (line[i] === " " || line[i] === " ")) i++;
2983
+ continue;
2984
+ }
2985
+ if (ch === "%") break;
2986
+ if (ch === '"') {
2987
+ const end = line.indexOf('"', i + 1);
2988
+ if (end !== -1) {
2989
+ tokens.push({ type: "chord_symbol", value: line.slice(i + 1, end) });
2990
+ i = end + 1;
2991
+ continue;
2992
+ }
2993
+ }
2994
+ if (ch === "!") {
2995
+ const end = line.indexOf("!", i + 1);
2996
+ if (end !== -1) {
2997
+ tokens.push({ type: "decoration", value: line.slice(i + 1, end) });
2998
+ i = end + 1;
2999
+ continue;
3000
+ }
3001
+ }
3002
+ if (ch === "{") {
3003
+ tokens.push({ type: "grace_start", value: "{" });
3004
+ i++;
3005
+ continue;
3006
+ }
3007
+ if (ch === "}") {
3008
+ tokens.push({ type: "grace_end", value: "}" });
3009
+ i++;
3010
+ continue;
3011
+ }
3012
+ if (ch === "(" && i + 1 < line.length && /\d/.test(line[i + 1])) {
3013
+ const tupletResult = parseTuplet(line, i);
3014
+ if (tupletResult) {
3015
+ tokens.push(tupletResult.token);
3016
+ i = tupletResult.nextIndex;
3017
+ continue;
3018
+ }
3019
+ }
3020
+ if (ch === "(") {
3021
+ tokens.push({ type: "slur_start", value: "(" });
3022
+ i++;
3023
+ continue;
3024
+ }
3025
+ if (ch === ")") {
3026
+ tokens.push({ type: "slur_end", value: ")" });
3027
+ i++;
3028
+ continue;
3029
+ }
3030
+ if (ch === "-") {
3031
+ tokens.push({ type: "tie", value: "-" });
3032
+ i++;
3033
+ continue;
3034
+ }
3035
+ if (ch === "|" || ch === ":" || ch === "[" && (i + 1 < line.length && line[i + 1] === "|")) {
3036
+ const barResult = parseBarLine(line, i);
3037
+ if (barResult) {
3038
+ tokens.push(barResult.token);
3039
+ i = barResult.nextIndex;
3040
+ if (i < line.length && /\d/.test(line[i])) {
3041
+ const numMatch = line.slice(i).match(/^(\d+)/);
3042
+ if (numMatch) {
3043
+ tokens.push({ type: "ending", value: numMatch[1] });
3044
+ i += numMatch[1].length;
3045
+ if (i < line.length && line[i] === " ") i++;
3046
+ }
3047
+ }
3048
+ continue;
3049
+ }
3050
+ }
3051
+ if (ch === "[" && i + 1 < line.length && /\d/.test(line[i + 1])) {
3052
+ const numMatch = line.slice(i + 1).match(/^(\d+)/);
3053
+ if (numMatch) {
3054
+ tokens.push({ type: "ending", value: numMatch[1], bracket: true });
3055
+ i += 1 + numMatch[1].length;
3056
+ if (i < line.length && line[i] === " ") i++;
3057
+ continue;
3058
+ }
3059
+ }
3060
+ if (ch === "[" && i + 1 < line.length && /[A-Za-z]/.test(line[i + 1])) {
3061
+ const colonIdx = line.indexOf(":", i + 2);
3062
+ if (colonIdx !== -1 && colonIdx <= i + 3) {
3063
+ const end = line.indexOf("]", colonIdx);
3064
+ if (end !== -1) {
3065
+ tokens.push({ type: "inline_field", value: line.slice(i + 1, end) });
3066
+ i = end + 1;
3067
+ continue;
3068
+ }
3069
+ }
3070
+ }
3071
+ if (ch === ">" || ch === "<") {
3072
+ let val = ch;
3073
+ let j = i + 1;
3074
+ while (j < line.length && line[j] === ch) {
3075
+ val += line[j];
3076
+ j++;
3077
+ }
3078
+ tokens.push({ type: "broken_rhythm", value: val });
3079
+ i = j;
3080
+ continue;
3081
+ }
3082
+ if (ch === "&") {
3083
+ tokens.push({ type: "overlay", value: "&" });
3084
+ i++;
3085
+ continue;
3086
+ }
3087
+ if (ch === "[") {
3088
+ tokens.push({ type: "chord_start", value: "[" });
3089
+ i++;
3090
+ continue;
3091
+ }
3092
+ if (ch === "]") {
3093
+ i++;
3094
+ const dur = parseDuration(line, i);
3095
+ i = dur.nextIndex;
3096
+ tokens.push({ type: "chord_end", value: "]", durationNum: dur.num, durationDen: dur.den });
3097
+ continue;
3098
+ }
3099
+ if ((ch === "v" || ch === "u" || ch === "T" || ch === "M") && i + 1 < line.length) {
3100
+ const nextCh = line[i + 1];
3101
+ if (isNoteStart(nextCh) || nextCh === "[" || nextCh === "z" || nextCh === "x" || nextCh === "v" || nextCh === "u" || nextCh === "T" || nextCh === "M" || nextCh === "(" || nextCh === "!" || nextCh === '"') {
3102
+ tokens.push({ type: "decoration", value: ch });
3103
+ i++;
3104
+ continue;
3105
+ }
3106
+ }
3107
+ if (isNoteStart(ch) || ch === "z" || ch === "Z" || ch === "x" || ch === "X") {
3108
+ const noteResult = parseNoteToken(line, i);
3109
+ if (noteResult) {
3110
+ tokens.push(noteResult.token);
3111
+ i = noteResult.nextIndex;
3112
+ continue;
3113
+ }
3114
+ }
3115
+ i++;
3116
+ }
3117
+ return tokens;
3118
+ }
3119
+ function isNoteStart(ch) {
3120
+ return /[A-Ga-g^_=]/.test(ch);
3121
+ }
3122
+ function parseTuplet(line, i) {
3123
+ const match = line.slice(i).match(/^\((\d+)(?::(\d*)(?::(\d*))?)?/);
3124
+ if (!match) return null;
3125
+ const p = parseInt(match[1], 10);
3126
+ let q;
3127
+ let r;
3128
+ if (match[2] !== void 0 && match[2] !== "") {
3129
+ q = parseInt(match[2], 10);
3130
+ }
3131
+ if (match[3] !== void 0 && match[3] !== "") {
3132
+ r = parseInt(match[3], 10);
3133
+ }
3134
+ if (q === void 0) {
3135
+ if (p === 2) q = 3;
3136
+ else if (p === 3) q = 2;
3137
+ else if (p === 4) q = 3;
3138
+ else if (p === 5 || p === 6) q = 2;
3139
+ else if (p === 7 || p === 8 || p === 9) q = 2;
3140
+ else q = 2;
3141
+ }
3142
+ if (r === void 0) {
3143
+ r = p;
3144
+ }
3145
+ return {
3146
+ token: { type: "tuplet", value: match[0], tupletP: p, tupletQ: q, tupletR: r },
3147
+ nextIndex: i + match[0].length
3148
+ };
3149
+ }
3150
+ function parseBarLine(line, i) {
3151
+ const patterns = [
3152
+ [":|]", "end-repeat-final"],
3153
+ [":||:", "double-repeat"],
3154
+ ["::", "double-repeat"],
3155
+ [":|:", "double-repeat"],
3156
+ ["|>|", "thick-thin"],
3157
+ ["|:", "start-repeat"],
3158
+ [":|", "end-repeat"],
3159
+ ["||", "double"],
3160
+ ["|]", "final"],
3161
+ ["[|", "heavy-light"],
3162
+ ["|", "regular"]
3163
+ ];
3164
+ for (const [pat, type] of patterns) {
3165
+ if (line.slice(i).startsWith(pat)) {
3166
+ return {
3167
+ token: { type: "bar", value: pat, barType: type },
3168
+ nextIndex: i + pat.length
3169
+ };
3170
+ }
3171
+ }
3172
+ if (line[i] === ":" && i + 1 < line.length && line[i + 1] === "|") {
3173
+ return {
3174
+ token: { type: "bar", value: ":|", barType: "end-repeat" },
3175
+ nextIndex: i + 2
3176
+ };
3177
+ }
3178
+ return null;
3179
+ }
3180
+ function parseNoteToken(line, i) {
3181
+ const start = i;
3182
+ if (line[i] === "z" || line[i] === "Z" || line[i] === "x" || line[i] === "X") {
3183
+ i++;
3184
+ const dur2 = parseDuration(line, i);
3185
+ i = dur2.nextIndex;
3186
+ return {
3187
+ token: {
3188
+ type: "rest",
3189
+ value: line.slice(start, i),
3190
+ durationNum: dur2.num,
3191
+ durationDen: dur2.den
3192
+ },
3193
+ nextIndex: i
3194
+ };
3195
+ }
3196
+ let accidental = 0;
3197
+ let explicitNatural = false;
3198
+ if (line[i] === "^") {
3199
+ accidental = 1;
3200
+ i++;
3201
+ if (i < line.length && line[i] === "^") {
3202
+ accidental = 2;
3203
+ i++;
3204
+ }
3205
+ } else if (line[i] === "_") {
3206
+ accidental = -1;
3207
+ i++;
3208
+ if (i < line.length && line[i] === "_") {
3209
+ accidental = -2;
3210
+ i++;
3211
+ }
3212
+ } else if (line[i] === "=") {
3213
+ accidental = 0;
3214
+ explicitNatural = true;
3215
+ i++;
3216
+ }
3217
+ if (i >= line.length || !/[A-Ga-g]/.test(line[i])) {
3218
+ return null;
3219
+ }
3220
+ const noteLetter = line[i];
3221
+ i++;
3222
+ const pitch = abcNoteToPitch(noteLetter, accidental);
3223
+ while (i < line.length && (line[i] === "'" || line[i] === ",")) {
3224
+ if (line[i] === "'") {
3225
+ pitch.octave++;
3226
+ } else {
3227
+ pitch.octave--;
3228
+ }
3229
+ i++;
3230
+ }
3231
+ const dur = parseDuration(line, i);
3232
+ i = dur.nextIndex;
3233
+ const token = {
3234
+ type: "note",
3235
+ value: line.slice(start, i),
3236
+ pitch,
3237
+ durationNum: dur.num,
3238
+ durationDen: dur.den,
3239
+ accidental: accidental !== 0 || explicitNatural ? accidental : void 0
3240
+ };
3241
+ if (explicitNatural) {
3242
+ token.explicitNatural = true;
3243
+ }
3244
+ return {
3245
+ token,
3246
+ nextIndex: i
3247
+ };
3248
+ }
3249
+ function abcNoteToPitch(letter, accidental) {
3250
+ const isLower = letter === letter.toLowerCase();
3251
+ const step = letter.toUpperCase();
3252
+ const octave = isLower ? 5 : 4;
3253
+ const pitch = { step, octave };
3254
+ if (accidental !== 0) {
3255
+ pitch.alter = accidental;
3256
+ }
3257
+ return pitch;
3258
+ }
3259
+ function parseDuration(line, i) {
3260
+ let num = 1;
3261
+ let den = 1;
3262
+ const numMatch = line.slice(i).match(/^(\d+)/);
3263
+ if (numMatch) {
3264
+ num = parseInt(numMatch[1], 10);
3265
+ i += numMatch[1].length;
3266
+ }
3267
+ if (i < line.length && line[i] === "/") {
3268
+ i++;
3269
+ const denMatch = line.slice(i).match(/^(\d+)/);
3270
+ if (denMatch) {
3271
+ den = parseInt(denMatch[1], 10);
3272
+ i += denMatch[1].length;
3273
+ } else {
3274
+ den = 2;
3275
+ while (i < line.length && line[i] === "/") {
3276
+ den *= 2;
3277
+ i++;
3278
+ }
3279
+ }
3280
+ }
3281
+ return { num, den, nextIndex: i };
3282
+ }
3283
+ function buildScore(header, voiceTokensList, voiceIds, headerFieldOrder, inlineVoiceMarkers = /* @__PURE__ */ new Map(), voiceDeclarationLines = [], bodyComments = [], bodyDirectives = [], wFields = [], voiceInterleavePattern = [], groupBarCounts = [], voiceComments = {}, preVoiceComments = [], trailingComments = []) {
3284
+ const unitNote = parseUnitNoteLength(header.unitNoteLength, header.meter);
3285
+ const timeSignature = parseTimeSignature2(header.meter || "4/4");
3286
+ const keySignature = parseKeySignature2(header.key || "C");
3287
+ const beatsNum = parseInt(timeSignature.beats, 10);
3288
+ const beatType = timeSignature.beatType;
3289
+ const measureDuration = Math.round(beatsNum / beatType * 4 * DIVISIONS);
3290
+ const parts = [];
3291
+ const partListEntries = [];
3292
+ const miscellaneous = [];
3293
+ if (header.referenceNumber !== void 0) {
3294
+ miscellaneous.push({ name: "abc-reference-number", value: String(header.referenceNumber) });
3295
+ }
3296
+ if (header.unitNoteLength) {
3297
+ miscellaneous.push({ name: "abc-unit-note-length", value: header.unitNoteLength });
3298
+ }
3299
+ if (header.tempo) {
3300
+ miscellaneous.push({ name: "abc-tempo", value: header.tempo });
3301
+ }
3302
+ const extraCreators = [];
3303
+ let sourceValue;
3304
+ const encoderValues = [];
3305
+ if (header.extraFields && header.extraFields.length > 0) {
3306
+ for (const ef of header.extraFields) {
3307
+ switch (ef.field) {
3308
+ case "S":
3309
+ sourceValue = ef.value;
3310
+ break;
3311
+ case "Z":
3312
+ encoderValues.push(ef.value);
3313
+ break;
3314
+ case "O":
3315
+ extraCreators.push({ type: "origin", value: ef.value });
3316
+ break;
3317
+ default:
3318
+ miscellaneous.push({ name: `abc-${ef.field}`, value: ef.value });
3319
+ break;
3320
+ }
3321
+ }
3322
+ }
3323
+ if (header.directives && header.directives.length > 0) {
3324
+ miscellaneous.push({ name: "abc-directives", value: JSON.stringify(header.directives) });
3325
+ }
3326
+ if (headerFieldOrder.length > 0) {
3327
+ miscellaneous.push({ name: "abc-header-order", value: JSON.stringify(headerFieldOrder) });
3328
+ }
3329
+ if (voiceIds.length > 0) {
3330
+ miscellaneous.push({ name: "abc-voice-ids", value: JSON.stringify(voiceIds) });
3331
+ }
3332
+ if (inlineVoiceMarkers.size > 0) {
3333
+ const markersObj = {};
3334
+ for (const [id, marker] of inlineVoiceMarkers) {
3335
+ markersObj[id] = marker;
3336
+ }
3337
+ miscellaneous.push({ name: "abc-inline-voice-markers", value: JSON.stringify(markersObj) });
3338
+ }
3339
+ if (voiceDeclarationLines.length > 0 && inlineVoiceMarkers.size > 0) {
3340
+ miscellaneous.push({ name: "abc-voice-declaration-lines", value: JSON.stringify(voiceDeclarationLines) });
3341
+ }
3342
+ if (voiceDeclarationLines.length > 0) {
3343
+ miscellaneous.push({ name: "abc-body-voice-lines", value: JSON.stringify(voiceDeclarationLines) });
3344
+ }
3345
+ if (bodyComments.length > 0) {
3346
+ miscellaneous.push({ name: "abc-body-comments", value: JSON.stringify(bodyComments) });
3347
+ }
3348
+ if (bodyDirectives.length > 0) {
3349
+ miscellaneous.push({ name: "abc-body-directives", value: JSON.stringify(bodyDirectives) });
3350
+ }
3351
+ if (wFields.length > 0) {
3352
+ miscellaneous.push({ name: "abc-w-fields", value: JSON.stringify(wFields) });
3353
+ }
3354
+ if (voiceInterleavePattern.length > 0) {
3355
+ miscellaneous.push({ name: "abc-voice-interleave", value: JSON.stringify(voiceInterleavePattern) });
3356
+ }
3357
+ if (groupBarCounts.length > 0) {
3358
+ miscellaneous.push({ name: "abc-group-bar-counts", value: JSON.stringify(groupBarCounts) });
3359
+ }
3360
+ if (Object.keys(voiceComments).length > 0) {
3361
+ miscellaneous.push({ name: "abc-voice-comments", value: JSON.stringify(voiceComments) });
3362
+ }
3363
+ if (preVoiceComments.length > 0) {
3364
+ miscellaneous.push({ name: "abc-pre-voice-comments", value: JSON.stringify(preVoiceComments) });
3365
+ }
3366
+ if (trailingComments.length > 0) {
3367
+ miscellaneous.push({ name: "abc-trailing-comments", value: JSON.stringify(trailingComments) });
3368
+ }
3369
+ const voiceFullLines = {};
3370
+ for (const voice of header.voices || []) {
3371
+ if (voice.fullLine) {
3372
+ voiceFullLines[voice.id] = voice.fullLine;
3373
+ }
3374
+ }
3375
+ if (Object.keys(voiceFullLines).length > 0) {
3376
+ miscellaneous.push({ name: "abc-voice-full-lines", value: JSON.stringify(voiceFullLines) });
3377
+ }
3378
+ for (let voiceIndex = 0; voiceIndex < voiceTokensList.length; voiceIndex++) {
3379
+ const tokens = voiceTokensList[voiceIndex];
3380
+ const partId = `P${voiceIndex + 1}`;
3381
+ const voiceId = voiceIds[voiceIndex];
3382
+ const headerVoice = header.voices?.find((v) => v.id === voiceId) || header.voices && header.voices[voiceIndex];
3383
+ const voiceName = headerVoice ? headerVoice.name || `Voice ${voiceIndex + 1}` : voiceTokensList.length > 1 ? `Voice ${voiceIndex + 1}` : "Music";
3384
+ partListEntries.push({
3385
+ _id: generateId(),
3386
+ type: "score-part",
3387
+ id: partId,
3388
+ name: voiceName
3389
+ });
3390
+ const voiceClef = headerVoice ? abcClefToMusicXml(headerVoice.clef) : void 0;
3391
+ const buildResult = buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDuration, voiceClef);
3392
+ parts.push({
3393
+ _id: generateId(),
3394
+ id: partId,
3395
+ measures: buildResult.measures
3396
+ });
3397
+ if (buildResult.lineBreaks.length > 0) {
3398
+ if (voiceIndex === 0) {
3399
+ miscellaneous.push({ name: "abc-line-breaks", value: JSON.stringify(buildResult.lineBreaks) });
3400
+ }
3401
+ miscellaneous.push({ name: `abc-line-breaks-${voiceIndex}`, value: JSON.stringify(buildResult.lineBreaks) });
3402
+ }
3403
+ if (buildResult.hasIndividualChordDurations) {
3404
+ miscellaneous.push({ name: "abc-chord-individual-durations", value: "true" });
3405
+ }
3406
+ if (voiceIndex === 0) {
3407
+ let hasLyrics2 = false;
3408
+ let lyricsAfterAll = true;
3409
+ let seenLyrics = false;
3410
+ const lyricLineCounts = [];
3411
+ for (const token of tokens) {
3412
+ if (token.type === "lyrics") {
3413
+ hasLyrics2 = true;
3414
+ seenLyrics = true;
3415
+ lyricLineCounts.push(token.syllables?.length || 0);
3416
+ } else if (seenLyrics && (token.type === "note" || token.type === "rest" || token.type === "bar")) {
3417
+ lyricsAfterAll = false;
3418
+ }
3419
+ }
3420
+ if (hasLyrics2 && lyricsAfterAll) {
3421
+ miscellaneous.push({ name: "abc-lyrics-after-all", value: "true" });
3422
+ }
3423
+ if (hasLyrics2 && lyricLineCounts.length > 0) {
3424
+ miscellaneous.push({ name: "abc-lyrics-line-counts", value: JSON.stringify(lyricLineCounts) });
3425
+ }
3426
+ }
3427
+ }
3428
+ if (header.tempo && parts.length > 0 && parts[0].measures.length > 0) {
3429
+ const tempoDirection = parseTempoToDirection(header.tempo);
3430
+ if (tempoDirection) {
3431
+ parts[0].measures[0].entries.unshift(tempoDirection);
3432
+ }
3433
+ }
3434
+ const creators = [];
3435
+ if (header.composer) {
3436
+ creators.push({ type: "composer", value: header.composer });
3437
+ }
3438
+ creators.push(...extraCreators);
3439
+ const encoding = {
3440
+ software: ["musicxml-io (ABC import)"]
3441
+ };
3442
+ if (encoderValues.length > 0) {
3443
+ encoding.encoder = encoderValues;
3444
+ }
3445
+ return {
3446
+ _id: generateId(),
3447
+ metadata: {
3448
+ movementTitle: header.title,
3449
+ creators: creators.length > 0 ? creators : void 0,
3450
+ source: sourceValue,
3451
+ encoding,
3452
+ miscellaneous: miscellaneous.length > 0 ? miscellaneous : void 0
3453
+ },
3454
+ partList: partListEntries,
3455
+ parts,
3456
+ version: "4.0"
3457
+ };
3458
+ }
3459
+ function parseTempoToDirection(tempoStr) {
3460
+ const match = tempoStr.match(/(?:(\d+)\/(\d+)\s*=\s*)?(\d+)/);
3461
+ if (!match) return null;
3462
+ const perMinute = parseInt(match[3], 10);
3463
+ let beatUnit = "quarter";
3464
+ if (match[1] && match[2]) {
3465
+ const num = parseInt(match[1], 10);
3466
+ const den = parseInt(match[2], 10);
3467
+ const quarterNotes = num / den * 4;
3468
+ const found = NOTE_TYPE_MAP[quarterNotes];
3469
+ if (found) beatUnit = found;
3470
+ }
3471
+ return {
3472
+ _id: generateId(),
3473
+ type: "direction",
3474
+ directionTypes: [{ kind: "metronome", beatUnit, perMinute }],
3475
+ placement: "above",
3476
+ sound: { tempo: perMinute }
3477
+ };
3478
+ }
3479
+ function abcClefToMusicXml(abcClef) {
3480
+ if (!abcClef) return { sign: "G", line: 2 };
3481
+ const c = abcClef.toLowerCase();
3482
+ if (c === "treble" || c === "treble-8va" || c === "treble+8") return { sign: "G", line: 2 };
3483
+ if (c === "bass" || c === "bass3") return { sign: "F", line: 4 };
3484
+ if (c === "alto") return { sign: "C", line: 3 };
3485
+ if (c === "tenor") return { sign: "C", line: 4 };
3486
+ if (c === "soprano") return { sign: "C", line: 1 };
3487
+ if (c === "mezzo" || c === "mezzo-soprano") return { sign: "C", line: 2 };
3488
+ if (c === "baritone") return { sign: "C", line: 5 };
3489
+ if (c === "perc" || c === "percussion") return { sign: "percussion" };
3490
+ return { sign: "G", line: 2 };
3491
+ }
3492
+ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDuration, clef) {
3493
+ const measures = [];
3494
+ let currentEntries = [];
3495
+ let hasIndividualChordDurations = false;
3496
+ let currentBarlines = [];
3497
+ let currentPosition = 0;
3498
+ let measureNumber = 1;
3499
+ let isFirstMeasure = true;
3500
+ let pendingTie = false;
3501
+ let slurDepth = 0;
3502
+ let pendingSlurStarts = 0;
3503
+ let slurStartNotes = [];
3504
+ let inGrace = false;
3505
+ let pendingBrokenRhythm = null;
3506
+ let tupletState = null;
3507
+ const pendingPreNoteItems = [];
3508
+ let pendingEndingNumber = null;
3509
+ let currentLyrics = [];
3510
+ let noteCountForLyrics = 0;
3511
+ let inChord = false;
3512
+ let chordNotes = [];
3513
+ let chordNoteTies = [];
3514
+ let currentUnitNote = { ...unitNote };
3515
+ let pendingTupletStart = false;
3516
+ let pendingKeyChange = null;
3517
+ const lineBreaks = [];
3518
+ function flushPendingPreNoteItems() {
3519
+ for (const item of pendingPreNoteItems) {
3520
+ if (item.kind === "harmony") {
3521
+ const harmony = createHarmonyEntry(item.value);
3522
+ if (harmony) currentEntries.push(harmony);
3523
+ } else if (item.kind === "dynamic") {
3524
+ const dynDir = createDynamicsDirection(item.value);
3525
+ if (dynDir) currentEntries.push(dynDir);
3526
+ } else if (item.kind === "decoration") {
3527
+ const decoDir = {
3528
+ _id: generateId(),
3529
+ type: "direction",
3530
+ directionTypes: [{ kind: "words", text: item.value }]
3531
+ };
3532
+ currentEntries.push(decoDir);
3533
+ }
3534
+ }
3535
+ pendingPreNoteItems.length = 0;
3536
+ }
3537
+ function finalizeMeasure(endBarType) {
3538
+ const measure = {
3539
+ _id: generateId(),
3540
+ number: String(measureNumber),
3541
+ entries: currentEntries
3542
+ };
3543
+ if (isFirstMeasure) {
3544
+ measure.attributes = {
3545
+ divisions: DIVISIONS,
3546
+ time: timeSignature,
3547
+ key: keySignature,
3548
+ clef: [clef || { sign: "G", line: 2 }]
3549
+ };
3550
+ isFirstMeasure = false;
3551
+ }
3552
+ if (pendingKeyChange) {
3553
+ if (!measure.attributes) {
3554
+ measure.attributes = {};
3555
+ }
3556
+ const kValue = pendingKeyChange.replace(/^K:\s*/, "");
3557
+ measure.attributes.key = parseKeySignature2(kValue);
3558
+ pendingKeyChange = null;
3559
+ }
3560
+ if (endBarType || currentBarlines.length > 0) {
3561
+ measure.barlines = [...currentBarlines];
3562
+ if (endBarType) {
3563
+ const barline = createBarline(endBarType, "right", pendingEndingNumber);
3564
+ if (barline) {
3565
+ measure.barlines.push(barline);
3566
+ }
3567
+ pendingEndingNumber = null;
3568
+ }
3569
+ }
3570
+ measures.push(measure);
3571
+ currentEntries = [];
3572
+ currentBarlines = [];
3573
+ currentPosition = 0;
3574
+ measureNumber++;
3575
+ }
3576
+ for (let ti = 0; ti < tokens.length; ti++) {
3577
+ const token = tokens[ti];
3578
+ switch (token.type) {
3579
+ case "note": {
3580
+ if (inChord) {
3581
+ chordNotes.push(token);
3582
+ break;
3583
+ }
3584
+ const entry = createNoteEntry(token, currentUnitNote, pendingTie, inGrace, tupletState);
3585
+ pendingTie = false;
3586
+ if (pendingBrokenRhythm) {
3587
+ const n = pendingBrokenRhythm.length;
3588
+ const divisor = Math.pow(2, n);
3589
+ if (pendingBrokenRhythm[0] === ">") {
3590
+ entry.duration = Math.round(entry.duration / divisor);
3591
+ } else {
3592
+ entry.duration = Math.round(entry.duration * (2 * divisor - 1) / divisor);
3593
+ }
3594
+ pendingBrokenRhythm = null;
3595
+ }
3596
+ if (pendingTupletStart && !inGrace) {
3597
+ pendingTupletStart = false;
3598
+ }
3599
+ flushPendingPreNoteItems();
3600
+ while (pendingSlurStarts > 0) {
3601
+ if (!entry.notations) entry.notations = [];
3602
+ entry.notations.push({ type: "slur", slurType: "start", number: slurDepth - pendingSlurStarts + 1 });
3603
+ slurStartNotes.push(entry);
3604
+ pendingSlurStarts--;
3605
+ }
3606
+ if (currentLyrics.length > noteCountForLyrics && !entry.rest && !entry.grace) {
3607
+ const syllable = currentLyrics[noteCountForLyrics];
3608
+ if (syllable && syllable !== "" && syllable !== "*") {
3609
+ const isHyphenated = syllable.endsWith("-");
3610
+ const text = isHyphenated ? syllable.slice(0, -1) : syllable;
3611
+ entry.lyrics = [{
3612
+ number: 1,
3613
+ text,
3614
+ syllabic: isHyphenated ? "begin" : "single"
3615
+ }];
3616
+ if (isHyphenated && noteCountForLyrics + 1 < currentLyrics.length) {
3617
+ const nextSyllable = currentLyrics[noteCountForLyrics + 1];
3618
+ if (nextSyllable && !nextSyllable.endsWith("-")) {
3619
+ }
3620
+ }
3621
+ if (noteCountForLyrics > 0) {
3622
+ const prevSyllable = currentLyrics[noteCountForLyrics - 1];
3623
+ if (prevSyllable && prevSyllable.endsWith("-")) {
3624
+ entry.lyrics[0].syllabic = isHyphenated ? "middle" : "end";
3625
+ }
3626
+ }
3627
+ }
3628
+ noteCountForLyrics++;
3629
+ }
3630
+ if (!inGrace) {
3631
+ currentEntries.push(entry);
3632
+ currentPosition += entry.duration;
3633
+ if (tupletState) {
3634
+ tupletState.remaining--;
3635
+ if (tupletState.remaining <= 0) {
3636
+ tupletState = null;
3637
+ }
3638
+ }
3639
+ } else {
3640
+ currentEntries.push(entry);
3641
+ }
3642
+ break;
3643
+ }
3644
+ case "rest": {
3645
+ if (inChord) break;
3646
+ const restEntry = createRestEntry(token, currentUnitNote, tupletState, measureDuration);
3647
+ flushPendingPreNoteItems();
3648
+ currentEntries.push(restEntry);
3649
+ currentPosition += restEntry.duration;
3650
+ if (tupletState) {
3651
+ tupletState.remaining--;
3652
+ if (tupletState.remaining <= 0) {
3653
+ tupletState = null;
3654
+ }
3655
+ }
3656
+ break;
3657
+ }
3658
+ case "chord_start":
3659
+ inChord = true;
3660
+ chordNotes = [];
3661
+ chordNoteTies = [];
3662
+ break;
3663
+ case "chord_end": {
3664
+ inChord = false;
3665
+ if (chordNotes.length > 0) {
3666
+ for (const item of pendingPreNoteItems) {
3667
+ if (item.kind === "harmony") {
3668
+ const harmony = createHarmonyEntry(item.value);
3669
+ if (harmony) currentEntries.push(harmony);
3670
+ } else if (item.kind === "dynamic") {
3671
+ const dynDir = createDynamicsDirection(item.value);
3672
+ if (dynDir) currentEntries.push(dynDir);
3673
+ }
3674
+ }
3675
+ pendingPreNoteItems.length = 0;
3676
+ const chordDurNum = token.durationNum || 1;
3677
+ const chordDurDen = token.durationDen || 1;
3678
+ const hasIndividualDurations = chordNotes.some(
3679
+ (cn) => cn.durationNum !== void 0 && cn.durationNum !== 1 || cn.durationDen !== void 0 && cn.durationDen !== 1
3680
+ );
3681
+ const useIndividualDurations = hasIndividualDurations && chordDurNum === 1 && chordDurDen === 1;
3682
+ if (useIndividualDurations) hasIndividualChordDurations = true;
3683
+ for (let ci = 0; ci < chordNotes.length; ci++) {
3684
+ const chordToken = chordNotes[ci];
3685
+ const originalNum = chordToken.durationNum;
3686
+ const originalDen = chordToken.durationDen;
3687
+ if (!useIndividualDurations) {
3688
+ chordToken.durationNum = chordDurNum;
3689
+ chordToken.durationDen = chordDurDen;
3690
+ }
3691
+ const entry = createNoteEntry(chordToken, currentUnitNote, false, inGrace, tupletState);
3692
+ chordToken.durationNum = originalNum;
3693
+ chordToken.durationDen = originalDen;
3694
+ if (ci === 0) {
3695
+ while (pendingSlurStarts > 0) {
3696
+ if (!entry.notations) entry.notations = [];
3697
+ entry.notations.push({ type: "slur", slurType: "start", number: slurDepth - pendingSlurStarts + 1 });
3698
+ slurStartNotes.push(entry);
3699
+ pendingSlurStarts--;
3700
+ }
3701
+ if (useIndividualDurations) {
3702
+ entry._abcIndividualChordDurations = true;
3703
+ }
3704
+ } else {
3705
+ entry.chord = true;
3706
+ }
3707
+ if (chordNoteTies[ci]) {
3708
+ entry.tie = { type: "start" };
3709
+ entry.ties = [{ type: "start" }];
3710
+ if (!entry.notations) entry.notations = [];
3711
+ entry.notations.push({ type: "tied", tiedType: "start" });
3712
+ }
3713
+ currentEntries.push(entry);
3714
+ if (ci === 0) {
3715
+ currentPosition += entry.duration;
3716
+ }
3717
+ }
3718
+ }
3719
+ chordNotes = [];
3720
+ break;
3721
+ }
3722
+ case "bar": {
3723
+ flushPendingPreNoteItems();
3724
+ const barType = token.barType || "regular";
3725
+ if (barType === "double-repeat") {
3726
+ finalizeMeasure("end-repeat");
3727
+ currentBarlines.push(createBarline("start-repeat", "left", null));
3728
+ } else if (barType === "end-repeat") {
3729
+ finalizeMeasure("end-repeat");
3730
+ } else if (barType === "start-repeat") {
3731
+ if (currentEntries.length > 0) {
3732
+ finalizeMeasure("regular");
3733
+ }
3734
+ currentBarlines.push(createBarline("start-repeat", "left", null));
3735
+ } else if (barType === "final") {
3736
+ finalizeMeasure("final");
3737
+ } else if (barType === "end-repeat-final") {
3738
+ finalizeMeasure("end-repeat");
3739
+ } else {
3740
+ if (currentEntries.length > 0 || currentBarlines.length > 0) {
3741
+ finalizeMeasure(barType !== "regular" ? barType : void 0);
3742
+ }
3743
+ }
3744
+ break;
3745
+ }
3746
+ case "ending": {
3747
+ const prevTok = ti > 0 ? tokens[ti - 1] : null;
3748
+ const prevBarType = prevTok?.barType || "regular";
3749
+ if (!token.bracket && prevTok?.type === "bar" && prevBarType !== "regular" && measures.length > 0) {
3750
+ const lastMeasure = measures[measures.length - 1];
3751
+ const rightBl = lastMeasure.barlines?.find((b) => b.location === "right");
3752
+ if (rightBl) {
3753
+ const endingBarline2 = {
3754
+ _id: generateId(),
3755
+ location: "left",
3756
+ barStyle: rightBl.barStyle,
3757
+ repeat: rightBl.repeat,
3758
+ ending: { number: token.value, type: "start" }
3759
+ };
3760
+ lastMeasure.barlines = lastMeasure.barlines.filter((b) => b !== rightBl);
3761
+ currentBarlines.push(endingBarline2);
3762
+ break;
3763
+ }
3764
+ }
3765
+ const endingBarline = {
3766
+ _id: generateId(),
3767
+ location: "left",
3768
+ ending: {
3769
+ number: token.value,
3770
+ type: "start"
3771
+ }
3772
+ };
3773
+ if (ti > 0 && tokens[ti - 1].type === "bar" && (!tokens[ti - 1].barType || tokens[ti - 1].barType === "regular") && currentEntries.length === 0) {
3774
+ endingBarline.barStyle = "regular";
3775
+ }
3776
+ currentBarlines.push(endingBarline);
3777
+ break;
3778
+ }
3779
+ case "tie":
3780
+ if (inChord) {
3781
+ if (chordNotes.length > 0) {
3782
+ chordNoteTies[chordNotes.length - 1] = true;
3783
+ }
3784
+ } else {
3785
+ pendingTie = true;
3786
+ for (let ei = currentEntries.length - 1; ei >= 0; ei--) {
3787
+ const e = currentEntries[ei];
3788
+ if (e.type === "note" && !e.rest) {
3789
+ e.tie = { type: "start" };
3790
+ e.ties = [{ type: "start" }];
3791
+ if (!e.notations) e.notations = [];
3792
+ e.notations.push({ type: "tied", tiedType: "start" });
3793
+ break;
3794
+ }
3795
+ }
3796
+ }
3797
+ break;
3798
+ case "slur_start":
3799
+ slurDepth++;
3800
+ pendingSlurStarts++;
3801
+ break;
3802
+ case "slur_end":
3803
+ if (slurDepth > 0) {
3804
+ slurDepth--;
3805
+ for (let ei = currentEntries.length - 1; ei >= 0; ei--) {
3806
+ const e = currentEntries[ei];
3807
+ if (e.type === "note") {
3808
+ if (!e.notations) e.notations = [];
3809
+ e.notations.push({ type: "slur", slurType: "stop", number: slurDepth + 1 });
3810
+ break;
3811
+ }
3812
+ }
3813
+ }
3814
+ break;
3815
+ case "grace_start":
3816
+ inGrace = true;
3817
+ break;
3818
+ case "grace_end":
3819
+ inGrace = false;
3820
+ break;
3821
+ case "broken_rhythm": {
3822
+ const brokenN = token.value.length;
3823
+ const brokenDivisor = Math.pow(2, brokenN);
3824
+ for (let ei = currentEntries.length - 1; ei >= 0; ei--) {
3825
+ const e = currentEntries[ei];
3826
+ if (e.type === "note") {
3827
+ if (token.value[0] === ">") {
3828
+ e.duration = Math.round(e.duration * (2 * brokenDivisor - 1) / brokenDivisor);
3829
+ } else {
3830
+ e.duration = Math.round(e.duration / brokenDivisor);
3831
+ }
3832
+ break;
3833
+ }
3834
+ }
3835
+ pendingBrokenRhythm = token.value;
3836
+ break;
3837
+ }
3838
+ case "tuplet":
3839
+ tupletState = {
3840
+ p: token.tupletP,
3841
+ q: token.tupletQ,
3842
+ remaining: token.tupletR || token.tupletP
3843
+ };
3844
+ pendingTupletStart = true;
3845
+ break;
3846
+ case "chord_symbol":
3847
+ pendingPreNoteItems.push({ kind: "harmony", value: token.value });
3848
+ break;
3849
+ case "decoration":
3850
+ if (DYNAMICS_VALUES.has(token.value)) {
3851
+ pendingPreNoteItems.push({ kind: "dynamic", value: token.value });
3852
+ } else {
3853
+ const isShorthand = token.value.length === 1 && /^[vuTM]$/.test(token.value);
3854
+ const decoText = isShorthand ? token.value : `!${token.value}!`;
3855
+ pendingPreNoteItems.push({ kind: "decoration", value: decoText });
3856
+ }
3857
+ break;
3858
+ case "lyrics": {
3859
+ const syllables = token.syllables || [];
3860
+ applyLyricsToExistingNotes(measures, currentEntries, syllables);
3861
+ break;
3862
+ }
3863
+ case "overlay": {
3864
+ if (currentPosition > 0) {
3865
+ const backupEntry = {
3866
+ _id: generateId(),
3867
+ type: "backup",
3868
+ duration: currentPosition
3869
+ };
3870
+ currentEntries.push(backupEntry);
3871
+ currentPosition = 0;
3872
+ }
3873
+ break;
3874
+ }
3875
+ case "inline_field": {
3876
+ const lMatch = token.value.match(/^L:\s*(\d+)\/(\d+)/);
3877
+ if (lMatch) {
3878
+ currentUnitNote = { num: parseInt(lMatch[1], 10), den: parseInt(lMatch[2], 10) };
3879
+ const inlineEntry = {
3880
+ _id: generateId(),
3881
+ type: "direction",
3882
+ directionTypes: [{ kind: "words", text: `[L:${lMatch[1]}/${lMatch[2]}]` }]
3883
+ };
3884
+ currentEntries.push(inlineEntry);
3885
+ }
3886
+ const kMatch = token.value.match(/^K:\s*(.*)/);
3887
+ if (kMatch) {
3888
+ pendingKeyChange = `K:${kMatch[1]}`;
3889
+ }
3890
+ break;
3891
+ }
3892
+ case "space":
3893
+ break;
3894
+ case "line_break":
3895
+ flushPendingPreNoteItems();
3896
+ if (currentEntries.length > 0) {
3897
+ const breakText = token.value === "\\\n" ? "__abc_line_cont__" : "__abc_line_break__";
3898
+ const lineBreakDir = {
3899
+ _id: generateId(),
3900
+ type: "direction",
3901
+ directionTypes: [{ kind: "words", text: breakText }]
3902
+ };
3903
+ currentEntries.push(lineBreakDir);
3904
+ }
3905
+ if (measures.length > 0) {
3906
+ if (token.value === "\\\n") {
3907
+ lineBreaks.push(-measures.length);
3908
+ } else {
3909
+ lineBreaks.push(measures.length);
3910
+ }
3911
+ }
3912
+ break;
3913
+ default:
3914
+ break;
3915
+ }
3916
+ }
3917
+ if (currentEntries.length > 0) {
3918
+ finalizeMeasure();
3919
+ }
3920
+ applyTieStops(measures);
3921
+ return { measures, lineBreaks, hasIndividualChordDurations };
3922
+ }
3923
+ function applyLyricsToExistingNotes(finalizedMeasures, currentEntries, syllables) {
3924
+ const allNotes = [];
3925
+ for (const measure of finalizedMeasures) {
3926
+ for (const entry of measure.entries) {
3927
+ if (entry.type === "note" && !entry.rest && !entry.grace && !entry.chord) {
3928
+ allNotes.push(entry);
3929
+ }
3930
+ }
3931
+ }
3932
+ for (const entry of currentEntries) {
3933
+ if (entry.type === "note" && !entry.rest && !entry.grace && !entry.chord) {
3934
+ allNotes.push(entry);
3935
+ }
3936
+ }
3937
+ const unlyricedNotes = allNotes.filter((n) => !n.lyrics || n.lyrics.length === 0);
3938
+ const targetNotes = unlyricedNotes.slice(0, syllables.length);
3939
+ for (let si = 0; si < syllables.length && si < targetNotes.length; si++) {
3940
+ const syllable = syllables[si];
3941
+ if (!syllable || syllable === "" || syllable === "*") continue;
3942
+ const note = targetNotes[si];
3943
+ const isHyphenated = syllable.endsWith("-");
3944
+ const text = isHyphenated ? syllable.slice(0, -1) : syllable;
3945
+ let syllabic = isHyphenated ? "begin" : "single";
3946
+ if (si > 0) {
3947
+ const prevSyllable = syllables[si - 1];
3948
+ if (prevSyllable && prevSyllable.endsWith("-")) {
3949
+ syllabic = isHyphenated ? "middle" : "end";
3950
+ }
3951
+ }
3952
+ note.lyrics = [{
3953
+ number: 1,
3954
+ text,
3955
+ syllabic
3956
+ }];
3957
+ }
3958
+ }
3959
+ function applyTieStops(measures) {
3960
+ const allNotes = [];
3961
+ for (const measure of measures) {
3962
+ for (const entry of measure.entries) {
3963
+ if (entry.type === "note" && !entry.grace) {
3964
+ allNotes.push(entry);
3965
+ }
3966
+ }
3967
+ }
3968
+ for (let i = 0; i < allNotes.length - 1; i++) {
3969
+ const note = allNotes[i];
3970
+ if (note.tie?.type === "start" && note.pitch) {
3971
+ for (let j = i + 1; j < allNotes.length; j++) {
3972
+ const next = allNotes[j];
3973
+ if (next.pitch && next.pitch.step === note.pitch.step && next.pitch.octave === note.pitch.octave) {
3974
+ if (next.tie?.type === "start") {
3975
+ next.tie = { type: "stop" };
3976
+ next.ties = [{ type: "stop" }, { type: "start" }];
3977
+ } else {
3978
+ next.tie = { type: "stop" };
3979
+ next.ties = [{ type: "stop" }];
3980
+ }
3981
+ if (!next.notations) next.notations = [];
3982
+ next.notations.push({ type: "tied", tiedType: "stop" });
3983
+ break;
3984
+ }
3985
+ }
3986
+ }
3987
+ }
3988
+ }
3989
+ function createNoteEntry(token, unitNote, _hasTieStop, isGrace, tupletState) {
3990
+ const num = token.durationNum || 1;
3991
+ const den = token.durationDen || 1;
3992
+ let duration;
3993
+ if (isGrace) {
3994
+ duration = 0;
3995
+ } else {
3996
+ duration = lengthToDuration(num, den, unitNote);
3997
+ if (tupletState) {
3998
+ duration = Math.round(duration * tupletState.q / tupletState.p);
3999
+ }
4000
+ }
4001
+ const { noteType, dots } = durationToNoteType(isGrace ? lengthToDuration(num, den, unitNote) : duration);
4002
+ const entry = {
4003
+ _id: generateId(),
4004
+ type: "note",
4005
+ pitch: token.pitch,
4006
+ duration,
4007
+ voice: 1,
4008
+ noteType,
4009
+ dots: dots > 0 ? dots : void 0
4010
+ };
4011
+ if (isGrace) {
4012
+ entry.grace = { slash: true };
4013
+ entry.noteType = "eighth";
4014
+ }
4015
+ if (tupletState && !isGrace) {
4016
+ entry.timeModification = {
4017
+ actualNotes: tupletState.p,
4018
+ normalNotes: tupletState.q
4019
+ };
4020
+ }
4021
+ if (token.explicitNatural) {
4022
+ entry.accidental = { value: "natural" };
4023
+ } else if (token.accidental !== void 0 && token.accidental !== 0) {
4024
+ switch (token.accidental) {
4025
+ case 1:
4026
+ entry.accidental = { value: "sharp" };
4027
+ break;
4028
+ case 2:
4029
+ entry.accidental = { value: "double-sharp" };
4030
+ break;
4031
+ case -1:
4032
+ entry.accidental = { value: "flat" };
4033
+ break;
4034
+ case -2:
4035
+ entry.accidental = { value: "double-flat" };
4036
+ break;
4037
+ }
4038
+ }
4039
+ return entry;
4040
+ }
4041
+ function createRestEntry(token, unitNote, tupletState, measureDuration) {
4042
+ const num = token.durationNum || 1;
4043
+ const den = token.durationDen || 1;
4044
+ const isWholeMeasure = token.value.startsWith("Z");
4045
+ const isInvisible = token.value.startsWith("x") || token.value.startsWith("X");
4046
+ let duration;
4047
+ if (isWholeMeasure) {
4048
+ duration = measureDuration;
4049
+ } else {
4050
+ duration = lengthToDuration(num, den, unitNote);
4051
+ if (tupletState) {
4052
+ duration = Math.round(duration * tupletState.q / tupletState.p);
4053
+ }
4054
+ }
4055
+ const { noteType, dots } = durationToNoteType(duration);
4056
+ const entry = {
4057
+ _id: generateId(),
4058
+ type: "note",
4059
+ rest: isWholeMeasure ? { measure: true } : {},
4060
+ duration,
4061
+ voice: 1,
4062
+ noteType,
4063
+ dots: dots > 0 ? dots : void 0
4064
+ };
4065
+ if (isInvisible) {
4066
+ entry.printObject = false;
4067
+ }
4068
+ return entry;
4069
+ }
4070
+ function createBarline(barType, location, endingNumber) {
4071
+ const barline = {
4072
+ _id: generateId(),
4073
+ location
4074
+ };
4075
+ switch (barType) {
4076
+ case "start-repeat":
4077
+ barline.barStyle = "heavy-light";
4078
+ barline.repeat = { direction: "forward" };
4079
+ break;
4080
+ case "end-repeat":
4081
+ barline.barStyle = "light-heavy";
4082
+ barline.repeat = { direction: "backward" };
4083
+ break;
4084
+ case "final":
4085
+ barline.barStyle = "light-heavy";
4086
+ break;
4087
+ case "double":
4088
+ barline.barStyle = "light-light";
4089
+ break;
4090
+ case "heavy-light":
4091
+ barline.barStyle = "heavy-light";
4092
+ break;
4093
+ case "thick-thin":
4094
+ barline.barStyle = "heavy-light";
4095
+ break;
4096
+ default:
4097
+ return null;
4098
+ }
4099
+ if (endingNumber) {
4100
+ barline.ending = {
4101
+ number: endingNumber,
4102
+ type: location === "left" ? "start" : "stop"
4103
+ };
4104
+ }
4105
+ return barline;
4106
+ }
4107
+ function createHarmonyEntry(chordStr) {
4108
+ 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)?))?/);
4109
+ if (!match) return null;
4110
+ const rootStep = match[1];
4111
+ const rootAlter = match[2] === "#" ? 1 : match[2] === "b" ? -1 : void 0;
4112
+ const quality = match[3] || "";
4113
+ const bassNote = match[5];
4114
+ let kind = "major";
4115
+ switch (quality) {
4116
+ case "m":
4117
+ case "min":
4118
+ kind = "minor";
4119
+ break;
4120
+ case "7":
4121
+ kind = "dominant";
4122
+ break;
4123
+ case "maj7":
4124
+ case "M7":
4125
+ kind = "major-seventh";
4126
+ break;
4127
+ case "m7":
4128
+ case "min7":
4129
+ kind = "minor-seventh";
4130
+ break;
4131
+ case "dim":
4132
+ kind = "diminished";
4133
+ break;
4134
+ case "dim7":
4135
+ kind = "diminished-seventh";
4136
+ break;
4137
+ case "aug":
4138
+ kind = "augmented";
4139
+ break;
4140
+ case "aug7":
4141
+ kind = "augmented-seventh";
4142
+ break;
4143
+ case "6":
4144
+ kind = "major-sixth";
4145
+ break;
4146
+ case "m6":
4147
+ kind = "minor-sixth";
4148
+ break;
4149
+ case "9":
4150
+ kind = "dominant-ninth";
4151
+ break;
4152
+ case "m9":
4153
+ kind = "minor-ninth";
4154
+ break;
4155
+ case "sus4":
4156
+ kind = "suspended-fourth";
4157
+ break;
4158
+ case "sus2":
4159
+ kind = "suspended-second";
4160
+ break;
4161
+ }
4162
+ const entry = {
4163
+ _id: generateId(),
4164
+ type: "harmony",
4165
+ root: { rootStep, rootAlter: rootAlter !== void 0 ? rootAlter : void 0 },
4166
+ kind
4167
+ };
4168
+ if (bassNote) {
4169
+ const bassMatch = bassNote.match(/^([A-G])(#|b)?/);
4170
+ if (bassMatch) {
4171
+ entry.bass = {
4172
+ bassStep: bassMatch[1],
4173
+ bassAlter: bassMatch[2] === "#" ? 1 : bassMatch[2] === "b" ? -1 : void 0
4174
+ };
4175
+ }
4176
+ }
4177
+ return entry;
4178
+ }
4179
+ function createDynamicsDirection(dynamic) {
4180
+ if (!DYNAMICS_VALUES.has(dynamic)) return null;
4181
+ return {
4182
+ _id: generateId(),
4183
+ type: "direction",
4184
+ directionTypes: [{
4185
+ kind: "dynamics",
4186
+ value: dynamic
4187
+ }],
4188
+ placement: "below"
4189
+ };
4190
+ }
4191
+ function parseAbc(abcString) {
4192
+ const lines = abcString.split("\n");
4193
+ const { header, bodyStartIndex, headerFieldOrder } = parseHeader(lines);
4194
+ const bodyLines = lines.slice(bodyStartIndex);
4195
+ const { tokens: voiceTokensList, voiceIds, inlineVoiceMarkers, voiceDeclarationLines, bodyComments, bodyDirectives, wFields, voiceInterleavePattern, groupBarCounts, voiceComments, preVoiceComments, trailingComments } = tokenizeBody(bodyLines);
4196
+ const score = buildScore(header, voiceTokensList, voiceIds, headerFieldOrder, inlineVoiceMarkers, voiceDeclarationLines, bodyComments, bodyDirectives, wFields, voiceInterleavePattern, groupBarCounts, voiceComments, preVoiceComments, trailingComments);
4197
+ const bodyText = bodyLines.join("\n");
4198
+ const hasExplicitHalf = /[A-Ga-gzx]\/2/.test(bodyText);
4199
+ if (hasExplicitHalf) {
4200
+ if (!score.metadata.miscellaneous) score.metadata.miscellaneous = [];
4201
+ score.metadata.miscellaneous.push({ name: "abc-explicit-half", value: "true" });
4202
+ }
4203
+ return score;
4204
+ }
4205
+
2462
4206
  // src/validator/index.ts
2463
4207
  var DEFAULT_OPTIONS = {
2464
4208
  checkDivisions: true,
@@ -5759,6 +7503,1123 @@ function buildMidiFile(tracks, ticksPerQuarterNote) {
5759
7503
  return new Uint8Array(chunks);
5760
7504
  }
5761
7505
 
7506
+ // src/exporters/abc.ts
7507
+ var DIVISIONS_PER_QUARTER = 960;
7508
+ var useExplicitHalf = false;
7509
+ var useIndividualChordDurations = false;
7510
+ var FIFTHS_TO_KEY_MAJOR = {
7511
+ [-7]: "Cb",
7512
+ [-6]: "Gb",
7513
+ [-5]: "Db",
7514
+ [-4]: "Ab",
7515
+ [-3]: "Eb",
7516
+ [-2]: "Bb",
7517
+ [-1]: "F",
7518
+ 0: "C",
7519
+ 1: "G",
7520
+ 2: "D",
7521
+ 3: "A",
7522
+ 4: "E",
7523
+ 5: "B",
7524
+ 6: "F#",
7525
+ 7: "C#"
7526
+ };
7527
+ var FIFTHS_TO_KEY_MINOR = {
7528
+ [-7]: "Ab",
7529
+ [-6]: "Eb",
7530
+ [-5]: "Bb",
7531
+ [-4]: "F",
7532
+ [-3]: "C",
7533
+ [-2]: "G",
7534
+ [-1]: "D",
7535
+ 0: "A",
7536
+ 1: "E",
7537
+ 2: "B",
7538
+ 3: "F#",
7539
+ 4: "C#",
7540
+ 5: "G#",
7541
+ 6: "D#",
7542
+ 7: "A#"
7543
+ };
7544
+ var MODE_FIFTHS_OFFSET = {
7545
+ "major": 0,
7546
+ "ionian": 0,
7547
+ "minor": 3,
7548
+ "aeolian": 3,
7549
+ "dorian": 2,
7550
+ "phrygian": 4,
7551
+ "lydian": -1,
7552
+ "mixolydian": 1,
7553
+ "locrian": 5
7554
+ };
7555
+ var NOTE_TYPE_TO_QUARTER_LENGTH = {
7556
+ "long": 16,
7557
+ "breve": 8,
7558
+ "whole": 4,
7559
+ "half": 2,
7560
+ "quarter": 1,
7561
+ "eighth": 0.5,
7562
+ "16th": 0.25,
7563
+ "32nd": 0.125,
7564
+ "64th": 0.0625,
7565
+ "128th": 0.03125
7566
+ };
7567
+ function musicXmlClefToAbc(clef) {
7568
+ if (clef.sign === "G" && (clef.line === 2 || clef.line === void 0)) return "treble";
7569
+ if (clef.sign === "F" && (clef.line === 4 || clef.line === void 0)) return "bass";
7570
+ if (clef.sign === "C" && clef.line === 3) return "alto";
7571
+ if (clef.sign === "C" && clef.line === 4) return "tenor";
7572
+ if (clef.sign === "C" && clef.line === 1) return "soprano";
7573
+ if (clef.sign === "C" && clef.line === 2) return "mezzo-soprano";
7574
+ if (clef.sign === "C" && clef.line === 5) return "baritone";
7575
+ if (clef.sign === "percussion") return "perc";
7576
+ return null;
7577
+ }
7578
+ function serializeKey2(key) {
7579
+ const mode = key.mode || "major";
7580
+ const modeOffset = MODE_FIFTHS_OFFSET[mode] ?? 0;
7581
+ const majorFifths = key.fifths + modeOffset;
7582
+ let keyNote;
7583
+ if (mode === "minor" || mode === "aeolian") {
7584
+ keyNote = FIFTHS_TO_KEY_MINOR[key.fifths] || "A";
7585
+ return keyNote + "m";
7586
+ }
7587
+ keyNote = FIFTHS_TO_KEY_MAJOR[majorFifths] || "C";
7588
+ const modeStr = modeToAbcString(mode);
7589
+ return keyNote + modeStr;
7590
+ }
7591
+ function modeToAbcString(mode) {
7592
+ switch (mode) {
7593
+ case "major":
7594
+ case "ionian":
7595
+ return "";
7596
+ case "minor":
7597
+ case "aeolian":
7598
+ return "m";
7599
+ case "dorian":
7600
+ return "dor";
7601
+ case "phrygian":
7602
+ return "phr";
7603
+ case "lydian":
7604
+ return "lyd";
7605
+ case "mixolydian":
7606
+ return "mix";
7607
+ case "locrian":
7608
+ return "loc";
7609
+ default:
7610
+ return "";
7611
+ }
7612
+ }
7613
+ function serializeTimeSignature(time) {
7614
+ if (time.symbol === "common") return "C";
7615
+ if (time.symbol === "cut") return "C|";
7616
+ return `${time.beats}/${time.beatType}`;
7617
+ }
7618
+ function computeUnitNoteLength(score) {
7619
+ const firstMeasure = score.parts[0]?.measures[0];
7620
+ const time = firstMeasure?.attributes?.time;
7621
+ if (time) {
7622
+ const beats = parseInt(time.beats, 10);
7623
+ const beatType = time.beatType;
7624
+ const ratio = beats / beatType;
7625
+ if (ratio < 0.75) {
7626
+ return { num: 1, den: 16 };
7627
+ }
7628
+ }
7629
+ return { num: 1, den: 8 };
7630
+ }
7631
+ function durationToAbcFraction(duration, divisions, unitNote) {
7632
+ const abcNum = duration * unitNote.den;
7633
+ const abcDen = divisions * 4 * unitNote.num;
7634
+ const g = gcd(abcNum, abcDen);
7635
+ return { num: abcNum / g, den: abcDen / g };
7636
+ }
7637
+ function gcd(a, b) {
7638
+ a = Math.abs(a);
7639
+ b = Math.abs(b);
7640
+ while (b) {
7641
+ [a, b] = [b, a % b];
7642
+ }
7643
+ return a;
7644
+ }
7645
+ function formatAbcDuration(num, den) {
7646
+ if (num === 1 && den === 1) return "";
7647
+ if (den === 1) return String(num);
7648
+ if (num === 1) {
7649
+ if (den === 2 && !useExplicitHalf) return "/";
7650
+ return `/${den}`;
7651
+ }
7652
+ return `${num}/${den}`;
7653
+ }
7654
+ function serializePitch2(pitch, explicitNatural) {
7655
+ let result = "";
7656
+ if (explicitNatural) {
7657
+ result += "=";
7658
+ } else if (pitch.alter !== void 0 && pitch.alter !== 0) {
7659
+ if (pitch.alter === 1) result += "^";
7660
+ else if (pitch.alter === 2) result += "^^";
7661
+ else if (pitch.alter === -1) result += "_";
7662
+ else if (pitch.alter === -2) result += "__";
7663
+ }
7664
+ const step = pitch.step;
7665
+ const octave = pitch.octave;
7666
+ if (octave >= 5) {
7667
+ result += step.toLowerCase();
7668
+ for (let o = 6; o <= octave; o++) {
7669
+ result += "'";
7670
+ }
7671
+ } else {
7672
+ result += step;
7673
+ for (let o = 3; o >= octave; o--) {
7674
+ result += ",";
7675
+ }
7676
+ }
7677
+ return result;
7678
+ }
7679
+ function serializeHarmony2(harmony) {
7680
+ let result = harmony.root.rootStep;
7681
+ if (harmony.root.rootAlter === 1) result += "#";
7682
+ else if (harmony.root.rootAlter === -1) result += "b";
7683
+ switch (harmony.kind) {
7684
+ case "major":
7685
+ break;
7686
+ // no suffix
7687
+ case "minor":
7688
+ result += "m";
7689
+ break;
7690
+ case "dominant":
7691
+ result += "7";
7692
+ break;
7693
+ case "major-seventh":
7694
+ result += "maj7";
7695
+ break;
7696
+ case "minor-seventh":
7697
+ result += "m7";
7698
+ break;
7699
+ case "diminished":
7700
+ result += "dim";
7701
+ break;
7702
+ case "diminished-seventh":
7703
+ result += "dim7";
7704
+ break;
7705
+ case "augmented":
7706
+ result += "aug";
7707
+ break;
7708
+ case "augmented-seventh":
7709
+ result += "aug7";
7710
+ break;
7711
+ case "major-sixth":
7712
+ result += "6";
7713
+ break;
7714
+ case "minor-sixth":
7715
+ result += "m6";
7716
+ break;
7717
+ case "dominant-ninth":
7718
+ result += "9";
7719
+ break;
7720
+ case "minor-ninth":
7721
+ result += "m9";
7722
+ break;
7723
+ case "suspended-fourth":
7724
+ result += "sus4";
7725
+ break;
7726
+ case "suspended-second":
7727
+ result += "sus2";
7728
+ break;
7729
+ default:
7730
+ if (harmony.kindText) result += harmony.kindText;
7731
+ break;
7732
+ }
7733
+ if (harmony.bass) {
7734
+ result += "/" + harmony.bass.bassStep;
7735
+ if (harmony.bass.bassAlter === 1) result += "#";
7736
+ else if (harmony.bass.bassAlter === -1) result += "b";
7737
+ }
7738
+ return '"' + result + '"';
7739
+ }
7740
+ function serializeDynamics(direction) {
7741
+ for (const dt of direction.directionTypes) {
7742
+ if (dt.kind === "dynamics" && dt.value) {
7743
+ return `!${dt.value}!`;
7744
+ }
7745
+ }
7746
+ return null;
7747
+ }
7748
+ function serializeTempo(direction) {
7749
+ for (const dt of direction.directionTypes) {
7750
+ if (dt.kind === "metronome") {
7751
+ const beatUnit = dt.beatUnit;
7752
+ const perMinute = dt.perMinute;
7753
+ if (!perMinute) return null;
7754
+ const quarterLen = NOTE_TYPE_TO_QUARTER_LENGTH[beatUnit] ?? 1;
7755
+ const den = Math.round(4 / quarterLen);
7756
+ return `1/${den}=${perMinute}`;
7757
+ }
7758
+ }
7759
+ return null;
7760
+ }
7761
+ function serializeAbc(score, options) {
7762
+ const opts = {
7763
+ referenceNumber: options?.referenceNumber ?? 1,
7764
+ notesPerLine: options?.notesPerLine ?? 0,
7765
+ includeChordSymbols: options?.includeChordSymbols ?? true,
7766
+ includeDynamics: options?.includeDynamics ?? true,
7767
+ includeLyrics: options?.includeLyrics ?? true
7768
+ };
7769
+ useExplicitHalf = score.metadata.miscellaneous?.find((m) => m.name === "abc-explicit-half")?.value === "true";
7770
+ useIndividualChordDurations = score.metadata.miscellaneous?.some((m) => m.name === "abc-chord-individual-durations" && m.value === "true") ?? false;
7771
+ const storedUnitNote = score.metadata.miscellaneous?.find((m) => m.name === "abc-unit-note-length")?.value;
7772
+ let unitNote;
7773
+ if (storedUnitNote) {
7774
+ const match = storedUnitNote.match(/^(\d+)\/(\d+)$/);
7775
+ if (match) {
7776
+ unitNote = { num: parseInt(match[1], 10), den: parseInt(match[2], 10) };
7777
+ } else {
7778
+ unitNote = computeUnitNoteLength(score);
7779
+ }
7780
+ } else {
7781
+ unitNote = computeUnitNoteLength(score);
7782
+ }
7783
+ const lines = [];
7784
+ const firstPart = score.parts[0];
7785
+ const firstMeasure = firstPart?.measures[0];
7786
+ const attrs = firstMeasure?.attributes;
7787
+ const storedHeaderOrder = score.metadata.miscellaneous?.find((m) => m.name === "abc-header-order")?.value;
7788
+ if (storedHeaderOrder) {
7789
+ const headerOrder = JSON.parse(storedHeaderOrder);
7790
+ for (const field of headerOrder) {
7791
+ lines.push(field);
7792
+ }
7793
+ } else {
7794
+ const storedRefNum = score.metadata.miscellaneous?.find((m) => m.name === "abc-reference-number")?.value;
7795
+ const refNum = storedRefNum ? parseInt(storedRefNum, 10) : opts.referenceNumber;
7796
+ lines.push(`X:${refNum}`);
7797
+ if (score.metadata.movementTitle) {
7798
+ lines.push(`T:${score.metadata.movementTitle}`);
7799
+ }
7800
+ const composer = score.metadata.creators?.find((c) => c.type === "composer");
7801
+ if (composer) {
7802
+ lines.push(`C:${composer.value}`);
7803
+ }
7804
+ const rhythmFields = score.metadata.miscellaneous?.filter((m) => m.name === "abc-R");
7805
+ if (rhythmFields) {
7806
+ for (const rf of rhythmFields) lines.push(`R:${rf.value}`);
7807
+ }
7808
+ const originCreators = score.metadata.creators?.filter((c) => c.type === "origin");
7809
+ if (originCreators) {
7810
+ for (const oc of originCreators) lines.push(`O:${oc.value}`);
7811
+ }
7812
+ if (score.metadata.source) {
7813
+ lines.push(`S:${score.metadata.source}`);
7814
+ }
7815
+ const noteFields = score.metadata.miscellaneous?.filter((m) => m.name === "abc-N");
7816
+ if (noteFields) {
7817
+ for (const nf of noteFields) lines.push(`N:${nf.value}`);
7818
+ }
7819
+ if (score.metadata.encoding?.encoder) {
7820
+ for (const enc of score.metadata.encoding.encoder) lines.push(`Z:${enc}`);
7821
+ }
7822
+ const instrFields = score.metadata.miscellaneous?.filter((m) => m.name === "abc-I");
7823
+ if (instrFields) {
7824
+ for (const inf of instrFields) lines.push(`I:${inf.value}`);
7825
+ }
7826
+ const fileFields = score.metadata.miscellaneous?.filter((m) => m.name === "abc-F");
7827
+ if (fileFields) {
7828
+ for (const ff of fileFields) lines.push(`F:${ff.value}`);
7829
+ }
7830
+ if (attrs?.time) {
7831
+ lines.push(`M:${serializeTimeSignature(attrs.time)}`);
7832
+ }
7833
+ lines.push(`L:${unitNote.num}/${unitNote.den}`);
7834
+ const storedTempo = score.metadata.miscellaneous?.find((m) => m.name === "abc-tempo")?.value;
7835
+ if (storedTempo) {
7836
+ lines.push(`Q:${storedTempo}`);
7837
+ } else {
7838
+ const tempoStr = findTempoInMeasure(firstMeasure);
7839
+ if (tempoStr) {
7840
+ lines.push(`Q:${tempoStr}`);
7841
+ }
7842
+ }
7843
+ if (attrs?.key) {
7844
+ lines.push(`K:${serializeKey2(attrs.key)}`);
7845
+ } else {
7846
+ lines.push("K:C");
7847
+ }
7848
+ }
7849
+ const lineBreaksStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-line-breaks")?.value;
7850
+ const lineBreaks = lineBreaksStr ? JSON.parse(lineBreaksStr) : [];
7851
+ const storedVoiceIdsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-ids")?.value;
7852
+ const storedVoiceIds = storedVoiceIdsStr ? JSON.parse(storedVoiceIdsStr) : [];
7853
+ const lyricsAfterAll = score.metadata.miscellaneous?.find((m) => m.name === "abc-lyrics-after-all")?.value === "true";
7854
+ const lyricsLineCountsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-lyrics-line-counts")?.value;
7855
+ const lyricsLineCounts = lyricsLineCountsStr ? JSON.parse(lyricsLineCountsStr) : [];
7856
+ const inlineVoiceMarkersStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-inline-voice-markers")?.value;
7857
+ const inlineVoiceMarkers = inlineVoiceMarkersStr ? JSON.parse(inlineVoiceMarkersStr) : {};
7858
+ const useInlineVoiceMarkers = Object.keys(inlineVoiceMarkers).length > 0;
7859
+ if (useInlineVoiceMarkers) {
7860
+ const voiceDeclStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-declaration-lines")?.value;
7861
+ if (voiceDeclStr) {
7862
+ const voiceDeclLines = JSON.parse(voiceDeclStr);
7863
+ for (const vl of voiceDeclLines) {
7864
+ lines.push(vl);
7865
+ }
7866
+ }
7867
+ }
7868
+ const bodyCommentsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-body-comments")?.value;
7869
+ const bodyComments = bodyCommentsStr ? JSON.parse(bodyCommentsStr) : [];
7870
+ const bodyDirectivesStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-body-directives")?.value;
7871
+ const bodyDirectives = bodyDirectivesStr ? JSON.parse(bodyDirectivesStr) : [];
7872
+ const wFieldsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-w-fields")?.value;
7873
+ const wFieldsList = wFieldsStr ? JSON.parse(wFieldsStr) : [];
7874
+ const voiceInterleaveStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-interleave")?.value;
7875
+ const voiceInterleavePattern = voiceInterleaveStr ? JSON.parse(voiceInterleaveStr) : [];
7876
+ const groupBarCountsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-group-bar-counts")?.value;
7877
+ const groupBarCounts = groupBarCountsStr ? JSON.parse(groupBarCountsStr) : [];
7878
+ const bodyVoiceLinesStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-body-voice-lines")?.value;
7879
+ const bodyVoiceLines = bodyVoiceLinesStr ? JSON.parse(bodyVoiceLinesStr) : [];
7880
+ const voiceFullLinesStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-full-lines")?.value;
7881
+ const voiceFullLines = voiceFullLinesStr ? JSON.parse(voiceFullLinesStr) : {};
7882
+ const voiceCommentsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-voice-comments")?.value;
7883
+ const voiceCommentsData = voiceCommentsStr ? JSON.parse(voiceCommentsStr) : {};
7884
+ const preVoiceCommentsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-pre-voice-comments")?.value;
7885
+ const preVoiceComments = preVoiceCommentsStr ? JSON.parse(preVoiceCommentsStr) : [];
7886
+ const multiVoice = score.parts.length > 1;
7887
+ const partMeasureStrings = [];
7888
+ const partDivisions = [];
7889
+ for (let partIdx = 0; partIdx < score.parts.length; partIdx++) {
7890
+ const part = score.parts[partIdx];
7891
+ const divisions = getPartDivisions(part);
7892
+ partDivisions.push(divisions);
7893
+ const measStrings = [];
7894
+ let currentUnitNote = { ...unitNote };
7895
+ let currentKey = part.measures[0]?.attributes?.key;
7896
+ for (let mi = 0; mi < part.measures.length; mi++) {
7897
+ const measure = part.measures[mi];
7898
+ const measDivisions = measure.attributes?.divisions ?? divisions;
7899
+ let measureStr = "";
7900
+ if (mi > 0 && measure.attributes?.key) {
7901
+ const newKey = measure.attributes.key;
7902
+ if (currentKey === void 0 || newKey.fifths !== currentKey.fifths || (newKey.mode || "major") !== (currentKey.mode || "major")) {
7903
+ measureStr += "\nK:" + serializeKey2(newKey) + "\n";
7904
+ currentKey = newKey;
7905
+ }
7906
+ }
7907
+ const leftBarline = measure.barlines?.find((b) => b.location === "left");
7908
+ if (leftBarline) {
7909
+ measureStr += serializeBarline2(leftBarline);
7910
+ }
7911
+ const { noteStr, updatedUnitNote } = serializeMeasureEntries(measure, measDivisions, currentUnitNote, opts);
7912
+ if (updatedUnitNote) currentUnitNote = updatedUnitNote;
7913
+ measureStr += noteStr;
7914
+ const rightBarline = measure.barlines?.find((b) => b.location === "right");
7915
+ if (rightBarline) {
7916
+ measureStr += serializeBarline2(rightBarline);
7917
+ } else if (mi < part.measures.length - 1) {
7918
+ const nextMeasure = part.measures[mi + 1];
7919
+ const nextLeftBarline = nextMeasure?.barlines?.find((b) => b.location === "left" && (b.barStyle || b.repeat));
7920
+ if (!nextLeftBarline) {
7921
+ measureStr += "|";
7922
+ }
7923
+ } else {
7924
+ const prevMeas = mi > 0 ? part.measures[mi - 1] : null;
7925
+ const prevRightBl = prevMeas?.barlines?.find((b) => b.location === "right");
7926
+ const prevWasFinal = prevRightBl?.barStyle === "light-heavy" && !prevRightBl.repeat;
7927
+ if (!prevWasFinal) {
7928
+ measureStr += "|";
7929
+ }
7930
+ }
7931
+ measStrings.push(measureStr);
7932
+ }
7933
+ partMeasureStrings.push(measStrings);
7934
+ }
7935
+ const hasInterleave = voiceInterleavePattern.length > 0 && multiVoice;
7936
+ if (hasInterleave && !useInlineVoiceMarkers) {
7937
+ const voiceMeasureCursor = {};
7938
+ for (const voiceId of storedVoiceIds) {
7939
+ voiceMeasureCursor[voiceId] = 0;
7940
+ }
7941
+ const voiceIdToPartIdx = {};
7942
+ for (let i = 0; i < storedVoiceIds.length; i++) {
7943
+ voiceIdToPartIdx[storedVoiceIds[i]] = i;
7944
+ }
7945
+ let voiceDeclIdx = 0;
7946
+ let commentIdx = 0;
7947
+ for (let gi = 0; gi < voiceInterleavePattern.length; gi++) {
7948
+ const group = voiceInterleavePattern[gi];
7949
+ for (const voiceId of group) {
7950
+ const partIdx = voiceIdToPartIdx[voiceId];
7951
+ if (partIdx === void 0) continue;
7952
+ const measStrings = partMeasureStrings[partIdx];
7953
+ const bodyVoiceLineIdx = bodyVoiceLines.findIndex((l, idx) => {
7954
+ const m = l.match(/^V:\s*(\S+)/);
7955
+ return m && m[1] === voiceId && idx >= voiceDeclIdx;
7956
+ });
7957
+ if (bodyVoiceLineIdx >= 0) {
7958
+ if (bodyVoiceLineIdx < preVoiceComments.length) {
7959
+ for (const c of preVoiceComments[bodyVoiceLineIdx]) {
7960
+ lines.push(c);
7961
+ }
7962
+ }
7963
+ lines.push(bodyVoiceLines[bodyVoiceLineIdx]);
7964
+ voiceDeclIdx = bodyVoiceLineIdx + 1;
7965
+ } else if (voiceFullLines[voiceId]) {
7966
+ } else {
7967
+ let voiceLine = `V:${voiceId}`;
7968
+ const partClef = score.parts[partIdx]?.measures[0]?.attributes?.clef?.[0];
7969
+ if (partClef) {
7970
+ const clefName = musicXmlClefToAbc(partClef);
7971
+ if (clefName && clefName !== "treble") {
7972
+ voiceLine += ` clef=${clefName}`;
7973
+ }
7974
+ }
7975
+ lines.push(voiceLine);
7976
+ }
7977
+ const voiceIdxInGroup = group.indexOf(voiceId);
7978
+ const barCount = groupBarCounts[gi]?.[voiceIdxInGroup] ?? 0;
7979
+ const measuresInGroup = barCount > 0 ? barCount : Math.ceil((partMeasureStrings[0]?.length || 0) / voiceInterleavePattern.length);
7980
+ const startMeasure = voiceMeasureCursor[voiceId] || 0;
7981
+ const endMeasure = Math.min(startMeasure + measuresInGroup, measStrings.length);
7982
+ const voiceLineBreaksStr = score.metadata.miscellaneous?.find((m) => m.name === `abc-line-breaks-${partIdx}`)?.value;
7983
+ const voiceLineBreaks = voiceLineBreaksStr ? JSON.parse(voiceLineBreaksStr) : lineBreaks;
7984
+ const voiceLineBreakSet = new Set(voiceLineBreaks.map((v) => Math.abs(v)));
7985
+ const voiceLineContinuationSet = new Set(voiceLineBreaks.filter((v) => v < 0).map((v) => Math.abs(v)));
7986
+ const thisVoiceComments = voiceCommentsData[voiceId] || [];
7987
+ let groupMusic = "";
7988
+ for (let mi = startMeasure; mi < endMeasure; mi++) {
7989
+ for (const vc of thisVoiceComments) {
7990
+ if (vc.barIndex === mi) {
7991
+ groupMusic += vc.comment + "\n";
7992
+ }
7993
+ }
7994
+ groupMusic += measStrings[mi];
7995
+ if (mi < endMeasure - 1) {
7996
+ const absMeasureNum = mi + 1;
7997
+ if (voiceLineContinuationSet.has(absMeasureNum)) {
7998
+ groupMusic += "\\\n";
7999
+ } else if (voiceLineBreakSet.has(absMeasureNum)) {
8000
+ groupMusic += "\n";
8001
+ }
8002
+ }
8003
+ }
8004
+ voiceMeasureCursor[voiceId] = endMeasure;
8005
+ lines.push(groupMusic);
8006
+ }
8007
+ if (gi < voiceInterleavePattern.length - 1 && commentIdx < bodyComments.length) {
8008
+ lines.push(bodyComments[commentIdx]);
8009
+ commentIdx++;
8010
+ }
8011
+ }
8012
+ } else {
8013
+ for (let partIdx = 0; partIdx < score.parts.length; partIdx++) {
8014
+ const part = score.parts[partIdx];
8015
+ if (multiVoice && !useInlineVoiceMarkers) {
8016
+ const voiceId = storedVoiceIds[partIdx] || String(partIdx + 1);
8017
+ if (voiceFullLines[voiceId]) {
8018
+ lines.push(voiceFullLines[voiceId]);
8019
+ } else {
8020
+ let voiceLine = `V:${voiceId}`;
8021
+ const partClef = part.measures[0]?.attributes?.clef?.[0];
8022
+ if (partClef) {
8023
+ const clefName = musicXmlClefToAbc(partClef);
8024
+ if (clefName && clefName !== "treble") {
8025
+ voiceLine += ` clef=${clefName}`;
8026
+ }
8027
+ }
8028
+ lines.push(voiceLine);
8029
+ }
8030
+ }
8031
+ const divisions = partDivisions[partIdx];
8032
+ const bodyResult = serializePartBody(part, divisions, unitNote, opts, lineBreaks, lyricsAfterAll, lyricsLineCounts);
8033
+ let musicLine = bodyResult.music;
8034
+ if (multiVoice && useInlineVoiceMarkers) {
8035
+ const voiceId = storedVoiceIds[partIdx] || String(partIdx + 1);
8036
+ const marker = inlineVoiceMarkers[voiceId] || `[V:${voiceId}]`;
8037
+ musicLine = marker + musicLine;
8038
+ }
8039
+ lines.push(musicLine);
8040
+ if (opts.includeLyrics && bodyResult.lyrics) {
8041
+ lines.push(bodyResult.lyrics);
8042
+ }
8043
+ }
8044
+ }
8045
+ for (const directive of bodyDirectives) {
8046
+ lines.push(directive);
8047
+ }
8048
+ for (const wField of wFieldsList) {
8049
+ lines.push(wField);
8050
+ }
8051
+ const trailingCommentsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-trailing-comments")?.value;
8052
+ const trailingComments = trailingCommentsStr ? JSON.parse(trailingCommentsStr) : [];
8053
+ for (const comment of trailingComments) {
8054
+ lines.push(comment);
8055
+ }
8056
+ return lines.join("\n") + "\n";
8057
+ }
8058
+ function findTempoInMeasure(measure) {
8059
+ if (!measure) return null;
8060
+ for (const entry of measure.entries) {
8061
+ if (entry.type === "direction") {
8062
+ const tempo = serializeTempo(entry);
8063
+ if (tempo) return tempo;
8064
+ }
8065
+ }
8066
+ return null;
8067
+ }
8068
+ function getPartDivisions(part) {
8069
+ for (const measure of part.measures) {
8070
+ if (measure.attributes?.divisions) {
8071
+ return measure.attributes.divisions;
8072
+ }
8073
+ for (const entry of measure.entries) {
8074
+ if (entry.type === "attributes" && entry.attributes.divisions) {
8075
+ return entry.attributes.divisions;
8076
+ }
8077
+ }
8078
+ }
8079
+ return DIVISIONS_PER_QUARTER;
8080
+ }
8081
+ function serializePartBody(part, divisions, initialUnitNote, opts, lineBreaks = [], lyricsAfterAll = false, lyricsLineCounts = []) {
8082
+ let unitNote = { ...initialUnitNote };
8083
+ const musicParts = [];
8084
+ const allLyrics = /* @__PURE__ */ new Map();
8085
+ let currentKey = part.measures[0]?.attributes?.key;
8086
+ for (let mi = 0; mi < part.measures.length; mi++) {
8087
+ const measure = part.measures[mi];
8088
+ const measDivisions = measure.attributes?.divisions ?? divisions;
8089
+ if (mi > 0 && measure.attributes?.key) {
8090
+ const newKey = measure.attributes.key;
8091
+ if (currentKey === void 0 || newKey.fifths !== currentKey.fifths || (newKey.mode || "major") !== (currentKey.mode || "major")) {
8092
+ musicParts.push("\nK:" + serializeKey2(newKey) + "\n");
8093
+ currentKey = newKey;
8094
+ }
8095
+ }
8096
+ const leftBarline = measure.barlines?.find((b) => b.location === "left");
8097
+ if (leftBarline) {
8098
+ musicParts.push(serializeBarline2(leftBarline));
8099
+ }
8100
+ const { noteStr, lyrics, updatedUnitNote } = serializeMeasureEntries(
8101
+ measure,
8102
+ measDivisions,
8103
+ unitNote,
8104
+ opts
8105
+ );
8106
+ if (updatedUnitNote) {
8107
+ unitNote = updatedUnitNote;
8108
+ }
8109
+ musicParts.push(noteStr);
8110
+ if (lyrics.length > 0) {
8111
+ allLyrics.set(mi, lyrics);
8112
+ }
8113
+ const rightBarline = measure.barlines?.find((b) => b.location === "right");
8114
+ if (rightBarline) {
8115
+ musicParts.push(serializeBarline2(rightBarline));
8116
+ } else if (mi < part.measures.length - 1) {
8117
+ const nextMeas = part.measures[mi + 1];
8118
+ const nextLeftBar = nextMeas?.barlines?.find((b) => b.location === "left" && (b.barStyle || b.repeat));
8119
+ if (!nextLeftBar) {
8120
+ musicParts.push("|");
8121
+ }
8122
+ } else {
8123
+ const prevMeas = mi > 0 ? part.measures[mi - 1] : null;
8124
+ const prevRightBl = prevMeas?.barlines?.find((b) => b.location === "right");
8125
+ const prevWasFinal = prevRightBl?.barStyle === "light-heavy" && !prevRightBl.repeat;
8126
+ if (!prevWasFinal) {
8127
+ musicParts.push("|");
8128
+ }
8129
+ }
8130
+ if (mi < part.measures.length - 1) {
8131
+ if (lineBreaks.includes(mi + 1)) {
8132
+ musicParts.push("\n");
8133
+ } else if (lineBreaks.includes(-(mi + 1))) {
8134
+ musicParts.push("\\\n");
8135
+ }
8136
+ }
8137
+ }
8138
+ const musicStr = musicParts.join("");
8139
+ if (allLyrics.size === 0) {
8140
+ return { music: musicStr, lyrics: null };
8141
+ }
8142
+ const lineBreakSet = new Set(lineBreaks.map((v) => Math.abs(v)));
8143
+ const lyricsByLine = [];
8144
+ let lineStart = 0;
8145
+ for (let mi = 0; mi < part.measures.length; mi++) {
8146
+ if (lineBreakSet.has(mi + 1) || mi === part.measures.length - 1) {
8147
+ const syllables = [];
8148
+ for (let m = lineStart; m <= mi; m++) {
8149
+ const ls = allLyrics.get(m);
8150
+ if (ls) syllables.push(...ls);
8151
+ }
8152
+ if (syllables.length > 0) {
8153
+ lyricsByLine.push({ startMeasure: lineStart, endMeasure: mi, syllables });
8154
+ }
8155
+ lineStart = mi + 1;
8156
+ }
8157
+ }
8158
+ function formatLyrics(syllables) {
8159
+ let result = "w:";
8160
+ for (let i = 0; i < syllables.length; i++) {
8161
+ const syllable = syllables[i];
8162
+ if (i > 0) {
8163
+ const prev = syllables[i - 1];
8164
+ if (prev.endsWith("-")) {
8165
+ } else {
8166
+ result += " ";
8167
+ }
8168
+ }
8169
+ result += syllable;
8170
+ }
8171
+ return result;
8172
+ }
8173
+ if (lyricsAfterAll) {
8174
+ const allSyllables2 = [];
8175
+ const sortedMeasures2 = Array.from(allLyrics.keys()).sort((a, b) => a - b);
8176
+ for (const mi of sortedMeasures2) {
8177
+ allSyllables2.push(...allLyrics.get(mi));
8178
+ }
8179
+ if (lyricsLineCounts.length > 0) {
8180
+ const lyricsLines = [];
8181
+ let offset = 0;
8182
+ for (const count of lyricsLineCounts) {
8183
+ const chunk = allSyllables2.slice(offset, offset + count);
8184
+ if (chunk.length > 0) {
8185
+ lyricsLines.push(formatLyrics(chunk));
8186
+ }
8187
+ offset += count;
8188
+ }
8189
+ if (offset < allSyllables2.length) {
8190
+ lyricsLines.push(formatLyrics(allSyllables2.slice(offset)));
8191
+ }
8192
+ return { music: musicStr, lyrics: lyricsLines.join("\n") };
8193
+ }
8194
+ return { music: musicStr, lyrics: formatLyrics(allSyllables2) };
8195
+ }
8196
+ if (lyricsByLine.length > 0 && lineBreaks.length > 0) {
8197
+ const musicLines = musicStr.split("\n");
8198
+ const resultLines = [];
8199
+ let lyricIdx = 0;
8200
+ for (let li = 0; li < musicLines.length; li++) {
8201
+ resultLines.push(musicLines[li]);
8202
+ if (lyricIdx < lyricsByLine.length) {
8203
+ const lyricGroup = lyricsByLine[lyricIdx];
8204
+ resultLines.push(formatLyrics(lyricGroup.syllables));
8205
+ lyricIdx++;
8206
+ }
8207
+ }
8208
+ while (lyricIdx < lyricsByLine.length) {
8209
+ resultLines.push(formatLyrics(lyricsByLine[lyricIdx].syllables));
8210
+ lyricIdx++;
8211
+ }
8212
+ return { music: resultLines.join("\n"), lyrics: null };
8213
+ }
8214
+ const allSyllables = [];
8215
+ const sortedMeasures = Array.from(allLyrics.keys()).sort((a, b) => a - b);
8216
+ for (const mi of sortedMeasures) {
8217
+ allSyllables.push(...allLyrics.get(mi));
8218
+ }
8219
+ return { music: musicStr, lyrics: formatLyrics(allSyllables) };
8220
+ }
8221
+ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
8222
+ const parts = [];
8223
+ const lyrics = [];
8224
+ let currentUnitNote = { ...unitNote };
8225
+ let chordPitches = [];
8226
+ let chordDurationStr = "";
8227
+ let chordTieStr = "";
8228
+ let chordSlurStart = "";
8229
+ let chordSlurEnd = "";
8230
+ let inChord = false;
8231
+ let chordHasIndividualDurations = false;
8232
+ let tupletRemaining = 0;
8233
+ for (let ei = 0; ei < measure.entries.length; ei++) {
8234
+ const entry = measure.entries[ei];
8235
+ switch (entry.type) {
8236
+ case "note": {
8237
+ const note = entry;
8238
+ if (note.grace) {
8239
+ const graceNotes = [];
8240
+ let gi = ei;
8241
+ while (gi < measure.entries.length) {
8242
+ const ge = measure.entries[gi];
8243
+ if (ge.type === "note" && ge.grace) {
8244
+ const gs = serializeNote2(ge, divisions, currentUnitNote, false);
8245
+ graceNotes.push(gs.pitch);
8246
+ gi++;
8247
+ } else {
8248
+ break;
8249
+ }
8250
+ }
8251
+ parts.push("{" + graceNotes.join("") + "}");
8252
+ ei = gi - 1;
8253
+ break;
8254
+ }
8255
+ const serialized = serializeNote2(note, divisions, currentUnitNote, false);
8256
+ if (note.chord) {
8257
+ if (!inChord) {
8258
+ inChord = true;
8259
+ }
8260
+ let chordNoteStr = serialized.pitch;
8261
+ if (chordHasIndividualDurations) {
8262
+ chordNoteStr += serialized.duration;
8263
+ }
8264
+ if (note.tie?.type === "start" || note.ties?.some((t) => t.type === "start")) {
8265
+ chordNoteStr += "-";
8266
+ }
8267
+ chordPitches.push(chordNoteStr);
8268
+ break;
8269
+ }
8270
+ if (inChord) {
8271
+ parts.push("[" + chordPitches.join("") + "]" + chordDurationStr + chordTieStr + chordSlurEnd);
8272
+ inChord = false;
8273
+ chordHasIndividualDurations = false;
8274
+ chordPitches = [];
8275
+ chordDurationStr = "";
8276
+ chordTieStr = "";
8277
+ chordSlurStart = "";
8278
+ chordSlurEnd = "";
8279
+ }
8280
+ if (note.lyrics && note.lyrics.length > 0 && opts.includeLyrics) {
8281
+ const lyric = note.lyrics[0];
8282
+ if (lyric.text) {
8283
+ const syllabic = lyric.syllabic || "single";
8284
+ const suffix = syllabic === "begin" || syllabic === "middle" ? "-" : "";
8285
+ lyrics.push(lyric.text + suffix);
8286
+ }
8287
+ }
8288
+ let tupletPrefix = "";
8289
+ if (note.timeModification && !note.chord && !note.grace && tupletRemaining <= 0) {
8290
+ const p = note.timeModification.actualNotes;
8291
+ tupletRemaining = p;
8292
+ tupletPrefix = `(${p}`;
8293
+ }
8294
+ if (note.timeModification && !note.chord && !note.grace) {
8295
+ tupletRemaining--;
8296
+ }
8297
+ let effectiveSerialized = serialized;
8298
+ if (note.timeModification && note.pitch) {
8299
+ const baseDuration = Math.round(note.duration * note.timeModification.actualNotes / note.timeModification.normalNotes);
8300
+ const { num, den } = durationToAbcFraction(baseDuration, divisions, currentUnitNote);
8301
+ const baseDurationStr = formatAbcDuration(num, den);
8302
+ const pitchStr = serializePitch2(note.pitch, note.accidental?.value === "natural");
8303
+ let tieStr = "";
8304
+ if (note.tie?.type === "start" || note.ties?.some((t) => t.type === "start")) {
8305
+ tieStr = "-";
8306
+ }
8307
+ let slurStart = "";
8308
+ let slurEnd = "";
8309
+ if (note.notations) {
8310
+ for (const notation of note.notations) {
8311
+ if (notation.type === "slur") {
8312
+ if (notation.slurType === "start") slurStart += "(";
8313
+ if (notation.slurType === "stop") slurEnd += ")";
8314
+ }
8315
+ }
8316
+ }
8317
+ effectiveSerialized = {
8318
+ full: slurStart + pitchStr + baseDurationStr + tieStr + slurEnd,
8319
+ pitch: pitchStr,
8320
+ duration: baseDurationStr,
8321
+ slurStart,
8322
+ slurEnd,
8323
+ tieStr
8324
+ };
8325
+ }
8326
+ const nextEntry = ei + 1 < measure.entries.length ? measure.entries[ei + 1] : null;
8327
+ if (nextEntry && nextEntry.type === "note" && nextEntry.chord) {
8328
+ inChord = true;
8329
+ const hasIndividualDur = detectChordIndividualDurations(measure.entries, ei);
8330
+ chordHasIndividualDurations = hasIndividualDur;
8331
+ const hasPerNoteTies = detectChordPerNoteTies(measure.entries, ei);
8332
+ const firstNoteTie = note.tie?.type === "start" || note.ties?.some((t) => t.type === "start");
8333
+ let firstNoteStr = hasIndividualDur ? effectiveSerialized.pitch + effectiveSerialized.duration : effectiveSerialized.pitch;
8334
+ if (hasPerNoteTies && firstNoteTie) {
8335
+ firstNoteStr += "-";
8336
+ }
8337
+ chordPitches = [firstNoteStr];
8338
+ chordDurationStr = hasIndividualDur ? "" : effectiveSerialized.duration;
8339
+ chordTieStr = "";
8340
+ chordSlurStart = "";
8341
+ chordSlurEnd = "";
8342
+ if (firstNoteTie && !hasPerNoteTies) {
8343
+ chordTieStr = "-";
8344
+ }
8345
+ if (note.notations) {
8346
+ for (const notation of note.notations) {
8347
+ if (notation.type === "slur") {
8348
+ if (notation.slurType === "start") chordSlurStart = "(";
8349
+ if (notation.slurType === "stop") chordSlurEnd = ")";
8350
+ }
8351
+ }
8352
+ }
8353
+ if (chordSlurStart) {
8354
+ let insertIdx = parts.length;
8355
+ for (let pi = parts.length - 1; pi >= 0; pi--) {
8356
+ const p = parts[pi];
8357
+ if (/^[vuTM]$/.test(p) || /^![^!]+!$/.test(p) || /^"[^"]*"$/.test(p)) {
8358
+ insertIdx = pi;
8359
+ } else {
8360
+ break;
8361
+ }
8362
+ }
8363
+ if (insertIdx < parts.length) {
8364
+ parts.splice(insertIdx, 0, chordSlurStart);
8365
+ parts.push(tupletPrefix);
8366
+ break;
8367
+ }
8368
+ }
8369
+ parts.push(tupletPrefix + chordSlurStart);
8370
+ break;
8371
+ }
8372
+ if (effectiveSerialized.slurStart) {
8373
+ let insertIdx = parts.length;
8374
+ for (let pi = parts.length - 1; pi >= 0; pi--) {
8375
+ const p = parts[pi];
8376
+ if (/^[vuTM]$/.test(p) || /^![^!]+!$/.test(p) || /^"[^"]*"$/.test(p)) {
8377
+ insertIdx = pi;
8378
+ } else {
8379
+ break;
8380
+ }
8381
+ }
8382
+ if (insertIdx < parts.length) {
8383
+ parts.splice(insertIdx, 0, effectiveSerialized.slurStart);
8384
+ const noteOnly = effectiveSerialized.pitch + effectiveSerialized.duration + effectiveSerialized.tieStr + effectiveSerialized.slurEnd;
8385
+ parts.push(tupletPrefix + noteOnly);
8386
+ break;
8387
+ }
8388
+ }
8389
+ const brokenResult = detectBrokenRhythm(measure.entries, ei, note, divisions, currentUnitNote);
8390
+ if (brokenResult && !note.chord && !note.grace && !note.timeModification) {
8391
+ const baseDurStr1 = formatAbcDuration(brokenResult.baseFrac.num, brokenResult.baseFrac.den);
8392
+ const pitchStr1 = effectiveSerialized.pitch;
8393
+ const tie1 = effectiveSerialized.tieStr;
8394
+ const slurS1 = effectiveSerialized.slurStart;
8395
+ const slurE1 = effectiveSerialized.slurEnd;
8396
+ parts.push(tupletPrefix + slurS1 + pitchStr1 + baseDurStr1 + tie1 + brokenResult.marker);
8397
+ const note2 = brokenResult.nextNote;
8398
+ const pitchStr2 = serializePitch2(note2.pitch, note2.accidental?.value === "natural");
8399
+ let tieStr2 = "";
8400
+ if (note2.tie?.type === "start" || note2.ties?.some((t) => t.type === "start")) tieStr2 = "-";
8401
+ let slurEnd2 = "";
8402
+ if (note2.notations) {
8403
+ for (const notation of note2.notations) {
8404
+ if (notation.type === "slur" && notation.slurType === "stop") slurEnd2 += ")";
8405
+ }
8406
+ }
8407
+ parts.push(pitchStr2 + baseDurStr1 + tieStr2 + slurEnd2 + slurE1);
8408
+ ei = brokenResult.nextIndex;
8409
+ break;
8410
+ }
8411
+ parts.push(tupletPrefix + effectiveSerialized.full);
8412
+ break;
8413
+ }
8414
+ case "harmony": {
8415
+ if (opts.includeChordSymbols) {
8416
+ parts.push(serializeHarmony2(entry));
8417
+ }
8418
+ break;
8419
+ }
8420
+ case "direction": {
8421
+ let handledAsSpecial = false;
8422
+ for (const dt of entry.directionTypes) {
8423
+ if (dt.kind === "words") {
8424
+ if (dt.text === "__abc_line_cont__") {
8425
+ parts.push("\\\n");
8426
+ handledAsSpecial = true;
8427
+ break;
8428
+ }
8429
+ if (dt.text === "__abc_line_break__") {
8430
+ parts.push("\n");
8431
+ handledAsSpecial = true;
8432
+ break;
8433
+ }
8434
+ const lMatch = dt.text.match(/^\[L:\s*(\d+)\/(\d+)\]$/);
8435
+ if (lMatch) {
8436
+ parts.push(dt.text);
8437
+ currentUnitNote = { num: parseInt(lMatch[1], 10), den: parseInt(lMatch[2], 10) };
8438
+ handledAsSpecial = true;
8439
+ break;
8440
+ }
8441
+ const decoMatch = dt.text.match(/^!([^!]+)!$/);
8442
+ if (decoMatch) {
8443
+ parts.push(dt.text);
8444
+ handledAsSpecial = true;
8445
+ break;
8446
+ }
8447
+ if (dt.text.length === 1 && /^[vuTM]$/.test(dt.text)) {
8448
+ parts.push(dt.text);
8449
+ handledAsSpecial = true;
8450
+ break;
8451
+ }
8452
+ }
8453
+ }
8454
+ if (handledAsSpecial) break;
8455
+ if (opts.includeDynamics) {
8456
+ const dynStr = serializeDynamics(entry);
8457
+ if (dynStr) {
8458
+ parts.push(dynStr);
8459
+ }
8460
+ }
8461
+ break;
8462
+ }
8463
+ case "backup":
8464
+ parts.push(" & ");
8465
+ break;
8466
+ case "forward":
8467
+ break;
8468
+ default:
8469
+ break;
8470
+ }
8471
+ }
8472
+ if (inChord && chordPitches.length > 0) {
8473
+ parts.push("[" + chordPitches.join("") + "]" + chordDurationStr + chordTieStr + chordSlurEnd);
8474
+ }
8475
+ const unitNoteChanged = currentUnitNote.num !== unitNote.num || currentUnitNote.den !== unitNote.den;
8476
+ return { noteStr: parts.join(""), lyrics, updatedUnitNote: unitNoteChanged ? currentUnitNote : void 0 };
8477
+ }
8478
+ function detectBrokenRhythm(entries, currentIdx, currentNote, divisions, unitNote) {
8479
+ if (currentNote.chord || currentNote.grace || currentNote.rest || currentNote.timeModification) return null;
8480
+ let nextIdx = currentIdx + 1;
8481
+ while (nextIdx < entries.length && entries[nextIdx].type === "note" && entries[nextIdx].chord) {
8482
+ nextIdx++;
8483
+ }
8484
+ while (nextIdx < entries.length && entries[nextIdx].type !== "note") {
8485
+ nextIdx++;
8486
+ }
8487
+ if (nextIdx >= entries.length) return null;
8488
+ const nextEntry = entries[nextIdx];
8489
+ if (nextEntry.type !== "note") return null;
8490
+ const nextNote = nextEntry;
8491
+ if (nextNote.chord || nextNote.grace || nextNote.rest || nextNote.timeModification) return null;
8492
+ const d1 = currentNote.duration;
8493
+ const d2 = nextNote.duration;
8494
+ if (d1 <= 0 || d2 <= 0) return null;
8495
+ const sum = d1 + d2;
8496
+ if (sum % 2 !== 0) return null;
8497
+ const base = sum / 2;
8498
+ const baseFrac = durationToAbcFraction(base, divisions, unitNote);
8499
+ if (baseFrac.den > 16 || baseFrac.num > 16) return null;
8500
+ const frac1 = durationToAbcFraction(d1, divisions, unitNote);
8501
+ const frac2 = durationToAbcFraction(d2, divisions, unitNote);
8502
+ if (frac1.den === 1 && frac2.den === 1) return null;
8503
+ let marker = null;
8504
+ if (d1 * 1 === d2 * 3) {
8505
+ marker = ">";
8506
+ } else if (d1 * 3 === d2 * 1) {
8507
+ marker = "<";
8508
+ } else if (d1 * 1 === d2 * 7) {
8509
+ marker = ">>";
8510
+ } else if (d1 * 7 === d2 * 1) {
8511
+ marker = "<<";
8512
+ }
8513
+ if (!marker) return null;
8514
+ return { marker, nextNote, nextIndex: nextIdx, baseFrac };
8515
+ }
8516
+ function detectChordPerNoteTies(entries, startIdx) {
8517
+ let tiedCount = 0;
8518
+ let totalCount = 1;
8519
+ const firstNote = entries[startIdx];
8520
+ if (firstNote.type === "note") {
8521
+ const hasTieStart2 = firstNote.tie?.type === "start" || firstNote.ties?.some((t) => t.type === "start");
8522
+ if (hasTieStart2) tiedCount++;
8523
+ }
8524
+ for (let i = startIdx + 1; i < entries.length; i++) {
8525
+ const e = entries[i];
8526
+ if (e.type !== "note" || !e.chord) break;
8527
+ totalCount++;
8528
+ const hasTieStart2 = e.tie?.type === "start" || e.ties?.some((t) => t.type === "start");
8529
+ if (hasTieStart2) tiedCount++;
8530
+ }
8531
+ return tiedCount > 1 || tiedCount > 0 && totalCount > 1;
8532
+ }
8533
+ function detectChordIndividualDurations(entries, startIdx) {
8534
+ const firstNote = entries[startIdx];
8535
+ if (firstNote.type !== "note") return false;
8536
+ if (firstNote._abcIndividualChordDurations) return true;
8537
+ if (useIndividualChordDurations) return true;
8538
+ const baseDuration = firstNote.duration;
8539
+ for (let i = startIdx + 1; i < entries.length; i++) {
8540
+ const e = entries[i];
8541
+ if (e.type !== "note" || !e.chord) break;
8542
+ if (e.duration !== baseDuration) return true;
8543
+ }
8544
+ return false;
8545
+ }
8546
+ function serializeNote2(note, divisions, unitNote, _inChord) {
8547
+ let pitchStr = "";
8548
+ let durationStr = "";
8549
+ if (note.rest) {
8550
+ if (note.rest.measure) {
8551
+ pitchStr = "Z";
8552
+ durationStr = "";
8553
+ } else {
8554
+ const isInvisible = note.printObject === false;
8555
+ pitchStr = isInvisible ? "x" : "z";
8556
+ const { num, den } = durationToAbcFraction(note.duration, divisions, unitNote);
8557
+ durationStr = formatAbcDuration(num, den);
8558
+ }
8559
+ } else if (note.grace) {
8560
+ if (note.pitch) {
8561
+ pitchStr = serializePitch2(note.pitch, note.accidental?.value === "natural");
8562
+ }
8563
+ durationStr = "";
8564
+ } else if (note.pitch) {
8565
+ pitchStr = serializePitch2(note.pitch, note.accidental?.value === "natural");
8566
+ const effectiveDuration = note.duration;
8567
+ const { num, den } = durationToAbcFraction(effectiveDuration, divisions, unitNote);
8568
+ durationStr = formatAbcDuration(num, den);
8569
+ }
8570
+ let tieStr = "";
8571
+ if (note.tie?.type === "start" || note.ties?.some((t) => t.type === "start")) {
8572
+ tieStr = "-";
8573
+ }
8574
+ let slurStart = "";
8575
+ let slurEnd = "";
8576
+ if (note.notations) {
8577
+ for (const notation of note.notations) {
8578
+ if (notation.type === "slur") {
8579
+ if (notation.slurType === "start") slurStart += "(";
8580
+ if (notation.slurType === "stop") slurEnd += ")";
8581
+ }
8582
+ }
8583
+ }
8584
+ const full = slurStart + pitchStr + durationStr + tieStr + slurEnd;
8585
+ return { full, pitch: pitchStr, duration: durationStr, slurStart, slurEnd, tieStr };
8586
+ }
8587
+ function serializeBarline2(barline) {
8588
+ const hasRepeatForward = barline.repeat?.direction === "forward";
8589
+ const hasRepeatBackward = barline.repeat?.direction === "backward";
8590
+ const hasEnding = barline.ending;
8591
+ let result = "";
8592
+ if (hasRepeatForward) {
8593
+ result += "|:";
8594
+ } else if (hasRepeatBackward) {
8595
+ result += ":|";
8596
+ } else if (hasEnding && !barline.barStyle) {
8597
+ } else {
8598
+ switch (barline.barStyle) {
8599
+ case "light-light":
8600
+ result += "||";
8601
+ break;
8602
+ case "light-heavy":
8603
+ result += "|]";
8604
+ break;
8605
+ case "heavy-light":
8606
+ result += "[|";
8607
+ break;
8608
+ default:
8609
+ result += "|";
8610
+ break;
8611
+ }
8612
+ }
8613
+ if (hasEnding && hasEnding.type === "start") {
8614
+ if (result) {
8615
+ result += hasEnding.number;
8616
+ } else {
8617
+ result += `[${hasEnding.number} `;
8618
+ }
8619
+ }
8620
+ return result;
8621
+ }
8622
+
5762
8623
  // src/utils/index.ts
5763
8624
  var STEPS = ["C", "D", "E", "F", "G", "A", "B"];
5764
8625
  var STEP_SEMITONES = {
@@ -11252,6 +14113,11 @@ var removeRepeat = removeRepeatBarline;
11252
14113
  // src/file.ts
11253
14114
  import { readFile, writeFile } from "fs/promises";
11254
14115
  async function parseFile(filePath) {
14116
+ const lowerPath = filePath.toLowerCase();
14117
+ if (lowerPath.endsWith(".abc")) {
14118
+ const data2 = await readFile(filePath, "utf-8");
14119
+ return parseAbc(data2);
14120
+ }
11255
14121
  const data = await readFile(filePath);
11256
14122
  if (isCompressed(data)) {
11257
14123
  return parseCompressed(data);
@@ -11273,7 +14139,10 @@ function decodeBuffer(buffer) {
11273
14139
  }
11274
14140
  async function serializeToFile(score, filePath, options = {}) {
11275
14141
  const lowerPath = filePath.toLowerCase();
11276
- if (lowerPath.endsWith(".mxl")) {
14142
+ if (lowerPath.endsWith(".abc")) {
14143
+ const abcString = serializeAbc(score);
14144
+ await writeFile(filePath, abcString, "utf-8");
14145
+ } else if (lowerPath.endsWith(".mxl")) {
11277
14146
  const data = serializeCompressed(score, options);
11278
14147
  await writeFile(filePath, data);
11279
14148
  } else if (lowerPath.endsWith(".mid") || lowerPath.endsWith(".midi")) {
@@ -11466,6 +14335,7 @@ export {
11466
14335
  modifyTempo,
11467
14336
  moveNoteToStaff,
11468
14337
  parse,
14338
+ parseAbc,
11469
14339
  parseAuto,
11470
14340
  parseCompressed,
11471
14341
  parseFile,
@@ -11501,6 +14371,7 @@ export {
11501
14371
  removeWedge,
11502
14372
  scoresEqual,
11503
14373
  serialize,
14374
+ serializeAbc,
11504
14375
  serializeCompressed,
11505
14376
  serializeToFile,
11506
14377
  setBarline,