@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goplayerjuggler/abc-tools",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "sorting algorithm and implementation for ABC tunes; plus other tools for parsing and manipulating ABC tunes",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/incipit.js CHANGED
@@ -308,7 +308,7 @@ function getIncipit(data) {
308
308
 
309
309
  function getIncipitForContourGeneration(
310
310
  abc,
311
- { numBars = new Fraction(3, 2) } = {}
311
+ { numBars = new Fraction(2, 1) } = {}
312
312
  ) {
313
313
  return getIncipit({
314
314
  abc,
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
  };
@@ -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 barDurations = calculateBarDurations(parsed);
102
- const expectedBarDuration = new Fraction(parsed.meter[0], parsed.meter[1]);
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 (parsed.bars.length === 0) {
115
+ if (!firstNumberedBarLine) {
105
116
  return false;
106
117
  }
107
118
 
108
- const firstBarDuration = barDurations[0];
109
- return firstBarDuration.compare(expectedBarDuration) < 0;
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't match either smallMeter or largeMeter
159
+ * @throws {Error} If the current meter doesnt 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's a space
310
+ // Remove bar line and ensure theres 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's a number
396
+ // Convert numBars to Fraction if its a number
383
397
  const numBarsFraction =
384
398
  typeof numBars === "number" ? new Fraction(numBars) : numBars;
385
399
 
386
- // Estimate maxBars needed - simple ceiling with buffer
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 with estimated maxBars
403
+ // Parse ABC
391
404
  const parsed = parseAbc(abc, { maxBars: estimatedMaxBars });
392
405
  const { bars, headerLines, barLines, musicText, meter } = parsed;
393
406
 
394
- const barDurations = calculateBarDurations(parsed);
395
- const expectedBarDuration = new Fraction(meter[0], meter[1]);
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
- //todo
409
- if (firstCompleteBarIdx === -1) {
410
- throw new Error("No complete bars found");
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 hasPickup = firstCompleteBarIdx > 0;
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
- // Determine starting position in the music text
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
- if (hasPickup && withAnacrucis) {
421
- // Include anacrusis in output
422
- startPos = 0;
423
- } else if (hasPickup && !withAnacrucis) {
424
- // Skip anacrusis - start after its bar line
425
- const anacrusisBarLine = barLines[firstCompleteBarIdx - 1];
426
- if (anacrusisBarLine) {
427
- startPos = anacrusisBarLine.sourceIndex + anacrusisBarLine.sourceLength;
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
- // Calculate accumulated duration for target calculation
432
- let accumulatedDuration = new Fraction(0, 1);
433
- if (hasPickup && withAnacrucis && countAnacrucisInTotal) {
434
- // Count anacrusis toward target
435
- accumulatedDuration = barDurations[0];
436
- }
437
-
438
- // Find the end position by accumulating bar durations from first complete bar
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
- for (let i = firstCompleteBarIdx; i < bars.length; i++) {
442
- const barDuration = barDurations[i];
443
- const newAccumulated = accumulatedDuration.add(barDuration);
523
+ let accumulated = new Fraction(0, 1);
444
524
 
445
- if (newAccumulated.compare(targetDuration) >= 0) {
446
- // We've reached or exceeded target
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
- if (newAccumulated.compare(targetDuration) === 0) {
449
- // Exact match - include full bar with its bar line
450
- if (i < barLines.length) {
451
- endPos = barLines[i].sourceIndex + barLines[i].sourceLength;
452
- }
453
- } else {
454
- // Need partial bar
455
- const remainingDuration = targetDuration.subtract(accumulatedDuration);
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
- barAccumulated = barAccumulated.add(token.duration);
546
+ const newAccumulated = accumulated.add(token.duration);
466
547
 
467
- // Check if we've reached or exceeded the remaining duration
468
- if (barAccumulated.compare(remainingDuration) >= 0) {
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 (endPos === startPos) {
492
- // throw new Error(
493
- // `Not enough bars to satisfy request. Requested ${numBars} bars.`
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;