diff --git a/ly/musicxml/ly2xml_mediator.py b/ly/musicxml/ly2xml_mediator.py index e8c7d72..fa65549 100644 --- a/ly/musicxml/ly2xml_mediator.py +++ b/ly/musicxml/ly2xml_mediator.py @@ -117,6 +117,14 @@ def new_lyric_section(self, name, voice_id): self.insert_into = lyrics self.lyric_sections[name] = lyrics + def new_addlyrics_section(self): + r"""Collect \addlyrics into a lyrics section that is merged + into the preceding music when the lyrics end.""" + lyrics = xml_objs.LyricsSection('addlyrics', None) + lyrics.merge_into = self.insert_into + self.insert_into = lyrics + self.lyric = None + def check_name(self, name, nr=1): n = self.get_var_byname(name) if n: @@ -275,6 +283,14 @@ def check_lyrics(self, voice_id): else: print("Warning can't merge in lyrics!", voice_section) + def check_addlyrics(self): + r"""Merge the finished \addlyrics section into the music it follows.""" + lyrics_section = self.insert_into + if self.lyric and self.lyric[1] == 'middle': + self.lyric[1] = 'end' + self.insert_into = lyrics_section.merge_into + self.insert_into.merge_lyrics(lyrics_section) + def check_part(self): """Adds the latest active section to the part.""" if len(self.sections)>1: diff --git a/ly/musicxml/lymus2musxml.py b/ly/musicxml/lymus2musxml.py index 11ed001..c5f4e53 100644 --- a/ly/musicxml/lymus2musxml.py +++ b/ly/musicxml/lymus2musxml.py @@ -76,6 +76,7 @@ def __init__(self): self.numericTime = False self.voice_sep = False self.sims_and_seqs = [] + self.implicit_staff = [] self.override_dict = {} self.ottava = False self.with_contxt = None @@ -235,6 +236,10 @@ def check_context(self, context, context_id=None, token=""): self.mediator.add_staff_id(context_id) elif context == 'Voice': self.sims_and_seqs.append('voice') + # A Voice outside any Staff gets an implicit Staff (and part) + self.implicit_staff.append(self.mediator.part is None) + if self.implicit_staff[-1]: + self.mediator.new_part() if context_id: self.mediator.new_section(context_id) else: @@ -580,6 +585,9 @@ def FigureMode(self, figmode): def LyricMode(self, lyricmode): r"""A \lyricmode, \lyrics or \addlyrics expression.""" self.alt_mode = 'lyric' + if lyricmode.token == '\\addlyrics': + self.mediator.new_addlyrics_section() + self.sims_and_seqs.append('lyrics') def Override(self, override): r"""An \override command.""" @@ -636,6 +644,8 @@ def End(self, end): if end.node.context() == 'Voice': self.mediator.check_voices() self.sims_and_seqs.pop() + if self.implicit_staff.pop(): + self.mediator.check_part() elif end.node.context() in group_contexts: self.mediator.close_group() elif end.node.context() in staff_contexts: @@ -665,6 +675,9 @@ def End(self, end): elif end.node.token == '\\lyricsto': self.mediator.check_lyrics(end.node.context_id()) self.sims_and_seqs.pop() + elif end.node.token == '\\addlyrics': + self.mediator.check_addlyrics() + self.sims_and_seqs.pop() elif end.node.token == '\\with': self.with_contxt = None elif end.node.token == '\\drums': diff --git a/tests/test_xml.py b/tests/test_xml.py index e5ca26b..b00fac7 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -80,6 +80,14 @@ def test_chord_duration(): compare_output('chord_duration') +def test_addlyrics(): + compare_output('addlyrics') + + +def test_voice_in_group(): + compare_output('voice_in_group') + + def ly_to_xml(filename): """Read Lilypond file and return XML string.""" writer = ly.musicxml.writer() diff --git a/tests/test_xml_files/addlyrics.ly b/tests/test_xml_files/addlyrics.ly new file mode 100644 index 0000000..974f468 --- /dev/null +++ b/tests/test_xml_files/addlyrics.ly @@ -0,0 +1,8 @@ +\version "2.18.2" + +\score { + \new Staff { + \relative c' { d2 c | bes4 bes c8 bes a4 } + } + \addlyrics { Du tout plon -- giet au de -- ses } +} diff --git a/tests/test_xml_files/addlyrics.xml b/tests/test_xml_files/addlyrics.xml new file mode 100644 index 0000000..bf40726 --- /dev/null +++ b/tests/test_xml_files/addlyrics.xml @@ -0,0 +1,130 @@ + + + + + + python-ly 0.9.10 + 2026-09-26 + + + + + + + + + + + 2 + + + G + 2 + + + + + D + 4 + + 4 + 1 + half + + single + Du + + + + + C + 4 + + 4 + 1 + half + + single + tout + + + + + + + B + -1 + 3 + + 2 + 1 + quarter + flat + + begin + plon + + + + + B + -1 + 3 + + 2 + 1 + quarter + flat + + end + giet + + + + + C + 4 + + 1 + 1 + eighth + + single + au + + + + + B + -1 + 3 + + 1 + 1 + eighth + flat + + begin + de + + + + + A + 3 + + 2 + 1 + quarter + + end + ses + + + + + diff --git a/tests/test_xml_files/voice_in_group.ly b/tests/test_xml_files/voice_in_group.ly new file mode 100644 index 0000000..2ed8beb --- /dev/null +++ b/tests/test_xml_files/voice_in_group.ly @@ -0,0 +1,10 @@ +\version "2.18.2" + +\score { + \new StaffGroup << + \new Voice = "S" { \set Staff.instrumentName = #"Cantus" c''2 d'' } + \addlyrics { Du tout } + \new Voice = "B" { \set Staff.instrumentName = #"Bassus" c2 g, } + \addlyrics { plon giet } + >> +} diff --git a/tests/test_xml_files/voice_in_group.xml b/tests/test_xml_files/voice_in_group.xml new file mode 100644 index 0000000..fae729f --- /dev/null +++ b/tests/test_xml_files/voice_in_group.xml @@ -0,0 +1,105 @@ + + + + + + python-ly 0.9.10 + 2026-09-26 + + + + + bracket + + + Cantus + + + Bassus + + + + + + + 1 + + + G + 2 + + + + + C + 5 + + 2 + 1 + half + + single + Du + + + + + D + 5 + + 2 + 1 + half + + single + tout + + + + + + + + 1 + + + G + 2 + + + + + C + 3 + + 2 + 1 + half + + single + plon + + + + + G + 2 + + 2 + 1 + half + + single + giet + + + + +