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