@goplayerjuggler/abc-tools 1.0.17 → 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 +274 -40
- package/src/parse/accidental-helpers.js +455 -0
- package/src/parse/getMetadata.js +37 -15
- package/src/parse/token-utils.js +2 -2
- package/src/sort/get-contour.js +1 -1
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);
|
|
@@ -179,6 +190,7 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
179
190
|
|
|
180
191
|
const parsed = parseAbc(abc);
|
|
181
192
|
const { headerLines, barLines, musicText, bars, meter } = parsed;
|
|
193
|
+
|
|
182
194
|
// throw if there's a change of meter or unit length in the tune
|
|
183
195
|
if (barLines.find((bl) => bl.newMeter || bl.newUnitLength)) {
|
|
184
196
|
throw new Error("change of meter or unit length not handled");
|
|
@@ -196,6 +208,11 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
196
208
|
|
|
197
209
|
if (isSmall) {
|
|
198
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
|
+
|
|
199
216
|
// Get bar info to understand musical structure
|
|
200
217
|
getBarInfo(bars, barLines, meter, {
|
|
201
218
|
barNumbers: true,
|
|
@@ -214,27 +231,6 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
214
231
|
const barLineDecisions = new Map();
|
|
215
232
|
const barLinesToConvert = new Map(); // variant markers to convert from |N to [N
|
|
216
233
|
|
|
217
|
-
//Discarded
|
|
218
|
-
// // Map bar numbers to their sequential index among complete bars
|
|
219
|
-
// // This handles cases where bar numbers skip (due to anacrusis or partials)
|
|
220
|
-
// const completeBarIndexByNumber = new Map();
|
|
221
|
-
// let completeBarIndex = 0;
|
|
222
|
-
|
|
223
|
-
// for (let i = 0; i < barLines.length; i++) {
|
|
224
|
-
// const barLine = barLines[i];
|
|
225
|
-
// if (
|
|
226
|
-
// barLine.barNumber !== null
|
|
227
|
-
// //&& !barLine.isSectionBreak
|
|
228
|
-
// ) {
|
|
229
|
-
// const isCompleteMusicBar =
|
|
230
|
-
// !barLine.isPartial || barLine.completesMusicBar === true;
|
|
231
|
-
// if (isCompleteMusicBar) {
|
|
232
|
-
// completeBarIndexByNumber.set(barLine.barNumber, completeBarIndex);
|
|
233
|
-
// completeBarIndex++;
|
|
234
|
-
// }
|
|
235
|
-
// }
|
|
236
|
-
// }
|
|
237
|
-
|
|
238
234
|
const hasAnacrucis = hasAnacrucisFromParsed(null, barLines);
|
|
239
235
|
|
|
240
236
|
for (let i = 0; i < barLines.length; i++) {
|
|
@@ -255,7 +251,6 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
255
251
|
// Section breaks are always kept
|
|
256
252
|
if (barLine.isSectionBreak) {
|
|
257
253
|
barLineDecisions.set(i, { action: "keep" });
|
|
258
|
-
// Don't count section breaks - they're structural markers, not part of pairing
|
|
259
254
|
continue;
|
|
260
255
|
}
|
|
261
256
|
|
|
@@ -269,24 +264,22 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
269
264
|
continue;
|
|
270
265
|
}
|
|
271
266
|
|
|
272
|
-
// If the current bar starts with a variant, keep its bar line
|
|
273
|
-
const currentBarVariant = barStartsWithVariant.get(i);
|
|
274
|
-
if (currentBarVariant) {
|
|
275
|
-
barLineDecisions.set(i, { action: "keep" });
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
267
|
// This is a complete bar - use its barNumber to decide
|
|
280
268
|
// Without anacrucis: Remove complete bars with even barNumber (0, 2, 4, ...), keep odd ones (1, 3, 5, ...)
|
|
281
269
|
// With anacrucis: the other way round!
|
|
282
|
-
|
|
283
|
-
//Discarded
|
|
284
|
-
// const index = completeBarIndexByNumber.get(barLine.barNumber);
|
|
285
|
-
// if (index !== undefined && index % 2 === 0) {
|
|
286
270
|
const remove = hasAnacrucis
|
|
287
271
|
? barLine.barNumber % 2 !== 0
|
|
288
272
|
: barLine.barNumber % 2 === 0;
|
|
289
273
|
if (remove) {
|
|
274
|
+
// Check if current bar starts with variant
|
|
275
|
+
if (barStartsWithVariant.has(i)) {
|
|
276
|
+
const variantToken = barStartsWithVariant.get(i);
|
|
277
|
+
barLinesToConvert.set(variantToken.sourceIndex, {
|
|
278
|
+
oldLength: variantToken.sourceLength,
|
|
279
|
+
oldText: variantToken.token
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// Also check if next bar starts with variant
|
|
290
283
|
const nextBarIdx = i + 1;
|
|
291
284
|
if (nextBarIdx < bars.length && barStartsWithVariant.has(nextBarIdx)) {
|
|
292
285
|
const variantToken = barStartsWithVariant.get(nextBarIdx);
|
|
@@ -302,6 +295,88 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
302
295
|
}
|
|
303
296
|
}
|
|
304
297
|
|
|
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 ===
|
|
379
|
+
|
|
305
380
|
// Reconstruct music
|
|
306
381
|
let newMusic = "";
|
|
307
382
|
let pos = 0;
|
|
@@ -338,6 +413,23 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
338
413
|
skipLength++;
|
|
339
414
|
}
|
|
340
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
|
+
|
|
341
433
|
continue;
|
|
342
434
|
} else if (decision && decision.action === "keep") {
|
|
343
435
|
// Keep this bar line
|
|
@@ -355,17 +447,134 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
355
447
|
return `${newHeaders.join("\n")}\n${newMusic}`;
|
|
356
448
|
} else {
|
|
357
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
|
+
|
|
358
455
|
const barInfo = getBarInfo(bars, barLines, meter, {
|
|
359
456
|
divideBarsBy: 2
|
|
360
457
|
});
|
|
361
458
|
|
|
362
459
|
const { midpoints } = barInfo;
|
|
363
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
|
+
|
|
364
554
|
// Insert bar lines at calculated positions
|
|
365
555
|
const insertionPoints = [...midpoints].sort((a, b) => b - a);
|
|
366
|
-
|
|
556
|
+
|
|
557
|
+
// Adjust positions based on replacements
|
|
558
|
+
const adjustedInsertionPoints = [];
|
|
367
559
|
|
|
368
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) {
|
|
369
578
|
newMusic = newMusic.substring(0, pos) + "| " + newMusic.substring(pos);
|
|
370
579
|
}
|
|
371
580
|
|
|
@@ -373,6 +582,32 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter, currentMeter) {
|
|
|
373
582
|
}
|
|
374
583
|
}
|
|
375
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
|
+
|
|
376
611
|
/**
|
|
377
612
|
* Toggle between M:4/4 and M:4/2 by surgically adding/removing bar lines
|
|
378
613
|
* This is a true inverse operation - going there and back preserves the ABC exactly
|
|
@@ -383,10 +618,9 @@ function toggleMeter_4_4_to_4_2(abc, currentMeter) {
|
|
|
383
618
|
}
|
|
384
619
|
|
|
385
620
|
const defaultCommentForReelConversion =
|
|
386
|
-
"*abc-tools: convert
|
|
387
|
-
const defaultCommentForHornpipeConversion =
|
|
388
|
-
|
|
389
|
-
const defaultCommentForJigConversion = "*abc-tools: convert jig to M:12/8*";
|
|
621
|
+
"*abc-tools: convert to M:4/4 & L:1/16*";
|
|
622
|
+
const defaultCommentForHornpipeConversion = "*abc-tools: convert to M:4/2*";
|
|
623
|
+
const defaultCommentForJigConversion = "*abc-tools: convert to M:12/8*";
|
|
390
624
|
/**
|
|
391
625
|
* Adjusts bar lengths and L, M fields - a
|
|
392
626
|
* reel written in the normal way (M:4/4 L:1/8) is written
|
|
@@ -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]
|