forked from seisiuneer/abctools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
xml2abc.js
2308 lines (2223 loc) · 111 KB
/
xml2abc.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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
259
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
315
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
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
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
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
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
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
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
779
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
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
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
997
998
999
1000
//~ Revision: 118, Copyright (C) 2014-2021: Willem Vree
//~ This program is free software; you can redistribute it and/or modify it under the terms of the
//~ Lesser GNU General Public License as published by the Free Software Foundation;
//~ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
//~ without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//~ See the Lesser GNU General Public License for more details. <http://www.gnu.org/licenses/lgpl.html>.
var xml2abc_VERSION = 118;
var vertaal;
// MAE 13 June 2024
var gGotMicrotonalAccidental = false;
(function () { // all definitions inside an anonymous function
'use strict'
function repstr (n, s) { return new Array (n + 1).join (s); } // repeat string s n times
function reparr (n, v) { var arr = []; while (n) { arr.push (v); --n; } return arr; } // arr = n * [v]
function dict (ks, vs) { // init object with key list and value list
for (var i = 0, obj = {}; i < ks.length; ++i) obj[ ks [i]] = vs [i]; return obj;
}
function format (str, vals) { // help for sprintf string formatting
var a = str.split (/%[ds]/); // only works for simple %d and %s
if (a.length > vals.length) vals.push ('');
return vals.map (function (x, i) { return a[i] + x; }).join ('');
}
function infof (str, vals) { abcOut.info (format (str, vals)); }
function endswith (str, suffix) { return str.indexOf (suffix, str.length - suffix.length) !== -1; }
function keyints (obj) { return Object.keys (obj).map (function (x) { return parseInt (x); }); }
var max_int = Math.pow (2, 53); // Ecma largest positive mantissa.
function sortitems (d, onkey) { // {key:value} -> [[key, value]] or [value] -> [[ix, value]]
var xs = [], f, k;
if (Array.isArray (d)) for (k = 0; k < d.length; ++k ) { if (k in d) xs.push ([k, d[k]]); }
else for (k in d) xs.push ([k, d[k]]);
if (onkey) f = function (a,b) { return a[0] - b[0]; };
else f = function (a,b) { return a[1] - b[1] || b[0] - a[0]; }; // tie (1) -> reverse sort on key (0)
xs.sort (f);
return xs;
}
var note_ornamentation_map = { // for notations, modified from EasyABC
'ornaments>trill-mark': 'T',
'ornaments>mordent': 'M',
'ornaments>inverted-mordent': 'P',
'ornaments>turn': '!turn!',
'ornaments>inverted-turn': '!invertedturn!',
'technical>up-bow': 'u',
'technical>down-bow': 'v',
'technical>harmonic': '!open!',
'technical>open-string': '!open!',
'technical>stopped': '!plus!',
'technical>snap-pizzicato': '!snap!',
'technical>thumb-position': '!thumb!',
'articulations>accent': '!>!',
'articulations>strong-accent':'!^!',
'articulations>staccato': '.',
'articulations>staccatissimo':'!wedge!',
'articulations>scoop': '!slide!',
'fermata': '!fermata!',
'arpeggiate': '!arpeggio!',
'articulations>tenuto': '!tenuto!',
'articulations>spiccato': '!wedge!', // not sure whether this is the right translation
'articulations>breath-mark': '!breath!', // this may need to be tested to make sure it appears on the right side of the note
'articulations>detached-legato': '!tenuto!.'
}
var dynamics_map = { // for direction/direction-type/dynamics/
'p': '!p!',
'pp': '!pp!',
'ppp': '!ppp!',
'pppp': '!pppp!',
'f': '!f!',
'ff': '!ff!',
'fff': '!fff!',
'ffff': '!ffff!',
'mp': '!mp!',
'mf': '!mf!',
'sfz': '!sfz!'
}
var percSvg = ['%%beginsvg\n<defs>',
'<text id="x" x="-3" y="0"></text>',
'<text id="x-" x="-3" y="0"></text>',
'<text id="x+" x="-3" y="0"></text>',
'<text id="normal" x="-3.7" y="0"></text>',
'<text id="normal-" x="-3.7" y="0"></text>',
'<text id="normal+" x="-3.7" y="0"></text>',
'<g id="circle-x"><text x="-3" y="0"></text><circle r="4" class="stroke"></circle></g>',
'<g id="circle-x-"><text x="-3" y="0"></text><circle r="4" class="stroke"></circle></g>',
'<path id="triangle" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>',
'<path id="triangle-" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>',
'<path id="triangle+" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="fill:#000"></path>',
'<path id="square" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>',
'<path id="square-" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>',
'<path id="square+" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="fill:#000"></path>',
'<path id="diamond" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>',
'<path id="diamond-" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>',
'<path id="diamond+" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="fill:#000"></path>',
'</defs>\n%%endsvg'];
var tabSvg = ['%%beginsvg\n',
'<style type="text/css">\n',
'.bf {font-family:sans-serif; font-size:7px}\n',
'</style>\n',
'<defs>\n',
'<rect id="clr" x="-3" y="-1" width="6" height="5" fill="white"></rect>\n',
'<rect id="clr2" x="-3" y="-1" width="11" height="5" fill="white"></rect>\n']
var kopSvg = '<g id="kop%s" class="bf"><use xlink:href="#clr"></use><text x="-2" y="3">%s</text></g>\n'
var kopSvg2 = '<g id="kop%s" class="bf"><use xlink:href="#clr2"></use><text x="-2" y="3">%s</text></g>\n'
var abcOut;
//-------------------
// data abstractions
//-------------------
function Measure (p) {
this.reset ();
this.ixp = p; // part number
this.ixm = 0; // measure number
this.mdur = 0; // measure duration (nominal metre value in divisions)
this.divs = 0; // number of divisions per 1/4
this.mtr = [4,4]; // meter
}
Measure.prototype.reset = function () { // reset each measure
this.attr = ''; // measure signatures, tempo
this.lline = ''; // left barline, but only holds ':' at start of repeat, otherwise empty
this.rline = '|'; // right barline
this.lnum = ''; // (left) volta number
}
function Note (dur, n) {
this.tijd = 0; // the time in XML division units
this.dur = dur; // duration of a note in XML divisions
this.fact = null; // time modification for tuplet notes (num, div)
this.tup = ['']; // start(s) and/or stop(s) of tuplet
this.tupabc = ''; // abc tuplet string to issue before note
this.beam = 0; // 1 = beamed
this.grace = 0; // 1 = grace note
this.before = []; // abc string that goes before the note/chord
this.after = ''; // the same after the note/chord
this.ns = n ? [n] : []; // notes in the chord
this.lyrs = {}; // {number -> syllabe}
this.pos = 0; // position in Music.voices for stable sorting
this.tab = null; // [string number, fret number]
this.ntdec = ''; // !string!, !courtesy!
}
function Elem (string) {
this.tijd = 0 // the time in XML division units
this.str = string // any abc string that is not a note
this.pos = 0; // position in Music.voices for stable sorting
}
function Counter () {}
Counter.prototype.inc = function (key, voice) {
this.counters [key][voice] = (this.counters [key][voice] || 0) + 1;
}
Counter.prototype.clear = function (vnums) { // reset all counters
var ks = Object.keys (vnums);
var vs = reparr (ks.length, 0);
this.counters = {'note': dict (ks,vs), 'nopr': dict (ks,vs), 'nopt': dict (ks,vs)}
}
Counter.prototype.getv = function (key, voice) {
return this.counters[key][voice];
}
Counter.prototype.prcnt = function (ip) { // print summary of all non zero counters
for (var iv in this.counters ['note']) {
if (this.getv ('nopr', iv) != 0)
infof ('part %d, voice %d has %d skipped non printable notes', [ip, iv, this.getv ('nopr', iv)]);
if (this.getv ('nopt', iv) != 0)
infof ('part %d, voice %d has %d notes without pitch', [ip, iv, this.getv ('nopt', iv)]);
if (this.getv ('note', iv) == 0) // no real notes counted in this voice
infof ('part %d, skipped empty voice %d', [ip, iv]);
}
}
function Music (options) {
this.tijd = 0; // the current time
this.maxtime = 0; // maximum time in a measure
this.gMaten = []; // [voices,.. for all measures in a part], voices = {vnum: [Note | Elem]}
this.gLyrics = []; // [{num: (abc_lyric_string, melis)},.. for all measures in a part]
this.vnums = {}; // all used xml voice id's in a part (xml voice id's == numbers)
this.cnt = new Counter (); // global counter object
this.vceCnt = 1; // the global voice count over all parts
this.lastnote = null; // the last real note record inserted in this.voices
this.bpl = options.b; // the max number of bars per line when writing abc
this.cpl = options.n; // the number of chars per line when writing abc
this.repbra = 0; // true if volta is used somewhere
this.nvlt = options.v; // no volta on higher voice numbers
// MAE 16 Nov 2023 - To hide or show the stave numbers at the end of the staves
this.addstavenum = options.addstavenum; // Hide or show stave number comments at the end of each line
}
Music.prototype.initVoices = function (newPart) {
this.vtimes = {}; this.voices = {}; this.lyrics = {};
for (var v in this.vnums) {
this.vtimes [v] = 0; // {voice: the end time of the last item in each voice}
this.voices [v] = []; // {voice: [Note|Elem, ..]}
this.lyrics [v] = []; // {voice: [{num: syl}, ..]}
}
if (newPart) this.cnt.clear (this.vnums); // clear counters once per part
}
Music.prototype.incTime = function (dt) {
this.tijd += dt;
if (this.tijd < 0) this.tijd = 0; // erroneous <backup> element
if (this.tijd > this.maxtime) this.maxtime = this.tijd;
}
Music.prototype.appendElemCv = function (voices, elem) {
for (var v in voices)
this.appendElem (v, elem); // insert element in all voices
}
Music.prototype.insertElem = function (v, elem) { // insert at the start of voice v in the current measure
var obj = new Elem (elem);
obj.tijd = 0; // because voice is sorted later
this.voices [v].unshift (obj);
}
Music.prototype.appendObj = function (v, obj, dur) {
obj.tijd = this.tijd;
this.voices [v].push (obj);
this.incTime (dur);
if (this.tijd > this.vtimes[v]) this.vtimes[v] = this.tijd; // don't update for inserted earlier items
}
Music.prototype.appendElemT = function (v, elem, tijd) { // insert element at specified time
var obj = new Elem (elem);
obj.tijd = tijd;
this.voices [v].push (obj);
}
Music.prototype.appendElem = function (v, elem, tel) {
this.appendObj (v, new Elem (elem), 0);
if (tel) this.cnt.inc ('note', v); // count number of certain elements in each voice
}
Music.prototype.appendNote = function (v, note, noot) {
note.ns.push (note.ntdec + noot);
this.appendObj (v, note, parseInt (note.dur));
this.lastnote = note; // remember last note/rest for later modifications (chord, grace)
if (noot != 'z' && noot != 'x') { // real notes and grace notes
this.cnt.inc ('note', v); // count number of real notes in each voice
if (!note.grace) // for every real note
this.lyrics[v].push (note.lyrs); // even when it has no lyrics
}
}
Music.prototype.getLastRec = function (voice) {
if (this.gMaten.length) {
var m = this.gMaten [this.gMaten.length - 1][voice];
return m [m.length - 1]; // the last record in the last measure
}
return null; // no previous records in the first measure
}
Music.prototype.getLastMelis = function (voice, num) { // get melisma of last measure
if (this.gLyrics.length) {
var lyrdict = this.gLyrics [this.gLyrics.length - 1][voice]; // the previous lyrics dict in this voice
if (num in lyrdict) return lyrdict[num][1]; // lyrdict = num -> (lyric string, melisma)
}
return 0; // no previous lyrics in voice or line number
}
Music.prototype.addChord = function (note, noot) { // careful: we assume that chord notes follow immediately
for (var ix = 0; ix < note.before.length; ix++) {
var d = note.before [ix]; // put decorations before chord
if (this.lastnote.before.indexOf (d) < 0) this.lastnote.before.push (d);
}
this.lastnote.ns.push (note.ntdec + noot);
}
Music.prototype.addBar = function (lbrk, m) { // linebreak, measure data
if (m.mdur && this.maxtime > m.mdur) infof ('measure %d in part %d longer than metre', [m.ixm+1, m.ixp+1]);
this.tijd = this.maxtime; // the time of the bar lines inserted here
for (var v in this.vnums) {
if (m.lline || m.lnum) { // if left barline or left volta number
var p = this.getLastRec (v); // get the previous barline record
if (p) { // p == null: in measure 1 no previous measure is available
var x = p.str; // p.str is the ABC barline string
if (m.lline) // append begin of repeat, m.lline == ':'
x = (x + m.lline).replace (/:\|:/g,'::').replace (/\|\|/g,'|');
if (this.nvlt == 3) { // add volta number only to lowest voice in part 0
if (m.ixp + parseInt (v) == Math.min.apply (null, keyints (this.vnums))) x += m.lnum;
} else if (this.nvlt == 4) { // add volta to lowest voice in each part
if (parseInt (v) == Math.min.apply (null, keyints (this.vnums))) x += m.lnum;
} else if (m.lnum) { // new behaviour with I:repbra 0
x += m.lnum; // add volta number(s) or text to all voices
this.repbra = 1; // signal occurrence of a volta
}
p.str = x; // modify previous right barline
} else if (m.lline) { // begin of new part and left repeat bar is required
this.insertElem (v, '|:');
}
}
if (lbrk) {
p = this.getLastRec (v); // get the previous barline record
if (p) p.str += lbrk; // insert linebreak char after the barlines+volta
}
if (m.attr) // insert signatures at front of buffer
this.insertElem (v, '' + m.attr);
this.appendElem (v, ' ' + m.rline); // insert current barline record at time maxtime
this.voices[v] = sortMeasure (this.voices[v], m); // make all times consistent
var lyrs = this.lyrics[v]; // [{number: sylabe}, .. for all notes]
var lyrdict = {}; // {number: (abc_lyric_string, melis)} for this voice
var nums = lyrs.reduce (function (ns, lx) { return ns.concat (keyints (lx))}, []);
var maxNums = Math.max.apply (null, nums.concat ([0])); // the highest lyrics number in this measure
for (var i = maxNums; i > 0; --i) {
var xs = lyrs.map (function (syldict) { return syldict [i] || ''; }); // collect the syllabi with number i
var melis = this.getLastMelis (v, i); // get melisma from last measure
lyrdict [i] = abcLyr (xs, melis);
}
this.lyrics[v] = lyrdict; // {number: (abc_lyric_string, melis)} for this measure
mkBroken (this.voices[v]);
}
this.gMaten.push (this.voices);
this.gLyrics.push (this.lyrics);
this.tijd = this.maxtime = 0;
this.initVoices ();
}
Music.prototype.outVoices = function (divs, ip) { // output all voices of part ip
var lyrlines, i, n, lyrs, vvmap, unitL, lvc, iv, im, measure, xs, lyrstr, mis, t;
vvmap = {}; // xml voice number -> abc voice number (one part)
lvc = Math.min.apply (null, keyints (this.vnums) || [1]); // lowest xml voice number of this part
for (iv in this.vnums) {
if (this.cnt.getv ('note', iv) == 0) // no real notes counted in this voice
continue; // skip empty voices
if (abcOut.denL) unitL = abcOut.denL; // take the unit length from the -d option
else unitL = compUnitLength (iv, this.gMaten, divs); // compute the best unit length for this voice
abcOut.cmpL.push (unitL); // remember for header output
var vn = [], vl = {}; // for voice iv: collect all notes to vn and all lyric lines to vl
for (im = 0; im < this.gMaten.length; ++im) {
measure = this.gMaten [im][iv];
vn.push (outVoice (measure, divs [im], im, ip, unitL));
checkMelismas (this.gLyrics, this.gMaten, im, iv);
xs = this.gLyrics [im][iv];
for (n in xs) {
t = xs [n];
lyrstr = t[0];
if (n in vl) {
while (vl[n].length < im) vl[n].push (''); // fill in skipped measures
vl[n].push (lyrstr);
} else {
vl[n] = reparr (im, '').concat ([lyrstr]); // must skip im measures
}
}
}
for (n in vl) { // fill up possibly empty lyric measures at the end
lyrs = vl [n];
mis = vn.length - lyrs.length;
vl[n] = lyrs.concat (reparr (mis, ''));
}
abcOut.add ('V:' + this.vceCnt);
if (this.repbra) {
if (this.nvlt == 1 && this.vceCnt > 1) abcOut.add ('I:repbra 0'); // only volta on first voice
if (this.nvlt == 2 && parseInt (iv) > lvc) abcOut.add ('I:repbra 0'); // only volta on first voice of each part
}
if (this.cpl > 0) this.bpl = 0; // option -n (max chars per line) overrules -b (max bars per line)
else if (this.bpl == 0) this.cpl = 100; // the default: 100 chars per line
var bn = 0; // count bars
while (vn.length) { // while still measures available
var ib = 1;
var chunk = vn [0];
while (ib < vn.length) {
if (this.cpl > 0 && chunk.length + vn [ib].length >= this.cpl) break; // line full (number of chars)
if (this.bpl > 0 && ib >= this.bpl) break; // line full (number of bars)
chunk += vn [ib];
ib += 1;
}
bn += ib;
// MAE 16 Nov 2023
if (this.addstavenum){
abcOut.add (chunk + ' %' + bn);
}
else{
abcOut.add (chunk);
}
vn.splice (0, ib); // chop ib bars
lyrlines = sortitems (vl, 1); // order the numbered lyric lines for output (alphabitical on key)
for (i = 0; i < lyrlines.length; ++ i) {
t = lyrlines [i];
n = t[0]; lyrs = t[1];
abcOut.add ('w: ' + lyrs.slice (0, ib).join ('|') + '|');
lyrs.splice (0, ib);
}
}
vvmap [iv] = this.vceCnt; // xml voice number -> abc voice number
this.vceCnt += 1; // count voices over all parts
}
this.gMaten = []; // reset the follwing instance vars for each part
this.gLyrics = [];
this.cnt.prcnt (ip+1); // print summary of skipped items in this part
return vvmap;
}
function ABCoutput (fnmext, pad, X, options) {
this.fnmext = fnmext;
this.outlist = []; // list of ABC strings
this.infolist = []; // list of info messages
this.title = 'T:Title';
this.key = 'none';
this.clefs = {}; // clefs for all abc-voices
this.mtr = 'none'
this.tempo = 0; // 0 -> no tempo field
this.tempo_units = [1,4] // note type of tempo direction
this.pad = pad; // the output path or null
this.X = X + 1; // the abc tune number
this.denL = options.d; // denominator of the unit length (L:) from -d option
this.volpan = options.m; // 0 -> no %%MIDI, 1 -> only program, 2 -> all %%MIDI
this.cmpL = []; // computed optimal unit length for all voices
this.scale = ''; // float around 1.0
this.tstep = options.t // translate percmap to voicemap
this.stemless = 0 // use U:s=!stemless!
this.pagewidth = ''; // in cm
this.leftmargin = ''; // in cm
this.rightmargin = ''; // in cm
this.shiftStem = options.s; // shift note heads 3 units left
// MAE 14 June 2024
this.nolbrk = options.x; // generate no linebreaks ($)
this.mnum = options.mnum; // measure numbers
if (options.p.length == 4) {
this.scale = options.p [0] != '' ? parseFloat (options.p [0]) : '';
this.pagewidth = options.p [1] != '' ? parseFloat (options.p [1]) : '';
this.leftmargin = options.p [2] != '' ? parseFloat (options.p [2]) : '';
this.rightmargin = options.p [3] != '' ? parseFloat (options.p [3]) : '';
}
}
ABCoutput.prototype.add = function (str) {
this.outlist.push (str + '\n'); // collect all ABC output
}
ABCoutput.prototype.info = function (str, warn) {
var indent = (typeof warn == 'undefined' || warn) ? '-- ' : '';
this.infolist.push (indent + str);
}
ABCoutput.prototype.mkHeader = function (stfmap, partlist, midimap, vmpdct, koppen) { // stfmap = [parts], part = [staves], stave = [voices]
var accVce = [], accStf = [], x, staves, clfnms, part, partname, partabbrv, firstVoice, t, dmap, ks, kks;
var nm, snm, hd, tempo, d, defL, vnum, clef, ch, prg, vol, pan, i, abcNote, midiNote, step, notehead, m;
var staffs = stfmap.slice (); // stafmap is consumed by prgroupelem
for (i = 0; i < partlist.length; ++i) { // collect partnames into accVce and staff groups into accStf
x = partlist [i];
try { prgroupelem (x, ['', ''], '', stfmap, accVce, accStf); }
catch (err) { infof ('lousy musicxml: error in part-list',[]); }
}
staves = accStf.join (' ');
clfnms = {};
for (i = 0; i < staffs.length && i < accVce.length; ++ i) {
part = staffs [i];
t = accVce [i];
partname = t[1]; partabbrv = t[2];
if (part.length == 0) continue; // skip empty part
firstVoice = part[0][0]; // the first voice number in this part
nm = partname.replace (/\n/g,'\\n').replace (/\.:/g,'.').replace (/^:|:$/g,'');
snm = partabbrv.replace (/\n/g,'\\n').replace (/\.:/g,'.').replace (/^:|:$/g,'');
clfnms [firstVoice] = (nm ? 'nm="' + nm + '"' : '') + (snm ? ' snm="' + snm + '"' : '');
}
m = this.mnum > -1 ? '%%measurenb ' + this.mnum + '\n' : '';
hd = [format ('X:%d\n%s\n%s', [this.X, this.title, m])];
if (this.scale !== '') hd.push ('%%scale ' + this.scale + '\n');
if (this.pagewidth !== '') hd.push ('%%pagewidth ' + this.pagewidth + 'cm\n');
if (this.leftmargin !== '') hd.push ('%%leftmargin ' + this.leftmargin + 'cm\n');
if (this.rightmargin !== '') hd.push ('%%rightmargin ' + this.rightmargin + 'cm\n');
if (staves && accStf.length > 1) hd.push ('%%score ' + staves + '\n');
tempo = this.tempo ? format ('Q:%d/%d=%s\n', [this.tempo_units [0], this.tempo_units [1], this.tempo]) : ''; // default no tempo field
d = []; // determine the most frequently occurring unit length over all voices
for (i = 0; i < this.cmpL.length; ++i) { x = this.cmpL [i]; d[x] = (d[x] || 0) + 1; }
d = sortitems (d); // -> [[unitLength, numberOfTimes]], sorted on numberOfTimes (when tie select smallest unitL)
defL = d [d.length-1][0];
defL = this.denL ? this.denL : defL; // override default unit length with -d option
hd.push (format ('L:1/%d\n%sM:%s\n', [defL, tempo, this.mtr]));
// MAE 13 June 2024 - Only inject the linebreak annotation if actually being used
if (this.nolbrk){
hd.push (format ('K:%s\n', [this.key]));
}
else{
hd.push (format ('I:linebreak $\nK:%s\n', [this.key]));
}
// MAE 15 June 2025 - Add stretchlast
hd.push("%%stretchlast true\n");
if (this.stemless) hd.push ('U:s=!stemless!\n');
var vxs = Object.keys (vmpdct).sort ();
for (i = 0; i < vxs.length; ++i) hd = hd.concat (vmpdct [vxs [i]]);
this.dojef = 0 // translate percmap to voicemap
for (vnum in this.clefs) {
t = midimap [vnum-1];
ch = t[0]; prg = t[1]; vol = t[2]; pan = t[3]; dmap = t.slice (4);
clef = this.clefs [vnum];
if (dmap.length && clef.indexOf ('perc') < 0 ) clef = (clef + ' map=perc').trim ();
hd.push (format ('V:%d %s %s\n', [vnum, clef, clfnms [vnum] || '']));
if (vnum in vmpdct) {
hd.push (format ('%%voicemap tab%d\n', [vnum]));
hd.push ('K:none\nM:none\n%%clef none\n%%staffscale 1.6\n%%flatbeams true\n%%stemdir down\n');
}
if (clef.indexOf ('perc') > -1) hd.push ('K:none\n'); // no key for a perc voice
if (this.volpan > 1) { // option -m 2 -> output all recognized midi commands when needed and present in xml
if (ch > 0 && ch != vnum) hd.push ('%%MIDI channel ' + ch + '\n');
if (prg > 0) hd.push ('%%MIDI program ' + (prg - 1) + '\n');
if (vol >= 0) hd.push ('%%MIDI control 7 ' + vol + '\n'); // volume == 0 is possible ...
if (pan >= 0) hd.push ('%%MIDI control 10 ' + pan + '\n');
} else if (this.volpan > 0) { // default -> only output midi program command when present in xml
if (dmap.length && ch > 0) hd.push ('%%MIDI channel ' + ch + '\n'); // also channel if percussion part
if (prg > 0) hd.push ('%%MIDI program ' + (prg - 1) + '\n');
}
for (i = 0; i < dmap.length; ++i) {
abcNote = dmap [i].nt; step = dmap [i].step; midiNote = dmap [i].midi; notehead = dmap [i].nhd;
if (!notehead) notehead = 'normal';
if (abcMid (abcNote) != midiNote || abcNote != step) {
if (this.volpan > 0) hd.push ('%%MIDI drummap '+abcNote+' '+midiNote+'\n');
hd.push ('I:percmap '+abcNote+' '+step+' '+midiNote+' '+notehead+'\n');
this.dojef = this.tstep
}
}
if (defL != this.cmpL [vnum-1]) // only if computed unit length different from header
hd.push ('L:1/' + this.cmpL [vnum-1] + '\n');
}
this.outlist = hd.concat (this.outlist);
kks = Object.keys (koppen).sort ();
if (kks.length) { // output SVG stuff needed for tablature
ks = [];
var k1 = this.shiftStem ? kopSvg.replace ('-2','-5') : kopSvg; // shift note heads 3 units left
var k2 = this.shiftStem ? kopSvg2.replace ('-2','-5') : kopSvg2;
var tb = this.shiftStem ? tabSvg.map (function (x) { return x.replace ('-3','-6') }) : tabSvg ;
kks.forEach (function (k) { ks.push (k.length > 1 ? format (k2, [k, k]) : format (k1, [k, k])); })
this.outlist = tb.concat (ks, '</defs>\n%%endsvg\n', this.outlist);
}
}
ABCoutput.prototype.writeall = function () {
var str = abcOut.outlist.join ('');
if (this.dojef) str = perc2map (str);
// If there were microtonal accents injected, set the annotation font size
if (gGotMicrotonalAccidental){
str = replaceXWithText(str,"% Injected for microtonal accidental annotations\n%%annotationfont Palatino 9");
}
return [str, this.infolist.join ('\n')];
}
function replaceXWithText(inputText, additionalText) {
// Split the input text into lines
let lines = inputText.split('\n');
// Create a new array to hold the modified lines
let modifiedLines = [];
// Regular expression to match lines starting with "X:" followed by an integer
let regex = /^X:\d+/;
for (let i = 0; i < lines.length; i++) {
if (regex.test(lines[i])) {
// If the line matches the pattern, add it to the modified lines
modifiedLines.push(lines[i]);
// Add the additional text on the next line
modifiedLines.push(additionalText);
} else {
// If the line does not match the pattern, just add it to the modified lines
modifiedLines.push(lines[i]);
}
}
// Join the modified lines back into a single string
return modifiedLines.join('\n');
}
//----------------
// functions
//----------------
function abcLyr (xs, melis) { // Convert list xs to abc lyrics.
if (!xs.join ('')) return ['', 0]; // there is no lyrics in this measure
var res = [];
for (var i = 0; i < xs.length; ++i) {
var x = xs[i]; // xs has for every note a lyrics syllabe or an empty string
if (x == '') { // note without lyrics
if (melis) x = '_'; // set melisma
else x = '*'; // skip note
} else if (endswith (x,'_') && !endswith (x,'\\_')) { // start of new melisma
x = x.replace ('_', ''); // remove and set melis boolean
melis = 1; // so next skips will become melisma
} else melis = 0; // melisma stops on first syllable
res.push (x);
}
return ([res.join (' '), melis]);
}
function simplify (a, b) { // divide a and b by their greatest common divisor
var x = a, y = b, c;
while (b) {
c = a % b;
a = b; b = c;
}
return [x / a, y / a];
}
function abcdur (nx, divs, uL) { // convert an musicXML duration d to abc units with L:1/uL
if (nx.dur == 0) return ''; // when called for elements without duration
var num, den, numfac, denfac, dabc, t;
t = simplify (uL * nx.dur, divs * 4); // L=1/8 -> uL = 8 units
num = t[0]; den = t[1];
if (nx.fact) { // apply tuplet time modification
numfac = nx.fact [0];
denfac = nx.fact [1];
t = simplify (num * numfac, den * denfac);
num = t[0]; den = t[1];
}
if (den > 64) { // limit the denominator to a maximum of 64
var x = num / den, n = Math.floor (x);
if (x - n < 0.1 * x) { num = n; den = 1; }
t = simplify (Math.round (64 * num / den) || 1, 64);
infof ('denominator too small: %d/%d rounded to %d/%d', [num, den, t[0], t[1]]);
num = t[0]; den = t[1];
}
if (num == 1) {
if (den == 1) dabc = '';
else if (den == 2) dabc = '/';
else dabc = '/' + den;
} else if (den == 1) dabc = '' + num;
else dabc = num + '/' + den;
return dabc;
}
function abcMid (note) { // abc note -> midi pitch
var r = note.match (/([_^]*)([A-Ga-g])([',]*)/);
if (!r) return -1;
var acc = r[1], n = r[2], oct = r[3], nUp, p;
nUp = n.toUpperCase ();
p = 60 + [0,2,4,5,7,9,11]['CDEFGAB'.indexOf (nUp)] + (nUp != n ? 12 : 0);
if (acc) p += (acc[0] == '^' ? 1 : -1) * acc.length;
if (oct) p += (oct[0] == "'" ? 12 : -12) * oct.length;
return p;
}
function staffStep (ptc, o, clef, tstep) {
var n, ndif = 0;
if (clef.indexOf ('stafflines=1') >= 0) ndif += 4; // meaning of one line: E (xml) -> B (abc)
if (!tstep && clef.indexOf ('bass') >= 0) ndif += 12; // transpose bass -> treble (C3 -> A4)
if (ndif) { // diatonic transposition == addition modulo 7
var nm7 = 'CDEFGAB'.split ('');
n = nm7.indexOf (ptc) + ndif;
ptc = nm7 [n % 7];
o += Math.floor (n / 7);
}
if (o > 4) ptc = ptc.toLowerCase ();
if (o > 5) ptc = ptc + repstr (o-5, "'");
if (o < 4) ptc = ptc + repstr (4-o, ",");
return ptc;
}
function setKey (fifths, mode) {
var accs, key, msralts, sharpness, offTab;
sharpness = ['Fb', 'Cb','Gb','Db','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#','G#','D#','A#','E#','B#'];
offTab = {'maj':8, 'ion':8, 'm':11, 'min':11, 'aeo':11, 'mix':9, 'dor':10, 'phr':12, 'lyd':7, 'loc':13, 'non':8};
mode = mode.slice (0,3).toLowerCase (); // only first three chars, no case
key = sharpness [offTab [mode] + fifths] + (offTab [mode] != 8 ? mode : '');
accs = ['F','C','G','D','A','E','B'];
if (fifths >= 0) msralts = dict (accs.slice (0, fifths), reparr (fifths, 1));
else msralts = dict (accs.slice (fifths), reparr (-fifths, -1));
return [key, msralts];
}
function insTup (ix, notes, fact) { // read one nested tuplet
var tupcnt = 0, lastix, tupfact, fn, fd, fnum, fden, tupcntR, tupPrefix, t;
var nx = notes [ix];
var i = nx.tup.indexOf ('start');
if (i > -1) // splice (i, 1) == remove 1 element at i
nx.tup.splice (i, 1); // later do recursive calls when any starts remain
var tix = ix; // index of first tuplet note
fn = fact[0]; fd = fact[1]; // xml time-mod of the higher level
fnum = nx.fact[0]; fden = nx.fact[1]; // xml time-mod of the current level
tupfact = [fnum/fn, fden/fd]; // abc time mod of this level
while (ix < notes.length) {
nx = notes [ix];
if ((nx instanceof Elem) || nx.grace) {
ix += 1; // skip all non tuplet elements
continue;
}
if (nx.tup.indexOf ('start') > -1) { // more nested tuplets to start
t = insTup (ix, notes, tupfact);
ix = t[0]; tupcntR = t[1]; // ix is on the stop note!
tupcnt += tupcntR
} else if (nx.fact) {
tupcnt += 1; // count tuplet elements
}
i = nx.tup.indexOf ('stop')
if (i > -1) {
nx.tup.splice (i, 1);
break;
}
if (!nx.fact) { // stop on first non tuplet note
ix = lastix; // back to last tuplet note
break;
}
lastix = ix;
ix += 1;
}
// put abc tuplet notation before the recursive ones
var tup = [tupfact[0], tupfact[1], tupcnt];
if (tup.toString () == '3,2,3') tupPrefix = '(3';
else tupPrefix = format ('(%d:%d:%d', tup);
notes [tix].tupabc = tupPrefix + notes [tix].tupabc;
return [ix, tupcnt] // ix is on the last tuplet note
}
function mkBroken (vs) { // introduce broken rhythms (vs: one voice, one measure)
vs = vs.filter (function (n) { return n instanceof Note; });
var i = 0;
while (i < vs.length - 1) {
var n1 = vs[i], n2 = vs[i+1] // scan all adjacent pairs
if (!n1.fact && !n2.fact && n1.dur > 0 && n2.beam) { // skip if note in tuplet or has no duration or outside beam
if (n1.dur * 3 == n2.dur) {
n2.dur = (2 * n2.dur) / 3;
n1.dur = n1.dur * 2;
n1.after = '<' + n1.after;
i += 1; // do not chain broken rhythms
} else if (n2.dur * 3 == n1.dur) {
n1.dur = (2 * n1.dur) / 3;
n2.dur = n2.dur * 2;
n1.after = '>' + n1.after;
i += 1; // do not chain broken rhythms
}
}
i += 1;
}
}
function outVoice (measure, divs, im, ip, unitL) { // note/elem objects of one measure in one voice
var ix = 0, t;
while (ix < measure.length) { // set all (nested) tuplet annotations
var nx = measure [ix];
if ((nx instanceof Note) && nx.fact && !nx.grace) {
t = insTup (ix, measure, [1, 1]); // read one tuplet, insert annotation(s)
ix = t[0];
}
ix += 1;
}
var vs = [], nospace, s;
for (var i = 0; i < measure.length; ++i) {
nx = measure [i];
if (nx instanceof Note) {
var durstr = abcdur (nx, divs, unitL); // xml -> abc duration string
var chord = nx.ns.length > 1;
var cns = nx.ns.filter (function (n) { return endswith (n, '-') });
cns = cns.map (function (n) { return n.slice (0,-1) }); // chop tie
var tie = '';
if (chord && cns.length == nx.ns.length) { // all chord notes tied
nx.ns = cns // chord notes without tie
tie = '-' // one tie for whole chord
}
s = nx.tupabc + nx.before.join ('');
if (chord) s += '[';
s += nx.ns.join ('');
if (chord) s += ']' + tie;
if (endswith (s, '-')) {
s = s.slice (0,-1); // split off tie
tie = '-';
}
s += durstr + tie; // and put it back again
s += nx.after;
nospace = nx.beam;
} else {
if (nx.str instanceof Array) nx.str = nx.str [0];
s = nx.str;
nospace = 1;
}
if (nospace) vs.push (s);
else vs.push (' ' + s);
}
vs = vs.join (''); // ad hoc: remove multiple pedal directions
while (vs.indexOf ('!ped!!ped!') >= 0) vs = vs.replace (/!ped!!ped!/g,'!ped!');
while (vs.indexOf ('!ped-up!!ped-up!') >= 0) vs = vs.replace (/!ped-up!!ped-up!/g,'!ped-up!');
while (vs.indexOf ('!8va(!!8va)!') >= 0) vs = vs.replace (/!8va\(!!8va\)!/g,''); // remove empty ottava's
return vs;
}
function sortMeasure (voice, m) {
voice.map (function (e, ix) { e.pos = ix; }); // prepare for stable sorting
voice.sort (function (a, b) { return a.tijd - b.tijd || a.pos - b.pos; } ) // (stable) sort objects on time
var time = 0;
var v = [];
var rs = []; // holds rests in between notes
for (var i = 0; i < voice.length; ++i) { // establish sequentiality
var nx = voice [i];
if (nx.tijd > time && chkbug (nx.tijd - time, m)) { // fill hole with invisble rest
v.push (new Note (nx.tijd - time, 'x'));
rs.push (v.length - 1);
}
if (nx instanceof Elem) {
if (nx.tijd < time) nx.tijd = time; // shift elems without duration to where they fit
v.push (nx);
time = nx.tijd;
continue;
}
if (nx.tijd < time) { // overlapping element
if (nx.ns[0] == 'z') continue; // discard overlapping rest
var o = v [v.length - 1]; // last object in voice
if (o.tijd <= nx.tijd) { // we can do something
if (o.ns[0] == 'z') { // shorten rest
o.dur = nx.tijd - o.tijd;
if (o.dur == 0) v.pop (); // nothing left, remove note
infof ('overlap in part %d, measure %d: rest shortened', [m.ixp+1, m.ixm+1] );
} else { // make a chord of overlap
o.ns = o.ns.concat (nx.ns);
infof ('overlap in part %d, measure %d: added chord', [m.ixp+1, m.ixm+1] );
nx.dur = (nx.tijd + nx.dur) - time; // the remains
if (nx.dur <= 0) continue; // nothing left
nx.tijd = time; // append remains
}
} else { // give up
var s = 'overlapping notes in one voice! part %d, measure %d, note %s discarded';
infof (s, [m.ixp+1, m.ixm+1, nx instanceof Note ? nx.ns : nx.str]);
continue;
}
}
v.push (nx);
if (nx instanceof Note) {
var n = nx.ns [0];
if (n == 'x' || n == 'z') {
rs.push (v.length - 1); // remember rests between notes
} else if (rs.length) {
if (nx.beam && !nx.grace) { // copy beam into rests
for (var j = 0; j < rs.length; ++j) v[rs [j]].beam = nx.beam;
}
rs = []; // clear rests on each note
}
}
time = nx.tijd + nx.dur;
}
// when a measure contains no elements and no forwards -> no incTime -> this.maxtime = 0 -> right barline
// is inserted at time == 0 (in addbar) and is only element in the voice when sortMeasure is called
if (time == 0) infof ('empty measure in part %d, measure %d, it should contain at least a rest to advance the time!', [m.ixp+1, m.ixm+1] );
return v;
}
function getPartlist ($ps) { // correct part-list (from buggy xml-software)
function mkstop (num) { // make proper xml-element for missing part-group
var elemstr = '<part-group number="%d" type="%s"></part-group>';
var newelem = format (elemstr, [num, 'stop']); // xml string of (missing) part-group
return $ (newelem, xmldoc); // return a jquery object with xml owner document
}
var xs, e, $x, num, type, i, cs, inum, xmldoc = $ps[0];
xs = []; // the corrected part-list
e = []; // stack of opened part-groups
for (cs = $ps.children (), i = 0; i < cs.length; i++) {
$x = $(cs [i]); // insert missing stops, delete double starts
if ($x[0].tagName == 'part-group') {
num = $x.attr ('number'); type = $x.attr ('type');
inum = e.indexOf (num);
if (type == 'start') {
if (inum > -1) { // missing stop: insert one
xs.push (mkstop (num));
xs.push ($x);
} else { // normal start
xs.push ($x)
e.push (num)
}
} else {
if (inum > -1) { // normal stop
e.splice (inum, 1); // remove stop
xs.push ($x)
} // double stop: skip it
}
} else xs.push ($x);
}
for (i = e.length - 1; i >= 0; --i) { // fill missing stops at the end
num = e[i];
xs.push (mkstop (num)); // new stop element has no xml context -> tagName is uppercase
}
return xs;
}
function parseParts (xs, d, e) { // [] {} [] -> [[elems on current level], rest of xs]
var $x, num, type, s, n, elemsnext, rest1, elems, rest2, nums, sym, rest, name, t;
if (xs.length == 0) return [[],[]];
$x = xs.shift ();
if ($x[0].tagName == 'part-group') { // mkstop -> element without context -> uppercase
num = $x.attr ('number'); type = $x.attr ('type');
if (type == 'start') { // go one level deeper
s = []; // get group data
for (n in {'group-symbol':0,'group-barline':0,'group-name':0,'group-abbreviation':0})
s.push ($x.find (n).text () || '');
d [num] = s; // remember groupdata by group number
e.push (num); // make stack of open group numbers
t = parseParts (xs, d, e); // parse one level deeper to next stop
elemsnext = t[0]; rest1 = t[1];
t = parseParts (rest1, d, e); // parse the rest on this level
elems = t[0]; rest2 = t[1];
return [[elemsnext].concat (elems), rest2];
} else { // stop: close level and return group-data
nums = e.pop (); // last open group number in stack order
if (xs.length && xs[0].attr ('type') == 'stop') // two consequetive stops
if (num != nums) { // in the wrong order (tempory solution)
t = d[nums];
d[nums] = d[num]; d[num] = t; // exchange values (only works for two stops!!!)
}
sym = d[num]; // retrieve and return groupdata as last element of the group
return [[sym], xs];
}
} else {
t = parseParts (xs, d, e); // parse remaining elements on current level
elems = t[0]; rest = t[1];
name = ['name_tuple', $x.find ('part-name').text () || '', $x.find ('part-abbreviation').text () || ''];
return [[name].concat (elems), rest];
}
}
function bracePart (part) { // put a brace on multistaff part and group voices
var brace, ivs, i, j;
if (part.length == 0) return []; // empty part in the score
brace = [];
for (i = 0; i < part.length; ++i) {
ivs = part [i];
if (ivs.length == 1) // stave with one voice
brace.push ('' + ivs[0]);
else { // stave with multiple voices
brace.push ('(');
for (j = 0; j < ivs.length; ++j) brace.push ('' + ivs [j]);
brace.push (')');
}
brace.push('|');
}
brace.splice (-1, 1); // no barline at the end
if (part.length > 1)
brace = ['{'].concat (brace).concat (['}']);
return brace;
}
function prgroupelem (x, gnm, bar, pmap, accVce, accStf) { // collect partnames (accVce) and %%score map (accStf)
var y, nms;
if (x[0] == 'name_tuple') { // partname-tuple = ['name_tuple', part-name, part-abbrev]
y = pmap.shift ();
if (gnm[0]) { // put group-name before part-name
x[1] = gnm[0] + ':' + x[1]; // gnm == [group-name, group-abbrev]
x[2] = gnm[1] + ':' + x[2];
}
accVce.push (x);
accStf.push.apply (accStf, bracePart (y));
} else if (x.length == 2 && x[0][0] == 'name_tuple') { // misuse of group just to add extra name to stave
y = pmap.shift ();
nms = ['name_tuple','',''];
nms[1] = x[0][1] + ':' + x[1][2]; // x[0] = ['name_tuple', part-name, part-abbrev]
nms[2] = x[0][2] + ':' + x[1][3]; // x[1] = [bracket symbol, continue barline, group-name, group-abbrev]
accVce.push (nms)
accStf.push.apply (accStf, bracePart (y));
} else {
prgrouplist (x, bar, pmap, accVce, accStf);
}
}
function prgrouplist (x, pbar, pmap, accVce, accStf) { // collect partnames, scoremap for a part-group
var sym, bar, gnm, gabbr, i, t;
t = x [x.length-1]; // bracket symbol, continue barline, group-name-tuple
sym = t[0]; bar = t[1]; gnm = t[2]; gabbr = t[3];
bar = bar == 'yes' || pbar; // pbar -> the parent has bar
accStf.push (sym == 'brace' ? '{' : '[')
for (i = 0; i < x.length - 1; ++i) {
prgroupelem (x[i], [gnm, gabbr], bar, pmap, accVce, accStf)
if (bar) accStf.push ('|')
}
if (bar) accStf.splice (-1, 1); // remove last one before close
accStf.push (sym == 'brace' ? '}' : ']');
}
function compUnitLength (iv, maten, divs) { // compute optimal unit length
var uLmin = 0, minLen = max_int, i, j;
var xs = [4,8,16]; // try 1/4, 1/8 and 1/16
while (xs.length) {
var uL = xs.shift ();
var vLen = 0; // total length of abc duration strings in this voice
for (i = 0; i < maten.length; ++i) { // all measures
var m = maten [i][iv]; // voice iv
for (j = 0; j < m.length; ++j) {
var e = m[j]; // all notes in voice iv
if ((e instanceof Elem) || e.dur == 0) continue; // no real durations
vLen += abcdur (e, divs [i], uL).length; // add len of duration string
}
}
if (vLen < minLen) { uLmin = uL; minLen = vLen; } // remember the smallest
}
return uLmin;
}
function doSyllable ($lyr) {
var txt = ''; // collect all text and elision elements
var $xs = $lyr.children ();
for (var i = 0; i < $xs.length; ++i) {
var e = $xs [i];
switch (e.tagName) {
case 'elision': txt += '~'; break;
case 'text': // escape _, - and space
txt += $(e).text ().replace (/_/g,'\\_').replace (/-/g, '\\-').replace (/ /g, '~');
break;
}
}
if (!txt) return txt;
var s = $lyr.find ('syllabic').text ();
if (s == 'begin' || s == 'middle') txt += '-';
if ($lyr.find ('extend').length) txt += '_';
return txt;
}
function checkMelismas (lyrics, maten, im, iv) {
if (im == 0) return;
var maat = maten [im][iv]; // notes of the current measure