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