-
Notifications
You must be signed in to change notification settings - Fork 38
/
Copy pathabc2xml.py
2252 lines (2130 loc) · 129 KB
/
abc2xml.py
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
#!/usr/bin/env python
# coding=latin-1
'''
Copyright (C) 2012-2018: Willem G. Vree
Contributions: Nils Liberg, Nicolas Froment, Norman Schmidt, Reinier Maliepaard, Martin Tarenskeen,
Paul Villiger, Alexander Scheutzow, Herbert Schneider, David Randolph, Michael Strasser
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>.
'''
from functools import reduce
from pyparsing import Word, OneOrMore, Optional, Literal, NotAny, MatchFirst
from pyparsing import Group, oneOf, Suppress, ZeroOrMore, Combine, FollowedBy
from pyparsing import srange, CharsNotIn, StringEnd, LineEnd, White, Regex
from pyparsing import nums, alphas, alphanums, ParseException, Forward
try: import xml.etree.cElementTree as E
except: import xml.etree.ElementTree as E
import types, sys, os, re, datetime
VERSION = 245
python3 = sys.version_info[0] > 2
lmap = lambda f, xs: list (map (f, xs)) # eager map for python 3
if python3:
int_type = int
list_type = list
str_type = str
uni_type = str
stdin = sys.stdin.buffer if sys.stdin else None # read binary if stdin available!
else:
int_type = types.IntType
list_type = types.ListType
str_type = types.StringTypes
uni_type = types.UnicodeType
stdin = sys.stdin
info_list = [] # diagnostic messages
def info (s, warn=1):
x = (warn and '-- ' or '') + s
info_list.append (x + '\n') # collect messages
if __name__ == '__main__': # only write to stdout when called as main progeam
try: sys.stderr.write (x + '\n')
except: sys.stderr.write (repr (x) + '\n')
def getInfo (): # get string of diagnostic messages, then clear messages
global info_list
xs = ''.join (info_list)
info_list = []
return xs
def abc_grammar (): # header, voice and lyrics grammar for ABC
#-----------------------------------------------------------------
# expressions that catch and skip some syntax errors (see corresponding parse expressions)
#-----------------------------------------------------------------
b1 = Word (u"-,'<>\u2019#", exact=1) # catch misplaced chars in chords
b2 = Regex ('[^H-Wh-w~=]*') # same in user defined symbol definition
b3 = Regex ('[^=]*') # same, second part
#-----------------------------------------------------------------
# ABC header (field_str elements are matched later with reg. epr's)
#-----------------------------------------------------------------
number = Word (nums).setParseAction (lambda t: int (t[0]))
field_str = Regex (r'[^]]*') # match anything until end of field
field_str.setParseAction (lambda t: t[0].strip ()) # and strip spacing
userdef_symbol = Word (srange ('[H-Wh-w~]'), exact=1)
fieldId = oneOf ('K L M Q P I T C O A Z N G H R B D F S E r Y') # info fields
X_field = Literal ('X') + Suppress (':') + field_str
U_field = Literal ('U') + Suppress (':') + b2 + Optional (userdef_symbol, 'H') + b3 + Suppress ('=') + field_str
V_field = Literal ('V') + Suppress (':') + Word (alphanums + '_') + field_str
inf_fld = fieldId + Suppress (':') + field_str
ifield = Suppress ('[') + (X_field | U_field | V_field | inf_fld) + Suppress (']')
abc_header = OneOrMore (ifield) + StringEnd ()
#---------------------------------------------------------------------------------
# I:score with recursive part groups and {* grand staff marker
#---------------------------------------------------------------------------------
voiceId = Suppress (Optional ('*')) + Word (alphanums + '_')
voice_gr = Suppress ('(') + OneOrMore (voiceId | Suppress ('|')) + Suppress (')')
simple_part = voiceId | voice_gr | Suppress ('|')
grand_staff = oneOf ('{* {') + OneOrMore (simple_part) + Suppress ('}')
part = Forward ()
part_seq = OneOrMore (part | Suppress ('|'))
brace_gr = Suppress ('{') + part_seq + Suppress ('}')
bracket_gr = Suppress ('[') + part_seq + Suppress (']')
part <<= MatchFirst (simple_part | grand_staff | brace_gr | bracket_gr | Suppress ('|'))
abc_scoredef = Suppress (oneOf ('staves score')) + OneOrMore (part)
#----------------------------------------
# ABC lyric lines (white space sensitive)
#----------------------------------------
skip_note = oneOf ('* -')
extend_note = Literal ('_')
measure_end = Literal ('|')
syl_str = CharsNotIn ('*-_| \t\n\\]')
syl_chars = Combine (OneOrMore (syl_str | Regex (r'\\.')))
white = Word (' \t')
syllable = syl_chars + Optional ('-')
lyr_elem = (syllable | skip_note | extend_note | measure_end) + Optional (white).suppress ()
lyr_line = Optional (white).suppress () + ZeroOrMore (lyr_elem)
syllable.setParseAction (lambda t: pObj ('syl', t))
skip_note.setParseAction (lambda t: pObj ('skip', t))
extend_note.setParseAction (lambda t: pObj ('ext', t))
measure_end.setParseAction (lambda t: pObj ('sbar', t))
lyr_line_wsp = lyr_line.leaveWhitespace () # parse actions must be set before calling leaveWhitespace
#---------------------------------------------------------------------------------
# ABC voice (not white space sensitive, beams detected in note/rest parse actions)
#---------------------------------------------------------------------------------
inline_field = Suppress ('[') + (inf_fld | U_field | V_field) + Suppress (']')
lyr_fld = Suppress ('[') + Suppress ('w') + Suppress (':') + lyr_line_wsp + Suppress (']') # lyric line
lyr_blk = OneOrMore (lyr_fld) # verses
fld_or_lyr = inline_field | lyr_blk # inline field or block of lyric verses
note_length = Optional (number, 1) + Group (ZeroOrMore ('/')) + Optional (number, 2)
octaveHigh = OneOrMore ("'").setParseAction (lambda t: len(t))
octaveLow = OneOrMore (',').setParseAction (lambda t: -len(t))
octave = octaveHigh | octaveLow
basenote = oneOf ('C D E F G A B c d e f g a b y') # includes spacer for parse efficiency
accidental = oneOf ('^^ __ ^ _ =')
rest_sym = oneOf ('x X z Z')
slur_beg = oneOf ("( (, (' .( .(, .('") + ~Word (nums) # no tuplet_start
slur_ends = OneOrMore (oneOf (') .)'))
long_decoration = Combine (oneOf ('! +') + CharsNotIn ('!+ \n') + oneOf ('! +'))
staccato = Literal ('.') + ~Literal ('|') # avoid dotted barline
pizzicato = Literal ('!+!') # special case: plus sign is old style deco marker
decoration = slur_beg | staccato | userdef_symbol | long_decoration | pizzicato
decorations = OneOrMore (decoration)
tie = oneOf ('.- -')
rest = Optional (accidental) + rest_sym + note_length
pitch = Optional (accidental) + basenote + Optional (octave, 0)
note = pitch + note_length + Optional (tie) + Optional (slur_ends)
dec_note = Optional (decorations) + pitch + note_length + Optional (tie) + Optional (slur_ends)
chord_note = dec_note | rest | b1
grace_notes = Forward ()
chord = Suppress ('[') + OneOrMore (chord_note | grace_notes) + Suppress (']') + note_length + Optional (tie) + Optional (slur_ends)
stem = note | chord | rest
broken = Combine (OneOrMore ('<') | OneOrMore ('>'))
tuplet_num = Suppress ('(') + number
tuplet_into = Suppress (':') + Optional (number, 0)
tuplet_notes = Suppress (':') + Optional (number, 0)
tuplet_start = tuplet_num + Optional (tuplet_into + Optional (tuplet_notes))
acciaccatura = Literal ('/')
grace_stem = Optional (decorations) + stem
grace_notes <<= Group (Suppress ('{') + Optional (acciaccatura) + OneOrMore (grace_stem) + Suppress ('}'))
text_expression = Optional (oneOf ('^ _ < > @'), '^') + Optional (CharsNotIn ('"'), "")
chord_accidental = oneOf ('# b =')
triad = oneOf ('ma Maj maj M mi min m aug dim o + -')
seventh = oneOf ('7 ma7 Maj7 M7 maj7 mi7 min7 m7 dim7 o7 -7 aug7 +7 m7b5 mi7b5')
sixth = oneOf ('6 ma6 M6 mi6 min6 m6')
ninth = oneOf ('9 ma9 M9 maj9 Maj9 mi9 min9 m9')
elevn = oneOf ('11 ma11 M11 maj11 Maj11 mi11 min11 m11')
thirt = oneOf ('13 ma13 M13 maj13 Maj13 mi13 min13 m13')
suspended = oneOf ('sus sus2 sus4')
chord_degree = Combine (Optional (chord_accidental) + oneOf ('2 4 5 6 7 9 11 13'))
chord_kind = Optional (seventh | sixth | ninth | elevn | thirt | triad) + Optional (suspended)
chord_root = oneOf ('C D E F G A B') + Optional (chord_accidental)
chord_bass = oneOf ('C D E F G A B') + Optional (chord_accidental) # needs a different parse action
chordsym = chord_root + chord_kind + ZeroOrMore (chord_degree) + Optional (Suppress ('/') + chord_bass)
chord_sym = chordsym + Optional (Literal ('(') + CharsNotIn (')') + Literal (')')).suppress ()
chord_or_text = Suppress ('"') + (chord_sym ^ text_expression) + Suppress ('"')
volta_nums = Optional ('[').suppress () + Combine (Word (nums) + ZeroOrMore (oneOf (', -') + Word (nums)))
volta_text = Literal ('[').suppress () + Regex (r'"[^"]+"')
volta = volta_nums | volta_text
invisible_barline = oneOf ('[|] []')
dashed_barline = oneOf (': .|')
double_rep = Literal (':') + FollowedBy (':') # otherwise ambiguity with dashed barline
voice_overlay = Combine (OneOrMore ('&'))
bare_volta = FollowedBy (Literal ('[') + Word (nums)) # no barline, but volta follows (volta is parsed in next measure)
bar_left = (oneOf ('[|: |: [: :') + Optional (volta)) | Optional ('|').suppress () + volta | oneOf ('| [|')
bars = ZeroOrMore (':') + ZeroOrMore ('[') + OneOrMore (oneOf ('| ]'))
bar_right = invisible_barline | double_rep | Combine (bars) | dashed_barline | voice_overlay | bare_volta
errors = ~bar_right + Optional (Word (' \n')) + CharsNotIn (':&|', exact=1)
linebreak = Literal ('$') | ~decorations + Literal ('!') # no need for I:linebreak !!!
element = fld_or_lyr | broken | decorations | stem | chord_or_text | grace_notes | tuplet_start | linebreak | errors
measure = Group (ZeroOrMore (inline_field) + Optional (bar_left) + ZeroOrMore (element) + bar_right + Optional (linebreak) + Optional (lyr_blk))
noBarMeasure = Group (ZeroOrMore (inline_field) + Optional (bar_left) + OneOrMore (element) + Optional (linebreak) + Optional (lyr_blk))
abc_voice = ZeroOrMore (measure) + Optional (noBarMeasure | Group (bar_left)) + ZeroOrMore (inline_field).suppress () + StringEnd ()
#----------------------------------------
# I:percmap note [step] [midi] [note-head]
#----------------------------------------
white2 = (white | StringEnd ()).suppress ()
w3 = Optional (white2)
percid = Word (alphanums + '-')
step = basenote + Optional (octave, 0)
pitchg = Group (Optional (accidental, '') + step + FollowedBy (white2))
stepg = Group (step + FollowedBy (white2)) | Literal ('*')
midi = (Literal ('*') | number | pitchg | percid)
nhd = Optional (Combine (percid + Optional ('+')), '')
perc_wsp = Literal ('percmap') + w3 + pitchg + w3 + Optional (stepg, '*') + w3 + Optional (midi, '*') + w3 + nhd
abc_percmap = perc_wsp.leaveWhitespace ()
#----------------------------------------------------------------
# Parse actions to convert all relevant results into an abstract
# syntax tree where all tree nodes are instances of pObj
#----------------------------------------------------------------
ifield.setParseAction (lambda t: pObj ('field', t))
grand_staff.setParseAction (lambda t: pObj ('grand', t, 1)) # 1 = keep ordered list of results
brace_gr.setParseAction (lambda t: pObj ('bracegr', t, 1))
bracket_gr.setParseAction (lambda t: pObj ('bracketgr', t, 1))
voice_gr.setParseAction (lambda t: pObj ('voicegr', t, 1))
voiceId.setParseAction (lambda t: pObj ('vid', t, 1))
abc_scoredef.setParseAction (lambda t: pObj ('score', t, 1))
note_length.setParseAction (lambda t: pObj ('dur', (t[0], (t[2] << len (t[1])) >> 1)))
chordsym.setParseAction (lambda t: pObj ('chordsym', t))
chord_root.setParseAction (lambda t: pObj ('root', t))
chord_kind.setParseAction (lambda t: pObj ('kind', t))
chord_degree.setParseAction (lambda t: pObj ('degree', t))
chord_bass.setParseAction (lambda t: pObj ('bass', t))
text_expression.setParseAction (lambda t: pObj ('text', t))
inline_field.setParseAction (lambda t: pObj ('inline', t))
lyr_fld.setParseAction (lambda t: pObj ('lyr_fld', t, 1))
lyr_blk.setParseAction (lambda t: pObj ('lyr_blk', t, 1)) # 1 = keep ordered list of lyric lines
grace_notes.setParseAction (doGrace)
acciaccatura.setParseAction (lambda t: pObj ('accia', t))
note.setParseAction (noteActn)
rest.setParseAction (restActn)
decorations.setParseAction (lambda t: pObj ('deco', t))
pizzicato.setParseAction (lambda t: ['!plus!']) # translate !+!
slur_ends.setParseAction (lambda t: pObj ('slurs', t))
chord.setParseAction (lambda t: pObj ('chord', t, 1))
dec_note.setParseAction (noteActn)
tie.setParseAction (lambda t: pObj ('tie', t))
pitch.setParseAction (lambda t: pObj ('pitch', t))
bare_volta.setParseAction (lambda t: ['|']) # return barline that user forgot
dashed_barline.setParseAction (lambda t: ['.|'])
bar_right.setParseAction (lambda t: pObj ('rbar', t))
bar_left.setParseAction (lambda t: pObj ('lbar', t))
broken.setParseAction (lambda t: pObj ('broken', t))
tuplet_start.setParseAction (lambda t: pObj ('tup', t))
linebreak.setParseAction (lambda t: pObj ('linebrk', t))
measure.setParseAction (doMaat)
noBarMeasure.setParseAction (doMaat)
b1.setParseAction (errorWarn)
b2.setParseAction (errorWarn)
b3.setParseAction (errorWarn)
errors.setParseAction (errorWarn)
return abc_header, abc_voice, abc_scoredef, abc_percmap
class pObj (object): # every relevant parse result is converted into a pObj
def __init__ (s, name, t, seq=0): # t = list of nested parse results
s.name = name # name uniqueliy identifies this pObj
rest = [] # collect parse results that are not a pObj
attrs = {} # new attributes
for x in t: # nested pObj's become attributes of this pObj
if type (x) == pObj:
attrs [x.name] = attrs.get (x.name, []) + [x]
else:
rest.append (x) # collect non-pObj's (mostly literals)
for name, xs in attrs.items ():
if len (xs) == 1: xs = xs[0] # only list if more then one pObj
setattr (s, name, xs) # create the new attributes
s.t = rest # all nested non-pObj's (mostly literals)
s.objs = seq and t or [] # for nested ordered (lyric) pObj's
def __repr__ (s): # make a nice string representation of a pObj
r = []
for nm in dir (s):
if nm.startswith ('_'): continue # skip build in attributes
elif nm == 'name': continue # redundant
else:
x = getattr (s, nm)
if not x: continue # s.t may be empty (list of non-pObj's)
if type (x) == list_type: r.extend (x)
else: r.append (x)
xs = []
for x in r: # recursively call __repr__ and convert all strings to latin-1
if isinstance (x, str_type): xs.append (x) # string -> no recursion
else: xs.append (repr (x)) # pObj -> recursive call
return '(' + s.name + ' ' +','.join (xs) + ')'
global prevloc # global to remember previous match position of a note/rest
prevloc = 0
def detectBeamBreak (line, loc, t):
global prevloc # location in string 'line' of previous note match
xs = line[prevloc:loc+1] # string between previous and current note match
xs = xs.lstrip () # first note match starts on a space!
prevloc = loc # location in string 'line' of current note match
b = pObj ('bbrk', [' ' in xs]) # space somewhere between two notes -> beambreak
t.insert (0, b) # insert beambreak as a nested parse result
def noteActn (line, loc, t): # detect beambreak between previous and current note/rest
if 'y' in t[0].t: return [] # discard spacer
detectBeamBreak (line, loc, t) # adds beambreak to parse result t as side effect
return pObj ('note', t)
def restActn (line, loc, t): # detect beambreak between previous and current note/rest
detectBeamBreak (line, loc, t) # adds beambreak to parse result t as side effect
return pObj ('rest', t)
def errorWarn (line, loc, t): # warning for misplaced symbols and skip them
if not t[0]: return [] # only warn if catched string not empty
info ('**misplaced symbol: %s' % t[0], warn=0)
lineCopy = line [:]
if loc > 40:
lineCopy = line [loc - 40: loc + 40]
loc = 40
info (lineCopy.replace ('\n', ' '), warn=0)
info (loc * '-' + '^', warn=0)
return []
#-------------------------------------------------------------
# transformations of a measure (called by parse action doMaat)
#-------------------------------------------------------------
def simplify (a, b): # divide a and b by their greatest common divisor
x, y = a, b
while b: a, b = b, a % b
return x // a, y // a
def doBroken (prev, brk, x):
if not prev: info ('error in broken rhythm: %s' % x); return # no changes
nom1, den1 = prev.dur.t # duration of first note/chord
nom2, den2 = x.dur.t # duration of second note/chord
if brk == '>':
nom1, den1 = simplify (3 * nom1, 2 * den1)
nom2, den2 = simplify (1 * nom2, 2 * den2)
elif brk == '<':
nom1, den1 = simplify (1 * nom1, 2 * den1)
nom2, den2 = simplify (3 * nom2, 2 * den2)
elif brk == '>>':
nom1, den1 = simplify (7 * nom1, 4 * den1)
nom2, den2 = simplify (1 * nom2, 4 * den2)
elif brk == '<<':
nom1, den1 = simplify (1 * nom1, 4 * den1)
nom2, den2 = simplify (7 * nom2, 4 * den2)
else: return # give up
prev.dur.t = nom1, den1 # change duration of previous note/chord
x.dur.t = nom2, den2 # and current note/chord
def convertBroken (t): # convert broken rhythms to normal note durations
prev = None # the last note/chord before the broken symbol
brk = '' # the broken symbol
remove = [] # indexes to broken symbols (to be deleted) in measure
for i, x in enumerate (t): # scan all elements in measure
if x.name == 'note' or x.name == 'chord' or x.name == 'rest':
if brk: # a broken symbol was encountered before
doBroken (prev, brk, x) # change duration previous note/chord/rest and current one
brk = ''
else:
prev = x # remember the last note/chord/rest
elif x.name == 'broken':
brk = x.t[0] # remember the broken symbol (=string)
remove.insert (0, i) # and its index, highest index first
for i in remove: del t[i] # delete broken symbols from high to low
def ptc2midi (n): # convert parsed pitch attribute to a midi number
pt = getattr (n, 'pitch', '')
if pt:
p = pt.t
if len (p) == 3: acc, step, oct = p
else: acc = ''; step, oct = p
nUp = step.upper ()
oct = (4 if nUp == step else 5) + int (oct)
midi = oct * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (nUp)] + {'^':1,'_':-1}.get (acc, 0) + 12
else: midi = 130 # all non pitch objects first
return midi
def convertChord (t): # convert chord to sequence of notes in musicXml-style
ins = []
for i, x in enumerate (t):
if x.name == 'chord':
if hasattr (x, 'rest') and not hasattr (x, 'note'): # chords containing only rests
if type (x.rest) == list_type: x.rest = x.rest[0] # more rests == one rest
ins.insert (0, (i, [x.rest])) # just output a single rest, no chord
continue
num1, den1 = x.dur.t # chord duration
tie = getattr (x, 'tie', None) # chord tie
slurs = getattr (x, 'slurs', []) # slur endings
if type (x.note) != list_type: x.note = [x.note] # when chord has only one note ...
elms = []; j = 0 # sort chord notes, highest first
nss = sorted (x.objs, key = ptc2midi, reverse=1) if mxm.orderChords else x.objs
for nt in nss: # all chord elements (note | decorations | rest | grace note)
if nt.name == 'note':
num2, den2 = nt.dur.t # note duration * chord duration
nt.dur.t = simplify (num1 * num2, den1 * den2)
if tie: nt.tie = tie # tie on all chord notes
if j == 0 and slurs: nt.slurs = slurs # slur endings only on first chord note
if j > 0: nt.chord = pObj ('chord', [1]) # label all but first as chord notes
else: # remember all pitches of the chord in the first note
pitches = [n.pitch for n in x.note] # to implement conversion of erroneous ties to slurs
nt.pitches = pObj ('pitches', pitches)
j += 1
if nt.name not in ['dur','tie','slurs','rest']: elms.append (nt)
ins.insert (0, (i, elms)) # chord position, [note|decotation|grace note]
for i, notes in ins: # insert from high to low
for nt in reversed (notes):
t.insert (i+1, nt) # insert chord notes after chord
del t[i] # remove chord itself
def doMaat (t): # t is a Group() result -> the measure is in t[0]
convertBroken (t[0]) # remove all broken rhythms and convert to normal durations
convertChord (t[0]) # replace chords by note sequences in musicXML style
def doGrace (t): # t is a Group() result -> the grace sequence is in t[0]
convertChord (t[0]) # a grace sequence may have chords
for nt in t[0]: # flag all notes within the grace sequence
if nt.name == 'note': nt.grace = 1 # set grace attribute
return t[0] # ungroup the parse result
#--------------------
# musicXML generation
#----------------------------------
def compChordTab (): # avoid some typing work: returns mapping constant {ABC chordsyms -> musicXML kind}
maj, min, aug, dim, dom, ch7, ch6, ch9, ch11, ch13, hd = 'major minor augmented diminished dominant -seventh -sixth -ninth -11th -13th half-diminished'.split ()
triad = zip ('ma Maj maj M mi min m aug dim o + -'.split (), [maj, maj, maj, maj, min, min, min, aug, dim, dim, aug, min])
seventh = zip ('7 ma7 Maj7 M7 maj7 mi7 min7 m7 dim7 o7 -7 aug7 +7 m7b5 mi7b5'.split (),
[dom, maj+ch7, maj+ch7, maj+ch7, maj+ch7, min+ch7, min+ch7, min+ch7, dim+ch7, dim+ch7, min+ch7, aug+ch7, aug+ch7, hd, hd])
sixth = zip ('6 ma6 M6 mi6 min6 m6'.split (), [maj+ch6, maj+ch6, maj+ch6, min+ch6, min+ch6, min+ch6])
ninth = zip ('9 ma9 M9 maj9 Maj9 mi9 min9 m9'.split (), [dom+ch9, maj+ch9, maj+ch9, maj+ch9, maj+ch9, min+ch9, min+ch9, min+ch9])
elevn = zip ('11 ma11 M11 maj11 Maj11 mi11 min11 m11'.split (), [dom+ch11, maj+ch11, maj+ch11, maj+ch11, maj+ch11, min+ch11, min+ch11, min+ch11])
thirt = zip ('13 ma13 M13 maj13 Maj13 mi13 min13 m13'.split (), [dom+ch13, maj+ch13, maj+ch13, maj+ch13, maj+ch13, min+ch13, min+ch13, min+ch13])
sus = zip ('sus sus4 sus2'.split (), ['suspended-fourth', 'suspended-fourth', 'suspended-second'])
return dict (list (triad) + list (seventh) + list (sixth) + list (ninth) + list (elevn) + list (thirt) + list (sus))
def addElem (parent, child, level):
indent = 2
chldrn = list (parent)
if chldrn:
chldrn[-1].tail += indent * ' '
else:
parent.text = '\n' + level * indent * ' '
parent.append (child)
child.tail = '\n' + (level-1) * indent * ' '
def addElemT (parent, tag, text, level):
e = E.Element (tag)
e.text = text
addElem (parent, e, level)
return e
def mkTmod (tmnum, tmden, lev):
tmod = E.Element ('time-modification')
addElemT (tmod, 'actual-notes', str (tmnum), lev + 1)
addElemT (tmod, 'normal-notes', str (tmden), lev + 1)
return tmod
def addDirection (parent, elems, lev, gstaff, subelms=[], placement='below', cue_on=0):
dir = E.Element ('direction', placement=placement)
addElem (parent, dir, lev)
if type (elems) != list_type: elems = [(elems, subelms)] # ugly hack to provide for multiple direction types
for elem, subelms in elems: # add direction types
typ = E.Element ('direction-type')
addElem (dir, typ, lev + 1)
addElem (typ, elem, lev + 2)
for subel in subelms: addElem (elem, subel, lev + 3)
if cue_on: addElem (dir, E.Element ('level', size='cue'), lev + 1)
if gstaff: addElemT (dir, 'staff', str (gstaff), lev + 1)
return dir
def removeElems (root_elem, parent_str, elem_str):
for p in root_elem.findall (parent_str):
e = p.find (elem_str)
if e != None: p.remove (e)
def alignLyr (vce, lyrs):
empty_el = pObj ('leeg', '*')
for k, lyr in enumerate (lyrs): # lyr = one full line of lyrics
i = 0 # syl counter
for elem in vce: # reiterate the voice block for each lyrics line
if elem.name == 'note' and not (hasattr (elem, 'chord') or hasattr (elem, 'grace')):
if i >= len (lyr): lr = empty_el
else: lr = lyr [i]
lr.t[0] = lr.t[0].replace ('%5d',']')
elem.objs.append (lr)
if lr.name != 'sbar': i += 1
if elem.name == 'rbar' and i < len (lyr) and lyr[i].name == 'sbar': i += 1
return vce
slur_move = re.compile (r'(?<![!+])([}><][<>]?)(\)+)') # (?<!...) means: not preceeded by ...
mm_rest = re.compile (r'([XZ])(\d+)')
bar_space = re.compile (r'([:|][ |\[\]]+[:|])') # barlines with spaces
def fixSlurs (x): # repair slurs when after broken sign or grace-close
def f (mo): # replace a multi-measure rest by single measure rests
n = int (mo.group (2))
return (n * (mo.group (1) + '|')) [:-1]
def g (mo): # squash spaces in barline expressions
return mo.group (1).replace (' ','')
x = mm_rest.sub (f, x)
x = bar_space.sub (g, x)
return slur_move.sub (r'\2\1', x)
def splitHeaderVoices (abctext):
escField = lambda x: '[' + x.replace (']',r'%5d') + ']' # hope nobody uses %5d in a field
r1 = re.compile (r'%.*$') # comments
r2 = re.compile (r'^([A-Zw]:.*$)|\[[A-Zw]:[^]]*]$') # information field, including lyrics
r3 = re.compile (r'^%%(?=[^%])') # directive: ^%% folowed by not a %
xs, nx, mcont, fcont = [], 0, 0, 0 # result lines, X-encountered, music continuation, field continuation
mln = fln = '' # music line, field line
for x in abctext.splitlines ():
x = x.strip ()
if not x and nx == 1: break # end of tune (empty line)
if x.startswith ('X:'):
if nx == 1: break # second tune starts without an empty line !!
nx = 1 # start first tune
x = r3.sub ('I:', x) # replace %% -> I:
x2 = r1.sub ('', x) # remove comment
while x2.endswith ('*') and not (x2.startswith ('w:') or x2.startswith ('+:') or 'percmap' in x2):
x2 = x2[:-1] # remove old syntax for right adjusting
if not x2: continue # empty line
if x2[:2] == 'W:':
field = x2 [2:].strip ()
ftype = mxm.metaMap.get ('W', 'W') # respect the (user defined --meta) mapping of various ABC fields to XML meta data types
c = mxm.metadata.get (ftype, '')
mxm.metadata [ftype] = c + '\n' + field if c else field # concatenate multiple info fields with new line as separator
continue # skip W: lyrics
if x2[:2] == '+:': # field continuation
fln += x2[2:]
continue
ro = r2.match (x2) # single field on a line
if ro: # field -> inline_field, escape all ']'
if fcont: # old style \-info-continuation active
fcont = x2 [-1] == '\\' # possible further \-info-continuation
fln += re.sub (r'^.:(.*?)\\*$', r'\1', x2) # add continuation, remove .: and \
continue
if fln: mln += escField (fln)
if x2.startswith ('['): x2 = x2.strip ('[]')
fcont = x2 [-1] == '\\' # first encounter of old style \-info-continuation
fln = x2.rstrip ('\\') # remove continuation from field and inline brackets
continue
if nx == 1: # x2 is a new music line
fcont = 0 # stop \-continuations (-> only adjacent \-info-continuations are joined)
if fln:
mln += escField (fln)
fln = ''
if mcont:
mcont = x2 [-1] == '\\'
mln += x2.rstrip ('\\')
else:
if mln: xs.append (mln); mln = ''
mcont = x2 [-1] == '\\'
mln = x2.rstrip ('\\')
if not mcont: xs.append (mln); mln = ''
if fln: mln += escField (fln)
if mln: xs.append (mln)
hs = re.split (r'(\[K:[^]]*\])', xs [0]) # look for end of header K:
if len (hs) == 1: header = hs[0]; xs [0] = '' # no K: present
else: header = hs [0] + hs [1]; xs [0] = ''.join (hs[2:]) # h[1] is the first K:
abctext = '\n'.join (xs) # the rest is body text
hfs, vfs = [], []
for x in header[1:-1].split (']['):
if x[0] == 'V': vfs.append (x) # filter voice- and midi-definitions
elif x[:6] == 'I:MIDI': vfs.append (x) # from the header to vfs
elif x[:9] == 'I:percmap': vfs.append (x) # and also percmap
else: hfs.append (x) # all other fields stay in header
header = '[' + ']['.join (hfs) + ']' # restore the header
abctext = ('[' + ']['.join (vfs) + ']' if vfs else '') + abctext # prepend voice/midi from header before abctext
xs = abctext.split ('[V:')
if len (xs) == 1: abctext = '[V:1]' + abctext # abc has no voice defs at all
elif re.sub (r'\[[A-Z]:[^]]*\]', '', xs[0]).strip (): # remove inline fields from starting text, if any
abctext = '[V:1]' + abctext # abc with voices has no V: at start
r1 = re.compile (r'\[V:\s*(\S*)[ \]]') # get voice id from V: field (skip spaces betwee V: and ID)
vmap = {} # {voice id -> [voice abc string]}
vorder = {} # mark document order of voices
xs = re.split (r'(\[V:[^]]*\])', abctext) # split on every V-field (V-fields included in split result list)
if len (xs) == 1: raise ValueError ('bugs ...')
else:
pm = re.findall (r'\[P:.\]', xs[0]) # all P:-marks after K: but before first V:
if pm: xs[2] = ''.join (pm) + xs[2] # prepend P:-marks to the text of the first voice
header += re.sub (r'\[P:.\]', '', xs[0]) # clear all P:-marks from text between K: and first V: and put text in the header
i = 1
while i < len (xs): # xs = ['', V-field, voice abc, V-field, voice abc, ...]
vce, abc = xs[i:i+2]
id = r1.search (vce).group (1) # get voice ID from V-field
if not id: id, vce = '1', '[V:1]' # voice def has no ID
vmap[id] = vmap.get (id, []) + [vce, abc] # collect abc-text for each voice id (include V-fields)
if id not in vorder: vorder [id] = i # store document order of first occurrence of voice id
i += 2
voices = []
ixs = sorted ([(i, id) for id, i in vorder.items ()]) # restore document order of voices
for i, id in ixs:
voice = ''.join (vmap [id]) # all abc of one voice
voice = fixSlurs (voice) # put slurs right after the notes
voices.append ((id, voice))
return header, voices
def mergeMeasure (m1, m2, slur_offset, voice_offset, rOpt, is_grand=0, is_overlay=0):
slurs = m2.findall ('note/notations/slur')
for slr in slurs:
slrnum = int (slr.get ('number')) + slur_offset
slr.set ('number', str (slrnum)) # make unique slurnums in m2
vs = m2.findall ('note/voice') # set all voice number elements in m2
for v in vs: v.text = str (voice_offset + int (v.text))
ls = m1.findall ('note/lyric') # all lyric elements in m1
lnum_max = max ([int (l.get ('number')) for l in ls] + [0]) # highest lyric number in m1
ls = m2.findall ('note/lyric') # update lyric elements in m2
for el in ls:
n = int (el.get ('number'))
el.set ('number', str (n + lnum_max))
ns = m1.findall ('note') # determine the total duration of m1, subtract all backups
dur1 = sum (int (n.find ('duration').text) for n in ns
if n.find ('grace') == None and n.find ('chord') == None)
dur1 -= sum (int (b.text) for b in m1.findall ('backup/duration'))
repbar, nns, es = 0, 0, [] # nns = number of real notes in m2
for e in list (m2): # scan all elements of m2
if e.tag == 'attributes':
if not is_grand: continue # no attribute merging for normal voices
else: nns += 1 # but we do merge (clef) attributes for a grand staff
if e.tag == 'print': continue
if e.tag == 'note' and (rOpt or e.find ('rest') == None): nns += 1
if e.tag == 'barline' and e.find ('repeat') != None: repbar = e;
es.append (e) # buffer elements to be merged
if nns > 0: # only merge if m2 contains any real notes
if dur1 > 0: # only insert backup if duration of m1 > 0
b = E.Element ('backup')
addElem (m1, b, level=3)
addElemT (b, 'duration', str (dur1), level=4)
for e in es: addElem (m1, e, level=3) # merge buffered elements of m2
elif is_overlay and repbar: addElem (m1, repbar, level=3) # merge repeat in empty overlay
def mergePartList (parts, rOpt, is_grand=0): # merge parts, make grand staff when is_grand true
def delAttrs (part): # for the time being we only keep clef attributes
xs = [(m, e) for m in part.findall ('measure') for e in m.findall ('attributes')]
for m, e in xs:
for c in list (e):
if c.tag == 'clef': continue # keep clef attribute
if c.tag == 'staff-details': continue # keep staff-details attribute
e.remove (c) # delete all other attrinutes for higher staff numbers
if len (list (e)) == 0: m.remove (e) # remove empty attributes element
p1 = parts[0]
for p2 in parts[1:]:
if is_grand: delAttrs (p2) # delete all attributes except clef
for i in range (len (p1) + 1, len (p2) + 1): # second part longer than first one
maat = E.Element ('measure', number = str(i)) # append empty measures
addElem (p1, maat, 2)
slurs = p1.findall ('measure/note/notations/slur') # find highest slur num in first part
slur_max = max ([int (slr.get ('number')) for slr in slurs] + [0])
vs = p1.findall ('measure/note/voice') # all voice number elements in first part
vnum_max = max ([int (v.text) for v in vs] + [0]) # highest voice number in first part
for im, m2 in enumerate (p2.findall ('measure')): # merge all measures of p2 into p1
mergeMeasure (p1[im], m2, slur_max, vnum_max, rOpt, is_grand) # may change slur numbers in p1
return p1
def mergeParts (parts, vids, staves, rOpt, is_grand=0):
if not staves: return parts, vids # no voice mapping
partsnew, vidsnew = [], []
for voice_ids in staves:
pixs = []
for vid in voice_ids:
if vid in vids: pixs.append (vids.index (vid))
else: info ('score partname %s does not exist' % vid)
if pixs:
xparts = [parts[pix] for pix in pixs]
if len (xparts) > 1: mergedpart = mergePartList (xparts, rOpt, is_grand)
else: mergedpart = xparts [0]
partsnew.append (mergedpart)
vidsnew.append (vids [pixs[0]])
return partsnew, vidsnew
def mergePartMeasure (part, msre, ovrlaynum, rOpt): # merge msre into last measure of part, only for overlays
slur_offset = 0; # slur numbers determined by the slurstack size (as in a single voice)
last_msre = list (part)[-1] # last measure in part
mergeMeasure (last_msre, msre, slur_offset, ovrlaynum, rOpt, is_overlay=1) # voice offset = s.overlayVNum
def pushSlur (boogStapel, stem):
if stem not in boogStapel: boogStapel [stem] = [] # initialize slurstack for stem
boognum = sum (map (len, boogStapel.values ())) + 1 # number of open slurs in all (overlay) voices
boogStapel [stem].append (boognum)
return boognum
def setFristVoiceNameFromGroup (vids, vdefs): # vids = [vid], vdef = {vid -> (name, subname, voicedef)}
vids = [v for v in vids if v in vdefs] # only consider defined voices
if not vids: return vdefs
vid0 = vids [0] # first vid of the group
_, _, vdef0 = vdefs [vid0] # keep de voice definition (vdef0) when renaming vid0
for vid in vids:
nm, snm, vdef = vdefs [vid]
if nm: # first non empty name encountered will become
vdefs [vid0] = nm, snm, vdef0 # name of merged group == name of first voice in group (vid0)
break
return vdefs
def mkGrand (p, vdefs): # transform parse subtree into list needed for s.grands
xs = []
for i, x in enumerate (p.objs): # changing p.objs [i] alters the tree. changing x has no effect on the tree.
if type (x) == pObj:
us = mkGrand (x, vdefs) # first get transformation results of current pObj
if x.name == 'grand': # x.objs contains ordered list of nested parse results within x
vids = [y.objs[0] for y in x.objs[1:]] # the voice ids in the grand staff
nms = [vdefs [u][0] for u in vids if u in vdefs] # the names of those voices
accept = sum ([1 for nm in nms if nm]) == 1 # accept as grand staff when only one of the voices has a name
if accept or us[0] == '{*':
xs.append (us[1:]) # append voice ids as a list (discard first item '{' or '{*')
vdefs = setFristVoiceNameFromGroup (vids, vdefs)
p.objs [i] = x.objs[1] # replace voices by first one in the grand group (this modifies the parse tree)
else:
xs.extend (us[1:]) # extend current result with all voice ids of rejected grand staff
else: xs.extend (us) # extend current result with transformed pObj
else: xs.append (p.t[0]) # append the non pObj (== voice id string)
return xs
def mkStaves (p, vdefs): # transform parse tree into list needed for s.staves
xs = []
for i, x in enumerate (p.objs): # structure and comments identical to mkGrand
if type (x) == pObj:
us = mkStaves (x, vdefs)
if x.name == 'voicegr':
xs.append (us)
vids = [y.objs[0] for y in x.objs]
vdefs = setFristVoiceNameFromGroup (vids, vdefs)
p.objs [i] = x.objs[0]
else:
xs.extend (us)
else:
if p.t[0] not in '{*': xs.append (p.t[0])
return xs
def mkGroups (p): # transform parse tree into list needed for s.groups
xs = []
for x in p.objs:
if type (x) == pObj:
if x.name == 'vid': xs.extend (mkGroups (x))
elif x.name == 'bracketgr': xs.extend (['['] + mkGroups (x) + [']'])
elif x.name == 'bracegr': xs.extend (['{'] + mkGroups (x) + ['}'])
else: xs.extend (mkGroups (x) + ['}']) # x.name == 'grand' == rejected grand staff
else:
xs.append (p.t[0])
return xs
def stepTrans (step, soct, clef): # [A-G] (1...8)
if clef.startswith ('bass'):
nm7 = 'C,D,E,F,G,A,B'.split (',')
n = 14 + nm7.index (step) - 12 # two octaves extra to avoid negative numbers
step, soct = nm7 [n % 7], soct + n // 7 - 2 # subtract two octaves again
return step, soct
def reduceMids (parts, vidsnew, midiInst): # remove redundant instruments from a part
for pid, part in zip (vidsnew, parts):
mids, repls, has_perc = {}, {}, 0
for ipid, ivid, ch, prg, vol, pan in sorted (list (midiInst.values ())):
if ipid != pid: continue # only instruments from part pid
if ch == '10': has_perc = 1; continue # only consider non percussion instruments
instId, inst = 'I%s-%s' % (ipid, ivid), (ch, prg)
if inst in mids: # midi instrument already defined in this part
repls [instId] = mids [inst] # remember to replace instId by inst (see below)
del midiInst [instId] # instId is redundant
else: mids [inst] = instId # collect unique instruments in this part
if len (mids) < 2 and not has_perc: # only one instrument used -> no instrument tags needed in notes
removeElems (part, 'measure/note', 'instrument') # no instrument tag needed for one- or no-instrument parts
else:
for e in part.findall ('measure/note/instrument'):
id = e.get ('id') # replace all redundant instrument Id's
if id in repls: e.set ('id', repls [id])
class stringAlloc:
def __init__ (s):
s.snaarVrij = [] # [[(t1, t2) ...] for each string ]
s.snaarIx = [] # index in snaarVrij for each string
s.curstaff = -1 # staff being allocated
def beginZoek (s): # reset snaarIx at start of each voice
s.snaarIx = []
for i in range (len (s.snaarVrij)): s.snaarIx.append (0)
def setlines (s, stflines, stfnum):
if stfnum != s.curstaff: # initialize for new staff
s.curstaff = stfnum
s.snaarVrij = []
for i in range (stflines): s.snaarVrij.append ([])
s.beginZoek ()
def isVrij (s, snaar, t1, t2): # see if string snaar is free between t1 and t2
xs = s.snaarVrij [snaar]
for i in range (s.snaarIx [snaar], len (xs)):
tb, te = xs [i]
if t1 >= te: continue # te_prev < t1 <= te
if t1 >= tb: s.snaarIx [snaar] = i; return 0 # tb <= t1 < te
if t2 > tb: s.snaarIx [snaar] = i; return 0 # t1 < tb < t2
s.snaarIx [snaar] = i; # remember position for next call
xs.insert (i, (t1,t2)) # te_prev < t1 < t2 < tb
return 1
xs.append ((t1,t2))
s.snaarIx [snaar] = len (xs) - 1
return 1
def bezet (s, snaar, t1, t2): # force allocation of note (t1,t2) on string snaar
xs = s.snaarVrij [snaar]
for i, (tb, te) in enumerate (xs):
if t1 >= te: continue # te_prev < t1 <= te
xs.insert (i, (t1, t2))
return
xs.append ((t1,t2))
class MusicXml:
typeMap = {1:'long', 2:'breve', 4:'whole', 8:'half', 16:'quarter', 32:'eighth', 64:'16th', 128:'32nd', 256:'64th'}
dynaMap = {'p':1,'pp':1,'ppp':1,'pppp':1,'f':1,'ff':1,'fff':1,'ffff':1,'mp':1,'mf':1,'sfz':1}
tempoMap = {'larghissimo':40, 'moderato':104, 'adagissimo':44, 'allegretto':112, 'lentissimo':48, 'allegro':120, 'largo':56,
'vivace':168, 'adagio':59, 'vivo':180, 'lento':62, 'presto':192, 'larghetto':66, 'allegrissimo':208, 'adagietto':76,
'vivacissimo':220, 'andante':88, 'prestissimo':240, 'andantino':96}
wedgeMap = {'>(':1, '>)':1, '<(':1,'<)':1,'crescendo(':1,'crescendo)':1,'diminuendo(':1,'diminuendo)':1}
artMap = {'.':'staccato','>':'accent','accent':'accent','wedge':'staccatissimo','tenuto':'tenuto',
'breath':'breath-mark','marcato':'strong-accent','^':'strong-accent','slide':'scoop'}
ornMap = {'trill':'trill-mark','T':'trill-mark','turn':'turn','uppermordent':'inverted-mordent','lowermordent':'mordent',
'pralltriller':'inverted-mordent','mordent':'mordent','turn':'turn','invertedturn':'inverted-turn'}
tecMap = {'upbow':'up-bow', 'downbow':'down-bow', 'plus':'stopped','open':'open-string','snap':'snap-pizzicato',
'thumb':'thumb-position'}
capoMap = {'fine':('Fine','fine','yes'), 'D.S.':('D.S.','dalsegno','segno'), 'D.C.':('D.C.','dacapo','yes'),'dacapo':('D.C.','dacapo','yes'),
'dacoda':('To Coda','tocoda','coda'), 'coda':('coda','coda','coda'), 'segno':('segno','segno','segno')}
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, 'm':11, 'min':11, 'mix':9, 'dor':10, 'phr':12, 'lyd':7, 'loc':13}
modTab = {'maj':'major', 'm':'minor', 'min':'minor', 'mix':'mixolydian', 'dor':'dorian', 'phr':'phrygian', 'lyd':'lydian', 'loc':'locrian'}
clefMap = { 'alto1':('C','1'), 'alto2':('C','2'), 'alto':('C','3'), 'alto4':('C','4'), 'tenor':('C','4'),
'bass3':('F','3'), 'bass':('F','4'), 'treble':('G','2'), 'perc':('percussion',''), 'none':('',''), 'tab':('TAB','5')}
clefLineMap = {'B':'treble', 'G':'alto1', 'E':'alto2', 'C':'alto', 'A':'tenor', 'F':'bass3', 'D':'bass'}
alterTab = {'=':'0', '_':'-1', '__':'-2', '^':'1', '^^':'2'}
accTab = {'=':'natural', '_':'flat', '__':'flat-flat', '^':'sharp', '^^':'sharp-sharp'}
chordTab = compChordTab ()
uSyms = {'~':'roll', 'H':'fermata','L':'>','M':'lowermordent','O':'coda',
'P':'uppermordent','S':'segno','T':'trill','u':'upbow','v':'downbow'}
pageFmtDef = [0.75,297,210,18,18,10,10] # the abcm2ps page formatting defaults for A4
metaTab = {'O':'origin', 'A':'area', 'Z':'transcription', 'N':'notes', 'G':'group', 'H':'history', 'R':'rhythm',
'B':'book', 'D':'discography', 'F':'fileurl', 'S':'source', 'P':'partmap', 'W':'lyrics'}
metaMap = {'C':'composer'} # mapping of composer is fixed
metaTypes = {'composer':1,'lyricist':1,'poet':1,'arranger':1,'translator':1, 'rights':1} # valid MusicXML meta data types
tuningDef = 'E2,A2,D3,G3,B3,E4'.split (',') # default string tuning (guitar)
def __init__ (s):
s.pageFmtCmd = [] # set by command line option -p
s.reset ()
def reset (s, fOpt=False):
s.divisions = 2520 # xml duration of 1/4 note, 2^3 * 3^2 * 5 * 7 => 5,7,9 tuplets
s.ties = {} # {abc pitch tuple -> alteration} for all open ties
s.slurstack = {} # stack of open slur numbers per (overlay) voice
s.slurbeg = [] # type of slurs to start (when slurs are detected at element-level)
s.tmnum = 0 # time modification, numerator
s.tmden = 0 # time modification, denominator
s.ntup = 0 # number of tuplet notes remaining
s.trem = 0 # number of bars for tremolo
s.intrem = 0 # mark tremolo sequence (for duration doubling)
s.tupnts = [] # all tuplet modifiers with corresp. durations: [(duration, modifier), ...]
s.irrtup = 0 # 1 if an irregular tuplet
s.ntype = '' # the normal-type of a tuplet (== duration type of a normal tuplet note)
s.unitL = (1, 8) # default unit length
s.unitLcur = (1, 8) # unit length of current voice
s.keyAlts = {} # alterations implied by key
s.msreAlts = {} # temporarily alterations
s.curVolta = '' # open volta bracket
s.title = '' # title of music
s.creator = {} # {creator-type -> creator string}
s.metadata = {} # {metadata-type -> string}
s.lyrdash = {} # {lyric number -> 1 if dash between syllables}
s.usrSyms = s.uSyms # user defined symbols
s.prevNote = None # xml element of previous beamed note to correct beams (start, continue)
s.prevLyric = {} # xml element of previous lyric to add/correct extend type (start, continue)
s.grcbbrk = False # remember any bbrk in a grace sequence
s.linebrk = 0 # 1 if next measure should start with a line break
s.nextdecos = [] # decorations for the next note
s.prevmsre = None # the previous measure
s.supports_tag = 0 # issue supports-tag in xml file when abc uses explicit linebreaks
s.staveDefs = [] # collected %%staves or %%score instructions from score
s.staves = [] # staves = [[voice names to be merged into one stave]]
s.groups = [] # list of merged part names with interspersed {[ and }]
s.grands = [] # [[vid1, vid2, ..], ...] voiceIds to be merged in a grand staff
s.gStaffNums = {} # map each voice id in a grand staff to a staff number
s.gNstaves = {} # map each voice id in a grand staff to total number of staves
s.pageFmtAbc = [] # formatting from abc directives
s.mdur = (4,4) # duration of one measure
s.gtrans = 0 # octave transposition (by clef)
s.midprg = ['', '', '', ''] # MIDI channel nr, program nr, volume, panning for the current part
s.vid = '' # abc voice id for the current voice
s.pid = '' # xml part id for the current voice
s.gcue_on = 0 # insert <cue/> tag in each note
s.percVoice = 0 # 1 if percussion enabled
s.percMap = {} # (part-id, abc_pitch, xml-octave) -> (abc staff step, midi note number, xml notehead)
s.pMapFound = 0 # at least one I:percmap has been found
s.vcepid = {} # voice_id -> part_id
s.midiInst = {} # inst_id -> (part_id, voice_id, channel, midi_number), remember instruments used
s.capo = 0 # fret position of the capodastro
s.tunmid = [] # midi numbers of strings
s.tunTup = [] # ordered midi numbers of strings [(midi_num, string_num), ...] (midi_num from high to low)
s.fOpt = fOpt # force string/fret allocations for tab staves
s.orderChords = 0 # order notes in a chord
s.chordDecos = {} # decos that should be distributed to all chord notes for xml
ch10 = 'acoustic-bass-drum,35;bass-drum-1,36;side-stick,37;acoustic-snare,38;hand-clap,39;electric-snare,40;low-floor-tom,41;closed-hi-hat,42;high-floor-tom,43;pedal-hi-hat,44;low-tom,45;open-hi-hat,46;low-mid-tom,47;hi-mid-tom,48;crash-cymbal-1,49;high-tom,50;ride-cymbal-1,51;chinese-cymbal,52;ride-bell,53;tambourine,54;splash-cymbal,55;cowbell,56;crash-cymbal-2,57;vibraslap,58;ride-cymbal-2,59;hi-bongo,60;low-bongo,61;mute-hi-conga,62;open-hi-conga,63;low-conga,64;high-timbale,65;low-timbale,66;high-agogo,67;low-agogo,68;cabasa,69;maracas,70;short-whistle,71;long-whistle,72;short-guiro,73;long-guiro,74;claves,75;hi-wood-block,76;low-wood-block,77;mute-cuica,78;open-cuica,79;mute-triangle,80;open-triangle,81'
s.percsnd = [x.split (',') for x in ch10.split (';')] # {name -> midi number} of standard channel 10 sound names
s.gTime = (0,0) # (XML begin time, XML end time) in divisions
s.tabStaff = '' # == pid (part ID) for a tab staff
def mkPitch (s, acc, note, oct, lev):
if s.percVoice: # percussion map switched off by perc=off (see doClef)
octq = int (oct) + s.gtrans # honour the octave= transposition when querying percmap
tup = s.percMap.get ((s.pid, acc+note, octq), s.percMap.get (('', acc+note, octq), 0))
if tup: step, soct, midi, notehead = tup
else: step, soct = note, octq
octnum = (4 if step.upper() == step else 5) + int (soct)
if not tup: # add percussion map for unmapped notes in this part
midi = str (octnum * 12 + [0,2,4,5,7,9,11]['CDEFGAB'.index (step.upper())] + {'^':1,'_':-1}.get (acc, 0) + 12)
notehead = {'^':'x', '_':'circle-x'}.get (acc, 'normal')
if s.pMapFound: info ('no I:percmap for: %s%s in part %s, voice %s' % (acc+note, -oct*',' if oct<0 else oct*"'", s.pid, s.vid))
s.percMap [(s.pid, acc+note, octq)] = (note, octq, midi, notehead)
else: # correct step value for clef
step, octnum = stepTrans (step.upper (), octnum, s.curClef)
pitch = E.Element ('unpitched')
addElemT (pitch, 'display-step', step.upper (), lev + 1)
addElemT (pitch, 'display-octave', str (octnum), lev + 1)
return pitch, '', midi, notehead
nUp = note.upper ()
octnum = (4 if nUp == note else 5) + int (oct) + s.gtrans
pitch = E.Element ('pitch')
addElemT (pitch, 'step', nUp, lev + 1)
alter = ''
if (note, oct) in s.ties:
tied_alter, _, vnum, _ = s.ties [(note,oct)] # vnum = overlay voice number when tie started
if vnum == s.overlayVnum: alter = tied_alter # tied note in the same overlay -> same alteration
elif acc:
s.msreAlts [(nUp, octnum)] = s.alterTab [acc]
alter = s.alterTab [acc] # explicit notated alteration
elif (nUp, octnum) in s.msreAlts: alter = s.msreAlts [(nUp, octnum)] # temporary alteration
elif nUp in s.keyAlts: alter = s.keyAlts [nUp] # alteration implied by the key
if alter: addElemT (pitch, 'alter', alter, lev + 1)
addElemT (pitch, 'octave', str (octnum), lev + 1)
return pitch, alter, '', ''
def getNoteDecos (s, n):
decos = s.nextdecos # decorations encountered so far
ndeco = getattr (n, 'deco', 0) # possible decorations of notes of a chord
if ndeco: # add decorations, translate used defined symbols
decos += [s.usrSyms.get (d, d).strip ('!+') for d in ndeco.t]
s.nextdecos = []
if s.tabStaff == s.pid and s.fOpt and n.name != 'rest': # force fret/string allocation if explicit string decoration is missing
if [d for d in decos if d in '0123456789'] == []: decos.append ('0')
return decos
def mkNote (s, n, lev):
isgrace = getattr (n, 'grace', '')
ischord = getattr (n, 'chord', '')
if s.ntup >= 0 and not isgrace and not ischord:
s.ntup -= 1 # count tuplet notes only on non-chord, non grace notes
if s.ntup == -1 and s.trem <= 0:
s.intrem = 0 # tremolo pair ends at first note that is not a new tremolo pair (s.trem > 0)
nnum, nden = n.dur.t # abc dutation of note
if s.intrem: nnum += nnum # double duration of tremolo duplets
if nden == 0: nden = 1 # occurs with illegal ABC like: "A2 1". Now interpreted as A2/1
num, den = simplify (nnum * s.unitLcur[0], nden * s.unitLcur[1]) # normalised with unit length
if den > 64: # limit denominator to 64
num = int (round (64 * float (num) / den)) # scale note to num/64
num, den = simplify (max ([num, 1]), 64) # smallest num == 1
info ('duration too small: rounded to %d/%d' % (num, den))
if n.name == 'rest' and ('Z' in n.t or 'X' in n.t):
num, den = s.mdur # duration of one measure
noMsrRest = not (n.name == 'rest' and (num, den) == s.mdur) # not a measure rest
dvs = (4 * s.divisions * num) // den # divisions is xml-duration of 1/4
rdvs = dvs # real duration (will be 0 for chord/grace)
num, den = simplify (num, den * 4) # scale by 1/4 for s.typeMap
ndot = 0
if num == 3 and noMsrRest: ndot = 1; den = den // 2 # look for dotted notes
if num == 7 and noMsrRest: ndot = 2; den = den // 4
nt = E.Element ('note')
if isgrace: # a grace note (and possibly a chord note)
grace = E.Element ('grace')
if s.acciatura: grace.set ('slash', 'yes'); s.acciatura = 0
addElem (nt, grace, lev + 1)
dvs = rdvs = 0 # no (real) duration for a grace note
if den <= 16: den = 32 # not longer than 1/8 for a grace note
if s.gcue_on: # insert cue tag
cue = E.Element ('cue')
addElem (nt, cue, lev + 1)
if ischord: # a chord note
chord = E.Element ('chord')
addElem (nt, chord, lev + 1)
rdvs = 0 # chord notes no real duration
if den not in s.typeMap: # take the nearest smaller legal duration
info ('illegal duration %d/%d' % (nnum, nden))
den = min (x for x in s.typeMap.keys () if x > den)
xmltype = str (s.typeMap [den]) # xml needs the note type in addition to duration
acc, step, oct = '', 'C', '0' # abc-notated pitch elements (accidental, pitch step, octave)
alter, midi, notehead = '', '', '' # xml alteration
if n.name == 'rest':
if 'x' in n.t or 'X' in n.t: nt.set ('print-object', 'no')
rest = E.Element ('rest')
if not noMsrRest: rest.set ('measure', 'yes')
addElem (nt, rest, lev + 1)
else:
p = n.pitch.t # get pitch elements from parsed tokens
if len (p) == 3: acc, step, oct = p
else: step, oct = p