Skip to content

Commit

Permalink
Support grace notes. Support SpannerAnchors (if they are present in m…
Browse files Browse the repository at this point in the history
…usic21).

Offset and duration comparison shouldn't be exact.  Offset cost should use offset difference (not duration difference). Ignore more hidden stuff.
  • Loading branch information
gregchapman-dev committed Nov 20, 2022
1 parent f651f4b commit d857d5b
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 27 deletions.
16 changes: 15 additions & 1 deletion musicdiff/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ def __init__(self, general_note: m21.note.GeneralNote, enhanced_beam_list, tuple
self.note_head = type_number
# dots
self.dots = general_note.duration.dots
# graceness
if isinstance(general_note.duration, m21.duration.AppoggiaturaDuration):
self.graceType = 'acc'
self.graceSlash = general_note.duration.slash
elif isinstance(general_note.duration, m21.duration.GraceDuration):
self.graceType = 'nonacc'
self.graceSlash = general_note.duration.slash
else:
self.graceType = ''
self.graceSlash = False
# articulations
self.articulations = [a.name for a in general_note.articulations]
if self.articulations:
Expand Down Expand Up @@ -161,6 +171,10 @@ def __str__(self):
string += str(self.note_head) # add for notehead
for _ in range(self.dots): # add for dots
string += "*"
if self.graceType:
string += self.graceType
if self.graceSlash:
string += '/'
if len(self.beamings) > 0: # add for beaming
string += "B"
for b in self.beamings:
Expand Down Expand Up @@ -327,7 +341,7 @@ def __init__(self, voice: m21.stream.Voice, detail: DetailLevel = DetailLevel.De
currently equivalent to AllObjects).
"""
self.voice = voice.id
note_list = M21Utils.get_notes(voice)
note_list = M21Utils.get_notes_and_gracenotes(voice)
if not note_list:
self.en_beam_list = []
self.tuplet_list = []
Expand Down
28 changes: 24 additions & 4 deletions musicdiff/comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,16 @@ def _strings_leveinshtein_distance(str1: str, str2: str):
distance += max(counter.values())
return distance

@staticmethod
def _areDifferentEnough(flt1: float, flt2: float) -> bool:
diff: float = flt1 - flt2
if diff < 0:
diff = -diff

if diff > 0.0001:
return True
return False

@staticmethod
def _annotated_extra_diff(annExtra1: AnnExtra, annExtra2: AnnExtra):
"""compute the differences between two annotated extras
Expand All @@ -490,17 +500,21 @@ def _annotated_extra_diff(annExtra1: AnnExtra, annExtra2: AnnExtra):
op_list.append(("extracontentedit", annExtra1, annExtra2, content_cost))

# add for the offset
if annExtra1.offset != annExtra2.offset:
# Note: offset here is a float, and some file formats have only four
# decimal places of precision. So we should not compare exactly here.
if Comparison._areDifferentEnough(annExtra1.offset, annExtra2.offset):
# offset is in quarter-notes, so let's make the cost in quarter-notes as well.
# min cost is 1, though, don't round down to zero.
offset_cost: int = min(1, abs(annExtra1.duration - annExtra2.duration))
offset_cost: int = int(min(1, abs(annExtra1.offset - annExtra2.offset)))
cost += offset_cost
op_list.append(("extraoffsetedit", annExtra1, annExtra2, offset_cost))

# add for the duration
if annExtra1.duration != annExtra2.duration:
# Note: duration here is a float, and some file formats have only four
# decimal places of precision. So we should not compare exactly here.
if Comparison._areDifferentEnough(annExtra1.duration, annExtra2.duration):
# duration is in quarter-notes, so let's make the cost in quarter-notes as well.
duration_cost = min(1, abs(annExtra1.duration - annExtra2.duration))
duration_cost = int(min(1, abs(annExtra1.duration - annExtra2.duration)))
cost += duration_cost
op_list.append(("extradurationedit", annExtra1, annExtra2, duration_cost))

Expand Down Expand Up @@ -601,6 +615,12 @@ def _annotated_note_diff(annNote1: AnnNote, annNote2: AnnNote):
op_list.append(("dotdel", annNote1, annNote2, dots_diff))
else:
op_list.append(("dotins", annNote1, annNote2, dots_diff))
if annNote1.graceType != annNote2.graceType:
cost += 1
op_list.append(("graceedit", annNote1, annNote2, 1))
if annNote1.graceSlash != annNote2.graceSlash:
cost += 1
op_list.append(("graceslashedit", annNote1, annNote2, 1))
# add for the beamings
if annNote1.beamings != annNote2.beamings:
beam_op_list, beam_cost = Comparison._beamtuplet_leveinsthein_diff(
Expand Down
64 changes: 45 additions & 19 deletions musicdiff/m21utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,37 +407,63 @@ def get_notes_and_gracenotes(measure):
@staticmethod
def get_extras(measure: m21.stream.Measure, spannerBundle: m21.spanner.SpannerBundle) -> List[m21.base.Music21Object]:
# returns a list of every object contained in the measure (and in the measure's
# substreams/Voices), skipping any Streams, GeneralNotes (which are returned from
# get_notes/get_notes_and_gracenotes), and Barlines. We're looking for things
# substreams/Voices), skipping any Streams, layout stuff, and GeneralNotes (which
# are returned from get_notes/get_notes_and_gracenotes). We're looking for things
# like Clefs, TextExpressions, and Dynamics...
output: List[m21.base.Music21Object] = []

initialList: List[m21.base.Music21Object] = list(
measure.recurse().getElementsNotOfClass(
(m21.note.GeneralNote,
m21.stream.Stream,
m21.layout.LayoutBase) ) )
initialList: List[m21.base.Music21Object]
if hasattr(m21.spanner, 'SpannerAnchor'):
initialList = list(
measure.recurse().getElementsNotOfClass(
(m21.note.GeneralNote,
m21.spanner.SpannerAnchor,
m21.stream.Stream,
m21.layout.LayoutBase) ) )
else:
initialList = list(
measure.recurse().getElementsNotOfClass(
(m21.note.GeneralNote,
m21.stream.Stream,
m21.layout.LayoutBase) ) )

# loop over the initialList, filtering out (and complaining about) things we
# don't recognize.
# don't recognize. Also, we filter out hidden (non-printed) extras. And
# barlines of type 'none' (also not printed).
for el in initialList:
if M21Utils.extra_to_string(el) != '':
output.append(el)
# we ignore hidden extras
if el.hasStyleInformation and el.style.hideObjectOnPrint:
continue
if isinstance(el, m21.bar.Barline) and el.type == 'none':
continue
if M21Utils.extra_to_string(el) == '':
continue
output.append(el)

# Add any ArpeggioMarkSpanners/Crescendos/Diminuendos that start
# on GeneralNotes in this measure
# on GeneralNotes/SpannerAnchors in this measure
if hasattr(m21.expressions, 'ArpeggioMarkSpanner'):
spanner_types = (m21.expressions.ArpeggioMarkSpanner, m21.dynamics.DynamicWedge)
else:
spanner_types = (m21.dynamics.DynamicWedge,)

for gn in measure.recurse().getElementsByClass(m21.note.GeneralNote):
spannerList: List[m21.spanner.Spanner] = gn.getSpannerSites(spanner_types)
for sp in spannerList:
if sp not in spannerBundle:
continue
if sp.isFirst(gn):
output.append(sp)
if hasattr(m21.spanner, 'SpannerAnchor'):
for gn in measure.recurse().getElementsByClass(
(m21.note.GeneralNote, m21.spanner.SpannerAnchor)
):
spannerList: List[m21.spanner.Spanner] = gn.getSpannerSites(spanner_types)
for sp in spannerList:
if sp not in spannerBundle:
continue
if sp.isFirst(gn):
output.append(sp)
else:
for gn in measure.recurse().getElementsByClass(m21.note.GeneralNote):
spannerList: List[m21.spanner.Spanner] = gn.getSpannerSites(spanner_types)
for sp in spannerList:
if sp not in spannerBundle:
continue
if sp.isFirst(gn):
output.append(sp)

# Add any RepeatBracket spanners that start on this measure
rbList: List[m21.spanner.Spanner] = measure.getSpannerSites(m21.spanner.RepeatBracket)
Expand Down
32 changes: 32 additions & 0 deletions musicdiff/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,38 @@ def mark_diffs(
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)

elif op[0] == "graceedit":
assert isinstance(op[1], AnnNote)
assert isinstance(op[2], AnnNote)
# color the changed note/rest/chord (in both scores) using Visualization.CHANGED_COLOR
note1 = score1.recurse().getElementById(op[1].general_note)
note1.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("changed grace note")
textExp.style.color = Visualization.CHANGED_COLOR
note1.activeSite.insert(note1.offset, textExp)

note2 = score2.recurse().getElementById(op[2].general_note)
note2.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("changed grace note")
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)

elif op[0] == "graceslashedit":
assert isinstance(op[1], AnnNote)
assert isinstance(op[2], AnnNote)
# color the changed note/rest/chord (in both scores) using Visualization.CHANGED_COLOR
note1 = score1.recurse().getElementById(op[1].general_note)
note1.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("changed grace note slash")
textExp.style.color = Visualization.CHANGED_COLOR
note1.activeSite.insert(note1.offset, textExp)

note2 = score2.recurse().getElementById(op[2].general_note)
note2.style.color = Visualization.CHANGED_COLOR
textExp = m21.expressions.TextExpression("changed grace note slash")
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)

# beam
elif op[0] == "insbeam":
assert isinstance(op[1], AnnNote)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_nl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def test_annotNote1():
n1.id = 344
# create annotated note
anote = AnnNote(n1, [], [])
assert anote.__repr__() == "[('D5', 'sharp', False)],4,0,[],[],344,[],[]"
assert anote.__repr__() == "[('D5', 'sharp', False)],4,0,[],[],344,[],[],[],{}"
assert str(anote) == "[D5sharp]4"


Expand All @@ -17,7 +17,7 @@ def test_annotNote2():
# create annotated note
anote = AnnNote(n1, ["start"], ["start"])
assert (
anote.__repr__() == "[('E5', 'sharp', False)],4,0,['start'],['start'],344,[],[]"
anote.__repr__() == "[('E5', 'sharp', False)],4,0,['start'],['start'],344,[],[],[],{}"
)
assert str(anote) == "[E5sharp]4BsrTsr"

Expand All @@ -28,7 +28,7 @@ def test_annotNote3():
n1.tie = m21.tie.Tie("stop")
# create annotated note
anote = AnnNote(n1, [], [])
assert anote.__repr__() == "[('D5', 'None', True)],2,0,[],[],344,[],[]"
assert anote.__repr__() == "[('D5', 'None', True)],2,0,[],[],344,[],[],[],{}"
assert str(anote) == "[D5T]2"


Expand Down

0 comments on commit d857d5b

Please sign in to comment.