@goplayerjuggler/abc-tools 1.0.13 → 1.0.14

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.13",
3
+ "version": "1.0.14",
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/index.js CHANGED
@@ -15,12 +15,15 @@ const javascriptify = require("./javascriptify.js");
15
15
  const getContour = require("./sort/get-contour.js");
16
16
  const math = require("./math.js");
17
17
  const { getBarInfo } = require("./parse/getBarInfo.js");
18
+ const { getMetadata } = require("./parse/getMetadata.js");
18
19
 
19
20
  module.exports = {
20
21
  // Parser functions
21
22
  ...parser,
22
23
  ...miscParser,
23
24
  getBarInfo,
25
+ getMetadata,
26
+ ...incipit,
24
27
 
25
28
  // Manipulator functions
26
29
  ...manipulator,
@@ -31,8 +34,6 @@ module.exports = {
31
34
  ...contourToSvg,
32
35
  ...getContour,
33
36
 
34
- // Incipit functions
35
- ...incipit,
36
37
 
37
38
  // other
38
39
  javascriptify,
@@ -1,5 +1,5 @@
1
1
  const { Fraction } = require("./math.js");
2
- const { parseAbc, getMeter } = require("./parse/parser.js");
2
+ const { parseAbc, getMeter, getUnitLength } = require("./parse/parser.js");
3
3
 
4
4
  const { getBarInfo } = require("./parse/getBarInfo.js");
5
5
 
@@ -89,28 +89,31 @@ function filterHeaders(headerLines, headersToStrip) {
89
89
  }
90
90
 
91
91
  /**
92
- * Detect if ABC notation has an anacrusis (pickup bar) from parsed data
92
+ * Detect if ABC notation has an anacrusis (pickup bar) from parsed data or from barLines array
93
93
  * @param {object} parsed - Parsed ABC data from parseAbc
94
+ * @param {Array<object>} barLines - enriched barLine array from getBarInfo
94
95
  * @returns {boolean} - True if anacrusis is present
95
96
  */
96
- function hasAnacrucisFromParsed(parsed) {
97
- const { bars, barLines, meter } = parsed;
97
+ function hasAnacrucisFromParsed(parsed, barLines) {
98
+ const callParse = !barLines;
99
+ if (callParse) {
100
+ const { bars, meter } = parsed;
101
+ if (bars.length === 0) return false;
102
+ ({ barLines } = parsed);
103
+ // Use getBarInfo to analyse the first bar
104
+ getBarInfo(bars, barLines, meter, {
105
+ barNumbers: true,
106
+ isPartial: true,
107
+ cumulativeDuration: false,
108
+ });
109
+ }
98
110
 
99
- if (bars.length === 0 || barLines.length === 0) {
111
+ if (barLines.length === 0) {
100
112
  return false;
101
113
  }
102
114
 
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
115
  // 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
- );
116
+ const firstNumberedBarLine = barLines.find((bl) => bl.barNumber !== null);
114
117
 
115
118
  if (!firstNumberedBarLine) {
116
119
  return false;
@@ -155,12 +158,12 @@ function hasAnacrucis(abc) {
155
158
  * @param {string} abc - ABC notation string
156
159
  * @param {Array<number>} smallMeter - The smaller meter signature [numerator, denominator]
157
160
  * @param {Array<number>} largeMeter - The larger meter signature [numerator, denominator]
161
+ * @param {Array<number>} currentMeter - The current meter signature [numerator, denominator] of the abc tune - may be omitted. (If omitted, it gets fetched from `abc`)
158
162
  * @returns {string} ABC notation with toggled meter
159
163
  * @throws {Error} If the current meter doesn’t match either smallMeter or largeMeter
160
164
  */
161
-
162
- function toggleMeterDoubling(abc, smallMeter, largeMeter) {
163
- const currentMeter = getMeter(abc);
165
+ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
166
+ if (!currentMeter) currentMeter = getMeter(abc);
164
167
 
165
168
  const isSmall =
166
169
  currentMeter[0] === smallMeter[0] && currentMeter[1] === smallMeter[1];
@@ -175,6 +178,10 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter) {
175
178
 
176
179
  const parsed = parseAbc(abc);
177
180
  const { headerLines, barLines, musicText, bars, meter } = parsed;
181
+ // throw if there's a change of meter or unit length in the tune
182
+ if (barLines.find((bl) => bl.newMeter || bl.newUnitLength)) {
183
+ throw new Error("change of meter or unit length not handled");
184
+ }
178
185
 
179
186
  // Change meter in headers
180
187
  const newHeaders = headerLines.map((line) => {
@@ -206,22 +213,28 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter) {
206
213
  const barLineDecisions = new Map();
207
214
  const barLinesToConvert = new Map(); // variant markers to convert from |N to [N
208
215
 
209
- // Map bar numbers to their sequential index among complete bars
210
- // This handles cases where bar numbers skip (due to anacrusis or partials)
211
- const completeBarIndexByNumber = new Map();
212
- let completeBarIndex = 0;
213
-
214
- for (let i = 0; i < barLines.length; i++) {
215
- const barLine = barLines[i];
216
- if (barLine.barNumber !== null && !barLine.isSectionBreak) {
217
- const isCompleteMusicBar =
218
- !barLine.isPartial || barLine.completesMusicBar === true;
219
- if (isCompleteMusicBar) {
220
- completeBarIndexByNumber.set(barLine.barNumber, completeBarIndex);
221
- completeBarIndex++;
222
- }
223
- }
224
- }
216
+ //Discarded
217
+ // // Map bar numbers to their sequential index among complete bars
218
+ // // This handles cases where bar numbers skip (due to anacrusis or partials)
219
+ // const completeBarIndexByNumber = new Map();
220
+ // let completeBarIndex = 0;
221
+
222
+ // for (let i = 0; i < barLines.length; i++) {
223
+ // const barLine = barLines[i];
224
+ // if (
225
+ // barLine.barNumber !== null
226
+ // //&& !barLine.isSectionBreak
227
+ // ) {
228
+ // const isCompleteMusicBar =
229
+ // !barLine.isPartial || barLine.completesMusicBar === true;
230
+ // if (isCompleteMusicBar) {
231
+ // completeBarIndexByNumber.set(barLine.barNumber, completeBarIndex);
232
+ // completeBarIndex++;
233
+ // }
234
+ // }
235
+ // }
236
+
237
+ const hasAnacrucis = hasAnacrucisFromParsed(null, barLines);
225
238
 
226
239
  for (let i = 0; i < barLines.length; i++) {
227
240
  const barLine = barLines[i];
@@ -262,11 +275,17 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter) {
262
275
  continue;
263
276
  }
264
277
 
265
- // This is a complete bar - use its index to decide
266
- // Remove even-indexed complete bars (0th, 2nd, 4th...), keep odd-indexed (1st, 3rd, 5th...)
267
- const index = completeBarIndexByNumber.get(barLine.barNumber);
268
- if (index !== undefined && index % 2 === 0) {
269
- // This is 0th, 2nd, 4th... complete bar - remove it
278
+ // This is a complete bar - use its barNumber to decide
279
+ // Without anacrucis: Remove complete bars with even barNumber (0, 2, 4, ...), keep odd ones (1, 3, 5, ...)
280
+ // With anacrucis: the other way round!
281
+
282
+ //Discarded
283
+ // const index = completeBarIndexByNumber.get(barLine.barNumber);
284
+ // if (index !== undefined && index % 2 === 0) {
285
+ const remove = hasAnacrucis
286
+ ? barLine.barNumber % 2 !== 0
287
+ : barLine.barNumber % 2 === 0;
288
+ if (remove) {
270
289
  const nextBarIdx = i + 1;
271
290
  if (nextBarIdx < bars.length && barStartsWithVariant.has(nextBarIdx)) {
272
291
  const variantToken = barStartsWithVariant.get(nextBarIdx);
@@ -358,8 +377,84 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter) {
358
377
  * This is a true inverse operation - going there and back preserves the ABC exactly
359
378
  * Handles anacrusis correctly and preserves line breaks
360
379
  */
361
- function toggleMeter_4_4_to_4_2(abc) {
362
- return toggleMeterDoubling(abc, [4, 4], [4, 2]);
380
+ function toggleMeter_4_4_to_4_2(abc, currentMeter) {
381
+ return toggleMeterDoubling(abc, [4, 4], [4, 2], currentMeter);
382
+ }
383
+
384
+ const defaultCommentForReelConversion =
385
+ "*abc-tools: convert reel to M:4/4 & L:1/16*";
386
+ /**
387
+ * Adjusts bar lengths and L field to convert a
388
+ * reel written in the normal way (M:4/4 L:1/8) to the same reel
389
+ * written with M:4/4 L:1/16.
390
+ * Bars are twice as long, and the quick notes are semiquavers
391
+ * rather than quavers.
392
+ * @param {string} reel
393
+ * @param {string} comment - when non falsey, the comment will be injected as an N: header
394
+ * @param {bool} withSemiquavers - when unflagged, the L stays at 1/8, and the M is 4/2
395
+
396
+ * @returns
397
+ */
398
+ function convertStandardReel(
399
+ reel,
400
+ comment = defaultCommentForReelConversion,
401
+ withSemiquavers = true
402
+ ) {
403
+ const meter = getMeter(reel);
404
+ if (!Array.isArray(meter) || !meter || !meter[0] === 4 || !meter[1] === 4) {
405
+ throw new Error("invalid meter");
406
+ }
407
+ const unitLength = getUnitLength(reel);
408
+ if (unitLength.den !== 8) {
409
+ throw new Error("invalid L header");
410
+ }
411
+
412
+ let result = //toggleMeter_4_4_to_4_2(reel, meter);
413
+ toggleMeterDoubling(reel, [4, 4], [4, 2], meter);
414
+ if (comment) {
415
+ result = result.replace(/(\nK:)/, `\nN:${comment}$1`);
416
+ }
417
+ if (withSemiquavers) {
418
+ result = result.replace("M:4/2", "M:4/4").replace("L:1/8", "L:1/16");
419
+ }
420
+ return result;
421
+ }
422
+
423
+ /**
424
+ * Adjusts bar lengths and L field to convert a
425
+ * reel written in the abnormal way (M:4/4 L:1/16) to the same reel
426
+ * written with M:4/4 L:1/8, the normal or standard way.
427
+ * Bars are half as long, and the quick notes are quavers
428
+ * rather than semiquavers. Inverse operation to convertStandardReel
429
+ * @param {string} reel
430
+ * @param {string} comment - when non falsey, the comment (as an N:) will removed from the header
431
+ * @param {bool} withSemiquavers - when unflagged, the original reel was written in M:4/2 L:1/8
432
+ * @returns
433
+ */
434
+ function convertToStandardReel(
435
+ reel,
436
+ comment = defaultCommentForReelConversion,
437
+ withSemiquavers = true
438
+ ) {
439
+ if (withSemiquavers) {
440
+ reel = reel.replace("M:4/4", "M:4/2").reel("L:1/16", "L:1/8");
441
+ }
442
+
443
+ const unitLength = getUnitLength(reel);
444
+ if (unitLength.den !== 8) {
445
+ throw new Error("invalid L header");
446
+ }
447
+ const meter = getMeter(reel);
448
+ if (!Array.isArray(meter) || !meter || !meter[0] === 4 || !meter[1] === 4) {
449
+ throw new Error("invalid meter");
450
+ }
451
+
452
+ let result = // toggleMeter_4_4_to_4_2(reel, meter);
453
+ toggleMeterDoubling(reel, [4, 4], [4, 2], meter);
454
+ if (comment) {
455
+ result = result.replace(`\nN:${comment}`, "");
456
+ }
457
+ return result;
363
458
  }
364
459
 
365
460
  /**
@@ -578,10 +673,14 @@ function getFirstBars(
578
673
  }
579
674
 
580
675
  module.exports = {
676
+ convertStandardReel,
677
+ convertToStandardReel,
678
+ defaultCommentForReelConversion,
679
+ filterHeaders,
581
680
  getFirstBars,
582
681
  hasAnacrucis,
682
+ normaliseKey,
583
683
  toggleMeter_4_4_to_4_2,
584
684
  toggleMeter_6_8_to_12_8,
585
- filterHeaders,
586
- normaliseKey,
685
+ toggleMeterDoubling,
587
686
  };
@@ -1,10 +1,24 @@
1
1
  const { Fraction } = require("../math.js");
2
2
 
3
+ /**
4
+ * Enriches skipped barLines with information from the preceding barLine
5
+ *
6
+ * When consecutive barLines occur with no notes between them (e.g., `:|` followed by `|:`),
7
+ * the intermediate barLines are skipped during processing. This function copies relevant
8
+ * properties (barNumber, isPartial, cumulativeDuration, etc.) from the preceding barLine
9
+ * to ensure skipped barLines have complete information.
10
+ *
11
+ * Properties in the skipped barLine (like text, sourceIndex, isSectionBreak) are preserved,
12
+ * while missing properties are filled in from the preceding barLine.
13
+ *
14
+ * @param {Array<Object>} barLines - Array of barLine objects
15
+ * @param {Array<number>} skippedBarLineIndexes - Indices of barLines that were skipped
16
+ */
3
17
  function processSkippedBarLines(barLines, skippedBarLineIndexes) {
4
18
  for (let i = 0; i < skippedBarLineIndexes.length; i++) {
5
19
  const skippedIndex = skippedBarLineIndexes[i];
6
- if (skippedIndex === 0) continue; //don't see how this can happen, but seems best to check
7
- //copy any properties in the preceding barLine not in the skipped barLine over to the skipped barLine
20
+ if (skippedIndex === 0) continue; // Initial barLine can’t inherit from predecessor
21
+ // Copy any properties from the preceding barLine not already in the skipped barLine
8
22
  barLines[skippedIndex] = {
9
23
  ...barLines[skippedIndex - 1],
10
24
  ...barLines[skippedIndex],
@@ -0,0 +1,50 @@
1
+ const { normaliseKey } = require("../manipulator");
2
+
3
+ /**
4
+ * Extracts data in the ABC header T R C M K S F D N fields
5
+ * and returns it in a object with properties: title, rhythm, composer, meter, key,
6
+ * source, url, recording, and comments.
7
+ * Minimal parsing, but a few features:
8
+ * - only extracts the first T title; subsequent T entries are ignored
9
+ * - the key is normalised, so C, Cmaj, C maj, C major will all map to key:"C major"
10
+ * - the comments go in an array, with one array entry per N: line.
11
+ * @param {*} abc
12
+ * @returns {object} - The header info
13
+ */
14
+ function getMetadata(abc) {
15
+ const lines = abc.split("\n"),
16
+ metadata = {},
17
+ comments = [];
18
+
19
+ for (const line of lines) {
20
+ const trimmed = line.trim();
21
+ if (trimmed.startsWith("T:") && !metadata.title) {
22
+ metadata.title = trimmed.substring(2).trim();
23
+ } else if (trimmed.startsWith("R:")) {
24
+ metadata.rhythm = trimmed.substring(2).trim().toLowerCase();
25
+ } else if (trimmed.startsWith("C:")) {
26
+ metadata.composer = trimmed.substring(2).trim().toLowerCase();
27
+ } else if (trimmed.startsWith("M:")) {
28
+ metadata.meter = trimmed.substring(2).trim();
29
+ } else if (trimmed.startsWith("K:")) {
30
+ metadata.key = normaliseKey(trimmed.substring(2).trim()).join(" ");
31
+ // metadata.indexOfKey = i
32
+ break;
33
+ } else if (trimmed.startsWith("S:")) {
34
+ metadata.source = trimmed.substring(2).trim();
35
+ } else if (trimmed.startsWith("F:")) {
36
+ metadata.url = trimmed.substring(2).trim();
37
+ } else if (trimmed.startsWith("D:")) {
38
+ metadata.recording = trimmed.substring(2).trim();
39
+ } else if (trimmed.startsWith("N:")) {
40
+ comments.push(trimmed.substring(2).trim());
41
+ }
42
+ }
43
+ if (comments.length > 0) {
44
+ metadata.comments = comments;
45
+ }
46
+
47
+ return metadata;
48
+ }
49
+
50
+ module.exports = { getMetadata };