Skip to content

Commit 5a68f8d

Browse files
wattdaveclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 886ea0d commit 5a68f8d

5 files changed

Lines changed: 164 additions & 0 deletions

File tree

‎ly/musicxml/ly2xml_mediator.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ def new_lyric_section(self, name, voice_id):
117117
self.insert_into = lyrics
118118
self.lyric_sections[name] = lyrics
119119

120+
def new_addlyrics_section(self):
121+
r"""Collect \addlyrics into a lyrics section that is merged
122+
into the preceding music when the lyrics end."""
123+
lyrics = xml_objs.LyricsSection('addlyrics', None)
124+
lyrics.merge_into = self.insert_into
125+
self.insert_into = lyrics
126+
self.lyric = None
127+
120128
def check_name(self, name, nr=1):
121129
n = self.get_var_byname(name)
122130
if n:
@@ -275,6 +283,14 @@ def check_lyrics(self, voice_id):
275283
else:
276284
print("Warning can't merge in lyrics!", voice_section)
277285

286+
def check_addlyrics(self):
287+
r"""Merge the finished \addlyrics section into the music it follows."""
288+
lyrics_section = self.insert_into
289+
if self.lyric and self.lyric[1] == 'middle':
290+
self.lyric[1] = 'end'
291+
self.insert_into = lyrics_section.merge_into
292+
self.insert_into.merge_lyrics(lyrics_section)
293+
278294
def check_part(self):
279295
"""Adds the latest active section to the part."""
280296
if len(self.sections)>1:

‎ly/musicxml/lymus2musxml.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,9 @@ def FigureMode(self, figmode):
580580
def LyricMode(self, lyricmode):
581581
r"""A \lyricmode, \lyrics or \addlyrics expression."""
582582
self.alt_mode = 'lyric'
583+
if lyricmode.token == '\\addlyrics':
584+
self.mediator.new_addlyrics_section()
585+
self.sims_and_seqs.append('lyrics')
583586

584587
def Override(self, override):
585588
r"""An \override command."""
@@ -665,6 +668,9 @@ def End(self, end):
665668
elif end.node.token == '\\lyricsto':
666669
self.mediator.check_lyrics(end.node.context_id())
667670
self.sims_and_seqs.pop()
671+
elif end.node.token == '\\addlyrics':
672+
self.mediator.check_addlyrics()
673+
self.sims_and_seqs.pop()
668674
elif end.node.token == '\\with':
669675
self.with_contxt = None
670676
elif end.node.token == '\\drums':

‎tests/test_xml.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ def test_chord_duration():
8080
compare_output('chord_duration')
8181

8282

83+
def test_addlyrics():
84+
compare_output('addlyrics')
85+
86+
8387
def ly_to_xml(filename):
8488
"""Read Lilypond file and return XML string."""
8589
writer = ly.musicxml.writer()

‎tests/test_xml_files/addlyrics.ly‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
\version "2.18.2"
2+
3+
\score {
4+
\new Staff {
5+
\relative c' { d2 c | bes4 bes c8 bes a4 }
6+
}
7+
\addlyrics { Du tout plon -- giet au de -- ses }
8+
}

‎tests/test_xml_files/addlyrics.xml‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 2.0 Partwise//EN"
3+
"http://www.musicxml.org/dtds/partwise.dtd">
4+
<score-partwise version="3.0">
5+
<identification>
6+
<encoding>
7+
<software>python-ly 0.9.10</software>
8+
<encoding-date>2026-09-26</encoding-date>
9+
</encoding>
10+
</identification>
11+
<part-list>
12+
<score-part id="P1">
13+
<part-name />
14+
</score-part>
15+
</part-list>
16+
<part id="P1">
17+
<measure number="1">
18+
<attributes>
19+
<divisions>2</divisions>
20+
<time symbol="common">
21+
<beats>4</beats>
22+
<beat-type>4</beat-type>
23+
</time>
24+
<clef>
25+
<sign>G</sign>
26+
<line>2</line>
27+
</clef>
28+
</attributes>
29+
<note>
30+
<pitch>
31+
<step>D</step>
32+
<octave>4</octave>
33+
</pitch>
34+
<duration>4</duration>
35+
<voice>1</voice>
36+
<type>half</type>
37+
<lyric number="1">
38+
<syllabic>single</syllabic>
39+
<text>Du</text>
40+
</lyric>
41+
</note>
42+
<note>
43+
<pitch>
44+
<step>C</step>
45+
<octave>4</octave>
46+
</pitch>
47+
<duration>4</duration>
48+
<voice>1</voice>
49+
<type>half</type>
50+
<lyric number="1">
51+
<syllabic>single</syllabic>
52+
<text>tout</text>
53+
</lyric>
54+
</note>
55+
</measure>
56+
<measure number="2">
57+
<note>
58+
<pitch>
59+
<step>B</step>
60+
<alter>-1</alter>
61+
<octave>3</octave>
62+
</pitch>
63+
<duration>2</duration>
64+
<voice>1</voice>
65+
<type>quarter</type>
66+
<accidental>flat</accidental>
67+
<lyric number="1">
68+
<syllabic>begin</syllabic>
69+
<text>plon</text>
70+
</lyric>
71+
</note>
72+
<note>
73+
<pitch>
74+
<step>B</step>
75+
<alter>-1</alter>
76+
<octave>3</octave>
77+
</pitch>
78+
<duration>2</duration>
79+
<voice>1</voice>
80+
<type>quarter</type>
81+
<accidental>flat</accidental>
82+
<lyric number="1">
83+
<syllabic>end</syllabic>
84+
<text>giet</text>
85+
</lyric>
86+
</note>
87+
<note>
88+
<pitch>
89+
<step>C</step>
90+
<octave>4</octave>
91+
</pitch>
92+
<duration>1</duration>
93+
<voice>1</voice>
94+
<type>eighth</type>
95+
<lyric number="1">
96+
<syllabic>single</syllabic>
97+
<text>au</text>
98+
</lyric>
99+
</note>
100+
<note>
101+
<pitch>
102+
<step>B</step>
103+
<alter>-1</alter>
104+
<octave>3</octave>
105+
</pitch>
106+
<duration>1</duration>
107+
<voice>1</voice>
108+
<type>eighth</type>
109+
<accidental>flat</accidental>
110+
<lyric number="1">
111+
<syllabic>begin</syllabic>
112+
<text>de</text>
113+
</lyric>
114+
</note>
115+
<note>
116+
<pitch>
117+
<step>A</step>
118+
<octave>3</octave>
119+
</pitch>
120+
<duration>2</duration>
121+
<voice>1</voice>
122+
<type>quarter</type>
123+
<lyric number="1">
124+
<syllabic>end</syllabic>
125+
<text>ses</text>
126+
</lyric>
127+
</note>
128+
</measure>
129+
</part>
130+
</score-partwise>

0 commit comments

Comments
 (0)