@goplayerjuggler/abc-tools 1.0.24 → 1.1.0
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/README.md +8 -5
- package/package.json +6 -5
- package/src/incipit.js +17 -13
- package/src/index.js +8 -9
- package/src/manipulator.js +30 -3
- package/src/sort/contour-sort.js +1 -96
- package/src/sort/sort-constants.js +52 -0
- package/src/sort/sort.js +287 -0
package/README.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# abc-tools
|
|
2
|
-
|
|
3
2
|
A JavaScript library with utility functions for tunes written in ABC format. Particularly applicable to Irish, and other, traditional music.
|
|
3
|
+
|
|
4
4
|
# features
|
|
5
5
|
* contour sort algorithm
|
|
6
6
|
Sorts tunes by their contour, independent of key and mode.
|
|
7
7
|
The algorithm used for sorting is new/original, as far as I know, and is described [here](./docs/contour_sort.md)
|
|
8
|
+
* contour visualisations as SVGs
|
|
8
9
|
* Extracting initial bars and incipits
|
|
9
10
|
* Doubling/halving bar length - e.g. going from 4/4 to 4/2 and vice versa
|
|
10
|
-
This is work in progress; still
|
|
11
|
+
This is work in progress; it has a few small bugs but is still useable.
|
|
11
12
|
|
|
12
|
-
#
|
|
13
|
-
|
|
13
|
+
# where is it used
|
|
14
|
+
AFAIK at present this is only used by one other repo; a project of mine, [tuneTable](https://github.com/goplayerjuggler/tuneTable), which builds on this repo. There’s a [live demo](https://goplayerjuggler.github.io/tuneTable) featuring the contour sort - it sorts first by rhythm, then by contour.
|
|
15
|
+
|
|
16
|
+
# AI
|
|
17
|
+
A lot of the implementation was done with the help of AI, notably with Anthropic’s Claude.ai / Sonnet 4.6.
|
|
14
18
|
|
|
15
19
|
## license
|
|
16
20
|
|
|
@@ -24,7 +28,6 @@ This means you are free to use, modify, and distribute this software, but any de
|
|
|
24
28
|
npm i @goplayerjuggler/abc-tools
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
|
|
28
31
|
## contributing
|
|
29
32
|
|
|
30
33
|
Issues and pull requests welcome at https://github.com/goplayerjuggler/abc-tools
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goplayerjuggler/abc-tools",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "sorting
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "contour-sort and other sorting algorithms implementation for ABC tunes; and other tools for parsing and manipulating ABC tunes",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "jest",
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
"LICENSE"
|
|
17
17
|
],
|
|
18
18
|
"keywords": [
|
|
19
|
-
"abc",
|
|
20
|
-
"folk",
|
|
19
|
+
"abc-notation",
|
|
20
|
+
"folk-music",
|
|
21
|
+
"graphical-score",
|
|
21
22
|
"irish",
|
|
22
23
|
"melody",
|
|
23
24
|
"modal",
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
"parsing",
|
|
26
27
|
"sort",
|
|
27
28
|
"sorting",
|
|
28
|
-
"traditional",
|
|
29
|
+
"traditional-music",
|
|
29
30
|
"tune"
|
|
30
31
|
],
|
|
31
32
|
"author": "Malcolm Schonfield",
|
package/src/incipit.js
CHANGED
|
@@ -283,34 +283,38 @@ function getIncipit(data) {
|
|
|
283
283
|
|
|
284
284
|
const { withAnacrucis = true } =
|
|
285
285
|
typeof data === "string" ? { abc: data } : data;
|
|
286
|
-
|
|
286
|
+
let countAnacrucisInTotal = false;
|
|
287
287
|
if (!numBars) {
|
|
288
288
|
numBars = 2;
|
|
289
289
|
const currentMeter = getMeter(abc);
|
|
290
290
|
const unitLength = getUnitLength(abc);
|
|
291
291
|
if (!currentMeter) numBars = 2;
|
|
292
292
|
else if (
|
|
293
|
+
currentMeter[0] === 4 &&
|
|
294
|
+
currentMeter[1] === 2 &&
|
|
295
|
+
unitLength.den === 8
|
|
296
|
+
) {
|
|
297
|
+
numBars = 1; //hornpipes
|
|
298
|
+
countAnacrucisInTotal = true; //otherwise it seems a bit too long
|
|
299
|
+
} else if (
|
|
293
300
|
(currentMeter[0] === 4 &&
|
|
294
301
|
currentMeter[1] === 4 &&
|
|
295
302
|
unitLength.den === 16) ||
|
|
296
|
-
(currentMeter[0] ===
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
(currentMeter[0] === 12 && currentMeter[1] === 8)
|
|
300
|
-
) {
|
|
303
|
+
(currentMeter[0] === 12 && currentMeter[1] === 8) ||
|
|
304
|
+
(currentMeter[0] >= 12 && currentMeter[0] < 16)
|
|
305
|
+
)
|
|
301
306
|
numBars = new Fraction(3, 2);
|
|
302
|
-
|
|
307
|
+
else if (currentMeter[0] === 3 && [4, 8].indexOf(currentMeter[1]) >= 0)
|
|
303
308
|
numBars = 3;
|
|
304
|
-
|
|
309
|
+
else if (currentMeter[0] >= 16) numBars = 1;
|
|
305
310
|
}
|
|
306
311
|
abc = sanitise(abc);
|
|
307
|
-
return getFirstBars(abc, numBars, withAnacrucis,
|
|
312
|
+
return getFirstBars(abc, numBars, withAnacrucis, countAnacrucisInTotal, {
|
|
313
|
+
all: true
|
|
314
|
+
});
|
|
308
315
|
}
|
|
309
316
|
|
|
310
|
-
function getIncipitForContourGeneration(
|
|
311
|
-
abc,
|
|
312
|
-
{ numBars = new Fraction(2, 1) } = {}
|
|
313
|
-
) {
|
|
317
|
+
function getIncipitForContourGeneration(abc, { numBars = null } = {}) {
|
|
314
318
|
return getIncipit({
|
|
315
319
|
abc,
|
|
316
320
|
withAnacrucis: false,
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ABC Music Toolkit
|
|
3
|
-
* Main entry point for ABC parsing, manipulation, and sorting
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
const parser = require("./parse/parser.js");
|
|
7
2
|
const miscParser = require("./parse/misc-parser.js");
|
|
8
3
|
const manipulator = require("./manipulator.js");
|
|
9
|
-
const sort = require("./sort/
|
|
4
|
+
const sort = require("./sort/sort.js");
|
|
5
|
+
const sortConstants = require("./sort/sort-constants.js");
|
|
6
|
+
const { compare } = require("./sort/contour-sort.js");
|
|
10
7
|
const contourToSvg = require("./sort/contour-svg.js");
|
|
11
8
|
const displayContour = require("./sort/display-contour.js");
|
|
12
9
|
|
|
@@ -18,18 +15,20 @@ const { getBarInfo } = require("./parse/getBarInfo.js");
|
|
|
18
15
|
const { getMetadata } = require("./parse/getMetadata.js");
|
|
19
16
|
|
|
20
17
|
module.exports = {
|
|
21
|
-
//
|
|
18
|
+
// Get info
|
|
22
19
|
...parser,
|
|
23
20
|
...miscParser,
|
|
24
21
|
getBarInfo,
|
|
25
22
|
getMetadata,
|
|
26
23
|
...incipit,
|
|
27
24
|
|
|
28
|
-
//
|
|
25
|
+
// change things
|
|
29
26
|
...manipulator,
|
|
30
27
|
|
|
31
|
-
// Sort
|
|
28
|
+
// Sort
|
|
32
29
|
...sort,
|
|
30
|
+
...sortConstants,
|
|
31
|
+
compareContour: compare,
|
|
33
32
|
...displayContour,
|
|
34
33
|
...contourToSvg,
|
|
35
34
|
...getContour,
|
package/src/manipulator.js
CHANGED
|
@@ -725,6 +725,30 @@ function convertStandardHornpipe(
|
|
|
725
725
|
}
|
|
726
726
|
return result;
|
|
727
727
|
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Doubles the bar length of an ABC string when the
|
|
731
|
+
* ABC is eligible (as determined by canDoubleBarLength()).
|
|
732
|
+
* @param {string} abc
|
|
733
|
+
* @param {string} rhythm
|
|
734
|
+
* @returns {string}
|
|
735
|
+
*/
|
|
736
|
+
function maybeConvertStandardTune(abc, rhythm) {
|
|
737
|
+
if (!canDoubleBarLength(abc, { rhythm })) return abc;
|
|
738
|
+
switch (rhythm) {
|
|
739
|
+
case "reel":
|
|
740
|
+
return convertStandardReel(abc);
|
|
741
|
+
case "jig":
|
|
742
|
+
return convertStandardJig(abc);
|
|
743
|
+
case "polka":
|
|
744
|
+
return convertStandardPolka(abc);
|
|
745
|
+
case "hornpipe":
|
|
746
|
+
return convertStandardHornpipe(abc);
|
|
747
|
+
default:
|
|
748
|
+
return abc;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
728
752
|
function doubleBarLength(abc, comment = null) {
|
|
729
753
|
const meter = getMeter(abc);
|
|
730
754
|
if (!Array.isArray(meter) || !meter) {
|
|
@@ -1061,10 +1085,12 @@ function getFirstBars(
|
|
|
1061
1085
|
)}`;
|
|
1062
1086
|
}
|
|
1063
1087
|
|
|
1064
|
-
function canDoubleBarLength(abc) {
|
|
1065
|
-
const
|
|
1088
|
+
function canDoubleBarLength(abc, info = {}) {
|
|
1089
|
+
const {
|
|
1090
|
+
meter = getMeter(abc),
|
|
1066
1091
|
l = getUnitLength(abc),
|
|
1067
|
-
rhythm = getHeaderValue(abc, "R")
|
|
1092
|
+
rhythm = getHeaderValue(abc, "R")
|
|
1093
|
+
} = info;
|
|
1068
1094
|
if (
|
|
1069
1095
|
!rhythm ||
|
|
1070
1096
|
["reel", "hornpipe", "jig", "polka"].indexOf(rhythm.toLowerCase()) < 0
|
|
@@ -1125,6 +1151,7 @@ module.exports = {
|
|
|
1125
1151
|
filterHeaders,
|
|
1126
1152
|
getFirstBars,
|
|
1127
1153
|
hasAnacrucis,
|
|
1154
|
+
maybeConvertStandardTune,
|
|
1128
1155
|
normaliseKey,
|
|
1129
1156
|
toggleMeter_4_4_to_4_2,
|
|
1130
1157
|
toggleMeter_6_8_to_12_8,
|
package/src/sort/contour-sort.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { getIncipitForContourGeneration } = require("../incipit.js");
|
|
2
1
|
const { Fraction } = require("../math.js");
|
|
3
2
|
|
|
4
3
|
const { decodeChar, encodeToChar, silenceChar } = require("./encode.js");
|
|
@@ -176,100 +175,6 @@ function canBeCompared(tune1, tune2) {
|
|
|
176
175
|
return true;
|
|
177
176
|
}
|
|
178
177
|
|
|
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
|
-
|
|
189
|
-
/**
|
|
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.
|
|
197
|
-
*/
|
|
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
|
-
return {
|
|
209
|
-
withSvg: true
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
} = options;
|
|
213
|
-
|
|
214
|
-
const comparableMap = {};
|
|
215
|
-
//set up comparableMap s.t. all entries in each list of index i go under i_<first entry of i>
|
|
216
|
-
//that way we show the jigs with similar rhythms followed by reels etc.
|
|
217
|
-
for (let i = 0; i < comparable.length; i++) {
|
|
218
|
-
const list = comparable[i];
|
|
219
|
-
//map subsequent entries to the first entry
|
|
220
|
-
for (let j = 0; j < list.length; j++) {
|
|
221
|
-
comparableMap[list[j]] = `${i}_${list[0]}`;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
for (const tune of arr) {
|
|
226
|
-
if (!tune.contour) {
|
|
227
|
-
try {
|
|
228
|
-
const contourOptions = getContourOptions();
|
|
229
|
-
// if (
|
|
230
|
-
// tune.title?.indexOf("oldrick") >= 0 ||
|
|
231
|
-
// ) {
|
|
232
|
-
// console.log("debug");
|
|
233
|
-
// }
|
|
234
|
-
const withSwingTransform =
|
|
235
|
-
applySwingTransform.indexOf(tune.rhythm) >= 0;
|
|
236
|
-
const shortAbc = getAbcForContour(tune);
|
|
237
|
-
|
|
238
|
-
if (shortAbc) {
|
|
239
|
-
contourOptions.withSwingTransform = withSwingTransform;
|
|
240
|
-
if (Object.hasOwn(tune, "contourShift"))
|
|
241
|
-
contourOptions.contourShift = tune.contourShift;
|
|
242
|
-
tune.contour = getContour(shortAbc, contourOptions);
|
|
243
|
-
}
|
|
244
|
-
} catch (error) {
|
|
245
|
-
console.log(error);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (!tune.baseRhythm)
|
|
249
|
-
tune.baseRhythm = comparableMap[tune.rhythm] ?? tune.rhythm;
|
|
250
|
-
}
|
|
251
|
-
arr.sort((a, b) => {
|
|
252
|
-
const comparison =
|
|
253
|
-
a.baseRhythm !== b.baseRhythm
|
|
254
|
-
? a.baseRhythm < b.baseRhythm
|
|
255
|
-
? -1
|
|
256
|
-
: 1
|
|
257
|
-
: canBeCompared(a, b)
|
|
258
|
-
? compare(a.contour, b.contour)
|
|
259
|
-
: a.contour && !b.contour
|
|
260
|
-
? -1
|
|
261
|
-
: b.contour && !a.contour
|
|
262
|
-
? 1
|
|
263
|
-
: a.name !== b.name
|
|
264
|
-
? a.name < b.name
|
|
265
|
-
? -1
|
|
266
|
-
: 1
|
|
267
|
-
: 0;
|
|
268
|
-
return comparison;
|
|
269
|
-
});
|
|
270
|
-
arr.forEach((t) => delete t.baseRhythm);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
178
|
function simpleSort(arr) {
|
|
274
179
|
for (const item of arr) {
|
|
275
180
|
if (!item.contour && item.abc) {
|
|
@@ -304,7 +209,7 @@ function simpleSort(arr) {
|
|
|
304
209
|
|
|
305
210
|
module.exports = {
|
|
306
211
|
compare,
|
|
307
|
-
|
|
212
|
+
canBeCompared,
|
|
308
213
|
simpleSort,
|
|
309
214
|
decodeChar
|
|
310
215
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const DEFAULT_METER_SORT_SPECS = [
|
|
2
|
+
[
|
|
3
|
+
{ meters: [[2], [4], [8]] },
|
|
4
|
+
{
|
|
5
|
+
meters: [
|
|
6
|
+
[6, 2],
|
|
7
|
+
[6, 4]
|
|
8
|
+
],
|
|
9
|
+
unitLengths: [
|
|
10
|
+
[1, 8],
|
|
11
|
+
[1, 16]
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
[{ meters: [[3]] }],
|
|
16
|
+
[{ meters: [[6], [12]] }],
|
|
17
|
+
[{ meters: [[9], [18]] }],
|
|
18
|
+
[{ meters: [[5], [10]] }],
|
|
19
|
+
[{ meters: [[7], [14]] }],
|
|
20
|
+
[{ meters: [[11], [13]] }]
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const DEFAULT_RHYTHM_GROUPS = [
|
|
24
|
+
["jig", "slide", "single jig", "double jig"],
|
|
25
|
+
["reel", "single reel", "reel (single)", "strathspey", "double reel"],
|
|
26
|
+
["hornpipe", "barndance", "fling"]
|
|
27
|
+
];
|
|
28
|
+
const DEFAULT_NAME_PREFIXES = [
|
|
29
|
+
"The ",
|
|
30
|
+
"A ",
|
|
31
|
+
"An ",
|
|
32
|
+
/(?:(?:la )?(?:marche |bourrée |valse )|(?:le )?reel )(?:du |de la |de |à |des )?/i
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const DEFAULT_CONTOUR_OPTIONS = {
|
|
36
|
+
withSvg: true,
|
|
37
|
+
swingTransform: ["hornpipe", "barndance", "fling", "mazurka"],
|
|
38
|
+
incipitPart: "A"
|
|
39
|
+
};
|
|
40
|
+
const PREDEFINED_SORT_NAMES = [
|
|
41
|
+
"rhythmContourName",
|
|
42
|
+
"meterContourName",
|
|
43
|
+
"nameContour"
|
|
44
|
+
];
|
|
45
|
+
const sortConstants = {
|
|
46
|
+
DEFAULT_CONTOUR_OPTIONS,
|
|
47
|
+
DEFAULT_METER_SORT_SPECS,
|
|
48
|
+
DEFAULT_NAME_PREFIXES,
|
|
49
|
+
DEFAULT_RHYTHM_GROUPS,
|
|
50
|
+
PREDEFINED_SORT_NAMES
|
|
51
|
+
};
|
|
52
|
+
module.exports = { sortConstants };
|
package/src/sort/sort.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
const { compare, canBeCompared } = require("./contour-sort.js");
|
|
2
|
+
const { getContour } = require("./get-contour.js");
|
|
3
|
+
const { getIncipitForContourGeneration } = require("../incipit.js");
|
|
4
|
+
const { getMeter, getUnitLength } = require("../parse/parser.js");
|
|
5
|
+
const { sortConstants } = require("./sort-constants.js");
|
|
6
|
+
|
|
7
|
+
/// Returns the tune's meter as [n, d], or null. Uses cached _sortAbc.
|
|
8
|
+
function resolveMeter(tune) {
|
|
9
|
+
if (!tune._sortMeter) {
|
|
10
|
+
if (tune.meter !== undefined)
|
|
11
|
+
tune._sortMeter =
|
|
12
|
+
tune.meter instanceof String
|
|
13
|
+
? tune.meter?.split("/")
|
|
14
|
+
: Array.isArray(tune.meter) && tune.meter?.length
|
|
15
|
+
? tune.meter
|
|
16
|
+
: null;
|
|
17
|
+
const abc = tune._sortAbc;
|
|
18
|
+
if (!abc) tune._sortMeter = null;
|
|
19
|
+
const m = getMeter(abc);
|
|
20
|
+
tune._sortMeter = m?.length ? m : null;
|
|
21
|
+
}
|
|
22
|
+
return tune._sortMeter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Returns the tune's unit length as a Fraction, or null. Uses cached _sortAbc.
|
|
26
|
+
function resolveUnitLength(tune) {
|
|
27
|
+
if (tune.unitLength !== undefined) return tune.unitLength;
|
|
28
|
+
try {
|
|
29
|
+
return tune._sortAbc ? getUnitLength(tune._sortAbc) : null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
///Returns true if the given meter (and optionally unit length) satisfies one MeterSortSpec entry.
|
|
35
|
+
function meterMatchesSpec(meter, unitLength, spec) {
|
|
36
|
+
if (!meter) return false;
|
|
37
|
+
const [n, d] = meter;
|
|
38
|
+
const meterMatches = spec.meters.some(
|
|
39
|
+
([sn, sd]) => sn === n && (sd === undefined || sd === d)
|
|
40
|
+
);
|
|
41
|
+
if (!meterMatches) return false;
|
|
42
|
+
if (!spec.unitLengths) return true;
|
|
43
|
+
if (!unitLength) return false;
|
|
44
|
+
return spec.unitLengths.some(
|
|
45
|
+
([un, ud]) => unitLength.num === un && unitLength.den === ud
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
///Returns the 0-based index of the first matching group, or meterSortSpecs.length for the fallback group.
|
|
50
|
+
function getMeterGroup(meter, unitLength, meterSortSpecs) {
|
|
51
|
+
for (let i = 0; i < meterSortSpecs.length; i++) {
|
|
52
|
+
if (
|
|
53
|
+
meterSortSpecs[i].some((spec) =>
|
|
54
|
+
meterMatchesSpec(meter, unitLength, spec)
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
return i;
|
|
58
|
+
}
|
|
59
|
+
return meterSortSpecs.length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Returns the ABC fragment used for contour / meter resolution.
|
|
63
|
+
function getAbc_default(tune) {
|
|
64
|
+
return getIncipitForContourGeneration(
|
|
65
|
+
tune.incipit
|
|
66
|
+
? tune.incipit
|
|
67
|
+
: Array.isArray(tune.abc)
|
|
68
|
+
? tune.abc[0]
|
|
69
|
+
: tune.abc
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureContour(tune, contourOptions) {
|
|
74
|
+
if (tune.contour) return;
|
|
75
|
+
const {
|
|
76
|
+
swingTransform = sortConstants.DEFAULT_CONTOUR_OPTIONS.swingTransform,
|
|
77
|
+
incipitPart = "A",
|
|
78
|
+
...getContourOpts
|
|
79
|
+
} = contourOptions;
|
|
80
|
+
|
|
81
|
+
if (incipitPart !== "A")
|
|
82
|
+
throw new Error(`incipitPart "${incipitPart}" is not yet implemented`);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const shortAbc = tune._sortAbc;
|
|
86
|
+
if (!shortAbc) return;
|
|
87
|
+
getContourOpts.withSwingTransform = swingTransform.includes(tune.rhythm);
|
|
88
|
+
if (Object.hasOwn(tune, "contourShift"))
|
|
89
|
+
getContourOpts.contourShift = tune.contourShift;
|
|
90
|
+
tune.contour = getContour(shortAbc, getContourOpts);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function compareContour(a, b) {
|
|
96
|
+
if (a.contour && b.contour && canBeCompared(a, b))
|
|
97
|
+
return compare(a.contour, b.contour);
|
|
98
|
+
if (a.contour) return -1;
|
|
99
|
+
if (b.contour) return 1;
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns name with any matching prefix stripped, for comparison purposes only.
|
|
105
|
+
* @param {string} name
|
|
106
|
+
* @param {(string|RegExp)[]} prefixes
|
|
107
|
+
*/
|
|
108
|
+
function stripPrefix(name, prefixes) {
|
|
109
|
+
if (!name) return "";
|
|
110
|
+
for (const p of prefixes) {
|
|
111
|
+
if (p instanceof RegExp) {
|
|
112
|
+
const m = name.match(p);
|
|
113
|
+
if (m && m.index === 0) {
|
|
114
|
+
return name.slice(m[0].length);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
if (name.toLowerCase().startsWith(p.toLowerCase()))
|
|
118
|
+
return name.slice(p.length);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return name;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function annotateTunes(tunes, levels) {
|
|
125
|
+
for (const level of levels) {
|
|
126
|
+
switch (level.type) {
|
|
127
|
+
case "rhythm": {
|
|
128
|
+
const groups =
|
|
129
|
+
level.rhythmGroups ?? sortConstants.DEFAULT_RHYTHM_GROUPS;
|
|
130
|
+
const map = {};
|
|
131
|
+
groups.forEach((g, i) => g.forEach((r) => (map[r] = `${i}_${g[0]}`)));
|
|
132
|
+
tunes.forEach((t) => (t._sortRhythm = map[t.rhythm] ?? t.rhythm ?? ""));
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case "meter": {
|
|
137
|
+
const specs =
|
|
138
|
+
level.meterSortSpecs ?? sortConstants.DEFAULT_METER_SORT_SPECS;
|
|
139
|
+
const needsUnitLength = specs.some((g) => g.some((s) => s.unitLengths));
|
|
140
|
+
tunes.forEach((t) => {
|
|
141
|
+
const meter = resolveMeter(t);
|
|
142
|
+
const ul = needsUnitLength ? resolveUnitLength(t) : null;
|
|
143
|
+
t._sortMeterGroup = getMeterGroup(meter, ul, specs);
|
|
144
|
+
console.log(`${t.name}|${t.meter}|${t._sortMeterGroup}`);
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case "origin":
|
|
150
|
+
tunes.forEach((t) => {
|
|
151
|
+
const raw = t.origin ?? null;
|
|
152
|
+
t._sortOrigin = raw ? raw.split(";")[0].trim() : null;
|
|
153
|
+
});
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case "name": {
|
|
157
|
+
const prefixes =
|
|
158
|
+
level.ignoreNamePrefixes ?? sortConstants.DEFAULT_NAME_PREFIXES;
|
|
159
|
+
tunes.forEach((t) => {
|
|
160
|
+
t._sortName = stripPrefix(t.name ?? "", prefixes);
|
|
161
|
+
});
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case "contour": {
|
|
166
|
+
const opts =
|
|
167
|
+
level.contourOptions ?? sortConstants.DEFAULT_CONTOUR_OPTIONS;
|
|
168
|
+
tunes.forEach((t) => ensureContour(t, opts));
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
default:
|
|
173
|
+
throw new Error(`Unknown sort level type: "${level.type}"`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function makeLevelComparator(level) {
|
|
179
|
+
const dir = level.order === "desc" ? -1 : 1;
|
|
180
|
+
const collator =
|
|
181
|
+
level.type === "name"
|
|
182
|
+
? new Intl.Collator("en", { sensitivity: "base" })
|
|
183
|
+
: null;
|
|
184
|
+
switch (level.type) {
|
|
185
|
+
case "rhythm":
|
|
186
|
+
return (a, b) =>
|
|
187
|
+
a._sortRhythm === b._sortRhythm
|
|
188
|
+
? 0
|
|
189
|
+
: dir * (a._sortRhythm < b._sortRhythm ? -1 : 1);
|
|
190
|
+
case "meter":
|
|
191
|
+
return (a, b) =>
|
|
192
|
+
a._sortMeterGroup === b._sortMeterGroup
|
|
193
|
+
? 0
|
|
194
|
+
: dir * (a._sortMeterGroup < b._sortMeterGroup ? -1 : 1);
|
|
195
|
+
case "origin":
|
|
196
|
+
return (a, b) => {
|
|
197
|
+
const [oa, ob] = [a._sortOrigin, b._sortOrigin];
|
|
198
|
+
if (oa === ob) return 0;
|
|
199
|
+
if (!oa) return 1;
|
|
200
|
+
if (!ob) return -1;
|
|
201
|
+
return dir * (oa < ob ? -1 : 1);
|
|
202
|
+
};
|
|
203
|
+
case "name":
|
|
204
|
+
return (a, b) => {
|
|
205
|
+
const [na, nb] = [a._sortName ?? "", b._sortName ?? ""];
|
|
206
|
+
return dir * collator.compare(na, nb);
|
|
207
|
+
};
|
|
208
|
+
case "contour":
|
|
209
|
+
return (a, b) => dir * compareContour(a, b);
|
|
210
|
+
default:
|
|
211
|
+
throw new Error(`Unknown sort level type: "${level.type}"`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveLevels(options) {
|
|
216
|
+
const {
|
|
217
|
+
predefinedSort,
|
|
218
|
+
sortLevels,
|
|
219
|
+
contourOptions = sortConstants.DEFAULT_CONTOUR_OPTIONS
|
|
220
|
+
} = options;
|
|
221
|
+
if (predefinedSort && sortLevels)
|
|
222
|
+
throw new Error("Cannot specify both predefinedSort and sortLevels");
|
|
223
|
+
if (sortLevels) return sortLevels;
|
|
224
|
+
|
|
225
|
+
const contourLevel = { type: "contour", contourOptions };
|
|
226
|
+
const nameLevel = { type: "name" };
|
|
227
|
+
|
|
228
|
+
switch (predefinedSort ?? "rhythmContourName") {
|
|
229
|
+
case "rhythmContourName":
|
|
230
|
+
return [{ type: "rhythm" }, contourLevel, nameLevel];
|
|
231
|
+
case "meterContourName":
|
|
232
|
+
return [{ type: "meter" }, contourLevel, nameLevel];
|
|
233
|
+
case "nameContour":
|
|
234
|
+
return [nameLevel, contourLevel];
|
|
235
|
+
default:
|
|
236
|
+
throw new Error(`Unknown predefinedSort: "${predefinedSort}"`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {Object[]} tunes - Array of tune objects to sort in-place.
|
|
242
|
+
* @param {Object} [options]
|
|
243
|
+
* @param {function(Object): string} [options.getAbc]
|
|
244
|
+
* Returns an ABC fragment for a tune, used for contour generation and for
|
|
245
|
+
* resolving meter / unit length when those fields are absent from the tune
|
|
246
|
+
* object. Called at most once per tune; result is cached during the sort.
|
|
247
|
+
* Defaults to incipit-based extraction.
|
|
248
|
+
* @param {string} [options.predefinedSort="rhythmContourName"]
|
|
249
|
+
* Name of a predefined sort. One of "rhythmContourName", "meterContourName",
|
|
250
|
+
* "nameContour". Mutually exclusive with `sortLevels`.
|
|
251
|
+
* @param {Object[]} [options.sortLevels]
|
|
252
|
+
* Explicit sort-level descriptors. Mutually exclusive with `predefinedSort`.
|
|
253
|
+
* Each entry has the shape { type, order?, ...levelOptions } where type is
|
|
254
|
+
* one of "rhythm" | "meter" | "origin" | "name" | "contour".
|
|
255
|
+
* @param {Object} [options.contourOptions]
|
|
256
|
+
* Options forwarded to getContour(). Applied to all contour levels in
|
|
257
|
+
* predefined sorts; for custom sortLevels, set contourOptions per level.
|
|
258
|
+
* Shape: { withSvg?, swingTransform?, incipitPart?, ...getContourPassthrough }
|
|
259
|
+
*/
|
|
260
|
+
function sort(tunes, options = {}) {
|
|
261
|
+
const { getAbc = getAbc_default } = options;
|
|
262
|
+
const levels = resolveLevels(options);
|
|
263
|
+
const comparators = levels.map(makeLevelComparator);
|
|
264
|
+
|
|
265
|
+
tunes.forEach((t) => (t._sortAbc = getAbc(t)));
|
|
266
|
+
annotateTunes(tunes, levels);
|
|
267
|
+
|
|
268
|
+
tunes.sort((a, b) => {
|
|
269
|
+
for (const cmp of comparators) {
|
|
270
|
+
const r = cmp(a, b);
|
|
271
|
+
if (r !== 0) return r;
|
|
272
|
+
}
|
|
273
|
+
return 0;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const annotationKeys = [
|
|
277
|
+
"_sortAbc",
|
|
278
|
+
"_sortRhythm",
|
|
279
|
+
"_sortMeterGroup",
|
|
280
|
+
"_sortOrigin",
|
|
281
|
+
"_sortName",
|
|
282
|
+
"_sortMeter"
|
|
283
|
+
];
|
|
284
|
+
tunes.forEach((t) => annotationKeys.forEach((k) => delete t[k]));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = { sort };
|