Skip to content

Commit a26c2e3

Browse files
Instead of a note offset (in measure) for every note, compute what the gap is between the note and the preceding note (or start of measure, if it's the first note in the voice/measure). That way most notes have the same zero gap, so we don't get a ton of spurious diffs when a gap/space is inserted in one of the two scores being diffed.
1 parent 8fdc0f4 commit a26c2e3

File tree

3 files changed

+80
-42
lines changed

3 files changed

+80
-42
lines changed

musicdiff/annotation.py

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import typing as t
2020

2121
import music21 as m21
22-
from music21.common.numberTools import OffsetQL
22+
from music21.common.numberTools import OffsetQL, opFrac
2323

2424
from musicdiff import M21Utils
2525
from musicdiff import DetailLevel
@@ -28,7 +28,7 @@ class AnnNote:
2828
def __init__(
2929
self,
3030
general_note: m21.note.GeneralNote,
31-
offsetInMeasure: OffsetQL,
31+
gap_dur: OffsetQL,
3232
enhanced_beam_list: list[str],
3333
tuplet_list: list[str],
3434
tuplet_info: list[str],
@@ -39,6 +39,8 @@ def __init__(
3939
4040
Args:
4141
general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
42+
gap_dur (OffsetQL): gap since end of last note (or since start of measure, if
43+
first note in measure). Usually zero.
4244
enhanced_beam_list (list): A list of beaming information about this GeneralNote.
4345
tuplet_list (list): A list of tuplet info about this GeneralNote.
4446
detail (DetailLevel): What level of detail to use during the diff.
@@ -48,7 +50,7 @@ def __init__(
4850
4951
"""
5052
self.general_note: int | str = general_note.id
51-
self.offsetInMeasure: OffsetQL = offsetInMeasure
53+
self.gap_dur: OffsetQL = gap_dur
5254
self.beamings: list[str] = enhanced_beam_list
5355
self.tuplets: list[str] = tuplet_list
5456
self.tuplet_info: list[str] = tuplet_info
@@ -244,28 +246,31 @@ def __str__(self) -> str:
244246

245247
if len(self.articulations) > 0: # add for articulations
246248
for a in self.articulations:
247-
string += a
249+
string += ' ' + a
248250
if len(self.expressions) > 0: # add for articulations
249251
for e in self.expressions:
250-
string += e
252+
string += ' ' + e
251253
if len(self.lyrics) > 0: # add for lyrics
252254
for lyric in self.lyrics:
253-
string += lyric
255+
string += ' ' + lyric
254256

255257
if self.noteshape != 'normal':
256-
string += f"noteshape={self.noteshape}"
258+
string += f" noteshape={self.noteshape}"
257259
if self.noteheadFill is not None:
258-
string += f"noteheadFill={self.noteheadFill}"
260+
string += f" noteheadFill={self.noteheadFill}"
259261
if self.noteheadParenthesis:
260-
string += f"noteheadParenthesis={self.noteheadParenthesis}"
262+
string += f" noteheadParenthesis={self.noteheadParenthesis}"
261263
if self.stemDirection != 'unspecified':
262-
string += f"stemDirection={self.stemDirection}"
264+
string += f" stemDirection={self.stemDirection}"
263265

264-
# offset
265-
string += f" {self.offsetInMeasure}"
266+
# gap_dur
267+
if self.gap_dur != 0:
268+
string += f" spaceBefore={self.gap_dur}"
266269

267270
# and then the style fields
268271
for i, (k, v) in enumerate(self.styledict.items()):
272+
if i == 0:
273+
string += ' '
269274
if i > 0:
270275
string += ","
271276
string += f"{k}={v}"
@@ -286,25 +291,6 @@ def __eq__(self, other) -> bool:
286291
# equality does not consider the MEI id!
287292
return self.precomputed_str == other.precomputed_str
288293

289-
# if not isinstance(other, AnnNote):
290-
# return False
291-
# elif self.pitches != other.pitches:
292-
# return False
293-
# elif self.note_head != other.note_head:
294-
# return False
295-
# elif self.dots != other.dots:
296-
# return False
297-
# elif self.beamings != other.beamings:
298-
# return False
299-
# elif self.tuplets != other.tuplets:
300-
# return False
301-
# elif self.articulations != other.articulations:
302-
# return False
303-
# elif self.expressions != other.expressions:
304-
# return False
305-
# else:
306-
# return True
307-
308294

309295
class AnnExtra:
310296
def __init__(
@@ -466,11 +452,21 @@ def __init__(
466452
# create a list of notes with beaming and tuplets information attached
467453
self.annot_notes = []
468454
for i, n in enumerate(note_list):
469-
offset: OffsetQL = n.getOffsetInHierarchy(enclosingMeasure)
455+
expectedOffsetInMeas: OffsetQL = 0
456+
if i > 0:
457+
prevNoteStart: OffsetQL = (
458+
note_list[i - 1].getOffsetInHierarchy(enclosingMeasure)
459+
)
460+
prevNoteDurQL: OffsetQL = (
461+
note_list[i - 1].duration.quarterLength
462+
)
463+
expectedOffsetInMeas = opFrac(prevNoteStart + prevNoteDurQL)
464+
465+
gapDurQL: OffsetQL = n.getOffsetInHierarchy(enclosingMeasure) - expectedOffsetInMeas
470466
self.annot_notes.append(
471467
AnnNote(
472468
n,
473-
offset,
469+
gapDurQL,
474470
self.en_beam_list[i],
475471
self.tuplet_list[i],
476472
self.tuplet_info[i],
@@ -490,9 +486,6 @@ def __eq__(self, other) -> bool:
490486
return False
491487

492488
return self.precomputed_str == other.precomputed_str
493-
# return all(
494-
# [an[0] == an[1] for an in zip(self.annot_notes, other.annot_notes)]
495-
# )
496489

497490
def notation_size(self) -> int:
498491
"""

musicdiff/comparison.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -938,10 +938,17 @@ def _annotated_note_diff(annNote1: AnnNote, annNote2: AnnNote):
938938
op_list.extend(lyr_op_list)
939939
cost += lyr_cost
940940

941-
# add for offset in quarter notes from start of measure (i.e. horizontal position)
942-
if annNote1.offsetInMeasure != annNote2.offsetInMeasure:
941+
# add for gap from previous note or start of measure if first note in measure
942+
# (i.e. horizontal position shift)
943+
if annNote1.gap_dur != annNote2.gap_dur:
943944
cost += 1
944-
op_list.append(("editnoteoffset", annNote1, annNote2, 1))
945+
if annNote1.gap_dur == 0:
946+
op_list.append(("insspace", annNote1, annNote2, 1))
947+
elif annNote2.gap_dur == 0:
948+
op_list.append(("delspace", annNote1, annNote2, 1))
949+
else:
950+
# neither is zero
951+
op_list.append(("editspace", annNote1, annNote2, 1))
945952

946953
# add for noteshape
947954
if annNote1.noteshape != annNote2.noteshape:

musicdiff/visualization.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -785,22 +785,60 @@ def mark_diffs(
785785
textExp.style.color = Visualization.CHANGED_COLOR
786786
note2.activeSite.insert(note2.offset, textExp)
787787

788-
elif op[0] == "editnoteoffset":
788+
elif op[0] == "editspace":
789789
assert isinstance(op[1], AnnNote)
790790
assert isinstance(op[2], AnnNote)
791791
note1 = score1.recurse().getElementById(op[1].general_note) # type: ignore
792792
if t.TYPE_CHECKING:
793793
assert note1 is not None
794794
note1.style.color = Visualization.CHANGED_COLOR
795-
textExp = m21.expressions.TextExpression("changed note offset")
795+
textExp = m21.expressions.TextExpression("changed space before")
796796
textExp.style.color = Visualization.CHANGED_COLOR
797797
note1.activeSite.insert(note1.offset, textExp)
798798

799799
note2 = score2.recurse().getElementById(op[2].general_note) # type: ignore
800800
if t.TYPE_CHECKING:
801801
assert note2 is not None
802802
note2.style.color = Visualization.CHANGED_COLOR
803-
textExp = m21.expressions.TextExpression("changed note offset")
803+
textExp = m21.expressions.TextExpression("changed space before")
804+
textExp.style.color = Visualization.CHANGED_COLOR
805+
note2.activeSite.insert(note2.offset, textExp)
806+
807+
elif op[0] == "insspace":
808+
assert isinstance(op[1], AnnNote)
809+
assert isinstance(op[2], AnnNote)
810+
note1 = score1.recurse().getElementById(op[1].general_note) # type: ignore
811+
if t.TYPE_CHECKING:
812+
assert note1 is not None
813+
note1.style.color = Visualization.CHANGED_COLOR
814+
textExp = m21.expressions.TextExpression("inserted space before")
815+
textExp.style.color = Visualization.CHANGED_COLOR
816+
note1.activeSite.insert(note1.offset, textExp)
817+
818+
note2 = score2.recurse().getElementById(op[2].general_note) # type: ignore
819+
if t.TYPE_CHECKING:
820+
assert note2 is not None
821+
note2.style.color = Visualization.CHANGED_COLOR
822+
textExp = m21.expressions.TextExpression("inserted space before")
823+
textExp.style.color = Visualization.CHANGED_COLOR
824+
note2.activeSite.insert(note2.offset, textExp)
825+
826+
elif op[0] == "delspace":
827+
assert isinstance(op[1], AnnNote)
828+
assert isinstance(op[2], AnnNote)
829+
note1 = score1.recurse().getElementById(op[1].general_note) # type: ignore
830+
if t.TYPE_CHECKING:
831+
assert note1 is not None
832+
note1.style.color = Visualization.CHANGED_COLOR
833+
textExp = m21.expressions.TextExpression("deleted space before")
834+
textExp.style.color = Visualization.CHANGED_COLOR
835+
note1.activeSite.insert(note1.offset, textExp)
836+
837+
note2 = score2.recurse().getElementById(op[2].general_note) # type: ignore
838+
if t.TYPE_CHECKING:
839+
assert note2 is not None
840+
note2.style.color = Visualization.CHANGED_COLOR
841+
textExp = m21.expressions.TextExpression("deleted space before")
804842
textExp.style.color = Visualization.CHANGED_COLOR
805843
note2.activeSite.insert(note2.offset, textExp)
806844

0 commit comments

Comments
 (0)