Skip to content

Commit

Permalink
Instead of a note offset (in measure) for every note, compute what th…
Browse files Browse the repository at this point in the history
…e 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.
  • Loading branch information
gregchapman-dev committed Mar 28, 2024
1 parent 8fdc0f4 commit a26c2e3
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 42 deletions.
65 changes: 29 additions & 36 deletions musicdiff/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import typing as t

import music21 as m21
from music21.common.numberTools import OffsetQL
from music21.common.numberTools import OffsetQL, opFrac

from musicdiff import M21Utils
from musicdiff import DetailLevel
Expand All @@ -28,7 +28,7 @@ class AnnNote:
def __init__(
self,
general_note: m21.note.GeneralNote,
offsetInMeasure: OffsetQL,
gap_dur: OffsetQL,
enhanced_beam_list: list[str],
tuplet_list: list[str],
tuplet_info: list[str],
Expand All @@ -39,6 +39,8 @@ def __init__(
Args:
general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
gap_dur (OffsetQL): gap since end of last note (or since start of measure, if
first note in measure). Usually zero.
enhanced_beam_list (list): A list of beaming information about this GeneralNote.
tuplet_list (list): A list of tuplet info about this GeneralNote.
detail (DetailLevel): What level of detail to use during the diff.
Expand All @@ -48,7 +50,7 @@ def __init__(
"""
self.general_note: int | str = general_note.id
self.offsetInMeasure: OffsetQL = offsetInMeasure
self.gap_dur: OffsetQL = gap_dur
self.beamings: list[str] = enhanced_beam_list
self.tuplets: list[str] = tuplet_list
self.tuplet_info: list[str] = tuplet_info
Expand Down Expand Up @@ -244,28 +246,31 @@ def __str__(self) -> str:

if len(self.articulations) > 0: # add for articulations
for a in self.articulations:
string += a
string += ' ' + a
if len(self.expressions) > 0: # add for articulations
for e in self.expressions:
string += e
string += ' ' + e
if len(self.lyrics) > 0: # add for lyrics
for lyric in self.lyrics:
string += lyric
string += ' ' + lyric

if self.noteshape != 'normal':
string += f"noteshape={self.noteshape}"
string += f" noteshape={self.noteshape}"
if self.noteheadFill is not None:
string += f"noteheadFill={self.noteheadFill}"
string += f" noteheadFill={self.noteheadFill}"
if self.noteheadParenthesis:
string += f"noteheadParenthesis={self.noteheadParenthesis}"
string += f" noteheadParenthesis={self.noteheadParenthesis}"
if self.stemDirection != 'unspecified':
string += f"stemDirection={self.stemDirection}"
string += f" stemDirection={self.stemDirection}"

# offset
string += f" {self.offsetInMeasure}"
# gap_dur
if self.gap_dur != 0:
string += f" spaceBefore={self.gap_dur}"

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

# if not isinstance(other, AnnNote):
# return False
# elif self.pitches != other.pitches:
# return False
# elif self.note_head != other.note_head:
# return False
# elif self.dots != other.dots:
# return False
# elif self.beamings != other.beamings:
# return False
# elif self.tuplets != other.tuplets:
# return False
# elif self.articulations != other.articulations:
# return False
# elif self.expressions != other.expressions:
# return False
# else:
# return True


class AnnExtra:
def __init__(
Expand Down Expand Up @@ -466,11 +452,21 @@ def __init__(
# create a list of notes with beaming and tuplets information attached
self.annot_notes = []
for i, n in enumerate(note_list):
offset: OffsetQL = n.getOffsetInHierarchy(enclosingMeasure)
expectedOffsetInMeas: OffsetQL = 0
if i > 0:
prevNoteStart: OffsetQL = (
note_list[i - 1].getOffsetInHierarchy(enclosingMeasure)
)
prevNoteDurQL: OffsetQL = (
note_list[i - 1].duration.quarterLength
)
expectedOffsetInMeas = opFrac(prevNoteStart + prevNoteDurQL)

gapDurQL: OffsetQL = n.getOffsetInHierarchy(enclosingMeasure) - expectedOffsetInMeas
self.annot_notes.append(
AnnNote(
n,
offset,
gapDurQL,
self.en_beam_list[i],
self.tuplet_list[i],
self.tuplet_info[i],
Expand All @@ -490,9 +486,6 @@ def __eq__(self, other) -> bool:
return False

return self.precomputed_str == other.precomputed_str
# return all(
# [an[0] == an[1] for an in zip(self.annot_notes, other.annot_notes)]
# )

def notation_size(self) -> int:
"""
Expand Down
13 changes: 10 additions & 3 deletions musicdiff/comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -938,10 +938,17 @@ def _annotated_note_diff(annNote1: AnnNote, annNote2: AnnNote):
op_list.extend(lyr_op_list)
cost += lyr_cost

# add for offset in quarter notes from start of measure (i.e. horizontal position)
if annNote1.offsetInMeasure != annNote2.offsetInMeasure:
# add for gap from previous note or start of measure if first note in measure
# (i.e. horizontal position shift)
if annNote1.gap_dur != annNote2.gap_dur:
cost += 1
op_list.append(("editnoteoffset", annNote1, annNote2, 1))
if annNote1.gap_dur == 0:
op_list.append(("insspace", annNote1, annNote2, 1))
elif annNote2.gap_dur == 0:
op_list.append(("delspace", annNote1, annNote2, 1))
else:
# neither is zero
op_list.append(("editspace", annNote1, annNote2, 1))

# add for noteshape
if annNote1.noteshape != annNote2.noteshape:
Expand Down
44 changes: 41 additions & 3 deletions musicdiff/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,22 +785,60 @@ def mark_diffs(
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)

elif op[0] == "editnoteoffset":
elif op[0] == "editspace":
assert isinstance(op[1], AnnNote)
assert isinstance(op[2], AnnNote)
note1 = score1.recurse().getElementById(op[1].general_note) # type: ignore
if t.TYPE_CHECKING:
assert note1 is not None
note1.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("changed note offset")
textExp = m21.expressions.TextExpression("changed space before")
textExp.style.color = Visualization.CHANGED_COLOR
note1.activeSite.insert(note1.offset, textExp)

note2 = score2.recurse().getElementById(op[2].general_note) # type: ignore
if t.TYPE_CHECKING:
assert note2 is not None
note2.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("changed note offset")
textExp = m21.expressions.TextExpression("changed space before")
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)

elif op[0] == "insspace":
assert isinstance(op[1], AnnNote)
assert isinstance(op[2], AnnNote)
note1 = score1.recurse().getElementById(op[1].general_note) # type: ignore
if t.TYPE_CHECKING:
assert note1 is not None
note1.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("inserted space before")
textExp.style.color = Visualization.CHANGED_COLOR
note1.activeSite.insert(note1.offset, textExp)

note2 = score2.recurse().getElementById(op[2].general_note) # type: ignore
if t.TYPE_CHECKING:
assert note2 is not None
note2.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("inserted space before")
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)

elif op[0] == "delspace":
assert isinstance(op[1], AnnNote)
assert isinstance(op[2], AnnNote)
note1 = score1.recurse().getElementById(op[1].general_note) # type: ignore
if t.TYPE_CHECKING:
assert note1 is not None
note1.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("deleted space before")
textExp.style.color = Visualization.CHANGED_COLOR
note1.activeSite.insert(note1.offset, textExp)

note2 = score2.recurse().getElementById(op[2].general_note) # type: ignore
if t.TYPE_CHECKING:
assert note2 is not None
note2.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("deleted space before")
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)

Expand Down

0 comments on commit a26c2e3

Please sign in to comment.