@goplayerjuggler/abc-tools 1.0.15 → 1.0.17
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 +5 -3
- package/src/incipit.js +5 -5
- package/src/index.js +1 -2
- package/src/javascriptify.js +21 -7
- package/src/manipulator.js +197 -11
- package/src/math.js +1 -1
- package/src/parse/barline-parser.js +2 -2
- package/src/parse/getBarInfo.js +7 -7
- package/src/parse/getMetadata.js +3 -1
- package/src/parse/header-parser.js +13 -6
- package/src/parse/misc-parser.js +6 -6
- package/src/parse/note-parser.js +12 -12
- package/src/parse/parser.js +19 -19
- package/src/parse/token-utils.js +4 -4
- package/src/sort/contour-sort.js +100 -3
- package/src/sort/contour-svg.js +3 -3
- package/src/sort/display-contour.js +1 -1
- package/src/sort/encode.js +7 -1
- package/src/sort/get-contour.js +35 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goplayerjuggler/abc-tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
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": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"license": "GPL-3.0-or-later",
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "https://github.com/goplayerjuggler/abc-tools.git"
|
|
35
|
+
"url": "git+https://github.com/goplayerjuggler/abc-tools.git"
|
|
36
36
|
},
|
|
37
37
|
"bugs": {
|
|
38
38
|
"url": "https://github.com/goplayerjuggler/abc-tools/issues"
|
|
@@ -44,11 +44,13 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@eslint/eslintrc": "^3.3.1",
|
|
46
46
|
"@eslint/js": "^9.37.0",
|
|
47
|
+
"clipboardy": "^5.0.1",
|
|
47
48
|
"eslint": "^9.37.0",
|
|
48
49
|
"eslint-plugin-jest": "^29.0.1",
|
|
49
50
|
"globals": "^16.4.0",
|
|
50
51
|
"jest": "^30.2.0",
|
|
51
|
-
"jsdoc": "^4.0.5"
|
|
52
|
+
"jsdoc": "^4.0.5",
|
|
53
|
+
"prettier": "^3.8.1"
|
|
52
54
|
},
|
|
53
55
|
"jest": {
|
|
54
56
|
"testEnvironment": "node",
|
package/src/incipit.js
CHANGED
|
@@ -278,7 +278,7 @@ function sanitise(theTune) {
|
|
|
278
278
|
function getIncipit(data) {
|
|
279
279
|
let {
|
|
280
280
|
abc,
|
|
281
|
-
numBars
|
|
281
|
+
numBars //, part=null
|
|
282
282
|
} = typeof data === "string" ? { abc: data } : data;
|
|
283
283
|
|
|
284
284
|
const { withAnacrucis = true } =
|
|
@@ -313,7 +313,7 @@ function getIncipitForContourGeneration(
|
|
|
313
313
|
return getIncipit({
|
|
314
314
|
abc,
|
|
315
315
|
withAnacrucis: false,
|
|
316
|
-
numBars
|
|
316
|
+
numBars
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
319
|
|
|
@@ -322,7 +322,7 @@ function getContourFromFullAbc(
|
|
|
322
322
|
{
|
|
323
323
|
withSvg = true,
|
|
324
324
|
withSwingTransform = false,
|
|
325
|
-
numBars = new Fraction(3, 2)
|
|
325
|
+
numBars = new Fraction(3, 2)
|
|
326
326
|
} = {}
|
|
327
327
|
) {
|
|
328
328
|
if (Array.isArray(abc)) {
|
|
@@ -331,12 +331,12 @@ function getContourFromFullAbc(
|
|
|
331
331
|
}
|
|
332
332
|
return getContour(getIncipitForContourGeneration(abc, { numBars }), {
|
|
333
333
|
withSvg,
|
|
334
|
-
withSwingTransform
|
|
334
|
+
withSwingTransform
|
|
335
335
|
});
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
module.exports = {
|
|
339
339
|
getIncipit,
|
|
340
340
|
getIncipitForContourGeneration,
|
|
341
|
-
getContourFromFullAbc
|
|
341
|
+
getContourFromFullAbc
|
|
342
342
|
};
|
package/src/index.js
CHANGED
package/src/javascriptify.js
CHANGED
|
@@ -12,8 +12,12 @@ function javascriptify(value, indent = 0) {
|
|
|
12
12
|
const nextIndentStr = " ".repeat(indent + 1);
|
|
13
13
|
|
|
14
14
|
// Handle null and undefined
|
|
15
|
-
if (value === null) {
|
|
16
|
-
|
|
15
|
+
if (value === null) {
|
|
16
|
+
return "null";
|
|
17
|
+
}
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return "undefined";
|
|
20
|
+
}
|
|
17
21
|
|
|
18
22
|
// Handle primitives
|
|
19
23
|
if (typeof value === "number") {
|
|
@@ -39,7 +43,9 @@ function javascriptify(value, indent = 0) {
|
|
|
39
43
|
|
|
40
44
|
// Handle arrays
|
|
41
45
|
if (Array.isArray(value)) {
|
|
42
|
-
if (value.length === 0) {
|
|
46
|
+
if (value.length === 0) {
|
|
47
|
+
return "[]";
|
|
48
|
+
}
|
|
43
49
|
|
|
44
50
|
const items = value
|
|
45
51
|
.map((item) => nextIndentStr + javascriptify(item, indent + 1))
|
|
@@ -54,15 +60,23 @@ function javascriptify(value, indent = 0) {
|
|
|
54
60
|
const keys = Object.keys(value).filter((k) => {
|
|
55
61
|
const val = value[k];
|
|
56
62
|
// Omit all falsey values (including false, but keep 0)
|
|
57
|
-
if (val === 0) {
|
|
63
|
+
if (val === 0) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
58
66
|
//if (val === null || val === undefined || val === "") return false;
|
|
59
|
-
if (!val) {
|
|
67
|
+
if (!val) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
60
70
|
// Omit empty arrays
|
|
61
|
-
if (Array.isArray(val) && val.length === 0) {
|
|
71
|
+
if (Array.isArray(val) && val.length === 0) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
62
74
|
return true;
|
|
63
75
|
});
|
|
64
76
|
|
|
65
|
-
if (keys.length === 0) {
|
|
77
|
+
if (keys.length === 0) {
|
|
78
|
+
return "{}";
|
|
79
|
+
}
|
|
66
80
|
|
|
67
81
|
const properties = keys
|
|
68
82
|
.map((key) => {
|
package/src/manipulator.js
CHANGED
|
@@ -2,6 +2,7 @@ const { Fraction } = require("./math.js");
|
|
|
2
2
|
const { parseAbc, getMeter, getUnitLength } = require("./parse/parser.js");
|
|
3
3
|
|
|
4
4
|
const { getBarInfo } = require("./parse/getBarInfo.js");
|
|
5
|
+
const { getHeaderValue } = require("./parse/header-parser.js");
|
|
5
6
|
|
|
6
7
|
// ============================================================================
|
|
7
8
|
// ABC manipulation functions
|
|
@@ -56,7 +57,7 @@ function normaliseKey(keyHeader) {
|
|
|
56
57
|
lyd: "lydian",
|
|
57
58
|
lydian: "lydian",
|
|
58
59
|
loc: "locrian",
|
|
59
|
-
locrian: "locrian"
|
|
60
|
+
locrian: "locrian"
|
|
60
61
|
};
|
|
61
62
|
const mode = Object.keys(modeMap).find((m) => key.includes(m)) || "major";
|
|
62
63
|
|
|
@@ -104,7 +105,7 @@ function hasAnacrucisFromParsed(parsed, barLines) {
|
|
|
104
105
|
getBarInfo(bars, barLines, meter, {
|
|
105
106
|
barNumbers: true,
|
|
106
107
|
isPartial: true,
|
|
107
|
-
cumulativeDuration: false
|
|
108
|
+
cumulativeDuration: false
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -198,7 +199,7 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
198
199
|
// Get bar info to understand musical structure
|
|
199
200
|
getBarInfo(bars, barLines, meter, {
|
|
200
201
|
barNumbers: true,
|
|
201
|
-
isPartial: true
|
|
202
|
+
isPartial: true
|
|
202
203
|
});
|
|
203
204
|
|
|
204
205
|
// Build a map of which bars start with variant endings
|
|
@@ -291,7 +292,7 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
291
292
|
const variantToken = barStartsWithVariant.get(nextBarIdx);
|
|
292
293
|
barLinesToConvert.set(variantToken.sourceIndex, {
|
|
293
294
|
oldLength: variantToken.sourceLength,
|
|
294
|
-
oldText: variantToken.token
|
|
295
|
+
oldText: variantToken.token
|
|
295
296
|
});
|
|
296
297
|
}
|
|
297
298
|
barLineDecisions.set(i, { action: "remove" });
|
|
@@ -355,7 +356,7 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
355
356
|
} else {
|
|
356
357
|
// Going from large to small: add bar lines at midpoints
|
|
357
358
|
const barInfo = getBarInfo(bars, barLines, meter, {
|
|
358
|
-
divideBarsBy: 2
|
|
359
|
+
divideBarsBy: 2
|
|
359
360
|
});
|
|
360
361
|
|
|
361
362
|
const { midpoints } = barInfo;
|
|
@@ -383,10 +384,13 @@ function toggleMeter_4_4_to_4_2(abc, currentMeter) {
|
|
|
383
384
|
|
|
384
385
|
const defaultCommentForReelConversion =
|
|
385
386
|
"*abc-tools: convert reel to M:4/4 & L:1/16*";
|
|
387
|
+
const defaultCommentForHornpipeConversion =
|
|
388
|
+
"*abc-tools: convert hornpipe to M:4/2*";
|
|
389
|
+
const defaultCommentForJigConversion = "*abc-tools: convert jig to M:12/8*";
|
|
386
390
|
/**
|
|
387
|
-
* Adjusts bar lengths and L
|
|
388
|
-
* reel written in the normal way (M:4/4 L:1/8)
|
|
389
|
-
* written with M:4/4 L:1/16
|
|
391
|
+
* Adjusts bar lengths and L, M fields - a
|
|
392
|
+
* reel written in the normal way (M:4/4 L:1/8) is written
|
|
393
|
+
* written with M:4/4 and L:1/16 (or M:4/2 and L:1/8, if withSemiquavers is unflagged)
|
|
390
394
|
* Bars are twice as long, and the quick notes are semiquavers
|
|
391
395
|
* rather than quavers.
|
|
392
396
|
* @param {string} reel
|
|
@@ -420,6 +424,76 @@ function convertStandardReel(
|
|
|
420
424
|
return result;
|
|
421
425
|
}
|
|
422
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Adjusts bar lengths and M field to alter a
|
|
429
|
+
* jig written in the normal way (M:6/8) so it’s
|
|
430
|
+
* written with M:12/8.
|
|
431
|
+
* Bars are twice as long.
|
|
432
|
+
* @param {string} jig
|
|
433
|
+
* @param {string} comment - when non falsey, the comment will be injected as an N: header
|
|
434
|
+
|
|
435
|
+
* @returns
|
|
436
|
+
*/
|
|
437
|
+
function convertStandardJig(jig, comment = defaultCommentForJigConversion) {
|
|
438
|
+
const meter = getMeter(jig);
|
|
439
|
+
if (!Array.isArray(meter) || !meter || !meter[0] === 6 || !meter[1] === 8) {
|
|
440
|
+
throw new Error("invalid meter");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let result = //toggleMeter_4_4_to_4_2(reel, meter);
|
|
444
|
+
toggleMeterDoubling(jig, [6, 8], [12, 8], meter);
|
|
445
|
+
if (comment) {
|
|
446
|
+
result = result.replace(/(\nK:)/, `\nN:${comment}$1`);
|
|
447
|
+
}
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Adjusts bar lengths and M field to alter a
|
|
453
|
+
* hornpipe written in the normal way (M:6/8) so it’s
|
|
454
|
+
* written with M:12/8.
|
|
455
|
+
* Bars are twice as long.
|
|
456
|
+
* @param {string} hornpipe
|
|
457
|
+
* @param {string} comment - when non falsey, the comment will be injected as an N: header
|
|
458
|
+
|
|
459
|
+
* @returns
|
|
460
|
+
*/
|
|
461
|
+
function convertStandardHornpipe(
|
|
462
|
+
hornpipe,
|
|
463
|
+
comment = defaultCommentForHornpipeConversion
|
|
464
|
+
) {
|
|
465
|
+
const meter = getMeter(hornpipe);
|
|
466
|
+
if (!Array.isArray(meter) || !meter || !meter[0] === 4 || !meter[1] === 4) {
|
|
467
|
+
throw new Error("invalid meter");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let result = toggleMeter_4_4_to_4_2(hornpipe, meter);
|
|
471
|
+
|
|
472
|
+
if (comment) {
|
|
473
|
+
result = result.replace(/(\nK:)/, `\nN:${comment}$1`);
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
function doubleBarLength(abc, comment = null) {
|
|
478
|
+
const meter = getMeter(abc);
|
|
479
|
+
if (!Array.isArray(meter) || !meter) {
|
|
480
|
+
throw new Error("invalid meter");
|
|
481
|
+
}
|
|
482
|
+
// const newMeter = [meter[0], meter[1]];
|
|
483
|
+
// if ([16, 8, 4, 2].indexOf(meter[1]) >= 0) newMeter[1] /= 2;
|
|
484
|
+
// else {
|
|
485
|
+
// newMeter[0] *= 2;
|
|
486
|
+
// }
|
|
487
|
+
const newMeter = [meter[0] * 2, meter[1]];
|
|
488
|
+
|
|
489
|
+
let result = //toggleMeter_4_4_to_4_2(reel, meter);
|
|
490
|
+
toggleMeterDoubling(abc, meter, newMeter, meter);
|
|
491
|
+
if (comment) {
|
|
492
|
+
result = result.replace(/(\nK:)/, `\nN:${comment}$1`);
|
|
493
|
+
}
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
|
|
423
497
|
/**
|
|
424
498
|
* Adjusts bar lengths and L field to convert a
|
|
425
499
|
* reel written in the abnormal way (M:4/4 L:1/16) to the same reel
|
|
@@ -437,7 +511,9 @@ function convertToStandardReel(
|
|
|
437
511
|
withSemiquavers = true
|
|
438
512
|
) {
|
|
439
513
|
if (withSemiquavers) {
|
|
440
|
-
reel = reel
|
|
514
|
+
reel = reel
|
|
515
|
+
.replace(/\nM:\s*4\/4/, "\nM:4/2")
|
|
516
|
+
.replace(/\nL:\s*1\/16/, "\nL:1/8");
|
|
441
517
|
}
|
|
442
518
|
|
|
443
519
|
const unitLength = getUnitLength(reel);
|
|
@@ -456,6 +532,51 @@ function convertToStandardReel(
|
|
|
456
532
|
}
|
|
457
533
|
return result;
|
|
458
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Adjusts bar lengths to rewrite a
|
|
537
|
+
* jig written in the abnormal way (M:12/8) to M:6/8, the normal or standard way.
|
|
538
|
+
* Bars are half as long. Inverse operation to convertStandardJig
|
|
539
|
+
* @param {string} jig
|
|
540
|
+
* @param {string} comment - when non falsey, the comment (as an N:) will removed from the header
|
|
541
|
+
* @param {bool} withSemiquavers - when unflagged, the original jig was written in M:4/2 L:1/8
|
|
542
|
+
* @returns
|
|
543
|
+
*/
|
|
544
|
+
function convertToStandardJig(jig, comment = defaultCommentForJigConversion) {
|
|
545
|
+
const unitLength = getUnitLength(jig);
|
|
546
|
+
if (unitLength.den !== 8) {
|
|
547
|
+
throw new Error("invalid L header");
|
|
548
|
+
}
|
|
549
|
+
const meter = getMeter(jig);
|
|
550
|
+
if (!Array.isArray(meter) || !meter || !meter[0] === 12 || !meter[1] === 8) {
|
|
551
|
+
throw new Error("invalid meter");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let result = toggleMeter_6_8_to_12_8(jig); // toggleMeter_4_4_to_4_2(jig, meter);
|
|
555
|
+
if (comment) {
|
|
556
|
+
result = result.replace(`\nN:${comment}`, "");
|
|
557
|
+
}
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function convertToStandardHornpipe(
|
|
562
|
+
hornpipe,
|
|
563
|
+
comment = defaultCommentForHornpipeConversion
|
|
564
|
+
) {
|
|
565
|
+
const unitLength = getUnitLength(hornpipe);
|
|
566
|
+
if (unitLength.den !== 8) {
|
|
567
|
+
throw new Error("invalid L header");
|
|
568
|
+
}
|
|
569
|
+
const meter = getMeter(hornpipe);
|
|
570
|
+
if (!Array.isArray(meter) || !meter || !meter[0] === 4 || !meter[1] === 2) {
|
|
571
|
+
throw new Error("invalid meter");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let result = toggleMeter_4_4_to_4_2(hornpipe); // toggleMeter_4_4_to_4_2(jig, meter);
|
|
575
|
+
if (comment) {
|
|
576
|
+
result = result.replace(`\nN:${comment}`, "");
|
|
577
|
+
}
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
459
580
|
|
|
460
581
|
/**
|
|
461
582
|
* Toggle between M:6/8 and M:12/8 by surgically adding/removing bar lines
|
|
@@ -508,7 +629,7 @@ function getFirstBars(
|
|
|
508
629
|
barNumbers: true,
|
|
509
630
|
isPartial: true,
|
|
510
631
|
cumulativeDuration: true,
|
|
511
|
-
stopAfterBarNumber
|
|
632
|
+
stopAfterBarNumber
|
|
512
633
|
});
|
|
513
634
|
|
|
514
635
|
const enrichedBarLines = barInfo.barLines;
|
|
@@ -672,15 +793,80 @@ function getFirstBars(
|
|
|
672
793
|
)}`;
|
|
673
794
|
}
|
|
674
795
|
|
|
796
|
+
function canDoubleBarLength(abc) {
|
|
797
|
+
const meter = getMeter(abc),
|
|
798
|
+
l = getUnitLength(abc),
|
|
799
|
+
rhythm = getHeaderValue(abc, "R");
|
|
800
|
+
if (
|
|
801
|
+
!rhythm ||
|
|
802
|
+
["reel", "hornpipe", "jig"].indexOf(rhythm.toLowerCase()) < 0
|
|
803
|
+
) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
return (
|
|
807
|
+
!abc.match(/\[M:/) && //inline meter marking
|
|
808
|
+
!abc.match(/\[L:/) &&
|
|
809
|
+
(((rhythm === "reel" || rhythm === "hornpipe") &&
|
|
810
|
+
l.equals(new Fraction(1, 8)) &&
|
|
811
|
+
meter[0] === 4 &&
|
|
812
|
+
meter[1] === 4) ||
|
|
813
|
+
(rhythm === "jig" &&
|
|
814
|
+
l.equals(new Fraction(1, 8)) &&
|
|
815
|
+
meter[0] === 6 &&
|
|
816
|
+
meter[1] === 8))
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
function canHalveBarLength(abc) {
|
|
820
|
+
const meter = getMeter(abc),
|
|
821
|
+
l = getUnitLength(abc),
|
|
822
|
+
rhythm = getHeaderValue(abc, "R");
|
|
823
|
+
if (
|
|
824
|
+
!rhythm ||
|
|
825
|
+
["reel", "hornpipe", "jig"].indexOf(rhythm.toLowerCase()) < 0
|
|
826
|
+
) {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (
|
|
831
|
+
!rhythm ||
|
|
832
|
+
["reel", "hornpipe", "jig"].indexOf(rhythm.toLowerCase()) < 0
|
|
833
|
+
) {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
return (
|
|
837
|
+
!abc.match(/\[M:/) && //inline meter marking
|
|
838
|
+
!abc.match(/\[L:/) &&
|
|
839
|
+
((rhythm === "reel" &&
|
|
840
|
+
l.equals(new Fraction(1, 16)) &&
|
|
841
|
+
meter[0] === 4 &&
|
|
842
|
+
meter[1] === 4) ||
|
|
843
|
+
((rhythm === "reel" || rhythm === "hornpipe") &&
|
|
844
|
+
l.equals(new Fraction(1, 8)) &&
|
|
845
|
+
meter[0] === 4 &&
|
|
846
|
+
meter[1] === 2) ||
|
|
847
|
+
(rhythm === "jig" &&
|
|
848
|
+
l.equals(new Fraction(1, 8)) &&
|
|
849
|
+
meter[0] === 12 &&
|
|
850
|
+
meter[1] === 8))
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
675
854
|
module.exports = {
|
|
855
|
+
canDoubleBarLength,
|
|
856
|
+
canHalveBarLength,
|
|
857
|
+
convertStandardJig,
|
|
858
|
+
convertStandardHornpipe,
|
|
676
859
|
convertStandardReel,
|
|
860
|
+
convertToStandardJig,
|
|
861
|
+
convertToStandardHornpipe,
|
|
677
862
|
convertToStandardReel,
|
|
678
863
|
defaultCommentForReelConversion,
|
|
864
|
+
doubleBarLength,
|
|
679
865
|
filterHeaders,
|
|
680
866
|
getFirstBars,
|
|
681
867
|
hasAnacrucis,
|
|
682
868
|
normaliseKey,
|
|
683
869
|
toggleMeter_4_4_to_4_2,
|
|
684
870
|
toggleMeter_6_8_to_12_8,
|
|
685
|
-
toggleMeterDoubling
|
|
871
|
+
toggleMeterDoubling
|
|
686
872
|
};
|
package/src/math.js
CHANGED
|
@@ -40,7 +40,7 @@ function parseBarLine(barLineStr) {
|
|
|
40
40
|
const trimmed = barLineStr.trim();
|
|
41
41
|
const result = {
|
|
42
42
|
text: barLineStr,
|
|
43
|
-
trimmed
|
|
43
|
+
trimmed
|
|
44
44
|
};
|
|
45
45
|
// Start repeat
|
|
46
46
|
if (trimmed.match(/:$/)) {
|
|
@@ -60,5 +60,5 @@ function parseBarLine(barLineStr) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
module.exports = {
|
|
63
|
-
parseBarLine
|
|
63
|
+
parseBarLine
|
|
64
64
|
};
|
package/src/parse/getBarInfo.js
CHANGED
|
@@ -21,7 +21,7 @@ function processSkippedBarLines(barLines, skippedBarLineIndexes) {
|
|
|
21
21
|
// Copy any properties from the preceding barLine not already in the skipped barLine
|
|
22
22
|
barLines[skippedIndex] = {
|
|
23
23
|
...barLines[skippedIndex - 1],
|
|
24
|
-
...barLines[skippedIndex]
|
|
24
|
+
...barLines[skippedIndex]
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -101,7 +101,7 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
101
101
|
isPartial = true,
|
|
102
102
|
cumulativeDuration = true,
|
|
103
103
|
divideBarsBy = null,
|
|
104
|
-
stopAfterBarNumber = null
|
|
104
|
+
stopAfterBarNumber = null
|
|
105
105
|
} = options;
|
|
106
106
|
|
|
107
107
|
if (divideBarsBy !== null && divideBarsBy !== 2) {
|
|
@@ -205,7 +205,7 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
205
205
|
durationSinceLastComplete: durationBeforeVariant.clone(),
|
|
206
206
|
lastCompleteBarLineIdx: lastCompleteBarLineIdx,
|
|
207
207
|
meter: [...currentMeter],
|
|
208
|
-
fullBarDuration: fullBarDuration.clone()
|
|
208
|
+
fullBarDuration: fullBarDuration.clone()
|
|
209
209
|
};
|
|
210
210
|
variantCounter = 0;
|
|
211
211
|
currentVariantId = variantCounter;
|
|
@@ -287,7 +287,7 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
287
287
|
processSkippedBarLines(barLines, skippedBarLineIndexes);
|
|
288
288
|
return {
|
|
289
289
|
barLines: barLines.slice(0, barLineIdx + 1),
|
|
290
|
-
midpoints
|
|
290
|
+
midpoints
|
|
291
291
|
};
|
|
292
292
|
}
|
|
293
293
|
}
|
|
@@ -313,7 +313,7 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
313
313
|
if (cumulativeDuration) {
|
|
314
314
|
barLine.cumulativeDuration = {
|
|
315
315
|
sinceLastBarLine: barDuration.clone(),
|
|
316
|
-
sinceLastComplete: durationSinceLastComplete.clone()
|
|
316
|
+
sinceLastComplete: durationSinceLastComplete.clone()
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
319
|
|
|
@@ -425,10 +425,10 @@ function getBarInfo(bars, barLines, meter, options = {}) {
|
|
|
425
425
|
processSkippedBarLines(barLines, skippedBarLineIndexes);
|
|
426
426
|
return {
|
|
427
427
|
barLines,
|
|
428
|
-
midpoints
|
|
428
|
+
midpoints
|
|
429
429
|
};
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
module.exports = {
|
|
433
|
-
getBarInfo
|
|
433
|
+
getBarInfo
|
|
434
434
|
};
|
package/src/parse/getMetadata.js
CHANGED
|
@@ -23,7 +23,7 @@ function getMetadata(abc) {
|
|
|
23
23
|
} else if (trimmed.startsWith("R:")) {
|
|
24
24
|
metadata.rhythm = trimmed.substring(2).trim().toLowerCase();
|
|
25
25
|
} else if (trimmed.startsWith("C:")) {
|
|
26
|
-
metadata.composer = trimmed.substring(2).trim()
|
|
26
|
+
metadata.composer = trimmed.substring(2).trim();
|
|
27
27
|
} else if (trimmed.startsWith("M:")) {
|
|
28
28
|
metadata.meter = trimmed.substring(2).trim();
|
|
29
29
|
} else if (trimmed.startsWith("K:")) {
|
|
@@ -32,6 +32,8 @@ function getMetadata(abc) {
|
|
|
32
32
|
break;
|
|
33
33
|
} else if (trimmed.startsWith("S:")) {
|
|
34
34
|
metadata.source = trimmed.substring(2).trim();
|
|
35
|
+
} else if (trimmed.startsWith("O:")) {
|
|
36
|
+
metadata.origin = trimmed.substring(2).trim();
|
|
35
37
|
} else if (trimmed.startsWith("F:")) {
|
|
36
38
|
metadata.url = trimmed.substring(2).trim();
|
|
37
39
|
} else if (trimmed.startsWith("D:")) {
|
|
@@ -77,7 +77,13 @@ function getUnitLength(abc) {
|
|
|
77
77
|
* @returns {[string]} - array of titles
|
|
78
78
|
*/
|
|
79
79
|
function getTitles(abc) {
|
|
80
|
-
return [...abc.matchAll(/^(?:T:\s
|
|
80
|
+
return [...abc.matchAll(/^(?:T:\s*(.+)\n)/gm)];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getHeaderValue(abc, header) {
|
|
84
|
+
const r = new RegExp(String.raw`(?:${header}:\s*(.+)\n)`, "m"),
|
|
85
|
+
m = abc.match(r);
|
|
86
|
+
return m ? m[1]?.trim() : null;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
/**
|
|
@@ -138,7 +144,7 @@ function getMusicLines(abc) {
|
|
|
138
144
|
originalLine: line,
|
|
139
145
|
content: trimmed,
|
|
140
146
|
comment,
|
|
141
|
-
hasContinuation
|
|
147
|
+
hasContinuation
|
|
142
148
|
});
|
|
143
149
|
|
|
144
150
|
// Track position where newline would be (unless continuation)
|
|
@@ -155,15 +161,16 @@ function getMusicLines(abc) {
|
|
|
155
161
|
lineMetadata,
|
|
156
162
|
newlinePositions,
|
|
157
163
|
headerLines,
|
|
158
|
-
headerEndIndex
|
|
164
|
+
headerEndIndex
|
|
159
165
|
};
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
module.exports = {
|
|
163
|
-
|
|
169
|
+
getHeaderValue,
|
|
170
|
+
getKey,
|
|
164
171
|
getMeter,
|
|
165
|
-
getUnitLength,
|
|
166
172
|
getMusicLines,
|
|
167
173
|
getTitles,
|
|
168
|
-
|
|
174
|
+
getTonalBase,
|
|
175
|
+
getUnitLength
|
|
169
176
|
};
|
package/src/parse/misc-parser.js
CHANGED
|
@@ -21,7 +21,7 @@ function analyzeSpacing(segment, tokenEndPos) {
|
|
|
21
21
|
whitespace: "",
|
|
22
22
|
backquotes: 0,
|
|
23
23
|
beamBreak: false,
|
|
24
|
-
lineBreak: false
|
|
24
|
+
lineBreak: false
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -35,7 +35,7 @@ function analyzeSpacing(segment, tokenEndPos) {
|
|
|
35
35
|
whitespace: "",
|
|
36
36
|
backquotes: 0,
|
|
37
37
|
beamBreak: false,
|
|
38
|
-
lineBreak: false
|
|
38
|
+
lineBreak: false
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -51,7 +51,7 @@ function analyzeSpacing(segment, tokenEndPos) {
|
|
|
51
51
|
whitespace,
|
|
52
52
|
backquotes,
|
|
53
53
|
beamBreak: whitespace.length > 1 || whitespace.includes("\n"), // Multiple spaces or newline breaks beam
|
|
54
|
-
lineBreak: whitespace.includes("\n")
|
|
54
|
+
lineBreak: whitespace.includes("\n")
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -68,7 +68,7 @@ function parseTuplet(token, isCompoundTimeSignature) {
|
|
|
68
68
|
const pqr = {
|
|
69
69
|
p: parseInt(tupleMatch[1]),
|
|
70
70
|
q: tupleMatch[2],
|
|
71
|
-
r: tupleMatch[3]
|
|
71
|
+
r: tupleMatch[3]
|
|
72
72
|
};
|
|
73
73
|
const { p } = pqr;
|
|
74
74
|
let { q, r } = pqr;
|
|
@@ -107,12 +107,12 @@ function parseTuplet(token, isCompoundTimeSignature) {
|
|
|
107
107
|
isTuple: true,
|
|
108
108
|
p,
|
|
109
109
|
q,
|
|
110
|
-
r
|
|
110
|
+
r
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
return null;
|
|
114
114
|
}
|
|
115
115
|
module.exports = {
|
|
116
116
|
analyzeSpacing,
|
|
117
|
-
parseTuplet
|
|
117
|
+
parseTuplet
|
|
118
118
|
};
|
package/src/parse/note-parser.js
CHANGED
|
@@ -38,7 +38,7 @@ function parseDecorations(noteStr) {
|
|
|
38
38
|
T: "trill",
|
|
39
39
|
H: "fermata",
|
|
40
40
|
u: "upbow",
|
|
41
|
-
v: "downbow"
|
|
41
|
+
v: "downbow"
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
for (const [symbol, name] of Object.entries(symbolDecorations)) {
|
|
@@ -82,7 +82,7 @@ function parseAnnotation(noteStr) {
|
|
|
82
82
|
if (annotationMatch) {
|
|
83
83
|
return {
|
|
84
84
|
position: annotationMatch[1],
|
|
85
|
-
text: annotationMatch[2]
|
|
85
|
+
text: annotationMatch[2]
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
return null;
|
|
@@ -190,7 +190,7 @@ function parseChord(chordStr, unitLength) {
|
|
|
190
190
|
}
|
|
191
191
|
return {
|
|
192
192
|
isChord: true,
|
|
193
|
-
notes
|
|
193
|
+
notes
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -256,7 +256,7 @@ function parseGraceNotes(graceStr) {
|
|
|
256
256
|
isGraceNote: true,
|
|
257
257
|
duration: new Fraction(0, 1),
|
|
258
258
|
isChord: true,
|
|
259
|
-
chordNotes: chord.notes
|
|
259
|
+
chordNotes: chord.notes
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
262
|
} else {
|
|
@@ -266,7 +266,7 @@ function parseGraceNotes(graceStr) {
|
|
|
266
266
|
graceNotes.push({
|
|
267
267
|
...pitchData,
|
|
268
268
|
isGraceNote: true,
|
|
269
|
-
duration: new Fraction(0, 1)
|
|
269
|
+
duration: new Fraction(0, 1)
|
|
270
270
|
});
|
|
271
271
|
}
|
|
272
272
|
}
|
|
@@ -298,7 +298,7 @@ function parseBrokenRhythm(token) {
|
|
|
298
298
|
return {
|
|
299
299
|
isBrokenRhythm: true,
|
|
300
300
|
direction: symbol[0],
|
|
301
|
-
dots: symbol.length
|
|
301
|
+
dots: symbol.length
|
|
302
302
|
};
|
|
303
303
|
}
|
|
304
304
|
return null;
|
|
@@ -375,7 +375,7 @@ function parseNote(noteStr, unitLength, currentTuple) {
|
|
|
375
375
|
if (cleanStr.match(/^y$/)) {
|
|
376
376
|
return {
|
|
377
377
|
isDummy: true,
|
|
378
|
-
duration: new Fraction(0, 1, decorations, annotation)
|
|
378
|
+
duration: new Fraction(0, 1, decorations, annotation)
|
|
379
379
|
};
|
|
380
380
|
}
|
|
381
381
|
|
|
@@ -385,7 +385,7 @@ function parseNote(noteStr, unitLength, currentTuple) {
|
|
|
385
385
|
const duration = getDuration({
|
|
386
386
|
unitLength,
|
|
387
387
|
noteString: cleanStr,
|
|
388
|
-
currentTuple
|
|
388
|
+
currentTuple
|
|
389
389
|
});
|
|
390
390
|
const result = { isSilence: true, duration, text: silenceMatch[0] };
|
|
391
391
|
if (decorations) {
|
|
@@ -427,7 +427,7 @@ function parseNote(noteStr, unitLength, currentTuple) {
|
|
|
427
427
|
const duration = getDuration({
|
|
428
428
|
unitLength,
|
|
429
429
|
noteString: cleanStr,
|
|
430
|
-
currentTuple
|
|
430
|
+
currentTuple
|
|
431
431
|
});
|
|
432
432
|
topNote.duration = duration;
|
|
433
433
|
// Apply duration to all notes in chord
|
|
@@ -442,7 +442,7 @@ function parseNote(noteStr, unitLength, currentTuple) {
|
|
|
442
442
|
chordSymbol: chordSymbol || chord.chordSymbol,
|
|
443
443
|
decorations: decorations || chord.decorations,
|
|
444
444
|
isChord: true,
|
|
445
|
-
tied
|
|
445
|
+
tied
|
|
446
446
|
};
|
|
447
447
|
}
|
|
448
448
|
}
|
|
@@ -453,7 +453,7 @@ function parseNote(noteStr, unitLength, currentTuple) {
|
|
|
453
453
|
const duration = getDuration({
|
|
454
454
|
unitLength,
|
|
455
455
|
noteString: cleanStr,
|
|
456
|
-
currentTuple
|
|
456
|
+
currentTuple
|
|
457
457
|
});
|
|
458
458
|
|
|
459
459
|
const result = { pitch, octave, duration, tied };
|
|
@@ -480,5 +480,5 @@ module.exports = {
|
|
|
480
480
|
parseNote,
|
|
481
481
|
parseBrokenRhythm,
|
|
482
482
|
applyBrokenRhythm,
|
|
483
|
-
parseGraceNotes
|
|
483
|
+
parseGraceNotes
|
|
484
484
|
};
|
package/src/parse/parser.js
CHANGED
|
@@ -4,19 +4,19 @@ const {
|
|
|
4
4
|
getMeter,
|
|
5
5
|
getKey,
|
|
6
6
|
getUnitLength,
|
|
7
|
-
getMusicLines
|
|
7
|
+
getMusicLines
|
|
8
8
|
} = require("./header-parser.js");
|
|
9
9
|
const {
|
|
10
10
|
parseNote,
|
|
11
11
|
parseBrokenRhythm,
|
|
12
12
|
applyBrokenRhythm,
|
|
13
|
-
parseGraceNotes
|
|
13
|
+
parseGraceNotes
|
|
14
14
|
} = require("./note-parser.js");
|
|
15
15
|
const { parseBarLine } = require("./barline-parser.js");
|
|
16
16
|
const {
|
|
17
17
|
getTokenRegex,
|
|
18
18
|
parseInlineField,
|
|
19
|
-
repeat_1Or2
|
|
19
|
+
repeat_1Or2
|
|
20
20
|
} = require("./token-utils.js");
|
|
21
21
|
const { analyzeSpacing, parseTuplet } = require("./misc-parser.js");
|
|
22
22
|
|
|
@@ -338,7 +338,7 @@ function parseAbc(abc, options = {}) {
|
|
|
338
338
|
lineMetadata,
|
|
339
339
|
headerLines,
|
|
340
340
|
headerEndIndex,
|
|
341
|
-
newlinePositions
|
|
341
|
+
newlinePositions
|
|
342
342
|
} = getMusicLines(abc);
|
|
343
343
|
|
|
344
344
|
// Create a Set of newline positions for O(1) lookup
|
|
@@ -413,8 +413,8 @@ function parseAbc(abc, options = {}) {
|
|
|
413
413
|
whitespace: "",
|
|
414
414
|
backquotes: 0,
|
|
415
415
|
beamBreak: false,
|
|
416
|
-
lineBreak: false
|
|
417
|
-
|
|
416
|
+
lineBreak: false
|
|
417
|
+
}
|
|
418
418
|
});
|
|
419
419
|
});
|
|
420
420
|
// Grace notes don’t update previousRealNote
|
|
@@ -432,7 +432,7 @@ function parseAbc(abc, options = {}) {
|
|
|
432
432
|
token: fullToken,
|
|
433
433
|
sourceIndex: tokenStartPos,
|
|
434
434
|
sourceLength: fullToken.length,
|
|
435
|
-
spacing
|
|
435
|
+
spacing
|
|
436
436
|
};
|
|
437
437
|
currentBar.push(inlineFieldObj);
|
|
438
438
|
|
|
@@ -482,7 +482,7 @@ function parseAbc(abc, options = {}) {
|
|
|
482
482
|
|
|
483
483
|
// Add the broken rhythm marker to the bar
|
|
484
484
|
currentBar.push({
|
|
485
|
-
...brokenRhythm
|
|
485
|
+
...brokenRhythm
|
|
486
486
|
});
|
|
487
487
|
|
|
488
488
|
// Add the next note to the bar
|
|
@@ -491,7 +491,7 @@ function parseAbc(abc, options = {}) {
|
|
|
491
491
|
token: nextToken,
|
|
492
492
|
sourceIndex: nextTokenStartPos,
|
|
493
493
|
sourceLength: nextToken.length,
|
|
494
|
-
spacing: nextSpacing
|
|
494
|
+
spacing: nextSpacing
|
|
495
495
|
};
|
|
496
496
|
currentBar.push(nextNoteObj);
|
|
497
497
|
previousRealNote = null; //can’t have successive broken rhythms
|
|
@@ -518,7 +518,7 @@ function parseAbc(abc, options = {}) {
|
|
|
518
518
|
...tuple,
|
|
519
519
|
token: fullToken,
|
|
520
520
|
sourceIndex: tokenStartPos,
|
|
521
|
-
sourceLength: fullToken.length
|
|
521
|
+
sourceLength: fullToken.length
|
|
522
522
|
});
|
|
523
523
|
previousRealNote = null; // Tuplet markers break note sequences
|
|
524
524
|
continue;
|
|
@@ -533,7 +533,7 @@ function parseAbc(abc, options = {}) {
|
|
|
533
533
|
token: fullToken,
|
|
534
534
|
sourceIndex: tokenStartPos,
|
|
535
535
|
sourceLength: fullToken.length,
|
|
536
|
-
spacing
|
|
536
|
+
spacing
|
|
537
537
|
});
|
|
538
538
|
// Chord symbols don’t break note sequences
|
|
539
539
|
continue;
|
|
@@ -547,7 +547,7 @@ function parseAbc(abc, options = {}) {
|
|
|
547
547
|
token: fullToken,
|
|
548
548
|
sourceIndex: tokenStartPos,
|
|
549
549
|
sourceLength: fullToken.length,
|
|
550
|
-
spacing
|
|
550
|
+
spacing
|
|
551
551
|
});
|
|
552
552
|
// Decorations don’t break note sequences
|
|
553
553
|
continue;
|
|
@@ -562,7 +562,7 @@ function parseAbc(abc, options = {}) {
|
|
|
562
562
|
token: fullToken,
|
|
563
563
|
sourceIndex: tokenStartPos,
|
|
564
564
|
sourceLength: fullToken.length,
|
|
565
|
-
spacing
|
|
565
|
+
spacing
|
|
566
566
|
};
|
|
567
567
|
currentBar.push(noteObj);
|
|
568
568
|
// Only track as previous note if it has non-zero duration (for broken rhythms)
|
|
@@ -581,7 +581,7 @@ function parseAbc(abc, options = {}) {
|
|
|
581
581
|
token: fullToken,
|
|
582
582
|
sourceIndex: tokenStartPos,
|
|
583
583
|
sourceLength: fullToken.length,
|
|
584
|
-
spacing
|
|
584
|
+
spacing
|
|
585
585
|
};
|
|
586
586
|
firstOrSecondRepeat = !!fullToken.match(new RegExp(repeat_1Or2));
|
|
587
587
|
if (firstOrSecondRepeat) {
|
|
@@ -606,8 +606,8 @@ function parseAbc(abc, options = {}) {
|
|
|
606
606
|
? { barLineText: musicText, barLinePos: musicText.length }
|
|
607
607
|
: {
|
|
608
608
|
barLineText: fullToken,
|
|
609
|
-
barLinePos: tokenStartPos
|
|
610
|
-
|
|
609
|
+
barLinePos: tokenStartPos
|
|
610
|
+
};
|
|
611
611
|
|
|
612
612
|
//if (lastBarPos > 0) lastBarPos--; //the last character in a barline expression may be needed to match variant endings - eg `|1`
|
|
613
613
|
|
|
@@ -625,7 +625,7 @@ function parseAbc(abc, options = {}) {
|
|
|
625
625
|
sourceIndex: barLinePos,
|
|
626
626
|
sourceLength: barLineText.length,
|
|
627
627
|
//barNumber: barCount,
|
|
628
|
-
hasLineBreak: hasLineBreakAfterBar
|
|
628
|
+
hasLineBreak: hasLineBreakAfterBar
|
|
629
629
|
});
|
|
630
630
|
|
|
631
631
|
// Update the last token in current bar to mark lineBreak if bar line has one
|
|
@@ -681,7 +681,7 @@ function parseAbc(abc, options = {}) {
|
|
|
681
681
|
lineMetadata,
|
|
682
682
|
headerLines,
|
|
683
683
|
headerEndIndex,
|
|
684
|
-
musicText
|
|
684
|
+
musicText
|
|
685
685
|
};
|
|
686
686
|
}
|
|
687
687
|
}
|
|
@@ -768,5 +768,5 @@ module.exports = {
|
|
|
768
768
|
getUnitLength,
|
|
769
769
|
getMusicLines,
|
|
770
770
|
analyzeSpacing,
|
|
771
|
-
parseBarLine
|
|
771
|
+
parseBarLine
|
|
772
772
|
};
|
package/src/parse/token-utils.js
CHANGED
|
@@ -84,7 +84,7 @@ const // captures not only |1 |2, but also :|1 :||1 :|2 :||2
|
|
|
84
84
|
*
|
|
85
85
|
* Strategy: Match `[` only when NOT followed by inline field or digit; match `|` and `]` freely
|
|
86
86
|
*/
|
|
87
|
-
barLine: String.raw`:*\.*(?:[|\]]|\[(?![KLMP]:|[0-9]))*(?:\||::+)(?:[|\]:]|\[(?![KLMP]:|[0-9]))*
|
|
87
|
+
barLine: String.raw`:*\.*(?:[|\]]|\[(?![KLMP]:|[0-9]))*(?:\||::+)(?:[|\]:]|\[(?![KLMP]:|[0-9]))* *`
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
/**
|
|
@@ -133,7 +133,7 @@ const getTokenRegex = (options = {}) => {
|
|
|
133
133
|
s.broken,
|
|
134
134
|
s.variantEnding,
|
|
135
135
|
s.repeat_1Or2,
|
|
136
|
-
s.barLine
|
|
136
|
+
s.barLine
|
|
137
137
|
].join("|");
|
|
138
138
|
|
|
139
139
|
return new RegExp(fullPattern, "g");
|
|
@@ -157,7 +157,7 @@ function parseInlineField(token) {
|
|
|
157
157
|
if (fieldMatch) {
|
|
158
158
|
return {
|
|
159
159
|
field: fieldMatch[1],
|
|
160
|
-
value: fieldMatch[2].trim()
|
|
160
|
+
value: fieldMatch[2].trim()
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
return null;
|
|
@@ -165,5 +165,5 @@ function parseInlineField(token) {
|
|
|
165
165
|
module.exports = {
|
|
166
166
|
repeat_1Or2,
|
|
167
167
|
getTokenRegex,
|
|
168
|
-
parseInlineField
|
|
168
|
+
parseInlineField
|
|
169
169
|
};
|
package/src/sort/contour-sort.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { getIncipitForContourGeneration } = require("../incipit.js");
|
|
1
2
|
const { Fraction } = require("../math.js");
|
|
2
3
|
|
|
3
4
|
const { decodeChar, encodeToChar, silenceChar } = require("./encode.js");
|
|
@@ -161,10 +162,105 @@ function compare(objA, objB) {
|
|
|
161
162
|
return posA >= keyA.length ? -1 : 1;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
function canBeCompared(tune1, tune2) {
|
|
166
|
+
if (!tune1.contour || !tune2.contour) return false;
|
|
167
|
+
|
|
168
|
+
// but not hop jigs with different meters
|
|
169
|
+
if (
|
|
170
|
+
tune1.rhythm?.indexOf("hop jig") >= 0 &&
|
|
171
|
+
tune2.rhythm?.indexOf("hop jig") >= 0 &&
|
|
172
|
+
tune1.meter !== tune2.meter
|
|
173
|
+
)
|
|
174
|
+
return false;
|
|
175
|
+
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getAbcForContour_default(tune) {
|
|
180
|
+
return getIncipitForContourGeneration(
|
|
181
|
+
tune.incipit
|
|
182
|
+
? tune.incipit
|
|
183
|
+
: Array.isArray(tune.abc)
|
|
184
|
+
? tune.abc[0]
|
|
185
|
+
: tune.abc
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
/**
|
|
165
190
|
* Sort an array of objects containing ABC notation
|
|
191
|
+
* - based on a contour property
|
|
192
|
+
* - when the contour is missing, attempts to generate it
|
|
193
|
+
* - adds info to each object like the contour, the type of rhythm
|
|
194
|
+
* @param {Array<Object>} arr - array of tune objects to sort
|
|
195
|
+
* @param {Object} [options] - options for the sorting
|
|
196
|
+
* @param {function(Object):string} [options.getAbc] - function returning an abc fragment to be used to calculate the contour.
|
|
166
197
|
*/
|
|
167
|
-
function
|
|
198
|
+
function sort(arr, options = {}) {
|
|
199
|
+
const {
|
|
200
|
+
comparable = [
|
|
201
|
+
["jig", "slide", "single jig", "double jig"],
|
|
202
|
+
["reel", "single reel", "reel (single)", "strathspey", "double reel"],
|
|
203
|
+
["hornpipe", "barndance", "fling"]
|
|
204
|
+
],
|
|
205
|
+
applySwingTransform = ["hornpipe", "barndance", "fling", "mazurka"],
|
|
206
|
+
getAbc: getAbcForContour = getAbcForContour_default,
|
|
207
|
+
getContourOptions = {
|
|
208
|
+
withSvg: true
|
|
209
|
+
}
|
|
210
|
+
} = options;
|
|
211
|
+
|
|
212
|
+
const comparableMap = {};
|
|
213
|
+
//set up comparableMap s.t. all entries in each list of index i go under i_<first entry of i>
|
|
214
|
+
//that way we show the jigs with similar rhythms followed by reels etc.
|
|
215
|
+
for (let i = 0; i < comparable.length; i++) {
|
|
216
|
+
const list = comparable[i];
|
|
217
|
+
//map subsequent entries to the first entry
|
|
218
|
+
for (let j = 0; j < list.length; j++) {
|
|
219
|
+
comparableMap[list[j]] = `${i}_${list[0]}`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const tune of arr) {
|
|
224
|
+
if (!tune.contour) {
|
|
225
|
+
try {
|
|
226
|
+
const withSwingTransform =
|
|
227
|
+
applySwingTransform.indexOf(tune.rhythm) >= 0;
|
|
228
|
+
const shortAbc = getAbcForContour(tune);
|
|
229
|
+
|
|
230
|
+
if (shortAbc) {
|
|
231
|
+
getContourOptions.withSwingTransform = withSwingTransform;
|
|
232
|
+
tune.contour = getContour(shortAbc, getContourOptions);
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.log(error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!tune.baseRhythm)
|
|
239
|
+
tune.baseRhythm = comparableMap[tune.rhythm] ?? tune.rhythm;
|
|
240
|
+
}
|
|
241
|
+
arr.sort((a, b) => {
|
|
242
|
+
const comparison =
|
|
243
|
+
a.baseRhythm !== b.baseRhythm
|
|
244
|
+
? a.baseRhythm < b.baseRhythm
|
|
245
|
+
? -1
|
|
246
|
+
: 1
|
|
247
|
+
: canBeCompared(a, b)
|
|
248
|
+
? compare(a.contour, b.contour)
|
|
249
|
+
: a.contour && !b.contour
|
|
250
|
+
? -1
|
|
251
|
+
: b.contour && !a.contour
|
|
252
|
+
? 1
|
|
253
|
+
: a.name !== b.name
|
|
254
|
+
? a.name < b.name
|
|
255
|
+
? -1
|
|
256
|
+
: 1
|
|
257
|
+
: 0;
|
|
258
|
+
return comparison;
|
|
259
|
+
});
|
|
260
|
+
arr.forEach((t) => delete t.baseRhythm);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function simpleSort(arr) {
|
|
168
264
|
for (const item of arr) {
|
|
169
265
|
if (!item.contour && item.abc) {
|
|
170
266
|
try {
|
|
@@ -198,6 +294,7 @@ function sortArray(arr) {
|
|
|
198
294
|
|
|
199
295
|
module.exports = {
|
|
200
296
|
compare,
|
|
201
|
-
|
|
202
|
-
|
|
297
|
+
sort,
|
|
298
|
+
simpleSort,
|
|
299
|
+
decodeChar
|
|
203
300
|
};
|
package/src/sort/contour-svg.js
CHANGED
|
@@ -61,7 +61,7 @@ const contourToSvg_defaultConfig = {
|
|
|
61
61
|
yAxisWidth: 0.5,
|
|
62
62
|
yAxisTickLength: 4,
|
|
63
63
|
yAxisTonicTickLength: 6,
|
|
64
|
-
yAxisTickWidth: 0.5
|
|
64
|
+
yAxisTickWidth: 0.5
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -150,7 +150,7 @@ function contourToSvg(contour, svgConfig = {}) {
|
|
|
150
150
|
width: segmentWidth,
|
|
151
151
|
position: decoded.position,
|
|
152
152
|
isHeld: decoded.isHeld,
|
|
153
|
-
isSilence: decoded.isSilence
|
|
153
|
+
isSilence: decoded.isSilence
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
xPosition += segmentWidth;
|
|
@@ -332,5 +332,5 @@ function contourToSvg(contour, svgConfig = {}) {
|
|
|
332
332
|
|
|
333
333
|
module.exports = {
|
|
334
334
|
contourToSvg,
|
|
335
|
-
contourToSvg_defaultConfig
|
|
335
|
+
contourToSvg_defaultConfig
|
|
336
336
|
};
|
package/src/sort/encode.js
CHANGED
|
@@ -52,9 +52,15 @@ function decodeChar(char) {
|
|
|
52
52
|
return { position, isHeld, isSilence: false };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function shiftChar(char, nbOctaves) {
|
|
56
|
+
const { position, isHeld } = decodeChar(char);
|
|
57
|
+
return encodeToChar(position + nbOctaves * OCTAVE_SHIFT, isHeld);
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
module.exports = {
|
|
56
61
|
calculateModalPosition,
|
|
57
62
|
encodeToChar,
|
|
58
63
|
decodeChar,
|
|
59
|
-
|
|
64
|
+
shiftChar,
|
|
65
|
+
silenceChar
|
|
60
66
|
};
|
package/src/sort/get-contour.js
CHANGED
|
@@ -4,13 +4,14 @@ const {
|
|
|
4
4
|
calculateModalPosition,
|
|
5
5
|
encodeToChar,
|
|
6
6
|
silenceChar,
|
|
7
|
+
shiftChar
|
|
7
8
|
} = require("./encode.js");
|
|
8
9
|
|
|
9
10
|
const {
|
|
10
11
|
getTonalBase,
|
|
11
12
|
getUnitLength,
|
|
12
13
|
parseAbc,
|
|
13
|
-
getMeter
|
|
14
|
+
getMeter
|
|
14
15
|
} = require("../parse/parser.js");
|
|
15
16
|
|
|
16
17
|
const { contourToSvg } = require("./contour-svg.js");
|
|
@@ -28,11 +29,12 @@ const { contourToSvg } = require("./contour-svg.js");
|
|
|
28
29
|
function getContour(
|
|
29
30
|
abc,
|
|
30
31
|
{
|
|
32
|
+
contourShift = null,
|
|
31
33
|
withSvg = false,
|
|
32
34
|
withSwingTransform = false,
|
|
33
|
-
maxNbBars = new Fraction(3,
|
|
35
|
+
maxNbBars = new Fraction(3, 1),
|
|
34
36
|
maxNbUnitLengths = 12,
|
|
35
|
-
svgConfig = {}
|
|
37
|
+
svgConfig = {}
|
|
36
38
|
} = {}
|
|
37
39
|
) {
|
|
38
40
|
const tonalBase = getTonalBase(abc);
|
|
@@ -52,10 +54,10 @@ function getContour(
|
|
|
52
54
|
const maxDuration = maxNbBars.multiply(meterFraction);
|
|
53
55
|
|
|
54
56
|
const {
|
|
55
|
-
bars
|
|
57
|
+
bars
|
|
56
58
|
} = //todo: could add as an argument; default null
|
|
57
59
|
parseAbc(abc, {
|
|
58
|
-
maxBars: Math.ceil(maxNbBars.toNumber())
|
|
60
|
+
maxBars: Math.ceil(maxNbBars.toNumber())
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
let cumulatedDuration = new Fraction(0, 1);
|
|
@@ -65,6 +67,7 @@ function getContour(
|
|
|
65
67
|
let index = 0;
|
|
66
68
|
// get the parsed notes - notes are tokens with a duration
|
|
67
69
|
const notes = [];
|
|
70
|
+
let totalPosition = 0;
|
|
68
71
|
let tied = false,
|
|
69
72
|
previousPosition = null;
|
|
70
73
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -90,6 +93,10 @@ function getContour(
|
|
|
90
93
|
? { encoded: silenceChar, encodedHeld: silenceChar, position: 0 }
|
|
91
94
|
: getEncodedFromNote(note, tonalBase, tied, previousPosition);
|
|
92
95
|
|
|
96
|
+
if (!isSilence) {
|
|
97
|
+
totalPosition += position;
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
if (note.tied) {
|
|
94
101
|
tied = true;
|
|
95
102
|
previousPosition = position;
|
|
@@ -140,8 +147,27 @@ function getContour(
|
|
|
140
147
|
}
|
|
141
148
|
});
|
|
142
149
|
|
|
150
|
+
// next loops shift the base octave so as to mninimise the average absolute distance
|
|
151
|
+
if (contourShift !== 0) {
|
|
152
|
+
if (Number.isInteger(contourShift))
|
|
153
|
+
sortKey.forEach((c, i) => (sortKey[i] = shiftChar(c, contourShift)));
|
|
154
|
+
else {
|
|
155
|
+
let avg = totalPosition / notes.length;
|
|
156
|
+
if (avg > 0)
|
|
157
|
+
while (avg >= 7) {
|
|
158
|
+
sortKey.forEach((c, i) => (sortKey[i] = shiftChar(c, -1)));
|
|
159
|
+
avg -= 7;
|
|
160
|
+
}
|
|
161
|
+
else
|
|
162
|
+
while (avg < -0.5) {
|
|
163
|
+
sortKey.forEach((c, i) => (sortKey[i] = shiftChar(c, 1)));
|
|
164
|
+
avg += 7;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
143
169
|
const result = {
|
|
144
|
-
sortKey: sortKey.join("")
|
|
170
|
+
sortKey: sortKey.join("")
|
|
145
171
|
//debugPositions: debugPositions.join(","),
|
|
146
172
|
};
|
|
147
173
|
if (durations.length > 0) {
|
|
@@ -304,7 +330,7 @@ function pushShortNote(
|
|
|
304
330
|
const relativeDuration = duration.divide(unitLength),
|
|
305
331
|
d = {
|
|
306
332
|
i: index,
|
|
307
|
-
d: relativeDuration.den
|
|
333
|
+
d: relativeDuration.den
|
|
308
334
|
};
|
|
309
335
|
if (relativeDuration.num !== 1) {
|
|
310
336
|
d.n = relativeDuration.num;
|
|
@@ -324,10 +350,10 @@ function getEncodedFromNote(note, tonalBase, tied, previousPosition) {
|
|
|
324
350
|
return {
|
|
325
351
|
encoded: tied && position === previousPosition ? encodedHeld : encoded,
|
|
326
352
|
encodedHeld,
|
|
327
|
-
position
|
|
353
|
+
position
|
|
328
354
|
};
|
|
329
355
|
}
|
|
330
356
|
|
|
331
357
|
module.exports = {
|
|
332
|
-
getContour
|
|
358
|
+
getContour
|
|
333
359
|
};
|