@goplayerjuggler/abc-tools 1.0.11 → 1.0.12
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/package.json +1 -1
- package/src/incipit.js +1 -1
- package/src/index.js +5 -1
- package/src/manipulator.js +73 -70
- package/src/parse/getBarInfo.js +49 -11
- package/src/parse/header-parser.js +2 -2
- package/src/parse/misc-parser.js +118 -0
- package/src/parse/parser.js +398 -243
- package/src/parse/token-utils.js +90 -180
- package/src/sort/contour-svg.js +51 -45
- package/src/sort/get-contour.js +1 -1
package/package.json
CHANGED
package/src/incipit.js
CHANGED
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const parser = require("./parse/parser.js");
|
|
7
|
+
const miscParser = require("./parse/misc-parser.js");
|
|
7
8
|
const manipulator = require("./manipulator.js");
|
|
8
9
|
const sort = require("./sort/contour-sort.js");
|
|
9
10
|
const contourToSvg = require("./sort/contour-svg.js");
|
|
@@ -12,10 +13,12 @@ const displayContour = require("./sort/display-contour.js");
|
|
|
12
13
|
const incipit = require("./incipit.js");
|
|
13
14
|
const javascriptify = require("./javascriptify.js");
|
|
14
15
|
const getContour = require("./sort/get-contour.js");
|
|
16
|
+
const math = require("./math.js");
|
|
15
17
|
|
|
16
18
|
module.exports = {
|
|
17
19
|
// Parser functions
|
|
18
20
|
...parser,
|
|
21
|
+
...miscParser,
|
|
19
22
|
|
|
20
23
|
// Manipulator functions
|
|
21
24
|
...manipulator,
|
|
@@ -29,6 +32,7 @@ module.exports = {
|
|
|
29
32
|
// Incipit functions
|
|
30
33
|
...incipit,
|
|
31
34
|
|
|
32
|
-
//
|
|
35
|
+
// other
|
|
33
36
|
javascriptify,
|
|
37
|
+
...math,
|
|
34
38
|
};
|
package/src/manipulator.js
CHANGED
|
@@ -156,7 +156,7 @@ function hasAnacrucis(abc) {
|
|
|
156
156
|
* @param {Array<number>} smallMeter - The smaller meter signature [numerator, denominator]
|
|
157
157
|
* @param {Array<number>} largeMeter - The larger meter signature [numerator, denominator]
|
|
158
158
|
* @returns {string} ABC notation with toggled meter
|
|
159
|
-
* @throws {Error} If the current meter doesn
|
|
159
|
+
* @throws {Error} If the current meter doesn’t match either smallMeter or largeMeter
|
|
160
160
|
*/
|
|
161
161
|
function toggleMeterDoubling(abc, smallMeter, largeMeter) {
|
|
162
162
|
const currentMeter = getMeter(abc);
|
|
@@ -307,7 +307,7 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter) {
|
|
|
307
307
|
decision.action === "remove" &&
|
|
308
308
|
!decision.variantToken
|
|
309
309
|
) {
|
|
310
|
-
// Remove bar line and ensure there
|
|
310
|
+
// Remove bar line and ensure there’s a space
|
|
311
311
|
// Check if we already added a space (last char in newMusic)
|
|
312
312
|
const needsSpace =
|
|
313
313
|
newMusic.length === 0 || newMusic[newMusic.length - 1] !== " ";
|
|
@@ -393,7 +393,7 @@ function getFirstBars(
|
|
|
393
393
|
countAnacrucisInTotal = false,
|
|
394
394
|
headersToStrip
|
|
395
395
|
) {
|
|
396
|
-
// Convert numBars to Fraction if it
|
|
396
|
+
// Convert numBars to Fraction if it’s a number
|
|
397
397
|
const numBarsFraction =
|
|
398
398
|
typeof numBars === "number" ? new Fraction(numBars) : numBars;
|
|
399
399
|
|
|
@@ -423,7 +423,7 @@ function getFirstBars(
|
|
|
423
423
|
|
|
424
424
|
const enrichedBarLines = barInfo.barLines;
|
|
425
425
|
|
|
426
|
-
// Detect if there
|
|
426
|
+
// Detect if there’s an anacrusis
|
|
427
427
|
const firstNumberedBarLine = enrichedBarLines.find(
|
|
428
428
|
(bl) => bl.barNumber !== null
|
|
429
429
|
);
|
|
@@ -439,10 +439,10 @@ function getFirstBars(
|
|
|
439
439
|
const expectedBarDuration = new Fraction(meter[0], meter[1]);
|
|
440
440
|
const targetDuration = expectedBarDuration.multiply(numBarsFraction);
|
|
441
441
|
|
|
442
|
-
// Determine starting position
|
|
442
|
+
// Determine starting position and how much duration we need to accumulate
|
|
443
443
|
let startPos = 0;
|
|
444
|
-
let
|
|
445
|
-
let
|
|
444
|
+
let remainingDurationNeeded = targetDuration.clone();
|
|
445
|
+
let countingFromBarNumber = 0;
|
|
446
446
|
|
|
447
447
|
if (hasPickup) {
|
|
448
448
|
if (withAnacrucis) {
|
|
@@ -456,13 +456,13 @@ function getFirstBars(
|
|
|
456
456
|
if (anacrusisBarLine && anacrusisBarLine.cumulativeDuration) {
|
|
457
457
|
const anacrusisDuration =
|
|
458
458
|
anacrusisBarLine.cumulativeDuration.sinceLastBarLine;
|
|
459
|
-
|
|
460
|
-
|
|
459
|
+
remainingDurationNeeded =
|
|
460
|
+
remainingDurationNeeded.subtract(anacrusisDuration);
|
|
461
461
|
}
|
|
462
|
-
|
|
462
|
+
countingFromBarNumber = 1; // Start counting from first complete bar
|
|
463
463
|
} else {
|
|
464
|
-
// Don
|
|
465
|
-
|
|
464
|
+
// Don’t count anacrusis - we want full numBars after it
|
|
465
|
+
countingFromBarNumber = 1;
|
|
466
466
|
}
|
|
467
467
|
} else {
|
|
468
468
|
// Skip anacrusis - start after its bar line
|
|
@@ -473,42 +473,43 @@ function getFirstBars(
|
|
|
473
473
|
const anacrusisBarLine = enrichedBarLines[anacrusisBarLineIdx];
|
|
474
474
|
startPos = anacrusisBarLine.sourceIndex + anacrusisBarLine.sourceLength;
|
|
475
475
|
}
|
|
476
|
-
|
|
476
|
+
countingFromBarNumber = 1;
|
|
477
477
|
}
|
|
478
478
|
} else {
|
|
479
479
|
// No anacrusis
|
|
480
480
|
startPos = 0;
|
|
481
|
-
|
|
481
|
+
countingFromBarNumber = 0;
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
-
//
|
|
485
|
-
const
|
|
486
|
-
|
|
484
|
+
// Determine which bar numbers we need based on remaining duration
|
|
485
|
+
const wholeBarsInRemaining = Math.floor(
|
|
486
|
+
remainingDurationNeeded.divide(expectedBarDuration).toNumber()
|
|
487
|
+
);
|
|
488
|
+
const fractionalPart = remainingDurationNeeded.subtract(
|
|
489
|
+
expectedBarDuration.multiply(new Fraction(wholeBarsInRemaining, 1))
|
|
490
|
+
);
|
|
487
491
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
finalTargetBarNumber =
|
|
494
|
-
targetBarNumber + Math.floor(numBarsFraction.toNumber());
|
|
495
|
-
}
|
|
492
|
+
const needsFractionalBar = fractionalPart.compare(new Fraction(0, 1)) > 0;
|
|
493
|
+
const finalTargetBarNumber =
|
|
494
|
+
countingFromBarNumber +
|
|
495
|
+
wholeBarsInRemaining +
|
|
496
|
+
(needsFractionalBar ? 0 : -1);
|
|
496
497
|
|
|
497
498
|
// Find end position
|
|
498
499
|
let endPos = startPos;
|
|
499
500
|
let foundEnd = false;
|
|
500
501
|
|
|
501
|
-
//
|
|
502
|
-
const
|
|
502
|
+
// Find all bar lines with our target bar numbers
|
|
503
|
+
const relevantBarLines = enrichedBarLines.filter(
|
|
503
504
|
(bl) =>
|
|
504
505
|
bl.barNumber !== null &&
|
|
505
|
-
bl.barNumber >=
|
|
506
|
+
bl.barNumber >= countingFromBarNumber &&
|
|
506
507
|
bl.barNumber <= finalTargetBarNumber
|
|
507
508
|
);
|
|
508
509
|
|
|
509
|
-
if (
|
|
510
|
+
if (!needsFractionalBar) {
|
|
510
511
|
// For whole bars, find the bar line at finalTargetBarNumber
|
|
511
|
-
const finalBarLine =
|
|
512
|
+
const finalBarLine = relevantBarLines.find(
|
|
512
513
|
(bl) => bl.barNumber === finalTargetBarNumber
|
|
513
514
|
);
|
|
514
515
|
if (finalBarLine) {
|
|
@@ -516,58 +517,60 @@ function getFirstBars(
|
|
|
516
517
|
foundEnd = true;
|
|
517
518
|
}
|
|
518
519
|
} else {
|
|
519
|
-
// For fractional bars, we need to do duration counting within
|
|
520
|
-
|
|
521
|
-
new Fraction(Math.floor(numBarsFraction.toNumber()), 1)
|
|
522
|
-
);
|
|
523
|
-
const fractionalDuration = expectedBarDuration.multiply(fractionalPart);
|
|
520
|
+
// For fractional bars, we need to do duration counting within bars with finalTargetBarNumber
|
|
521
|
+
// Note: there may be multiple segments with the same barNumber (partial bars)
|
|
524
522
|
|
|
525
|
-
|
|
526
|
-
const finalBarIndex = bars.findIndex((bar, idx) => {
|
|
527
|
-
const correspondingBarLineIdx = enrichedBarLines.findIndex(
|
|
528
|
-
(bl) => bl.sourceIndex > (bar[0]?.sourceIndex || 0)
|
|
529
|
-
);
|
|
530
|
-
if (correspondingBarLineIdx >= 0) {
|
|
531
|
-
return (
|
|
532
|
-
enrichedBarLines[correspondingBarLineIdx].barNumber ===
|
|
533
|
-
finalTargetBarNumber
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
return false;
|
|
537
|
-
});
|
|
523
|
+
let accumulated = new Fraction(0, 1);
|
|
538
524
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
525
|
+
// Iterate through bars and accumulate duration for bars with finalTargetBarNumber
|
|
526
|
+
for (let i = 0; i < bars.length; i++) {
|
|
527
|
+
const bar = bars[i];
|
|
528
|
+
if (bar.length === 0) continue;
|
|
542
529
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
530
|
+
// Find the bar line that follows this bar to get its barNumber
|
|
531
|
+
const barLineIdx = enrichedBarLines.findIndex(
|
|
532
|
+
(bl) => bl.sourceIndex >= bar[bar.length - 1].sourceIndex
|
|
533
|
+
);
|
|
547
534
|
|
|
548
|
-
|
|
535
|
+
if (barLineIdx < 0) continue;
|
|
549
536
|
|
|
550
|
-
|
|
551
|
-
// Found the position
|
|
552
|
-
endPos = token.sourceIndex + token.sourceLength;
|
|
537
|
+
const barLine = enrichedBarLines[barLineIdx];
|
|
553
538
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
musicText[endPos] === " "
|
|
560
|
-
) {
|
|
561
|
-
endPos++;
|
|
539
|
+
if (barLine.barNumber === finalTargetBarNumber) {
|
|
540
|
+
// This bar segment is part of our target bar number
|
|
541
|
+
for (const token of bar) {
|
|
542
|
+
if (!token.duration) {
|
|
543
|
+
continue;
|
|
562
544
|
}
|
|
563
|
-
|
|
564
|
-
|
|
545
|
+
|
|
546
|
+
const newAccumulated = accumulated.add(token.duration);
|
|
547
|
+
|
|
548
|
+
if (newAccumulated.compare(fractionalPart) >= 0) {
|
|
549
|
+
// Found the position - include this token
|
|
550
|
+
endPos = token.sourceIndex + token.sourceLength;
|
|
551
|
+
|
|
552
|
+
// Skip trailing space if present
|
|
553
|
+
if (
|
|
554
|
+
token.spacing &&
|
|
555
|
+
token.spacing.whitespace &&
|
|
556
|
+
endPos < musicText.length &&
|
|
557
|
+
musicText[endPos] === " "
|
|
558
|
+
) {
|
|
559
|
+
endPos++;
|
|
560
|
+
}
|
|
561
|
+
foundEnd = true;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
accumulated = newAccumulated;
|
|
565
566
|
}
|
|
567
|
+
|
|
568
|
+
if (foundEnd) break;
|
|
566
569
|
}
|
|
567
570
|
}
|
|
568
571
|
}
|
|
569
572
|
|
|
570
|
-
// Fallback: if we didn
|
|
573
|
+
// Fallback: if we didn’t find an end, use the last available position
|
|
571
574
|
if (!foundEnd || endPos === startPos) {
|
|
572
575
|
endPos = musicText.length;
|
|
573
576
|
}
|
package/src/parse/getBarInfo.js
CHANGED
|
@@ -24,6 +24,10 @@ const { Fraction } = require("../math.js");
|
|
|
24
24
|
* All bars at the same position across different variant endings share the same barNumber
|
|
25
25
|
* but have different variantId values (0 for first ending, 1 for second, etc.).
|
|
26
26
|
*
|
|
27
|
+
* Meter and unit length changes: When a barline has newMeter or newUnitLength fields (from inline
|
|
28
|
+
* field changes), the new meter/length is applied to the following bar. The fullBarDuration is
|
|
29
|
+
* recalculated accordingly. Partial bar accumulation continues under the new meter.
|
|
30
|
+
*
|
|
27
31
|
* IMPORTANT: The music segment associated with a bar line is the segment PRECEDING the bar line.
|
|
28
32
|
* Example: In `A4|B4||c2d2|]`, the segment for `|` is `A4`, for `||` is `B4`, and for `|]` is `c2d2`.
|
|
29
33
|
*
|
|
@@ -47,7 +51,12 @@ const { Fraction } = require("../math.js");
|
|
|
47
51
|
* - After [1D2: isPartial: true, completesMusicBar: undefined (only 2 beats)
|
|
48
52
|
* - After [2DF: isPartial: true, completesMusicBar: undefined (only 2 beats)
|
|
49
53
|
*
|
|
50
|
-
*
|
|
54
|
+
* Example 4: `C4|[M:3/4]D3|E3|` (M:4/4 → 3/4, L:1/4)
|
|
55
|
+
* - After C4: barNumber: 0 (complete 4/4 bar)
|
|
56
|
+
* - After D3: barNumber: 1 (complete 3/4 bar under new meter)
|
|
57
|
+
* - After E3: barNumber: 2 (complete 3/4 bar)
|
|
58
|
+
*
|
|
59
|
+
* Not handled: Change of meter or unit length mid-bar (inline M: or L: fields within a bar).
|
|
51
60
|
*
|
|
52
61
|
* @param {Array<Array<Object>>} bars - Array of bar arrays from parseAbc
|
|
53
62
|
* @param {Array<Object>} barLines - Array of barLine objects from parseAbc
|
|
@@ -72,13 +81,10 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
72
81
|
if (divideBarsBy !== null && divideBarsBy !== 2) {
|
|
73
82
|
throw new Error("divideBarsBy currently only supports value 2");
|
|
74
83
|
}
|
|
75
|
-
if (!barLines || barLines.length < bars.length) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
"currently not handling bars without a bar line at the end"
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
// Track current meter (can change via inline fields)
|
|
86
|
+
let currentMeter = [...meter];
|
|
87
|
+
let fullBarDuration = new Fraction(currentMeter[0], currentMeter[1]);
|
|
82
88
|
const midpoints = [];
|
|
83
89
|
|
|
84
90
|
let currentBarNumber = 0;
|
|
@@ -112,6 +118,19 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
112
118
|
const bar = bars[barIdx];
|
|
113
119
|
const barLineIdx = barIdx + barLineOffset;
|
|
114
120
|
|
|
121
|
+
// Check if the previous barline had a meter or unit length change
|
|
122
|
+
if (barLineIdx > 0) {
|
|
123
|
+
const prevBarLine = barLines[barLineIdx - 1];
|
|
124
|
+
|
|
125
|
+
if (prevBarLine.newMeter) {
|
|
126
|
+
currentMeter = [...prevBarLine.newMeter];
|
|
127
|
+
fullBarDuration = new Fraction(currentMeter[0], currentMeter[1]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Note: newUnitLength doesn't directly affect fullBarDuration
|
|
131
|
+
// (token durations are already calculated in the correct units)
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
// Calculate duration of this bar segment
|
|
116
135
|
// Process tokens and handle variant endings during traversal
|
|
117
136
|
let barDuration = new Fraction(0, 1);
|
|
@@ -130,15 +149,16 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
130
149
|
if (!inVariantGroup) {
|
|
131
150
|
// Starting a new variant group - store state including duration accumulated up to this point
|
|
132
151
|
// Add current barDuration to get the state before this variant token
|
|
133
|
-
const durationBeforeVariant =
|
|
134
|
-
|
|
135
|
-
);
|
|
152
|
+
const durationBeforeVariant =
|
|
153
|
+
durationSinceLastComplete.add(barDuration);
|
|
136
154
|
|
|
137
155
|
inVariantGroup = true;
|
|
138
156
|
variantBranchPoint = {
|
|
139
157
|
barNumber: currentBarNumber,
|
|
140
158
|
durationSinceLastComplete: durationBeforeVariant.clone(),
|
|
141
159
|
lastCompleteBarLineIdx: lastCompleteBarLineIdx,
|
|
160
|
+
meter: [...currentMeter],
|
|
161
|
+
fullBarDuration: fullBarDuration.clone(),
|
|
142
162
|
};
|
|
143
163
|
variantCounter = 0;
|
|
144
164
|
currentVariantId = variantCounter;
|
|
@@ -151,6 +171,8 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
151
171
|
durationSinceLastComplete =
|
|
152
172
|
variantBranchPoint.durationSinceLastComplete.clone();
|
|
153
173
|
lastCompleteBarLineIdx = variantBranchPoint.lastCompleteBarLineIdx;
|
|
174
|
+
currentMeter = [...variantBranchPoint.meter];
|
|
175
|
+
fullBarDuration = variantBranchPoint.fullBarDuration.clone();
|
|
154
176
|
// Discard barDuration accumulated so far (it's from the wrong variant path)
|
|
155
177
|
barDuration = token.duration
|
|
156
178
|
? token.duration.clone()
|
|
@@ -291,9 +313,25 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
291
313
|
|
|
292
314
|
// Calculate midpoints if requested
|
|
293
315
|
if (divideBarsBy === 2) {
|
|
294
|
-
|
|
316
|
+
// Track meter for midpoint calculation
|
|
317
|
+
let midpointMeter = [...meter];
|
|
318
|
+
let halfBarDuration = new Fraction(
|
|
319
|
+
midpointMeter[0],
|
|
320
|
+
midpointMeter[1]
|
|
321
|
+
).divide(new Fraction(2, 1));
|
|
295
322
|
|
|
296
323
|
for (let barIdx = 0; barIdx < bars.length; barIdx++) {
|
|
324
|
+
const barLineIdx = barIdx + barLineOffset;
|
|
325
|
+
|
|
326
|
+
// Update meter if previous barline had a change
|
|
327
|
+
if (barLineIdx > 0 && barLines[barLineIdx - 1].newMeter) {
|
|
328
|
+
midpointMeter = [...barLines[barLineIdx - 1].newMeter];
|
|
329
|
+
halfBarDuration = new Fraction(
|
|
330
|
+
midpointMeter[0],
|
|
331
|
+
midpointMeter[1]
|
|
332
|
+
).divide(new Fraction(2, 1));
|
|
333
|
+
}
|
|
334
|
+
|
|
297
335
|
const bar = bars[barIdx];
|
|
298
336
|
let accumulated = new Fraction(0, 1);
|
|
299
337
|
|
|
@@ -28,10 +28,10 @@ function getTonalBase(abc) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Extract key signature from ABC header
|
|
31
|
+
* Extract full key signature from ABC header
|
|
32
32
|
*
|
|
33
33
|
* @param {string} abc - ABC notation string
|
|
34
|
-
* @returns {string} -
|
|
34
|
+
* @returns {string} - Key (e.g., 'C dorian', 'D mixo', 'G')
|
|
35
35
|
* @throws {Error} - If no key signature found
|
|
36
36
|
*/
|
|
37
37
|
function getKey(abc) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze whitespace and back quotes after a token
|
|
3
|
+
* Returns object describing the spacing/beaming context
|
|
4
|
+
* Back quotes (`) are ignored for beaming but preserved for reconstruction
|
|
5
|
+
*
|
|
6
|
+
* @param {string} segment - The music segment to analyze
|
|
7
|
+
* @param {number} tokenEndPos - Position where the token ends
|
|
8
|
+
* @returns {object} - Spacing analysis object
|
|
9
|
+
*
|
|
10
|
+
* Return object structure:
|
|
11
|
+
* {
|
|
12
|
+
* whitespace: string, // Actual whitespace characters (back quotes removed)
|
|
13
|
+
* backquotes: number, // Number of ` characters for reconstruction
|
|
14
|
+
* beamBreak: boolean, // True if beam should break (multiple spaces/newline)
|
|
15
|
+
* lineBreak: boolean // True if there was a newline after this token
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
function analyzeSpacing(segment, tokenEndPos) {
|
|
19
|
+
if (tokenEndPos >= segment.length) {
|
|
20
|
+
return {
|
|
21
|
+
whitespace: "",
|
|
22
|
+
backquotes: 0,
|
|
23
|
+
beamBreak: false,
|
|
24
|
+
lineBreak: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const remaining = segment.substring(tokenEndPos);
|
|
29
|
+
|
|
30
|
+
// Match whitespace and/or back quotes
|
|
31
|
+
const spacingMatch = remaining.match(/^([\s`]+)/);
|
|
32
|
+
|
|
33
|
+
if (!spacingMatch) {
|
|
34
|
+
return {
|
|
35
|
+
whitespace: "",
|
|
36
|
+
backquotes: 0,
|
|
37
|
+
beamBreak: false,
|
|
38
|
+
lineBreak: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fullSpacing = spacingMatch[1];
|
|
43
|
+
|
|
44
|
+
// Count back quotes
|
|
45
|
+
const backquotes = (fullSpacing.match(/`/g) || []).length;
|
|
46
|
+
|
|
47
|
+
// Extract just whitespace (no back quotes)
|
|
48
|
+
const whitespace = fullSpacing.replace(/`/g, "");
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
whitespace,
|
|
52
|
+
backquotes,
|
|
53
|
+
beamBreak: whitespace.length > 1 || whitespace.includes("\n"), // Multiple spaces or newline breaks beam
|
|
54
|
+
lineBreak: whitespace.includes("\n"),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse tuplet notation from a token
|
|
60
|
+
*
|
|
61
|
+
* @param {string} token - Tuplet token (e.g., '(3', '(3:2', '(3:2:4')
|
|
62
|
+
* @param {boolean} isCompoundTimeSignature - Whether current time signature is compound (affects default q value)
|
|
63
|
+
* @returns {object|null} - { isTuple: true, p, q, r } or null if not a valid tuplet
|
|
64
|
+
*/
|
|
65
|
+
function parseTuplet(token, isCompoundTimeSignature) {
|
|
66
|
+
const tupleMatch = token.match(/^\(([2-9])(?::(\d)?)?(?::(\d)?)?$/);
|
|
67
|
+
if (tupleMatch) {
|
|
68
|
+
const pqr = {
|
|
69
|
+
p: parseInt(tupleMatch[1]),
|
|
70
|
+
q: tupleMatch[2],
|
|
71
|
+
r: tupleMatch[3],
|
|
72
|
+
};
|
|
73
|
+
const { p } = pqr;
|
|
74
|
+
let { q, r } = pqr;
|
|
75
|
+
if (q) {
|
|
76
|
+
q = parseInt(q);
|
|
77
|
+
} else {
|
|
78
|
+
switch (p) {
|
|
79
|
+
case 2:
|
|
80
|
+
q = 3;
|
|
81
|
+
break;
|
|
82
|
+
case 3:
|
|
83
|
+
q = 2;
|
|
84
|
+
break;
|
|
85
|
+
case 4:
|
|
86
|
+
q = 3;
|
|
87
|
+
break;
|
|
88
|
+
case 5:
|
|
89
|
+
case 7:
|
|
90
|
+
case 9:
|
|
91
|
+
q = isCompoundTimeSignature ? 3 : 2;
|
|
92
|
+
break;
|
|
93
|
+
case 6:
|
|
94
|
+
q = 2;
|
|
95
|
+
break;
|
|
96
|
+
case 8:
|
|
97
|
+
q = 3;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (r) {
|
|
102
|
+
r = parseInt(r);
|
|
103
|
+
} else {
|
|
104
|
+
r = p;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
isTuple: true,
|
|
108
|
+
p,
|
|
109
|
+
q,
|
|
110
|
+
r,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
module.exports = {
|
|
116
|
+
analyzeSpacing,
|
|
117
|
+
parseTuplet,
|
|
118
|
+
};
|