]> git.donarmstrong.com Git - lilypond.git/blobdiff - scripts/musicxml2ly.py
MusicXML: Fix missing tie end tag
[lilypond.git] / scripts / musicxml2ly.py
index f6fcaa5cc1096ef5b2c164757addaaba0636494d..d9dfaf3be64171ae4de27938ac1f56be4b22434c 100644 (file)
@@ -45,37 +45,6 @@ def error_message (str):
 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))))""",
-
-  "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. 
@@ -304,15 +273,15 @@ def extract_score_information (tree):
         if value:
             header.set_field (field, musicxml.escape_ly_output_string (value))
 
+    movement_title = tree.get_maybe_exist_named_child ('movement-title')
+    if movement_title:
+        set_if_exists ('title', movement_title.get_text ())
     work = tree.get_maybe_exist_named_child ('work')
     if work:
+        # Overwrite the title from movement-title with work->title
         set_if_exists ('title', work.get_work_title ())
         set_if_exists ('worknumber', work.get_work_number ())
         set_if_exists ('opus', work.get_opus ())
-    else:
-        movement_title = tree.get_maybe_exist_named_child ('movement-title')
-        if movement_title:
-            set_if_exists ('title', movement_title.get_text ())
     
     identifications = tree.get_named_children ('identification')
     for ids in identifications:
@@ -338,18 +307,23 @@ def extract_score_information (tree):
 
         # 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"))
-        # ditto for Dolet 3.5
-        if "Dolet 3.5 for Sibelius" in software:
-            conversion_settings.ignore_beaming = True
-            progress (_ ("Encountered file created by Dolet 3.5 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
-
+        ignore_beaming_software = {
+            "Dolet 4 for Sibelius, Beta 2": "Dolet 4 for Sibelius, Beta 2",
+            "Dolet 3.5 for Sibelius": "Dolet 3.5 for Sibelius",
+            "Dolet 3.4 for Sibelius": "Dolet 3.4 for Sibelius",
+            "Dolet 3.3 for Sibelius": "Dolet 3.3 for Sibelius",
+            "Dolet 3.2 for Sibelius": "Dolet 3.2 for Sibelius",
+            "Dolet 3.1 for Sibelius": "Dolet 3.1 for Sibelius",
+            "Dolet for Sibelius 1.3": "Dolet for Sibelius 1.3",
+            "Noteworthy Composer": "Noteworthy Composer's nwc2xm[",
+        }
+        for s in software:
+            app_description = ignore_beaming_software.get (s, False);
+            if app_description:
+                conversion_settings.ignore_beaming = True
+                progress (_ ("Encountered file created by %s, containing wrong beaming information. All beaming information in the MusicXML file will be ignored") % app_description)
+
+    # TODO: Check for other unsupported features
     return header
 
 class PartGroupInfo:
@@ -754,7 +728,7 @@ def musicxml_tuplet_to_lily (tuplet_elt, time_modification):
     actual_type = tuplet_elt.get_actual_type ()
     if actual_type:
         actual_note = musicexp.Duration ()
-        (actual_note.duration_log, actual_note.dots) = normal_type
+        (actual_note.duration_log, actual_note.dots) = actual_type
         tsm.actual_type = actual_note
 
     # Obtain non-default nrs of notes from the tuplet object!
@@ -772,14 +746,8 @@ def musicxml_tuplet_to_lily (tuplet_elt, time_modification):
     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
@@ -963,6 +931,30 @@ def musicxml_attributes_to_lily (attrs):
                 elts.append (ev)
     
     return elts
+    
+def musicxml_print_to_lily (el):
+    # TODO: Implement other print attributes
+    #  <!ELEMENT print (page-layout?, system-layout?, staff-layout*,
+    #          measure-layout?, measure-numbering?, part-name-display?, 
+    #          part-abbreviation-display?)>
+    #  <!ATTLIST print
+    #      staff-spacing %tenths; #IMPLIED
+    #      new-system %yes-no; #IMPLIED
+    #      new-page %yes-no-number; #IMPLIED
+    #      blank-page NMTOKEN #IMPLIED
+    #      page-number CDATA #IMPLIED 
+    #  >
+    elts = []
+    if (hasattr (el, "new-system")):
+        val = getattr (el, "new-system")
+        if (val == "yes"):
+            elts.append (musicexp.Break ("break"))
+    if (hasattr (el, "new-page")):
+        val = getattr (el, "new-page")
+        if (val == "yes"):
+            elts.append (musicexp.Break ("pageBreak"))
+    return elts
+
 
 class Marker (musicexp.Music):
     def __init__ (self):
@@ -1144,12 +1136,6 @@ def musicxml_fingering_event (mxl_event):
     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 ()
@@ -1211,7 +1197,7 @@ articulations_dict = {
     #"schleifer": "?",
     #"scoop": "?",
     #"shake": "?",
-    "snap-pizzicato": musicxml_snappizzicato_event,
+    "snap-pizzicato": "snappizzicato",
     #"spiccato": "?",
     "staccatissimo": (musicexp.ShortArticulationEvent, "|"), # or "staccatissimo"
     "staccato": (musicexp.ShortArticulationEvent, "."), # or "staccato"
@@ -1454,7 +1440,7 @@ def musicxml_harp_pedals_to_ly (mxl_event):
 
 def musicxml_eyeglasses_to_ly (mxl_event):
     needed_additional_definitions.append ("eyeglasses")
-    return musicexp.MarkEvent ("\\eyeglasses")
+    return musicexp.MarkEvent ("\\markup { \\eyeglasses }")
 
 def next_non_hash_index (lst, pos):
     pos += 1
@@ -1662,6 +1648,7 @@ chordkind_dict = {
     'diminished': 'dim5',
         # Sevenths:
     'dominant': '7',
+    'dominant-seventh': '7',
     'major-seventh': 'maj7',
     'minor-seventh': 'm7',
     'diminished-seventh': 'dim7',
@@ -1948,12 +1935,12 @@ class LilyPondVoiceBuilder:
     def current_duration (self):
         return self.end_moment - self.begin_moment
         
-    def add_music (self, music, duration):
+    def add_music (self, music, duration, relevant = True):
         assert isinstance (music, musicexp.Music)
         if self.pending_multibar > Rational (0):
             self._insert_multibar ()
 
-        self.has_relevant_elements = True
+        self.has_relevant_elements = self.has_relevant_elements or relevant
         self.elements.append (music)
         self.begin_moment = self.end_moment
         self.set_duration (duration)
@@ -1965,18 +1952,27 @@ class LilyPondVoiceBuilder:
             self.pending_dynamics = []
 
     # Insert some music command that does not affect the position in the measure
-    def add_command (self, command):
+    def add_command (self, command, relevant = True):
         assert isinstance (command, musicexp.Music)
         if self.pending_multibar > Rational (0):
             self._insert_multibar ()
-        self.has_relevant_elements = True
+        self.has_relevant_elements = self.has_relevant_elements or relevant
         self.elements.append (command)
-    def add_barline (self, barline):
-        # TODO: Implement merging of default barline and custom bar line
-        self.add_music (barline, Rational (0))
+    def add_barline (self, barline, relevant = False):
+        # Insert only if we don't have a barline already
+        # TODO: Implement proper merging of default barline and custom bar line
+        has_relevant = self.has_relevant_elements
+        if (not (self.elements) or 
+            not (isinstance (self.elements[-1], musicexp.BarLine)) or 
+            (self.pending_multibar > Rational (0))):
+            self.add_music (barline, Rational (0))
+        self.has_relevant_elements = has_relevant or relevant
     def add_partial (self, command):
         self.ignore_skips = True
+        # insert the partial, but restore relevant_elements (partial is not relevant)
+        relevant = self.has_relevant_elements
         self.add_command (command)
+        self.has_relevant_elements = relevant
 
     def add_dynamics (self, dynamic):
         # store the dynamic item(s) until we encounter the next note/rest:
@@ -1985,11 +1981,9 @@ class LilyPondVoiceBuilder:
     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
@@ -2023,7 +2017,7 @@ class LilyPondVoiceBuilder:
 
             evc = musicexp.ChordEvent ()
             evc.elements.append (skip)
-            self.add_music (evc, diff)
+            self.add_music (evc, diff, False)
 
         if diff > Rational (0) and moment == 0:
             self.ignore_skips = False
@@ -2114,6 +2108,7 @@ def musicxml_voice_to_lily_voice (voice):
     voice_builder.set_measure_length (current_measure_length)
 
     for n in voice._elements:
+        tie_started = False
         if n.get_name () == 'forward':
             continue
         staff = n.get_maybe_exist_named_child ('staff')
@@ -2127,6 +2122,8 @@ def musicxml_voice_to_lily_voice (voice):
             a = musicxml_partial_to_lily (n.partial)
             if a:
                 voice_builder.add_partial (a)
+                figured_bass_builder.add_partial (a)
+                chordnames_builder.add_partial (a)
             continue
 
         is_chord = n.get_maybe_exist_named_child ('chord')
@@ -2134,8 +2131,12 @@ def musicxml_voice_to_lily_voice (voice):
         if not is_chord and not is_after_grace:
             try:
                 voice_builder.jumpto (n._when)
+                figured_bass_builder.jumpto (n._when)
+                chordnames_builder.jumpto (n._when)
             except NegativeSkip, neg:
                 voice_builder.correct_negative_skip (n._when)
+                figured_bass_builder.correct_negative_skip (n._when)
+                chordnames_builder.correct_negative_skip (n._when)
                 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):
@@ -2143,8 +2144,18 @@ def musicxml_voice_to_lily_voice (voice):
             for a in barlines:
                 if isinstance (a, musicexp.BarLine):
                     voice_builder.add_barline (a)
+                    figured_bass_builder.add_barline (a, False)
+                    chordnames_builder.add_barline (a, False)
                 elif isinstance (a, RepeatMarker) or isinstance (a, EndingMarker):
                     voice_builder.add_command (a)
+                    figured_bass_builder.add_barline (a, False)
+                    chordnames_builder.add_barline (a, False)
+            continue
+
+
+        if isinstance (n, musicxml.Print):
+            for a in musicxml_print_to_lily (n):
+                voice_builder.add_command (a, False)
             continue
 
         # Continue any multimeasure-rests before trying to add bar checks!
@@ -2340,6 +2351,7 @@ def musicxml_voice_to_lily_voice (voice):
                 if mxl_tie and mxl_tie.type == 'start':
                     ev_chord.append (musicexp.TieEvent ())
                     is_tied = True
+                    tie_started = True
                 else:
                     is_tied = False
 
@@ -2435,7 +2447,13 @@ def musicxml_voice_to_lily_voice (voice):
                 if not lnr in note_lyrics_processed:
                     lyrics[lnr].append ("\skip4")
 
-    ## force trailing mm rests to be written out.   
+        # Assume that a <tie> element only lasts for one note.
+        # This might not be correct MusicXML interpretation, but works for
+        # most cases and fixes broken files, which have the end tag missing
+        if is_tied and not tie_started:
+            is_tied = False
+
+    ## force trailing mm rests to be written out.
     voice_builder.add_music (musicexp.ChordEvent (), Rational (0))
     
     ly_voice = group_tuplets (voice_builder.elements, tuplet_events)
@@ -2473,7 +2491,7 @@ def musicxml_voice_to_lily_voice (voice):
     # create \figuremode { figured bass elements }
     if figured_bass_builder.has_relevant_elements:
         fbass_music = musicexp.SequentialMusic ()
-        fbass_music.elements = figured_bass_builder.elements
+        fbass_music.elements = group_repeats (figured_bass_builder.elements)
         v = musicexp.ModeChangingMusicWrapper()
         v.mode = 'figuremode'
         v.element = fbass_music
@@ -2482,7 +2500,7 @@ def musicxml_voice_to_lily_voice (voice):
     # create \chordmode { chords }
     if chordnames_builder.has_relevant_elements:
         cname_music = musicexp.SequentialMusic ()
-        cname_music.elements = chordnames_builder.elements
+        cname_music.elements = group_repeats (chordnames_builder.elements)
         v = musicexp.ModeChangingMusicWrapper()
         v.mode = 'chordmode'
         v.element = cname_music
@@ -2541,7 +2559,17 @@ def voices_in_part (part):
 
 def voices_in_part_in_parts (parts):
     """return a Part -> Name -> Voice dictionary"""
-    return dict([(p.id, voices_in_part (p)) for p in parts])
+    # don't crash if p doesn't have an id (that's invalid MusicXML,
+    # but such files are out in the wild!
+    dictionary = {}
+    for p in parts:
+        voices = voices_in_part (p)
+        if (hasattr (p, "id")):
+             dictionary[p.id] = voices
+        else:
+             # TODO: extract correct part id from other sources
+             dictionary[None] = voices
+    return dictionary;
 
 
 def get_all_voices (parts):