diff --git "a/xml2abc.py" "b/xml2abc.py" new file mode 100644--- /dev/null +++ "b/xml2abc.py" @@ -0,0 +1,2411 @@ +#!/usr/bin/env python +# coding=latin-1 +""" +Copyright (C) 2012-2018: W.G. Vree +Contributions: M. Tarenskeen, N. Liberg, Paul Villiger, Janus Meuris, Larry Myerscough, +Dick Jackson, Jan Wybren de Jong, Mark Zealey. + +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. . +""" + +try: + import xml.etree.cElementTree as E +except: + import xml.etree.ElementTree as E +import os +import sys +import types +import re +import math + +VERSION = 145 + +python3 = sys.version_info.major > 2 +if python3: + tupletype = tuple + listtype = list + max_int = sys.maxsize +else: + tupletype = types.TupleType + listtype = types.ListType + max_int = sys.maxint + +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!", + # not sure whether this is the right translation + "articulations/staccatissimo": "!wedge!", + # not sure whether this is the right translation + "articulations/spiccato": "!wedge!", + # this may need to be tested to make sure it appears on the right side of the note + "articulations/breath-mark": "!breath!", + "articulations/detached-legato": "!tenuto!.", +} + +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!", +} + +percSvg = """%%beginsvg + + + + + + + + + + + + + + + + + + + + %%endsvg""" + +tabSvg = """%%beginsvg + + + + """ + +kopSvg = '%s\n' +kopSvg2 = '%s\n' + +info_list = [] # diagnostic messages + + +def info(s, warn=1): + x = ("-- " if warn else "") + s + "\n" + info_list.append(x) + if __name__ == "__main__": # only when run from the command line + sys.stderr.write(x) + + +# ------------------- +# data abstractions +# ------------------- + + +class Measure: + def __init__(s, p): + s.reset() + s.ixp = p # part number + s.ixm = 0 # measure number + s.mdur = 0 # measure duration (nominal metre value in divisions) + s.divs = 0 # number of divisions per 1/4 + s.mtr = 4, 4 # meter + + def reset(s): # reset each measure + s.attr = "" # measure signatures, tempo + s.lline = ( + "" # left barline, but only holds ':' at start of repeat, otherwise empty + ) + s.rline = "|" # right barline + s.lnum = "" # (left) volta number + + +class Note: + def __init__(s, dur=0, n=None): + s.tijd = 0 # the time in XML division units + s.dur = dur # duration of a note in XML divisions + s.fact = None # time modification for tuplet notes (num, div) + s.tup = [""] # start(s) and/or stop(s) of tuplet + s.tupabc = "" # abc tuplet string to issue before note + s.beam = 0 # 1 = beamed + s.grace = 0 # 1 = grace note + s.before = [] # abc string that goes before the note/chord + s.after = "" # the same after the note/chord + s.ns = n and [n] or [] # notes in the chord + s.lyrs = {} # {number -> syllabe} + s.tab = None # (string number, fret number) + s.ntdec = "" # !string!, !courtesy! + + +class Elem: + def __init__(s, string): + s.tijd = 0 # the time in XML division units + s.str = string # any abc string that is not a note + + +class Counter: + def inc(s, key, voice): + s.counters[key][voice] = s.counters[key].get(voice, 0) + 1 + + def clear(s, vnums): # reset all counters + tups = list(zip(vnums.keys(), len(vnums) * [0])) + s.counters = {"note": dict(tups), "nopr": dict(tups), "nopt": dict(tups)} + + def getv(s, key, voice): + return s.counters[key][voice] + + def prcnt(s, ip): # print summary of all non zero counters + for iv in s.counters["note"]: + if s.getv("nopr", iv) != 0: + info( + "part %d, voice %d has %d skipped non printable notes" + % (ip, iv, s.getv("nopr", iv)) + ) + if s.getv("nopt", iv) != 0: + info( + "part %d, voice %d has %d notes without pitch" + % (ip, iv, s.getv("nopt", iv)) + ) + if s.getv("note", iv) == 0: # no real notes counted in this voice + info("part %d, skipped empty voice %d" % (ip, iv)) + + +class Music: + def __init__(s, options): + s.tijd = 0 # the current time + s.maxtime = 0 # maximum time in a measure + s.gMaten = [] # [voices,.. for all measures in a part] + # [{num: (abc_lyric_string, melis)},.. for all measures in a part] + s.gLyrics = [] + # all used voice id's in a part (xml voice id's == numbers) + s.vnums = {} + s.cnt = Counter() # global counter object + s.vceCnt = 1 # the global voice count over all parts + s.lastnote = None # the last real note record inserted in s.voices + s.bpl = options.b # the max number of bars per line when writing abc + s.cpl = options.n # the number of chars per line when writing abc + s.repbra = 0 # true if volta is used somewhere + s.nvlt = options.v # no volta on higher voice numbers + s.jscript = options.j # compatibility with javascript version + + def initVoices(s, newPart=0): + s.vtimes, s.voices, s.lyrics = {}, {}, {} + for v in s.vnums: + # {voice: the end time of the last item in each voice} + s.vtimes[v] = 0 + s.voices[v] = [] # {voice: [Note|Elem, ..]} + s.lyrics[v] = [] # {voice: [{num: syl}, ..]} + if newPart: + s.cnt.clear(s.vnums) # clear counters once per part + + def incTime(s, dt): + s.tijd += dt + if s.tijd < 0: + s.tijd = 0 # erroneous element + if s.tijd > s.maxtime: + s.maxtime = s.tijd + + def appendElemCv(s, voices, elem): + for v in voices: + s.appendElem(v, elem) # insert element in all voices + + def insertElem(s, v, elem): # insert at the start of voice v in the current measure + obj = Elem(elem) + obj.tijd = 0 # because voice is sorted later + s.voices[v].insert(0, obj) + + def appendObj(s, v, obj, dur): + obj.tijd = s.tijd + s.voices[v].append(obj) + s.incTime(dur) + if s.tijd > s.vtimes[v]: + s.vtimes[v] = s.tijd # don't update for inserted earlier items + + def appendElem(s, v, elem, tel=0): + s.appendObj(v, Elem(elem), 0) + if tel: + # count number of certain elements in each voice (in addition to notes) + s.cnt.inc("note", v) + + def appendElemT(s, v, elem, tijd): # insert element at specified time + obj = Elem(elem) + obj.tijd = tijd + s.voices[v].append(obj) + + def appendNote(s, v, note, noot): + note.ns.append(note.ntdec + noot) + s.appendObj(v, note, int(note.dur)) + # remember last note/rest for later modifications (chord, grace) + s.lastnote = note + if noot != "z" and noot != "x": # real notes and grace notes + s.cnt.inc("note", v) # count number of real notes in each voice + if not note.grace: # for every real note + s.lyrics[v].append(note.lyrs) # even when it has no lyrics + + def getLastRec(s, voice): + if s.gMaten: + # the last record in the last measure + return s.gMaten[-1][voice][-1] + return None # no previous records in the first measure + + def getLastMelis(s, voice, num): # get melisma of last measure + if s.gLyrics: + # the previous lyrics dict in this voice + lyrdict = s.gLyrics[-1][voice] + if num in lyrdict: + # lyrdict = num -> (lyric string, melisma) + return lyrdict[num][1] + return 0 # no previous lyrics in voice or line number + + def addChord( + s, note, noot + ): # careful: we assume that chord notes follow immediately + for d in note.before: # put all decorations before chord + if d not in s.lastnote.before: + s.lastnote.before += [d] + s.lastnote.ns.append(note.ntdec + noot) + + def addBar(s, lbrk, m): # linebreak, measure data + if m.mdur and s.maxtime > m.mdur: + info("measure %d in part %d longer than metre" % (m.ixm + 1, m.ixp + 1)) + s.tijd = s.maxtime # the time of the bar lines inserted here + for v in s.vnums: + if m.lline or m.lnum: # if left barline or left volta number + p = s.getLastRec(v) # get the previous barline record + if p: # in measure 1 no previous measure is available + x = p.str # p.str is the ABC barline string + if m.lline: # append begin of repeat, m.lline == ':' + x = (x + m.lline).replace(":|:", "::").replace("||", "|") + if s.nvlt == 3: # add volta number only to lowest voice in part 0 + if m.ixp + v == min(s.vnums): + x += m.lnum + elif m.lnum: # new behaviour with I:repbra 0 + # add volta number(s) or text to all voices + x += m.lnum + s.repbra = 1 # signal occurrence of a volta + p.str = x # modify previous right barline + elif m.lline: # begin of new part and left repeat bar is required + s.insertElem(v, "|:") + if lbrk: + p = s.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 + s.insertElem(v, "%s" % m.attr) + # insert current barline record at time maxtime + s.appendElem(v, " %s" % m.rline) + # make all times consistent + s.voices[v] = sortMeasure(s.voices[v], m) + lyrs = s.lyrics[v] # [{number: sylabe}, .. for all notes] + # {number: (abc_lyric_string, melis)} for this voice + lyrdict = {} + # the lyrics numbers in this measure + nums = [num for d in lyrs for num in d.keys()] + # the highest lyrics number in this measure + maxNums = max(nums + [0]) + for i in range(maxNums, 0, -1): + # collect the syllabi with number i + xs = [syldict.get(i, "") for syldict in lyrs] + melis = s.getLastMelis(v, i) # get melisma from last measure + lyrdict[i] = abcLyr(xs, melis) + # {number: (abc_lyric_string, melis)} for this measure + s.lyrics[v] = lyrdict + mkBroken(s.voices[v]) + s.gMaten.append(s.voices) + s.gLyrics.append(s.lyrics) + s.tijd = s.maxtime = 0 + s.initVoices() + + def outVoices(s, divs, ip, isSib): # output all voices of part ip + # xml voice number -> abc voice number (one part) + vvmap = {} + vnum_keys = list(s.vnums.keys()) + if s.jscript or isSib: + vnum_keys.sort() + lvc = min(vnum_keys or [1]) # lowest xml voice number of this part + for iv in vnum_keys: + if s.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: + # compute the best unit length for this voice + unitL = compUnitLength(iv, s.gMaten, divs) + abcOut.cmpL.append(unitL) # remember for header output + vn, vl = ( + [], + {}, + ) # for voice iv: collect all notes to vn and all lyric lines to vl + for im in range(len(s.gMaten)): + measure = s.gMaten[im][iv] + vn.append(outVoice(measure, divs[im], im, ip, unitL)) + checkMelismas(s.gLyrics, s.gMaten, im, iv) + for n, (lyrstr, melis) in s.gLyrics[im][iv].items(): + if n in vl: + while len(vl[n]) < im: + vl[n].append("") # fill in skipped measures + vl[n].append(lyrstr) + else: + vl[n] = im * [""] + [lyrstr] # must skip im measures + for ( + n, + lyrs, + ) in vl.items(): # fill up possibly empty lyric measures at the end + mis = len(vn) - len(lyrs) + lyrs += mis * [""] + abcOut.add("V:%d" % s.vceCnt) + if s.repbra: + if s.nvlt == 1 and s.vceCnt > 1: + abcOut.add("I:repbra 0") # only volta on first voice + if s.nvlt == 2 and iv > lvc: + # only volta on first voice of each part + abcOut.add("I:repbra 0") + if s.cpl > 0: + # option -n (max chars per line) overrules -b (max bars per line) + s.bpl = 0 + elif s.bpl == 0: + s.cpl = 100 # the default: 100 chars per line + bn = 0 # count bars + while vn: # while still measures available + ib = 1 + chunk = vn[0] + while ib < len(vn): + if s.cpl > 0 and len(chunk) + len(vn[ib]) >= s.cpl: + break # line full (number of chars) + if s.bpl > 0 and ib >= s.bpl: + # line full (number of bars) + break + chunk += vn[ib] + ib += 1 + bn += ib + abcOut.add(chunk + " %%%d" % bn) # line with barnumer + del vn[:ib] # chop ib bars + # order the numbered lyric lines for output + lyrlines = sorted(vl.items()) + for n, lyrs in lyrlines: + abcOut.add("w: " + "|".join(lyrs[:ib]) + "|") + del lyrs[:ib] + vvmap[iv] = s.vceCnt # xml voice number -> abc voice number + s.vceCnt += 1 # count voices over all parts + s.gMaten = [] # reset the follwing instance vars for each part + s.gLyrics = [] + # print summary of skipped items in this part + s.cnt.prcnt(ip + 1) + return vvmap + + +class ABCoutput: + pagekeys = ( + "scale,pageheight,pagewidth,leftmargin,rightmargin,topmargin,botmargin".split( + "," + ) + ) + + def __init__(s, fnmext, pad, X, options): + s.fnmext = fnmext + s.outlist = [] # list of ABC strings + s.title = "T:Title" + s.key = "none" + s.clefs = {} # clefs for all abc-voices + s.mtr = "none" + s.tempo = 0 # 0 -> no tempo field + s.tempo_units = (1, 4) # note type of tempo direction + s.pad = pad # the output path or none + s.X = X + 1 # the abc tune number + # denominator of the unit length (L:) from -d option + s.denL = options.d + # 0 -> no %%MIDI, 1 -> only program, 2 -> all %%MIDI + s.volpan = int(options.m) + s.cmpL = [] # computed optimal unit length for all voices + s.jscript = options.j # compatibility with javascript version + s.tstep = options.t # translate percmap to voicemap + s.stemless = 0 # use U:s=!stemless! + s.shiftStem = options.s # shift note heads 3 units left + s.dojef = 0 # => s.tstep in mkHeader + if pad: + _, base_name = os.path.split(fnmext) + s.outfile = open(os.path.join(pad, base_name), "w") + else: + s.outfile = sys.stdout + if s.jscript: + s.X = 1 # always X:1 in javascript version + s.pageFmt = {} + for k in s.pagekeys: + s.pageFmt[k] = None + if len(options.p) == 7: + for k, v in zip(s.pagekeys, options.p): + try: + s.pageFmt[k] = float(v) + except: + info("illegal float %s for %s", (k, v)) + continue + + def add(s, str): + s.outlist.append(str + "\n") # collect all ABC output + + # stfmap = [parts], part = [staves], stave = [voices] + def mkHeader(s, stfmap, partlist, midimap, vmpdct, koppen): + accVce, accStf, staffs = [], [], stfmap[:] # staffs is consumed + for x in partlist: # collect partnames into accVce and staff groups into accStf + try: + prgroupelem(x, ("", ""), "", stfmap, accVce, accStf) + except: + info("lousy musicxml: error in part-list") + staves = " ".join(accStf) + clfnms = {} + for part, (partname, partabbrv) in zip(staffs, accVce): + if not part: + continue # skip empty part + firstVoice = part[0][0] # the first voice number in this part + nm = partname.replace("\n", "\\n").replace(".:", ".").strip(":") + snm = partabbrv.replace("\n", "\\n").replace(".:", ".").strip(":") + clfnms[firstVoice] = (nm and 'nm="%s"' % nm or "") + ( + snm and ' snm="%s"' % snm or "" + ) + hd = ["X:%d\n%s\n" % (s.X, s.title)] + for i, k in enumerate(s.pagekeys): + if s.jscript and k in ["pageheight", "topmargin", "botmargin"]: + continue + if s.pageFmt[k] != None: + hd.append("%%%%%s %.2f%s\n" % (k, s.pageFmt[k], i > 0 and "cm" or "")) + if staves and len(accStf) > 1: + hd.append("%%score " + staves + "\n") + # default no tempo field + tempo = ( + s.tempo + and "Q:%d/%d=%s\n" % (s.tempo_units[0], s.tempo_units[1], s.tempo) + or "" + ) + d = {} # determine the most frequently occurring unit length over all voices + for x in s.cmpL: + d[x] = d.get(x, 0) + 1 + if s.jscript: + # when tie (1) sort on key (0) + defLs = sorted(d.items(), key=lambda x: (-x[1], x[0])) + else: + defLs = sorted(d.items(), key=lambda x: -x[1]) + # override default unit length with -d option + defL = s.denL and s.denL or defLs[0][0] + hd.append("L:1/%d\n%sM:%s\n" % (defL, tempo, s.mtr)) + hd.append("I:linebreak $\nK:%s\n" % s.key) + if s.stemless: + hd.append("U:s=!stemless!\n") + vxs = sorted(vmpdct.keys()) + for vx in vxs: + hd.extend(vmpdct[vx]) + s.dojef = 0 # translate percmap to voicemap + for vnum, clef in s.clefs.items(): + ch, prg, vol, pan = midimap[vnum - 1][:4] + # map of abc percussion notes to midi notes + dmap = midimap[vnum - 1][4:] + if dmap and "perc" not in clef: + clef = (clef + " map=perc").strip() + hd.append("V:%d %s %s\n" % (vnum, clef, clfnms.get(vnum, ""))) + if vnum in vmpdct: + hd.append("%%%%voicemap tab%d\n" % vnum) + hd.append( + "K:none\nM:none\n%%clef none\n%%staffscale 1.6\n%%flatbeams true\n%%stemdir down\n" + ) + if "perc" in clef: + hd.append("K:none\n") # no key for a perc voice + if ( + s.volpan > 1 + ): # option -m 2 -> output all recognized midi commands when needed and present in xml + if ch > 0 and ch != vnum: + hd.append("%%%%MIDI channel %d\n" % ch) + if prg > 0: + hd.append("%%%%MIDI program %d\n" % (prg - 1)) + if vol >= 0: + # volume == 0 is possible ... + hd.append("%%%%MIDI control 7 %.0f\n" % vol) + if pan >= 0: + hd.append("%%%%MIDI control 10 %.0f\n" % pan) + elif ( + s.volpan > 0 + ): # default -> only output midi program command when present in xml + if dmap and ch > 0: + # also channel if percussion part + hd.append("%%%%MIDI channel %d\n" % ch) + if prg > 0: + hd.append("%%%%MIDI program %d\n" % (prg - 1)) + for abcNote, step, midiNote, notehead in dmap: + if not notehead: + notehead = "normal" + if abcMid(abcNote) != midiNote or abcNote != step: + if s.volpan > 0: + hd.append("%%%%MIDI drummap %s %s\n" % (abcNote, midiNote)) + hd.append( + "I:percmap %s %s %s %s\n" % (abcNote, step, midiNote, notehead) + ) + s.dojef = s.tstep # == options.t + if ( + defL != s.cmpL[vnum - 1] + ): # only if computed unit length different from header + hd.append("L:1/%d\n" % s.cmpL[vnum - 1]) + s.outlist = hd + s.outlist + if koppen: # output SVG stuff needed for tablature + # shift note heads 3 units left + k1 = kopSvg.replace("-2", "-5") if s.shiftStem else kopSvg + k2 = kopSvg2.replace("-2", "-5") if s.shiftStem else kopSvg2 + tb = tabSvg.replace("-3", "-6") if s.shiftStem else tabSvg + # javascript compatibility + ks = sorted(koppen.keys()) + ks = [k2 % (k, k) if len(k) == 2 else k1 % (k, k) for k in ks] + # javascript compatibility + tbs = map(lambda x: x.strip() + "\n", tb.splitlines()) + s.outlist = tbs + ks + ["\n%%endsvg\n"] + s.outlist + + def getABC(s): + str = "".join(s.outlist) + if s.dojef: + str = perc2map(str) + return str + + def writeall(s): # determine the required encoding of the entire ABC output + str = s.getABC() + if python3: + s.outfile.write(str) + else: + s.outfile.write(str.encode("utf-8")) + if s.pad: + s.outfile.close() # close each file with -o option + else: + # add empty line between tunes on stdout + s.outfile.write("\n") + # info('%s written with %d voices' % (s.fnmext, len(s.clefs)), warn=0) + + +# ---------------- +# functions +# ---------------- + + +def abcLyr(xs, melis): # Convert list xs to abc lyrics. + if not "".join(xs): + return "", 0 # there is no lyrics in this measure + res = [] + for x in xs: # 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 + elif x.endswith("_") and not x.endswith("\_"): # 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.append(x) + return (" ".join(res), melis) + + +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 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 + num, den = simplify(uL * nx.dur, divs * 4) # L=1/8 -> uL = 8 units + if nx.fact: # apply tuplet time modification + numfac, denfac = nx.fact + num, den = simplify(num * numfac, den * denfac) + if den > 64: # limit the denominator to a maximum of 64 + x = float(num) / den + n = math.floor(x) # when just above an integer n + if x - n < 0.1 * x: + num, den = n, 1 # round to n + num64 = 64.0 * num / den + 1.0e-15 # to get Python2 behaviour of round + num, den = simplify(int(round(num64)), 64) + if num == 1: + if den == 1: + dabc = "" + elif den == 2: + dabc = "/" + else: + dabc = "/%d" % den + elif den == 1: + dabc = "%d" % num + else: + dabc = "%d/%d" % (num, den) + return dabc + + +def abcMid(note): # abc note -> midi pitch + r = re.search(r"([_^]*)([A-Ga-g])([',]*)", note) + if not r: + return -1 + acc, n, oct = r.groups() + nUp = n.upper() + p = 60 + [0, 2, 4, 5, 7, 9, 11]["CDEFGAB".index(nUp)] + (12 if nUp != n else 0) + if acc: + p += (1 if acc[0] == "^" else -1) * len(acc) + if oct: + p += (12 if oct[0] == "'" else -12) * len(oct) + return p + + +def staffStep(ptc, o, clef, tstep): + ndif = 0 + if "stafflines=1" in clef: + ndif += 4 # meaning of one line: E (xml) -> B (abc) + if not tstep and clef.startswith("bass"): + ndif += 12 # transpose bass -> treble (C3 -> A4) + if ndif: # diatonic transposition == addition modulo 7 + nm7 = "C,D,E,F,G,A,B".split(",") + n = nm7.index(ptc) + ndif + ptc, o = nm7[n % 7], o + n // 7 + if o > 4: + ptc = ptc.lower() + if o > 5: + ptc = ptc + (o - 5) * "'" + if o < 4: + ptc = ptc + (4 - o) * "," + return ptc + + +def setKey(fifths, mode): + 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.lower()[:3] # only first three chars, no case + key = sharpness[offTab[mode] + fifths] + (mode if offTab[mode] != 8 else "") + accs = ["F", "C", "G", "D", "A", "E", "B"] + if fifths >= 0: + msralts = dict(zip(accs[:fifths], fifths * [1])) + else: + msralts = dict(zip(accs[fifths:], -fifths * [-1])) + return key, msralts + + +def insTup(ix, notes, fact): # read one nested tuplet + tupcnt = 0 + nx = notes[ix] + if "start" in nx.tup: + nx.tup.remove("start") # do recursive calls when starts remain + tix = ix # index of first tuplet note + fn, fd = fact # xml time-mod of the higher level + fnum, fden = nx.fact # xml time-mod of the current level + tupfact = fnum // fn, fden // fd # abc time mod of this level + while ix < len(notes): + lastix = ix - 1 + nx = notes[ix] + if isinstance(nx, Elem) or nx.grace: + ix += 1 # skip all non tuplet elements + continue + if "start" in nx.tup: # more nested tuplets to start + # ix is on the stop note! + ix, tupcntR = insTup(ix, notes, tupfact) + tupcnt += tupcntR + elif nx.fact: + tupcnt += 1 # count tuplet elements + if "stop" in nx.tup: + nx.tup.remove("stop") + break + if not nx.fact: # stop on first non tuplet note + ix = lastix # back to last tuplet note + break + ix += 1 + # put abc tuplet notation before the recursive ones + tup = (tupfact[0], tupfact[1], tupcnt) + if tup == (3, 2, 3): + tupPrefix = "(3" + else: + tupPrefix = "(%d:%d:%d" % tup + notes[tix].tupabc = tupPrefix + notes[tix].tupabc + return ix, tupcnt # ix is on the last tuplet note + + +def mkBroken(vs): # introduce broken rhythms (vs: one voice, one measure) + vs = [n for n in vs if isinstance(n, Note)] + i = 0 + while i < len(vs) - 1: + n1, n2 = vs[i], vs[i + 1] # scan all adjacent pairs + # skip if note in tuplet or has no duration or outside beam + if not n1.fact and not n2.fact and n1.dur > 0 and n2.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 + elif 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 + + +def outVoice( + measure, divs, im, ip, unitL +): # note/elem objects of one measure in one voice + ix = 0 + while ix < len(measure): # set all (nested) tuplet annotations + nx = measure[ix] + if isinstance(nx, Note) and nx.fact and not nx.grace: + # read one tuplet, insert annotation(s) + ix, tupcnt = insTup(ix, measure, (1, 1)) + ix += 1 + vs = [] + for nx in measure: + if isinstance(nx, Note): + # xml -> abc duration string + durstr = abcdur(nx, divs, unitL) + chord = len(nx.ns) > 1 + cns = [nt[:-1] for nt in nx.ns if nt.endswith("-")] + tie = "" + if chord and len(cns) == len(nx.ns): # all chord notes tied + nx.ns = cns # chord notes without tie + tie = "-" # one tie for whole chord + s = nx.tupabc + "".join(nx.before) + if chord: + s += "[" + for nt in nx.ns: + s += nt + if chord: + s += "]" + tie + if s.endswith("-"): + s, tie = s[:-1], "-" # split off tie + s += durstr + tie # and put it back again + s += nx.after + nospace = nx.beam + else: + if isinstance(nx.str, listtype): + nx.str = nx.str[0] + s = nx.str + nospace = 1 + if nospace: + vs.append(s) + else: + vs.append(" " + s) + vs = "".join(vs) # ad hoc: remove multiple pedal directions + while vs.find("!ped!!ped!") >= 0: + vs = vs.replace("!ped!!ped!", "!ped!") + while vs.find("!ped-up!!ped-up!") >= 0: + vs = vs.replace("!ped-up!!ped-up!", "!ped-up!") + while vs.find("!8va(!!8va)!") >= 0: + vs = vs.replace("!8va(!!8va)!", "") # remove empty ottava's + return vs + + +def sortMeasure(voice, m): + voice.sort(key=lambda o: o.tijd) # sort on time + time = 0 + v = [] + rs = [] # holds rests in between notes + for i, nx in enumerate(voice): # establish sequentiality + if nx.tijd > time and chkbug(nx.tijd - time, m): + # fill hole with invisble rest + v.append(Note(nx.tijd - time, "x")) + rs.append(len(v) - 1) + if isinstance(nx, Elem): + if nx.tijd < time: + nx.tijd = time # shift elems without duration to where they fit + v.append(nx) + time = nx.tijd + continue + if nx.tijd < time: # overlapping element + if nx.ns[0] == "z": + continue # discard overlapping rest + if v[-1].tijd <= nx.tijd: # we can do something + if v[-1].ns[0] == "z": # shorten rest + v[-1].dur = nx.tijd - v[-1].tijd + if v[-1].dur == 0: + del v[-1] # nothing left + info( + "overlap in part %d, measure %d: rest shortened" + % (m.ixp + 1, m.ixm + 1) + ) + else: # make a chord of overlap + v[-1].ns += nx.ns + info( + "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 + info( + "overlapping notes in one voice! part %d, measure %d, note %s discarded" + % (m.ixp + 1, m.ixm + 1, isinstance(nx, Note) and nx.ns or nx.str) + ) + continue + v.append(nx) + if isinstance(nx, Note): + if nx.ns[0] in "zx": + rs.append(len(v) - 1) # remember rests between notes + elif len(rs): + if nx.beam and not nx.grace: # copy beam into rests + for j in rs: + v[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 -> s.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: + info( + "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 + + +def getPartlist(ps): # correct part-list (from buggy xml-software) + xs = [] # the corrected part-list + e = [] # stack of opened part-groups + for x in list(ps): # insert missing stops, delete double starts + if x.tag == "part-group": + num, type = x.get("number"), x.get("type") + if type == "start": + if num in e: # missing stop: insert one + xs.append(E.Element("part-group", number=num, type="stop")) + xs.append(x) + else: # normal start + xs.append(x) + e.append(num) + else: + if num in e: # normal stop + e.remove(num) + xs.append(x) + else: + pass # double stop: skip it + else: + xs.append(x) + for num in reversed(e): # fill missing stops at the end + xs.append(E.Element("part-group", number=num, type="stop")) + return xs + + +def parseParts(xs, d, e): # -> [elems on current level], rest of xs + if not xs: + return [], [] + x = xs.pop(0) + if x.tag == "part-group": + num, type = x.get("number"), x.get("type") + if type == "start": # go one level deeper + s = [ + x.findtext(n, "") + for n in [ + "group-symbol", + "group-barline", + "group-name", + "group-abbreviation", + ] + ] + d[num] = s # remember groupdata by group number + e.append(num) # make stack of open group numbers + # parse one level deeper to next stop + elemsnext, rest1 = parseParts(xs, d, e) + # parse the rest on this level + elems, rest2 = parseParts(rest1, d, e) + return [elemsnext] + elems, rest2 + else: # stop: close level and return group-data + nums = e.pop() # last open group number in stack order + if xs and xs[0].get("type") == "stop": # two consequetive stops + # in the wrong order (tempory solution) + if num != nums: + # exchange values (only works for two stops!!!) + d[nums], d[num] = d[num], d[nums] + # retrieve an return groupdata as last element of the group + sym = d[num] + return [sym], xs + else: + # parse remaining elements on current level + elems, rest = parseParts(xs, d, e) + name = x.findtext("part-name", ""), x.findtext("part-abbreviation", "") + return [name] + elems, rest + + +def bracePart(part): # put a brace on multistaff part and group voices + if not part: + return [] # empty part in the score + brace = [] + for ivs in part: + if len(ivs) == 1: # stave with one voice + brace.append("%s" % ivs[0]) + else: # stave with multiple voices + brace += ["("] + ["%s" % iv for iv in ivs] + [")"] + brace.append("|") + del brace[-1] # no barline at the end + if len(part) > 1: + brace = ["{"] + brace + ["}"] + return brace + + +# collect partnames (accVce) and %%score map (accStf) +def prgroupelem(x, gnm, bar, pmap, accVce, accStf): + if type(x) == tupletype: # partname-tuple = (part-name, part-abbrev) + y = pmap.pop(0) + if gnm[0]: + # put group-name before part-name + x = [n1 + ":" + n2 for n1, n2 in zip(gnm, x)] + accVce.append(x) + accStf.extend(bracePart(y)) + # misuse of group just to add extra name to stave + elif len(x) == 2 and type(x[0]) == tupletype: + y = pmap.pop(0) + # x[0] = partname-tuple, x[1][2:] = groupname-tuple + nms = [n1 + ":" + n2 for n1, n2 in zip(x[0], x[1][2:])] + accVce.append(nms) + accStf.extend(bracePart(y)) + else: + prgrouplist(x, bar, pmap, accVce, accStf) + + +# collect partnames, scoremap for a part-group +def prgrouplist(x, pbar, pmap, accVce, accStf): + # bracket symbol, continue barline, group-name-tuple + sym, bar, gnm, gabbr = x[-1] + bar = bar == "yes" or pbar # pbar -> the parent has bar + accStf.append(sym == "brace" and "{" or "[") + for z in x[:-1]: + prgroupelem(z, (gnm, gabbr), bar, pmap, accVce, accStf) + if bar: + accStf.append("|") + if bar: + del accStf[-1] # remove last one before close + accStf.append(sym == "brace" and "}" or "]") + + +def compUnitLength(iv, maten, divs): # compute optimal unit length + uLmin, minLen = 0, max_int + for uL in [4, 8, 16]: # try 1/4, 1/8 and 1/16 + vLen = 0 # total length of abc duration strings in this voice + for im, m in enumerate(maten): # all measures + for e in m[iv]: # all notes in voice iv + if isinstance(e, Elem) or e.dur == 0: + continue # no real durations + # add len of duration string + vLen += len(abcdur(e, divs[im], uL)) + if vLen < minLen: + uLmin, minLen = uL, vLen # remember the smallest + return uLmin + + +def doSyllable(syl): + txt = "" + for e in syl: + if e.tag == "elision": + txt += "~" + elif e.tag == "text": # escape - and space characters + txt += ( + (e.text or "").replace("_", "\_").replace("-", r"\-").replace(" ", "~") + ) + if not txt: + return txt + if syl.findtext("syllabic") in ["begin", "middle"]: + txt += "-" + if syl.find("extend") is not None: + txt += "_" + return txt + + +def checkMelismas(lyrics, maten, im, iv): + if im == 0: + return + maat = maten[im][iv] # notes of the current measure + curlyr = lyrics[im][iv] # lyrics dict of current measure + prvlyr = lyrics[im - 1][iv] # lyrics dict of previous measure + for n, ( + lyrstr, + melis, + ) in prvlyr.items(): # all lyric numbers in the previous measure + if ( + n not in curlyr and melis + ): # melisma required, but no lyrics present -> make one! + ms = getMelisma(maat) # get a melisma for the current measure + if ms: + # set melisma as the n-th lyrics of the current measure + curlyr[n] = (ms, 0) + + +def getMelisma(maat): # get melisma from notes in maat + ms = [] + for note in maat: # every note should get an underscore + if not isinstance(note, Note): + continue # skip Elem's + if note.grace: + continue # skip grace notes + if note.ns[0] in "zx": + break # stop on first rest + ms.append("_") + return " ".join(ms) + + +def perc2map(abcIn): + fillmap = {"diamond": 1, "triangle": 1, "square": 1, "normal": 1} + abc = map(lambda x: x.strip(), percSvg.splitlines()) + id = "default" + maps = {"default": []} + dmaps = {"default": []} + r1 = re.compile(r"V:\s*(\S+)") + ls = abcIn.splitlines() + for x in ls: + if "I:percmap" in x: + noot, step, midi, kop = map(lambda x: x.strip(), x.split()[1:]) + if kop in fillmap: + kop = kop + "+" + "," + kop + x = "%%%%map perc%s %s print=%s midi=%s heads=%s" % ( + id, + noot, + step, + midi, + kop, + ) + maps[id].append(x) + if "%%MIDI" in x: + dmaps[id].append(x) + if "V:" in x: + r = r1.match(x) + if r: + id = r.group(1) + if id not in maps: + maps[id] = [] + dmaps[id] = [] + ids = sorted(maps.keys()) + for id in ids: + abc += maps[id] + id = "default" + for x in ls: + if "I:percmap" in x: + continue + if "%%MIDI" in x: + continue + if "V:" in x or "K:" in x: + r = r1.match(x) + if r: + id = r.group(1) + abc.append(x) + if id in dmaps and len(dmaps[id]) > 0: + abc.extend(dmaps[id]) + del dmaps[id] + if "perc" in x and "map=" not in x: + x += " map=perc" + if "map=perc" in x and len(maps[id]) > 0: + abc.append("%%voicemap perc" + id) + if "map=off" in x: + abc.append("%%voicemap") + else: + abc.append(x) + return "\n".join(abc) + "\n" + + +def addoct(ptc, o): # xml staff step, xml octave number + p = ptc + if o > 4: + p = ptc.lower() + if o > 5: + p = p + (o - 5) * "'" + if o < 4: + p = p + (4 - o) * "," + return p # abc pitch == abc note without accidental + + +def chkbug(dt, m): + if dt > m.divs / 16: + return 1 # duration should be > 1/64 note + info( + "MuseScore bug: incorrect duration, smaller then 1/64! in measure %d, part %d" + % (m.ixm, m.ixp) + ) + return 0 + + +# ---------------- +# parser +# ---------------- + + +class Parser: + note_alts = [ # 3 alternative notations of the same note for tablature mapping + [ + x.strip() + for x in "=C, ^C, =D, ^D, =E, =F, ^F, =G, ^G, =A, ^A, =B".split(",") + ], + [ + x.strip() + for x in "^B, _D,^^C, _E, _F, ^E, _G,^^F, _A,^^G, _B, _C".split(",") + ], + [ + x.strip() + for x in "__D,^^B,__E,__F,^^D,__G,^^E,__A,_/A,__B,__C,^^A".split(",") + ], + ] + step_map = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11} + + def __init__(s, options): + # unfold repeats, number of chars per line, credit filter level, volta option + s.slurBuf = {} # dict of open slurs keyed by slur number + # {direction-type + number -> (type, voice | time)} dict for proper closing + s.dirStk = {} + s.ingrace = 0 # marks a sequence of grace notes + s.msc = Music(options) # global music data abstraction + s.unfold = options.u # turn unfolding repeats on + s.ctf = options.c # credit text filter level + s.gStfMap = [] # [[abc voice numbers] for all parts] + s.midiMap = [] # midi-settings for each abc voice, in order + s.drumInst = {} # inst_id -> midi pitch for channel 10 notes + s.drumNotes = {} # (xml voice, abc note) -> (midi note, note head) + s.instMid = [] # [{inst id -> midi-settings} for all parts] + # default midi settings for channel, program, volume, panning + s.midDflt = [-1, -1, -1, -91] + # xml-notenames (without octave) with accidentals from the key + s.msralts = {} + # abc-notenames (with voice number) with passing accidentals + s.curalts = {} + s.stfMap = {} # xml staff number -> [xml voice number] + s.vce2stf = {} # xml voice number -> allocated staff number + s.clefMap = {} # xml staff number -> abc clef (for header only) + s.curClef = {} # xml staff number -> current abc clef + s.stemDir = {} # xml voice number -> current stem direction + s.clefOct = {} # xml staff number -> current clef-octave-change + s.curStf = {} # xml voice number -> current xml staff number + s.nolbrk = options.x # generate no linebreaks ($) + s.jscript = options.j # compatibility with javascript version + s.ornaments = sorted(note_ornamentation_map.items()) + s.doPageFmt = len(options.p) == 1 # translate xml page format + s.tstep = options.t # clef determines step on staff (percussion) + s.dirtov1 = options.v1 # all directions to first voice of staff + s.ped = options.ped # render pedal directions + s.wstems = options.stm # translate stem elements + s.pedVce = None # voice for pedal directions + s.repeat_str = {} # staff number -> [measure number, repeat-text] + s.tabVceMap = {} # abc voice num -> [%%map ...] for tab voices + s.koppen = {} # noteheads needed for %%map + + # match slur number n in voice v2, add abc code to before/after + def matchSlur(s, type2, n, v2, note2, grace, stopgrace): + if type2 not in ["start", "stop"]: + return # slur type continue has no abc equivalent + if n == None: + n = "1" + if n in s.slurBuf: + type1, v1, note1, grace1 = s.slurBuf[n] + if type2 != type1: # slur complete, now check the voice + if v2 == v1: # begins and ends in the same voice: keep it + # normal slur: start before stop and no grace slur + if type1 == "start" and (not grace1 or not stopgrace): + # keep left-right order! + note1.before = ["("] + note1.before + note2.after += ")" + # no else: don't bother with reversed stave spanning slurs + del s.slurBuf[n] # slur finished, remove from stack + else: # double definition, keep the last + info( + "double slur numbers %s-%s in part %d, measure %d, voice %d note %s, first discarded" + % (type2, n, s.msr.ixp + 1, s.msr.ixm + 1, v2, note2.ns) + ) + s.slurBuf[n] = (type2, v2, note2, grace) + else: # unmatched slur, put in dict + s.slurBuf[n] = (type2, v2, note2, grace) + + def doNotations(s, note, nttn, isTab): + for key, val in s.ornaments: + if nttn.find(key) != None: + note.before += [val] # just concat all ornaments + trem = nttn.find("ornaments/tremolo") + if trem != None: + type = trem.get("type") + if type == "single": + note.before.insert(0, "!%s!" % (int(trem.text) * "/")) + else: + note.fact = None # no time modification in ABC + if s.tstep: # abc2svg version + if type == "stop": + note.before.insert(0, "!trem%s!" % trem.text) + else: # abc2xml version + if type == "start": + note.before.insert(0, "!%s-!" % (int(trem.text) * "/")) + fingering = nttn.findall("technical/fingering") + for finger in fingering: # handle multiple finger annotations + if not isTab: + # fingering goes before chord (addChord) + note.before += ["!%s!" % finger.text] + snaar = nttn.find("technical/string") + if snaar != None and isTab: + if s.tstep: + fret = nttn.find("technical/fret") + if fret != None: + note.tab = (snaar.text, fret.text) + else: + # no double string decos (bug in musescore) + deco = "!%s!" % snaar.text + if deco not in note.ntdec: + note.ntdec += deco + wvlns = nttn.findall("ornaments/wavy-line") + for wvln in wvlns: + if wvln.get("type") == "start": + # keep left-right order! + note.before = ["!trill(!"] + note.before + elif wvln.get("type") == "stop": + note.after += "!trill)!" + glis = nttn.find("glissando") + if glis == None: + glis = nttn.find("slide") # treat slide as glissando + if glis != None: + lt = "~" if glis.get("line-type") == "wavy" else "-" + if glis.get("type") == "start": + # keep left-right order! + note.before = ["!%s(!" % lt] + note.before + elif glis.get("type") == "stop": + note.before = ["!%s)!" % lt] + note.before + + def tabnote(s, alt, ptc, oct, v, ntrec): + p = s.step_map[ptc] + int(alt or "0") # p in -2 .. 13 + if p > 11: + oct += 1 # octave correction + if p < 0: + oct -= 1 + p = p % 12 # remap p into 0..11 + snaar_nw, fret_nw = ntrec.tab # the computed/annotated allocation of nt + for i in range(4): # support same note on 4 strings + # get alternative representation of same note + na = s.note_alts[i % 3][p] + o = oct + if na in ["^B", "^^B"]: + o -= 1 # because in adjacent octave + if na in ["_C", "__C"]: + o += 1 + if "/" in na or i == 3: + o = 9 # emergency notation for 4th string case + nt = addoct(na, o) + # the current allocation of nt + snaar, fret = s.tabmap.get((v, nt), ("", "")) + if not snaar: + break # note not yet allocated + if snaar_nw == snaar: + return nt # use present allocation + if i == 3: # new allocaion needed but none is free + fmt = "rejected: voice %d note %3s string %s fret %2s remains: string %s fret %s" + info(fmt % (v, nt, snaar_nw, fret_nw, snaar, fret), 1) + ntrec.tab = (snaar, fret) + # for tablature map (voice, note) -> (string, fret) + s.tabmap[v, nt] = ntrec.tab + # ABC code always in key C (with midi pitch alterations) + return nt + + def ntAbc(s, ptc, oct, note, v, ntrec, isTab): # pitch, octave -> abc notation + acc2alt = { + "double-flat": -2, + "flat-flat": -2, + "flat": -1, + "natural": 0, + "sharp": 1, + "sharp-sharp": 2, + "double-sharp": 2, + } + oct += s.clefOct.get(s.curStf[v], 0) # minus clef-octave-change value + acc = note.findtext("accidental") # should be the notated accidental + alt = note.findtext("pitch/alter") # pitch alteration (midi) + if ntrec.tab: + # implies s.tstep is true (options.t was given) + return s.tabnote(alt, ptc, oct, v, ntrec) + elif isTab and s.tstep: + nt = ["__", "_", "", "^", "^^"][int(alt or "0") + 2] + addoct(ptc, oct) + info("no string notation found for note %s in voice %d" % (nt, v), 1) + p = addoct(ptc, oct) + if alt == None and s.msralts.get(ptc, 0): + alt = 0 # no alt but key implies alt -> natural!! + if alt == None and (p, v) in s.curalts: + alt = 0 # no alt but previous note had one -> natural!! + if acc == None and alt == None: + return p # no acc, no alt + elif acc != None: + alt = acc2alt[acc] # acc takes precedence over the pitch here! + else: # now see if we really must add an accidental + alt = int(float(alt)) + if (p, v) in s.curalts: # the note in this voice has been altered before + if alt == s.curalts[(p, v)]: + return p # alteration still the same + elif alt == s.msralts.get(ptc, 0): + return p # alteration implied by the key + # in xml we have separate notated ties and playback ties + tieElms = note.findall("tie") + note.findall("notations/tied") + if "stop" in [e.get("type") for e in tieElms]: + return p # don't alter tied notes + info( + "accidental %d added in part %d, measure %d, voice %d note %s" + % (alt, s.msr.ixp + 1, s.msr.ixm + 1, v + 1, p) + ) + s.curalts[(p, v)] = alt + # and finally ... prepend the accidental + p = ["__", "_", "=", "^", "^^"][alt + 2] + p + return p + + def doNote(s, n): # parse a musicXML note tag + note = Note() + v = int(n.findtext("voice", "1")) + if s.isSib: + v += 100 * int(n.findtext("staff", "1")) # repair bug in Sibelius + chord = n.find("chord") != None + p = n.findtext("pitch/step") or n.findtext("unpitched/display-step") + o = n.findtext("pitch/octave") or n.findtext("unpitched/display-octave") + r = n.find("rest") + numer = n.findtext("time-modification/actual-notes") + if numer: + denom = n.findtext("time-modification/normal-notes") + note.fact = (int(numer), int(denom)) + note.tup = [x.get("type") for x in n.findall("notations/tuplet")] + dur = n.findtext("duration") + grc = n.find("grace") + note.grace = grc != None + # strings with ABC stuff that goes before or after a note/chord + note.before, note.after = [], "" + if note.grace and not s.ingrace: # open a grace sequence + s.ingrace = 1 + note.before = ["{"] + if grc.get("slash") == "yes": + note.before += ["/"] # acciaccatura + stopgrace = not note.grace and s.ingrace + if stopgrace: # close the grace sequence + s.ingrace = 0 + s.msc.lastnote.after += "}" # close grace on lastenote.after + if dur == None or note.grace: + dur = 0 + if r == None and n.get("print-object") == "no": + if chord: + return + # turn invisible notes (that advance the time) into invisible rests + r = 1 + note.dur = int(dur) + if r == None and (not p or not o): # not a rest and no pitch + s.msc.cnt.inc("nopt", v) # count unpitched notes + o, p = 5, "E" # make it an E5 ?? + isTab = s.curClef and s.curClef.get(s.curStf[v], "").startswith("tab") + nttn = n.find("notations") # add ornaments + if nttn != None: + s.doNotations(note, nttn, isTab) + e = n.find("stem") if r == None else None # no !stemless! before rest + if e != None and e.text == "none" and (not isTab or v in s.hasStems or s.tstep): + note.before += ["s"] + abcOut.stemless = 1 + e = n.find("accidental") + if e != None and e.get("parentheses") == "yes": + note.ntdec += "!courtesy!" + if r != None: + noot = "x" if n.get("print-object") == "no" or isTab else "z" + else: + noot = s.ntAbc(p, int(o), n, v, note, isTab) + if n.find("unpitched") != None: + clef = s.curClef[s.curStf[v]] # the current clef for this voice + # (clef independent) step value of note on the staff + step = staffStep(p, int(o), clef, s.tstep) + instr = n.find("instrument") + instId = instr.get("id") if instr != None else "dummyId" + midi = s.drumInst.get(instId, abcMid(noot)) + # replace spaces in xml notehead names for percmap + nh = n.findtext("notehead", "").replace(" ", "-") + if nh == "x": + noot = "^" + noot.replace("^", "").replace("_", "") + if nh in ["circle-x", "diamond", "triangle"]: + noot = "_" + noot.replace("^", "").replace("_", "") + if nh and n.find("notehead").get("filled", "") == "yes": + nh += "+" + if nh and n.find("notehead").get("filled", "") == "no": + nh += "-" + # keep data for percussion map + s.drumNotes[(v, noot)] = (step, midi, nh) + # in xml we have separate notated ties and playback ties + tieElms = n.findall("tie") + n.findall("notations/tied") + # n can have stop and start tie + if "start" in [e.get("type") for e in tieElms]: + noot = noot + "-" + note.beam = sum( + [1 for b in n.findall("beam") if b.text in ["continue", "end"]] + ) + int(note.grace) + lyrlast = 0 + rsib = re.compile(r"^.*verse") + for e in n.findall("lyric"): + # also do Sibelius numbers + lyrnum = int(rsib.sub("", e.get("number", "1"))) + if lyrnum == 0: + lyrnum = lyrlast + 1 # and correct Sibelius bugs + else: + lyrlast = lyrnum + note.lyrs[lyrnum] = doSyllable(e) + stemdir = n.findtext("stem") + if s.wstems and (stemdir == "up" or stemdir == "down"): + if stemdir != s.stemDir.get(v, ""): + s.stemDir[v] = stemdir + s.msc.appendElem(v, "[I:stemdir %s]" % stemdir) + if chord: + s.msc.addChord(note, noot) + else: + xmlstaff = int(n.findtext("staff", "1")) + if s.curStf[v] != xmlstaff: # the note should go to another staff + dstaff = xmlstaff - s.curStf[v] # relative new staff number + # remember the new staff for this voice + s.curStf[v] = xmlstaff + # insert a move before the note + s.msc.appendElem(v, "[I:staff %+d]" % dstaff) + s.msc.appendNote(v, note, noot) + # s.msc.lastnote points to the last real note/chord inserted above + for slur in n.findall("notations/slur"): + s.matchSlur( + slur.get("type"), + slur.get("number"), + v, + s.msc.lastnote, + note.grace, + stopgrace, + ) # match slur definitions + + def doAttr(s, e): # parse a musicXML attribute tag + teken = { + "C1": "alto1", + "C2": "alto2", + "C3": "alto", + "C4": "tenor", + "F4": "bass", + "F3": "bass3", + "G2": "treble", + "TAB": "tab", + "percussion": "perc", + } + dvstxt = e.findtext("divisions") + if dvstxt: + s.msr.divs = int(dvstxt) + # for transposing instrument + steps = int(e.findtext("transpose/chromatic", "0")) + fifths = e.findtext("key/fifths") + first = s.msc.tijd == 0 and s.msr.ixm == 0 # first attributes in first measure + if fifths: + key, s.msralts = setKey(int(fifths), e.findtext("key/mode", "major")) + if first and not steps and abcOut.key == "none": + # first measure -> header, if not transposing instrument or percussion part! + abcOut.key = key + elif key != abcOut.key or not first: + s.msr.attr += "[K:%s]" % key # otherwise -> voice + beats = e.findtext("time/beats") + if beats: + unit = e.findtext("time/beat-type") + mtr = beats + "/" + unit + if first: + abcOut.mtr = mtr # first measure -> header + else: + s.msr.attr += "[M:%s]" % mtr # otherwise -> voice + s.msr.mtr = int(beats), int(unit) + # duration of measure in xml-divisions + s.msr.mdur = (s.msr.divs * s.msr.mtr[0] * 4) // s.msr.mtr[1] + for ms in e.findall("measure-style"): + n = int(ms.get("number", "1")) # staff number + voices = s.stfMap[n] # all voices of staff n + for mr in ms.findall("measure-repeat"): + ty = mr.get("type") + if ( + ty == "start" + ): # remember start measure number and text voor each staff + s.repeat_str[n] = [s.msr.ixm, mr.text] + for ( + v + ) in ( + voices + ): # insert repeat into all voices, value will be overwritten at stop + s.msc.insertElem(v, s.repeat_str[n]) + elif ty == "stop": # calculate repeat measure count for this staff n + start_ix, text_ = s.repeat_str[n] + repeat_count = s.msr.ixm - start_ix + if text_: + mid_str = "%s " % text_ + repeat_count /= int(text_) + else: + mid_str = "" # overwrite repeat with final string + s.repeat_str[n][0] = "[I:repeat %s%d]" % (mid_str, repeat_count) + del s.repeat_str[n] # remove closed repeats + toct = e.findtext("transpose/octave-change", "") + if toct: + steps += 12 * int(toct) # extra transposition of toct octaves + for clef in e.findall("clef"): # a part can have multiple staves + # local staff number for this clef + n = int(clef.get("number", "1")) + sgn = clef.findtext("sign") + line = clef.findtext("line", "") if sgn not in ["percussion", "TAB"] else "" + cs = teken.get(sgn + line, "") + oct = clef.findtext("clef-octave-change", "") or "0" + if oct: + cs += {-2: "-15", -1: "-8", 1: "+8", 2: "+15"}.get(int(oct), "") + # xml playback pitch -> abc notation pitch + s.clefOct[n] = -int(oct) + if steps: + cs += " transpose=" + str(steps) + stfdtl = e.find("staff-details") + if stfdtl and int(stfdtl.get("number", "1")) == n: + lines = stfdtl.findtext("staff-lines") + if lines: + lns = "|||" if lines == "3" and sgn == "TAB" else lines + cs += " stafflines=%s" % lns + s.stafflines = int(lines) # remember for tab staves + strings = stfdtl.findall("staff-tuning") + if strings: + tuning = [ + st.findtext("tuning-step") + st.findtext("tuning-octave") + for st in strings + ] + cs += " strings=%s" % ",".join(tuning) + capo = stfdtl.findtext("capo") + if capo: + cs += " capo=%s" % capo + # keep track of current clef (for percmap) + s.curClef[n] = cs + if first: + # clef goes to header (where it is mapped to voices) + s.clefMap[n] = cs + else: + # clef change to all voices of staff n + voices = s.stfMap[n] + for v in voices: + if n != s.curStf[v]: # voice is not at its home staff n + dstaff = n - s.curStf[v] + # reset current staff at start of measure to home position + s.curStf[v] = n + s.msc.appendElem(v, "[I:staff %+d]" % dstaff) + s.msc.appendElem(v, "[K:%s]" % cs) + + def findVoice(s, i, es): + # directions belong to a staff + stfnum = int(es[i].findtext("staff", 1)) + vs = s.stfMap[stfnum] # voices in this staff + # directions to first voice of staff + v1 = vs[0] if vs else 1 + if s.dirtov1: + return stfnum, v1, v1 # option --v1 + for e in es[i + 1 :]: # or to the voice of the next note + if e.tag == "note": + v = int(e.findtext("voice", "1")) + if s.isSib: + # repair bug in Sibelius + v += 100 * int(e.findtext("staff", "1")) + # use our own staff allocation + stf = s.vce2stf[v] + return stf, v, v1 # voice of next note, first voice of staff + if e.tag == "backup": + break + return stfnum, v1, v1 # no note found, fall back to v1 + + def doDirection(s, e, i, es): # parse a musicXML direction tag + def addDirection(x, vs, tijd, stfnum): + if not x: + return + vs = ( + s.stfMap[stfnum] if "!8v" in x else [vs] + ) # ottava's go to all voices of staff + for v in vs: + if tijd != None: # insert at time of encounter + s.msc.appendElemT( + v, x.replace("(", ")").replace("ped", "ped-up"), tijd + ) + else: + s.msc.appendElem(v, x) + + def startStop(dtype, vs, stfnum=1): + typmap = { + "down": "!8va(!", + "up": "!8vb(!", + "crescendo": "!<(!", + "diminuendo": "!>(!", + "start": "!ped!", + } + type = t.get("type", "") + # key to match the closing direction + k = dtype + t.get("number", "1") + if type in typmap: # opening the direction + x = typmap[type] + if k in s.dirStk: # closing direction already encountered + stype, tijd = s.dirStk[k] + del s.dirStk[k] + if stype == "stop": + addDirection(x, vs, tijd, stfnum) + else: + info( + "%s direction %s has no stop in part %d, measure %d, voice %d" + % (dtype, stype, s.msr.ixp + 1, s.msr.ixm + 1, vs + 1) + ) + # remember voice and type for closing + s.dirStk[k] = (type, vs) + else: + # remember voice and type for closing + s.dirStk[k] = (type, vs) + elif type == "stop": + if k in s.dirStk: # matching open direction found + type, vs = s.dirStk[k] + del s.dirStk[k] # into the same voice + if type == "stop": + info( + "%s direction %s has double stop in part %d, measure %d, voice %d" + % (dtype, type, s.msr.ixp + 1, s.msr.ixm + 1, vs + 1) + ) + x = "" + else: + x = typmap[type].replace("(", ")").replace("ped", "ped-up") + else: # closing direction found before opening + s.dirStk[k] = ("stop", s.msc.tijd) + x = "" # delay code generation until opening found + else: + raise ValueError("wrong direction type") + addDirection(x, vs, None, stfnum) + + tempo, wrdstxt = None, "" + plcmnt = e.get("placement") + stf, vs, v1 = s.findVoice(i, es) + jmp = "" # for jump sound elements: dacapo, dalsegno and family + jmps = [ + ("dacapo", "D.C."), + ("dalsegno", "D.S."), + ("tocoda", "dacoda"), + ("fine", "fine"), + ("coda", "O"), + ("segno", "S"), + ] + # there are many possible attributes for sound + t = e.find("sound") + if t != None: + minst = t.find("midi-instrument") + if minst: + prg = t.findtext("midi-instrument/midi-program") + chn = t.findtext("midi-instrument/midi-channel") + vids = [v for v, id in s.vceInst.items() if id == minst.get("id")] + if vids: + # direction for the indentified voice, not the staff + vs = vids[0] + parm, inst = ("program", str(int(prg) - 1)) if prg else ("channel", chn) + if inst and abcOut.volpan > 0: + s.msc.appendElem(vs, "[I:MIDI= %s %s]" % (parm, inst)) + tempo = t.get("tempo") # look for tempo attribute + if tempo: + # hope it is a number and insert in voice 1 + tempo = "%.0f" % float(tempo) + tempo_units = (1, 4) # always 1/4 for sound elements! + for r, v in jmps: + if t.get(r, ""): + jmp = v + break + dirtypes = e.findall("direction-type") + for dirtyp in dirtypes: + units = { + "whole": (1, 1), + "half": (1, 2), + "quarter": (1, 4), + "eighth": (1, 8), + } + metr = dirtyp.find("metronome") + if metr != None: + t = metr.findtext("beat-unit", "") + if t in units: + tempo_units = units[t] + else: + tempo_units = units["quarter"] + if metr.find("beat-unit-dot") != None: + tempo_units = simplify(tempo_units[0] * 3, tempo_units[1] * 2) + # look for a number + tmpro = re.search("[.\d]+", metr.findtext("per-minute")) + if tmpro: + tempo = ( + tmpro.group() + ) # overwrites the value set by the sound element of this direction + t = dirtyp.find("wedge") + if t != None: + startStop("wedge", vs) + allwrds = dirtyp.findall("words") # insert text annotations + if not allwrds: + # treat rehearsal mark as text annotation + allwrds = dirtyp.findall("rehearsal") + for wrds in allwrds: + if ( + jmp + ): # ignore the words when a jump sound element is present in this direction + s.msc.appendElem(vs, "!%s!" % jmp, 1) # to voice + break + plc = plcmnt == "below" and "_" or "^" + if float(wrds.get("default-y", "0")) < 0: + plc = "_" + wrdstxt += (wrds.text or "").replace('"', '\\"').replace("\n", "\\n") + wrdstxt = wrdstxt.strip() + for key, val in dynamics_map.items(): + if dirtyp.find("dynamics/" + key) != None: + s.msc.appendElem(vs, val, 1) # to voice + if dirtyp.find("coda") != None: + s.msc.appendElem(vs, "O", 1) + if dirtyp.find("segno") != None: + s.msc.appendElem(vs, "S", 1) + t = dirtyp.find("octave-shift") + if t != None: + # assume size == 8 for the time being + startStop("octave-shift", vs, stf) + t = dirtyp.find("pedal") + if t != None and s.ped: + if not s.pedVce: + s.pedVce = vs + startStop("pedal", s.pedVce) + if dirtyp.findtext("other-direction") == "diatonic fretting": + s.diafret = 1 + if tempo: + # hope it is a number and insert in voice 1 + tempo = "%.0f" % float(tempo) + if s.msc.tijd == 0 and s.msr.ixm == 0: # first measure -> header + abcOut.tempo = tempo + abcOut.tempo_units = tempo_units + else: + # otherwise -> 1st voice + s.msc.appendElem( + v1, "[Q:%d/%d=%s]" % (tempo_units[0], tempo_units[1], tempo) + ) + if wrdstxt: + s.msc.appendElem( + vs, '"%s%s"' % (plc, wrdstxt), 1 + ) # to voice, but after tempo + + def doHarmony(s, e, i, es): # parse a musicXMl harmony tag + _, vt, _ = s.findVoice(i, es) + short = { + "major": "", + "minor": "m", + "augmented": "+", + "diminished": "dim", + "dominant": "7", + "half-diminished": "m7b5", + } + accmap = { + "major": "maj", + "dominant": "", + "minor": "m", + "diminished": "dim", + "augmented": "+", + "suspended": "sus", + } + modmap = { + "second": "2", + "fourth": "4", + "seventh": "7", + "sixth": "6", + "ninth": "9", + "11th": "11", + "13th": "13", + } + altmap = {"1": "#", "0": "", "-1": "b"} + root = e.findtext("root/root-step", "") + alt = altmap.get(e.findtext("root/root-alter"), "") + sus = "" + kind = e.findtext("kind", "") + if kind in short: + kind = short[kind] + elif "-" in kind: # xml chord names: - + triad, mod = kind.split("-") + kind = accmap.get(triad, "") + modmap.get(mod, "") + if kind.startswith("sus"): + kind, sus = "", kind # sus-suffix goes to the end + elif kind == "none": + kind = e.find("kind").get("text", "") + degrees = e.findall("degree") + for d in degrees: # chord alterations + kind += altmap.get(d.findtext("degree-alter"), "") + d.findtext( + "degree-value", "" + ) + kind = kind.replace("79", "9").replace("713", "13").replace("maj6", "6") + bass = e.findtext("bass/bass-step", "") + altmap.get( + e.findtext("bass/bass-alter"), "" + ) + s.msc.appendElem( + vt, '"%s%s%s%s%s"' % (root, alt, kind, sus, bass and "/" + bass), 1 + ) + + def doBarline(s, e): # 0 = no repeat, 1 = begin repeat, 2 = end repeat + rep = e.find("repeat") + if rep != None: + rep = rep.get("direction") + if s.unfold: # unfold repeat, don't translate barlines + return rep and (rep == "forward" and 1 or 2) or 0 + loc = e.get("location", "right") # right is the default + if loc == "right": # only change style for the right side + style = e.findtext("bar-style") + if style == "light-light": + s.msr.rline = "||" + elif style == "light-heavy": + s.msr.rline = "|]" + if rep != None: # repeat found + if rep == "forward": + s.msr.lline = ":" + else: + s.msr.rline = ":|" # override barline style + end = e.find("ending") + if end != None: + if end.get("type") == "start": + n = end.get("number", "1").replace(".", "").replace(" ", "") + try: + # should be a list of integers + list(map(int, n.split(","))) + except: + n = '"%s"' % n.strip() # illegal musicXML + s.msr.lnum = n # assume a start is always at the beginning of a measure + elif s.msr.rline == "|": # stop and discontinue the same in ABC ? + s.msr.rline = "||" # to stop on a normal barline use || in ABC ? + return 0 + + def doPrint(s, e): # print element, measure number -> insert a line break + if e.get("new-system") == "yes" or e.get("new-page") == "yes": + if not s.nolbrk: + return "$" # a line break + + def doPartList( + s, e + ): # translate the start/stop-event-based xml-partlist into proper tree + for sp in e.findall("part-list/score-part"): + midi = {} + for m in sp.findall("midi-instrument"): + x = [ + m.findtext(p, s.midDflt[i]) + for i, p in enumerate( + ["midi-channel", "midi-program", "volume", "pan"] + ) + ] + pan = float(x[3]) + if pan >= -90 and pan <= 90: # would be better to map behind-pannings + pan = (float(x[3]) + 90) / 180 * 127 # xml between -90 and +90 + midi[m.get("id")] = [ + int(x[0]), + int(x[1]), + float(x[2]) * 1.27, + pan, + ] # volume 100 -> midi 127 + up = m.findtext("midi-unpitched") + if up: + # store midi-pitch for channel 10 notes + s.drumInst[m.get("id")] = int(up) - 1 + s.instMid.append(midi) + ps = e.find("part-list") # partlist = [groupelem] + # groupelem = partname | grouplist + xs = getPartlist(ps) + # grouplist = [groupelem, ..., groupdata] + partlist, _ = parseParts(xs, {}, []) + # groupdata = [group-symbol, group-barline, group-name, group-abbrev] + return partlist + + def mkTitle(s, e): + def filterCredits(y): # y == filter level, higher filters less + cs = [] + for x in credits: # skip redundant credit lines + if y < 6 and (x in title or x in mvttl): + continue # sure skip + if y < 5 and (x in composer or x in lyricist): + continue # almost sure skip + if y < 4 and ((title and title in x) or (mvttl and mvttl in x)): + continue # may skip too much + if y < 3 and ( + [1 for c in composer if c in x] or [1 for c in lyricist if c in x] + ): + continue # skips too much + if y < 2 and re.match(r"^[\d\W]*$", x): + continue # line only contains numbers and punctuation + cs.append(x) + if y == 0 and (title + mvttl): + cs = "" # default: only credit when no title set + return cs + + title = e.findtext("work/work-title", "").strip() + mvttl = e.findtext("movement-title", "").strip() + composer, lyricist, credits = [], [], [] + for creator in e.findall("identification/creator"): + if creator.text: + if creator.get("type") == "composer": + composer += [line.strip() for line in creator.text.split("\n")] + elif creator.get("type") in ("lyricist", "transcriber"): + lyricist += [line.strip() for line in creator.text.split("\n")] + for rights in e.findall("identification/rights"): + if rights.text: + lyricist += [line.strip() for line in rights.text.split("\n")] + for credit in e.findall("credit"): + cs = "".join(e.text or "" for e in credit.findall("credit-words")) + credits += [re.sub(r"\s*[\r\n]\s*", " ", cs)] + credits = filterCredits(s.ctf) + if title: + title = "T:%s\n" % title.replace("\n", "\nT:") + if mvttl: + title += "T:%s\n" % mvttl.replace("\n", "\nT:") + if credits: + title += "\n".join(["T:%s" % c for c in credits]) + "\n" + if composer: + title += "\n".join(["C:%s" % c for c in composer]) + "\n" + if lyricist: + title += "\n".join(["Z:%s" % c for c in lyricist]) + "\n" + if title: + abcOut.title = title[:-1] + s.isSib = "Sibelius" in (e.findtext("identification/encoding/software") or "") + if s.isSib: + info("Sibelius MusicXMl is unreliable") + + def doDefaults(s, e): + if not s.doPageFmt: + return # return if -pf option absent + d = e.find("defaults") + if d == None: + return + mils = d.findtext("scaling/millimeters") # mills == staff height (mm) + tenths = d.findtext("scaling/tenths") # staff height in tenths + if not mils or not tenths: + return + xmlScale = float(mils) / float(tenths) / 10 # tenths -> mm + space = 10 * xmlScale # space between staff lines == 10 tenths + # 0.2117 cm = 6pt = space between staff lines for scale = 1.0 in abcm2ps + abcScale = space / 0.2117 + abcOut.pageFmt["scale"] = abcScale + eks = 2 * ["page-layout/"] + 4 * ["page-layout/page-margins/"] + eks = [ + a + b + for a, b in zip( + eks, + "page-height,page-width,left-margin,right-margin,top-margin,bottom-margin".split( + "," + ), + ) + ] + for i in range(6): + v = d.findtext(eks[i]) + # pagekeys [0] == scale already done, skip it + k = abcOut.pagekeys[i + 1] + if not abcOut.pageFmt[k] and v: + try: + abcOut.pageFmt[k] = float(v) * xmlScale # -> cm + except: + info("illegal value %s for xml element %s", (v, eks[i])) + continue # just skip illegal values + + def locStaffMap(s, part, maten): # map voice to staff with majority voting + vmap = {} # {voice -> {staff -> n}} count occurrences of voice in staff + s.vceInst = {} # {voice -> instrument id} for this part + s.msc.vnums = {} # voice id's + # XML voice nums with at least one note with a stem (for tab key) + s.hasStems = {} + s.stfMap, s.clefMap = {}, {} # staff -> [voices], staff -> clef + ns = part.findall("measure/note") + for n in ns: # count staff allocations for all notes + v = int(n.findtext("voice", "1")) + if s.isSib: + # repair bug in Sibelius + v += 100 * int(n.findtext("staff", "1")) + s.msc.vnums[v] = 1 # collect all used voice id's in this part + sn = int(n.findtext("staff", "1")) + s.stfMap[sn] = [] + if v not in vmap: + vmap[v] = {sn: 1} + else: + d = vmap[v] # counter for voice v + # ++ number of allocations for staff sn + d[sn] = d.get(sn, 0) + 1 + x = n.find("instrument") + if x != None: + s.vceInst[v] = x.get("id") + x, noRest = n.findtext("stem"), n.find("rest") == None + if noRest and (not x or x != "none"): + s.hasStems[v] = 1 # XML voice v has at least one stem + vks = list(vmap.keys()) + if s.jscript or s.isSib: + vks.sort() + for v in vks: # choose staff with most allocations for each voice + xs = [(n, sn) for sn, n in vmap[v].items()] + xs.sort() + stf = xs[-1][1] # the winner: staff with most notes of voice v + s.stfMap[stf].append(v) + s.vce2stf[v] = stf # reverse map + s.curStf[v] = stf # current staff of XML voice v + + def addStaffMap(s, vvmap): # vvmap: xml voice number -> global abc voice number + part = [] # default: brace on staffs of one part + # s.stfMap has xml staff and voice numbers + for stf, voices in sorted(s.stfMap.items()): + locmap = [vvmap[iv] for iv in voices if iv in vvmap] + nostem = [ + (iv not in s.hasStems) for iv in voices if iv in vvmap + ] # same order as locmap + if locmap: # abc voice number of staff stf + part.append(locmap) + # {xml staff number -> clef} + clef = s.clefMap.get(stf, "treble") + for i, iv in enumerate(locmap): + clef_attr = "" + if clef.startswith("tab"): + if nostem[i] and "nostems" not in clef: + clef_attr = " nostems" + if s.diafret and "diafret" not in clef: + clef_attr += " diafret" # for all voices in the part + # add nostems when all notes of voice had no stem + abcOut.clefs[iv] = clef + clef_attr + s.gStfMap.append(part) + + def addMidiMap(s, ip, vvmap): # map abc voices to midi settings + instr = s.instMid[ip] # get the midi settings for this part + if instr.values(): + # default settings = first instrument + defInstr = list(instr.values())[0] + else: + defInstr = s.midDflt # no instruments defined + xs = [] + for v, vabc in vvmap.items(): # xml voice num, abc voice num + ks = sorted(s.drumNotes.items()) + ds = [ + (nt, step, midi, head) for (vd, nt), (step, midi, head) in ks if v == vd + ] # map perc notes + # get the instrument-id for part with multiple instruments + id = s.vceInst.get(v, "") + if id in instr: # id is defined as midi-instrument in part-list + xs.append((vabc, instr[id] + ds)) # get midi settings for id + else: + # only one instrument for this part + xs.append((vabc, defInstr + ds)) + xs.sort() # put abc voices in order + s.midiMap.extend([midi for v, midi in xs]) + snaarmap = ["E", "G", "B", "d", "f", "a", "c'", "e'"] + diamap = ( + "0,1-,1,1+,2,3,3,4,4,5,6,6+,7,8-,8,8+,9,10,10,11,11,12,13,13+,14".split(",") + ) + for k in sorted(s.tabmap.keys()): # add %%map's for all tab voices + v, noot = k + snaar, fret = s.tabmap[k] + if s.diafret: + fret = diamap[int(fret)] + vabc = vvmap[v] + snaar = s.stafflines - int(snaar) + xs = s.tabVceMap.get(vabc, []) + xs.append( + "%%%%map tab%d %s print=%s heads=kop%s\n" + % (vabc, noot, snaarmap[snaar], fret) + ) + s.tabVceMap[vabc] = xs + s.koppen[fret] = 1 # collect noteheads for SVG defs + + def parse(s, xmltxt): + vvmapAll = {} # collect xml->abc voice maps (vvmap) of all parts + e = E.fromstring(xmltxt) + s.mkTitle(e) + s.doDefaults(e) + partlist = s.doPartList(e) + parts = e.findall("part") + for ip, p in enumerate(parts): + maten = p.findall("measure") + s.locStaffMap(p, maten) # {voice -> staff} for this part + # (xml voice, abc note) -> (midi note, note head) + s.drumNotes = {} + s.clefOct = {} # xml staff number -> current clef-octave-change + s.curClef = {} # xml staff number -> current abc clef + s.stemDir = {} # xml voice number -> current stem direction + s.tabmap = {} # (xml voice, abc note) -> (string, fret) + s.diafret = 0 # use diatonic fretting + s.stafflines = 5 + s.msc.initVoices(newPart=1) # create all voices + aantalHerhaald = 0 # keep track of number of repititions + herhaalMaat = 0 # target measure of the repitition + divisions = [] # current value of for each measure + s.msr = Measure(ip) # various measure data + while s.msr.ixm < len(maten): + maat = maten[s.msr.ixm] + herhaal, lbrk = 0, "" + s.msr.reset() + s.curalts = {} # passing accidentals are reset each measure + es = list(maat) + for i, e in enumerate(es): + if e.tag == "note": + s.doNote(e) + elif e.tag == "attributes": + s.doAttr(e) + elif e.tag == "direction": + s.doDirection(e, i, es) + elif e.tag == "sound": + # sound element directly in measure! + s.doDirection(maat, i, es) + elif e.tag == "harmony": + s.doHarmony(e, i, es) + elif e.tag == "barline": + herhaal = s.doBarline(e) + elif e.tag == "backup": + dt = int(e.findtext("duration")) + if chkbug(dt, s.msr): + s.msc.incTime(-dt) + elif e.tag == "forward": + dt = int(e.findtext("duration")) + if chkbug(dt, s.msr): + s.msc.incTime(dt) + elif e.tag == "print": + lbrk = s.doPrint(e) + s.msc.addBar(lbrk, s.msr) + divisions.append(s.msr.divs) + if herhaal == 1: + herhaalMaat = s.msr.ixm + s.msr.ixm += 1 + elif herhaal == 2: + if aantalHerhaald < 1: # jump + s.msr.ixm = herhaalMaat + aantalHerhaald += 1 + else: + aantalHerhaald = 0 # reset + s.msr.ixm += 1 # just continue + else: + s.msr.ixm += 1 # on to the next measure + for ( + rv + ) in s.repeat_str.values(): # close hanging measure-repeats without stop + rv[0] = "[I:repeat %s %d]" % (rv[1], 1) + vvmap = s.msc.outVoices(divisions, ip, s.isSib) + s.addStaffMap(vvmap) # update global staff map + s.addMidiMap(ip, vvmap) + vvmapAll.update(vvmap) + if vvmapAll: # skip output if no part has any notes + abcOut.mkHeader(s.gStfMap, partlist, s.midiMap, s.tabVceMap, s.koppen) + else: + info("nothing written, %s has no notes ..." % abcOut.fnmext) + + +def vertaal(xmltxt, **options_parm): + class options: # the default option values + u = 0 + b = 0 + n = 0 + c = 0 + v = 0 + d = 0 + m = 0 + x = 0 + t = 0 + stm = 0 + mnum = -1 + p = "f" + s = 0 + j = 0 + v1 = 0 + ped = 0 + + global abcOut, info_list + info_list = [] + str = "" + for opt in options_parm: # assign the given options + setattr(options, opt, options_parm[opt]) + options.p = options.p.split(",") if options.p else [] # [] | [string] + abcOut = ABCoutput("", "", 0, options) + psr = Parser(options) + try: + psr.parse(xmltxt) # parse xmltxt + str = abcOut.getABC() # ABC output + info("%s written with %d voices" % (abcOut.fnmext, len(abcOut.clefs)), warn=0) + except: + etype, value, traceback = sys.exc_info() # works in python 2 & 3 + info("** %s occurred: %s" % (etype, value), 0) + return (str, "".join(info_list)) # and the diagnostic messages + + +# ---------------- +# Main Program +# ---------------- +if __name__ == "__main__": + from optparse import OptionParser + from glob import glob + from zipfile import ZipFile + + ustr = "%prog [-h] [-u] [-m] [-c C] [-d D] [-n CPL] [-b BPL] [-o DIR] [-v V]\n" + ustr += ( + "[-x] [-p PFMT] [-t] [-s] [-i] [--v1] [--noped] [--stems] [ ...]" + ) + parser = OptionParser(usage=ustr, version=str(VERSION)) + parser.add_option("-u", action="store_true", help="unfold simple repeats") + parser.add_option( + "-m", + action="store", + help="0 -> no %%MIDI, 1 -> minimal %%MIDI, 2-> all %%MIDI", + default=0, + ) + parser.add_option( + "-c", + action="store", + type="int", + help="set credit text filter to C", + default=0, + metavar="C", + ) + parser.add_option( + "-d", action="store", type="int", help="set L:1/D", default=0, metavar="D" + ) + parser.add_option( + "-n", + action="store", + type="int", + help="CPL: max number of characters per line (default 100)", + default=0, + metavar="CPL", + ) + parser.add_option( + "-b", + action="store", + type="int", + help="BPL: max number of bars per line", + default=0, + metavar="BPL", + ) + parser.add_option( + "-o", action="store", help="store abc files in DIR", default="", metavar="DIR" + ) + parser.add_option( + "-v", + action="store", + type="int", + help="set volta typesetting behaviour to V", + default=0, + metavar="V", + ) + parser.add_option("-x", action="store_true", help="output no line breaks") + parser.add_option( + "-p", + action="store", + help="pageformat PFMT (cm) = scale, pageheight, pagewidth, leftmargin, rightmargin, topmargin, botmargin", + default="", + metavar="PFMT", + ) + parser.add_option( + "-j", + action="store_true", + help="switch for compatibility with javascript version", + ) + parser.add_option( + "-t", + action="store_true", + help="translate perc- and tab-staff to ABC code with %%map, %%voicemap", + ) + parser.add_option( + "-s", action="store_true", help="shift node heads 3 units left in a tab staff" + ) + parser.add_option( + "--v1", + action="store_true", + help="start-stop directions allways to first voice of staff", + ) + parser.add_option( + "--noped", + action="store_false", + help="skip all pedal directions", + dest="ped", + default=True, + ) + parser.add_option( + "--stems", + action="store_true", + help="translate stem directions", + dest="stm", + default=False, + ) + parser.add_option( + "-i", action="store_true", help="read xml file from standard input" + ) + options, args = parser.parse_args() + if options.n < 0: + parser.error("only values >= 0") + if options.b < 0: + parser.error("only values >= 0") + if options.d and options.d not in [2**n for n in range(10)]: + parser.error("D should be on of %s" % ",".join([str(2**n) for n in range(10)])) + options.p = options.p and options.p.split(",") or [] # ==> [] | [string] + if len(args) == 0 and not options.i: + parser.error("no input file given") + pad = options.o + if pad: + if not os.path.exists(pad): + os.mkdir(pad) + if not os.path.isdir(pad): + parser.error("%s is not a directory" % pad) + fnmext_list = [] + for i in args: + fnmext_list += glob(i) + if options.i: + fnmext_list = ["stdin.xml"] + if not fnmext_list: + parser.error("none of the input files exist") + for X, fnmext in enumerate(fnmext_list): + fnm, ext = os.path.splitext(fnmext) + if ext.lower() not in (".xml", ".mxl", ".musicxml"): + info( + "skipped input file %s, it should have extension .xml or .mxl" % fnmext + ) + continue + if os.path.isdir(fnmext): + info("skipped directory %s. Only files are accepted" % fnmext) + continue + if fnmext == "stdin.xml": + fobj = sys.stdin + elif ext.lower() == ".mxl": # extract .xml file from .mxl file + z = ZipFile(fnmext) + for ( + n + ) in z.namelist(): # assume there is always an xml file in a mxl archive !! + if (n[:4] != "META") and (n[-4:].lower() == ".xml"): + fobj = z.open(n) + break # assume only one MusicXML file per archive + else: + fobj = open(fnmext, "rb") # open regular xml file + + # create global ABC output object + abcOut = ABCoutput(fnm + ".abc", pad, X, options) + psr = Parser(options) # xml parser + try: + # parse file fobj and write abc to .abc + psr.parse(fobj.read()) + abcOut.writeall() + except: + etype, value, traceback = sys.exc_info() # works in python 2 & 3 + info("** %s occurred: %s in %s" % (etype, value, fnmext), 0)