From 5a68f8d2e9722e3a875fbbefc751be1ba68965ea Mon Sep 17 00:00:00 2001 From: David Watt Date: Sat, 26 Sep 2026 21:41:47 -0700 Subject: [PATCH] Fix crash on \addlyrics in MusicXML export Lyrics following music via \addlyrics were appended as plain lists directly into the barlist of the current part, so iterating the part crashed with "AttributeError: 'list' object has no attribute 'pickup'". Collect \addlyrics into a LyricsSection and merge it into the preceding music when it ends, as is already done for \lyricsto. This also makes the lyrics appear in the exported MusicXML. Co-Authored-By: Claude Opus 5.5 --- ly/musicxml/ly2xml_mediator.py | 16 ++++ ly/musicxml/lymus2musxml.py | 6 ++ tests/test_xml.py | 4 + tests/test_xml_files/addlyrics.ly | 8 ++ tests/test_xml_files/addlyrics.xml | 130 +++++++++++++++++++++++++++++ 5 files changed, 164 insertions(+) create mode 100644 tests/test_xml_files/addlyrics.ly create mode 100644 tests/test_xml_files/addlyrics.xml 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..8a30750 100644 --- a/ly/musicxml/lymus2musxml.py +++ b/ly/musicxml/lymus2musxml.py @@ -580,6 +580,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.""" @@ -665,6 +668,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..68b379e 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -80,6 +80,10 @@ def test_chord_duration(): compare_output('chord_duration') +def test_addlyrics(): + compare_output('addlyrics') + + 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 + + + + +