diff --git a/ly/musicxml/ly2xml_mediator.py b/ly/musicxml/ly2xml_mediator.py index e8607ea7..e67004e0 100644 --- a/ly/musicxml/ly2xml_mediator.py +++ b/ly/musicxml/ly2xml_mediator.py @@ -78,6 +78,7 @@ def __init__(self): self.prev_tremolo = 8 self.tupl_dur = 0 self.tupl_sum = 0 + self.slur_stack = [] self.multiple_rest = False self.multiple_rest_bar = None self.current_mark = 1 @@ -707,10 +708,21 @@ def tie_to_next(self): self.tied = True self.current_note.set_tie(tie_type) - def set_slur(self, nr, slur_type): + def set_slur(self, nr, slur_type, phrasing=False): """ - Set the slur start or stop for the current note. """ - self.current_note.set_slur(nr, slur_type) + Set the slur start or stop for the current note. + phrasing should be set to True if the slur is meant to be a phrasing mark. + """ + + slur_start = None + + if slur_type == 'stop': + slur_start = self.slur_stack.pop() + + self.current_note.set_slur(nr, slur_type, phrasing, slur_start) + + if slur_type == 'start': + self.slur_stack.append(self.current_note.slur[-1]) def new_articulation(self, art_token): """ diff --git a/ly/musicxml/lymus2musxml.py b/ly/musicxml/lymus2musxml.py index 46effdb5..ddec52df 100644 --- a/ly/musicxml/lymus2musxml.py +++ b/ly/musicxml/lymus2musxml.py @@ -429,9 +429,9 @@ def PhrasingSlur(self, phrslur): if phrslur.token == '\(': self.slurcount += 1 self.phrslurnr = self.slurcount - self.mediator.set_slur(self.phrslurnr, "start") + self.mediator.set_slur(self.phrslurnr, "start", phrasing=True) elif phrslur.token == '\)': - self.mediator.set_slur(self.phrslurnr, "stop") + self.mediator.set_slur(self.phrslurnr, "stop", phrasing=True) self.slurcount -= 1 def Dynamic(self, dynamic): diff --git a/ly/musicxml/xml_objs.py b/ly/musicxml/xml_objs.py index 8cca5e4a..098757b9 100644 --- a/ly/musicxml/xml_objs.py +++ b/ly/musicxml/xml_objs.py @@ -312,6 +312,17 @@ def merge_voice(self, voice, override=False): part.merge_voice(voice, override) +class SlurCount: + """Utility class meant for keeping count of started slurs in a section""" + def __init__(self): + self.count = 0 + + def inc(self): + self.count += 1 + + def dec(self): + self.count -= 1 + class ScoreSection(): """ object to keep track of music section """ def __init__(self, name, glob=False): @@ -319,13 +330,16 @@ def __init__(self, name, glob=False): self.barlist = [] self.glob = glob + # Keeps track of the number of started slurs in the section + self.active_slur_count = SlurCount() + def __repr__(self): return '<{0} {1}>'.format(self.__class__.__name__, self.name) def merge_voice(self, voice, override=False): """Merge in other ScoreSection.""" for org_v, add_v in zip(self.barlist, voice.barlist): - org_v.inject_voice(add_v, override) + org_v.inject_voice(add_v, override, self.active_slur_count) bl_len = len(self.barlist) if len(voice.barlist) > bl_len: self.barlist += voice.barlist[bl_len:] @@ -500,7 +514,7 @@ def is_skip(self, obj_list=None): return False return True - def inject_voice(self, new_voice, override=False): + def inject_voice(self, new_voice, override=False, active_slur_count=None): """ Adding new voice to bar. Omitting double or conflicting bar attributes as long as override is false. Omitting also bars with only skips.""" @@ -519,9 +533,36 @@ def inject_voice(self, new_voice, override=False): pass if not self.is_skip(backup_list): self.create_backup() + + if active_slur_count: + # Update active_slur_count wrt to already existing slur starts + # and slur ends in the bar, before we add backup_list + + for n in self.obj_list: + if isinstance(n, BarNote): + for slur in n.slur: + if slur.slurtype == 'start': + active_slur_count.inc() + elif slur.slurtype == 'stop': + active_slur_count.dec() + for bl in backup_list: self.add(bl) + if active_slur_count and isinstance(bl, BarNote): + # If the slur is starting: increase active_slur_count and set slur number + # to that value. + # If the slur is ending: set slur number to be the same as the origin slur number. + + for slur in bl.slur: + if slur.slurtype == 'start': + active_slur_count.inc() + slur.nr = active_slur_count.count + elif slur.slurtype == 'stop': + active_slur_count.dec() + + if slur.start_node: + slur.nr = slur.start_node.nr class BarMus(): """ Common class for notes and rests. """ @@ -621,11 +662,13 @@ def __init__(self, fraction, ttype, nr, acttype, normtype): self.normtype = normtype class Slur(): - """Stores information about slur.""" - def __init__(self, nr, slurtype): + """Stores information about slur. start_node is only interesting if slurtype is 'stop'. + start_node must be None or a Slur instance.""" + def __init__(self, nr, slurtype, phrasing=False, start_node=None): self.nr = nr self.slurtype = slurtype - + self.phrasing = phrasing + self.start_node = start_node ## # Subclasses of BarMus @@ -668,8 +711,8 @@ def set_octave(self, octave): def set_tie(self, tie_type): self.tie.append(tie_type) - def set_slur(self, nr, slur_type): - self.slur.append(Slur(nr, slur_type)) + def set_slur(self, nr, slur_type, phrasing=False, slur_start_node=None): + self.slur.append(Slur(nr, slur_type, phrasing, slur_start_node)) def add_articulation(self, art_name): self.artic.append(art_name) diff --git a/tests/test_xml.py b/tests/test_xml.py index 07477dca..33ddfbb3 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -30,6 +30,8 @@ def test_dynamics(): def test_tuplet(): compare_output('tuplet') +def test_merge_voice_slurs(): + compare_output('merge_voice_slurs') def test_break(): compare_output('break') diff --git a/tests/test_xml_files/merge_voice_slurs.ly b/tests/test_xml_files/merge_voice_slurs.ly new file mode 100644 index 00000000..bae6d0f4 --- /dev/null +++ b/tests/test_xml_files/merge_voice_slurs.ly @@ -0,0 +1,37 @@ +\version "2.19.55" + +\header { + title = "merge voices with slurs" +} + +sopranoOne = \relative c'' { + % Music follows here. + c1( | c) | + c( | c) | + c( | c) | + + c2( d) | + + c4\( r2 d4( | e) r2 c4\) | +} + +sopranoTwo = \relative c'' { + % Music follows here. + c1( | c) | + r2 c2( | c2) r2 | + r2 c2( | r2 c2) | + + r4 e( f) r4 | + + r4 c4\( d( r4 | r4 d) c4\) r4 | +} + +\score { + \new ChoirStaff << + \new Staff << + \new Voice = "soprano1" { \voiceOne \sopranoOne } + \new Voice = "soprano2" { \voiceTwo \sopranoTwo } + >> + >> + \layout { } +} diff --git a/tests/test_xml_files/merge_voice_slurs.xml b/tests/test_xml_files/merge_voice_slurs.xml new file mode 100644 index 00000000..f55aaeb6 --- /dev/null +++ b/tests/test_xml_files/merge_voice_slurs.xml @@ -0,0 +1,439 @@ + + + + merge voices with slurs + + + python-ly 0.9.4 + 2016-03-28 + + + + + bracket + + + + + + + + + + 1 + + + G + 2 + + + + + C + 5 + + 4 + 1 + whole + + + + + + 4 + + + + C + 5 + + 4 + 2 + whole + + + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + 4 + + + + C + 5 + + 4 + 2 + whole + + + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + 4 + + + + 2 + 2 + half + + + + C + 5 + + 2 + 2 + half + + + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + 4 + + + + C + 5 + + 2 + 2 + half + + + + + + + 2 + 2 + half + + + + + + C + 5 + + 4 + 1 + whole + + + + + + 4 + + + + 2 + 2 + half + + + + C + 5 + + 2 + 2 + half + + + + + + + + + C + 5 + + 4 + 1 + whole + + + + + + 4 + + + + 2 + 2 + half + + + + C + 5 + + 2 + 2 + half + + + + + + + + + C + 5 + + 2 + 1 + half + + + + + + + D + 5 + + 2 + 1 + half + + + + + + 4 + + + + 1 + 2 + quarter + + + + E + 5 + + 1 + 2 + quarter + + + + + + + F + 5 + + 1 + 2 + quarter + + + + + + + 1 + 2 + quarter + + + + + + C + 5 + + 1 + 1 + quarter + + + + + + + 2 + 1 + half + + + + D + 5 + + 1 + 1 + quarter + + + + + + 4 + + + + 1 + 2 + quarter + + + + C + 5 + + 1 + 2 + quarter + + + + + + + D + 5 + + 1 + 2 + quarter + + + + + + + 1 + 2 + quarter + + + + + + E + 5 + + 1 + 1 + quarter + + + + + + + 2 + 1 + half + + + + C + 5 + + 1 + 1 + quarter + + + + + + 4 + + + + 1 + 2 + quarter + + + + D + 5 + + 1 + 2 + quarter + + + + + + + C + 5 + + 1 + 2 + quarter + + + + + + + 1 + 2 + quarter + + + +