From db57aab7205cb050f225d4a6698664ecad65d5d2 Mon Sep 17 00:00:00 2001 From: Reinhold Kainhofer Date: Fri, 19 Oct 2007 02:52:38 +0200 Subject: [PATCH] MusicXML: Implement Barline styles and repeats in musicxml2ly -) The element is now converted to a \bar"...." lilypond expression. Not all bar styles from MusicXML are supported by lilypond, though. -) Convert repeats and alternative endings from MusicXML to lilypond. While lilypond has a nice hierarchy (i.e. nested music) for repeating structures, MusicXML only has markers "here starts/ends a repeat or altern.ending", so I need to somehow build up that repeat hierarchy from the flat data. I've imlemented this via a loop that looks for a repeat structure, replaces that one repeat by a proper instance of musicexp.RepeatedMusic. Since I directly modify the list of event chords, after replacing one repeat, I have to start the whole loop from the start (because the iterators are off after the list is modified). -) Add some more unit test files for barlines and repeating structures (some even do not make sense from a musical point of view) -) Fix typo in the coverage/regression tests make file --- .../musicxml/09a-SimpleRepeat-Finale.xml | 66 ++++ .../09b-RepeatWithAlternatives-Finale.xml | 108 ++++++ ...nes-Finale.xml => 09c-Barlines-Finale.xml} | 348 +++++++++--------- .../09d-RepeatMultipleTimes-Finale.xml | 124 +++++++ .../musicxml/09e-Alternatives-Finale.xml | 180 +++++++++ ...eats-Finale.xml => 09f-Repeats-Finale.xml} | 0 .../musicxml/09g-Endings-Finale.xml | 104 ++++++ input/regression/musicxml/GNUmakefile | 2 +- python/musicexp.py | 51 ++- python/musicxml.py | 14 +- scripts/musicxml2ly.py | 172 ++++++++- 11 files changed, 980 insertions(+), 189 deletions(-) create mode 100644 input/regression/musicxml/09a-SimpleRepeat-Finale.xml create mode 100644 input/regression/musicxml/09b-RepeatWithAlternatives-Finale.xml rename input/regression/musicxml/{09b-Barlines-Finale.xml => 09c-Barlines-Finale.xml} (96%) create mode 100644 input/regression/musicxml/09d-RepeatMultipleTimes-Finale.xml create mode 100644 input/regression/musicxml/09e-Alternatives-Finale.xml rename input/regression/musicxml/{09a-Repeats-Finale.xml => 09f-Repeats-Finale.xml} (100%) create mode 100644 input/regression/musicxml/09g-Endings-Finale.xml diff --git a/input/regression/musicxml/09a-SimpleRepeat-Finale.xml b/input/regression/musicxml/09a-SimpleRepeat-Finale.xml new file mode 100644 index 0000000000..cf34f14144 --- /dev/null +++ b/input/regression/musicxml/09a-SimpleRepeat-Finale.xml @@ -0,0 +1,66 @@ + + + + + + Finale 2007 for Windows + Dolet Light for Finale 2007 + 2007-10-17 + + + + + MusicXML Part + + Grand Piano + + + 1 + 1 + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + + 4 + 1 + + + light-heavy + + + + + + + + 4 + 1 + + + light-heavy + + + + + diff --git a/input/regression/musicxml/09b-RepeatWithAlternatives-Finale.xml b/input/regression/musicxml/09b-RepeatWithAlternatives-Finale.xml new file mode 100644 index 0000000000..23b0212698 --- /dev/null +++ b/input/regression/musicxml/09b-RepeatWithAlternatives-Finale.xml @@ -0,0 +1,108 @@ + + + + + + Finale 2007 for Windows + Dolet Light for Finale 2007 + 2007-10-17 + + + + + MusicXML Part + + Grand Piano + + + 1 + 1 + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + + C + 5 + + 4 + 1 + whole + + + + + + + + + + C + 5 + + 4 + 1 + whole + + + light-heavy + + + + + + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + + + + + C + 5 + + 4 + 1 + whole + + + light-heavy + + + + + diff --git a/input/regression/musicxml/09b-Barlines-Finale.xml b/input/regression/musicxml/09c-Barlines-Finale.xml similarity index 96% rename from input/regression/musicxml/09b-Barlines-Finale.xml rename to input/regression/musicxml/09c-Barlines-Finale.xml index e3d1a92655..611760a819 100644 --- a/input/regression/musicxml/09b-Barlines-Finale.xml +++ b/input/regression/musicxml/09c-Barlines-Finale.xml @@ -1,174 +1,174 @@ - - - - Barline test - - Reinhold Kainhofer - Public Domain - - Finale 2007 for Windows - Dolet Light for Finale 2007 - 2007-09-21 - - - - - MusicXML Part - - Grand Piano - - - 1 - 1 - - - - - - - - 1 - - 0 - major - - - - G - 2 - - - - - - 4 - 1 - - - light-light - - - - - - - 4 - 1 - - - light-heavy - - - - - - - 4 - 1 - - - heavy - - - - - - - - 4 - 1 - - - dashed - - - - - - - 4 - 1 - - - dotted - - - - - - - 4 - 1 - - - none - - - - - - - 4 - 1 - - - - - - - - 4 - 1 - - - - - - - 4 - 1 - - - - - - - 4 - 1 - - - - - - - 4 - 1 - - - - - - - - 4 - 1 - - - - - - - 4 - 1 - - - light-heavy - - - - - + + + + Barline test + + Reinhold Kainhofer + Public Domain + + Finale 2007 for Windows + Dolet Light for Finale 2007 + 2007-09-21 + + + + + MusicXML Part + + Grand Piano + + + 1 + 1 + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + + 4 + 1 + + + light-light + + + + + + + 4 + 1 + + + light-heavy + + + + + + + 4 + 1 + + + heavy + + + + + + + + 4 + 1 + + + dashed + + + + + + + 4 + 1 + + + dotted + + + + + + + 4 + 1 + + + none + + + + + + + 4 + 1 + + + + + + + + 4 + 1 + + + + + + + 4 + 1 + + + + + + + 4 + 1 + + + + + + + 4 + 1 + + + + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + diff --git a/input/regression/musicxml/09d-RepeatMultipleTimes-Finale.xml b/input/regression/musicxml/09d-RepeatMultipleTimes-Finale.xml new file mode 100644 index 0000000000..6cbe6db71f --- /dev/null +++ b/input/regression/musicxml/09d-RepeatMultipleTimes-Finale.xml @@ -0,0 +1,124 @@ + + + + + + Finale 2007 for Windows + Dolet Light for Finale 2007 + 2007-10-19 + + + + + MusicXML Part + + Grand Piano + + + 1 + 1 + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + + 4 + 1 + + + + + + heavy-light + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + + 4 + 1 + + + + + + + 4 + 1 + + + + + + + 4 + 1 + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + + 4 + 1 + + + light-heavy + + + + + diff --git a/input/regression/musicxml/09e-Alternatives-Finale.xml b/input/regression/musicxml/09e-Alternatives-Finale.xml new file mode 100644 index 0000000000..dbbb9d4946 --- /dev/null +++ b/input/regression/musicxml/09e-Alternatives-Finale.xml @@ -0,0 +1,180 @@ + + + + Alternatives Test + + + Finale 2007 for Windows + Dolet Light for Finale 2007 + 2007-10-15 + + + + + MusicXML Part + + Grand Piano + + + 1 + 1 + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + + 4 + 1 + + + + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + + + + + 4 + 1 + + + + + + + + 4 + 1 + + + + + + + 4 + 1 + + + + + + + + + + + + + 4 + 1 + + + + + + + + + + 4 + 1 + + + + + + + + 4 + 1 + + + + + + + 4 + 1 + + + + + + + + + + 4 + 1 + + + + + + + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + + + 4 + 1 + + + light-heavy + + + + + diff --git a/input/regression/musicxml/09a-Repeats-Finale.xml b/input/regression/musicxml/09f-Repeats-Finale.xml similarity index 100% rename from input/regression/musicxml/09a-Repeats-Finale.xml rename to input/regression/musicxml/09f-Repeats-Finale.xml diff --git a/input/regression/musicxml/09g-Endings-Finale.xml b/input/regression/musicxml/09g-Endings-Finale.xml new file mode 100644 index 0000000000..82b4c5524a --- /dev/null +++ b/input/regression/musicxml/09g-Endings-Finale.xml @@ -0,0 +1,104 @@ + + + + + + Finale 2007 for Windows + Dolet Light for Finale 2007 + 2007-10-13 + + + + + MusicXML Part + + Grand Piano + + + 1 + 1 + + + + + + + + 1 + + 0 + major + + + + G + 2 + + + + + + 4 + 1 + + + + + + + + + + 4 + 1 + + + + + + + + + + + + + 4 + 1 + + + + + + + + + + + 4 + 1 + + + light-heavy + + + + + + + + + 4 + 1 + + + light-heavy + + + + + diff --git a/input/regression/musicxml/GNUmakefile b/input/regression/musicxml/GNUmakefile index ec647d8507..53ecd0eca3 100644 --- a/input/regression/musicxml/GNUmakefile +++ b/input/regression/musicxml/GNUmakefile @@ -4,5 +4,5 @@ STEPMAKE_TEMPLATES=documentation texinfo tex LOCALSTEPMAKE_TEMPLATES=lilypond ly lysdoc musicxml include $(depth)/make/stepmake.make -TITLE=Lilypon musicxml2ly Regression Tests +TITLE=Lilypond musicxml2ly Regression Tests diff --git a/python/musicexp.py b/python/musicexp.py index a78885dd3c..369542226f 100644 --- a/python/musicexp.py +++ b/python/musicexp.py @@ -416,7 +416,7 @@ class SequentialMusic (NestedMusic): at = len( self.elements ) - 1 while (at >= 0 and not isinstance (self.elements[at], EventChord) and - not isinstance (self.elements[at], BarCheck)): + not isinstance (self.elements[at], BarLine)): at -= 1 if (at >= 0 and isinstance (self.elements[at], EventChord)): @@ -450,6 +450,36 @@ class SequentialMusic (NestedMusic): e.set_start (start) start += e.get_length() +class RepeatedMusic: + def __init__ (self): + self.repeat_type = "volta" + self.repeat_count = 2 + self.endings = [] + self.music = None + def set_music (self, music): + if isinstance (music, Music): + self.music = music + elif isinstance (music, list): + self.music = SequentialMusic () + self.music.elements = music + else: + sys.stderr.write ("WARNING: Unable to set the music %s for the repeat %s" % (music, self)) + def add_ending (self, music): + self.endings.append (music) + def print_ly (self, printer): + printer.dump ('\\repeat %s %s' % (self.repeat_type, self.repeat_count)) + if self.music: + self.music.print_ly (printer) + else: + sys.stderr.write ("WARNING: Encountered repeat without body\n") + printer.dump ('{}') + if self.endings: + printer.dump ('\\alternative {') + for e in self.endings: + e.print_ly (printer) + printer.dump ('}') + + class Lyrics: def __init__ (self): self.lyrics_syllables = [] @@ -548,20 +578,27 @@ class Partial (Music): if self.partial: printer.dump ("\\partial %s" % self.partial.ly_expression ()) -class BarCheck (Music): +class BarLine (Music): def __init__ (self): Music.__init__ (self) self.bar_number = 0 + self.type = None def print_ly (self, printer): + bar_symbol = { 'regular': "|", 'dotted': ":", 'dashed': ":", + 'heavy': "|", 'light-light': "||", 'light-heavy': "|.", + 'heavy-light': ".|", 'heavy-heavy': ".|.", 'tick': "'", + 'short': "'", 'none': "" }.get (self.type, None) + if bar_symbol <> None: + printer.dump ('\\bar "%s"' % bar_symbol) + else: + printer.dump ("|") + if self.bar_number > 0 and (self.bar_number % 10) == 0: - printer.dump ("| \\barNumberCheck #%d " % self.bar_number) - printer.newline () + printer.dump ("\\barNumberCheck #%d " % self.bar_number) else: - printer.dump ("| ") printer.print_verbatim (' %% %d' % self.bar_number) - printer.newline () - + printer.newline () def ly_expression (self): return " | " diff --git a/python/musicxml.py b/python/musicxml.py index 9a5d43eeec..81d2e82465 100644 --- a/python/musicxml.py +++ b/python/musicxml.py @@ -270,7 +270,11 @@ class Attributes (Measure_element): fifths = int (key.get_maybe_exist_named_child ('fifths').get_text ()) return (fifths, mode) - + +class Barline (Measure_element): + pass +class BarStyle (Music_xml_node): + pass class Partial (Measure_element): def __init__ (self, partial): Measure_element.__init__ (self) @@ -604,7 +608,9 @@ class Part (Music_xml_node): for n in elements: voice_id = n.get_maybe_exist_typed_child (get_class ('voice')) - if not (voice_id or isinstance (n, Attributes) or isinstance (n, Direction) or isinstance (n, Partial) ): + if not (voice_id or isinstance (n, Attributes) or + isinstance (n, Direction) or isinstance (n, Partial) or + isinstance (n, Barline) ): continue if isinstance (n, Attributes) and not start_attr: @@ -619,7 +625,7 @@ class Part (Music_xml_node): voices[v].add_element (staff_attributes) continue - if isinstance (n, Partial): + if isinstance (n, Partial) or isinstance (n, Barline): for v in voices.keys (): voices[v].add_element (n) continue @@ -784,6 +790,8 @@ class_dict = { '#text': Hash_text, 'accidental': Accidental, 'attributes': Attributes, + 'barline': Barline, + 'bar-style': BarStyle, 'beam' : Beam, 'bend' : Bend, 'chord': Chord, diff --git a/scripts/musicxml2ly.py b/scripts/musicxml2ly.py index ba841e0d00..26ee3e4abf 100644 --- a/scripts/musicxml2ly.py +++ b/scripts/musicxml2ly.py @@ -173,6 +173,92 @@ def musicxml_partial_to_lily (partial_len): else: return Null +# Detect repeats and alternative endings in the chord event list (music_list) +# and convert them to the corresponding musicexp objects, containing nested +# music +def group_repeats (music_list): + repeat_replaced = True + music_start = 0 + i=0 + # Walk through the list of expressions, looking for repeat structure + # (repeat start/end, corresponding endings). If we find one, try to find the + # last event of the repeat, replace the whole structure and start over again. + # For nested repeats, as soon as we encounter another starting repeat bar, + # treat that one first, and start over for the outer repeat. + while repeat_replaced and i<10: + i += 1 + repeat_start = -1 # position of repeat start / end + repeat_end = -1 # position of repeat start / end + repeat_times = 0 + ending_start = -1 # position of current ending start + endings = [] # list of already finished endings + pos = 0 + repeat_replaced = False + final_marker = 0 + while pos < len (music_list) and not repeat_replaced: + e = music_list[pos] + repeat_finished = False + if isinstance (e, RepeatMarker): + if not repeat_times and e.times: + repeat_times = e.times + if e.direction == -1: + if repeat_end >= 0: + repeat_finished = True + else: + repeat_start = pos + repeat_end = -1 + ending_start = -1 + endings = [] + elif e.direction == 1: + if repeat_start < 0: + repeat_start = 0 + if repeat_end < 0: + repeat_end = pos + final_marker = pos + elif isinstance (e, EndingMarker): + if e.direction == -1: + if repeat_start < 0: + repeat_start = 0 + repeat_end = pos + ending_start = pos + elif e.direction == 1: + if ending_start < 0: + ending_start = 0 + endings.append ([ending_start, pos]) + ending_start = -1 + final_marker = pos + elif not isinstance (e, musicexp.BarLine): + # As soon as we encounter an element when repeat start and end + # is set and we are not inside an alternative ending, + # this whole repeat structure is finished => replace it + if repeat_start >= 0 and repeat_end > 0 and ending_start < 0: + repeat_finished = True + + if repeat_finished: + # We found the whole structure replace it! + r = musicexp.RepeatedMusic () + if repeat_times <= 0: + repeat_times = 2 + r.repeat_count = repeat_times + # don't erase the first element for "implicit" repeats (i.e. no + # starting repeat bars at the very beginning) + start = repeat_start+1 + if repeat_start == music_start: + start = music_start + r.set_music (music_list[start:repeat_end]) + for (start, end) in endings: + s = musicexp.SequentialMusic () + s.elements = music_list[start+1:end] + r.add_ending (s) + del music_list[repeat_start:final_marker+1] + music_list.insert (repeat_start, r) + repeat_replaced = True + pos += 1 + # TODO: Implement repeats until the end without explicit ending bar + return music_list + + + def group_tuplets (music_list, events): @@ -274,6 +360,71 @@ def musicxml_attributes_to_lily (attrs): return elts +class Marker (musicexp.Music): + def __init__ (self): + self.direction = 0 + self.event = None + def print_ly (self, printer): + sys.stderr.write ("Encountered unprocessed marker %s\n" % self) + pass + def ly_expression (self): + return "" +class RepeatMarker (Marker): + def __init__ (self): + Marker.__init__ (self) + self.times = 0 +class EndingMarker (Marker): + pass + +# Convert the element to musicxml.BarLine (for non-standard barlines) +# and to RepeatMarker and EndingMarker objects for repeat and +# alternatives start/stops +def musicxml_barline_to_lily (barline): + # retval contains all possible markers in the order: + # 0..bw_ending, 1..bw_repeat, 2..barline, 3..fw_repeat, 4..fw_ending + retval = {} + bartype_element = barline.get_maybe_exist_named_child ("bar-style") + repeat_element = barline.get_maybe_exist_named_child ("repeat") + ending_element = barline.get_maybe_exist_named_child ("ending") + + bartype = None + if bartype_element: + bartype = bartype_element.get_text () + + if repeat_element and hasattr (repeat_element, 'direction'): + repeat = RepeatMarker () + repeat.direction = {"forward": -1, "backward": 1}.get (repeat_element.direction, 0) + + if ( (repeat_element.direction == "forward" and bartype == "heavy-light") or + (repeat_element.direction == "backward" and bartype == "light-heavy") ): + bartype = None + if hasattr (repeat_element, 'times'): + try: + repeat.times = int (repeat_element.times) + except ValueError: + repeat.times = 2 + repeat.event = barline + if repeat.direction == -1: + retval[1] = repeat + else: + retval[3] = repeat + + if ending_element and hasattr (ending_element, 'type'): + ending = EndingMarker () + ending.direction = {"start": -1, "stop": 1, "discontinue": 1}.get (ending_element.type, 0) + ending.event = barline + if ending.direction == -1: + retval[0] = ending + else: + retval[4] = ending + + if bartype: + b = musicexp.BarLine () + b.type = bartype + retval[2] = b + + return retval.values () + spanner_event_dict = { 'slur' : musicexp.SlurEvent, 'beam' : musicexp.BeamEvent, @@ -597,6 +748,9 @@ class LilyPondVoiceBuilder: if self.pending_multibar > Rational (0): self._insert_multibar () 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_partial (self, command): self.ignore_skips = True self.add_command (command) @@ -606,9 +760,9 @@ class LilyPondVoiceBuilder: self.pending_dynamics.append (dynamic) def add_bar_check (self, number): - b = musicexp.BarCheck () + b = musicexp.BarLine () b.bar_number = number - self.add_command (b) + self.add_barline (b) def jumpto (self, moment): current_end = self.end_moment + self.pending_multibar @@ -638,7 +792,7 @@ class LilyPondVoiceBuilder: at = len( self.elements ) - 1 while (at >= 0 and not isinstance (self.elements[at], musicexp.EventChord) and - not isinstance (self.elements[at], musicexp.BarCheck)): + not isinstance (self.elements[at], musicexp.BarLine)): at -= 1 if (self.elements @@ -703,11 +857,20 @@ def musicxml_voice_to_lily_voice (voice): 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 + if isinstance (n, musicxml.Barline): + barlines = musicxml_barline_to_lily (n) + for a in barlines: + if isinstance (a, musicexp.BarLine): + voice_builder.add_barline (a) + elif isinstance (a, RepeatMarker) or isinstance (a, EndingMarker): + voice_builder.add_command (a) + continue + if not n.__class__.__name__ == 'Note': error_message ('not a Note or Attributes? %s' % n) continue @@ -887,6 +1050,7 @@ def musicxml_voice_to_lily_voice (voice): voice_builder.add_music (musicexp.EventChord (), Rational (0)) ly_voice = group_tuplets (voice_builder.elements, tuplet_events) + ly_voice = group_repeats (ly_voice) seq_music = musicexp.SequentialMusic () -- 2.39.5