xml2b40.py
author wim
Mon, 25 May 2020 10:39:18 +0200
changeset 1 7fd6cac1a69d
child 6 193999e56a90
permissions -rwxr-xr-x
- hard link to segLibB40 added

#!/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