--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/xml2b40.py Mon May 25 10:39:18 2020 +0200
@@ -0,0 +1,189 @@
+#!/usr/bin/env python
+# coding: latin-1
+''' Maakt een .b40 file uit een .xml file (MusicXML).
+muziek = [parts], part = [maten], maat = [voices]
+voice = [[dur, tijd, noot1, noot2, ....]]
+per part: len (voices) = maxvoice
+vceCnt telt het aantal stemmen in het hele stuk (d.w.z. over meerdere parts)
+In deze versie worden eerst alle noten in één part archter elkaar gezet en dan pas worden
+gebonden noten samengevoegd. Dit in verband met het terugzetten van de tijd door backup, waardoor
+eem gebonden noot tussen twee stemmen verloren gaat als voor de backup (naar de andere stem)
+de noot nog een keer voorkomt.
+'''
+import operator, sys, os
+import xml.etree.ElementTree as E
+
+b40List = ['Cbb','Cb','C','C#','C##','-','Dbb','Db','D','D#','D##','-','Ebb','Eb','E','E#','E##','Fbb','Fb','F',
+ 'F#','F##','-','Gbb','Gb','G','G#','G##','-','Abb','Ab','A','A#','A##','-','Bbb','Bb','B','B#','B##']
+b40Dict = dict (zip (b40List, range (len (b40List))))
+
+gtikkenPerKwart = 384 # ticks per quarter note
+gC5 = False # central C == C5 if gC5 else C4
+
+def ntB40 (p, o, alt):
+ if alt:
+ alt = int (alt)
+ while alt > 0: p += '#'; alt -= 1
+ while alt < 0: p += 'b'; alt += 1
+ b40 = o * 40 + b40Dict[p] + 1
+ return str (b40)
+
+def doNote (n):
+ global tijd, maxVoice
+ chord = n.find ('chord') != None
+ p = n.findtext ('pitch/step')
+ alt = n.findtext ('pitch/alter')
+ o = n.findtext ('pitch/octave')
+ r = n.find ('rest')
+ dur = int (n.findtext ('duration'))
+ v = int (n.findtext ('voice')) - 1
+ if v > maxVoice: maxVoice = v
+ if r != None: noot = 'z'
+ else: noot = ntB40 (p, int (o) + (1 if gC5 else 0), alt)
+ tie = n.find ('tie')
+ if tie != None and tie.get ('type') == 'stop': noot = '-' + noot
+ if chord: addChord (v, noot)
+ else: appendNote (v, dur, noot)
+
+def doAttr (e):
+ global durUnit
+ kmaj = ['Cb','Gb','Dd','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#']
+ kmin = ['Ab','Eb','Bb','F', 'C', 'G', 'D','A','E','B','F#','C#','G#','D#','A#']
+ dvstxt = e.findtext ('divisions')
+ if dvstxt: # niet alle attribuut knopen hebben een divisions (b.v. key change)
+ #~ print 'divisions per kwart', int (dvstxt)
+ durUnit = int (dvstxt)
+ key = ''
+ f = e.findtext ('key/fifths')
+ m = e.findtext ('key/mode')
+ if m == 'major': key = kmaj [7 + int (f)]
+ if m == 'minor': key = kmin [7 + int (f)] + 'min'
+ if key:
+ for v in range (nVoices): appendNote (v, 0, 'K:%s\n' % key)
+ beats = e.findtext ('time/beats')
+ if beats:
+ met = beats + '/' + e.findtext ('time/beat-type')
+ for v in range (nVoices): appendNote (v, 0, 'M:%s\n' % met)
+
+def appendNote (v, dur, noot):
+ global tijd
+ t = vtimes [v]
+ if tijd > t: voices [v].append ([tijd - t, t, 'z'])
+ if tijd < t: raise 'kenniet'
+ voices [v].append ([dur, tijd, noot])
+ tijd += int (dur)
+ vtimes[v] = tijd
+
+def addChord (v, noot):
+ t = voices[v][-1]
+ voices[v][-1] = t + [noot]
+
+def addBar ():
+ global voices
+ for v in range (nVoices): appendNote (v, 0, ' |\n')
+ gMaten.append (voices)
+ voices = [[] for i in range (nVoices)]
+
+def outMaat (i, mbuf, ns):
+ for nx in mbuf:
+ dur = nx[0] * gTikkenPerKwart / durUnit
+ tijd = nx[1] * gTikkenPerKwart / durUnit
+ if not dur: continue # alleen echte noten
+ for noot in nx[2:]: # de noot of noten in chord
+ if 'z' in noot: continue # skip rusten
+ ns.append ((tijd, noot, dur))
+
+def remTies (ns):
+ global noten
+ tieBufDur = {} # tijdelijk vasthouden van noten voor evt. volgende tie
+ tieBufTijd = {} # twee aparte buffers voor duur en tijd
+ ns.sort ()
+ for tijd, noot, dur in ns: # nu nog voor gebonden noten checken
+ if noot.startswith ('-'): tieBufDur [noot[1:]] += dur
+ else:
+ if noot in tieBufDur: # bij de volgdende niet gebonden noot vorige uitvoeren
+ noten.append ((tieBufTijd [noot], noot, tieBufDur[noot]))
+ tieBufTijd [noot] = tijd # alle noten vast houden voor evt. volgende tie
+ tieBufDur [noot] = dur
+ for noot, dur in tieBufDur.iteritems (): # alle vastgehouden noten nu uitvoeren
+ noten.append ((tieBufTijd[noot], noot, dur))
+
+def outVoices (vceCnt): # output alle stemmen in een part, nummering begint bij vceCnt
+ global tijd, gMaten, voices, vtimes, tupcnt, maxVoice, noten
+ ns = []
+ for maat in gMaten: # aantal gebruikte voices in gMaten = maxVoice + 1
+ mbuf = reduce (operator.concat, maat[:maxVoice + 1]) # plak de gebruikte voices aanelkaar
+ outMaat (vceCnt, mbuf, ns) # alle stemmen in deze maat => ns, en nummer telkens vanaf vceCnt
+ remTies (ns)
+ vceCnt += maxVoice + 1 # de volgende part start met aansluitend hogere voice nummers
+ gMaten = []
+ voices = [[] for i in range (nVoices)]
+ vtimes = nVoices * [0]
+ tijd = 0
+ tupcnt = 0
+ maxVoice = 0
+ return vceCnt
+
+def xml2b40 (fnmext, tpk=384, C5=1): # base name, ticks per quarter, central C == C5
+ global nVoices, gMaten, voices, vtimes, tijd, tupcnt, maxVoice, vceCnt, noten, gTikkenPerKwart, gC5
+ gTikkenPerKwart = tpk
+ gC5 = C5
+ nVoices = 10
+ gMaten = []
+ voices = [[] for i in range (nVoices)]
+ vtimes = nVoices * [0]
+ tijd = 0
+ tupcnt = 0
+ maxVoice = 0
+ vceCnt = 1
+ noten = []
+
+ e = E.parse (fnmext)
+ parts = e.findall ('part')
+ noten = [] # de uitvoer van alle noten
+ for p in parts:
+ maten = p.findall ('measure')
+ for maat in maten:
+ es = maat.getchildren ()
+ for e in es:
+ if e.tag == 'note': doNote (e)
+ elif e.tag == 'attributes': doAttr (e)
+ elif e.tag == 'backup':
+ dt = int (e.findtext ('duration'))
+ tijd -= dt
+ elif e.tag == 'forward':
+ dt = int (e.findtext ('duration'))
+ tijd += dt
+ else: pass
+ addBar ()
+ vceCnt = outVoices (vceCnt)
+ offnoten = []
+ for tijd, noot, dur in noten:
+ noot = int (noot) # straks numeriek ordenen i.v.m. vergelijken oude b40 files
+ offnoten.append ((tijd, noot))
+ offnoten.append ((tijd + dur, -noot))
+ noten = offnoten
+ noten.sort ()
+ return noten
+
+
+if __name__ == '__main__':
+ from optparse import OptionParser
+ parser = OptionParser (usage='%prog [-h] [-g TPQ] [--C5] <file1>')
+ parser.add_option ("-t", action="store", type="int", help="ticks per quarternote", default=120, metavar='TPQ')
+ parser.add_option ("--C5", action="store_true", help="central C is C5", default=False)
+ parser.add_option ("-w", action="store_true", help="write .b40 file", default=False)
+ options, args = parser.parse_args ()
+ if len (args) < 1: parser.error ('file argument needed')
+ fnmext = args [0]
+ fnm, ext = os.path.splitext (fnmext)
+ if ext != '.xml': parser.error ('.xml file needed file needed')
+ if not os.path.exists (fnmext): parser.error ('%s does not exist' % fnmext)
+ xml2b40 (fnmext, options.t, options.C5)
+ if options.w:
+ g = file (fnm + '.b40', 'w')
+ for tijd, noot in noten: g.write ('%d %s\n' % (tijd, noot))
+ g.close ()
+ print fnm + '.b40 written'
+ else:
+ for tijd, noot in noten: print tijd, noot