@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 +1 -1
- package/src/index.js +3 -2
- package/src/manipulator.js +142 -43
- package/src/parse/getBarInfo.js +16 -2
- package/src/parse/getMetadata.js +50 -0
package/package.json
CHANGED
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,
|
package/src/manipulator.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
266
|
-
// Remove
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
586
|
-
normaliseKey,
|
|
685
|
+
toggleMeterDoubling,
|
|
587
686
|
};
|
package/src/parse/getBarInfo.js
CHANGED
|
@@ -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; //
|
|
7
|
-
//
|
|
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 };
|