piano_transcription / xml2abc.py
admin
sync
07c7745
raw
history blame
No virus
98 kB
#!/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. <http://www.gnu.org/licenses/lgpl.html>.
"""
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
<defs>
<text id="x" x="-3" y="0">&#xe263;</text>
<text id="x-" x="-3" y="0">&#xe263;</text>
<text id="x+" x="-3" y="0">&#xe263;</text>
<text id="normal" x="-3.7" y="0">&#xe0a3;</text>
<text id="normal-" x="-3.7" y="0">&#xe0a3;</text>
<text id="normal+" x="-3.7" y="0">&#xe0a4;</text>
<g id="circle-x"><text x="-3" y="0">&#xe263;</text><circle r="4" class="stroke"></circle></g>
<g id="circle-x-"><text x="-3" y="0">&#xe263;</text><circle r="4" class="stroke"></circle></g>
<path id="triangle" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>
<path id="triangle-" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>
<path id="triangle+" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="fill:#000"></path>
<path id="square" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>
<path id="square-" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>
<path id="square+" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="fill:#000"></path>
<path id="diamond" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>
<path id="diamond-" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>
<path id="diamond+" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="fill:#000"></path>
</defs>
%%endsvg"""
tabSvg = """%%beginsvg
<style type="text/css">
.bf {font-family:sans-serif; font-size:7px}
</style>
<defs>
<rect id="clr" x="-3" y="-1" width="6" height="5" fill="white"></rect>
<rect id="clr2" x="-3" y="-1" width="11" height="5" fill="white"></rect>"""
kopSvg = '<g id="kop%s" class="bf"><use xlink:href="#clr"></use><text x="-2" y="3">%s</text></g>\n'
kopSvg2 = '<g id="kop%s" class="bf"><use xlink:href="#clr2"></use><text x="-2" y="3">%s</text></g>\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 <backup> 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 + ["</defs>\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 name>-<modification>
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 <divisions> 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] <file1> [<file2> ...]"
)
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 <fnm>.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)