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