@goplayerjuggler/abc-tools 1.0.18 → 1.0.19
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/manipulator.js +260 -48
- package/src/parse/accidental-helpers.js +455 -0
- package/src/parse/getMetadata.js +37 -15
- package/src/parse/token-utils.js +2 -2
package/package.json
CHANGED
package/src/manipulator.js
CHANGED
|
@@ -4,6 +4,14 @@ const { parseAbc, getMeter, getUnitLength } = require("./parse/parser.js");
|
|
|
4
4
|
const { getBarInfo } = require("./parse/getBarInfo.js");
|
|
5
5
|
const { getHeaderValue } = require("./parse/header-parser.js");
|
|
6
6
|
|
|
7
|
+
const {
|
|
8
|
+
getKeySignatureAccidentals,
|
|
9
|
+
getBarAccidentals,
|
|
10
|
+
addAccidentalsForMergedBar,
|
|
11
|
+
removeRedundantAccidentals,
|
|
12
|
+
reconstructMusicFromTokens
|
|
13
|
+
} = require("./parse/accidental-helpers.js");
|
|
14
|
+
|
|
7
15
|
// ============================================================================
|
|
8
16
|
// ABC manipulation functions
|
|
9
17
|
// ============================================================================
|
|
@@ -146,22 +154,25 @@ function hasAnacrucis(abc) {
|
|
|
146
154
|
* - Converts variant ending markers from |1, |2 to [1, [2 format
|
|
147
155
|
* - Respects section breaks (||, :|, etc.) and resets pairing after them
|
|
148
156
|
* - Handles bars starting with variant endings by keeping the bar line after them
|
|
157
|
+
* - Adds accidentals when merging bars to restore key signature defaults
|
|
149
158
|
*
|
|
150
159
|
* When going from large to small meters (e.g., 4/2→4/4):
|
|
151
160
|
* - Inserts bar lines at halfway points within each bar
|
|
152
161
|
* - Preserves variant ending markers in [1, [2 format (does not convert back to |1, |2)
|
|
153
162
|
* - Inserts bar lines before variant endings when they occur at the split point
|
|
163
|
+
* - Removes redundant accidentals in the second half of split bars
|
|
154
164
|
*
|
|
155
165
|
* This is nearly a true inverse operation - going there and back preserves musical content
|
|
156
166
|
* but may change spacing around bar lines and normalises variant ending syntax to [1, [2 format.
|
|
157
|
-
* Correctly handles anacrusis (pickup bars), multi-bar variant endings, partial bars,
|
|
167
|
+
* Correctly handles anacrusis (pickup bars), multi-bar variant endings, partial bars, preserves line breaks,
|
|
168
|
+
* and manages accidentals correctly when bars are merged or split.
|
|
158
169
|
*
|
|
159
170
|
* @param {string} abc - ABC notation string
|
|
160
171
|
* @param {Array<number>} smallMeter - The smaller meter signature [numerator, denominator]
|
|
161
172
|
* @param {Array<number>} largeMeter - The larger meter signature [numerator, denominator]
|
|
162
173
|
* @param {Array<number>} currentMeter - The current meter signature [numerator, denominator] of the abc tune - may be omitted. (If omitted, it gets fetched from `abc`)
|
|
163
174
|
* @returns {string} ABC notation with toggled meter
|
|
164
|
-
* @throws {Error} If the current meter doesn
|
|
175
|
+
* @throws {Error} If the current meter doesn't match either smallMeter or largeMeter
|
|
165
176
|
*/
|
|
166
177
|
function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
167
178
|
if (!currentMeter) currentMeter = getMeter(abc);
|
|
@@ -197,6 +208,11 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
197
208
|
|
|
198
209
|
if (isSmall) {
|
|
199
210
|
// Going from small to large: remove every other complete musical bar line
|
|
211
|
+
|
|
212
|
+
// Get initial key and build key map
|
|
213
|
+
const initialKey = getHeaderValue(abc, "K");
|
|
214
|
+
const keyAtBar = getKeyAtEachBar(barLines, initialKey);
|
|
215
|
+
|
|
200
216
|
// Get bar info to understand musical structure
|
|
201
217
|
getBarInfo(bars, barLines, meter, {
|
|
202
218
|
barNumbers: true,
|
|
@@ -215,27 +231,6 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
215
231
|
const barLineDecisions = new Map();
|
|
216
232
|
const barLinesToConvert = new Map(); // variant markers to convert from |N to [N
|
|
217
233
|
|
|
218
|
-
//Discarded
|
|
219
|
-
// // Map bar numbers to their sequential index among complete bars
|
|
220
|
-
// // This handles cases where bar numbers skip (due to anacrusis or partials)
|
|
221
|
-
// const completeBarIndexByNumber = new Map();
|
|
222
|
-
// let completeBarIndex = 0;
|
|
223
|
-
|
|
224
|
-
// for (let i = 0; i < barLines.length; i++) {
|
|
225
|
-
// const barLine = barLines[i];
|
|
226
|
-
// if (
|
|
227
|
-
// barLine.barNumber !== null
|
|
228
|
-
// //&& !barLine.isSectionBreak
|
|
229
|
-
// ) {
|
|
230
|
-
// const isCompleteMusicBar =
|
|
231
|
-
// !barLine.isPartial || barLine.completesMusicBar === true;
|
|
232
|
-
// if (isCompleteMusicBar) {
|
|
233
|
-
// completeBarIndexByNumber.set(barLine.barNumber, completeBarIndex);
|
|
234
|
-
// completeBarIndex++;
|
|
235
|
-
// }
|
|
236
|
-
// }
|
|
237
|
-
// }
|
|
238
|
-
|
|
239
234
|
const hasAnacrucis = hasAnacrucisFromParsed(null, barLines);
|
|
240
235
|
|
|
241
236
|
for (let i = 0; i < barLines.length; i++) {
|
|
@@ -256,7 +251,6 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
256
251
|
// Section breaks are always kept
|
|
257
252
|
if (barLine.isSectionBreak) {
|
|
258
253
|
barLineDecisions.set(i, { action: "keep" });
|
|
259
|
-
// Don't count section breaks - they're structural markers, not part of pairing
|
|
260
254
|
continue;
|
|
261
255
|
}
|
|
262
256
|
|
|
@@ -273,10 +267,6 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
273
267
|
// This is a complete bar - use its barNumber to decide
|
|
274
268
|
// Without anacrucis: Remove complete bars with even barNumber (0, 2, 4, ...), keep odd ones (1, 3, 5, ...)
|
|
275
269
|
// With anacrucis: the other way round!
|
|
276
|
-
|
|
277
|
-
//Discarded
|
|
278
|
-
// const index = completeBarIndexByNumber.get(barLine.barNumber);
|
|
279
|
-
// if (index !== undefined && index % 2 === 0) {
|
|
280
270
|
const remove = hasAnacrucis
|
|
281
271
|
? barLine.barNumber % 2 !== 0
|
|
282
272
|
: barLine.barNumber % 2 === 0;
|
|
@@ -305,25 +295,87 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
305
295
|
}
|
|
306
296
|
}
|
|
307
297
|
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
298
|
+
// === ACCIDENTAL HANDLING ===
|
|
299
|
+
// Build map of bars to replace: bar start position -> {originalEnd, replacementText}
|
|
300
|
+
|
|
301
|
+
const barReplacements = new Map();
|
|
302
|
+
|
|
303
|
+
for (let i = 0; i < barLines.length; i++) {
|
|
304
|
+
const decision = barLineDecisions.get(i);
|
|
305
|
+
|
|
306
|
+
if (decision && decision.action === "remove") {
|
|
307
|
+
// Find which bar comes after this bar line
|
|
308
|
+
const barLineEnd = barLines[i].sourceIndex + barLines[i].sourceLength;
|
|
309
|
+
|
|
310
|
+
let bar2Idx = -1;
|
|
311
|
+
for (let b = 0; b < bars.length; b++) {
|
|
312
|
+
if (bars[b].length > 0 && bars[b][0].sourceIndex >= barLineEnd) {
|
|
313
|
+
bar2Idx = b;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Find the bar that ends at or before this bar line
|
|
319
|
+
let bar1Idx = -1;
|
|
320
|
+
for (let b = bars.length - 1; b >= 0; b--) {
|
|
321
|
+
if (bars[b].length > 0) {
|
|
322
|
+
const barEnd =
|
|
323
|
+
bars[b][bars[b].length - 1].sourceIndex +
|
|
324
|
+
bars[b][bars[b].length - 1].sourceLength;
|
|
325
|
+
if (barEnd <= barLines[i].sourceIndex) {
|
|
326
|
+
bar1Idx = b;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (bar1Idx >= 0 && bar2Idx >= 0 && bar2Idx < bars.length) {
|
|
333
|
+
// Get current key
|
|
334
|
+
const currentKey = keyAtBar.get(bar1Idx) || initialKey;
|
|
335
|
+
const keyAccidentals = getKeySignatureAccidentals(
|
|
336
|
+
currentKey,
|
|
337
|
+
normaliseKey
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Get accidentals at end of bar 1
|
|
341
|
+
const firstBarAccidentals = getBarAccidentals(
|
|
342
|
+
bars[bar1Idx],
|
|
343
|
+
keyAccidentals
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Modify bar 2 tokens
|
|
347
|
+
const modifiedBar2Tokens = addAccidentalsForMergedBar(
|
|
348
|
+
bars[bar2Idx],
|
|
349
|
+
firstBarAccidentals,
|
|
350
|
+
keyAccidentals,
|
|
351
|
+
musicText
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Reconstruct bar 2 text
|
|
355
|
+
const bar2Start = bars[bar2Idx][0].sourceIndex;
|
|
356
|
+
const bar2End =
|
|
357
|
+
bars[bar2Idx][bars[bar2Idx].length - 1].sourceIndex +
|
|
358
|
+
bars[bar2Idx][bars[bar2Idx].length - 1].sourceLength;
|
|
359
|
+
|
|
360
|
+
const modifiedText = reconstructMusicFromTokens(
|
|
361
|
+
modifiedBar2Tokens,
|
|
362
|
+
musicText
|
|
363
|
+
);
|
|
364
|
+
// console.log(
|
|
365
|
+
// `Replacement text: "${modifiedText}" (length: ${modifiedText.length})`
|
|
366
|
+
// );
|
|
367
|
+
// console.log(`First char code: ${modifiedText.charCodeAt(0)}`);
|
|
368
|
+
|
|
369
|
+
// Store replacement: when we reach bar2Start, replace until bar2End with modifiedText
|
|
370
|
+
barReplacements.set(bar2Start, {
|
|
371
|
+
originalEnd: bar2End,
|
|
372
|
+
replacementText: modifiedText
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// === END ACCIDENTAL HANDLING ===
|
|
327
379
|
|
|
328
380
|
// Reconstruct music
|
|
329
381
|
let newMusic = "";
|
|
@@ -361,6 +413,23 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
361
413
|
skipLength++;
|
|
362
414
|
}
|
|
363
415
|
pos += skipLength;
|
|
416
|
+
|
|
417
|
+
// NOW check if the bar after this removed bar line needs replacement
|
|
418
|
+
if (barReplacements.has(pos)) {
|
|
419
|
+
// const replacement = barReplacements.get(pos);
|
|
420
|
+
// newMusic += replacement.replacementText;
|
|
421
|
+
// pos = replacement.originalEnd;
|
|
422
|
+
|
|
423
|
+
const replacement = barReplacements.get(pos);
|
|
424
|
+
// console.log(`REPLACING: adding "${replacement.replacementText}"`);
|
|
425
|
+
// console.log(
|
|
426
|
+
// `REPLACING: jumping from ${pos} to ${replacement.originalEnd}`
|
|
427
|
+
// );
|
|
428
|
+
newMusic += replacement.replacementText;
|
|
429
|
+
pos = replacement.originalEnd;
|
|
430
|
+
// console.log(`newMusic so far: "${newMusic}"`);
|
|
431
|
+
}
|
|
432
|
+
|
|
364
433
|
continue;
|
|
365
434
|
} else if (decision && decision.action === "keep") {
|
|
366
435
|
// Keep this bar line
|
|
@@ -378,17 +447,134 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
378
447
|
return `${newHeaders.join("\n")}\n${newMusic}`;
|
|
379
448
|
} else {
|
|
380
449
|
// Going from large to small: add bar lines at midpoints
|
|
450
|
+
|
|
451
|
+
// Get initial key and build key map
|
|
452
|
+
const initialKey = getHeaderValue(abc, "K");
|
|
453
|
+
const keyAtBar = getKeyAtEachBar(barLines, initialKey);
|
|
454
|
+
|
|
381
455
|
const barInfo = getBarInfo(bars, barLines, meter, {
|
|
382
456
|
divideBarsBy: 2
|
|
383
457
|
});
|
|
384
458
|
|
|
385
459
|
const { midpoints } = barInfo;
|
|
386
460
|
|
|
461
|
+
// === ACCIDENTAL HANDLING ===
|
|
462
|
+
// Build map of bar sections to replace
|
|
463
|
+
|
|
464
|
+
const barReplacements = new Map();
|
|
465
|
+
|
|
466
|
+
for (let i = 0; i < bars.length; i++) {
|
|
467
|
+
const bar = bars[i];
|
|
468
|
+
if (bar.length === 0) continue;
|
|
469
|
+
|
|
470
|
+
const barStart = bar[0].sourceIndex;
|
|
471
|
+
const barEnd =
|
|
472
|
+
bar[bar.length - 1].sourceIndex + bar[bar.length - 1].sourceLength;
|
|
473
|
+
const midpoint = midpoints.find((mp) => mp > barStart && mp < barEnd);
|
|
474
|
+
|
|
475
|
+
if (midpoint) {
|
|
476
|
+
// Get current key
|
|
477
|
+
const currentKey = keyAtBar.get(i) || initialKey;
|
|
478
|
+
const keyAccidentals = getKeySignatureAccidentals(
|
|
479
|
+
currentKey,
|
|
480
|
+
normaliseKey
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// Split into first and second half
|
|
484
|
+
const firstHalfTokens = bar.filter((t) => t.sourceIndex < midpoint);
|
|
485
|
+
const secondHalfTokens = bar.filter((t) => t.sourceIndex >= midpoint);
|
|
486
|
+
|
|
487
|
+
if (secondHalfTokens.length > 0) {
|
|
488
|
+
// Get accidentals from first half
|
|
489
|
+
const firstHalfAccidentals = getBarAccidentals(
|
|
490
|
+
firstHalfTokens,
|
|
491
|
+
keyAccidentals
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// Remove redundant accidentals from second half
|
|
495
|
+
const modifiedSecondHalf = removeRedundantAccidentals(
|
|
496
|
+
secondHalfTokens,
|
|
497
|
+
firstHalfAccidentals,
|
|
498
|
+
keyAccidentals
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// Reconstruct
|
|
502
|
+
const secondHalfStart = secondHalfTokens[0].sourceIndex;
|
|
503
|
+
const secondHalfEnd =
|
|
504
|
+
secondHalfTokens[secondHalfTokens.length - 1].sourceIndex +
|
|
505
|
+
secondHalfTokens[secondHalfTokens.length - 1].sourceLength;
|
|
506
|
+
|
|
507
|
+
const modifiedText = reconstructMusicFromTokens(
|
|
508
|
+
modifiedSecondHalf,
|
|
509
|
+
musicText
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// console.log(
|
|
513
|
+
// `Replacement text: "${modifiedText}" (length: ${modifiedText.length})`
|
|
514
|
+
// );
|
|
515
|
+
// console.log(`First char code: ${modifiedText.charCodeAt(0)}`);
|
|
516
|
+
|
|
517
|
+
barReplacements.set(secondHalfStart, {
|
|
518
|
+
originalEnd: secondHalfEnd,
|
|
519
|
+
replacementText: modifiedText
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Reconstruct music with replacements
|
|
526
|
+
let newMusic = "";
|
|
527
|
+
let pos = 0;
|
|
528
|
+
|
|
529
|
+
while (pos < musicText.length) {
|
|
530
|
+
// Check if we're at a bar section that needs replacement
|
|
531
|
+
if (barReplacements.has(pos)) {
|
|
532
|
+
// const replacement = barReplacements.get(pos);
|
|
533
|
+
// newMusic += replacement.replacementText;
|
|
534
|
+
// pos = replacement.originalEnd;
|
|
535
|
+
// continue;
|
|
536
|
+
|
|
537
|
+
const replacement = barReplacements.get(pos);
|
|
538
|
+
// console.log(`REPLACING: adding "${replacement.replacementText}"`);
|
|
539
|
+
// console.log(
|
|
540
|
+
// `REPLACING: jumping from ${pos} to ${replacement.originalEnd}`
|
|
541
|
+
// );
|
|
542
|
+
newMusic += replacement.replacementText;
|
|
543
|
+
pos = replacement.originalEnd;
|
|
544
|
+
// console.log(`newMusic so far: "${newMusic}"`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Regular character
|
|
548
|
+
newMusic += musicText[pos];
|
|
549
|
+
pos++;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// === END ACCIDENTAL HANDLING ===
|
|
553
|
+
|
|
387
554
|
// Insert bar lines at calculated positions
|
|
388
555
|
const insertionPoints = [...midpoints].sort((a, b) => b - a);
|
|
389
|
-
|
|
556
|
+
|
|
557
|
+
// Adjust positions based on replacements
|
|
558
|
+
const adjustedInsertionPoints = [];
|
|
390
559
|
|
|
391
560
|
for (const pos of insertionPoints) {
|
|
561
|
+
let adjustedPos = pos;
|
|
562
|
+
|
|
563
|
+
// Calculate offset from replacements before this position
|
|
564
|
+
for (const [replStart, replInfo] of barReplacements.entries()) {
|
|
565
|
+
if (replStart < pos) {
|
|
566
|
+
const originalLength = replInfo.originalEnd - replStart;
|
|
567
|
+
const newLength = replInfo.replacementText.length;
|
|
568
|
+
adjustedPos += newLength - originalLength;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
adjustedInsertionPoints.push(adjustedPos);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Insert bar lines (in reverse order to maintain positions)
|
|
576
|
+
adjustedInsertionPoints.sort((a, b) => b - a);
|
|
577
|
+
for (const pos of adjustedInsertionPoints) {
|
|
392
578
|
newMusic = newMusic.substring(0, pos) + "| " + newMusic.substring(pos);
|
|
393
579
|
}
|
|
394
580
|
|
|
@@ -396,6 +582,32 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
396
582
|
}
|
|
397
583
|
}
|
|
398
584
|
|
|
585
|
+
/**
|
|
586
|
+
* Build a map of bar index to active key signature
|
|
587
|
+
* Tracks key changes from barLines[].newKey
|
|
588
|
+
*
|
|
589
|
+
* @param {Array} barLines - Bar line array from parseAbc
|
|
590
|
+
* @param {string} initialKey - Initial K: header value
|
|
591
|
+
* @returns {Map<number, string>} - Map from bar index to key signature string
|
|
592
|
+
*/
|
|
593
|
+
function getKeyAtEachBar(barLines, initialKey) {
|
|
594
|
+
const keyMap = new Map();
|
|
595
|
+
let currentKey = initialKey;
|
|
596
|
+
|
|
597
|
+
// Bar index 0 is before the first bar line (or at it if there's an initial bar line)
|
|
598
|
+
keyMap.set(0, currentKey);
|
|
599
|
+
|
|
600
|
+
for (let i = 0; i < barLines.length; i++) {
|
|
601
|
+
if (barLines[i].newKey) {
|
|
602
|
+
currentKey = barLines[i].newKey;
|
|
603
|
+
}
|
|
604
|
+
// The key after this bar line applies to the next bar
|
|
605
|
+
keyMap.set(i + 1, currentKey);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return keyMap;
|
|
609
|
+
}
|
|
610
|
+
|
|
399
611
|
/**
|
|
400
612
|
* Toggle between M:4/4 and M:4/2 by surgically adding/removing bar lines
|
|
401
613
|
* This is a true inverse operation - going there and back preserves the ABC exactly
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accidental handling helpers for toggleMeterDoubling
|
|
3
|
+
* Uses existing parsed token structure from parseAbc
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract accidentals implied by a key signature
|
|
8
|
+
* @param {string} keyHeader - The K: header value (e.g., "D dorian", "F# minor")
|
|
9
|
+
* @param {Function} normaliseKey - The normaliseKey function from manipulator.js
|
|
10
|
+
* @returns {Map<string, string>} - Map of note letter to accidental ('^', '_', or null for natural)
|
|
11
|
+
*/
|
|
12
|
+
function getKeySignatureAccidentals(keyHeader, normaliseKey) {
|
|
13
|
+
// Parse key using existing normaliseKey
|
|
14
|
+
const parsed = normaliseKey(keyHeader);
|
|
15
|
+
const tonic = parsed[0];
|
|
16
|
+
const mode = parsed[1];
|
|
17
|
+
|
|
18
|
+
// Remove unicode accidentals from tonic to get base note
|
|
19
|
+
const baseNote = tonic.replace(/[♯♭]/g, "");
|
|
20
|
+
const tonicAccidental =
|
|
21
|
+
tonic.length > 1 ? (tonic.includes("♯") ? "^" : "_") : null;
|
|
22
|
+
|
|
23
|
+
// Semitone positions for each natural note
|
|
24
|
+
const noteValues = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
25
|
+
|
|
26
|
+
// Mode patterns (semitones from tonic)
|
|
27
|
+
const modePatterns = {
|
|
28
|
+
major: [0, 2, 4, 5, 7, 9, 11], // Ionian
|
|
29
|
+
minor: [0, 2, 3, 5, 7, 8, 10], // Aeolian
|
|
30
|
+
mixolydian: [0, 2, 4, 5, 7, 9, 10],
|
|
31
|
+
dorian: [0, 2, 3, 5, 7, 9, 10],
|
|
32
|
+
phrygian: [0, 1, 3, 5, 7, 8, 10],
|
|
33
|
+
lydian: [0, 2, 4, 6, 7, 9, 11],
|
|
34
|
+
locrian: [0, 1, 3, 5, 6, 8, 10]
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const pattern = modePatterns[mode] || modePatterns.major;
|
|
38
|
+
const notes = ["C", "D", "E", "F", "G", "A", "B"];
|
|
39
|
+
|
|
40
|
+
// Calculate tonic's actual semitone position
|
|
41
|
+
let tonicValue = noteValues[baseNote];
|
|
42
|
+
if (tonicAccidental === "^") tonicValue = (tonicValue + 1) % 12;
|
|
43
|
+
if (tonicAccidental === "_") tonicValue = (tonicValue + 11) % 12;
|
|
44
|
+
|
|
45
|
+
// Build the scale and determine accidentals
|
|
46
|
+
const accidentals = new Map();
|
|
47
|
+
for (let i = 0; i < 7; i++) {
|
|
48
|
+
const noteLetter = notes[(notes.indexOf(baseNote) + i) % 7];
|
|
49
|
+
const expectedValue = (tonicValue + pattern[i]) % 12;
|
|
50
|
+
const naturalValue = noteValues[noteLetter];
|
|
51
|
+
|
|
52
|
+
const diff = (expectedValue - naturalValue + 12) % 12;
|
|
53
|
+
if (diff === 1) {
|
|
54
|
+
accidentals.set(noteLetter, "^"); // Sharp
|
|
55
|
+
} else if (diff === 11) {
|
|
56
|
+
accidentals.set(noteLetter, "_"); // Flat
|
|
57
|
+
}
|
|
58
|
+
// diff === 0 means natural (no entry)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return accidentals;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract accidental and note information from a parsed note token
|
|
66
|
+
* Handles regular notes and chords in brackets
|
|
67
|
+
*
|
|
68
|
+
* @param {object} token - Parsed token object from parseAbc
|
|
69
|
+
* @returns {Array<object>} - Array of {noteLetter, octaveMarkers, accidental} objects
|
|
70
|
+
*/
|
|
71
|
+
function extractNoteInfo(token) {
|
|
72
|
+
if (!token.pitch) return [];
|
|
73
|
+
|
|
74
|
+
const result = [];
|
|
75
|
+
|
|
76
|
+
// Handle chord in brackets [CEG]
|
|
77
|
+
if (token.isChord && token.chordNotes) {
|
|
78
|
+
// Process each note in the chord
|
|
79
|
+
for (const chordNote of token.chordNotes) {
|
|
80
|
+
const info = extractNoteInfo(chordNote);
|
|
81
|
+
result.push(...info);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Regular note - extract from the token string
|
|
87
|
+
// Format: [decorations][accidental]pitch[octave][duration][tie]
|
|
88
|
+
// We need to extract the accidental, pitch letter, and octave markers
|
|
89
|
+
|
|
90
|
+
const tokenStr = token.token;
|
|
91
|
+
|
|
92
|
+
// Match pattern: optional accidental (=, ^, _, ^^, __) followed by pitch followed by octave markers
|
|
93
|
+
const noteMatch = tokenStr.match(/(__|_|=|\^\^|\^)?([A-Ga-g])([',]*)/);
|
|
94
|
+
|
|
95
|
+
if (noteMatch) {
|
|
96
|
+
const accidental = noteMatch[1] || null;
|
|
97
|
+
const noteLetter = noteMatch[2];
|
|
98
|
+
const octaveMarkers = noteMatch[3] || "";
|
|
99
|
+
|
|
100
|
+
result.push({
|
|
101
|
+
noteLetter,
|
|
102
|
+
octaveMarkers,
|
|
103
|
+
accidental,
|
|
104
|
+
noteWithOctave: noteLetter + octaveMarkers
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Track accidentals in effect within a bar using parsed tokens
|
|
113
|
+
* @param {Array<object>} barTokens - Array of parsed tokens from a bar
|
|
114
|
+
* @param {Map<string, string>} keyAccidentals - Accidentals from key signature
|
|
115
|
+
* @returns {Map<string, string>} - Map of note (with octave) to its current accidental state
|
|
116
|
+
*/
|
|
117
|
+
function getBarAccidentals(barTokens, keyAccidentals) {
|
|
118
|
+
const accidentals = new Map();
|
|
119
|
+
|
|
120
|
+
for (const token of barTokens) {
|
|
121
|
+
// Skip non-note tokens
|
|
122
|
+
if (
|
|
123
|
+
token.isSilence ||
|
|
124
|
+
token.isDummy ||
|
|
125
|
+
token.isInlineField ||
|
|
126
|
+
token.isChordSymbol ||
|
|
127
|
+
token.isTuple ||
|
|
128
|
+
token.isBrokenRhythm ||
|
|
129
|
+
token.isVariantEnding ||
|
|
130
|
+
token.isDecoration ||
|
|
131
|
+
token.isGraceNote
|
|
132
|
+
) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const noteInfos = extractNoteInfo(token);
|
|
137
|
+
|
|
138
|
+
for (const {
|
|
139
|
+
noteLetter,
|
|
140
|
+
octaveMarkers,
|
|
141
|
+
accidental,
|
|
142
|
+
noteWithOctave
|
|
143
|
+
} of noteInfos) {
|
|
144
|
+
const baseNoteLetter = noteLetter.toUpperCase();
|
|
145
|
+
|
|
146
|
+
if (accidental) {
|
|
147
|
+
// Explicit accidental - record it
|
|
148
|
+
accidentals.set(noteWithOctave, accidental);
|
|
149
|
+
} else if (!accidentals.has(noteWithOctave)) {
|
|
150
|
+
// No explicit accidental, use key signature default
|
|
151
|
+
const keyAccidental = keyAccidentals.get(baseNoteLetter) || null;
|
|
152
|
+
accidentals.set(noteWithOctave, keyAccidental);
|
|
153
|
+
}
|
|
154
|
+
// If accidental already set for this note in this bar, it carries over
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return accidentals;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Add correct accidentals when merging bars
|
|
163
|
+
* Modifies the tokens in the second bar to add accidentals where needed
|
|
164
|
+
*
|
|
165
|
+
* @param {Array<object>} secondBarTokens - Array of parsed tokens from second bar
|
|
166
|
+
* @param {Map<string, string>} firstBarAccidentals - Accidentals in effect from first bar
|
|
167
|
+
* @param {Map<string, string>} keyAccidentals - Accidentals from key signature
|
|
168
|
+
* @param {string} musicText - Original music text for reconstruction
|
|
169
|
+
* @returns {Array<object>} - Modified tokens with accidentals added
|
|
170
|
+
*/
|
|
171
|
+
function addAccidentalsForMergedBar(
|
|
172
|
+
secondBarTokens,
|
|
173
|
+
firstBarAccidentals,
|
|
174
|
+
keyAccidentals,
|
|
175
|
+
musicText
|
|
176
|
+
) {
|
|
177
|
+
const modifiedTokens = [];
|
|
178
|
+
const secondBarAccidentals = new Map();
|
|
179
|
+
|
|
180
|
+
for (const token of secondBarTokens) {
|
|
181
|
+
// Non-note tokens pass through unchanged
|
|
182
|
+
if (
|
|
183
|
+
token.isSilence ||
|
|
184
|
+
token.isDummy ||
|
|
185
|
+
token.isInlineField ||
|
|
186
|
+
token.isChordSymbol ||
|
|
187
|
+
token.isTuple ||
|
|
188
|
+
token.isBrokenRhythm ||
|
|
189
|
+
token.isVariantEnding ||
|
|
190
|
+
token.isDecoration ||
|
|
191
|
+
token.isGraceNote
|
|
192
|
+
) {
|
|
193
|
+
modifiedTokens.push(token);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const noteInfos = extractNoteInfo(token);
|
|
198
|
+
|
|
199
|
+
// If no notes extracted, pass through
|
|
200
|
+
if (noteInfos.length === 0) {
|
|
201
|
+
modifiedTokens.push(token);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if we need to modify this token
|
|
206
|
+
let needsModification = false;
|
|
207
|
+
const modificationsNeeded = [];
|
|
208
|
+
|
|
209
|
+
for (const {
|
|
210
|
+
noteLetter,
|
|
211
|
+
octaveMarkers,
|
|
212
|
+
accidental,
|
|
213
|
+
noteWithOctave
|
|
214
|
+
} of noteInfos) {
|
|
215
|
+
const baseNoteLetter = noteLetter.toUpperCase();
|
|
216
|
+
const firstBarAccidental = firstBarAccidentals.get(noteWithOctave);
|
|
217
|
+
const keyAccidental = keyAccidentals.get(baseNoteLetter) || null;
|
|
218
|
+
|
|
219
|
+
if (accidental) {
|
|
220
|
+
// Has explicit accidental
|
|
221
|
+
const currentAccidental = secondBarAccidentals.get(noteWithOctave);
|
|
222
|
+
|
|
223
|
+
if (currentAccidental !== undefined) {
|
|
224
|
+
// Already set in this bar (merged context)
|
|
225
|
+
secondBarAccidentals.set(noteWithOctave, accidental);
|
|
226
|
+
modificationsNeeded.push(null);
|
|
227
|
+
} else if (accidental === firstBarAccidental) {
|
|
228
|
+
// Redundant - same as what's in effect from first bar
|
|
229
|
+
needsModification = true;
|
|
230
|
+
secondBarAccidentals.set(noteWithOctave, accidental);
|
|
231
|
+
modificationsNeeded.push("remove");
|
|
232
|
+
} else {
|
|
233
|
+
// Different accidental, keep it
|
|
234
|
+
secondBarAccidentals.set(noteWithOctave, accidental);
|
|
235
|
+
modificationsNeeded.push(null);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
// No explicit accidental
|
|
239
|
+
const currentAccidental = secondBarAccidentals.get(noteWithOctave);
|
|
240
|
+
|
|
241
|
+
if (currentAccidental !== undefined) {
|
|
242
|
+
// Already set in this bar, no modification
|
|
243
|
+
modificationsNeeded.push(null);
|
|
244
|
+
} else if (
|
|
245
|
+
firstBarAccidental !== undefined &&
|
|
246
|
+
firstBarAccidental !== keyAccidental
|
|
247
|
+
) {
|
|
248
|
+
// Bar 1 had this note with a different accidental than key signature
|
|
249
|
+
// Need to add the key signature accidental to restore it
|
|
250
|
+
needsModification = true;
|
|
251
|
+
const neededAccidental = keyAccidental || "=";
|
|
252
|
+
secondBarAccidentals.set(noteWithOctave, neededAccidental);
|
|
253
|
+
modificationsNeeded.push(neededAccidental);
|
|
254
|
+
} else {
|
|
255
|
+
// No modification needed (bar 1 didn't have this note, or had same as key)
|
|
256
|
+
secondBarAccidentals.set(noteWithOctave, keyAccidental);
|
|
257
|
+
modificationsNeeded.push(null);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (needsModification) {
|
|
263
|
+
// Reconstruct the token with added accidentals
|
|
264
|
+
const modifiedToken = { ...token };
|
|
265
|
+
let modifiedTokenStr = token.token;
|
|
266
|
+
|
|
267
|
+
// For simple single notes (not chords), we can modify directly
|
|
268
|
+
if (!token.isChord && modificationsNeeded[0]) {
|
|
269
|
+
// Find where the note letter starts (after decorations)
|
|
270
|
+
const noteMatch = modifiedTokenStr.match(
|
|
271
|
+
/(^[~.MPSTHUV!]*(?:![^!]+!)*\s*)?(__|_|=|\^\^|\^)?([A-Ga-g])/
|
|
272
|
+
);
|
|
273
|
+
if (noteMatch) {
|
|
274
|
+
const prefix = noteMatch[1] || "";
|
|
275
|
+
const existingAcc = noteMatch[2] || "";
|
|
276
|
+
const noteLetter = noteMatch[3];
|
|
277
|
+
const afterNote = modifiedTokenStr.substring(
|
|
278
|
+
prefix.length + existingAcc.length + noteLetter.length
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (modificationsNeeded[0] === "remove") {
|
|
282
|
+
// Remove the existing accidental
|
|
283
|
+
modifiedTokenStr = prefix + noteLetter + afterNote;
|
|
284
|
+
} else {
|
|
285
|
+
// Add or change the accidental
|
|
286
|
+
modifiedTokenStr =
|
|
287
|
+
prefix + modificationsNeeded[0] + noteLetter + afterNote;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// For chords, this is more complex - we'd need to modify the chord parsing
|
|
292
|
+
// For now, mark that it needs modification and handle in integration
|
|
293
|
+
|
|
294
|
+
modifiedToken.token = modifiedTokenStr;
|
|
295
|
+
modifiedToken.needsAccidentalModification = needsModification;
|
|
296
|
+
modifiedToken.accidentalModifications = modificationsNeeded;
|
|
297
|
+
modifiedTokens.push(modifiedToken);
|
|
298
|
+
} else {
|
|
299
|
+
modifiedTokens.push(token);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return modifiedTokens;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Remove redundant accidentals when splitting a bar
|
|
308
|
+
*
|
|
309
|
+
* @param {Array<object>} secondHalfTokens - Tokens from second half after split
|
|
310
|
+
* @param {Map<string, string>} firstHalfAccidentals - Accidentals from first half
|
|
311
|
+
* @param {Map<string, string>} keyAccidentals - Accidentals from key signature
|
|
312
|
+
* @returns {Array<object>} - Modified tokens with redundant accidentals removed
|
|
313
|
+
*/
|
|
314
|
+
function removeRedundantAccidentals(
|
|
315
|
+
secondHalfTokens,
|
|
316
|
+
firstHalfAccidentals,
|
|
317
|
+
keyAccidentals
|
|
318
|
+
) {
|
|
319
|
+
const modifiedTokens = [];
|
|
320
|
+
const secondHalfAccidentals = new Map();
|
|
321
|
+
|
|
322
|
+
for (const token of secondHalfTokens) {
|
|
323
|
+
// Non-note tokens pass through
|
|
324
|
+
if (
|
|
325
|
+
token.isSilence ||
|
|
326
|
+
token.isDummy ||
|
|
327
|
+
token.isInlineField ||
|
|
328
|
+
token.isChordSymbol ||
|
|
329
|
+
token.isTuple ||
|
|
330
|
+
token.isBrokenRhythm ||
|
|
331
|
+
token.isVariantEnding ||
|
|
332
|
+
token.isDecoration ||
|
|
333
|
+
token.isGraceNote
|
|
334
|
+
) {
|
|
335
|
+
modifiedTokens.push(token);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const noteInfos = extractNoteInfo(token);
|
|
340
|
+
|
|
341
|
+
if (noteInfos.length === 0) {
|
|
342
|
+
modifiedTokens.push(token);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check if we need to remove accidentals
|
|
347
|
+
let needsModification = false;
|
|
348
|
+
const modificationsNeeded = [];
|
|
349
|
+
|
|
350
|
+
for (const {
|
|
351
|
+
noteLetter,
|
|
352
|
+
octaveMarkers,
|
|
353
|
+
accidental,
|
|
354
|
+
noteWithOctave
|
|
355
|
+
} of noteInfos) {
|
|
356
|
+
const baseNoteLetter = noteLetter.toUpperCase();
|
|
357
|
+
const keyAccidental = keyAccidentals.get(baseNoteLetter) || null;
|
|
358
|
+
const currentAccidental = secondHalfAccidentals.get(noteWithOctave);
|
|
359
|
+
|
|
360
|
+
// Normalize accidentals for comparison: treat '=' and null as equivalent (both natural)
|
|
361
|
+
const normalizedAccidental = accidental === "=" ? null : accidental;
|
|
362
|
+
const normalizedKeyAccidental =
|
|
363
|
+
keyAccidental === "=" ? null : keyAccidental;
|
|
364
|
+
|
|
365
|
+
if (currentAccidental !== undefined) {
|
|
366
|
+
// Already set in second half
|
|
367
|
+
modificationsNeeded.push(null);
|
|
368
|
+
} else if (
|
|
369
|
+
normalizedAccidental === normalizedKeyAccidental &&
|
|
370
|
+
accidental !== null
|
|
371
|
+
) {
|
|
372
|
+
// Redundant - explicit accidental matches key signature
|
|
373
|
+
// (only remove explicit accidentals, not implicit ones)
|
|
374
|
+
needsModification = true;
|
|
375
|
+
secondHalfAccidentals.set(noteWithOctave, accidental);
|
|
376
|
+
modificationsNeeded.push("remove");
|
|
377
|
+
} else {
|
|
378
|
+
// Keep it
|
|
379
|
+
if (accidental) {
|
|
380
|
+
secondHalfAccidentals.set(noteWithOctave, accidental);
|
|
381
|
+
} else {
|
|
382
|
+
secondHalfAccidentals.set(noteWithOctave, keyAccidental);
|
|
383
|
+
}
|
|
384
|
+
modificationsNeeded.push(null);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (needsModification) {
|
|
389
|
+
const modifiedToken = { ...token };
|
|
390
|
+
let modifiedTokenStr = token.token;
|
|
391
|
+
|
|
392
|
+
// For simple single notes
|
|
393
|
+
if (!token.isChord && modificationsNeeded[0] === "remove") {
|
|
394
|
+
// Remove the accidental
|
|
395
|
+
const noteMatch = modifiedTokenStr.match(
|
|
396
|
+
/(^[~.MPSTHUV!]*(?:![^!]+!)*\s*)?(__|_|=|\^\^|\^)?([A-Ga-g])/
|
|
397
|
+
);
|
|
398
|
+
if (noteMatch && noteMatch[2]) {
|
|
399
|
+
const prefix = noteMatch[1] || "";
|
|
400
|
+
const accToRemove = noteMatch[2];
|
|
401
|
+
const noteLetter = noteMatch[3];
|
|
402
|
+
const afterNote = modifiedTokenStr.substring(
|
|
403
|
+
prefix.length + accToRemove.length + noteLetter.length
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
modifiedTokenStr = prefix + noteLetter + afterNote;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
modifiedToken.token = modifiedTokenStr;
|
|
411
|
+
modifiedToken.needsAccidentalModification = needsModification;
|
|
412
|
+
modifiedToken.accidentalModifications = modificationsNeeded;
|
|
413
|
+
modifiedTokens.push(modifiedToken);
|
|
414
|
+
} else {
|
|
415
|
+
modifiedTokens.push(token);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return modifiedTokens;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Reconstruct music text from tokens
|
|
424
|
+
* @param {Array<object>} tokens - Array of token objects
|
|
425
|
+
* @param {string} originalMusicText - Original music text for spacing reference
|
|
426
|
+
* @returns {string} - Reconstructed music text
|
|
427
|
+
*/
|
|
428
|
+
function reconstructMusicFromTokens(tokens, originalMusicText) {
|
|
429
|
+
if (tokens.length === 0) return "";
|
|
430
|
+
|
|
431
|
+
let result = "";
|
|
432
|
+
|
|
433
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
434
|
+
const token = tokens[i];
|
|
435
|
+
|
|
436
|
+
// Add the token (possibly modified)
|
|
437
|
+
result += token.token;
|
|
438
|
+
|
|
439
|
+
// Add spacing after token (but not after the last token)
|
|
440
|
+
if (i < tokens.length - 1 && token.spacing && token.spacing.whitespace) {
|
|
441
|
+
result += token.spacing.whitespace;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = {
|
|
449
|
+
getKeySignatureAccidentals,
|
|
450
|
+
getBarAccidentals,
|
|
451
|
+
extractNoteInfo,
|
|
452
|
+
addAccidentalsForMergedBar,
|
|
453
|
+
removeRedundantAccidentals,
|
|
454
|
+
reconstructMusicFromTokens
|
|
455
|
+
};
|
package/src/parse/getMetadata.js
CHANGED
|
@@ -1,50 +1,72 @@
|
|
|
1
1
|
const { normaliseKey } = require("../manipulator");
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Extracts data in the ABC
|
|
4
|
+
* Extracts data in the ABC _header_ T R C M K S F D N H fields
|
|
5
5
|
* and returns it in a object with properties: title, rhythm, composer, meter, key,
|
|
6
|
-
* source, url, recording, and
|
|
6
|
+
* source, url, recording, comments, and hComments.
|
|
7
7
|
* Minimal parsing, but a few features:
|
|
8
8
|
* - only extracts the first T title; subsequent T entries are ignored
|
|
9
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
|
|
10
|
+
* - the comments (i.e. the N / notes) go in an array called `comments`, with one array entry per N: line
|
|
11
|
+
* - the history (H) lines are joined up with spaces into a single line that is returned as `hComments`
|
|
12
|
+
* - the field continuation `+:` is handled only for lines following an initial H (history)
|
|
13
|
+
* - if there’s more than one T (title), then titles after the first one are returned in an array `titles`
|
|
11
14
|
* @param {*} abc
|
|
12
15
|
* @returns {object} - The header info
|
|
13
16
|
*/
|
|
14
17
|
function getMetadata(abc) {
|
|
15
18
|
const lines = abc.split("\n"),
|
|
16
19
|
metadata = {},
|
|
17
|
-
comments = []
|
|
20
|
+
comments = [],
|
|
21
|
+
hComments = [],
|
|
22
|
+
titles = [];
|
|
23
|
+
|
|
24
|
+
let currentHeader = "";
|
|
18
25
|
|
|
19
26
|
for (const line of lines) {
|
|
20
27
|
const trimmed = line.trim();
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
const trimmed2 = trimmed.substring(2).trim().replace(/%.+/, "");
|
|
29
|
+
if (trimmed.startsWith("T:")) {
|
|
30
|
+
if (!metadata.title) metadata.title = trimmed2;
|
|
31
|
+
else titles.push(trimmed2);
|
|
23
32
|
} else if (trimmed.startsWith("R:")) {
|
|
24
|
-
metadata.rhythm =
|
|
33
|
+
metadata.rhythm = trimmed2.toLowerCase();
|
|
25
34
|
} else if (trimmed.startsWith("C:")) {
|
|
26
|
-
metadata.composer =
|
|
35
|
+
metadata.composer = trimmed2;
|
|
27
36
|
} else if (trimmed.startsWith("M:")) {
|
|
28
|
-
metadata.meter =
|
|
37
|
+
metadata.meter = trimmed2;
|
|
29
38
|
} else if (trimmed.startsWith("K:")) {
|
|
30
|
-
metadata.key = normaliseKey(
|
|
39
|
+
metadata.key = normaliseKey(trimmed2).join(" ");
|
|
31
40
|
// metadata.indexOfKey = i
|
|
32
41
|
break;
|
|
33
42
|
} else if (trimmed.startsWith("S:")) {
|
|
34
|
-
metadata.source =
|
|
43
|
+
metadata.source = trimmed2;
|
|
35
44
|
} else if (trimmed.startsWith("O:")) {
|
|
36
|
-
metadata.origin =
|
|
45
|
+
metadata.origin = trimmed2;
|
|
37
46
|
} else if (trimmed.startsWith("F:")) {
|
|
38
|
-
metadata.url =
|
|
47
|
+
metadata.url = trimmed2;
|
|
39
48
|
} else if (trimmed.startsWith("D:")) {
|
|
40
|
-
metadata.recording =
|
|
49
|
+
metadata.recording = trimmed2;
|
|
41
50
|
} else if (trimmed.startsWith("N:")) {
|
|
42
|
-
comments.push(
|
|
51
|
+
comments.push(trimmed2);
|
|
52
|
+
} else if (trimmed.startsWith("H:")) {
|
|
53
|
+
currentHeader = "H";
|
|
54
|
+
hComments.push(trimmed2);
|
|
55
|
+
} else if (trimmed.startsWith("+:")) {
|
|
56
|
+
switch (currentHeader) {
|
|
57
|
+
case "H":
|
|
58
|
+
hComments.push(trimmed2);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
63
|
if (comments.length > 0) {
|
|
46
64
|
metadata.comments = comments;
|
|
47
65
|
}
|
|
66
|
+
if (hComments.length > 0) {
|
|
67
|
+
metadata.hComments = hComments.join(" ");
|
|
68
|
+
}
|
|
69
|
+
if (titles.length > 0) metadata.titles = titles;
|
|
48
70
|
|
|
49
71
|
return metadata;
|
|
50
72
|
}
|
package/src/parse/token-utils.js
CHANGED
|
@@ -38,8 +38,8 @@ const // captures not only |1 |2, but also :|1 :||1 :|2 :||2
|
|
|
38
38
|
return String.raw`(?:${this.bangDecoration}\s*)*`;
|
|
39
39
|
},
|
|
40
40
|
|
|
41
|
-
// Accidental: :, ^, _ (natural, sharp, flat)
|
|
42
|
-
accidental: String.raw`
|
|
41
|
+
// Accidental: :, ^, _ (natural, sharp, flat; and double flat/sharp; and even add rarer ones like natural+sharp)
|
|
42
|
+
accidental: String.raw`(_|\^|=|\^\^|__|==|=_|=\^)?`,
|
|
43
43
|
|
|
44
44
|
// Note pitch: A-G (lower octave), a-g (middle octave), z/x (rest), y (dummy)
|
|
45
45
|
// Or chord in brackets: [CEG], [DF#A]
|