X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=scripts%2Fmusicxml2ly.py;h=f808a8325b5a67655ac0cd3ffce223258a332150;hb=f94802522d08743f376d1ab4ccaabbe4b1879a4c;hp=3c4cef063ebe26f44c5bfedd8fe2724a571d0573;hpb=da5455debc2cbd54cac454dded7db87381c9d23c;p=lilypond.git diff --git a/scripts/musicxml2ly.py b/scripts/musicxml2ly.py index 3c4cef063e..f808a8325b 100644 --- a/scripts/musicxml2ly.py +++ b/scripts/musicxml2ly.py @@ -6,28 +6,155 @@ import re import os import string import codecs -from gettext import gettext as _ +import zipfile +import StringIO """ @relocate-preamble@ """ import lilylib as ly +_ = ly._ import musicxml import musicexp from rational import Rational +# Store command-line options in a global variable, so we can access them everythwere +options = None + +class Conversion_Settings: + def __init__(self): + self.ignore_beaming = False + +conversion_settings = Conversion_Settings () +# Use a global variable to store the setting needed inside a \layout block. +# whenever we need to change a setting or add/remove an engraver, we can access +# this layout and add the corresponding settings +layout_information = musicexp.Layout () def progress (str): - sys.stderr.write (str + '\n') + ly.stderr_write (str + '\n') sys.stderr.flush () def error_message (str): - sys.stderr.write (str + '\n') + ly.stderr_write (str + '\n') sys.stderr.flush () +needed_additional_definitions = [] +additional_definitions = { + "snappizzicato": """#(define-markup-command (snappizzicato layout props) () + (interpret-markup layout props + (markup #:stencil + (ly:stencil-translate-axis + (ly:stencil-add + (make-circle-stencil 0.7 0.1 #f) + (ly:make-stencil + (list 'draw-line 0.1 0 0.1 0 1) + '(-0.1 . 0.1) '(0.1 . 1) + ) + ) + 0.7 X + ) + ) + ) +)""" +} + +def round_to_two_digits (val): + return round (val * 100) / 100 + +def extract_paper_information (tree): + paper = musicexp.Paper () + defaults = tree.get_maybe_exist_named_child ('defaults') + if not defaults: + return None + tenths = -1 + scaling = defaults.get_maybe_exist_named_child ('scaling') + if scaling: + mm = scaling.get_named_child ('millimeters') + mm = string.atof (mm.get_text ()) + tn = scaling.get_maybe_exist_named_child ('tenths') + tn = string.atof (tn.get_text ()) + tenths = mm / tn + paper.global_staff_size = mm * 72.27 / 25.4 + # We need the scaling (i.e. the size of staff tenths for everything! + if tenths < 0: + return None + + def from_tenths (txt): + return round_to_two_digits (string.atof (txt) * tenths / 10) + def set_paper_variable (varname, parent, element_name): + el = parent.get_maybe_exist_named_child (element_name) + if el: # Convert to cm from tenths + setattr (paper, varname, from_tenths (el.get_text ())) + + pagelayout = defaults.get_maybe_exist_named_child ('page-layout') + if pagelayout: + # TODO: How can one have different margins for even and odd pages??? + set_paper_variable ("page_height", pagelayout, 'page-height') + set_paper_variable ("page_width", pagelayout, 'page-width') + + pmargins = pagelayout.get_named_children ('page-margins') + for pm in pmargins: + set_paper_variable ("left_margin", pm, 'left-margin') + set_paper_variable ("right_margin", pm, 'right-margin') + set_paper_variable ("bottom_margin", pm, 'bottom-margin') + set_paper_variable ("top_margin", pm, 'top-margin') + + systemlayout = defaults.get_maybe_exist_named_child ('system-layout') + if systemlayout: + sl = systemlayout.get_maybe_exist_named_child ('system-margins') + if sl: + set_paper_variable ("system_left_margin", sl, 'left-margin') + set_paper_variable ("system_right_margin", sl, 'right-margin') + set_paper_variable ("system_distance", systemlayout, 'system-distance') + set_paper_variable ("top_system_distance", systemlayout, 'top-system-distance') + + stafflayout = defaults.get_named_children ('staff-layout') + for sl in stafflayout: + nr = getattr (sl, 'number', 1) + dist = sl.get_named_child ('staff-distance') + #TODO: the staff distance needs to be set in the Staff context!!! + + # TODO: Finish appearance?, music-font?, word-font?, lyric-font*, lyric-language* + appearance = defaults.get_named_child ('appearance') + if appearance: + lws = appearance.get_named_children ('line-width') + for lw in lws: + # Possible types are: beam, bracket, dashes, + # enclosure, ending, extend, heavy barline, leger, + # light barline, octave shift, pedal, slur middle, slur tip, + # staff, stem, tie middle, tie tip, tuplet bracket, and wedge + tp = lw.type + w = from_tenths (lw.get_text ()) + # TODO: Do something with these values! + nss = appearance.get_named_children ('note-size') + for ns in nss: + # Possible types are: cue, grace and large + tp = ns.type + sz = from_tenths (ns.get_text ()) + # TODO: Do something with these values! + # elements have no specified meaning + + rawmusicfont = defaults.get_named_child ('music-font') + if rawmusicfont: + # TODO: Convert the font + pass + rawwordfont = defaults.get_named_child ('word-font') + if rawwordfont: + # TODO: Convert the font + pass + rawlyricsfonts = defaults.get_named_children ('lyric-font') + for lyricsfont in rawlyricsfonts: + # TODO: Convert the font + pass + + return paper + + + # score information is contained in the , or tags # extract those into a hash, indexed by proper lilypond header attributes def extract_score_information (tree): @@ -60,6 +187,19 @@ def extract_score_information (tree): set_if_exists ('encoder', ids.get_encoding_person ()) set_if_exists ('encodingdescription', ids.get_encoding_description ()) + # Finally, apply the required compatibility modes + # Some applications created wrong MusicXML files, so we need to + # apply some compatibility mode, e.g. ignoring some features/tags + # in those files + software = ids.get_encoding_software_list () + + # Case 1: "Sibelius 5.1" with the "Dolet 3.4 for Sibelius" plugin + # is missing all beam ends => ignore all beaming information + if "Dolet 3.4 for Sibelius" in software: + conversion_settings.ignore_beaming = True + progress (_ ("Encountered file created by Dolet 3.4 for Sibelius, containing wrong beaming information. All beaming information in the MusicXML file will be ignored")) + # TODO: Check for other unsupported features + return header class PartGroupInfo: @@ -73,21 +213,100 @@ class PartGroupInfo: def add_end (self, g): self.end[getattr (g, 'number', "1")] = g def print_ly (self, printer): - error_message ("Unprocessed PartGroupInfo %s encountered" % self) + error_message (_ ("Unprocessed PartGroupInfo %s encountered") % self) def ly_expression (self): - error_message ("Unprocessed PartGroupInfo %s encountered" % self) + error_message (_ ("Unprocessed PartGroupInfo %s encountered") % self) return '' +def staff_attributes_to_string_tunings (mxl_attr): + details = mxl_attr.get_maybe_exist_named_child ('staff-details') + if not details: + return [] + lines = 6 + staff_lines = details.get_maybe_exist_named_child ('staff-lines') + if staff_lines: + lines = string.atoi (staff_lines.get_text ()) + + tunings = [0]*lines + staff_tunings = details.get_named_children ('staff-tuning') + for i in staff_tunings: + p = musicexp.Pitch() + line = 0 + try: + line = string.atoi (i.line) - 1 + except ValueError: + pass + tunings[line] = p + + step = i.get_named_child (u'tuning-step') + step = step.get_text ().strip () + p.step = musicxml_step_to_lily (step) + + octave = i.get_named_child (u'tuning-octave') + octave = octave.get_text ().strip () + p.octave = int (octave) - 4 + + alter = i.get_named_child (u'tuning-alter') + if alter: + p.alteration = int (alter.get_text ().strip ()) + # lilypond seems to use the opposite ordering than MusicXML... + tunings.reverse () + + return tunings + + +def staff_attributes_to_lily_staff (mxl_attr): + if not mxl_attr: + return musicexp.Staff () + + (staff_id, attributes) = mxl_attr.items ()[0] + + # distinguish by clef: + # percussion (percussion and rhythmic), tab, and everything else + clef_sign = None + clef = attributes.get_maybe_exist_named_child ('clef') + if clef: + sign = clef.get_maybe_exist_named_child ('sign') + if sign: + clef_sign = {"percussion": "percussion", "TAB": "tab"}.get (sign.get_text (), None) + + lines = 5 + details = attributes.get_named_children ('staff-details') + for d in details: + staff_lines = d.get_maybe_exist_named_child ('staff-lines') + if staff_lines: + lines = string.atoi (staff_lines.get_text ()) + + staff = None + if clef_sign == "percussion" and lines == 1: + staff = musicexp.RhythmicStaff () + elif clef_sign == "percussion": + staff = musicexp.DrumStaff () + # staff.drum_style_table = ??? + elif clef_sign == "tab": + staff = musicexp.TabStaff () + staff.string_tunings = staff_attributes_to_string_tunings (attributes) + # staff.tablature_format = ??? + else: + # TODO: Handle case with lines <> 5! + staff = musicexp.Staff () + + return staff + -def extract_score_layout (part_list): - layout = musicexp.StaffGroup (None) +def extract_score_structure (part_list, staffinfo): + structure = musicexp.StaffGroup (None) if not part_list: - return layout + return structure def read_score_part (el): if not isinstance (el, musicxml.Score_part): return - staff = musicexp.Staff () + # Depending on the attributes of the first measure, we create different + # types of staves (Staff, RhythmicStaff, DrumStaff, TabStaff, etc.) + staff = staff_attributes_to_lily_staff (staffinfo.get (el.id, None)) + if not staff: + return None staff.id = el.id partname = el.get_maybe_exist_named_child ('part-name') # Finale gives unnamed parts the name "MusicXML Part" automatically! @@ -132,7 +351,9 @@ def extract_score_layout (part_list): if not group_info.is_empty (): staves.append (group_info) group_info = PartGroupInfo () - staves.append (read_score_part (el)) + staff = read_score_part (el) + if staff: + staves.append (staff) elif isinstance (el, musicxml.Part_group): if el.type == "start": group_info.add_start (el) @@ -201,8 +422,8 @@ def extract_score_layout (part_list): if len (staves) == 1: return staves[0] for i in staves: - layout.append_staff (i) - return layout + structure.append_staff (i) + return structure @@ -225,8 +446,8 @@ def rational_to_lily_duration (rational_len): d.duration_log = {1: 0, 2: 1, 4:2, 8:3, 16:4, 32:5, 64:6, 128:7, 256:8, 512:9}.get (rational_len.denominator (), -1) d.factor = Rational (rational_len.numerator ()) if d.duration_log < 0: - error_message ("Encountered rational duration with denominator %s, " - "unable to convert to lilypond duration" % + error_message (_ ("Encountered rational duration with denominator %s, " + "unable to convert to lilypond duration") % rational_len.denominator ()) # TODO: Test the above error message return None @@ -408,7 +629,7 @@ def musicxml_key_to_lily (attributes): start_pitch.step = n start_pitch.alteration = a except KeyError: - error_message ('unknown mode %s' % mode) + error_message (_ ("unknown mode %s, expecting 'major' or 'minor'") % mode) fifth = musicexp.Pitch() fifth.step = 4 @@ -446,7 +667,7 @@ class Marker (musicexp.Music): self.direction = 0 self.event = None def print_ly (self, printer): - sys.stderr.write ("Encountered unprocessed marker %s\n" % self) + ly.stderr_write (_ ("Encountered unprocessed marker %s\n") % self) pass def ly_expression (self): return "" @@ -506,13 +727,21 @@ def musicxml_barline_to_lily (barline): return retval.values () +# Brackets need a special engraver added the Staff context! +def musicxml_bracket_to_ly (): + layout_information.set_context_item ('Staff', '\consists "Horizontal_bracket_engraver" % for \\startGroup and \\stopGroup brackets') + return musicexp.BracketSpannerEvent () + spanner_event_dict = { - 'slur' : musicexp.SlurEvent, 'beam' : musicexp.BeamEvent, + 'dashes' : musicexp.TextSpannerEvent, + 'bracket' : musicxml_bracket_to_ly, 'glissando' : musicexp.GlissandoEvent, + 'octave-shift' : musicexp.OctaveShiftEvent, 'pedal' : musicexp.PedalEvent, + 'slide' : musicexp.GlissandoEvent, + 'slur' : musicexp.SlurEvent, 'wavy-line' : musicexp.TrillSpanEvent, - 'octave-shift' : musicexp.OctaveShiftEvent, 'wedge' : musicexp.HairpinEvent } spanner_type_dict = { @@ -522,6 +751,7 @@ spanner_type_dict = { 'decreschendo': -1, 'diminuendo': -1, 'continue': 0, + 'change': 0, 'up': -1, 'down': -1, 'stop': 1, @@ -536,7 +766,7 @@ def musicxml_spanner_to_lily_event (mxl_event): if func: ev = func() else: - error_message ('unknown span event %s' % mxl_event) + error_message (_ ('unknown span event %s') % mxl_event) type = mxl_event.get_type () @@ -546,7 +776,7 @@ def musicxml_spanner_to_lily_event (mxl_event): if span_direction != None: ev.span_direction = span_direction else: - error_message ('unknown span type %s for %s' % (type, name)) + error_message (_ ('unknown span type %s for %s') % (type, name)) ev.set_span_type (type) ev.line_type = getattr (mxl_event, 'line-type', 'solid') @@ -557,20 +787,47 @@ def musicxml_spanner_to_lily_event (mxl_event): return ev def musicxml_direction_to_indicator (direction): - return { "above": 1, "upright": 1, "below": -1, "downright": -1 }.get (direction, '') + return { "above": 1, "upright": 1, "up": 1, "below": -1, "downright": -1, "down": -1, "inverted": -1 }.get (direction, 0) def musicxml_fermata_to_lily_event (mxl_event): ev = musicexp.ArticulationEvent () - ev.type = "fermata" + txt = mxl_event.get_text () + # The contents of the element defined the shape, possible are normal, angled and square + ev.type = { "angled": "shortfermata", "square": "longfermata" }.get (txt, "fermata") if hasattr (mxl_event, 'type'): dir = musicxml_direction_to_indicator (mxl_event.type) - if dir: + if dir and options.convert_directions: ev.force_direction = dir return ev +def musicxml_arpeggiate_to_lily_event (mxl_event): + ev = musicexp.ArpeggioEvent () + ev.direction = musicxml_direction_to_indicator (getattr (mxl_event, 'direction', None)) + return ev + +def musicxml_nonarpeggiate_to_lily_event (mxl_event): + ev = musicexp.ArpeggioEvent () + ev.non_arpeggiate = True + ev.direction = musicxml_direction_to_indicator (getattr (mxl_event, 'direction', None)) + return ev + def musicxml_tremolo_to_lily_event (mxl_event): ev = musicexp.TremoloEvent () - ev.bars = mxl_event.get_text () + txt = mxl_event.get_text () + if txt: + ev.bars = txt + else: + ev.bars = "3" + return ev + +def musicxml_falloff_to_lily_event (mxl_event): + ev = musicexp.BendEvent () + ev.alter = -4 + return ev + +def musicxml_doit_to_lily_event (mxl_event): + ev = musicexp.BendEvent () + ev.alter = 4 return ev def musicxml_bend_to_lily_event (mxl_event): @@ -578,12 +835,24 @@ def musicxml_bend_to_lily_event (mxl_event): ev.alter = mxl_event.bend_alter () return ev +def musicxml_caesura_to_lily_event (mxl_event): + ev = musicexp.MarkupEvent () + # FIXME: default to straight or curved caesura? + ev.contents = "\\musicglyph #\"scripts.caesura.straight\"" + ev.force_direction = 1 + return ev def musicxml_fingering_event (mxl_event): ev = musicexp.ShortArticulationEvent () ev.type = mxl_event.get_text () return ev +def musicxml_snappizzicato_event (mxl_event): + needed_additional_definitions.append ("snappizzicato") + ev = musicexp.MarkupEvent () + ev.contents = "\\snappizzicato" + return ev + def musicxml_string_event (mxl_event): ev = musicexp.NoDirectionArticulationEvent () ev.type = mxl_event.get_text () @@ -618,17 +887,17 @@ def musicxml_accidental_mark (mxl_event): # -) (class, name) (like string, only that a different class than ArticulationEvent is used) # TODO: Some translations are missing! articulations_dict = { - "accent": (musicexp.ShortArticulationEvent, ">"), + "accent": (musicexp.ShortArticulationEvent, ">"), # or "accent" "accidental-mark": musicxml_accidental_mark, "bend": musicxml_bend_to_lily_event, "breath-mark": (musicexp.NoDirectionArticulationEvent, "breathe"), - #"caesura": "caesura", + "caesura": musicxml_caesura_to_lily_event, #"delayed-turn": "?", - #"detached-legato": "", - #"doit": "", + "detached-legato": (musicexp.ShortArticulationEvent, "_"), # or "portato" + "doit": musicxml_doit_to_lily_event, #"double-tongue": "", "down-bow": "downbow", - #"falloff": "", + "falloff": musicxml_falloff_to_lily_event, "fingering": musicxml_fingering_event, #"fingernails": "", #"fret": "", @@ -638,25 +907,24 @@ articulations_dict = { "inverted-mordent": "prall", "inverted-turn": "reverseturn", "mordent": "mordent", - #"open-string": "", + "open-string": "open", #"plop": "", #"pluck": "", - #"portato": (musicexp.ShortArticulationEvent, "_"), # does not exist in MusicXML #"pull-off": "", #"schleifer": "?", #"scoop": "", #"shake": "?", - #"snap-pizzicato": "", + "snap-pizzicato": musicxml_snappizzicato_event, #"spiccato": "", - "staccatissimo": (musicexp.ShortArticulationEvent, "|"), - "staccato": (musicexp.ShortArticulationEvent, "."), - "stopped": (musicexp.ShortArticulationEvent, "+"), + "staccatissimo": (musicexp.ShortArticulationEvent, "|"), # or "staccatissimo" + "staccato": (musicexp.ShortArticulationEvent, "."), # or "staccato" + "stopped": (musicexp.ShortArticulationEvent, "+"), # or "stopped" #"stress": "", "string": musicxml_string_event, - "strong-accent": (musicexp.ShortArticulationEvent, "^"), + "strong-accent": (musicexp.ShortArticulationEvent, "^"), # or "marcato" #"tap": "", - "tenuto": (musicexp.ShortArticulationEvent, "-"), - #"thumb-position": "", + "tenuto": (musicexp.ShortArticulationEvent, "-"), # or "tenuto" + "thumb-position": "thumb", #"toe": "", "turn": "turn", "tremolo": musicxml_tremolo_to_lily_event, @@ -688,22 +956,35 @@ def musicxml_articulation_to_lily_event (mxl_event): # Some articulations use the type attribute, other the placement... dir = None - if hasattr (mxl_event, 'type'): + if hasattr (mxl_event, 'type') and options.convert_directions: dir = musicxml_direction_to_indicator (mxl_event.type) - if hasattr (mxl_event, 'placement'): + if hasattr (mxl_event, 'placement') and options.convert_directions: dir = musicxml_direction_to_indicator (mxl_event.placement) + if dir: + ev.force_direction = dir return ev + def musicxml_dynamics_to_lily_event (dynentry): - dynamics_available = ( "p", "pp", "ppp", "pppp", "ppppp", "pppppp", - "f", "ff", "fff", "ffff", "fffff", "ffffff", - "mp", "mf", "sf", "sfp", "sfpp", "fp", - "rf", "rfz", "sfz", "sffz", "fz" ) - if not dynentry.get_name() in dynamics_available: + dynamics_available = ( + "ppppp", "pppp", "ppp", "pp", "p", "mp", "mf", + "f", "ff", "fff", "ffff", "fp", "sf", "sff", "sp", "spp", "sfz", "rfz" ) + dynamicsname = dynentry.get_name () + if dynamicsname == "other-dynamics": + dynamicsname = dynentry.get_text () + if not dynamicsname or dynamicsname=="#text": return + + if not dynamicsname in dynamics_available: + # Get rid of - in tag names (illegal in ly tags!) + dynamicstext = dynamicsname + dynamicsname = string.replace (dynamicsname, "-", "") + additional_definitions[dynamicsname] = dynamicsname + \ + " = #(make-dynamic-script \"" + dynamicstext + "\")" + needed_additional_definitions.append (dynamicsname) event = musicexp.DynamicsEvent () - event.type = dynentry.get_name () + event.type = dynamicsname return event # Convert single-color two-byte strings to numbers 0.0 - 1.0 @@ -730,7 +1011,7 @@ def musicxml_words_to_lily_event (words): text = re.sub (' *\n? *$', '', text) event.text = text - if hasattr (words, 'default-y'): + if hasattr (words, 'default-y') and options.convert_directions: offset = getattr (words, 'default-y') try: off = string.atoi (offset) @@ -780,16 +1061,140 @@ def musicxml_words_to_lily_event (words): return event -direction_spanners = [ 'octave-shift', 'pedal', 'wedge' ] +# convert accordion-registration to lilypond. +# Since lilypond does not have any built-in commands, we need to create +# the markup commands manually and define our own variables. +# Idea was taken from: http://lsr.dsi.unimi.it/LSR/Item?id=194 +def musicxml_accordion_to_markup (mxl_event): + commandname = "accReg" + command = "" + + high = mxl_event.get_maybe_exist_named_child ('accordion-high') + if high: + commandname += "H" + command += """\\combine + \\raise #2.5 \\musicglyph #\"accordion.accDot\" + """ + middle = mxl_event.get_maybe_exist_named_child ('accordion-middle') + if middle: + # By default, use one dot (when no or invalid content is given). The + # MusicXML spec is quiet about this case... + txt = 1 + try: + txt = string.atoi (middle.get_text ()) + except ValueError: + pass + if txt == 3: + commandname += "MMM" + command += """\\combine + \\raise #1.5 \\musicglyph #\"accordion.accDot\" + \\combine + \\raise #1.5 \\translate #(cons 1 0) \\musicglyph #\"accordion.accDot\" + \\combine + \\raise #1.5 \\translate #(cons -1 0) \\musicglyph #\"accordion.accDot\" + """ + elif txt == 2: + commandname += "MM" + command += """\\combine + \\raise #1.5 \\translate #(cons 0.5 0) \\musicglyph #\"accordion.accDot\" + \\combine + \\raise #1.5 \\translate #(cons -0.5 0) \\musicglyph #\"accordion.accDot\" + """ + elif not txt <= 0: + commandname += "M" + command += """\\combine + \\raise #1.5 \\musicglyph #\"accordion.accDot\" + """ + low = mxl_event.get_maybe_exist_named_child ('accordion-low') + if low: + commandname += "L" + command += """\\combine + \\raise #0.5 \musicglyph #\"accordion.accDot\" + """ + + command += "\musicglyph #\"accordion.accDiscant\"" + command = "\\markup { \\normalsize %s }" % command + # Define the newly built command \accReg[H][MMM][L] + additional_definitions[commandname] = "%s = %s" % (commandname, command) + needed_additional_definitions.append (commandname) + return "\\%s" % commandname + +def musicxml_accordion_to_ly (mxl_event): + txt = musicxml_accordion_to_markup (mxl_event) + if txt: + ev = musicexp.MarkEvent (txt) + return ev + return + + +def musicxml_rehearsal_to_ly_mark (mxl_event): + text = mxl_event.get_text () + if not text: + return + # default is boxed rehearsal marks! + encl = "box" + if hasattr (mxl_event, 'enclosure'): + encl = {"none": None, "square": "box", "circle": "circle" }.get (mxl_event.enclosure, None) + if encl: + text = "\\%s { %s }" % (encl, text) + ev = musicexp.MarkEvent ("\\markup { %s }" % text) + return ev + +# translate directions into Events, possible values: +# -) string (MarkEvent with that command) +# -) function (function(mxl_event) needs to return a full Event-derived object +# -) (class, name) (like string, only that a different class than MarkEvent is used) +directions_dict = { + 'accordion-registration' : musicxml_accordion_to_ly, + 'coda' : (musicexp.MusicGlyphMarkEvent, "coda"), +# 'damp' : ??? +# 'damp-all' : ??? +# 'eyeglasses': ?????? +# 'harp-pedals' : +# 'image' : +# 'metronome' : + 'rehearsal' : musicxml_rehearsal_to_ly_mark, +# 'scordatura' : + 'segno' : (musicexp.MusicGlyphMarkEvent, "segno"), + 'words' : musicxml_words_to_lily_event, +} +directions_spanners = [ 'octave-shift', 'pedal', 'wedge', 'dashes', 'bracket' ] def musicxml_direction_to_lily (n): # TODO: Handle the element! res = [] + # placement applies to all children! + dir = None + if hasattr (n, 'placement') and options.convert_directions: + dir = musicxml_direction_to_indicator (n.placement) dirtype_children = [] + # TODO: The direction-type is used for grouping (e.g. dynamics with text), + # so we can't simply flatten them out! for dt in n.get_typed_children (musicxml.DirType): dirtype_children += dt.get_all_children () for entry in dirtype_children: + # backets, dashes, octave shifts. pedal marks, hairpins etc. are spanners: + if entry.get_name() in directions_spanners: + event = musicxml_spanner_to_lily_event (entry) + if event: + res.append (event) + continue + + # now treat all the "simple" ones, that can be translated using the dict + ev = None + tmp_tp = directions_dict.get (entry.get_name (), None) + if isinstance (tmp_tp, str): # string means MarkEvent + ev = musicexp.MarkEvent (tmp_tp) + elif isinstance (tmp_tp, tuple): # tuple means (EventClass, "text") + ev = tmp_tp[0] (tmp_tp[1]) + elif tmp_tp: + ev = tmp_tp (entry) + if ev: + # TODO: set the correct direction! Unfortunately, \mark in ly does + # not seem to support directions! + res.append (ev) + continue if entry.get_name () == "dynamics": for dynentry in entry.get_all_children (): @@ -797,18 +1202,6 @@ def musicxml_direction_to_lily (n): if ev: res.append (ev) - if entry.get_name () == "words": - ev = musicxml_words_to_lily_event (entry) - if ev: - res.append (ev) - - # octave shifts. pedal marks, hairpins etc. are spanners: - if entry.get_name() in direction_spanners: - event = musicxml_spanner_to_lily_event (entry) - if event: - res.append (event) - - return res def musicxml_frame_to_lily_event (frame): @@ -857,39 +1250,49 @@ instrument_drumtype_dict = { def musicxml_note_to_lily_main_event (n): pitch = None duration = None - - mxl_pitch = n.get_maybe_exist_typed_child (musicxml.Pitch) event = None + + mxl_pitch = n.get_maybe_exist_typed_child (musicxml.Pitch) if mxl_pitch: pitch = musicxml_pitch_to_lily (mxl_pitch) - event = musicexp.NoteEvent() + event = musicexp.NoteEvent () event.pitch = pitch acc = n.get_maybe_exist_named_child ('accidental') if acc: # let's not force accs everywhere. event.cautionary = acc.editorial + + elif n.get_maybe_exist_typed_child (musicxml.Unpitched): + # Unpitched elements have display-step and can also have + # display-octave. + unpitched = n.get_maybe_exist_typed_child (musicxml.Unpitched) + event = musicexp.NoteEvent () + event.pitch = musicxml_unpitched_to_lily (unpitched) elif n.get_maybe_exist_typed_child (musicxml.Rest): # rests can have display-octave and display-step, which are # treated like an ordinary note pitch rest = n.get_maybe_exist_typed_child (musicxml.Rest) - event = musicexp.RestEvent() + event = musicexp.RestEvent () pitch = musicxml_restdisplay_to_lily (rest) event.pitch = pitch + elif n.instrument_name: event = musicexp.NoteEvent () drum_type = instrument_drumtype_dict.get (n.instrument_name) if drum_type: event.drum_type = drum_type else: - n.message ("drum %s type unknown, please add to instrument_drumtype_dict" % n.instrument_name) + n.message (_ ("drum %s type unknown, please add to instrument_drumtype_dict") % n.instrument_name) event.drum_type = 'acousticsnare' - - if not event: - n.message ("cannot find suitable event") - event.duration = musicxml_duration_to_lily (n) + else: + n.message (_ ("cannot find suitable event")) + + if event: + event.duration = musicxml_duration_to_lily (n) + return event @@ -936,7 +1339,7 @@ class LilyPondVoiceBuilder: self.set_duration (duration) # Insert all pending dynamics right after the note/rest: - if isinstance (music, musicexp.EventChord) and self.pending_dynamics: + if isinstance (music, musicexp.ChordEvent) and self.pending_dynamics: for d in self.pending_dynamics: music.append (d) self.pending_dynamics = [] @@ -968,15 +1371,29 @@ class LilyPondVoiceBuilder: diff = moment - current_end if diff < Rational (0): - error_message ('Negative skip %s' % diff) + error_message (_ ('Negative skip %s') % diff) diff = Rational (0) if diff > Rational (0) and not (self.ignore_skips and moment == 0): skip = musicexp.SkipEvent() - skip.duration.duration_log = 0 - skip.duration.factor = diff - - evc = musicexp.EventChord () + duration_factor = 1 + duration_log = {1: 0, 2: 1, 4:2, 8:3, 16:4, 32:5, 64:6, 128:7, 256:8, 512:9}.get (diff.denominator (), -1) + duration_dots = 0 + if duration_log > 0: # denominator is a power of 2... + if diff.numerator () == 3: + duration_log -= 1 + duration_dots = 1 + else: + duration_factor = Rational (diff.numerator ()) + else: + # for skips of a whole or more, simply use s1*factor + duration_log = 0 + duration_factor = diff + skip.duration.duration_log = duration_log + skip.duration.factor = duration_factor + skip.duration.dots = duration_dots + + evc = musicexp.ChordEvent () evc.elements.append (skip) self.add_music (evc, diff) @@ -987,16 +1404,16 @@ class LilyPondVoiceBuilder: value = None - # if the position matches, find the last EventChord, do not cross a bar line! + # if the position matches, find the last ChordEvent, do not cross a bar line! at = len( self.elements ) - 1 while (at >= 0 and - not isinstance (self.elements[at], musicexp.EventChord) and + not isinstance (self.elements[at], musicexp.ChordEvent) and not isinstance (self.elements[at], musicexp.BarLine)): at -= 1 if (self.elements and at >= 0 - and isinstance (self.elements[at], musicexp.EventChord) + and isinstance (self.elements[at], musicexp.ChordEvent) and self.begin_moment == starting_at): value = self.elements[at] else: @@ -1007,7 +1424,7 @@ class LilyPondVoiceBuilder: def correct_negative_skip (self, goto): self.end_moment = goto self.begin_moment = goto - evc = musicexp.EventChord () + evc = musicexp.ChordEvent () self.elements.append (evc) @@ -1018,24 +1435,34 @@ class VoiceData: self.lyrics_dict = {} self.lyrics_order = [] +def musicxml_step_to_lily (step): + if step: + return (ord (step) - ord ('A') + 7 - 2) % 7 + else: + return None + def musicxml_voice_to_lily_voice (voice): tuplet_events = [] modes_found = {} lyrics = {} return_value = VoiceData () return_value.voicedata = voice + + # First pitch needed for relative mode (if selected in command-line options) + first_pitch = None # Needed for melismata detection (ignore lyrics on those notes!): inside_slur = False is_tied = False is_chord = False + is_beamed = False ignore_lyrics = False current_staff = None - # TODO: Make sure that the keys in the dict don't get reordered, since - # we need the correct ordering of the lyrics stanzas! By default, - # a dict will reorder its keys + # Make sure that the keys in the dict don't get reordered, since + # we need the correct ordering of the lyrics stanzas! By default, + # a dict will reorder its keys return_value.lyrics_order = voice.get_lyrics_numbers () for k in return_value.lyrics_order: lyrics[k] = [] @@ -1080,7 +1507,7 @@ def musicxml_voice_to_lily_voice (voice): voice_builder.jumpto (n._when) except NegativeSkip, neg: voice_builder.correct_negative_skip (n._when) - n.message ("Negative skip? from %s to %s, diff %s" % (neg.here, neg.dest, neg.dest - neg.here)) + n.message (_ ("Negative skip found: from %s to %s, difference is %s") % (neg.here, neg.dest, neg.dest - neg.here)) if isinstance (n, musicxml.Attributes): if n.is_first () and n._measure_position == Rational (0): @@ -1105,7 +1532,7 @@ def musicxml_voice_to_lily_voice (voice): continue if not n.__class__.__name__ == 'Note': - error_message ('not a Note or Attributes? %s' % n) + error_message (_ ('unexpected %s; expected %s or %s or %s') % (n, 'Note', 'Attributes', 'Barline')) continue rest = n.get_maybe_exist_typed_child (musicxml.Rest) @@ -1124,15 +1551,17 @@ def musicxml_voice_to_lily_voice (voice): voice_builder.add_bar_check (num) main_event = musicxml_note_to_lily_main_event (n) - ignore_lyrics = inside_slur or is_tied or is_chord + if main_event and not first_pitch: + first_pitch = main_event.pitch + # ignore lyrics for notes inside a slur, tie, chord or beam + ignore_lyrics = inside_slur or is_tied or is_chord or is_beamed - if hasattr (main_event, 'drum_type') and main_event.drum_type: + if main_event and hasattr (main_event, 'drum_type') and main_event.drum_type: modes_found['drummode'] = True - ev_chord = voice_builder.last_event_chord (n._when) if not ev_chord: - ev_chord = musicexp.EventChord() + ev_chord = musicexp.ChordEvent() voice_builder.add_music (ev_chord, n._duration) grace = n.get_maybe_exist_typed_child (musicxml.Grace) @@ -1141,14 +1570,12 @@ def musicxml_voice_to_lily_voice (voice): if n.get_maybe_exist_typed_child (musicxml.Chord) and ev_chord.grace_elements: grace_chord = ev_chord.grace_elements.get_last_event_chord () if not grace_chord: - grace_chord = musicexp.EventChord () + grace_chord = musicexp.ChordEvent () ev_chord.append_grace (grace_chord) if hasattr (grace, 'slash'): # TODO: use grace_type = "appoggiatura" for slurred grace notes if grace.slash == "yes": ev_chord.grace_type = "acciaccatura" - elif grace.slash == "no": - ev_chord.grace_type = "grace" # now that we have inserted the chord into the grace music, insert # everything into that chord instead of the ev_chord ev_chord = grace_chord @@ -1162,7 +1589,7 @@ def musicxml_voice_to_lily_voice (voice): if voice_builder.current_duration () == 0 and n._duration > 0: voice_builder.set_duration (n._duration) - notations = n.get_maybe_exist_typed_child (musicxml.Notations) + notations_children = n.get_typed_children (musicxml.Notations) tuplet_event = None span_events = [] @@ -1171,9 +1598,8 @@ def musicxml_voice_to_lily_voice (voice): # ornaments | technical | articulations | dynamics | # +fermata | arpeggiate | non-arpeggiate | # accidental-mark | other-notation - if notations: - if notations.get_tuplet(): - tuplet_event = notations.get_tuplet() + for notations in notations_children: + for tuplet_event in notations.get_tuplets(): mod = n.get_maybe_exist_typed_child (musicxml.Time_modification) frac = (1,1) if mod: @@ -1185,7 +1611,7 @@ def musicxml_voice_to_lily_voice (voice): if s.get_type () in ('start','stop')] if slurs: if len (slurs) > 1: - error_message ('more than 1 slur?') + error_message (_ ('cannot have two simultaneous slurs')) # record the slur status for the next note in the loop if not grace: if slurs[0].get_type () == 'start': @@ -1211,9 +1637,18 @@ def musicxml_voice_to_lily_voice (voice): arpeggiate = notations.get_named_children ('arpeggiate') for a in arpeggiate: - ev_chord.append (musicexp.ArpeggioEvent ()) + ev = musicxml_arpeggiate_to_lily_event (a) + if ev: + ev_chord.append (ev) + + arpeggiate = notations.get_named_children ('non-arpeggiate') + for a in arpeggiate: + ev = musicxml_nonarpeggiate_to_lily_event (a) + if ev: + ev_chord.append (ev) glissandos = notations.get_named_children ('glissando') + glissandos += notations.get_named_children ('slide') for a in glissandos: ev = musicxml_spanner_to_lily_event (a) if ev: @@ -1235,12 +1670,6 @@ def musicxml_voice_to_lily_voice (voice): # shake | wavy-line | mordent | inverted-mordent | # schleifer | tremolo | other-ornament, accidental-mark ornaments = notations.get_named_children ('ornaments') - for a in ornaments: - for ch in a.get_named_children ('tremolo'): - ev = musicxml_tremolo_to_lily_event (ch) - if ev: - ev_chord.append (ev) - ornaments += notations.get_named_children ('articulations') ornaments += notations.get_named_children ('technical') @@ -1257,30 +1686,18 @@ def musicxml_voice_to_lily_voice (voice): if ev: ev_chord.append (ev) - # Extract the lyrics - if not rest and not ignore_lyrics: - note_lyrics_processed = [] - note_lyrics_elements = n.get_typed_children (musicxml.Lyric) - for l in note_lyrics_elements: - if l.get_number () < 0: - for k in lyrics.keys (): - lyrics[k].append (l.lyric_to_text ()) - note_lyrics_processed.append (k) - else: - lyrics[l.number].append(l.lyric_to_text ()) - note_lyrics_processed.append (l.number) - for lnr in lyrics.keys (): - if not lnr in note_lyrics_processed: - lyrics[lnr].append ("\skip4") - mxl_beams = [b for b in n.get_named_children ('beam') if (b.get_type () in ('begin', 'end') and b.is_primary ())] - if mxl_beams: + if mxl_beams and not conversion_settings.ignore_beaming: beam_ev = musicxml_spanner_to_lily_event (mxl_beams[0]) if beam_ev: ev_chord.append (beam_ev) + if beam_ev.span_direction == -1: # beam and thus melisma starts here + is_beamed = True + elif beam_ev.span_direction == 1: # beam and thus melisma ends here + is_beamed = False if tuplet_event: mod = n.get_maybe_exist_typed_child (musicxml.Time_modification) @@ -1290,8 +1707,24 @@ def musicxml_voice_to_lily_voice (voice): tuplet_events.append ((ev_chord, tuplet_event, frac)) + # Extract the lyrics + if not rest and not ignore_lyrics: + note_lyrics_processed = [] + note_lyrics_elements = n.get_typed_children (musicxml.Lyric) + for l in note_lyrics_elements: + if l.get_number () < 0: + for k in lyrics.keys (): + lyrics[k].append (l.lyric_to_text ()) + note_lyrics_processed.append (k) + else: + lyrics[l.number].append(l.lyric_to_text ()) + note_lyrics_processed.append (l.number) + for lnr in lyrics.keys (): + if not lnr in note_lyrics_processed: + lyrics[lnr].append ("\skip4") + ## force trailing mm rests to be written out. - voice_builder.add_music (musicexp.EventChord (), Rational (0)) + voice_builder.add_music (musicexp.ChordEvent (), Rational (0)) ly_voice = group_tuplets (voice_builder.elements, tuplet_events) ly_voice = group_repeats (ly_voice) @@ -1310,7 +1743,13 @@ def musicxml_voice_to_lily_voice (voice): if len (modes_found) > 1: - error_message ('Too many modes found %s' % modes_found.keys ()) + error_message (_ ('cannot simultaneously have more than one mode: %s') % modes_found.keys ()) + + if options.relative: + v = musicexp.RelativeMusic () + v.element = seq_music + v.basepitch = first_pitch + seq_music = v return_value.ly_voice = seq_music for mode in modes_found.keys (): @@ -1321,7 +1760,6 @@ def musicxml_voice_to_lily_voice (voice): return return_value - def musicxml_id_to_lily (id): digits = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten'] @@ -1333,20 +1771,30 @@ def musicxml_id_to_lily (id): id = re.sub ('[^a-zA-Z]', 'X', id) return id - def musicxml_pitch_to_lily (mxl_pitch): - p = musicexp.Pitch() + p = musicexp.Pitch () p.alteration = mxl_pitch.get_alteration () - p.step = (ord (mxl_pitch.get_step ()) - ord ('A') + 7 - 2) % 7 + p.step = musicxml_step_to_lily (mxl_pitch.get_step ()) p.octave = mxl_pitch.get_octave () - 4 return p +def musicxml_unpitched_to_lily (mxl_unpitched): + p = None + step = mxl_unpitched.get_step () + if step: + p = musicexp.Pitch () + p.step = musicxml_step_to_lily (step) + octave = mxl_unpitched.get_octave () + if octave and p: + p.octave = octave - 4 + return p + def musicxml_restdisplay_to_lily (mxl_rest): p = None step = mxl_rest.get_step () if step: - p = musicexp.Pitch() - p.step = (ord (step) - ord ('A') + 7 - 2) % 7 + p = musicexp.Pitch () + p.step = musicxml_step_to_lily (step) octave = mxl_rest.get_octave () if octave and p: p.octave = octave - 4 @@ -1356,99 +1804,142 @@ def voices_in_part (part): """Return a Name -> Voice dictionary for PART""" part.interpret () part.extract_voices () - voice_dict = part.get_voices () + voices = part.get_voices () + part_info = part.get_staff_attributes () - return voice_dict + return (voices, part_info) def voices_in_part_in_parts (parts): """return a Part -> Name -> Voice dictionary""" - return dict([(p, voices_in_part (p)) for p in parts]) + return dict([(p.id, voices_in_part (p)) for p in parts]) def get_all_voices (parts): all_voices = voices_in_part_in_parts (parts) all_ly_voices = {} - for p, name_voice in all_voices.items (): + all_ly_staffinfo = {} + for p, (name_voice, staff_info) in all_voices.items (): part_ly_voices = {} for n, v in name_voice.items (): - progress ("Converting to LilyPond expressions...") + progress (_ ("Converting to LilyPond expressions...")) # musicxml_voice_to_lily_voice returns (lily_voice, {nr->lyrics, nr->lyrics}) part_ly_voices[n] = musicxml_voice_to_lily_voice (v) all_ly_voices[p] = part_ly_voices - - return all_ly_voices + all_ly_staffinfo[p] = staff_info + + return (all_ly_voices, all_ly_staffinfo) def option_parser (): - p = ly.get_option_parser(usage=_ ("musicxml2ly FILE.xml"), - version=('''%prog (LilyPond) @TOPLEVEL_VERSION@\n\n''' + p = ly.get_option_parser (usage = _ ("musicxml2ly [options] FILE.xml"), + description = _ ("Convert %s to LilyPond input.") % 'MusicXML' + "\n", + add_help_option=False) + + p.add_option("-h", "--help", + action="help", + help=_ ("show this help and exit")) + + p.version = ('''%prog (LilyPond) @TOPLEVEL_VERSION@\n\n''' + _ ("""This program is free software. It is covered by the GNU General Public License and you are welcome to change it and/or distribute copies of it under certain conditions. Invoke as `%s --warranty' for more information.""") % 'lilypond' + """ -Copyright (c) 2005--2007 by - Han-Wen Nienhuys and - Jan Nieuwenhuizen -"""), - description=_ ("Convert %s to LilyPond input.") % 'MusicXML' + "\n") +Copyright (c) 2005--2008 by + Han-Wen Nienhuys , + Jan Nieuwenhuizen and + Reinhold Kainhofer +""") + p.add_option("--version", + action="version", + help=_ ("show version number and exit")) + p.add_option ('-v', '--verbose', - action="store_true", - dest='verbose', - help=_ ("be verbose")) + action = "store_true", + dest = 'verbose', + help = _ ("be verbose")) p.add_option ('', '--lxml', - action="store_true", - default=False, - dest="use_lxml", - help=_ ("Use lxml.etree; uses less memory and cpu time.")) - + action = "store_true", + default = False, + dest = "use_lxml", + help = _ ("use lxml.etree; uses less memory and cpu time")) + + p.add_option ('-z', '--compressed', + action = "store_true", + dest = 'compressed', + default = False, + help = _ ("input file is a zip-compressed MusicXML file")) + + p.add_option ('-r', '--relative', + action = "store_true", + default = True, + dest = "relative", + help = _ ("convert pitches in relative mode (default)")) + + p.add_option ('-a', '--absolute', + action = "store_false", + dest = "relative", + help = _ ("convert pitches in absolute mode")) + + p.add_option ('-l', '--language', + metavar = _ ("LANG"), + action = "store", + help = _ ("use a different language file 'LANG.ly' and corresponding pitch names, e.g. 'deutsch' for deutsch.ly")) + + p.add_option ('--nd', '--no-articulation-directions', + action = "store_false", + default = True, + dest = "convert_directions", + help = _ ("do not convert directions (^, _ or -) for articulations, dynamics, etc.")) + + p.add_option ('--no-beaming', + action = "store_false", + default = True, + dest = "convert_beaming", + help = _ ("do not convert beaming information, use lilypond's automatic beaming instead")) + p.add_option ('-o', '--output', - metavar=_ ("FILE"), - action="store", - default=None, - type='string', - dest='output_name', - help=_ ("set output filename to FILE")) - p.add_option_group ('bugs', - description=(_ ("Report bugs via") + metavar = _ ("FILE"), + action = "store", + default = None, + type = 'string', + dest = 'output_name', + help = _ ("set output filename to FILE")) + p.add_option_group (ly.display_encode (_ ('Bugs')), + description = (_ ("Report bugs via") + ''' http://post.gmane.org/post.php''' '''?group=gmane.comp.gnu.lilypond.bugs\n''')) return p -def music_xml_voice_name_to_lily_name (part, name): - str = "Part%sVoice%s" % (part.id, name) +def music_xml_voice_name_to_lily_name (part_id, name): + str = "Part%sVoice%s" % (part_id, name) return musicxml_id_to_lily (str) -def music_xml_lyrics_name_to_lily_name (part, name, lyricsnr): - str = "Part%sVoice%sLyrics%s" % (part.id, name, lyricsnr) +def music_xml_lyrics_name_to_lily_name (part_id, name, lyricsnr): + str = "Part%sVoice%sLyrics%s" % (part_id, name, lyricsnr) return musicxml_id_to_lily (str) def print_voice_definitions (printer, part_list, voices): - part_dict={} - for (part, nv_dict) in voices.items(): - part_dict[part.id] = (part, nv_dict) - for part in part_list: - (p, nv_dict) = part_dict.get (part.id, (None, {})) - #for (name, ((voice, lyrics), mxlvoice)) in nv_dict.items (): + part_id = part.id + nv_dict = voices.get (part_id, {}) for (name, voice) in nv_dict.items (): - k = music_xml_voice_name_to_lily_name (p, name) + k = music_xml_voice_name_to_lily_name (part_id, name) printer.dump ('%s = ' % k) voice.ly_voice.print_ly (printer) printer.newline() - for l in voice.lyrics_order: - lname = music_xml_lyrics_name_to_lily_name (p, name, l) + lname = music_xml_lyrics_name_to_lily_name (part_id, name, l) printer.dump ('%s = ' %lname ) voice.lyrics_dict[l].print_ly (printer) printer.newline() - + def uniq_list (l): return dict ([(elt,1) for elt in l]).keys () @@ -1461,27 +1952,24 @@ def uniq_list (l): # ] # ] # raw_voices is of the form [(voicename, lyricsids)*] -def format_staff_info (part, staff_id, raw_voices): +def format_staff_info (part_id, staff_id, raw_voices): voices = [] for (v, lyricsids) in raw_voices: - voice_name = music_xml_voice_name_to_lily_name (part, v) - voice_lyrics = [music_xml_lyrics_name_to_lily_name (part, v, l) + voice_name = music_xml_voice_name_to_lily_name (part_id, v) + voice_lyrics = [music_xml_lyrics_name_to_lily_name (part_id, v, l) for l in lyricsids] voices.append ([voice_name, voice_lyrics]) return [staff_id, voices] def update_score_setup (score_structure, part_list, voices): - part_dict = dict ([(p.id, p) for p in voices.keys ()]) - final_part_dict = {} for part_definition in part_list: - part_name = part_definition.id - part = part_dict.get (part_name) - if not part: - error_message ('unknown part in part-list: %s' % part_name) + part_id = part_definition.id + nv_dict = voices.get (part_id) + if not nv_dict: + error_message (_ ('unknown part in part-list: %s') % part_id) continue - nv_dict = voices.get (part) staves = reduce (lambda x,y: x+ y, [voice.voicedata._staves.keys () for voice in nv_dict.values ()], @@ -1492,54 +1980,99 @@ def update_score_setup (score_structure, part_list, voices): staves = uniq_list (staves) staves.sort () for s in staves: - #((music, lyrics), mxlvoice)) thisstaff_raw_voices = [(voice_name, voice.lyrics_order) for (voice_name, voice) in nv_dict.items () if voice.voicedata._start_staff == s] - staves_info.append (format_staff_info (part, s, thisstaff_raw_voices)) + staves_info.append (format_staff_info (part_id, s, thisstaff_raw_voices)) else: thisstaff_raw_voices = [(voice_name, voice.lyrics_order) for (voice_name, voice) in nv_dict.items ()] - staves_info.append (format_staff_info (part, None, thisstaff_raw_voices)) - score_structure.set_part_information (part_name, staves_info) + staves_info.append (format_staff_info (part_id, None, thisstaff_raw_voices)) + score_structure.set_part_information (part_id, staves_info) + +# Set global values in the \layout block, like auto-beaming etc. +def update_layout_information (): + if not conversion_settings.ignore_beaming and layout_information: + layout_information.set_context_item ('Score', 'autoBeaming = ##f') def print_ly_preamble (printer, filename): printer.dump_version () printer.print_verbatim ('%% automatically converted from %s\n' % filename) -def read_musicxml (filename, use_lxml): +def print_ly_additional_definitions (printer, filename): + if needed_additional_definitions: + printer.newline () + printer.print_verbatim ('%% additional definitions required by the score:') + printer.newline () + for a in set(needed_additional_definitions): + printer.print_verbatim (additional_definitions.get (a, '')) + printer.newline () + printer.newline () + +# Read in the tree from the given I/O object (either file or string) and +# demarshall it using the classes from the musicxml.py file +def read_xml (io_object, use_lxml): if use_lxml: import lxml.etree - - tree = lxml.etree.parse (filename) + tree = lxml.etree.parse (io_object) mxl_tree = musicxml.lxml_demarshal_node (tree.getroot ()) return mxl_tree else: from xml.dom import minidom, Node - - doc = minidom.parse(filename) + doc = minidom.parse(io_object) node = doc.documentElement return musicxml.minidom_demarshal_node (node) - return None +def read_musicxml (filename, compressed, use_lxml): + raw_string = None + if compressed: + progress (_ ("Input file %s is compressed, extracting raw MusicXML data") % filename) + z = zipfile.ZipFile (filename, "r") + container_xml = z.read ("META-INF/container.xml") + if not container_xml: + return None + container = read_xml (StringIO.StringIO (container_xml), use_lxml) + if not container: + return None + rootfiles = container.get_maybe_exist_named_child ('rootfiles') + if not rootfiles: + return None + rootfile_list = rootfiles.get_named_children ('rootfile') + mxml_file = None + if len (rootfile_list) > 0: + mxml_file = getattr (rootfile_list[0], 'full-path', None) + if mxml_file: + raw_string = z.read (mxml_file) + + io_object = filename + if raw_string: + io_object = StringIO.StringIO (raw_string) + + return read_xml (io_object, use_lxml) + + def convert (filename, options): - progress ("Reading MusicXML from %s ..." % filename) - - tree = read_musicxml (filename, options.use_lxml) + progress (_ ("Reading MusicXML from %s ...") % filename) + + tree = read_musicxml (filename, options.compressed, options.use_lxml) + score_information = extract_score_information (tree) + paper_information = extract_paper_information (tree) + + parts = tree.get_typed_children (musicxml.Part) + (voices, staff_info) = get_all_voices (parts) score_structure = None mxl_pl = tree.get_maybe_exist_typed_child (musicxml.Part_list) if mxl_pl: - score_structure = extract_score_layout (mxl_pl) + score_structure = extract_score_structure (mxl_pl, staff_info) part_list = mxl_pl.get_named_children ("score-part") # score information is contained in the , or tags - score_information = extract_score_information (tree) - parts = tree.get_typed_children (musicxml.Part) - voices = get_all_voices (parts) update_score_setup (score_structure, part_list, voices) + # After the conversion, update the list of settings for the \layout block + update_layout_information () if not options.output_name: options.output_name = os.path.basename (filename) @@ -1552,17 +2085,23 @@ def convert (filename, options): driver_ly_name = options.output_name + '.ly' printer = musicexp.Output_printer() - progress ("Output to `%s'" % defs_ly_name) + progress (_ ("Output to `%s'") % defs_ly_name) printer.set_file (codecs.open (defs_ly_name, 'wb', encoding='utf-8')) print_ly_preamble (printer, filename) - score_information.print_ly (printer) + print_ly_additional_definitions (printer, filename) + if score_information: + score_information.print_ly (printer) + if paper_information: + paper_information.print_ly (printer) + if layout_information: + layout_information.print_ly (printer) print_voice_definitions (printer, part_list, voices) printer.close () - progress ("Output to `%s'" % driver_ly_name) + progress (_ ("Output to `%s'") % driver_ly_name) printer = musicexp.Output_printer() printer.set_file (codecs.open (driver_ly_name, 'wb', encoding='utf-8')) print_ly_preamble (printer, filename) @@ -1575,10 +2114,10 @@ def convert (filename, options): def get_existing_filename_with_extension (filename, ext): if os.path.exists (filename): return filename - newfilename = filename + ".xml" + newfilename = filename + "." + ext if os.path.exists (newfilename): return newfilename; - newfilename = filename + "xml" + newfilename = filename + ext if os.path.exists (newfilename): return newfilename; return '' @@ -1586,17 +2125,27 @@ def get_existing_filename_with_extension (filename, ext): def main (): opt_parser = option_parser() + global options (options, args) = opt_parser.parse_args () if not args: opt_parser.print_usage() sys.exit (2) - + + if options.language: + musicexp.set_pitch_language (options.language) + needed_additional_definitions.append (options.language) + additional_definitions[options.language] = "\\include \"%s.ly\"\n" % options.language + conversion_settings.ignore_beaming = not options.convert_beaming + # Allow the user to leave out the .xml or xml on the filename filename = get_existing_filename_with_extension (args[0], "xml") + if not filename: + filename = get_existing_filename_with_extension (args[0], "mxl") + options.compressed = True if filename and os.path.exists (filename): voices = convert (filename, options) else: - progress ("Unable to find input file %s" % args[0]) + progress (_ ("Unable to find input file %s") % args[0]) if __name__ == '__main__': main()