Skip to content

Commit 4919319

Browse files
Support for comparison of lyrics (#7)
* Support comparison of lyrics. * Make the lyric string a bit more thorough.
1 parent b33c955 commit 4919319

File tree

3 files changed

+86
-3
lines changed

3 files changed

+86
-3
lines changed

musicdiff/annotation.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
__docformat__ = "google"
1616

1717
from fractions import Fraction
18-
from typing import Optional
18+
from typing import Optional, List
1919

2020
import music21 as m21
2121

@@ -92,6 +92,23 @@ def __init__(self, general_note: m21.note.GeneralNote, enhanced_beam_list, tuple
9292
if self.expressions:
9393
self.expressions.sort()
9494

95+
# lyrics
96+
self.lyrics: List[str] = []
97+
for lyric in general_note.lyrics:
98+
lyricStr: str = ""
99+
if lyric.number is not None:
100+
lyricStr += f"number={lyric.number}"
101+
if lyric._identifier is not None:
102+
lyricStr += f" identifier={lyric._identifier}"
103+
if lyric.syllabic is not None:
104+
lyricStr += f" syllabic={lyric.syllabic}"
105+
if lyric.text is not None:
106+
lyricStr += f" text={lyric.text}"
107+
lyricStr += f" rawText={lyric.rawText}"
108+
if M21Utils.has_style(lyric):
109+
lyricStr += f" style={M21Utils.obj_to_styledict(lyric, detail)}"
110+
self.lyrics.append(lyricStr)
111+
95112
# precomputed representations for faster comparison
96113
self.precomputed_str = self.__str__()
97114

@@ -116,13 +133,15 @@ def notation_size(self):
116133
size += len(self.articulations)
117134
# add for the expressions
118135
size += len(self.expressions)
136+
# add for the lyrics
137+
size += len(self.lyrics)
119138
return size
120139

121140
def __repr__(self):
122141
# does consider the MEI id!
123142
return (f"{self.pitches},{self.note_head},{self.dots},{self.beamings}," +
124-
f"{self.tuplets},{self.general_note},{self.articulations},{self.expressions}" +
125-
f"{self.styledict}")
143+
f"{self.tuplets},{self.general_note},{self.articulations},{self.expressions}," +
144+
f"{self.lyrics},{self.styledict}")
126145

127146
def __str__(self):
128147
"""
@@ -172,6 +191,9 @@ def __str__(self):
172191
if len(self.expressions) > 0: # add for articulations
173192
for e in self.expressions:
174193
string += e
194+
if len(self.lyrics) > 0: # add for lyrics
195+
for lyric in self.lyrics:
196+
string += lyric
175197

176198
if self.noteshape != 'normal':
177199
string += f"noteshape={self.noteshape}"

musicdiff/comparison.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,18 @@ def _annotated_note_diff(annNote1: AnnNote, annNote2: AnnNote):
637637
)
638638
op_list.extend(expr_op_list)
639639
cost += expr_cost
640+
# add for the lyrics
641+
if annNote1.lyrics != annNote2.lyrics:
642+
lyr_op_list, lyr_cost = Comparison._generic_leveinsthein_diff(
643+
annNote1.lyrics,
644+
annNote2.lyrics,
645+
annNote1,
646+
annNote2,
647+
"lyric",
648+
)
649+
op_list.extend(lyr_op_list)
650+
cost += lyr_cost
651+
640652
# add for noteshape
641653
if annNote1.noteshape != annNote2.noteshape:
642654
cost += 1

musicdiff/visualization.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,55 @@ def mark_diffs(
896896
textExp.style.color = Visualization.CHANGED_COLOR
897897
note2.activeSite.insert(note2.offset, textExp)
898898

899+
# lyrics
900+
elif op[0] == "inslyric":
901+
assert isinstance(op[1], AnnNote)
902+
assert isinstance(op[2], AnnNote)
903+
# color the modified note in both scores using Visualization.INSERTED_COLOR
904+
note1 = score1.recurse().getElementById(op[1].general_note)
905+
note1.style.color = Visualization.INSERTED_COLOR
906+
textExp = m21.expressions.TextExpression("inserted lyric")
907+
textExp.style.color = Visualization.INSERTED_COLOR
908+
note1.activeSite.insert(note1.offset, textExp)
909+
910+
note2 = score2.recurse().getElementById(op[2].general_note)
911+
note2.style.color = Visualization.INSERTED_COLOR
912+
textExp = m21.expressions.TextExpression("inserted lyric")
913+
textExp.style.color = Visualization.INSERTED_COLOR
914+
note2.activeSite.insert(note2.offset, textExp)
915+
916+
elif op[0] == "dellyric":
917+
assert isinstance(op[1], AnnNote)
918+
assert isinstance(op[2], AnnNote)
919+
# color the modified note in both scores using Visualization.DELETED_COLOR
920+
note1 = score1.recurse().getElementById(op[1].general_note)
921+
note1.style.color = Visualization.DELETED_COLOR
922+
textExp = m21.expressions.TextExpression("deleted lyric")
923+
textExp.style.color = Visualization.DELETED_COLOR
924+
note1.activeSite.insert(note1.offset, textExp)
925+
926+
note2 = score2.recurse().getElementById(op[2].general_note)
927+
note2.style.color = Visualization.DELETED_COLOR
928+
textExp = m21.expressions.TextExpression("deleted lyric")
929+
textExp.style.color = Visualization.DELETED_COLOR
930+
note2.activeSite.insert(note2.offset, textExp)
931+
932+
elif op[0] == "editlyric":
933+
assert isinstance(op[1], AnnNote)
934+
assert isinstance(op[2], AnnNote)
935+
# color the modified note (in both scores) using Visualization.CHANGED_COLOR
936+
note1 = score1.recurse().getElementById(op[1].general_note)
937+
note1.style.color = Visualization.CHANGED_COLOR
938+
textExp = m21.expressions.TextExpression("changed lyric")
939+
textExp.style.color = Visualization.CHANGED_COLOR
940+
note1.activeSite.insert(note1.offset, textExp)
941+
942+
note2 = score2.recurse().getElementById(op[2].general_note)
943+
note2.style.color = Visualization.CHANGED_COLOR
944+
textExp = m21.expressions.TextExpression("changed lyric")
945+
textExp.style.color = Visualization.CHANGED_COLOR
946+
note2.activeSite.insert(note2.offset, textExp)
947+
899948
else:
900949
print(f"Annotation type {op[0]} not yet supported for visualization", file=sys.stderr)
901950

0 commit comments

Comments
 (0)