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
+
+
+
+
+