@goplayerjuggler/abc-tools 1.0.2 → 1.0.3
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 +8 -7
- package/src/contour-sort.js +402 -384
- package/src/incipit.js +3 -0
- package/src/parser.js +1004 -996
package/src/parser.js
CHANGED
|
@@ -1,996 +1,1004 @@
|
|
|
1
|
-
const { Fraction } = require("./math.js");
|
|
2
|
-
|
|
3
|
-
// ============================================================================
|
|
4
|
-
// ABC PARSING UTILITIES
|
|
5
|
-
// ============================================================================
|
|
6
|
-
//
|
|
7
|
-
// SUPPORTED FEATURES (ABC v2.1):
|
|
8
|
-
// - Basic note notation (pitch, octave markers, accidentals)
|
|
9
|
-
// - Duration modifiers (explicit numbers, fractions, slashes)
|
|
10
|
-
// - Rests/silences (z, x)
|
|
11
|
-
// - Dummy note: y (for spacing/alignment)
|
|
12
|
-
// -
|
|
13
|
-
// -
|
|
14
|
-
// -
|
|
15
|
-
// -
|
|
16
|
-
// -
|
|
17
|
-
// -
|
|
18
|
-
// -
|
|
19
|
-
// -
|
|
20
|
-
// -
|
|
21
|
-
// -
|
|
22
|
-
// - Line
|
|
23
|
-
// -
|
|
24
|
-
// -
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// -
|
|
29
|
-
// -
|
|
30
|
-
// -
|
|
31
|
-
// -
|
|
32
|
-
// -
|
|
33
|
-
// -
|
|
34
|
-
// -
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* @
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
let
|
|
90
|
-
let
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
trimmed = trimmed.replace(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
// *
|
|
154
|
-
// *
|
|
155
|
-
// *
|
|
156
|
-
// *
|
|
157
|
-
// * @
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
// *
|
|
174
|
-
// *
|
|
175
|
-
// *
|
|
176
|
-
// * @
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
*
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
*
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
*
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
*
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
.replace(
|
|
259
|
-
.replace(/[
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
*
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
//
|
|
356
|
-
chord.notes
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
case
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
case
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
p
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* {
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
* {
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
* {
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
* { pitch: '
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
let
|
|
779
|
-
let
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
currentTuple
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
bars
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1
|
+
const { Fraction } = require("./math.js");
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// ABC PARSING UTILITIES
|
|
5
|
+
// ============================================================================
|
|
6
|
+
//
|
|
7
|
+
// SUPPORTED FEATURES (ABC v2.1):
|
|
8
|
+
// - Basic note notation (pitch, octave markers, accidentals)
|
|
9
|
+
// - Duration modifiers (explicit numbers, fractions, slashes)
|
|
10
|
+
// - Rests/silences (z, x)
|
|
11
|
+
// - Dummy note: y (for spacing/alignment)
|
|
12
|
+
// - Repeat notation: |:, :|, |1, |2, etc.
|
|
13
|
+
// - Bar lines: |, ||, |], [|, etc.
|
|
14
|
+
// - Decorations: symbol decorations (~.MPSTHUV) and !name! decorations
|
|
15
|
+
// - Chord symbols: "Dm7", "G", etc.
|
|
16
|
+
// - Chords (multiple notes): [CEG], [CEG]2, etc.
|
|
17
|
+
// - Annotations: "^text", "<text", etc. (parsed but position markers preserved)
|
|
18
|
+
// - Inline fields: [K:...], [L:...], [M:...], [P:...]
|
|
19
|
+
// - Inline comments: % comment text
|
|
20
|
+
// - Line continuations: \ at end of line
|
|
21
|
+
// - Beaming: tracks whitespace between notes for beam grouping
|
|
22
|
+
// - Line breaks: preserves information about newlines in music
|
|
23
|
+
// - Ties: -
|
|
24
|
+
// - Triplets and general tuples (`(p:q:r` format): (3ABC, (3A/B/C/, (3A2B2C2;
|
|
25
|
+
// - Back quotes: ` (ignored spacing for legibility, preserved in metadata)
|
|
26
|
+
//
|
|
27
|
+
// NOT YET SUPPORTED:
|
|
28
|
+
// - Grace notes: {ABC}
|
|
29
|
+
// - Slurs: ()
|
|
30
|
+
// - Lyrics: w: lines
|
|
31
|
+
// - Multiple voices: V: fields
|
|
32
|
+
// - Macros and user-defined symbols
|
|
33
|
+
// - MIDI directives
|
|
34
|
+
// - Stylesheet directives
|
|
35
|
+
// - Many header fields (only X, T, M, L, K extracted)
|
|
36
|
+
//
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
// Note degree mapping for chord topmost note detection
|
|
40
|
+
const NOTE_TO_DEGREE = { C: 0, D: 1, E: 2, F: 3, G: 4, A: 5, B: 6 };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract key signature from ABC header
|
|
44
|
+
*/
|
|
45
|
+
function getTonalBase(abc) {
|
|
46
|
+
const keyMatch = abc.match(/^K:\s*([A-G])/m);
|
|
47
|
+
if (!keyMatch) {
|
|
48
|
+
throw new Error("No key signature found in ABC");
|
|
49
|
+
}
|
|
50
|
+
return keyMatch[1].toUpperCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract meter from ABC header
|
|
55
|
+
*/
|
|
56
|
+
function getMeter(abc) {
|
|
57
|
+
const meterMatch = abc.match(/^M:\s*(\d+)\/(\d+)/m);
|
|
58
|
+
if (meterMatch) {
|
|
59
|
+
return [parseInt(meterMatch[1]), parseInt(meterMatch[2])];
|
|
60
|
+
}
|
|
61
|
+
return [4, 4]; // Default to 4/4
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract unit note length as a Fraction object
|
|
66
|
+
*/
|
|
67
|
+
function getUnitLength(abc) {
|
|
68
|
+
const lengthMatch = abc.match(/^L:\s*(\d+)\/(\d+)/m);
|
|
69
|
+
if (lengthMatch) {
|
|
70
|
+
return new Fraction(parseInt(lengthMatch[1]), parseInt(lengthMatch[2]));
|
|
71
|
+
}
|
|
72
|
+
return new Fraction(1, 8); // Default to 1/8
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Process ABC lines: extract music lines with metadata
|
|
77
|
+
* Handles comments, line continuations, and separates headers from music
|
|
78
|
+
* Preserves newline positions for layout tracking
|
|
79
|
+
*
|
|
80
|
+
* @param {string} abc - ABC notation string
|
|
81
|
+
* @returns {object} - { musicText, lineMetadata, newlinePositions, headerLines, headerEndIndex }
|
|
82
|
+
*/
|
|
83
|
+
function getMusicLines(abc) {
|
|
84
|
+
const lines = abc.split("\n");
|
|
85
|
+
const musicLines = [];
|
|
86
|
+
const lineMetadata = [];
|
|
87
|
+
const newlinePositions = [];
|
|
88
|
+
const headerLines = [];
|
|
89
|
+
let headerEndIndex = 0;
|
|
90
|
+
let inHeaders = true;
|
|
91
|
+
let currentPos = 0;
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
let trimmed = line.trim();
|
|
96
|
+
|
|
97
|
+
// Skip empty lines and comment-only lines
|
|
98
|
+
if (trimmed === "" || trimmed.startsWith("%")) {
|
|
99
|
+
if (inHeaders) {
|
|
100
|
+
headerEndIndex = i + 1;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check for header lines
|
|
106
|
+
if (inHeaders && trimmed.match(/^[A-Z]:/)) {
|
|
107
|
+
headerLines.push(line);
|
|
108
|
+
headerEndIndex = i + 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
inHeaders = false;
|
|
112
|
+
|
|
113
|
+
// Extract inline comment if present
|
|
114
|
+
const commentMatch = trimmed.match(/\s*%(.*)$/);
|
|
115
|
+
const comment = commentMatch ? commentMatch[1].trim() : null;
|
|
116
|
+
|
|
117
|
+
// Check for line continuation
|
|
118
|
+
const hasContinuation = trimmed.match(/\\\s*(%|$)/) !== null;
|
|
119
|
+
|
|
120
|
+
// Remove inline comments and line continuation marker
|
|
121
|
+
trimmed = trimmed.replace(/\s*%.*$/, "").trim();
|
|
122
|
+
trimmed = trimmed.replace(/\\\s*$/, "").trim();
|
|
123
|
+
|
|
124
|
+
if (trimmed) {
|
|
125
|
+
musicLines.push(trimmed);
|
|
126
|
+
lineMetadata.push({
|
|
127
|
+
lineIndex: i,
|
|
128
|
+
originalLine: line,
|
|
129
|
+
content: trimmed,
|
|
130
|
+
comment,
|
|
131
|
+
hasContinuation,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Track position where newline would be (unless continuation)
|
|
135
|
+
if (!hasContinuation && musicLines.length > 1) {
|
|
136
|
+
newlinePositions.push(currentPos);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
currentPos += trimmed.length + 1; // +1 for the space we'll add when joining
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
musicText: musicLines.join("\n"),
|
|
145
|
+
lineMetadata,
|
|
146
|
+
newlinePositions,
|
|
147
|
+
headerLines,
|
|
148
|
+
headerEndIndex,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// /**
|
|
153
|
+
// * Expand triplet notation into fractional durations
|
|
154
|
+
// * Converts (3ABC -> A2/3 B2/3 C2/3, etc.
|
|
155
|
+
// * Also strips back quotes (`) which are ignored spacing characters
|
|
156
|
+
// *
|
|
157
|
+
// * @param {string} music - Music text
|
|
158
|
+
// * @returns {string} - Music with expanded triplets and stripped back quotes
|
|
159
|
+
// */
|
|
160
|
+
// function expandTriplets(music) {
|
|
161
|
+
// return music
|
|
162
|
+
// // Remove back quotes (ignored spacing characters per ABC spec 4.7)
|
|
163
|
+
// .replace(/`/g, '')
|
|
164
|
+
// // Simple triplets: (3CDE -> C2/3 D2/3 E2/3
|
|
165
|
+
// .replace(/\(3:?([A-Ga-g][',]*)([A-Ga-g][',]*)([A-Ga-g][',]*)(?![/0-9])/g, '$12/3$22/3$32/3')
|
|
166
|
+
// // Triplets with slashes: (3C/D/E/ -> C1/3 D1/3 E1/3
|
|
167
|
+
// .replace(/\(3:?([A-Ga-g][',]*)\/([A-Ga-g][',]*)\/([A-Ga-g][',]*)\/(?![/0-9])/g, '$11/3$21/3$31/3')
|
|
168
|
+
// // Triplets with double length: (3C2D2E2 -> C4/3 D4/3 E4/3
|
|
169
|
+
// .replace(/\(3:?([A-Ga-g][',]*)2([A-Ga-g][',]*)2([A-Ga-g][',]*)2(?![/0-9])/g, '$14/3$24/3$34/3');
|
|
170
|
+
// }
|
|
171
|
+
|
|
172
|
+
// /**
|
|
173
|
+
// * condense fractional durations into triplet notation
|
|
174
|
+
// * Converts A2/3 B2/3 C2/3 -> (3ABC etc.
|
|
175
|
+
// *
|
|
176
|
+
// * @param {string} music - Music text
|
|
177
|
+
// * @returns {string} - Music with condensed triplets
|
|
178
|
+
// */
|
|
179
|
+
// function condenseTriplets(music) {
|
|
180
|
+
// return music
|
|
181
|
+
// // Simple triplets: C2/3D2/3E2/3 -> (3CDE
|
|
182
|
+
// .replace(/([A-Ga-g][',]*)2\/3([A-Ga-g][',]*)2\/3([A-Ga-g][',]*)2\/3/g, '(3$1$2$3')
|
|
183
|
+
// // Triplets with slashes: C1/3D1/3E1/3 -> (3C/D/E/
|
|
184
|
+
// .replace(/([A-Ga-g][',]*)1\/3([A-Ga-g][',]*)1\/3([A-Ga-g][',]*)1\/3/g, '(3$1/$2/$3/')
|
|
185
|
+
// // Triplets with double length: (3C2D2E2 <- C4/3D4/3E4/3
|
|
186
|
+
// .replace(/([A-Ga-g][',]*)1\/3([A-Ga-g][',]*)1\/3([A-Ga-g][',]*)1\/3/g, '(3$12$22$32')
|
|
187
|
+
// }
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse decorations/ornaments from a token
|
|
191
|
+
* Returns array of decoration symbols found
|
|
192
|
+
*/
|
|
193
|
+
function parseDecorations(noteStr) {
|
|
194
|
+
const decorations = [];
|
|
195
|
+
|
|
196
|
+
// Symbol decorations (prefix the note)
|
|
197
|
+
const symbolDecorations = {
|
|
198
|
+
"~": "roll",
|
|
199
|
+
".": "staccato",
|
|
200
|
+
M: "lowermordent",
|
|
201
|
+
P: "uppermordent",
|
|
202
|
+
S: "segno",
|
|
203
|
+
T: "trill",
|
|
204
|
+
H: "fermata",
|
|
205
|
+
u: "upbow",
|
|
206
|
+
v: "downbow",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
for (const [symbol, name] of Object.entries(symbolDecorations)) {
|
|
210
|
+
if (noteStr.includes(symbol)) {
|
|
211
|
+
decorations.push(name);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// !decoration! style (can be anywhere in string)
|
|
216
|
+
const bangDecorations = noteStr.match(/!([^!]+)!/g);
|
|
217
|
+
if (bangDecorations) {
|
|
218
|
+
bangDecorations.forEach((dec) => {
|
|
219
|
+
const name = dec.slice(1, -1); // Remove ! marks
|
|
220
|
+
decorations.push(name);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return decorations.length > 0 ? decorations : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Parse chord symbols from a token
|
|
229
|
+
* Returns chord symbol string or null
|
|
230
|
+
*/
|
|
231
|
+
function parseChordSymbol(noteStr) {
|
|
232
|
+
const chordMatch = noteStr.match(/"([^"]+)"/);
|
|
233
|
+
return chordMatch ? chordMatch[1] : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parse annotations from a token
|
|
238
|
+
* Returns annotation text or null
|
|
239
|
+
*/
|
|
240
|
+
function parseAnnotation(noteStr) {
|
|
241
|
+
// Annotations can be in quotes with position markers like "^text" or "<text"
|
|
242
|
+
const annotationMatch = noteStr.match(/"([<>^_@])([^"]+)"/);
|
|
243
|
+
if (annotationMatch) {
|
|
244
|
+
return {
|
|
245
|
+
position: annotationMatch[1],
|
|
246
|
+
text: annotationMatch[2],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Strip decorations, chords, and annotations from a note string
|
|
254
|
+
* Returns clean note string for duration/pitch parsing
|
|
255
|
+
*/
|
|
256
|
+
function stripExtras(noteStr) {
|
|
257
|
+
return noteStr
|
|
258
|
+
.replace(/!([^!]+)!/g, "") // Remove !decorations!
|
|
259
|
+
.replace(/"[^"]*"/g, "") // Remove "chords" and "annotations"
|
|
260
|
+
.replace(/[~.MPSTHUV]/g, ""); // Remove symbol decorations
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Analyze whitespace and back quotes after a token
|
|
265
|
+
* Returns object describing the spacing/beaming context
|
|
266
|
+
* Back quotes (`) are ignored for beaming but preserved for reconstruction
|
|
267
|
+
*/
|
|
268
|
+
function analyzeSpacing(segment, tokenEndPos) {
|
|
269
|
+
if (tokenEndPos >= segment.length) {
|
|
270
|
+
return {
|
|
271
|
+
whitespace: "",
|
|
272
|
+
backquotes: 0,
|
|
273
|
+
beamBreak: false,
|
|
274
|
+
lineBreak: false,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const remaining = segment.substring(tokenEndPos);
|
|
279
|
+
|
|
280
|
+
// Match whitespace and/or back quotes
|
|
281
|
+
const spacingMatch = remaining.match(/^([\s`]+)/);
|
|
282
|
+
|
|
283
|
+
if (!spacingMatch) {
|
|
284
|
+
return {
|
|
285
|
+
whitespace: "",
|
|
286
|
+
backquotes: 0,
|
|
287
|
+
beamBreak: false,
|
|
288
|
+
lineBreak: false,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const fullSpacing = spacingMatch[1];
|
|
293
|
+
|
|
294
|
+
// Count back quotes
|
|
295
|
+
const backquotes = (fullSpacing.match(/`/g) || []).length;
|
|
296
|
+
|
|
297
|
+
// Extract just whitespace (no back quotes)
|
|
298
|
+
const whitespace = fullSpacing.replace(/`/g, "");
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
whitespace,
|
|
302
|
+
backquotes,
|
|
303
|
+
beamBreak: whitespace.length > 1 || whitespace.includes("\n"), // Multiple spaces or newline breaks beam
|
|
304
|
+
lineBreak: whitespace.includes("\n"),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Parse ABC note to extract pitch, octave, duration, and metadata
|
|
310
|
+
* For chords in brackets, extracts the topmost note for melody contour analysis
|
|
311
|
+
*/
|
|
312
|
+
function parseNote(noteStr, unitLength, currentTuple) {
|
|
313
|
+
// Extract metadata before stripping
|
|
314
|
+
const decorations = parseDecorations(noteStr);
|
|
315
|
+
const chordSymbol = parseChordSymbol(noteStr);
|
|
316
|
+
const annotation = parseAnnotation(noteStr);
|
|
317
|
+
|
|
318
|
+
// Strip extras for core parsing
|
|
319
|
+
const cleanStr = stripExtras(noteStr);
|
|
320
|
+
|
|
321
|
+
// dummy note 'y' (invisible placeholder)
|
|
322
|
+
if (cleanStr.match(/^y$/)) {
|
|
323
|
+
return {
|
|
324
|
+
isDummy: true,
|
|
325
|
+
duration: new Fraction(0, 1, decorations, annotation),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// rest/silence
|
|
330
|
+
const silenceMatch = cleanStr.match(/^[zx]/);
|
|
331
|
+
if (silenceMatch) {
|
|
332
|
+
const duration = getDuration({
|
|
333
|
+
unitLength,
|
|
334
|
+
noteString: cleanStr,
|
|
335
|
+
currentTuple,
|
|
336
|
+
});
|
|
337
|
+
const result = { isSilence: true, duration, text: silenceMatch[0] };
|
|
338
|
+
if (decorations) {
|
|
339
|
+
result.decorations = decorations;
|
|
340
|
+
}
|
|
341
|
+
if (chordSymbol) {
|
|
342
|
+
result.chordSymbol = chordSymbol;
|
|
343
|
+
}
|
|
344
|
+
if (annotation) {
|
|
345
|
+
result.annotation = annotation;
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const tied = !!cleanStr.match(/-$/)
|
|
351
|
+
// Handle chords - extract topmost note for contour sorting
|
|
352
|
+
if (cleanStr.match(/^\[.*\]/)) {
|
|
353
|
+
const chord = parseChord(noteStr, unitLength);
|
|
354
|
+
if (chord && chord.notes && chord.notes.length > 0) {
|
|
355
|
+
// Find topmost note (highest pitch + octave)
|
|
356
|
+
let topNote = chord.notes[0];
|
|
357
|
+
for (const note of chord.notes) {
|
|
358
|
+
if (note.isSilence) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const topPos =
|
|
362
|
+
(topNote.octave || 0) * 7 +
|
|
363
|
+
(NOTE_TO_DEGREE[topNote.pitch?.toUpperCase()] || 0);
|
|
364
|
+
const notePos =
|
|
365
|
+
(note.octave || 0) * 7 +
|
|
366
|
+
(NOTE_TO_DEGREE[note.pitch?.toUpperCase()] || 0);
|
|
367
|
+
if (notePos > topPos) {
|
|
368
|
+
topNote = note;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const duration = getDuration({
|
|
373
|
+
unitLength,
|
|
374
|
+
noteString: cleanStr,
|
|
375
|
+
currentTuple,
|
|
376
|
+
});
|
|
377
|
+
topNote.duration = duration;
|
|
378
|
+
// Apply duration to all notes in chord
|
|
379
|
+
chord.notes.forEach((note) => {
|
|
380
|
+
note.duration = duration;
|
|
381
|
+
});
|
|
382
|
+
// Return top note with chord metadata
|
|
383
|
+
return {
|
|
384
|
+
...topNote,
|
|
385
|
+
annotation,
|
|
386
|
+
chordNotes: chord.notes,
|
|
387
|
+
chordSymbol: chordSymbol || chord.chordSymbol,
|
|
388
|
+
decorations: decorations || chord.decorations,
|
|
389
|
+
isChord: true,
|
|
390
|
+
tied,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// single note
|
|
396
|
+
const { pitch, octave } = getPitch(cleanStr);
|
|
397
|
+
|
|
398
|
+
const duration = getDuration({
|
|
399
|
+
unitLength,
|
|
400
|
+
noteString: cleanStr,
|
|
401
|
+
currentTuple,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const result = { pitch, octave, duration, tied };
|
|
405
|
+
if (decorations) {
|
|
406
|
+
result.decorations = decorations;
|
|
407
|
+
}
|
|
408
|
+
if (chordSymbol) {
|
|
409
|
+
result.chordSymbol = chordSymbol;
|
|
410
|
+
}
|
|
411
|
+
if (annotation) {
|
|
412
|
+
result.annotation = annotation;
|
|
413
|
+
}
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getPitch(pitchStr) {
|
|
418
|
+
const pitchMatch = pitchStr.match(/[A-Ga-g]/);
|
|
419
|
+
if (!pitchMatch) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const pitch = pitchMatch[0];
|
|
424
|
+
|
|
425
|
+
// Count octave modifiers
|
|
426
|
+
const upOctaves = (pitchStr.match(/'/g) || []).length;
|
|
427
|
+
const downOctaves = (pitchStr.match(/,/g) || []).length;
|
|
428
|
+
const octave = upOctaves - downOctaves;
|
|
429
|
+
return { pitch, octave };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Parse a chord (multiple notes in brackets)
|
|
434
|
+
* Returns array of note objects or null
|
|
435
|
+
*/
|
|
436
|
+
function parseChord(chordStr, unitLength) {
|
|
437
|
+
if (!chordStr.startsWith("[") || !chordStr.endsWith("]")) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Split into individual notes
|
|
442
|
+
const noteMatches = chordStr.match(/[=^_]?[A-Ga-g][',]*/g);
|
|
443
|
+
if (!noteMatches) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const notes = [];
|
|
448
|
+
// const clonedTuple = currentTuple ? {... currentTuple} : undefined
|
|
449
|
+
for (const noteStr of noteMatches) {
|
|
450
|
+
const note = getPitch(noteStr, unitLength);
|
|
451
|
+
if (note) {
|
|
452
|
+
notes.push(note);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
isChord: true,
|
|
457
|
+
notes,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function getDuration({ unitLength, noteString, currentTuple } = {}) {
|
|
462
|
+
// Parse duration as Fraction
|
|
463
|
+
let duration = unitLength.clone();
|
|
464
|
+
|
|
465
|
+
// Handle explicit fractions (e.g., '3/2', '2/4', '/4')
|
|
466
|
+
const fracMatch = noteString.match(/(\d+)?\/(\d+)/);
|
|
467
|
+
if (fracMatch) {
|
|
468
|
+
const n = fracMatch[1] ? parseInt(fracMatch[1]) : 1;
|
|
469
|
+
duration = unitLength.multiply(n).divide(parseInt(fracMatch[2]));
|
|
470
|
+
} else {
|
|
471
|
+
// Handle explicit multipliers (e.g., '2', '3')
|
|
472
|
+
const multMatch = noteString.match(/(\d+)(?!'[/]')/);
|
|
473
|
+
if (multMatch) {
|
|
474
|
+
duration = duration.multiply(parseInt(multMatch[1]));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Handle divisions (e.g., '/', '//', '///')
|
|
478
|
+
const divMatch = noteString.match(/\/+/);
|
|
479
|
+
if (divMatch) {
|
|
480
|
+
const slashes = divMatch[0].length;
|
|
481
|
+
duration = duration.divide(Math.pow(2, slashes));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (currentTuple) {
|
|
486
|
+
duration = duration.divide(currentTuple.p).multiply(currentTuple.q);
|
|
487
|
+
currentTuple.r--;
|
|
488
|
+
}
|
|
489
|
+
return duration;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const getTokenRegex = () =>
|
|
493
|
+
/\(\d(?::\d?){0,2}|\[([KLMP]):[^\]]+\]|"[^"]+"|(?:!([^!]+)!\s*)?[~.MPSTHUV]*[=^_]?(?:[A-Ga-gzxy]|\[[A-Ga-gzxy]+\])[',]*[0-9]*\/?[0-9]*-?|!([^!]+)!/g;
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Parse inline field from music section
|
|
497
|
+
* Returns { field, value } or null
|
|
498
|
+
*/
|
|
499
|
+
function parseInlineField(token) {
|
|
500
|
+
const fieldMatch = token.match(/^\[([KLMP]):\s*(.+)\]$/);
|
|
501
|
+
if (fieldMatch) {
|
|
502
|
+
return {
|
|
503
|
+
field: fieldMatch[1],
|
|
504
|
+
value: fieldMatch[2].trim(),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Parse tuple from music section
|
|
512
|
+
*/
|
|
513
|
+
function parseTuple(token, isCompoundTimeSignature) {
|
|
514
|
+
const tupleMatch = token.match(/^\(([2-9])(?::(\d)?)?(?::(\d)?)?$/);
|
|
515
|
+
if (tupleMatch) {
|
|
516
|
+
const pqr = {
|
|
517
|
+
p: parseInt(tupleMatch[1]),
|
|
518
|
+
q: tupleMatch[2],
|
|
519
|
+
r: tupleMatch[3],
|
|
520
|
+
};
|
|
521
|
+
const { p } = pqr;
|
|
522
|
+
let { q, r } = pqr;
|
|
523
|
+
if (q) {
|
|
524
|
+
q = parseInt(q);
|
|
525
|
+
} else {
|
|
526
|
+
switch (p) {
|
|
527
|
+
case 2:
|
|
528
|
+
q = 3;
|
|
529
|
+
break;
|
|
530
|
+
case 3:
|
|
531
|
+
q = 2;
|
|
532
|
+
break;
|
|
533
|
+
case 4:
|
|
534
|
+
q = 3;
|
|
535
|
+
break;
|
|
536
|
+
case 5:
|
|
537
|
+
case 7:
|
|
538
|
+
case 9:
|
|
539
|
+
q = isCompoundTimeSignature ? 3 : 2;
|
|
540
|
+
break;
|
|
541
|
+
case 6:
|
|
542
|
+
q = 2;
|
|
543
|
+
break;
|
|
544
|
+
case 8:
|
|
545
|
+
q = 3;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (r) {
|
|
550
|
+
r = parseInt(r);
|
|
551
|
+
} else {
|
|
552
|
+
r = p;
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
isTuple: true,
|
|
556
|
+
p,
|
|
557
|
+
q,
|
|
558
|
+
r,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Classify bar line type
|
|
566
|
+
* Returns object with type classification and properties
|
|
567
|
+
*/
|
|
568
|
+
function classifyBarLine(barLineStr) {
|
|
569
|
+
const trimmed = barLineStr.trim();
|
|
570
|
+
|
|
571
|
+
// Repeat endings
|
|
572
|
+
if (trimmed.match(/^\|[1-6]$/)) {
|
|
573
|
+
return {
|
|
574
|
+
type: "repeat-ending",
|
|
575
|
+
ending: parseInt(trimmed[1]),
|
|
576
|
+
text: barLineStr,
|
|
577
|
+
isRepeat: true,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Start repeat
|
|
582
|
+
if (trimmed.match(/^\|:/) || trimmed.match(/^\[\|/)) {
|
|
583
|
+
return {
|
|
584
|
+
type: "repeat-start",
|
|
585
|
+
text: barLineStr,
|
|
586
|
+
isRepeat: true,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// End repeat
|
|
591
|
+
if (
|
|
592
|
+
trimmed.match(/^:\|/) ||
|
|
593
|
+
(trimmed.match(/^\|\]/) && !trimmed.match(/^\|\]$/))
|
|
594
|
+
) {
|
|
595
|
+
return {
|
|
596
|
+
type: "repeat-end",
|
|
597
|
+
text: barLineStr,
|
|
598
|
+
isRepeat: true,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Double repeat
|
|
603
|
+
if (
|
|
604
|
+
trimmed.match(/^::/) ||
|
|
605
|
+
trimmed.match(/^:\|:/) ||
|
|
606
|
+
trimmed.match(/^::\|:?/) ||
|
|
607
|
+
trimmed.match(/^::\|\|:?/)
|
|
608
|
+
) {
|
|
609
|
+
return {
|
|
610
|
+
type: "repeat-both",
|
|
611
|
+
text: barLineStr,
|
|
612
|
+
isRepeat: true,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Final bar
|
|
617
|
+
if (trimmed === "|]") {
|
|
618
|
+
return {
|
|
619
|
+
type: "final",
|
|
620
|
+
text: barLineStr,
|
|
621
|
+
isRepeat: false,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Double bar
|
|
626
|
+
if (trimmed === "||") {
|
|
627
|
+
return {
|
|
628
|
+
type: "double",
|
|
629
|
+
text: barLineStr,
|
|
630
|
+
isRepeat: false,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Regular bar
|
|
635
|
+
if (trimmed === "|") {
|
|
636
|
+
return {
|
|
637
|
+
type: "regular",
|
|
638
|
+
text: barLineStr,
|
|
639
|
+
isRepeat: false,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Unknown/complex bar line
|
|
644
|
+
return {
|
|
645
|
+
type: "other",
|
|
646
|
+
text: barLineStr,
|
|
647
|
+
isRepeat: trimmed.includes(":"),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Parse ABC into structured data with bars
|
|
653
|
+
*
|
|
654
|
+
* Returns object with:
|
|
655
|
+
* {
|
|
656
|
+
* bars: Array<Array<NoteObject>>, // Array of bars, each bar is array of notes/chords/fields
|
|
657
|
+
* unitLength: Fraction, // The L: field value (default 1/8)
|
|
658
|
+
* meter: [number, number], // The M: field value (default [4,4])
|
|
659
|
+
* tonalBase: string, // The tonic from K: field (e.g., 'D', 'G')
|
|
660
|
+
* lineMetadata: Array<LineMetadata> // Info about original lines (comments, continuations)
|
|
661
|
+
* }
|
|
662
|
+
*
|
|
663
|
+
* NoteObject structure (regular note):
|
|
664
|
+
* {
|
|
665
|
+
* pitch: string, // 'A'-'G' (uppercase for low octave, lowercase for middle)
|
|
666
|
+
* octave: number, // Relative octave offset (0 = middle, +1 = high, -1 = low)
|
|
667
|
+
* duration: Fraction, // Note duration as fraction of whole note
|
|
668
|
+
* isSilence: false, // Always false for pitched notes
|
|
669
|
+
* token: string, // Original ABC token (e.g., 'D2', '^F/')
|
|
670
|
+
* spacing: { // Whitespace/beaming info after this token
|
|
671
|
+
* whitespace: string, // Actual whitespace characters (back quotes removed)
|
|
672
|
+
* backquotes: number, // Number of ` characters for reconstruction
|
|
673
|
+
* beamBreak: boolean, // True if beam should break (multiple spaces/newline)
|
|
674
|
+
* lineBreak: boolean // True if there was a newline after this token
|
|
675
|
+
* },
|
|
676
|
+
*
|
|
677
|
+
* // Optional properties (only present if applicable):
|
|
678
|
+
* decorations: Array<string>, // e.g., ['trill', 'staccato']
|
|
679
|
+
* chordSymbol: string, // e.g., 'Dm7', 'G'
|
|
680
|
+
* annotation: { // Text annotation with position
|
|
681
|
+
* position: string, // '^' (above), '_' (below), '<' (left), '>' (right), '@' (auto)
|
|
682
|
+
* text: string
|
|
683
|
+
* },
|
|
684
|
+
* isChord: true, // Present if this is a chord [CEG]
|
|
685
|
+
* chordNotes: Array<NoteObject> // All notes in the chord (when isChord=true)
|
|
686
|
+
* }
|
|
687
|
+
*
|
|
688
|
+
* NoteObject structure (silence/rest):
|
|
689
|
+
* {
|
|
690
|
+
* isSilence: true,
|
|
691
|
+
* duration: Fraction,
|
|
692
|
+
* token: string,
|
|
693
|
+
* spacing: { ... }, // Same as regular note
|
|
694
|
+
* // Optional: decorations, chordSymbol, annotation (same as above)
|
|
695
|
+
* }
|
|
696
|
+
*
|
|
697
|
+
* NoteObject structure (dummy note):
|
|
698
|
+
* {
|
|
699
|
+
* isDummy: true,
|
|
700
|
+
* duration: Fraction,
|
|
701
|
+
* token: string,
|
|
702
|
+
* spacing: { ... }
|
|
703
|
+
* }
|
|
704
|
+
*
|
|
705
|
+
* NoteObject structure (inline field change):
|
|
706
|
+
* {
|
|
707
|
+
* isInlineField: true,
|
|
708
|
+
* field: string, // 'K', 'L', 'M', or 'P'
|
|
709
|
+
* value: string, // The field value (e.g., 'G major', '3/4')
|
|
710
|
+
* token: string // Original token (e.g., '[K:G]')
|
|
711
|
+
* spacing: { ... }
|
|
712
|
+
* }
|
|
713
|
+
*
|
|
714
|
+
* NoteObject structure (standalone chord symbol):
|
|
715
|
+
* {
|
|
716
|
+
* isChordSymbol: true,
|
|
717
|
+
* chordSymbol: string, // The chord name
|
|
718
|
+
* token: string,
|
|
719
|
+
* spacing: { ... }
|
|
720
|
+
* }
|
|
721
|
+
*
|
|
722
|
+
* LineMetadata structure:
|
|
723
|
+
* {
|
|
724
|
+
* originalLine: string, // Complete original line from ABC
|
|
725
|
+
* comment: string | null, // Text after % (null if no comment)
|
|
726
|
+
* hasContinuation: boolean // Whether line had \ continuation marker
|
|
727
|
+
* }
|
|
728
|
+
*
|
|
729
|
+
* @param {string} abc - ABC notation string
|
|
730
|
+
* @param {object} options - Parsing options
|
|
731
|
+
* @param {number} options.maxBars - Maximum number of bars to parse (optional)
|
|
732
|
+
* @returns {object} - Parsed structure as described above
|
|
733
|
+
*
|
|
734
|
+
* Example:
|
|
735
|
+
* parseABCWithBars('X:1\nL:1/4\nK:D\n"Dm"D2 [DF]A | ~B4 |]')
|
|
736
|
+
* // Returns:
|
|
737
|
+
* {
|
|
738
|
+
* bars: [
|
|
739
|
+
* [
|
|
740
|
+
* { isChordSymbol: true, chordSymbol: 'Dm', spacing: {...}, ... },
|
|
741
|
+
* { pitch: 'D', octave: 0, duration: Fraction(1,2), chordSymbol: 'Dm', spacing: {...}, ... },
|
|
742
|
+
* { pitch: 'F', octave: 0, duration: Fraction(1,4), isChord: true, chordNotes: [...], spacing: {...}, ... },
|
|
743
|
+
* { pitch: 'A', octave: 0, duration: Fraction(1,4), spacing: {...}, ... }
|
|
744
|
+
* ],
|
|
745
|
+
* [
|
|
746
|
+
* { pitch: 'B', octave: 0, duration: Fraction(1,1), decorations: ['roll'], spacing: {...}, ... }
|
|
747
|
+
* ]
|
|
748
|
+
* ],
|
|
749
|
+
* unitLength: Fraction(1,4),
|
|
750
|
+
* meter: [4,4],
|
|
751
|
+
* tonalBase: 'D',
|
|
752
|
+
* lineMetadata: [...]
|
|
753
|
+
* }
|
|
754
|
+
*/
|
|
755
|
+
function parseABCWithBars(abc, options = {}) {
|
|
756
|
+
const { maxBars = Infinity } = options;
|
|
757
|
+
|
|
758
|
+
let unitLength = getUnitLength(abc);
|
|
759
|
+
let meter = getMeter(abc);
|
|
760
|
+
let tonalBase = getTonalBase(abc);
|
|
761
|
+
|
|
762
|
+
const {
|
|
763
|
+
musicText,
|
|
764
|
+
lineMetadata,
|
|
765
|
+
headerLines,
|
|
766
|
+
headerEndIndex,
|
|
767
|
+
newlinePositions,
|
|
768
|
+
} = getMusicLines(abc);
|
|
769
|
+
|
|
770
|
+
// Create a Set of newline positions for O(1) lookup
|
|
771
|
+
const newlineSet = new Set(newlinePositions);
|
|
772
|
+
|
|
773
|
+
// Comprehensive bar line regex - includes trailing spaces
|
|
774
|
+
const barLineRegex = /(\|\]|\[\||(\|:?)|(:?\|)|::|(\|[1-6])) */g;
|
|
775
|
+
|
|
776
|
+
const bars = [];
|
|
777
|
+
const barLines = [];
|
|
778
|
+
let currentBar = [];
|
|
779
|
+
let barCount = 0;
|
|
780
|
+
|
|
781
|
+
// Split music text by bar lines while preserving positions
|
|
782
|
+
let lastBarPos = 0;
|
|
783
|
+
let match;
|
|
784
|
+
let first = true;
|
|
785
|
+
|
|
786
|
+
while ((match = barLineRegex.exec(musicText)) !== null || first) {
|
|
787
|
+
first = false;
|
|
788
|
+
const { barLineText, barLinePos } =
|
|
789
|
+
match === null
|
|
790
|
+
? { barLineText: musicText, barLinePos: musicText.length }
|
|
791
|
+
: {
|
|
792
|
+
barLineText: match[0],
|
|
793
|
+
barLinePos: match.index,
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// Process segment before this bar line
|
|
797
|
+
const segment = musicText.substring(lastBarPos, barLinePos);
|
|
798
|
+
|
|
799
|
+
if (segment.trim()) {
|
|
800
|
+
// Parse tokens in this segment
|
|
801
|
+
// Match: inline fields, chord symbols, chords in brackets, decorations, notes/rests/dummy
|
|
802
|
+
const tokenRegex = getTokenRegex();
|
|
803
|
+
|
|
804
|
+
let tokenMatch;
|
|
805
|
+
// let segmentPos = lastBarPos;
|
|
806
|
+
|
|
807
|
+
let currentTuple = null;
|
|
808
|
+
|
|
809
|
+
while ((tokenMatch = tokenRegex.exec(segment)) !== null) {
|
|
810
|
+
//check if all notes of the tuple have been parsed
|
|
811
|
+
if (currentTuple && currentTuple.r === 0) {
|
|
812
|
+
currentTuple = null;
|
|
813
|
+
}
|
|
814
|
+
const fullToken = tokenMatch[0];
|
|
815
|
+
const tokenStartPos = lastBarPos + tokenMatch.index;
|
|
816
|
+
// const tokenEndPos = tokenStartPos + fullToken.length;
|
|
817
|
+
const spacing = analyzeSpacing(
|
|
818
|
+
segment,
|
|
819
|
+
tokenMatch.index + fullToken.length
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
// Check for inline field
|
|
823
|
+
const inlineField = parseInlineField(fullToken);
|
|
824
|
+
if (inlineField) {
|
|
825
|
+
// Update context based on inline field
|
|
826
|
+
if (inlineField.field === "L") {
|
|
827
|
+
const lengthMatch = inlineField.value.match(/1\/(\d+)/);
|
|
828
|
+
if (lengthMatch) {
|
|
829
|
+
unitLength = new Fraction(1, parseInt(lengthMatch[1]));
|
|
830
|
+
}
|
|
831
|
+
} else if (inlineField.field === "M") {
|
|
832
|
+
const meterMatch = inlineField.value.match(/(\d+)\/(\d+)/);
|
|
833
|
+
if (meterMatch) {
|
|
834
|
+
meter = [parseInt(meterMatch[1]), parseInt(meterMatch[2])];
|
|
835
|
+
}
|
|
836
|
+
} else if (inlineField.field === "K") {
|
|
837
|
+
const keyMatch = inlineField.value.match(/^([A-G])/);
|
|
838
|
+
if (keyMatch) {
|
|
839
|
+
tonalBase = keyMatch[1].toUpperCase();
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
currentBar.push({
|
|
844
|
+
isInlineField: true,
|
|
845
|
+
field: inlineField.field,
|
|
846
|
+
value: inlineField.value,
|
|
847
|
+
token: fullToken,
|
|
848
|
+
sourceIndex: tokenStartPos,
|
|
849
|
+
sourceLength: fullToken.length,
|
|
850
|
+
spacing,
|
|
851
|
+
});
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// tuples
|
|
856
|
+
if (fullToken.match(/\(\d(?::\d?){0,2}/g)) {
|
|
857
|
+
const tuple = parseTuple(fullToken);
|
|
858
|
+
if (tuple) {
|
|
859
|
+
if (currentTuple) {
|
|
860
|
+
throw new Error("nested tuples not handled");
|
|
861
|
+
}
|
|
862
|
+
// Update context based on inline field
|
|
863
|
+
currentTuple = tuple;
|
|
864
|
+
currentBar.push({
|
|
865
|
+
...tuple,
|
|
866
|
+
token: fullToken,
|
|
867
|
+
sourceIndex: tokenStartPos,
|
|
868
|
+
sourceLength: fullToken.length,
|
|
869
|
+
});
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Standalone chord symbol
|
|
875
|
+
if (fullToken.match(/^"[^"]+"$/)) {
|
|
876
|
+
currentBar.push({
|
|
877
|
+
isChordSymbol: true,
|
|
878
|
+
chordSymbol: fullToken.slice(1, -1),
|
|
879
|
+
token: fullToken,
|
|
880
|
+
sourceIndex: tokenStartPos,
|
|
881
|
+
sourceLength: fullToken.length,
|
|
882
|
+
spacing,
|
|
883
|
+
});
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Standalone decoration
|
|
888
|
+
if (fullToken.match(/^!([^!]+)!$/)) {
|
|
889
|
+
currentBar.push({
|
|
890
|
+
isDecoration: true,
|
|
891
|
+
decoration: fullToken.slice(1, -1),
|
|
892
|
+
token: fullToken,
|
|
893
|
+
sourceIndex: tokenStartPos,
|
|
894
|
+
sourceLength: fullToken.length,
|
|
895
|
+
spacing,
|
|
896
|
+
});
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Regular note, rest, or dummy, or chord in brackets
|
|
901
|
+
const note = parseNote(fullToken, unitLength, currentTuple);
|
|
902
|
+
if (note) {
|
|
903
|
+
currentBar.push({
|
|
904
|
+
...note,
|
|
905
|
+
token: fullToken,
|
|
906
|
+
sourceIndex: tokenStartPos,
|
|
907
|
+
sourceLength: fullToken.length,
|
|
908
|
+
spacing,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Check if bar line has a newline after it
|
|
915
|
+
const barLineEndPos = barLinePos + barLineText.length;
|
|
916
|
+
const hasLineBreakAfterBar =
|
|
917
|
+
newlineSet.has(barLineEndPos + 1) ||
|
|
918
|
+
(barLineEndPos < musicText.length && musicText[barLineEndPos] === "\n");
|
|
919
|
+
|
|
920
|
+
// Store bar line information
|
|
921
|
+
const barLineInfo = classifyBarLine(barLineText);
|
|
922
|
+
barLines.push({
|
|
923
|
+
...barLineInfo,
|
|
924
|
+
sourceIndex: barLinePos,
|
|
925
|
+
sourceLength: barLineText.length,
|
|
926
|
+
barNumber: barCount,
|
|
927
|
+
hasLineBreak: hasLineBreakAfterBar,
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// Update the last token in current bar to mark lineBreak if bar line has one
|
|
931
|
+
if (currentBar.length > 0 && hasLineBreakAfterBar) {
|
|
932
|
+
const lastToken = currentBar[currentBar.length - 1];
|
|
933
|
+
if (lastToken.spacing) {
|
|
934
|
+
lastToken.spacing.lineBreak = true;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Save current bar if it has content
|
|
939
|
+
if (currentBar.length > 0) {
|
|
940
|
+
bars.push(currentBar);
|
|
941
|
+
barCount++;
|
|
942
|
+
currentBar = [];
|
|
943
|
+
|
|
944
|
+
// Check if we've reached max bars
|
|
945
|
+
if (barCount >= maxBars) {
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
lastBarPos = barLineEndPos;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Add final bar if it has content and we haven't reached max
|
|
954
|
+
if (currentBar.length > 0 && barCount < maxBars) {
|
|
955
|
+
bars.push(currentBar);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return {
|
|
959
|
+
bars,
|
|
960
|
+
barLines,
|
|
961
|
+
unitLength,
|
|
962
|
+
meter,
|
|
963
|
+
tonalBase,
|
|
964
|
+
lineMetadata,
|
|
965
|
+
headerLines,
|
|
966
|
+
headerEndIndex,
|
|
967
|
+
musicText,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Calculate bar durations from parsed ABC data
|
|
973
|
+
* Returns duration for each bar
|
|
974
|
+
*/
|
|
975
|
+
function calculateBarDurations(parsedData) {
|
|
976
|
+
const { bars, barLines } = parsedData;
|
|
977
|
+
const result = []
|
|
978
|
+
if(barLines && barLines[0] && barLines[0].sourceIndex === 0){
|
|
979
|
+
result.push(new Fraction(0,1))
|
|
980
|
+
}
|
|
981
|
+
bars.forEach((bar) => {
|
|
982
|
+
let total = new Fraction(0, 1);
|
|
983
|
+
for (const note of bar) {
|
|
984
|
+
if (!note.duration) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
total = total.add(note.duration);
|
|
988
|
+
}
|
|
989
|
+
result.push(total);
|
|
990
|
+
});
|
|
991
|
+
return result
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
module.exports = {
|
|
995
|
+
getTonalBase,
|
|
996
|
+
getMeter,
|
|
997
|
+
getUnitLength,
|
|
998
|
+
getMusicLines,
|
|
999
|
+
analyzeSpacing,
|
|
1000
|
+
parseABCWithBars,
|
|
1001
|
+
classifyBarLine,
|
|
1002
|
+
calculateBarDurations,
|
|
1003
|
+
NOTE_TO_DEGREE,
|
|
1004
|
+
};
|