diff --git a/ly/musicxml/create_musicxml.py b/ly/musicxml/create_musicxml.py index 44ff6402..8b950ed7 100644 --- a/ly/musicxml/create_musicxml.py +++ b/ly/musicxml/create_musicxml.py @@ -554,13 +554,32 @@ def add_bar_style(self, multirest=None): def new_system(self, force_break): etree.SubElement(self.current_bar, "print", {'new-system':force_break}) - def add_barline(self, bl_type, repeat=None): + def add_barline(self, bl_type, repeat=None, repeat_times=None): barnode = etree.SubElement(self.current_bar, "barline", location="right") barstyle = etree.SubElement(barnode, "bar-style") barstyle.text = bl_type if repeat: + if repeat == "forward": + barnode.attrib["location"] = "left" repeatnode = etree.SubElement(barnode, "repeat", direction=repeat) + if repeat_times and repeat_times > 2: + repeatnode.attrib['times'] = str(repeat_times) + + def add_ending(self, ending, number): + barnode = etree.SubElement(self.current_bar, "barline", location="right") + + if not(isinstance(number,list)): + number = [number] + + number_attr = ','.join(str(n) for n in number) + + endingnode = etree.SubElement(barnode, "ending", type=ending, number=number_attr) + + if ending == 'start': + barnode.attrib['location'] = 'left' + endingnode.text = ', '.join('{}.'.format(n) for n in number) + def add_backup(self, duration): if duration <= 0: return diff --git a/ly/musicxml/ly2xml_mediator.py b/ly/musicxml/ly2xml_mediator.py index e67004e0..fac3b6c0 100644 --- a/ly/musicxml/ly2xml_mediator.py +++ b/ly/musicxml/ly2xml_mediator.py @@ -41,6 +41,11 @@ class Mediator(): def __init__(self): """ create global lists """ self.score = xml_objs.Score() + + # Previous and active bar, necessary for putting repeat sign at correct position + self.prev_bar = None + self.bar = None + self.sections = [] """ default and initial values """ self.insert_into = None @@ -103,6 +108,7 @@ def new_section(self, name, glob=False): self.insert_into = section self.sections.append(section) self.bar = None + self.prev_bar = None def new_snippet(self, name): name = self.check_name(name) @@ -110,6 +116,7 @@ def new_snippet(self, name): self.insert_into = snippet self.sections.append(snippet) self.bar = None + self.prev_bar = None def new_lyric_section(self, name, voice_id): name = self.check_name(name) @@ -160,6 +167,7 @@ def new_part(self, pid=None, to_part=None, piano=False): self.score.partlist.append(self.part) self.insert_into = self.part self.bar = None + self.prev_bar = None def part_not_empty(self): return self.part and self.part.barlist @@ -323,6 +331,9 @@ def set_pickup(self): def new_bar(self, fill_prev=True): if self.bar and fill_prev: self.bar.list_full = True + + self.prev_bar = self.bar + self.current_attr = xml_objs.BarAttr() self.bar = xml_objs.Bar() if self.bar_is_pickup: @@ -342,13 +353,37 @@ def create_barline(self, bl): self.bar.add(barline) self.new_bar() - def new_repeat(self, rep): + def new_repeat(self, rep, times=None): + barline = xml_objs.BarAttr() barline.set_barline(rep) barline.repeat = rep + barline.repeat_times = times + if self.bar is None: self.new_bar() - self.bar.add(barline) + + if rep == 'backward' and not self.bar.has_music() and self.prev_bar: + # If we are ending a repeat, but the current bar has no music (no rests, too), + # use the previous bar + self.prev_bar.add(barline) + else: + self.bar.add(barline) + + def new_ending(self, type, number): + # type must be either 'start', 'stop' or 'discontinue' + # number must be ending number or a list of ending numbers + + ending = xml_objs.BarAttr() + ending.set_ending(type, number) + + if self.bar is None: + self.new_bar() + + if type in ['stop', 'discontinue'] and not self.bar.has_music() and self.prev_bar: + self.prev_bar.add(ending) + else: + self.bar.add(ending) def new_key(self, key_name, mode): if self.bar is None: diff --git a/ly/musicxml/lymus2musxml.py b/ly/musicxml/lymus2musxml.py index ddec52df..0b25b13e 100644 --- a/ly/musicxml/lymus2musxml.py +++ b/ly/musicxml/lymus2musxml.py @@ -197,6 +197,10 @@ def MusicList(self, musicList): self.sims_and_seqs.append('sim') elif musicList.token == '{': self.sims_and_seqs.append('seq') + if isinstance(musicList.parent().parent(), ly.music.items.Alternative): + # The grandparent node is an instance of \alternative, + # this indicates that this musiclist instance is an ending to a \repeat + self.alternative_handler(musicList, 'start') def Chord(self, chord): self.mediator.clear_chord() @@ -450,6 +454,54 @@ def Repeat(self, repeat): elif repeat.specifier() == 'tremolo': self.trem_rep = repeat.repeat_count() + def Alternative(self, alternative): + """\alternative""" + pass + + def alternative_handler(self, node, type): + """ + Helper method for handling alternative endings. + Generates number lists for the number attribute in MusicXML's ending element. + + It tries to follow the same pattern as the default in Lilypond + (see http://lilypond.org/doc/v2.18/Documentation/notation/long-repeats#normal-repeats) + """ + + # Should contain an array of MusicLists + alternative_container = node.parent() + + # Instance of \alternative + alternative_instance = alternative_container.parent() + + # instance of \repeat + repeat_instance = alternative_instance.parent() + + num_repeats = repeat_instance.repeat_count() + num_alternatives = len(alternative_container._children) + + idx = alternative_container.index(node) + 1 + ending_numbers = [idx] + + if num_alternatives < num_repeats: + # If there are fewer ending alternatives than repeats, generate + # ending numbers following the same order as Lilypond. + if idx == 1: + ending_numbers = list(range(1, num_repeats - num_alternatives + 2)) + else: + ending_numbers = [idx + num_repeats - num_alternatives] + + if type == 'start': + self.mediator.new_ending(type, ending_numbers) + elif type == 'stop': + if idx == 1 and num_alternatives < num_repeats: + self.mediator.new_repeat('backward', len(ending_numbers)+1) + elif idx < num_alternatives: + self.mediator.new_repeat('backward') + if idx == num_alternatives: + type = 'discontinue' + + self.mediator.new_ending(type, ending_numbers) + def Tremolo(self, tremolo): """A tremolo item ":".""" if self.look_ahead(tremolo, ly.music.items.Duration): @@ -624,7 +676,9 @@ def End(self, end): self.grace_seq = False elif end.node.token == '\\repeat': if end.node.specifier() == 'volta': - self.mediator.new_repeat('backward') + if not end.node.find_child(ly.music.items.Alternative, 1): + # the repeat does not contain alternative endings, treat as normal repeat + self.mediator.new_repeat('backward', end.node.repeat_count()) elif end.node.specifier() == 'tremolo': if self.look_ahead(end.node, ly.music.items.MusicList): self.mediator.set_tremolo(trem_type="stop") @@ -658,6 +712,8 @@ def End(self, end): if self.sims_and_seqs: self.sims_and_seqs.pop() elif end.node.token == '{': + if isinstance(end.node.parent().parent(), ly.music.items.Alternative): + self.alternative_handler(end.node, 'stop') if self.sims_and_seqs: self.sims_and_seqs.pop() elif end.node.token == '<': #chord diff --git a/ly/musicxml/xml_objs.py b/ly/musicxml/xml_objs.py index 098757b9..39fac695 100644 --- a/ly/musicxml/xml_objs.py +++ b/ly/musicxml/xml_objs.py @@ -134,9 +134,11 @@ def new_xml_bar_attr(self, obj): if obj.new_system: self.musxml.new_system(obj.new_system) if obj.repeat: - self.musxml.add_barline(obj.barline, obj.repeat) + self.musxml.add_barline(obj.barline, obj.repeat, obj.repeat_times) elif obj.barline: self.musxml.add_barline(obj.barline) + elif obj.ending: + self.musxml.add_ending(obj.ending, obj.ending_number) if obj.staves: self.musxml.add_staves(obj.staves) if obj.multiclef: @@ -471,7 +473,20 @@ def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, self.obj_list) def add(self, obj): - self.obj_list.append(obj) + if isinstance(obj, BarAttr): + # Find first idx of instance which is not a BarAttr + idx = len(self.obj_list) + for (i, x) in enumerate(self.obj_list): + if not isinstance(x, BarAttr): + idx = i + break + + if (obj not in self.obj_list): + # The candidate does not exists in our obj_list, add it at a fitting + # location in the list + self.obj_list.insert(idx, obj) + else: + self.obj_list.append(obj) def has_music(self): """ Check if bar contains music. """ @@ -796,6 +811,7 @@ def __init__(self): self.divs = 0 self.barline = None self.repeat = None + self.repeat_times = None self.staves = 0 self.multiclef = [] self.tempo = None @@ -804,9 +820,19 @@ def __init__(self): self.word = None self.new_system = None + # Ending type, 'start', 'stop' or 'discontinue' + self.ending = None + # Ending number should either be a number, or a list of numbers + self.ending_number = None + def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, self.time) + def __eq__(self, other): + if self.__class__ != other.__class__: + return False + return self.__dict__ == other.__dict__ + def add_break(self, force_break): self.new_system = force_break @@ -828,6 +854,10 @@ def set_barline(self, bl): def set_tempo(self, unit=0, unittype='', beats=0, dots=0, text=""): self.tempo = TempoDir(unit, unittype, beats, dots, text) + def set_ending(self, type, number): + self.ending = type + self.ending_number = number + def set_multp_rest(self, size=0): self.multirest = size diff --git a/tests/test_xml.py b/tests/test_xml.py index 33ddfbb3..6afc189f 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -69,6 +69,12 @@ def test_no_barcheck(): compare_output('no_barcheck') +def test_repeat(): + compare_output('repeat') + +def test_repeat_with_alternative(): + compare_output('repeat_with_alternative') + def ly_to_xml(filename): """Read Lilypond file and return XML string.""" writer = ly.musicxml.writer() diff --git a/tests/test_xml_files/repeat.ly b/tests/test_xml_files/repeat.ly new file mode 100644 index 00000000..a228f8b3 --- /dev/null +++ b/tests/test_xml_files/repeat.ly @@ -0,0 +1,43 @@ +\version "2.19.55" + +\header { + title = "repeat" +} + +\score { + \new ChoirStaff << + \new Staff { + \relative c' { + c1 | + \repeat volta 2{ c1| } + d1 | + \repeat volta 3{ d1| } + } + } + + \new Staff + << + \clef treble + \new Voice { + \voiceOne + \relative c' { + c1 | + \repeat volta 2{ d1| } + e1 | + \repeat volta 3{ d1| } + } + } + \new Voice { + \voiceTwo + \relative c' { + f4 f f f | + \repeat volta 2{ g g g g| } + a a a a | + \repeat volta 3{ g2 g| } + } + } + >> + >> + + \layout {} +} diff --git a/tests/test_xml_files/repeat.xml b/tests/test_xml_files/repeat.xml new file mode 100644 index 00000000..87455aca --- /dev/null +++ b/tests/test_xml_files/repeat.xml @@ -0,0 +1,307 @@ + + + + repeat + + + python-ly 0.9.5 + 2016-03-28 + + + + + bracket + + + + + + + + + + + + + 1 + + + G + 2 + + + + + C + 4 + + 4 + 1 + whole + + + + + heavy-light + + + + light-heavy + + + + + C + 4 + + 4 + 1 + whole + + + + + + D + 4 + + 4 + 1 + whole + + + + + heavy-light + + + + light-heavy + + + + + D + 4 + + 4 + 1 + whole + + + + + + + + 1 + + + G + 2 + + + + + C + 4 + + 4 + 1 + whole + + + + F + 4 + + 1 + 2 + quarter + + + + F + 4 + + 1 + 2 + quarter + + + + F + 4 + + 1 + 2 + quarter + + + + F + 4 + + 1 + 2 + quarter + + + + + heavy-light + + + + light-heavy + + + + + D + 4 + + 4 + 1 + whole + + + 4 + + + + G + 4 + + 1 + 2 + quarter + + + + G + 4 + + 1 + 2 + quarter + + + + G + 4 + + 1 + 2 + quarter + + + + G + 4 + + 1 + 2 + quarter + + + + + + E + 4 + + 4 + 1 + whole + + + 4 + + + + A + 4 + + 1 + 2 + quarter + + + + A + 4 + + 1 + 2 + quarter + + + + A + 4 + + 1 + 2 + quarter + + + + A + 4 + + 1 + 2 + quarter + + + + + heavy-light + + + + light-heavy + + + + + D + 4 + + 4 + 1 + whole + + + 4 + + + + G + 4 + + 2 + 2 + half + + + + G + 4 + + 2 + 2 + half + + + + + diff --git a/tests/test_xml_files/repeat_with_alternative.ly b/tests/test_xml_files/repeat_with_alternative.ly new file mode 100644 index 00000000..4c48fc2b --- /dev/null +++ b/tests/test_xml_files/repeat_with_alternative.ly @@ -0,0 +1,35 @@ +\version "2.19.55" + +\header { + title = "repeat with alternative" +} + +\score { + \relative c'{ + % Simple repeat with a simple alternative + c1 | + \repeat volta 2{ c1| } + \alternative { + { d1 | } + { d1 | e1 |} + } + + % Simple repeat with more repeats than alternatives + c1 | + \repeat volta 3{ c1| } + \alternative { + { d1 | } + { d1 | e1 |} + } + + % Simple repeat with many endings + c1 | + \repeat volta 5{ c1| } + \alternative { + { d1 | e1 |} + { d1 | f1 |} + { d1 |} + } + } + \layout {} +} diff --git a/tests/test_xml_files/repeat_with_alternative.xml b/tests/test_xml_files/repeat_with_alternative.xml new file mode 100644 index 00000000..06f69af7 --- /dev/null +++ b/tests/test_xml_files/repeat_with_alternative.xml @@ -0,0 +1,288 @@ + + + + repeat with alternative + + + python-ly 0.9.5 + 2016-03-28 + + + + + + + + + + + 1 + + + G + 2 + + + + + C + 4 + + 4 + 1 + whole + + + + + heavy-light + + + + + C + 4 + + 4 + 1 + whole + + + + + 1. + + + light-heavy + + + + + + + + D + 4 + + 4 + 1 + whole + + + + + 2. + + + + D + 4 + + 4 + 1 + whole + + + + + + + + + E + 4 + + 4 + 1 + whole + + + + + + C + 4 + + 4 + 1 + whole + + + + + heavy-light + + + + + C + 4 + + 4 + 1 + whole + + + + + 1., 2. + + + light-heavy + + + + + + + + D + 4 + + 4 + 1 + whole + + + + + 3. + + + + D + 4 + + 4 + 1 + whole + + + + + + + + + E + 4 + + 4 + 1 + whole + + + + + + C + 4 + + 4 + 1 + whole + + + + + heavy-light + + + + + C + 4 + + 4 + 1 + whole + + + + + 1., 2., 3. + + + + D + 4 + + 4 + 1 + whole + + + + + light-heavy + + + + + + + + E + 4 + + 4 + 1 + whole + + + + + 4. + + + + D + 4 + + 4 + 1 + whole + + + + + light-heavy + + + + + + + + F + 4 + + 4 + 1 + whole + + + + + 5. + + + + + + + D + 4 + + 4 + 1 + whole + + + + +