X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=scripts%2Fmusicxml2ly.py;h=dca950e8f7ae2a062eeb7ae05394807f18f4bfb5;hb=2ecafdd9071bd60aa13442f63aa0c4df04873cdd;hp=cf0318434af1cc8168f3145090bf428fcfd18941;hpb=8de1c936871ce52ca502ca8a49ecc6343bef7f40;p=lilypond.git diff --git a/scripts/musicxml2ly.py b/scripts/musicxml2ly.py index cf0318434a..dca950e8f7 100644 --- a/scripts/musicxml2ly.py +++ b/scripts/musicxml2ly.py @@ -1,5 +1,5 @@ #!@TARGET_PYTHON@ - +# -*- coding: utf-8 -*- import optparse import sys import re @@ -8,13 +8,13 @@ import string import codecs import zipfile import StringIO -from gettext import gettext as _ """ @relocate-preamble@ """ import lilylib as ly +_ = ly._ import musicxml import musicexp @@ -24,6 +24,16 @@ 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): ly.stderr_write (str + '\n') sys.stderr.flush () @@ -34,6 +44,7 @@ def error_message (str): needed_additional_definitions = [] additional_definitions = { + "snappizzicato": """#(define-markup-command (snappizzicato layout props) () (interpret-markup layout props (markup #:stencil @@ -42,20 +53,60 @@ additional_definitions = { (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 + '(-0.1 . 0.1) '(0.1 . 1))) + 0.7 X))))""", + + "eyeglasses": """eyeglassesps = #"0.15 setlinewidth + -0.9 0 translate + 1.1 1.1 scale + 1.2 0.7 moveto + 0.7 0.7 0.5 0 361 arc + stroke + 2.20 0.70 0.50 0 361 arc + stroke + 1.45 0.85 0.30 0 180 arc + stroke + 0.20 0.70 moveto + 0.80 2.00 lineto + 0.92 2.26 1.30 2.40 1.15 1.70 curveto + stroke + 2.70 0.70 moveto + 3.30 2.00 lineto + 3.42 2.26 3.80 2.40 3.65 1.70 curveto + stroke" +eyeglasses = \markup { \with-dimensions #'(0 . 4.4) #'(0 . 2.5) \postscript #eyeglassesps }""", + + "tuplet-note-wrapper": """ % a formatter function, which is simply a wrapper around an existing + % tuplet formatter function. It takes the value returned by the given + % function and appends a note of given length. + #(define-public ((tuplet-number::append-note-wrapper function note) grob) + (let* ((txt (if function (function grob) #f))) + (if txt + (markup txt #:fontsize -5 #:note note UP) + (markup #:fontsize -5 #:note note UP) ) ) - ) -)""" + )""", + + "tuplet-non-default-denominator": """#(define ((tuplet-number::non-default-tuplet-denominator-text denominator) grob) + (number->string (if denominator + denominator + (ly:event-property (event-cause grob) 'denominator)))) +""", + + "tuplet-non-default-fraction": """#(define ((tuplet-number::non-default-tuplet-fraction-text denominator numerator) grob) + (let* ((ev (event-cause grob)) + (den (if denominator denominator (ly:event-property ev 'denominator))) + (num (if numerator numerator (ly:event-property ev 'numerator)))) + (format "~a:~a" den num))) +""" + } def round_to_two_digits (val): return round (val * 100) / 100 -def extract_layout_information (tree): +def extract_paper_information (tree): paper = musicexp.Paper () defaults = tree.get_maybe_exist_named_child ('defaults') if not defaults: @@ -176,6 +227,24 @@ def extract_score_information (tree): set_if_exists ('encodingdate', ids.get_encoding_date ()) set_if_exists ('encoder', ids.get_encoding_person ()) set_if_exists ('encodingdescription', ids.get_encoding_description ()) + + set_if_exists ('texidoc', ids.get_file_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")) + if "Noteworthy Composer" in software: + conversion_settings.ignore_beaming = True + progress (_ ("Encountered file created by Noteworthy Composer's nwc2xml, containing wrong beaming information. All beaming information in the MusicXML file will be ignored")) + # TODO: Check for other unsupported features return header @@ -190,9 +259,9 @@ 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): @@ -248,9 +317,9 @@ def staff_attributes_to_lily_staff (mxl_attr): clef_sign = {"percussion": "percussion", "TAB": "tab"}.get (sign.get_text (), None) lines = 5 - details = attributes.get_maybe_exist_named_child ('staff-details') - if details: - staff_lines = details.get_maybe_exist_named_child ('staff-lines') + 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 ()) @@ -271,10 +340,13 @@ def staff_attributes_to_lily_staff (mxl_attr): return staff -def extract_score_layout (part_list, staffinfo): - layout = musicexp.StaffGroup (None) +def extract_score_structure (part_list, staffinfo): + score = musicexp.Score () + structure = musicexp.StaffGroup (None) + score.set_contents (structure) + if not part_list: - return layout + return structure def read_score_part (el): if not isinstance (el, musicxml.Score_part): @@ -380,8 +452,6 @@ def extract_score_layout (part_list, staffinfo): del staves[pos] # replace the staves with the whole group for j in staves[(prev_start + 1):pos]: - if j.is_group: - j.stafftype = "InnerStaffGroup" group.append_staff (j) del staves[(prev_start + 1):pos] staves.insert (prev_start + 1, group) @@ -399,37 +469,54 @@ def extract_score_layout (part_list, staffinfo): if len (staves) == 1: return staves[0] for i in staves: - layout.append_staff (i) - return layout - + structure.append_staff (i) + return score def musicxml_duration_to_lily (mxl_note): - d = musicexp.Duration () - # if the note has no Type child, then that method spits out a warning and - # returns 0, i.e. a whole note - d.duration_log = mxl_note.get_duration_log () + # if the note has no Type child, then that method returns None. In that case, + # use the tag instead. If that doesn't exist, either -> Error + dur = mxl_note.get_duration_info () + if dur: + d = musicexp.Duration () + d.duration_log = dur[0] + d.dots = dur[1] + # Grace notes by specification have duration 0, so no time modification + # factor is possible. It even messes up the output with *0/1 + if not mxl_note.get_maybe_exist_typed_child (musicxml.Grace): + d.factor = mxl_note._duration / d.get_length () + return d - d.dots = len (mxl_note.get_typed_children (musicxml.Dot)) - # Grace notes by specification have duration 0, so no time modification - # factor is possible. It even messes up the output with *0/1 - if not mxl_note.get_maybe_exist_typed_child (musicxml.Grace): - d.factor = mxl_note._duration / d.get_length () + else: + if mxl_note._duration > 0: + return rational_to_lily_duration (mxl_note._duration) + else: + mxl_note.message (_ ("Encountered note at %s without type and duration (=%s)") % (mxl_note.start, mxl_note._duration) ) + return None - return d def rational_to_lily_duration (rational_len): d = musicexp.Duration () - 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" % + + rational_len.normalize_self () + d_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) + + # Duration of the form 1/2^n or 3/2^n can be converted to a simple lilypond duration + if (d_log >= 0 and rational_len.numerator() in (1,3,5,7) ): + # account for the dots! + d.dots = (rational_len.numerator()-1)/2 + d.duration_log = d_log - d.dots + elif (d_log >= 0): + d.duration_log = d_log + d.factor = Rational (rational_len.numerator ()) + else: + 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 - else: - return d + + return d def musicxml_partial_to_lily (partial_len): if partial_len > 0: @@ -537,6 +624,62 @@ def group_repeats (music_list): return music_list +# Extract the settings for tuplets from the and the +# elements of the note: +def musicxml_tuplet_to_lily (tuplet_elt, time_modification): + tsm = musicexp.TimeScaledMusic () + fraction = (1,1) + if time_modification: + fraction = time_modification.get_fraction () + tsm.numerator = fraction[0] + tsm.denominator = fraction[1] + + + normal_type = tuplet_elt.get_normal_type () + if not normal_type and time_modification: + normal_type = time_modification.get_normal_type () + if not normal_type and time_modification: + note = time_modification.get_parent () + if note: + normal_type = note.get_duration_info () + if normal_type: + normal_note = musicexp.Duration () + (normal_note.duration_log, normal_note.dots) = normal_type + tsm.normal_type = normal_note + + actual_type = tuplet_elt.get_actual_type () + if actual_type: + actual_note = musicexp.Duration () + (actual_note.duration_log, actual_note.dots) = normal_type + tsm.actual_type = actual_note + + # Obtain non-default nrs of notes from the tuplet object! + tsm.display_numerator = tuplet_elt.get_normal_nr () + tsm.display_denominator = tuplet_elt.get_actual_nr () + + + if hasattr (tuplet_elt, 'bracket') and tuplet_elt.bracket == "no": + tsm.display_bracket = None + elif hasattr (tuplet_elt, 'line-shape') and getattr (tuplet_elt, 'line-shape') == "curved": + tsm.display_bracket = "curved" + else: + tsm.display_bracket = "bracket" + + display_values = {"none": None, "actual": "actual", "both": "both"} + if hasattr (tuplet_elt, "show-number"): + tsm.display_number = display_values.get (getattr (tuplet_elt, "show-number"), "actual") + if tsm.display_number == "actual" and tsm.display_denominator: + needed_additional_definitions.append ("tuplet-non-default-denominator") + elif tsm.display_number == "both" and (tsm.display_numerator or tsm.display_denominator): + needed_additional_definitions.append ("tuplet-non-default-fraction") + + if hasattr (tuplet_elt, "show-type"): + if getattr (tuplet_elt, "show-type") == "actual": + needed_additional_definitions.append ("tuplet-note-wrapper") + tsm.display_type = display_values.get (getattr (tuplet_elt, "show-type"), None) + + return tsm + def group_tuplets (music_list, events): @@ -547,22 +690,32 @@ def group_tuplets (music_list, events): indices = [] + brackets = {} j = 0 - for (ev_chord, tuplet_elt, fraction) in events: + for (ev_chord, tuplet_elt, time_modification) in events: while (j < len (music_list)): if music_list[j] == ev_chord: break j += 1 + nr = 0 + if hasattr (tuplet_elt, 'number'): + nr = getattr (tuplet_elt, 'number') if tuplet_elt.type == 'start': - indices.append ((j, None, fraction)) + tuplet_object = musicxml_tuplet_to_lily (tuplet_elt, time_modification) + tuplet_info = [j, None, tuplet_object] + indices.append (tuplet_info) + brackets[nr] = tuplet_info elif tuplet_elt.type == 'stop': - indices[-1] = (indices[-1][0], j, indices[-1][2]) + bracket_info = brackets.get (nr, None) + if bracket_info: + bracket_info[1] = j # Set the ending position to j + del brackets[nr] new_list = [] last = 0 - for (i1, i2, frac) in indices: - if i1 >= i2: + for (i1, i2, tsm) in indices: + if i1 > i2: continue new_list.extend (music_list[last:i1]) @@ -570,12 +723,8 @@ def group_tuplets (music_list, events): last = i2 + 1 seq.elements = music_list[i1:last] - tsm = musicexp.TimeScaledMusic () tsm.element = seq - tsm.numerator = frac[0] - tsm.denominator = frac[1] - new_list.append (tsm) new_list.extend (music_list[last:]) @@ -588,54 +737,123 @@ def musicxml_clef_to_lily (attributes): return change def musicxml_time_to_lily (attributes): - (beats, type) = attributes.get_time_signature () - + sig = attributes.get_time_signature () + if not sig: + return None change = musicexp.TimeSignatureChange() - change.fraction = (beats, type) - + change.fractions = sig + + time_elm = attributes.get_maybe_exist_named_child ('time') + if time_elm and hasattr (time_elm, 'symbol'): + change.style = { 'single-number': "'single-digit", + 'cut': None, + 'common': None, + 'normal': "'()"}.get (time_elm.symbol, "'()") + else: + change.style = "'()" + + # TODO: Handle senza-misura measures + # TODO: Handle hidden time signatures (print-object="no") + # TODO: What shall we do if the symbol clashes with the sig? e.g. "cut" + # with 3/8 or "single-number" with (2+3)/8 or 3/8+2/4? + return change def musicxml_key_to_lily (attributes): - start_pitch = musicexp.Pitch () - (fifths, mode) = attributes.get_key_signature () - try: - (n,a) = { - 'major' : (0,0), - 'minor' : (5,0), - }[mode] - start_pitch.step = n - start_pitch.alteration = a - except KeyError: - error_message ('unknown mode %s' % mode) - - fifth = musicexp.Pitch() - fifth.step = 4 - if fifths < 0: - fifths *= -1 - fifth.step *= -1 - fifth.normalize () + key_sig = attributes.get_key_signature () + if not key_sig or not (isinstance (key_sig, list) or isinstance (key_sig, tuple)): + error_message (_ ("Unable to extract key signature!")) + return None - for x in range (fifths): - start_pitch = start_pitch.transposed (fifth) + change = musicexp.KeySignatureChange() + + if len (key_sig) == 2 and not isinstance (key_sig[0], list): + # standard key signature, (fifths, mode) + (fifths, mode) = key_sig + change.mode = mode - start_pitch.octave = 0 + start_pitch = musicexp.Pitch () + start_pitch.octave = 0 + try: + (n,a) = { + 'major' : (0,0), + 'minor' : (5,0), + 'ionian' : (0,0), + 'dorian' : (1,0), + 'phrygian' : (2,0), + 'lydian' : (3,0), + 'mixolydian': (4,0), + 'aeolian' : (5,0), + 'locrian' : (6,0), + }[mode] + start_pitch.step = n + start_pitch.alteration = a + except KeyError: + error_message (_ ("unknown mode %s, expecting 'major' or 'minor' " + "or a church mode!") % mode) + + fifth = musicexp.Pitch() + fifth.step = 4 + if fifths < 0: + fifths *= -1 + fifth.step *= -1 + fifth.normalize () + for x in range (fifths): + start_pitch = start_pitch.transposed (fifth) + change.tonic = start_pitch - change = musicexp.KeySignatureChange() - change.mode = mode - change.tonic = start_pitch + else: + # Non-standard key signature of the form [[step,alter<,octave>],...] + change.non_standard_alterations = key_sig return change + +def musicxml_transpose_to_lily (attributes): + transpose = attributes.get_transposition () + if not transpose: + return None + + shift = musicexp.Pitch () + octave_change = transpose.get_maybe_exist_named_child ('octave-change') + if octave_change: + shift.octave = string.atoi (octave_change.get_text ()) + chromatic_shift = string.atoi (transpose.get_named_child ('chromatic').get_text ()) + chromatic_shift_normalized = chromatic_shift % 12; + (shift.step, shift.alteration) = [ + (0,0), (0,1), (1,0), (2,-1), (2,0), + (3,0), (3,1), (4,0), (5,-1), (5,0), + (6,-1), (6,0)][chromatic_shift_normalized]; + shift.octave += (chromatic_shift - chromatic_shift_normalized) / 12 + + diatonic = transpose.get_maybe_exist_named_child ('diatonic') + if diatonic: + diatonic_step = string.atoi (diatonic.get_text ()) % 7 + if diatonic_step != shift.step: + # We got the alter incorrect! + old_semitones = shift.semitones () + shift.step = diatonic_step + new_semitones = shift.semitones () + shift.alteration += old_semitones - new_semitones + + transposition = musicexp.Transposition () + transposition.pitch = musicexp.Pitch ().transposed (shift) + return transposition + + def musicxml_attributes_to_lily (attrs): elts = [] attr_dispatch = { 'clef': musicxml_clef_to_lily, 'time': musicxml_time_to_lily, - 'key': musicxml_key_to_lily + 'key': musicxml_key_to_lily, + 'transpose': musicxml_transpose_to_lily, } for (k, func) in attr_dispatch.items (): children = attrs.get_named_children (k) if children: - elts.append (func (attrs)) + ev = func (attrs) + if ev: + elts.append (ev) return elts @@ -644,7 +862,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 "" @@ -705,12 +923,15 @@ def musicxml_barline_to_lily (barline): return retval.values () spanner_event_dict = { - 'slur' : musicexp.SlurEvent, 'beam' : musicexp.BeamEvent, + 'dashes' : musicexp.TextSpannerEvent, + 'bracket' : musicexp.BracketSpannerEvent, '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 = { @@ -720,6 +941,7 @@ spanner_type_dict = { 'decreschendo': -1, 'diminuendo': -1, 'continue': 0, + 'change': 0, 'up': -1, 'down': -1, 'stop': 1, @@ -734,7 +956,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 () @@ -744,7 +966,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') @@ -755,7 +977,7 @@ def musicxml_spanner_to_lily_event (mxl_event): return ev def musicxml_direction_to_indicator (direction): - return { "above": 1, "upright": 1, "up":1, "below": -1, "downright": -1, "down": -1, "inverted": -1 }.get (direction, 0) + 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 () @@ -773,6 +995,12 @@ def musicxml_arpeggiate_to_lily_event (mxl_event): 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 () txt = mxl_event.get_text () @@ -857,42 +1085,42 @@ articulations_dict = { #"delayed-turn": "?", "detached-legato": (musicexp.ShortArticulationEvent, "_"), # or "portato" "doit": musicxml_doit_to_lily_event, - #"double-tongue": "", + #"double-tongue": "?", "down-bow": "downbow", "falloff": musicxml_falloff_to_lily_event, "fingering": musicxml_fingering_event, - #"fingernails": "", - #"fret": "", - #"hammer-on": "", + #"fingernails": "?", + #"fret": "?", + #"hammer-on": "?", "harmonic": "flageolet", - #"heel": "", + #"heel": "?", "inverted-mordent": "prall", "inverted-turn": "reverseturn", "mordent": "mordent", "open-string": "open", - #"plop": "", - #"pluck": "", - #"pull-off": "", + #"plop": "?", + #"pluck": "?", + #"pull-off": "?", #"schleifer": "?", - #"scoop": "", + #"scoop": "?", #"shake": "?", "snap-pizzicato": musicxml_snappizzicato_event, - #"spiccato": "", + #"spiccato": "?", "staccatissimo": (musicexp.ShortArticulationEvent, "|"), # or "staccatissimo" "staccato": (musicexp.ShortArticulationEvent, "."), # or "staccato" "stopped": (musicexp.ShortArticulationEvent, "+"), # or "stopped" - #"stress": "", + #"stress": "?", "string": musicxml_string_event, "strong-accent": (musicexp.ShortArticulationEvent, "^"), # or "marcato" - #"tap": "", + #"tap": "?", "tenuto": (musicexp.ShortArticulationEvent, "-"), # or "tenuto" "thumb-position": "thumb", - #"toe": "", + #"toe": "?", "turn": "turn", "tremolo": musicxml_tremolo_to_lily_event, "trill-mark": "trill", - #"triple-tongue": "", - #"unstress": "" + #"triple-tongue": "?", + #"unstress": "?" "up-bow": "upbow", #"wavy-line": "?", } @@ -943,7 +1171,7 @@ def musicxml_dynamics_to_lily_event (dynentry): dynamicstext = dynamicsname dynamicsname = string.replace (dynamicsname, "-", "") additional_definitions[dynamicsname] = dynamicsname + \ - "=#(make-dynamic-script \"" + dynamicstext + "\")" + " = #(make-dynamic-script \"" + dynamicstext + "\")" needed_additional_definitions.append (dynamicsname) event = musicexp.DynamicsEvent () event.type = dynamicsname @@ -998,7 +1226,7 @@ def musicxml_words_to_lily_event (words): "medium": '', "large": '\\large', "x-large": '\\huge', - "xx-large": '\\bigger\\huge' + "xx-large": '\\larger\\huge' }.get (size, '') if font_size: event.markup += font_size @@ -1023,16 +1251,210 @@ 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 + +def musicxml_harp_pedals_to_ly (mxl_event): + count = 0 + result = "\\harp-pedal #\"" + for t in mxl_event.get_named_children ('pedal-tuning'): + alter = t.get_named_child ('pedal-alter') + if alter: + val = int (alter.get_text ().strip ()) + result += {1: "v", 0: "-", -1: "^"}.get (val, "") + count += 1 + if count == 3: + result += "|" + ev = musicexp.MarkupEvent () + ev.contents = result + "\"" + return ev + +def musicxml_eyeglasses_to_ly (mxl_event): + needed_additional_definitions.append ("eyeglasses") + return musicexp.MarkEvent ("\\eyeglasses") + +def next_non_hash_index (lst, pos): + pos += 1 + while pos < len (lst) and isinstance (lst[pos], musicxml.Hash_text): + pos += 1 + return pos + +def musicxml_metronome_to_ly (mxl_event): + children = mxl_event.get_all_children () + if not children: + return + + index = -1 + index = next_non_hash_index (children, index) + if isinstance (children[index], musicxml.BeatUnit): + # first form of metronome-mark, using unit and beats/min or other unit + ev = musicexp.TempoMark () + if hasattr (mxl_event, 'parentheses'): + ev.set_parentheses (mxl_event.parentheses == "yes") + + d = musicexp.Duration () + d.duration_log = musicxml.musicxml_duration_to_log (children[index].get_text ()) + index = next_non_hash_index (children, index) + if isinstance (children[index], musicxml.BeatUnitDot): + d.dots = 1 + index = next_non_hash_index (children, index) + ev.set_base_duration (d) + if isinstance (children[index], musicxml.BeatUnit): + # Form "note = newnote" + newd = musicexp.Duration () + newd.duration_log = musicxml.musicxml_duration_to_log (children[index].get_text ()) + index = next_non_hash_index (children, index) + if isinstance (children[index], musicxml.BeatUnitDot): + newd.dots = 1 + index = next_non_hash_index (children, index) + ev.set_new_duration (newd) + elif isinstance (children[index], musicxml.PerMinute): + # Form "note = bpm" + try: + beats = int (children[index].get_text ()) + ev.set_beats_per_minute (beats) + except ValueError: + pass + else: + error_message (_ ("Unknown metronome mark, ignoring")) + return + return ev + else: + #TODO: Implement the other (more complex) way for tempo marks! + error_message (_ ("Metronome marks with complex relations ( in MusicXML) are not yet implemented.")) + return + +# 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': musicxml_eyeglasses_to_ly, + 'harp-pedals' : musicxml_harp_pedals_to_ly, +# 'image' : ??? + 'metronome' : musicxml_metronome_to_ly, + '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 (): @@ -1040,18 +1462,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): @@ -1085,7 +1495,206 @@ def musicxml_harmony_to_lily (n): ev = musicxml_frame_to_lily_event (f) if ev: res.append (ev) + return res + +notehead_styles_dict = { + 'slash': '\'slash', + 'triangle': '\'triangle', + 'diamond': '\'diamond', + 'square': '\'la', # TODO: Proper squared note head + 'cross': None, # TODO: + shaped note head + 'x': '\'cross', + 'circle-x': '\'xcircle', + 'inverted triangle': None, # TODO: Implement + 'arrow down': None, # TODO: Implement + 'arrow up': None, # TODO: Implement + 'slashed': None, # TODO: Implement + 'back slashed': None, # TODO: Implement + 'normal': None, + 'cluster': None, # TODO: Implement + 'none': '#f', + 'do': '\'do', + 're': '\'re', + 'mi': '\'mi', + 'fa': '\'fa', + 'so': None, + 'la': '\'la', + 'ti': '\'ti', + } + +def musicxml_notehead_to_lily (nh): + styles = [] + + # Notehead style + style = notehead_styles_dict.get (nh.get_text ().strip (), None) + style_elm = musicexp.NotestyleEvent () + if style: + style_elm.style = style + if hasattr (nh, 'filled'): + style_elm.filled = (getattr (nh, 'filled') == "yes") + if style_elm.style or (style_elm.filled != None): + styles.append (style_elm) + + # parentheses + if hasattr (nh, 'parentheses') and (nh.parentheses == "yes"): + styles.append (musicexp.ParenthesizeEvent ()) + + return styles + +def musicxml_chordpitch_to_lily (mxl_cpitch): + r = musicexp.ChordPitch () + r.alteration = mxl_cpitch.get_alteration () + r.step = musicxml_step_to_lily (mxl_cpitch.get_step ()) + return r + +chordkind_dict = { + 'major': '5', + 'minor': 'm5', + 'augmented': 'aug5', + 'diminished': 'dim5', + # Sevenths: + 'dominant': '7', + 'major-seventh': 'maj7', + 'minor-seventh': 'm7', + 'diminished-seventh': 'dim7', + 'augmented-seventh': 'aug7', + 'half-diminished': 'dim5m7', + 'major-minor': 'maj7m5', + # Sixths: + 'major-sixth': '6', + 'minor-sixth': 'm6', + # Ninths: + 'dominant-ninth': '9', + 'major-ninth': 'maj9', + 'minor-ninth': 'm9', + # 11ths (usually as the basis for alteration): + 'dominant-11th': '11', + 'major-11th': 'maj11', + 'minor-11th': 'm11', + # 13ths (usually as the basis for alteration): + 'dominant-13th': '13.11', + 'major-13th': 'maj13.11', + 'minor-13th': 'm13', + # Suspended: + 'suspended-second': 'sus2', + 'suspended-fourth': 'sus4', + # Functional sixths: + # TODO + #'Neapolitan': '???', + #'Italian': '???', + #'French': '???', + #'German': '???', + # Other: + #'pedal': '???',(pedal-point bass) + 'power': '5^3', + #'Tristan': '???', + 'other': '1', + 'none': None, +} + +def musicxml_chordkind_to_lily (kind): + res = chordkind_dict.get (kind, None) + # Check for None, since a major chord is converted to '' + if res == None: + error_message (_ ("Unable to convert chord type %s to lilypond.") % kind) + return res + +def musicxml_harmony_to_lily_chordname (n): + res = [] + root = n.get_maybe_exist_named_child ('root') + if root: + ev = musicexp.ChordNameEvent () + ev.root = musicxml_chordpitch_to_lily (root) + kind = n.get_maybe_exist_named_child ('kind') + if kind: + ev.kind = musicxml_chordkind_to_lily (kind.get_text ()) + if not ev.kind: + return res + bass = n.get_maybe_exist_named_child ('bass') + if bass: + ev.bass = musicxml_chordpitch_to_lily (bass) + inversion = n.get_maybe_exist_named_child ('inversion') + if inversion: + # TODO: Lilypond does not support inversions, does it? + + # Mail from Carl Sorensen on lilypond-devel, June 11, 2008: + # 4. LilyPond supports the first inversion in the form of added + # bass notes. So the first inversion of C major would be c:/g. + # To get the second inversion of C major, you would need to do + # e:6-3-^5 or e:m6-^5. However, both of these techniques + # require you to know the chord and calculate either the fifth + # pitch (for the first inversion) or the third pitch (for the + # second inversion) so they may not be helpful for musicxml2ly. + inversion_count = string.atoi (inversion.get_text ()) + if inversion_count == 1: + # TODO: Calculate the bass note for the inversion... + pass + pass + for deg in n.get_named_children ('degree'): + d = musicexp.ChordModification () + d.type = deg.get_type () + d.step = deg.get_value () + d.alteration = deg.get_alter () + ev.add_modification (d) + #TODO: convert the user-symbols attribute: + #major: a triangle, like Unicode 25B3 + #minor: -, like Unicode 002D + #augmented: +, like Unicode 002B + #diminished: (degree), like Unicode 00B0 + #half-diminished: (o with slash), like Unicode 00F8 + if ev and ev.root: + res.append (ev) + + return res + +def musicxml_figured_bass_note_to_lily (n): + res = musicexp.FiguredBassNote () + suffix_dict = { 'sharp' : "+", + 'flat' : "-", + 'natural' : "!", + 'double-sharp' : "++", + 'flat-flat' : "--", + 'sharp-sharp' : "++", + 'slash' : "/" } + prefix = n.get_maybe_exist_named_child ('prefix') + if prefix: + res.set_prefix (suffix_dict.get (prefix.get_text (), "")) + fnumber = n.get_maybe_exist_named_child ('figure-number') + if fnumber: + res.set_number (fnumber.get_text ()) + suffix = n.get_maybe_exist_named_child ('suffix') + if suffix: + res.set_suffix (suffix_dict.get (suffix.get_text (), "")) + if n.get_maybe_exist_named_child ('extend'): + # TODO: Implement extender lines (unfortunately, in lilypond you have + # to use \set useBassFigureExtenders = ##t, which turns them on + # globally, while MusicXML has a property for each note... + # I'm not sure there is a proper way to implement this cleanly + #n.extend + pass + return res + + + +def musicxml_figured_bass_to_lily (n): + if not isinstance (n, musicxml.FiguredBass): + return + res = musicexp.FiguredBassEvent () + for i in n.get_named_children ('figure'): + note = musicxml_figured_bass_note_to_lily (i) + if note: + res.append (note) + dur = n.get_maybe_exist_named_child ('duration') + if dur: + # apply the duration to res + length = Rational(int(dur.get_text()), n._divisions)*Rational(1,4) + res.set_real_duration (length) + duration = rational_to_lily_duration (length) + if duration: + res.set_duration (duration) + if hasattr (n, 'parentheses') and n.parentheses == "yes": + res.set_parentheses (True) return res instrument_drumtype_dict = { @@ -1134,17 +1743,51 @@ def musicxml_note_to_lily_main_event (n): 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' else: - n.message ("cannot find suitable event") + n.message (_ ("cannot find suitable event")) if event: - event.duration = musicxml_duration_to_lily (n) + event.duration = musicxml_duration_to_lily (n) + + noteheads = n.get_named_children ('notehead') + for nh in noteheads: + styles = musicxml_notehead_to_lily (nh) + for s in styles: + event.add_associated_event (s) return event +def musicxml_lyrics_to_text (lyrics): + # TODO: Implement text styles for lyrics syllables + continued = False + text = '' + for e in lyrics.get_all_children (): + if isinstance (e, musicxml.Syllabic): + continued = e.continued () + elif isinstance (e, musicxml.Text): + # We need to convert soft hyphens to -, otherwise the ascii codec as well + # as lilypond will barf on that character + text += string.replace( e.get_text(), u'\xad', '-' ) + elif isinstance (e, musicxml.Elision): + if text: + text += " " + continued = False + + if text == "-" and continued: + return "--" + elif text == "_" and continued: + return "__" + elif continued and text: + return musicxml.escape_ly_output_string (text) + " --" + elif continued: + return "--" + elif text: + return musicxml.escape_ly_output_string (text) + else: + return "" ## TODO class NegativeSkip: @@ -1160,17 +1803,25 @@ class LilyPondVoiceBuilder: self.begin_moment = Rational (0) self.pending_multibar = Rational (0) self.ignore_skips = False + self.has_relevant_elements = False + self.measure_length = Rational (4, 4) def _insert_multibar (self): + layout_information.set_context_item ('Score', 'skipBars = ##t') r = musicexp.MultiMeasureRest () - r.duration = musicexp.Duration() - r.duration.duration_log = 0 - r.duration.factor = self.pending_multibar + lenfrac = self.measure_length + r.duration = rational_to_lily_duration (lenfrac) + r.duration.factor *= self.pending_multibar / lenfrac self.elements.append (r) self.begin_moment = self.end_moment self.end_moment = self.begin_moment + self.pending_multibar self.pending_multibar = Rational (0) - + + def set_measure_length (self, mlen): + if (mlen != self.measure_length) and self.pending_multibar: + self._insert_multibar () + self.measure_length = mlen + def add_multibar_rest (self, duration): self.pending_multibar += duration @@ -1184,6 +1835,7 @@ class LilyPondVoiceBuilder: if self.pending_multibar > Rational (0): self._insert_multibar () + self.has_relevant_elements = True self.elements.append (music) self.begin_moment = self.end_moment self.set_duration (duration) @@ -1199,6 +1851,7 @@ class LilyPondVoiceBuilder: assert isinstance (command, musicexp.Music) if self.pending_multibar > Rational (0): self._insert_multibar () + self.has_relevant_elements = True self.elements.append (command) def add_barline (self, barline): # TODO: Implement merging of default barline and custom bar line @@ -1212,16 +1865,21 @@ class LilyPondVoiceBuilder: self.pending_dynamics.append (dynamic) def add_bar_check (self, number): + # re/store has_relevant_elements, so that a barline alone does not + # trigger output for figured bass, chord names + has_relevant = self.has_relevant_elements b = musicexp.BarLine () b.bar_number = number self.add_barline (b) + self.has_relevant_elements = has_relevant def jumpto (self, moment): current_end = self.end_moment + self.pending_multibar diff = moment - current_end if diff < Rational (0): - error_message ('Negative skip %s' % diff) + error_message (_ ('Negative skip %s (from position %s to %s)') % + (diff, current_end, moment)) diff = Rational (0) if diff > Rational (0) and not (self.ignore_skips and moment == 0): @@ -1229,6 +1887,8 @@ class LilyPondVoiceBuilder: 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 + # TODO: Use the time signature for skips, too. Problem: The skip + # might not start at a measure boundary! if duration_log > 0: # denominator is a power of 2... if diff.numerator () == 3: duration_log -= 1 @@ -1236,6 +1896,7 @@ class LilyPondVoiceBuilder: 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 @@ -1279,8 +1940,11 @@ class LilyPondVoiceBuilder: class VoiceData: def __init__ (self): + self.voicename = None self.voicedata = None self.ly_voice = None + self.figured_bass = None + self.chordnames = None self.lyrics_dict = {} self.lyrics_order = [] @@ -1290,6 +1954,12 @@ def musicxml_step_to_lily (step): else: return None +def measure_length_from_attributes (attr, current_measure_length): + len = attr.get_measure_length () + if not len: + len = current_measure_length + return len + def musicxml_voice_to_lily_voice (voice): tuplet_events = [] modes_found = {} @@ -1304,9 +1974,13 @@ def musicxml_voice_to_lily_voice (voice): inside_slur = False is_tied = False is_chord = False + is_beamed = False ignore_lyrics = False current_staff = None + + pending_figured_bass = [] + pending_chordnames = [] # Make sure that the keys in the dict don't get reordered, since # we need the correct ordering of the lyrics stanzas! By default, @@ -1315,7 +1989,11 @@ def musicxml_voice_to_lily_voice (voice): for k in return_value.lyrics_order: lyrics[k] = [] - voice_builder = LilyPondVoiceBuilder() + voice_builder = LilyPondVoiceBuilder () + figured_bass_builder = LilyPondVoiceBuilder () + chordnames_builder = LilyPondVoiceBuilder () + current_measure_length = Rational (4, 4) + voice_builder.set_measure_length (current_measure_length) for n in voice._elements: if n.get_name () == 'forward': @@ -1333,42 +2011,14 @@ def musicxml_voice_to_lily_voice (voice): voice_builder.add_partial (a) continue - if isinstance (n, musicxml.Direction): - for a in musicxml_direction_to_lily (n): - if a.wait_for_note (): - voice_builder.add_dynamics (a) - else: - voice_builder.add_command (a) - continue - - if isinstance (n, musicxml.Harmony): - for a in musicxml_harmony_to_lily (n): - if a.wait_for_note (): - voice_builder.add_dynamics (a) - else: - voice_builder.add_command (a) - continue - is_chord = n.get_maybe_exist_named_child ('chord') - if not is_chord: + is_after_grace = (isinstance (n, musicxml.Note) and n.is_after_grace ()); + if not is_chord and not is_after_grace: try: 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)) - - if isinstance (n, musicxml.Attributes): - if n.is_first () and n._measure_position == Rational (0): - try: - number = int (n.get_parent ().number) - except ValueError: - number = 0 - if number > 0: - voice_builder.add_bar_check (number) - - for a in musicxml_attributes_to_lily (n): - voice_builder.add_command (a) - continue + n.message (_ ("Negative skip found: from %s to %s, difference is %s") % (neg.here, neg.dest, neg.dest - neg.here)) if isinstance (n, musicxml.Barline): barlines = musicxml_barline_to_lily (n) @@ -1379,29 +2029,74 @@ def musicxml_voice_to_lily_voice (voice): voice_builder.add_command (a) continue - if not n.__class__.__name__ == 'Note': - error_message ('not a Note or Attributes? %s' % n) - continue - + # Continue any multimeasure-rests before trying to add bar checks! + # Don't handle new MM rests yet, because for them we want bar checks! rest = n.get_maybe_exist_typed_child (musicxml.Rest) - if (rest - and rest.is_whole_measure ()): - + if (rest and rest.is_whole_measure () + and voice_builder.pending_multibar > Rational (0)): voice_builder.add_multibar_rest (n._duration) continue - if n.is_first () and n._measure_position == Rational (0): - try: + + # print a bar check at the beginning of each measure! + if n.is_first () and n._measure_position == Rational (0) and n != voice._elements[0]: + try: num = int (n.get_parent ().number) except ValueError: num = 0 if num > 0: voice_builder.add_bar_check (num) + figured_bass_builder.add_bar_check (num) + chordnames_builder.add_bar_check (num) + # Start any new multimeasure rests + if (rest and rest.is_whole_measure ()): + voice_builder.add_multibar_rest (n._duration) + continue + + + if isinstance (n, musicxml.Direction): + for a in musicxml_direction_to_lily (n): + if a.wait_for_note (): + voice_builder.add_dynamics (a) + else: + voice_builder.add_command (a) + continue + + if isinstance (n, musicxml.Harmony): + for a in musicxml_harmony_to_lily (n): + if a.wait_for_note (): + voice_builder.add_dynamics (a) + else: + voice_builder.add_command (a) + for a in musicxml_harmony_to_lily_chordname (n): + pending_chordnames.append (a) + continue + + if isinstance (n, musicxml.FiguredBass): + a = musicxml_figured_bass_to_lily (n) + if a: + pending_figured_bass.append (a) + continue + + if isinstance (n, musicxml.Attributes): + for a in musicxml_attributes_to_lily (n): + voice_builder.add_command (a) + measure_length = measure_length_from_attributes (n, current_measure_length) + if current_measure_length != measure_length: + current_measure_length = measure_length + voice_builder.set_measure_length (current_measure_length) + continue + + if not n.__class__.__name__ == 'Note': + n.message (_ ('unexpected %s; expected %s or %s or %s') % (n, 'Note', 'Attributes', 'Barline')) + continue + main_event = musicxml_note_to_lily_main_event (n) if main_event and not first_pitch: first_pitch = main_event.pitch - ignore_lyrics = inside_slur or is_tied or is_chord + # 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 main_event and hasattr (main_event, 'drum_type') and main_event.drum_type: modes_found['drummode'] = True @@ -1411,15 +2106,31 @@ def musicxml_voice_to_lily_voice (voice): ev_chord = musicexp.ChordEvent() voice_builder.add_music (ev_chord, n._duration) + # For grace notes: grace = n.get_maybe_exist_typed_child (musicxml.Grace) - if grace: + if n.is_grace (): + is_after_grace = ev_chord.has_elements () or n.is_after_grace (); + is_chord = n.get_maybe_exist_typed_child (musicxml.Chord) + grace_chord = None - 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.ChordEvent () - ev_chord.append_grace (grace_chord) - if hasattr (grace, 'slash'): + + # after-graces and other graces use different lists; Depending on + # whether we have a chord or not, obtain either a new ChordEvent or + # the previous one to create a chord + if is_after_grace: + if ev_chord.after_grace_elements and n.get_maybe_exist_typed_child (musicxml.Chord): + grace_chord = ev_chord.after_grace_elements.get_last_event_chord () + if not grace_chord: + grace_chord = musicexp.ChordEvent () + ev_chord.append_after_grace (grace_chord) + elif n.is_grace (): + if ev_chord.grace_elements and n.get_maybe_exist_typed_child (musicxml.Chord): + grace_chord = ev_chord.grace_elements.get_last_event_chord () + if not grace_chord: + grace_chord = musicexp.ChordEvent () + ev_chord.append_grace (grace_chord) + + if hasattr (grace, 'slash') and not is_after_grace: # TODO: use grace_type = "appoggiatura" for slurred grace notes if grace.slash == "yes": ev_chord.grace_type = "acciaccatura" @@ -1436,6 +2147,34 @@ def musicxml_voice_to_lily_voice (voice): if voice_builder.current_duration () == 0 and n._duration > 0: voice_builder.set_duration (n._duration) + # if we have a figured bass, set its voice builder to the correct position + # and insert the pending figures + if pending_figured_bass: + try: + figured_bass_builder.jumpto (n._when) + except NegativeSkip, neg: + pass + for fb in pending_figured_bass: + # if a duration is given, use that, otherwise the one of the note + dur = fb.real_duration + if not dur: + dur = ev_chord.get_length () + if not fb.duration: + fb.duration = ev_chord.get_duration () + figured_bass_builder.add_music (fb, dur) + pending_figured_bass = [] + + if pending_chordnames: + try: + chordnames_builder.jumpto (n._when) + except NegativeSkip, neg: + pass + for cn in pending_chordnames: + # Assign the duration of the EventChord + cn.duration = ev_chord.get_duration () + chordnames_builder.add_music (cn, ev_chord.get_length ()) + pending_chordnames = [] + notations_children = n.get_typed_children (musicxml.Notations) tuplet_event = None span_events = [] @@ -1447,27 +2186,39 @@ def musicxml_voice_to_lily_voice (voice): # accidental-mark | other-notation 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: - frac = mod.get_fraction () - - tuplet_events.append ((ev_chord, tuplet_event, frac)) - - slurs = [s for s in notations.get_named_children ('slur') - if s.get_type () in ('start','stop')] - if slurs: - if len (slurs) > 1: - error_message ('more than 1 slur?') + time_mod = n.get_maybe_exist_typed_child (musicxml.Time_modification) + tuplet_events.append ((ev_chord, tuplet_event, time_mod)) + + # First, close all open slurs, only then start any new slur + # TODO: Record the number of the open slur to dtermine the correct + # closing slur! + endslurs = [s for s in notations.get_named_children ('slur') + if s.get_type () in ('stop')] + if endslurs and not inside_slur: + endslurs[0].message (_ ('Encountered closing slur, but no slur is open')) + elif endslurs: + if len (endslurs) > 1: + endslurs[0].message (_ ('Cannot have two simultaneous (closing) slurs')) + # record the slur status for the next note in the loop + if not grace: + inside_slur = False + lily_ev = musicxml_spanner_to_lily_event (endslurs[0]) + ev_chord.append (lily_ev) + + startslurs = [s for s in notations.get_named_children ('slur') + if s.get_type () in ('start')] + if startslurs and inside_slur: + startslurs[0].message (_ ('Cannot have a slur inside another slur')) + elif startslurs: + if len (startslurs) > 1: + startslurs[0].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': - inside_slur = True - elif slurs[0].get_type () == 'stop': - inside_slur = False - lily_ev = musicxml_spanner_to_lily_event (slurs[0]) + inside_slur = True + lily_ev = musicxml_spanner_to_lily_event (startslurs[0]) ev_chord.append (lily_ev) + if not grace: mxl_tie = notations.get_tie () if mxl_tie and mxl_tie.type == 'start': @@ -1488,12 +2239,25 @@ def musicxml_voice_to_lily_voice (voice): 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: ev_chord.append (ev) - + + # accidental-marks are direct children of ! + for a in notations.get_named_children ('accidental-mark'): + ev = musicxml_articulation_to_lily_event (a) + if ev: + ev_chord.append (ev) + # Articulations can contain the following child elements: # accent | strong-accent | staccato | tenuto | # detached-legato | staccatissimo | spiccato | @@ -1516,7 +2280,7 @@ def musicxml_voice_to_lily_voice (voice): for a in ornaments: for ch in a.get_all_children (): ev = musicxml_articulation_to_lily_event (ch) - if ev: + if ev: ev_chord.append (ev) dynamics = notations.get_named_children ('dynamics') @@ -1526,6 +2290,19 @@ def musicxml_voice_to_lily_voice (voice): if ev: ev_chord.append (ev) + + 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 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 + # Extract the lyrics if not rest and not ignore_lyrics: note_lyrics_processed = [] @@ -1533,32 +2310,15 @@ def musicxml_voice_to_lily_voice (voice): for l in note_lyrics_elements: if l.get_number () < 0: for k in lyrics.keys (): - lyrics[k].append (l.lyric_to_text ()) + lyrics[k].append (musicxml_lyrics_to_text (l)) note_lyrics_processed.append (k) else: - lyrics[l.number].append(l.lyric_to_text ()) + lyrics[l.number].append(musicxml_lyrics_to_text (l)) 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: - beam_ev = musicxml_spanner_to_lily_event (mxl_beams[0]) - if beam_ev: - ev_chord.append (beam_ev) - - if tuplet_event: - mod = n.get_maybe_exist_typed_child (musicxml.Time_modification) - frac = (1,1) - if mod: - frac = mod.get_fraction () - - tuplet_events.append ((ev_chord, tuplet_event, frac)) - ## force trailing mm rests to be written out. voice_builder.add_music (musicexp.ChordEvent (), Rational (0)) @@ -1579,7 +2339,7 @@ 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 () @@ -1594,6 +2354,24 @@ def musicxml_voice_to_lily_voice (voice): v.mode = mode return_value.ly_voice = v + # create \figuremode { figured bass elements } + if figured_bass_builder.has_relevant_elements: + fbass_music = musicexp.SequentialMusic () + fbass_music.elements = figured_bass_builder.elements + v = musicexp.ModeChangingMusicWrapper() + v.mode = 'figuremode' + v.element = fbass_music + return_value.figured_bass = v + + # create \chordmode { chords } + if chordnames_builder.has_relevant_elements: + cname_music = musicexp.SequentialMusic () + cname_music.elements = chordnames_builder.elements + v = musicexp.ModeChangingMusicWrapper() + v.mode = 'chordmode' + v.element = cname_music + return_value.chordnames = v + return return_value def musicxml_id_to_lily (id): @@ -1659,7 +2437,7 @@ def get_all_voices (parts): 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) @@ -1670,61 +2448,88 @@ def get_all_voices (parts): def option_parser (): - p = ly.get_option_parser(usage=_ ("musicxml2ly [options] FILE.xml"), - 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 + p = ly.get_option_parser (usage = _ ("musicxml2ly [OPTION]... FILE.xml"), + description = +_ ("""Convert MusicXML from FILE.xml to LilyPond input. +If the given filename is -, musicxml2ly reads from the command line. +"""), 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''' ++ +_ ("""Copyright (c) 2005--2008 by Han-Wen Nienhuys , Jan Nieuwenhuizen and Reinhold Kainhofer -"""), - description=_ ("Convert %s to LilyPond input.") % 'MusicXML' + "\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') + + 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.")) + 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.")) + 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, e.g. 'deutsch' for deutsch.ly.")) + help = _ ("use a different language file 'LANG.ly' and corresponding pitch names, e.g. 'deutsch' for deutsch.ly")) - p.add_option ('--no-articulation-directions', '--nd', + p.add_option ('--nd', '--no-articulation-directions', action = "store_false", default = True, dest = "convert_directions", - help = _ ("Do not convert directions (^, _ or -) for articulations.")) + 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, stdout if -")) + p.add_option_group ('', + description = (_ ("Report bugs via") + ''' http://post.gmane.org/post.php''' '''?group=gmane.comp.gnu.lilypond.bugs\n''')) return p @@ -1737,6 +2542,14 @@ 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 music_xml_figuredbass_name_to_lily_name (part_id, voicename): + str = "Part%sVoice%sFiguredBass" % (part_id, voicename) + return musicxml_id_to_lily (str) + +def music_xml_chordnames_name_to_lily_name (part_id, voicename): + str = "Part%sVoice%sChords" % (part_id, voicename) + return musicxml_id_to_lily (str) + def print_voice_definitions (printer, part_list, voices): for part in part_list: part_id = part.id @@ -1746,11 +2559,21 @@ def print_voice_definitions (printer, part_list, voices): printer.dump ('%s = ' % k) voice.ly_voice.print_ly (printer) printer.newline() + if voice.chordnames: + cnname = music_xml_chordnames_name_to_lily_name (part_id, name) + printer.dump ('%s = ' % cnname ) + voice.chordnames.print_ly (printer) + printer.newline() for l in voice.lyrics_order: lname = music_xml_lyrics_name_to_lily_name (part_id, name, l) - printer.dump ('%s = ' %lname ) + printer.dump ('%s = ' % lname ) voice.lyrics_dict[l].print_ly (printer) printer.newline() + if voice.figured_bass: + fbname = music_xml_figuredbass_name_to_lily_name (part_id, name) + printer.dump ('%s = ' % fbname ) + voice.figured_bass.print_ly (printer) + printer.newline() def uniq_list (l): @@ -1759,19 +2582,25 @@ def uniq_list (l): # format the information about the staff in the form # [staffid, # [ -# [voiceid1, [lyricsid11, lyricsid12,...] ...], -# [voiceid2, [lyricsid21, lyricsid22,...] ...], +# [voiceid1, [lyricsid11, lyricsid12,...], figuredbassid1], +# [voiceid2, [lyricsid21, lyricsid22,...], figuredbassid2], # ... # ] # ] -# raw_voices is of the form [(voicename, lyricsids)*] +# raw_voices is of the form [(voicename, lyricsids, havefiguredbass)*] def format_staff_info (part_id, staff_id, raw_voices): voices = [] - for (v, lyricsids) in raw_voices: + for (v, lyricsids, figured_bass, chordnames) in raw_voices: 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]) + figured_bass_name = '' + if figured_bass: + figured_bass_name = music_xml_figuredbass_name_to_lily_name (part_id, v) + chordnames_name = '' + if chordnames: + chordnames_name = music_xml_chordnames_name_to_lily_name (part_id, v) + voices.append ([voice_name, voice_lyrics, figured_bass_name, chordnames_name]) return [staff_id, voices] def update_score_setup (score_structure, part_list, voices): @@ -1780,7 +2609,7 @@ def update_score_setup (score_structure, part_list, voices): 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) + error_message (_ ('unknown part in part-list: %s') % part_id) continue staves = reduce (lambda x,y: x+ y, @@ -1793,16 +2622,21 @@ def update_score_setup (score_structure, part_list, voices): staves = uniq_list (staves) staves.sort () for s in staves: - thisstaff_raw_voices = [(voice_name, voice.lyrics_order) + thisstaff_raw_voices = [(voice_name, voice.lyrics_order, voice.figured_bass, voice.chordnames) for (voice_name, voice) in nv_dict.items () if voice.voicedata._start_staff == s] staves_info.append (format_staff_info (part_id, s, thisstaff_raw_voices)) else: - thisstaff_raw_voices = [(voice_name, voice.lyrics_order) + thisstaff_raw_voices = [(voice_name, voice.lyrics_order, voice.figured_bass, voice.chordnames) for (voice_name, voice) in nv_dict.items ()] 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) @@ -1836,8 +2670,12 @@ def read_xml (io_object, use_lxml): 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") + if filename == "-": + progress (_ ("Input is compressed, extracting raw MusicXML data from stdin") ) + z = zipfile.ZipFile (sys.stdin) + else: + 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 @@ -1854,30 +2692,39 @@ def read_musicxml (filename, compressed, use_lxml): if mxml_file: raw_string = z.read (mxml_file) - io_object = filename if raw_string: io_object = StringIO.StringIO (raw_string) + elif filename == "-": + io_object = sys.stdin + else: + io_object = filename return read_xml (io_object, use_lxml) def convert (filename, options): - progress ("Reading MusicXML from %s ..." % filename) - + if filename == "-": + progress (_ ("Reading MusicXML from Standard input ...") ) + else: + 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 + score = None mxl_pl = tree.get_maybe_exist_typed_child (musicxml.Part_list) if mxl_pl: - score_structure = extract_score_layout (mxl_pl, staff_info) + score = 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) - layout_information = extract_layout_information (tree) - update_score_setup (score_structure, part_list, voices) + update_score_setup (score, 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) @@ -1886,30 +2733,33 @@ def convert (filename, options): options.output_name = os.path.splitext (options.output_name)[0] - defs_ly_name = options.output_name + '-defs.ly' - driver_ly_name = options.output_name + '.ly' + #defs_ly_name = options.output_name + '-defs.ly' + if (options.output_name == "-"): + output_ly_name = 'Standard output' + else: + output_ly_name = options.output_name + '.ly' + progress (_ ("Output to `%s'") % output_ly_name) printer = musicexp.Output_printer() - progress ("Output to `%s'" % defs_ly_name) - printer.set_file (codecs.open (defs_ly_name, 'wb', encoding='utf-8')) - + #progress (_ ("Output to `%s'") % defs_ly_name) + if (options.output_name == "-"): + printer.set_file (codecs.getwriter ("utf-8")(sys.stdout)) + else: + printer.set_file (codecs.open (output_ly_name, 'wb', encoding='utf-8')) print_ly_preamble (printer, filename) 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) - printer = musicexp.Output_printer() - printer.set_file (codecs.open (driver_ly_name, 'wb', encoding='utf-8')) - print_ly_preamble (printer, filename) - printer.dump (r'\include "%s"' % os.path.basename (defs_ly_name)) - score_structure.print_ly (printer) + printer.newline () + printer.dump ("% The score definition") + printer.newline () + score.print_ly (printer) printer.newline () return voices @@ -1938,16 +2788,24 @@ def main (): 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") + basefilename = args[0].decode('utf-8') + if basefilename == "-": # Read from stdin + basefilename = "-" + else: + filename = get_existing_filename_with_extension (basefilename, "xml") + if not filename: + filename = get_existing_filename_with_extension (basefilename, "mxl") + options.compressed = True + if filename and filename.endswith ("mxl"): options.compressed = True - if filename and os.path.exists (filename): + + if filename and (filename == "-" or 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") % basefilename) if __name__ == '__main__': main()