|
1 #!/usr/bin/env python |
|
2 # coding: latin-1 |
|
3 ''' Maakt een .b40 file uit een .xml file (MusicXML). |
|
4 muziek = [parts], part = [maten], maat = [voices] |
|
5 voice = [[dur, tijd, noot1, noot2, ....]] |
|
6 per part: len (voices) = maxvoice |
|
7 vceCnt telt het aantal stemmen in het hele stuk (d.w.z. over meerdere parts) |
|
8 In deze versie worden eerst alle noten in één part archter elkaar gezet en dan pas worden |
|
9 gebonden noten samengevoegd. Dit in verband met het terugzetten van de tijd door backup, waardoor |
|
10 eem gebonden noot tussen twee stemmen verloren gaat als voor de backup (naar de andere stem) |
|
11 de noot nog een keer voorkomt. |
|
12 ''' |
|
13 import operator, sys, os |
|
14 import xml.etree.ElementTree as E |
|
15 |
|
16 b40List = ['Cbb','Cb','C','C#','C##','-','Dbb','Db','D','D#','D##','-','Ebb','Eb','E','E#','E##','Fbb','Fb','F', |
|
17 'F#','F##','-','Gbb','Gb','G','G#','G##','-','Abb','Ab','A','A#','A##','-','Bbb','Bb','B','B#','B##'] |
|
18 b40Dict = dict (zip (b40List, range (len (b40List)))) |
|
19 |
|
20 gtikkenPerKwart = 384 # ticks per quarter note |
|
21 gC5 = False # central C == C5 if gC5 else C4 |
|
22 |
|
23 def ntB40 (p, o, alt): |
|
24 if alt: |
|
25 alt = int (alt) |
|
26 while alt > 0: p += '#'; alt -= 1 |
|
27 while alt < 0: p += 'b'; alt += 1 |
|
28 b40 = o * 40 + b40Dict[p] + 1 |
|
29 return str (b40) |
|
30 |
|
31 def doNote (n): |
|
32 global tijd, maxVoice |
|
33 chord = n.find ('chord') != None |
|
34 p = n.findtext ('pitch/step') |
|
35 alt = n.findtext ('pitch/alter') |
|
36 o = n.findtext ('pitch/octave') |
|
37 r = n.find ('rest') |
|
38 dur = int (n.findtext ('duration')) |
|
39 v = int (n.findtext ('voice')) - 1 |
|
40 if v > maxVoice: maxVoice = v |
|
41 if r != None: noot = 'z' |
|
42 else: noot = ntB40 (p, int (o) + (1 if gC5 else 0), alt) |
|
43 tie = n.find ('tie') |
|
44 if tie != None and tie.get ('type') == 'stop': noot = '-' + noot |
|
45 if chord: addChord (v, noot) |
|
46 else: appendNote (v, dur, noot) |
|
47 |
|
48 def doAttr (e): |
|
49 global durUnit |
|
50 kmaj = ['Cb','Gb','Dd','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#'] |
|
51 kmin = ['Ab','Eb','Bb','F', 'C', 'G', 'D','A','E','B','F#','C#','G#','D#','A#'] |
|
52 dvstxt = e.findtext ('divisions') |
|
53 if dvstxt: # niet alle attribuut knopen hebben een divisions (b.v. key change) |
|
54 #~ print 'divisions per kwart', int (dvstxt) |
|
55 durUnit = int (dvstxt) |
|
56 key = '' |
|
57 f = e.findtext ('key/fifths') |
|
58 m = e.findtext ('key/mode') |
|
59 if m == 'major': key = kmaj [7 + int (f)] |
|
60 if m == 'minor': key = kmin [7 + int (f)] + 'min' |
|
61 if key: |
|
62 for v in range (nVoices): appendNote (v, 0, 'K:%s\n' % key) |
|
63 beats = e.findtext ('time/beats') |
|
64 if beats: |
|
65 met = beats + '/' + e.findtext ('time/beat-type') |
|
66 for v in range (nVoices): appendNote (v, 0, 'M:%s\n' % met) |
|
67 |
|
68 def appendNote (v, dur, noot): |
|
69 global tijd |
|
70 t = vtimes [v] |
|
71 if tijd > t: voices [v].append ([tijd - t, t, 'z']) |
|
72 if tijd < t: raise 'kenniet' |
|
73 voices [v].append ([dur, tijd, noot]) |
|
74 tijd += int (dur) |
|
75 vtimes[v] = tijd |
|
76 |
|
77 def addChord (v, noot): |
|
78 t = voices[v][-1] |
|
79 voices[v][-1] = t + [noot] |
|
80 |
|
81 def addBar (): |
|
82 global voices |
|
83 for v in range (nVoices): appendNote (v, 0, ' |\n') |
|
84 gMaten.append (voices) |
|
85 voices = [[] for i in range (nVoices)] |
|
86 |
|
87 def outMaat (i, mbuf, ns): |
|
88 for nx in mbuf: |
|
89 dur = nx[0] * gTikkenPerKwart / durUnit |
|
90 tijd = nx[1] * gTikkenPerKwart / durUnit |
|
91 if not dur: continue # alleen echte noten |
|
92 for noot in nx[2:]: # de noot of noten in chord |
|
93 if 'z' in noot: continue # skip rusten |
|
94 ns.append ((tijd, noot, dur)) |
|
95 |
|
96 def remTies (ns): |
|
97 global noten |
|
98 tieBufDur = {} # tijdelijk vasthouden van noten voor evt. volgende tie |
|
99 tieBufTijd = {} # twee aparte buffers voor duur en tijd |
|
100 ns.sort () |
|
101 for tijd, noot, dur in ns: # nu nog voor gebonden noten checken |
|
102 if noot.startswith ('-'): tieBufDur [noot[1:]] += dur |
|
103 else: |
|
104 if noot in tieBufDur: # bij de volgdende niet gebonden noot vorige uitvoeren |
|
105 noten.append ((tieBufTijd [noot], noot, tieBufDur[noot])) |
|
106 tieBufTijd [noot] = tijd # alle noten vast houden voor evt. volgende tie |
|
107 tieBufDur [noot] = dur |
|
108 for noot, dur in tieBufDur.iteritems (): # alle vastgehouden noten nu uitvoeren |
|
109 noten.append ((tieBufTijd[noot], noot, dur)) |
|
110 |
|
111 def outVoices (vceCnt): # output alle stemmen in een part, nummering begint bij vceCnt |
|
112 global tijd, gMaten, voices, vtimes, tupcnt, maxVoice, noten |
|
113 ns = [] |
|
114 for maat in gMaten: # aantal gebruikte voices in gMaten = maxVoice + 1 |
|
115 mbuf = reduce (operator.concat, maat[:maxVoice + 1]) # plak de gebruikte voices aanelkaar |
|
116 outMaat (vceCnt, mbuf, ns) # alle stemmen in deze maat => ns, en nummer telkens vanaf vceCnt |
|
117 remTies (ns) |
|
118 vceCnt += maxVoice + 1 # de volgende part start met aansluitend hogere voice nummers |
|
119 gMaten = [] |
|
120 voices = [[] for i in range (nVoices)] |
|
121 vtimes = nVoices * [0] |
|
122 tijd = 0 |
|
123 tupcnt = 0 |
|
124 maxVoice = 0 |
|
125 return vceCnt |
|
126 |
|
127 def xml2b40 (fnmext, tpk=384, C5=1): # base name, ticks per quarter, central C == C5 |
|
128 global nVoices, gMaten, voices, vtimes, tijd, tupcnt, maxVoice, vceCnt, noten, gTikkenPerKwart, gC5 |
|
129 gTikkenPerKwart = tpk |
|
130 gC5 = C5 |
|
131 nVoices = 10 |
|
132 gMaten = [] |
|
133 voices = [[] for i in range (nVoices)] |
|
134 vtimes = nVoices * [0] |
|
135 tijd = 0 |
|
136 tupcnt = 0 |
|
137 maxVoice = 0 |
|
138 vceCnt = 1 |
|
139 noten = [] |
|
140 |
|
141 e = E.parse (fnmext) |
|
142 parts = e.findall ('part') |
|
143 noten = [] # de uitvoer van alle noten |
|
144 for p in parts: |
|
145 maten = p.findall ('measure') |
|
146 for maat in maten: |
|
147 es = maat.getchildren () |
|
148 for e in es: |
|
149 if e.tag == 'note': doNote (e) |
|
150 elif e.tag == 'attributes': doAttr (e) |
|
151 elif e.tag == 'backup': |
|
152 dt = int (e.findtext ('duration')) |
|
153 tijd -= dt |
|
154 elif e.tag == 'forward': |
|
155 dt = int (e.findtext ('duration')) |
|
156 tijd += dt |
|
157 else: pass |
|
158 addBar () |
|
159 vceCnt = outVoices (vceCnt) |
|
160 offnoten = [] |
|
161 for tijd, noot, dur in noten: |
|
162 noot = int (noot) # straks numeriek ordenen i.v.m. vergelijken oude b40 files |
|
163 offnoten.append ((tijd, noot)) |
|
164 offnoten.append ((tijd + dur, -noot)) |
|
165 noten = offnoten |
|
166 noten.sort () |
|
167 return noten |
|
168 |
|
169 |
|
170 if __name__ == '__main__': |
|
171 from optparse import OptionParser |
|
172 parser = OptionParser (usage='%prog [-h] [-g TPQ] [--C5] <file1>') |
|
173 parser.add_option ("-t", action="store", type="int", help="ticks per quarternote", default=120, metavar='TPQ') |
|
174 parser.add_option ("--C5", action="store_true", help="central C is C5", default=False) |
|
175 parser.add_option ("-w", action="store_true", help="write .b40 file", default=False) |
|
176 options, args = parser.parse_args () |
|
177 if len (args) < 1: parser.error ('file argument needed') |
|
178 fnmext = args [0] |
|
179 fnm, ext = os.path.splitext (fnmext) |
|
180 if ext != '.xml': parser.error ('.xml file needed file needed') |
|
181 if not os.path.exists (fnmext): parser.error ('%s does not exist' % fnmext) |
|
182 xml2b40 (fnmext, options.t, options.C5) |
|
183 if options.w: |
|
184 g = file (fnm + '.b40', 'w') |
|
185 for tijd, noot in noten: g.write ('%d %s\n' % (tijd, noot)) |
|
186 g.close () |
|
187 print fnm + '.b40 written' |
|
188 else: |
|
189 for tijd, noot in noten: print tijd, noot |