diff --git a/ly/musicxml/create_musicxml.py b/ly/musicxml/create_musicxml.py
index 3142abc7..83febb58 100644
--- a/ly/musicxml/create_musicxml.py
+++ b/ly/musicxml/create_musicxml.py
@@ -649,7 +649,13 @@ def add_sound_dir(self, midi_tempo):
def add_lyric(self, txt, syll, nr, ext=False):
""" Add lyric element. """
- lyricnode = etree.SubElement(self.current_note, "lyric", number=str(nr))
+ lyricnode = etree.SubElement(self.current_note, "lyric")
+
+ if isinstance(nr, int) or nr.isnumeric():
+ lyricnode.attrib['number'] = str(nr)
+ else:
+ lyricnode.attrib['name'] = str(nr)
+
syllnode = etree.SubElement(lyricnode, "syllabic")
syllnode.text = syll
txtnode = etree.SubElement(lyricnode, "text")
diff --git a/ly/musicxml/ly2xml_mediator.py b/ly/musicxml/ly2xml_mediator.py
index e8607ea7..0f17045b 100644
--- a/ly/musicxml/ly2xml_mediator.py
+++ b/ly/musicxml/ly2xml_mediator.py
@@ -44,6 +44,10 @@ def __init__(self):
self.sections = []
""" default and initial values """
self.insert_into = None
+
+ # Current music section are useful when using "\addlyrics"
+ self.current_music_section = None
+
self.current_note = None
self.current_lynote = None
self.current_is_rest = False
@@ -99,6 +103,8 @@ def new_header_assignment(self, name, value):
def new_section(self, name, glob=False):
name = self.check_name(name)
section = xml_objs.ScoreSection(name, glob)
+
+ self.current_music_section = section
self.insert_into = section
self.sections.append(section)
self.bar = None
@@ -157,6 +163,8 @@ def new_part(self, pid=None, to_part=None, piano=False):
self.group.partlist.append(self.part)
else:
self.score.partlist.append(self.part)
+
+ self.current_music_section = self.part
self.insert_into = self.part
self.bar = None
@@ -270,9 +278,17 @@ def check_lyrics(self, voice_id):
lyrics_section = self.lyric_sections['lyricsto'+voice_id]
voice_section = self.get_var_byname(lyrics_section.voice_id)
if voice_section:
- voice_section.merge_lyrics(lyrics_section)
+ voice_section.merge_lyrics(lyrics_section, voice_id)
else:
- print("Warning can't merge in lyrics!", voice_section)
+ voice_section = self.score.find_section_for_voice(voice_id, self.sections)
+ if not voice_section:
+ # A potentially slow path, search the whole score
+ voice_section = self.score.find_section_for_voice(voice_id)
+ if voice_section:
+ # Must explicitly only merge with notes with the same voice_id
+ voice_section.merge_lyrics(lyrics_section, voice_id)
+ else:
+ print("Warning can't merge in lyrics!", voice_section)
def check_part(self):
"""Adds the latest active section to the part."""
@@ -333,6 +349,12 @@ def new_bar(self, fill_prev=True):
def add_to_bar(self, obj):
if self.bar is None:
self.new_bar()
+
+ if isinstance(obj, xml_objs.BarMus):
+ # assign the voice context the obj belongs to, useful when we must modify the note
+ # after section merging, eg. set lyrics to a named voice
+ obj.voice_context = self.insert_into.name
+
self.bar.add(obj)
def create_barline(self, bl):
@@ -911,7 +933,7 @@ def new_lyrics_item(self, item):
self.lyric_syll = True
elif item == '__':
self.lyric.append("extend")
- elif item == '\\skip':
+ elif item == '\\skip' or item == '_':
self.insert_into.barlist.append("skip")
def duration_from_tokens(self, tokens):
diff --git a/ly/musicxml/lymus2musxml.py b/ly/musicxml/lymus2musxml.py
index 46effdb5..ef5489c9 100644
--- a/ly/musicxml/lymus2musxml.py
+++ b/ly/musicxml/lymus2musxml.py
@@ -241,6 +241,8 @@ def check_context(self, context, context_id=None, token=""):
self.mediator.new_section('voice')
elif context == 'Devnull':
self.mediator.new_section('devnull', True)
+ elif context == 'Lyrics':
+ pass
else:
print("Context not implemented:", context)
@@ -336,6 +338,11 @@ def check_tuplet(self):
def Duration(self, duration):
"""A written duration"""
+
+ if 'lyrics' in self.sims_and_seqs:
+ # Skip the duration of the skip when typing lyrics
+ return
+
if self.tempo:
self.mediator.new_tempo(duration.token, duration.tokens, *self.tempo)
self.tempo = ()
@@ -478,6 +485,9 @@ def Set(self, cont_set):
self.mediator.set_by_property(cont_set.property(), val)
elif cont_set.context() in group_contexts:
self.mediator.set_by_property(cont_set.property(), val, group=True)
+ elif cont_set.property() == 'stanza' and self.alt_mode == 'lyric':
+ self.mediator.set_by_property(cont_set.property(), val)
+
def Command(self, command):
r""" \bar, \rest etc """
@@ -581,6 +591,11 @@ def LyricMode(self, lyricmode):
r"""A \lyricmode, \lyrics or \addlyrics expression."""
self.alt_mode = 'lyric'
+ if lyricmode.token == '\\addlyrics':
+ section = self.mediator.current_music_section
+ self.mediator.new_lyric_section('lyricsto' + section.name, section.name)
+ self.sims_and_seqs.append('lyrics')
+
def Override(self, override):
r"""An \override command."""
self.override_key = ''
@@ -672,6 +687,9 @@ def End(self, end):
elif isinstance(end.node, ly.music.items.Relative):
self.relative = False
self.rel_pitch_isset = False
+ elif isinstance(end.node, ly.music.items.LyricMode) and end.node.token == '\\addlyrics':
+ self.mediator.check_lyrics(self.mediator.insert_into.voice_id)
+ self.sims_and_seqs.pop()
else:
# print("end:", end.node.token)
pass
diff --git a/ly/musicxml/xml_objs.py b/ly/musicxml/xml_objs.py
index 8cca5e4a..0fc8d42a 100644
--- a/ly/musicxml/xml_objs.py
+++ b/ly/musicxml/xml_objs.py
@@ -292,6 +292,23 @@ def debug_group(g):
for i in self.partlist:
debug_group(i)
+ def find_section_for_voice(self, voice_context, partlist = None):
+ if not partlist:
+ partlist = self.partlist
+
+ for section in partlist[::-1]:
+ # Iterate over sections in partlist, in reverser order (newest to oldest)
+ if isinstance(section, ScorePartGroup):
+ section_candidate = self.find_section_for_voice(voice_context, section.partlist)
+ if section_candidate:
+ return section_candidate
+ elif isinstance(section, ScoreSection):
+ for bar in section.barlist:
+ for obj in bar.obj_list:
+ if isinstance(obj, BarMus) and obj.voice_context == voice_context:
+ return section
+ return None
+
class ScorePartGroup():
"""Object to keep track of part group."""
@@ -330,30 +347,62 @@ def merge_voice(self, voice, override=False):
if len(voice.barlist) > bl_len:
self.barlist += voice.barlist[bl_len:]
- def merge_lyrics(self, lyrics):
- """Merge in lyrics in music section."""
+ def merge_lyrics(self, lyrics, voice_context=None):
+ """
+ Merge in lyrics in music section.
+ If voice_context is set, it will only merge with notes that has the same voice_context
+ """
i = 0
- ext = False
+
+ # If we are at the end or inside the slur, but not at the start
+ inside_slur = False
+ inside_tie = False
+
for bar in self.barlist:
for obj in bar.obj_list:
- if isinstance(obj, BarNote):
- if ext:
- if obj.slur:
- ext = False
- else:
+ if isinstance(obj, BarNote) and \
+ not obj.chord and \
+ (not voice_context or obj.voice_context == voice_context):
+
+ tie_started = False
+ tie_stopped = False
+
+ # Ties can both start and stop at the same note, prefer starts
+ if 'start' in obj.tie:
+ tie_started = True
+ elif 'stop' in obj.tie:
+ tie_stopped = True
+
+ slur_started = False
+ slur_stopped = False
+
+ for slur in obj.slur:
+ #if slur.phrasing:
+ # # ignore slur object if it is a phrasing mark
+ # continue
+ if slur.slurtype == 'start':
+ slur_started = True
+ elif slur.slurtype == 'stop':
+ slur_stopped = True
+
+ if not inside_tie and not inside_slur:
try:
l = lyrics.barlist[i]
except IndexError:
break
if l != 'skip':
- try:
- if l[3] == "extend" and obj.slur:
- ext = True
- except IndexError:
- pass
obj.add_lyric(l)
i += 1
+ if slur_started:
+ inside_slur = True
+ elif slur_stopped:
+ inside_slur = False
+
+ if tie_started:
+ inside_tie = True
+ elif tie_stopped:
+ inside_tie = False
class Snippet(ScoreSection):
""" Short section intended to be merged.
@@ -537,6 +586,11 @@ def __init__(self, duration, voice=1):
self.dynamic = []
self.oct_shift = None
+ # set to some object identifying the context this belongs to, eg. string.
+ # It is useful to set this before merging a voice into another section
+ # This helps when adding lyrics to named voices outside a score context
+ self.voice_context = None
+
def __repr__(self):
return '<{0} {1}>'.format(self.__class__.__name__, self.duration)
diff --git a/tests/test_xml.py b/tests/test_xml.py
index 07477dca..7b92d915 100644
--- a/tests/test_xml.py
+++ b/tests/test_xml.py
@@ -30,6 +30,11 @@ def test_dynamics():
def test_tuplet():
compare_output('tuplet')
+def test_lyrics_simple_lyricsto():
+ compare_output('lyrics_simple_lyricsto')
+
+def test_lyrics_simple_addlyrics():
+ compare_output('lyrics_simple_addlyrics')
def test_break():
compare_output('break')
diff --git a/tests/test_xml_files/lyrics_simple_addlyrics.ly b/tests/test_xml_files/lyrics_simple_addlyrics.ly
new file mode 100644
index 00000000..d9880f30
--- /dev/null
+++ b/tests/test_xml_files/lyrics_simple_addlyrics.ly
@@ -0,0 +1,27 @@
+\version "2.19.55"
+
+\header {
+ title = "lyrics simple addlyrics"
+}
+
+verseOne = \lyricmode {
+ \set stanza = "v1"
+ My long sing song for voice one
+}
+verseTwo = \lyricmode {
+ \set stanza = "v2"
+ This is verse two, the last verse
+}
+
+\score {
+ \new Staff {
+ \relative c' {
+ \clef treble
+ c'32 c c16 c8 c4 c2 |
+ c1 |
+ }
+ }
+ \addlyrics \verseOne
+ \addlyrics \verseTwo
+ \layout { }
+}
\ No newline at end of file
diff --git a/tests/test_xml_files/lyrics_simple_addlyrics.xml b/tests/test_xml_files/lyrics_simple_addlyrics.xml
new file mode 100644
index 00000000..30414e16
--- /dev/null
+++ b/tests/test_xml_files/lyrics_simple_addlyrics.xml
@@ -0,0 +1,154 @@
+
+
+
+ lyrics simple addlyrics
+
+
+ python-ly 0.9.5
+ 2016-03-28
+
+
+
+
+
+
+
+
+
+
+ 8
+
+
+ G
+ 2
+
+
+
+
+ C
+ 5
+
+ 1
+ 1
+ 32nd
+
+ single
+ My
+
+
+ single
+ This
+
+
+
+
+ C
+ 5
+
+ 1
+ 1
+ 32nd
+
+ single
+ long
+
+
+ single
+ is
+
+
+
+
+ C
+ 5
+
+ 2
+ 1
+ 16th
+
+ single
+ sing
+
+
+ single
+ verse
+
+
+
+
+ C
+ 5
+
+ 4
+ 1
+ eighth
+
+ single
+ song
+
+
+ single
+ two,
+
+
+
+
+ C
+ 5
+
+ 8
+ 1
+ quarter
+
+ single
+ for
+
+
+ single
+ the
+
+
+
+
+ C
+ 5
+
+ 16
+ 1
+ half
+
+ single
+ voice
+
+
+ single
+ last
+
+
+
+
+
+
+ C
+ 5
+
+ 32
+ 1
+ whole
+
+ single
+ one
+
+
+ single
+ verse
+
+
+
+
+
+
diff --git a/tests/test_xml_files/lyrics_simple_lyricsto.ly b/tests/test_xml_files/lyrics_simple_lyricsto.ly
new file mode 100644
index 00000000..4cbd6cc6
--- /dev/null
+++ b/tests/test_xml_files/lyrics_simple_lyricsto.ly
@@ -0,0 +1,42 @@
+\version "2.19.55"
+
+\header {
+ title = "lyrics simple lyricsto"
+}
+
+verse = \lyricmode {
+ \set stanza = "1"
+ Do -- re -- mi fa
+ Hel -- lo __ the -- re
+}
+
+chorus = \lyricmode {
+ \set stanza = "chor"
+ This is the cho -- rus, la __ di -- da
+}
+
+\score {
+ \new Staff {
+ <<
+ \new Voice = "main" {
+ \voiceOne
+ \clef treble
+ \relative c' {
+ c4 d e f |
+ g16 a4( f8.) b4 c4 |
+ }
+ }
+ \new Voice = "mainTwo" {
+ \voiceTwo
+ \clef treble
+ \relative c' {
+ c2 c | g' g |
+ }
+ }
+ \new Lyrics \lyricsto "main" \verse
+ \new Lyrics \lyricsto "main" \chorus
+ >>
+ }
+ \layout { }
+}
+\voiceTwo
\ No newline at end of file
diff --git a/tests/test_xml_files/lyrics_simple_lyricsto.xml b/tests/test_xml_files/lyrics_simple_lyricsto.xml
new file mode 100644
index 00000000..ca61b217
--- /dev/null
+++ b/tests/test_xml_files/lyrics_simple_lyricsto.xml
@@ -0,0 +1,231 @@
+
+
+
+ lyrics simple lyricsto
+
+
+ python-ly 0.9.5
+ 2016-03-28
+
+
+
+
+
+
+
+
+
+
+ 4
+
+
+ G
+ 2
+
+
+
+
+ C
+ 4
+
+ 4
+ 1
+ quarter
+
+ begin
+ Do
+
+
+ single
+ This
+
+
+
+
+ D
+ 4
+
+ 4
+ 1
+ quarter
+
+ middle
+ re
+
+
+ single
+ is
+
+
+
+
+ E
+ 4
+
+ 4
+ 1
+ quarter
+
+ end
+ mi
+
+
+ single
+ the
+
+
+
+
+ F
+ 4
+
+ 4
+ 1
+ quarter
+
+ single
+ fa
+
+
+ begin
+ cho
+
+
+
+ 16
+
+
+
+ C
+ 4
+
+ 8
+ 2
+ half
+
+
+
+ C
+ 4
+
+ 8
+ 2
+ half
+
+
+
+
+
+ G
+ 4
+
+ 1
+ 1
+ 16th
+
+ begin
+ Hel
+
+
+ end
+ rus,
+
+
+
+
+ A
+ 4
+
+ 4
+ 1
+ quarter
+
+
+
+
+ end
+ lo
+
+
+
+ single
+ la
+
+
+
+
+
+ F
+ 4
+
+ 3
+ 1
+ eighth
+
+
+
+
+
+
+
+ B
+ 4
+
+ 4
+ 1
+ quarter
+
+ begin
+ the
+
+
+ begin
+ di
+
+
+
+
+ C
+ 5
+
+ 4
+ 1
+ quarter
+
+ end
+ re
+
+
+ end
+ da
+
+
+
+ 16
+
+
+
+ G
+ 4
+
+ 8
+ 2
+ half
+
+
+
+ G
+ 4
+
+ 8
+ 2
+ half
+
+
+
+
+