Spaces:
Running
Running
#!/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"></text> | |
<text id="x-" x="-3" y="0"></text> | |
<text id="x+" x="-3" y="0"></text> | |
<text id="normal" x="-3.7" y="0"></text> | |
<text id="normal-" x="-3.7" y="0"></text> | |
<text id="normal+" x="-3.7" y="0"></text> | |
<g id="circle-x"><text x="-3" y="0"></text><circle r="4" class="stroke"></circle></g> | |
<g id="circle-x-"><text x="-3" y="0"></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. * 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) | |