Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion ly/musicxml/create_musicxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
28 changes: 25 additions & 3 deletions ly/musicxml/ly2xml_mediator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
18 changes: 18 additions & 0 deletions ly/musicxml/lymus2musxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 = ()
Expand Down Expand Up @@ -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 """
Expand Down Expand Up @@ -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 = ''
Expand Down Expand Up @@ -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
Expand Down
80 changes: 67 additions & 13 deletions ly/musicxml/xml_objs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If #81 is merged, this block can be uncommented.

# # 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.
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 5 additions & 0 deletions tests/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
27 changes: 27 additions & 0 deletions tests/test_xml_files/lyrics_simple_addlyrics.ly
Original file line number Diff line number Diff line change
@@ -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 { }
}
Loading