@goplayerjuggler/abc-tools 1.0.10 → 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 +162 -85
- package/src/parse/barline-parser.js +3 -3
- package/src/parse/getBarInfo.js +215 -32
- package/src/parse/header-parser.js +2 -2
- package/src/parse/misc-parser.js +118 -0
- package/src/parse/parser.js +399 -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
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
const { Fraction } = require("./math.js");
|
|
2
|
-
const {
|
|
3
|
-
parseAbc,
|
|
4
|
-
getMeter,
|
|
5
|
-
calculateBarDurations,
|
|
6
|
-
} = require("./parse/parser.js");
|
|
2
|
+
const { parseAbc, getMeter } = require("./parse/parser.js");
|
|
7
3
|
|
|
8
4
|
const { getBarInfo } = require("./parse/getBarInfo.js");
|
|
9
5
|
|
|
@@ -93,20 +89,38 @@ function filterHeaders(headerLines, headersToStrip) {
|
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
/**
|
|
96
|
-
* Detect if ABC notation has an anacrusis (pickup bar)
|
|
92
|
+
* Detect if ABC notation has an anacrusis (pickup bar) from parsed data
|
|
97
93
|
* @param {object} parsed - Parsed ABC data from parseAbc
|
|
98
94
|
* @returns {boolean} - True if anacrusis is present
|
|
99
95
|
*/
|
|
100
96
|
function hasAnacrucisFromParsed(parsed) {
|
|
101
|
-
const
|
|
102
|
-
|
|
97
|
+
const { bars, barLines, meter } = parsed;
|
|
98
|
+
|
|
99
|
+
if (bars.length === 0 || barLines.length === 0) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Use getBarInfo to analyse the first bar
|
|
104
|
+
const barInfo = getBarInfo(bars, barLines, meter, {
|
|
105
|
+
barNumbers: true,
|
|
106
|
+
isPartial: true,
|
|
107
|
+
cumulativeDuration: false,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Find the first bar line with a barNumber (skip initial bar line if present)
|
|
111
|
+
const firstNumberedBarLine = barInfo.barLines.find(
|
|
112
|
+
(bl) => bl.barNumber !== null
|
|
113
|
+
);
|
|
103
114
|
|
|
104
|
-
if (
|
|
115
|
+
if (!firstNumberedBarLine) {
|
|
105
116
|
return false;
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
|
|
109
|
-
return
|
|
119
|
+
// Anacrusis is present if the first numbered bar line is partial with barNumber 0
|
|
120
|
+
return (
|
|
121
|
+
firstNumberedBarLine.barNumber === 0 &&
|
|
122
|
+
firstNumberedBarLine.isPartial === true
|
|
123
|
+
);
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
/**
|
|
@@ -142,7 +156,7 @@ function hasAnacrucis(abc) {
|
|
|
142
156
|
* @param {Array<number>} smallMeter - The smaller meter signature [numerator, denominator]
|
|
143
157
|
* @param {Array<number>} largeMeter - The larger meter signature [numerator, denominator]
|
|
144
158
|
* @returns {string} ABC notation with toggled meter
|
|
145
|
-
* @throws {Error} If the current meter doesn
|
|
159
|
+
* @throws {Error} If the current meter doesn’t match either smallMeter or largeMeter
|
|
146
160
|
*/
|
|
147
161
|
function toggleMeterDoubling(abc, smallMeter, largeMeter) {
|
|
148
162
|
const currentMeter = getMeter(abc);
|
|
@@ -293,7 +307,7 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter) {
|
|
|
293
307
|
decision.action === "remove" &&
|
|
294
308
|
!decision.variantToken
|
|
295
309
|
) {
|
|
296
|
-
// Remove bar line and ensure there
|
|
310
|
+
// Remove bar line and ensure there’s a space
|
|
297
311
|
// Check if we already added a space (last char in newMusic)
|
|
298
312
|
const needsSpace =
|
|
299
313
|
newMusic.length === 0 || newMusic[newMusic.length - 1] !== " ";
|
|
@@ -363,10 +377,10 @@ function toggleMeter_6_8_to_12_8(abc) {
|
|
|
363
377
|
}
|
|
364
378
|
|
|
365
379
|
/**
|
|
366
|
-
* Get the first N complete or partial bars from ABC notation, with or without the anacrusis
|
|
380
|
+
* Get the first N complete or partial musical bars from ABC notation, with or without the anacrusis
|
|
367
381
|
* Preserves all formatting, comments, spacing, and line breaks
|
|
368
382
|
* @param {string} abc - ABC notation
|
|
369
|
-
* @param {number|Fraction} numBars - Number of bars to extract (can be fractional, e.g., 1.5 or new Fraction(3,2))
|
|
383
|
+
* @param {number|Fraction} numBars - Number of musical bars to extract (can be fractional, e.g., 1.5 or new Fraction(3,2))
|
|
370
384
|
* @param {boolean} withAnacrucis - when flagged, the returned result also includes the anacrusis - incomplete bar (default: false)
|
|
371
385
|
* @param {boolean} countAnacrucisInTotal - when true AND withAnacrucis is true, the anacrusis counts toward numBars duration (default: false)
|
|
372
386
|
* @param {object} headersToStrip - optional header stripping configuration {all:boolean, toKeep:string}
|
|
@@ -379,94 +393,160 @@ function getFirstBars(
|
|
|
379
393
|
countAnacrucisInTotal = false,
|
|
380
394
|
headersToStrip
|
|
381
395
|
) {
|
|
382
|
-
// Convert numBars to Fraction if it
|
|
396
|
+
// Convert numBars to Fraction if it’s a number
|
|
383
397
|
const numBarsFraction =
|
|
384
398
|
typeof numBars === "number" ? new Fraction(numBars) : numBars;
|
|
385
399
|
|
|
386
|
-
// Estimate maxBars needed
|
|
387
|
-
const estimatedMaxBars =
|
|
388
|
-
Math.ceil(numBarsFraction.num / numBarsFraction.den) + 2;
|
|
400
|
+
// Estimate maxBars needed for parsing
|
|
401
|
+
const estimatedMaxBars = Math.ceil(numBarsFraction.toNumber()) * 2 + 2;
|
|
389
402
|
|
|
390
|
-
// Parse
|
|
403
|
+
// Parse ABC
|
|
391
404
|
const parsed = parseAbc(abc, { maxBars: estimatedMaxBars });
|
|
392
405
|
const { bars, headerLines, barLines, musicText, meter } = parsed;
|
|
393
406
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const targetDuration = expectedBarDuration.multiply(numBarsFraction);
|
|
397
|
-
|
|
398
|
-
// Find first complete bar index
|
|
399
|
-
let firstCompleteBarIdx = -1;
|
|
400
|
-
for (let i = 0; i < bars.length; i++) {
|
|
401
|
-
const barDuration = barDurations[i];
|
|
402
|
-
if (barDuration.compare(expectedBarDuration) === 0) {
|
|
403
|
-
firstCompleteBarIdx = i;
|
|
404
|
-
break;
|
|
405
|
-
}
|
|
407
|
+
if (bars.length === 0 || barLines.length === 0) {
|
|
408
|
+
throw new Error("No bars found");
|
|
406
409
|
}
|
|
407
410
|
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
411
|
+
// Determine which bar number to stop after
|
|
412
|
+
// We need to account for fractional bars, anacrusis handling, etc.
|
|
413
|
+
const wholeBarsNeeded = Math.ceil(numBarsFraction.toNumber());
|
|
414
|
+
const stopAfterBarNumber = wholeBarsNeeded + 1; // Add buffer for safety
|
|
415
|
+
|
|
416
|
+
// Get bar info up to the bar number we need
|
|
417
|
+
const barInfo = getBarInfo(bars, barLines, meter, {
|
|
418
|
+
barNumbers: true,
|
|
419
|
+
isPartial: true,
|
|
420
|
+
cumulativeDuration: true,
|
|
421
|
+
stopAfterBarNumber,
|
|
422
|
+
});
|
|
412
423
|
|
|
413
|
-
const
|
|
424
|
+
const enrichedBarLines = barInfo.barLines;
|
|
425
|
+
|
|
426
|
+
// Detect if there’s an anacrusis
|
|
427
|
+
const firstNumberedBarLine = enrichedBarLines.find(
|
|
428
|
+
(bl) => bl.barNumber !== null
|
|
429
|
+
);
|
|
430
|
+
const hasPickup =
|
|
431
|
+
firstNumberedBarLine &&
|
|
432
|
+
firstNumberedBarLine.barNumber === 0 &&
|
|
433
|
+
firstNumberedBarLine.isPartial === true;
|
|
414
434
|
|
|
415
435
|
// Filter headers if requested
|
|
416
436
|
const filteredHeaders = filterHeaders(headerLines, headersToStrip);
|
|
417
437
|
|
|
418
|
-
//
|
|
438
|
+
// Calculate the expected duration per musical bar
|
|
439
|
+
const expectedBarDuration = new Fraction(meter[0], meter[1]);
|
|
440
|
+
const targetDuration = expectedBarDuration.multiply(numBarsFraction);
|
|
441
|
+
|
|
442
|
+
// Determine starting position and how much duration we need to accumulate
|
|
419
443
|
let startPos = 0;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
444
|
+
let remainingDurationNeeded = targetDuration.clone();
|
|
445
|
+
let countingFromBarNumber = 0;
|
|
446
|
+
|
|
447
|
+
if (hasPickup) {
|
|
448
|
+
if (withAnacrucis) {
|
|
449
|
+
// Include anacrusis in output
|
|
450
|
+
startPos = 0;
|
|
451
|
+
if (countAnacrucisInTotal) {
|
|
452
|
+
// Subtract anacrusis duration from target
|
|
453
|
+
const anacrusisBarLine = enrichedBarLines.find(
|
|
454
|
+
(bl) => bl.barNumber === 0
|
|
455
|
+
);
|
|
456
|
+
if (anacrusisBarLine && anacrusisBarLine.cumulativeDuration) {
|
|
457
|
+
const anacrusisDuration =
|
|
458
|
+
anacrusisBarLine.cumulativeDuration.sinceLastBarLine;
|
|
459
|
+
remainingDurationNeeded =
|
|
460
|
+
remainingDurationNeeded.subtract(anacrusisDuration);
|
|
461
|
+
}
|
|
462
|
+
countingFromBarNumber = 1; // Start counting from first complete bar
|
|
463
|
+
} else {
|
|
464
|
+
// Don’t count anacrusis - we want full numBars after it
|
|
465
|
+
countingFromBarNumber = 1;
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
// Skip anacrusis - start after its bar line
|
|
469
|
+
const anacrusisBarLineIdx = enrichedBarLines.findIndex(
|
|
470
|
+
(bl) => bl.barNumber === 0
|
|
471
|
+
);
|
|
472
|
+
if (anacrusisBarLineIdx >= 0) {
|
|
473
|
+
const anacrusisBarLine = enrichedBarLines[anacrusisBarLineIdx];
|
|
474
|
+
startPos = anacrusisBarLine.sourceIndex + anacrusisBarLine.sourceLength;
|
|
475
|
+
}
|
|
476
|
+
countingFromBarNumber = 1;
|
|
428
477
|
}
|
|
478
|
+
} else {
|
|
479
|
+
// No anacrusis
|
|
480
|
+
startPos = 0;
|
|
481
|
+
countingFromBarNumber = 0;
|
|
429
482
|
}
|
|
430
483
|
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
);
|
|
491
|
+
|
|
492
|
+
const needsFractionalBar = fractionalPart.compare(new Fraction(0, 1)) > 0;
|
|
493
|
+
const finalTargetBarNumber =
|
|
494
|
+
countingFromBarNumber +
|
|
495
|
+
wholeBarsInRemaining +
|
|
496
|
+
(needsFractionalBar ? 0 : -1);
|
|
497
|
+
|
|
498
|
+
// Find end position
|
|
439
499
|
let endPos = startPos;
|
|
500
|
+
let foundEnd = false;
|
|
501
|
+
|
|
502
|
+
// Find all bar lines with our target bar numbers
|
|
503
|
+
const relevantBarLines = enrichedBarLines.filter(
|
|
504
|
+
(bl) =>
|
|
505
|
+
bl.barNumber !== null &&
|
|
506
|
+
bl.barNumber >= countingFromBarNumber &&
|
|
507
|
+
bl.barNumber <= finalTargetBarNumber
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
if (!needsFractionalBar) {
|
|
511
|
+
// For whole bars, find the bar line at finalTargetBarNumber
|
|
512
|
+
const finalBarLine = relevantBarLines.find(
|
|
513
|
+
(bl) => bl.barNumber === finalTargetBarNumber
|
|
514
|
+
);
|
|
515
|
+
if (finalBarLine) {
|
|
516
|
+
endPos = finalBarLine.sourceIndex + finalBarLine.sourceLength;
|
|
517
|
+
foundEnd = true;
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
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)
|
|
440
522
|
|
|
441
|
-
|
|
442
|
-
const barDuration = barDurations[i];
|
|
443
|
-
const newAccumulated = accumulatedDuration.add(barDuration);
|
|
523
|
+
let accumulated = new Fraction(0, 1);
|
|
444
524
|
|
|
445
|
-
|
|
446
|
-
|
|
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;
|
|
447
529
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const bar = bars[i];
|
|
457
|
-
let barAccumulated = new Fraction(0, 1);
|
|
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
|
+
);
|
|
534
|
+
|
|
535
|
+
if (barLineIdx < 0) continue;
|
|
536
|
+
|
|
537
|
+
const barLine = enrichedBarLines[barLineIdx];
|
|
458
538
|
|
|
539
|
+
if (barLine.barNumber === finalTargetBarNumber) {
|
|
540
|
+
// This bar segment is part of our target bar number
|
|
459
541
|
for (const token of bar) {
|
|
460
|
-
// Skip tokens with no duration
|
|
461
542
|
if (!token.duration) {
|
|
462
543
|
continue;
|
|
463
544
|
}
|
|
464
545
|
|
|
465
|
-
|
|
546
|
+
const newAccumulated = accumulated.add(token.duration);
|
|
466
547
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// Include this note
|
|
548
|
+
if (newAccumulated.compare(fractionalPart) >= 0) {
|
|
549
|
+
// Found the position - include this token
|
|
470
550
|
endPos = token.sourceIndex + token.sourceLength;
|
|
471
551
|
|
|
472
552
|
// Skip trailing space if present
|
|
@@ -478,24 +558,21 @@ function getFirstBars(
|
|
|
478
558
|
) {
|
|
479
559
|
endPos++;
|
|
480
560
|
}
|
|
561
|
+
foundEnd = true;
|
|
481
562
|
break;
|
|
482
563
|
}
|
|
564
|
+
|
|
565
|
+
accumulated = newAccumulated;
|
|
483
566
|
}
|
|
567
|
+
|
|
568
|
+
if (foundEnd) break;
|
|
484
569
|
}
|
|
485
|
-
break;
|
|
486
570
|
}
|
|
487
|
-
|
|
488
|
-
accumulatedDuration = newAccumulated;
|
|
489
571
|
}
|
|
490
572
|
|
|
491
|
-
// if
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
// );
|
|
495
|
-
// }
|
|
496
|
-
|
|
497
|
-
if (endPos === startPos) {
|
|
498
|
-
endPos = musicText.length - 1;
|
|
573
|
+
// Fallback: if we didn’t find an end, use the last available position
|
|
574
|
+
if (!foundEnd || endPos === startPos) {
|
|
575
|
+
endPos = musicText.length;
|
|
499
576
|
}
|
|
500
577
|
|
|
501
578
|
// Reconstruct ABC
|
|
@@ -49,11 +49,11 @@ function parseBarLine(barLineStr) {
|
|
|
49
49
|
// End repeat
|
|
50
50
|
if (trimmed.match(/^:/)) {
|
|
51
51
|
result.isRepeatL = true;
|
|
52
|
-
result.isSectionBreak = true
|
|
52
|
+
result.isSectionBreak = true;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// Double bar
|
|
56
|
-
if (trimmed.match(
|
|
55
|
+
// Double bar & other cases: "...ends with one of ||, :| |] or [|"
|
|
56
|
+
if (trimmed.match(/\|\||\|]|\[\|/)) {
|
|
57
57
|
result.isSectionBreak = true;
|
|
58
58
|
}
|
|
59
59
|
return result;
|