class AnnVoice:
- def __init__(self, voice):
+ def __init__(self, voice: m21.stream.Voice, detail: DetailLevel = DetailLevel.Default):
"""
Extend music21 Voice with some precomputed, easily compared information about it.
Args:
voice (music21.stream.Voice): The music21 voice to extend.
+ detail (DetailLevel): What level of detail to use during the diff. Can be
+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+ currently equivalent to AllObjects).
"""
self.voice = voice.id
- self.note_list = M21Utils.get_notes(voice)
- if not self.note_list:
+ note_list = M21Utils.get_notes(voice)
+ if not note_list:
self.en_beam_list = []
self.tuplet_list = []
self.tuple_info = []
self.annot_notes = []
else:
self.en_beam_list = M21Utils.get_enhance_beamings(
- self.note_list
+ note_list
) # beams and type (type for note shorter than quarter notes)
self.tuplet_list = M21Utils.get_tuplets_type(
- self.note_list
+ note_list
) # corrected tuplets (with "start" and "continue")
- self.tuple_info = M21Utils.get_tuplets_info(self.note_list)
+ self.tuple_info = M21Utils.get_tuplets_info(note_list)
# create a list of notes with beaming and tuplets information attached
self.annot_notes = []
- for i, n in enumerate(self.note_list):
+ for i, n in enumerate(note_list):
self.annot_notes.append(
- AnnNote(n, self.en_beam_list[i], self.tuplet_list[i])
+ AnnNote(n, self.en_beam_list[i], self.tuplet_list[i], detail)
)
self.n_of_notes = len(self.annot_notes)
@@ -1005,38 +1419,44 @@ Returns
View Source
- def __init__(self, voice):
+ def __init__(self, voice: m21.stream.Voice, detail: DetailLevel = DetailLevel.Default):
"""
Extend music21 Voice with some precomputed, easily compared information about it.
Args:
voice (music21.stream.Voice): The music21 voice to extend.
+ detail (DetailLevel): What level of detail to use during the diff. Can be
+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+ currently equivalent to AllObjects).
"""
self.voice = voice.id
- self.note_list = M21Utils.get_notes(voice)
- if not self.note_list:
+ note_list = M21Utils.get_notes(voice)
+ if not note_list:
self.en_beam_list = []
self.tuplet_list = []
self.tuple_info = []
self.annot_notes = []
else:
self.en_beam_list = M21Utils.get_enhance_beamings(
- self.note_list
+ note_list
) # beams and type (type for note shorter than quarter notes)
self.tuplet_list = M21Utils.get_tuplets_type(
- self.note_list
+ note_list
) # corrected tuplets (with "start" and "continue")
- self.tuple_info = M21Utils.get_tuplets_info(self.note_list)
+ self.tuple_info = M21Utils.get_tuplets_info(note_list)
# create a list of notes with beaming and tuplets information attached
self.annot_notes = []
- for i, n in enumerate(self.note_list):
+ for i, n in enumerate(note_list):
self.annot_notes.append(
- AnnNote(n, self.en_beam_list[i], self.tuplet_list[i])
+ AnnNote(n, self.en_beam_list[i], self.tuplet_list[i], detail)
)
self.n_of_notes = len(self.annot_notes)
@@ -1051,6 +1471,9 @@ Args
- voice (music21.stream.Voice): The music21 voice to extend.
+- detail (DetailLevel): What level of detail to use during the diff. Can be
+GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+currently equivalent to AllObjects).
@@ -1135,34 +1558,54 @@ Returns
View Source
class AnnMeasure:
- def __init__(self, measure):
+ def __init__(self, measure: m21.stream.Measure,
+ score: m21.stream.Score,
+ spannerBundle: m21.spanner.SpannerBundle,
+ detail: DetailLevel = DetailLevel.Default):
"""
Extend music21 Measure with some precomputed, easily compared information about it.
Args:
measure (music21.stream.Measure): The music21 measure to extend.
+ score (music21.stream.Score): the enclosing music21 Score.
+ spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
+ detail (DetailLevel): What level of detail to use during the diff. Can be
+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+ currently equivalent to AllObjects).
"""
self.measure = measure.id
self.voices_list = []
if (
len(measure.voices) == 0
): # there is a single AnnVoice ( == for the library there are no voices)
- ann_voice = AnnVoice(measure)
+ ann_voice = AnnVoice(measure, detail)
if ann_voice.n_of_notes > 0:
self.voices_list.append(ann_voice)
else: # there are multiple voices (or an array with just one voice)
for voice in measure.voices:
- ann_voice = AnnVoice(voice)
+ ann_voice = AnnVoice(voice, detail)
if ann_voice.n_of_notes > 0:
- self.voices_list.append(AnnVoice(voice))
+ self.voices_list.append(ann_voice)
self.n_of_voices = len(self.voices_list)
+ self.extras_list = []
+ if detail >= DetailLevel.AllObjects:
+ for extra in M21Utils.get_extras(measure, spannerBundle):
+ self.extras_list.append(AnnExtra(extra, measure, score, detail))
+
+ # For correct comparison, sort the extras_list, so that any list slices
+ # that all have the same offset are sorted alphabetically.
+ self.extras_list.sort(key=lambda e: ( e.offset, str(e) ))
+
# precomputed values to speed up the computation. As they start to be long, they are hashed
self.precomputed_str = hash(self.__str__())
self.precomputed_repr = hash(self.__repr__())
def __str__(self):
- return str([str(v) for v in self.voices_list])
+ return str([str(v) for v in self.voices_list]) + ' Extras:' + str([str(e) for e in self.extras_list])
+
+ def __repr__(self):
+ return self.voices_list.__repr__() + ' Extras:' + self.extras_list.__repr__()
def __eq__(self, other):
# equality does not consider MEI id!
@@ -1172,6 +1615,9 @@ Returns
if len(self.voices_list) != len(other.voices_list):
return False
+ if len(self.extras_list) != len(other.extras_list):
+ return False
+
return self.precomputed_str == other.precomputed_str
# return all([v[0] == v[1] for v in zip(self.voices_list, other.voices_list)])
@@ -1182,10 +1628,7 @@ Returns
Returns:
int: The notation size of the annotated measure
"""
- return sum([v.notation_size() for v in self.voices_list])
-
- def __repr__(self):
- return self.voices_list.__repr__()
+ return sum([v.notation_size() for v in self.voices_list]) + sum([e.notation_size() for e in self.extras_list])
def get_note_ids(self):
"""
@@ -1208,33 +1651,55 @@ Returns
-
AnnMeasure(measure)
+
AnnMeasure(
+ measure: music21.stream.base.Measure,
+ score: music21.stream.base.Score,
+ spannerBundle: music21.spanner.SpannerBundle,
+ detail: musicdiff.m21utils.DetailLevel = <DetailLevel.AllObjects: 2>
+)
View Source
- def __init__(self, measure):
+ def __init__(self, measure: m21.stream.Measure,
+ score: m21.stream.Score,
+ spannerBundle: m21.spanner.SpannerBundle,
+ detail: DetailLevel = DetailLevel.Default):
"""
Extend music21 Measure with some precomputed, easily compared information about it.
Args:
measure (music21.stream.Measure): The music21 measure to extend.
+ score (music21.stream.Score): the enclosing music21 Score.
+ spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
+ detail (DetailLevel): What level of detail to use during the diff. Can be
+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+ currently equivalent to AllObjects).
"""
self.measure = measure.id
self.voices_list = []
if (
len(measure.voices) == 0
): # there is a single AnnVoice ( == for the library there are no voices)
- ann_voice = AnnVoice(measure)
+ ann_voice = AnnVoice(measure, detail)
if ann_voice.n_of_notes > 0:
self.voices_list.append(ann_voice)
else: # there are multiple voices (or an array with just one voice)
for voice in measure.voices:
- ann_voice = AnnVoice(voice)
+ ann_voice = AnnVoice(voice, detail)
if ann_voice.n_of_notes > 0:
- self.voices_list.append(AnnVoice(voice))
+ self.voices_list.append(ann_voice)
self.n_of_voices = len(self.voices_list)
+ self.extras_list = []
+ if detail >= DetailLevel.AllObjects:
+ for extra in M21Utils.get_extras(measure, spannerBundle):
+ self.extras_list.append(AnnExtra(extra, measure, score, detail))
+
+ # For correct comparison, sort the extras_list, so that any list slices
+ # that all have the same offset are sorted alphabetically.
+ self.extras_list.sort(key=lambda e: ( e.offset, str(e) ))
+
# precomputed values to speed up the computation. As they start to be long, they are hashed
self.precomputed_str = hash(self.__str__())
self.precomputed_repr = hash(self.__repr__())
@@ -1248,6 +1713,11 @@ Args
- measure (music21.stream.Measure): The music21 measure to extend.
+- score (music21.stream.Score): the enclosing music21 Score.
+- spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
+- detail (DetailLevel): What level of detail to use during the diff. Can be
+GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+currently equivalent to AllObjects).
@@ -1270,7 +1740,7 @@ Args
Returns:
int: The notation size of the annotated measure
"""
- return sum([v.notation_size() for v in self.voices_list])
+ return sum([v.notation_size() for v in self.voices_list]) + sum([e.notation_size() for e in self.extras_list])
@@ -1335,17 +1805,25 @@ Returns
View Source
class AnnPart:
- def __init__(self, part):
+ def __init__(self, part: m21.stream.Part,
+ score: m21.stream.Score,
+ spannerBundle: m21.spanner.SpannerBundle,
+ detail: DetailLevel = DetailLevel.Default):
"""
Extend music21 Part/PartStaff with some precomputed, easily compared information about it.
Args:
part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff to extend.
+ score (music21.stream.Score): the enclosing music21 Score.
+ spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
+ detail (DetailLevel): What level of detail to use during the diff. Can be
+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+ currently equivalent to AllObjects).
"""
self.part = part.id
self.bar_list = []
for measure in part.getElementsByClass("Measure"):
- ann_bar = AnnMeasure(measure) # create the bar objects
+ ann_bar = AnnMeasure(measure, score, spannerBundle, detail) # create the bar objects
if ann_bar.n_of_voices > 0:
self.bar_list.append(ann_bar)
self.n_of_bars = len(self.bar_list)
@@ -1398,22 +1876,35 @@ Returns
-
AnnPart(part)
+
AnnPart(
+ part: music21.stream.base.Part,
+ score: music21.stream.base.Score,
+ spannerBundle: music21.spanner.SpannerBundle,
+ detail: musicdiff.m21utils.DetailLevel = <DetailLevel.AllObjects: 2>
+)
View Source
- def __init__(self, part):
+ def __init__(self, part: m21.stream.Part,
+ score: m21.stream.Score,
+ spannerBundle: m21.spanner.SpannerBundle,
+ detail: DetailLevel = DetailLevel.Default):
"""
Extend music21 Part/PartStaff with some precomputed, easily compared information about it.
Args:
part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff to extend.
+ score (music21.stream.Score): the enclosing music21 Score.
+ spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
+ detail (DetailLevel): What level of detail to use during the diff. Can be
+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+ currently equivalent to AllObjects).
"""
self.part = part.id
self.bar_list = []
for measure in part.getElementsByClass("Measure"):
- ann_bar = AnnMeasure(measure) # create the bar objects
+ ann_bar = AnnMeasure(measure, score, spannerBundle, detail) # create the bar objects
if ann_bar.n_of_voices > 0:
self.bar_list.append(ann_bar)
self.n_of_bars = len(self.bar_list)
@@ -1429,6 +1920,11 @@ Args
- part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff to extend.
+- score (music21.stream.Score): the enclosing music21 Score.
+- spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
+- detail (DetailLevel): What level of detail to use during the diff. Can be
+GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+currently equivalent to AllObjects).
@@ -1516,18 +2012,22 @@ Returns
View Source
class AnnScore:
- def __init__(self, score):
+ def __init__(self, score: m21.stream.Score, detail: DetailLevel = DetailLevel.Default):
"""
Take a music21 score and store it as a sequence of Full Trees.
The hierarchy is "score -> parts -> measures -> voices -> notes"
Args:
score (music21.stream.Score): The music21 score
+ detail (DetailLevel): What level of detail to use during the diff. Can be
+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+ currently equivalent to AllObjects).
"""
self.score = score.id
self.part_list = []
+ spannerBundle: m21.spanner.SpannerBundle = score.spannerBundle
for part in score.parts.stream():
# create and add the AnnPart object to part_list
- ann_part = AnnPart(part)
+ ann_part = AnnPart(part, score, spannerBundle, detail)
if ann_part.n_of_bars > 0:
self.part_list.append(ann_part)
self.n_of_parts = len(self.part_list)
@@ -1584,23 +2084,30 @@ Returns
View Source
- def __init__(self, score):
+ def __init__(self, score: m21.stream.Score, detail: DetailLevel = DetailLevel.Default):
"""
Take a music21 score and store it as a sequence of Full Trees.
The hierarchy is "score -> parts -> measures -> voices -> notes"
Args:
score (music21.stream.Score): The music21 score
+ detail (DetailLevel): What level of detail to use during the diff. Can be
+ GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+ currently equivalent to AllObjects).
"""
self.score = score.id
self.part_list = []
+ spannerBundle: m21.spanner.SpannerBundle = score.spannerBundle
for part in score.parts.stream():
# create and add the AnnPart object to part_list
- ann_part = AnnPart(part)
+ ann_part = AnnPart(part, score, spannerBundle, detail)
if ann_part.n_of_bars > 0:
self.part_list.append(ann_part)
self.n_of_parts = len(self.part_list)
@@ -1615,6 +2122,9 @@ Args
- score (music21.stream.Score): The music21 score
+- detail (DetailLevel): What level of detail to use during the diff. Can be
+GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
+currently equivalent to AllObjects).
diff --git a/docs/musicdiff/comparison.html b/docs/musicdiff/comparison.html
index 0359985..cd64936 100644
--- a/docs/musicdiff/comparison.html
+++ b/docs/musicdiff/comparison.html
@@ -79,10 +79,11 @@
import copy
from typing import List, Tuple
from collections import namedtuple
+from difflib import ndiff
import numpy as np
-from musicdiff.annotation import AnnScore, AnnNote
+from musicdiff.annotation import AnnScore, AnnNote, AnnVoice, AnnExtra
from musicdiff import M21Utils
# memoizers to speed up the recursive computation
@@ -97,6 +98,17 @@
return memoizer
+def _memoize_extras_diff_lin(func):
+ mem = {}
+
+ def memoizer(original, compare_to):
+ key = repr(original) + repr(compare_to)
+ if key not in mem:
+ mem[key] = func(original, compare_to)
+ return copy.deepcopy(mem[key])
+
+ return memoizer
+
def _memoize_block_diff_lin(func):
mem = {}
@@ -432,14 +444,22 @@
)
if (
original[0] == compare_to[0]
- ): # to avoid perform the _inside_bars_diff_lin if it's not needed
+ ): # to avoid performing the _voices_coupling_recursive if it's not needed
inside_bar_op_list = []
inside_bar_cost = 0
else:
- # run the voice coupling algorithm
+ # diff the bar extras (like _inside_bars_diff_lin, but with lists of AnnExtras
+ # instead of lists of AnnNotes)
+ extras_op_list, extras_cost = Comparison._extras_diff_lin(
+ original[0].extras_list, compare_to[0].extras_list
+ )
+
+ # run the voice coupling algorithm, and add to inside_bar_op_list and inside_bar_cost
inside_bar_op_list, inside_bar_cost = Comparison._voices_coupling_recursive(
original[0].voices_list, compare_to[0].voices_list
)
+ inside_bar_op_list.extend(extras_op_list)
+ inside_bar_cost += extras_cost
cost_dict["editbar"] += inside_bar_cost
op_list_dict["editbar"].extend(inside_bar_op_list)
# compute the minimum of the possibilities
@@ -447,6 +467,113 @@
out = op_list_dict[min_key], cost_dict[min_key]
return out
+ @staticmethod
+ @_memoize_extras_diff_lin
+ def _extras_diff_lin(original, compare_to):
+ # original and compare to are two lists of AnnExtra
+ if len(original) == 0 and len(compare_to) == 0:
+ return [], 0
+
+ if len(original) == 0:
+ cost = 0
+ op_list, cost = Comparison._extras_diff_lin(original, compare_to[1:])
+ op_list.append(("extrains", None, compare_to[0], compare_to[0].notation_size()))
+ cost += compare_to[0].notation_size()
+ return op_list, cost
+
+ if len(compare_to) == 0:
+ cost = 0
+ op_list, cost = Comparison._extras_diff_lin(original[1:], compare_to)
+ op_list.append(("extradel", original[0], None, original[0].notation_size()))
+ cost += original[0].notation_size()
+ return op_list, cost
+
+ # compute the cost and the op_list for the many possibilities of recursion
+ cost = {}
+ op_list = {}
+ # extradel
+ op_list["extradel"], cost["extradel"] = Comparison._extras_diff_lin(
+ original[1:], compare_to
+ )
+ cost["extradel"] += original[0].notation_size()
+ op_list["extradel"].append(
+ ("extradel", original[0], None, original[0].notation_size())
+ )
+ # extrains
+ op_list["extrains"], cost["extrains"] = Comparison._extras_diff_lin(
+ original, compare_to[1:]
+ )
+ cost["extrains"] += compare_to[0].notation_size()
+ op_list["extrains"].append(
+ ("extrains", None, compare_to[0], compare_to[0].notation_size())
+ )
+ # extrasub
+ op_list["extrasub"], cost["extrasub"] = Comparison._extras_diff_lin(
+ original[1:], compare_to[1:]
+ )
+ if (
+ original[0] == compare_to[0]
+ ): # avoid call another function if they are equal
+ extrasub_op, extrasub_cost = [], 0
+ else:
+ extrasub_op, extrasub_cost = Comparison._annotated_extra_diff(original[0], compare_to[0])
+ cost["extrasub"] += extrasub_cost
+ op_list["extrasub"].extend(extrasub_op)
+ # compute the minimum of the possibilities
+ min_key = min(cost, key=cost.get)
+ out = op_list[min_key], cost[min_key]
+ return out
+
+ @staticmethod
+ def _strings_leveinshtein_distance(str1: str, str2: str):
+ counter: dict = {"+": 0, "-": 0}
+ distance: int = 0
+ for edit_code, *_ in ndiff(str1, str2):
+ if edit_code == " ":
+ distance += max(counter.values())
+ counter = {"+": 0, "-": 0}
+ else:
+ counter[edit_code] += 1
+ distance += max(counter.values())
+ return distance
+
+ @staticmethod
+ def _annotated_extra_diff(annExtra1: AnnExtra, annExtra2: AnnExtra):
+ """compute the differences between two annotated extras
+ Each annotated extra consists of three values: content, offset, and duration
+ """
+ cost = 0
+ op_list = []
+
+ # add for the content
+ if annExtra1.content != annExtra2.content:
+ content_cost: int = Comparison._strings_leveinshtein_distance(
+ annExtra1.content, annExtra2.content)
+ cost += content_cost
+ op_list.append(("extracontentedit", annExtra1, annExtra2, content_cost))
+
+ # add for the offset
+ if 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))
+ cost += offset_cost
+ op_list.append(("extraoffsetedit", annExtra1, annExtra2, offset_cost))
+
+ # add for the duration
+ if 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))
+ cost += duration_cost
+ op_list.append(("extradurationedit", annExtra1, annExtra2, duration_cost))
+
+ # add for the style
+ if annExtra1.styledict != annExtra2.styledict:
+ cost += 1
+ op_list.append(("extrastyleedit", annExtra1, annExtra2, 1))
+
+ return op_list, cost
+
@staticmethod
@_memoize_inside_bars_diff_lin
def _inside_bars_diff_lin(original, compare_to):
@@ -573,6 +700,26 @@
)
op_list.extend(expr_op_list)
cost += expr_cost
+ # add for noteshape
+ if annNote1.noteshape != annNote2.noteshape:
+ cost += 1
+ op_list.append(("editnoteshape", annNote1, annNote2, 1))
+ # add for noteheadFill
+ if annNote1.noteheadFill != annNote2.noteheadFill:
+ cost += 1
+ op_list.append(("editnoteheadfill", annNote1, annNote2, 1))
+ # add for noteheadParenthesis
+ if annNote1.noteheadParenthesis != annNote2.noteheadParenthesis:
+ cost += 1
+ op_list.append(("editnoteheadparenthesis", annNote1, annNote2, 1))
+ # add for stemDirection
+ if annNote1.stemDirection != annNote2.stemDirection:
+ cost += 1
+ op_list.append(("editstemdirection", annNote1, annNote2, 1))
+ # add for the styledict
+ if annNote1.styledict != annNote2.styledict:
+ cost += 1
+ op_list.append(("editstyle", annNote1, annNote2, 1))
return op_list, cost
@@ -705,7 +852,7 @@
return out
@staticmethod
- def _voices_coupling_recursive(original: List, compare_to):
+ def _voices_coupling_recursive(original: List[AnnVoice], compare_to: List[AnnVoice]):
"""compare all the possible voices permutations, considering also deletion and insertion (equation on office lens)
original [list] -- a list of Voice
compare_to [list] -- a list of Voice
@@ -1091,14 +1238,22 @@
)
if (
original[0] == compare_to[0]
- ): # to avoid perform the _inside_bars_diff_lin if it's not needed
+ ): # to avoid performing the _voices_coupling_recursive if it's not needed
inside_bar_op_list = []
inside_bar_cost = 0
else:
- # run the voice coupling algorithm
+ # diff the bar extras (like _inside_bars_diff_lin, but with lists of AnnExtras
+ # instead of lists of AnnNotes)
+ extras_op_list, extras_cost = Comparison._extras_diff_lin(
+ original[0].extras_list, compare_to[0].extras_list
+ )
+
+ # run the voice coupling algorithm, and add to inside_bar_op_list and inside_bar_cost
inside_bar_op_list, inside_bar_cost = Comparison._voices_coupling_recursive(
original[0].voices_list, compare_to[0].voices_list
)
+ inside_bar_op_list.extend(extras_op_list)
+ inside_bar_cost += extras_cost
cost_dict["editbar"] += inside_bar_cost
op_list_dict["editbar"].extend(inside_bar_op_list)
# compute the minimum of the possibilities
@@ -1106,6 +1261,113 @@
out = op_list_dict[min_key], cost_dict[min_key]
return out
+ @staticmethod
+ @_memoize_extras_diff_lin
+ def _extras_diff_lin(original, compare_to):
+ # original and compare to are two lists of AnnExtra
+ if len(original) == 0 and len(compare_to) == 0:
+ return [], 0
+
+ if len(original) == 0:
+ cost = 0
+ op_list, cost = Comparison._extras_diff_lin(original, compare_to[1:])
+ op_list.append(("extrains", None, compare_to[0], compare_to[0].notation_size()))
+ cost += compare_to[0].notation_size()
+ return op_list, cost
+
+ if len(compare_to) == 0:
+ cost = 0
+ op_list, cost = Comparison._extras_diff_lin(original[1:], compare_to)
+ op_list.append(("extradel", original[0], None, original[0].notation_size()))
+ cost += original[0].notation_size()
+ return op_list, cost
+
+ # compute the cost and the op_list for the many possibilities of recursion
+ cost = {}
+ op_list = {}
+ # extradel
+ op_list["extradel"], cost["extradel"] = Comparison._extras_diff_lin(
+ original[1:], compare_to
+ )
+ cost["extradel"] += original[0].notation_size()
+ op_list["extradel"].append(
+ ("extradel", original[0], None, original[0].notation_size())
+ )
+ # extrains
+ op_list["extrains"], cost["extrains"] = Comparison._extras_diff_lin(
+ original, compare_to[1:]
+ )
+ cost["extrains"] += compare_to[0].notation_size()
+ op_list["extrains"].append(
+ ("extrains", None, compare_to[0], compare_to[0].notation_size())
+ )
+ # extrasub
+ op_list["extrasub"], cost["extrasub"] = Comparison._extras_diff_lin(
+ original[1:], compare_to[1:]
+ )
+ if (
+ original[0] == compare_to[0]
+ ): # avoid call another function if they are equal
+ extrasub_op, extrasub_cost = [], 0
+ else:
+ extrasub_op, extrasub_cost = Comparison._annotated_extra_diff(original[0], compare_to[0])
+ cost["extrasub"] += extrasub_cost
+ op_list["extrasub"].extend(extrasub_op)
+ # compute the minimum of the possibilities
+ min_key = min(cost, key=cost.get)
+ out = op_list[min_key], cost[min_key]
+ return out
+
+ @staticmethod
+ def _strings_leveinshtein_distance(str1: str, str2: str):
+ counter: dict = {"+": 0, "-": 0}
+ distance: int = 0
+ for edit_code, *_ in ndiff(str1, str2):
+ if edit_code == " ":
+ distance += max(counter.values())
+ counter = {"+": 0, "-": 0}
+ else:
+ counter[edit_code] += 1
+ distance += max(counter.values())
+ return distance
+
+ @staticmethod
+ def _annotated_extra_diff(annExtra1: AnnExtra, annExtra2: AnnExtra):
+ """compute the differences between two annotated extras
+ Each annotated extra consists of three values: content, offset, and duration
+ """
+ cost = 0
+ op_list = []
+
+ # add for the content
+ if annExtra1.content != annExtra2.content:
+ content_cost: int = Comparison._strings_leveinshtein_distance(
+ annExtra1.content, annExtra2.content)
+ cost += content_cost
+ op_list.append(("extracontentedit", annExtra1, annExtra2, content_cost))
+
+ # add for the offset
+ if 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))
+ cost += offset_cost
+ op_list.append(("extraoffsetedit", annExtra1, annExtra2, offset_cost))
+
+ # add for the duration
+ if 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))
+ cost += duration_cost
+ op_list.append(("extradurationedit", annExtra1, annExtra2, duration_cost))
+
+ # add for the style
+ if annExtra1.styledict != annExtra2.styledict:
+ cost += 1
+ op_list.append(("extrastyleedit", annExtra1, annExtra2, 1))
+
+ return op_list, cost
+
@staticmethod
@_memoize_inside_bars_diff_lin
def _inside_bars_diff_lin(original, compare_to):
@@ -1232,6 +1494,26 @@
)
op_list.extend(expr_op_list)
cost += expr_cost
+ # add for noteshape
+ if annNote1.noteshape != annNote2.noteshape:
+ cost += 1
+ op_list.append(("editnoteshape", annNote1, annNote2, 1))
+ # add for noteheadFill
+ if annNote1.noteheadFill != annNote2.noteheadFill:
+ cost += 1
+ op_list.append(("editnoteheadfill", annNote1, annNote2, 1))
+ # add for noteheadParenthesis
+ if annNote1.noteheadParenthesis != annNote2.noteheadParenthesis:
+ cost += 1
+ op_list.append(("editnoteheadparenthesis", annNote1, annNote2, 1))
+ # add for stemDirection
+ if annNote1.stemDirection != annNote2.stemDirection:
+ cost += 1
+ op_list.append(("editstemdirection", annNote1, annNote2, 1))
+ # add for the styledict
+ if annNote1.styledict != annNote2.styledict:
+ cost += 1
+ op_list.append(("editstyle", annNote1, annNote2, 1))
return op_list, cost
@@ -1364,7 +1646,7 @@
return out
@staticmethod
- def _voices_coupling_recursive(original: List, compare_to):
+ def _voices_coupling_recursive(original: List[AnnVoice], compare_to: List[AnnVoice]):
"""compare all the possible voices permutations, considering also deletion and insertion (equation on office lens)
original [list] -- a list of Voice
compare_to [list] -- a list of Voice
diff --git a/docs/musicdiff/visualization.html b/docs/musicdiff/visualization.html
index 5bbcce3..aaddfcb 100644
--- a/docs/musicdiff/visualization.html
+++ b/docs/musicdiff/visualization.html
@@ -94,7 +94,7 @@
import music21 as m21
-from musicdiff.annotation import AnnMeasure, AnnVoice, AnnNote
+from musicdiff.annotation import AnnMeasure, AnnVoice, AnnNote, AnnExtra
class Visualization:
@@ -183,19 +183,164 @@
for el in voice1.recurse().notesAndRests:
el.style.color = Visualization.DELETED_COLOR
+ # extra
+ elif op[0] == "extrains":
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.INSERTED_COLOR, and add a textExpression
+ # describing the insertion.
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp = m21.expressions.TextExpression(f"inserted {extra2.classes[0]}")
+ textExp.style.color = Visualization.INSERTED_COLOR
+ if isinstance(extra2, m21.spanner.Spanner):
+ insertionPoint = extra2.getFirst()
+ insertionPoint.activeSite.insert(insertionPoint.offset, textExp)
+ else:
+ extra2.activeSite.insert(extra2.offset, textExp)
+
+ elif op[0] == "extradel":
+ assert isinstance(op[1], AnnExtra)
+ # color the extra using Visualization.DELETED_COLOR, and add a textExpression
+ # describing the deletion.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ textExp = m21.expressions.TextExpression(f"deleted {extra1.classes[0]}")
+ textExp.style.color = Visualization.DELETED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint = extra1.getFirst()
+ insertionPoint.activeSite.insert(insertionPoint.offset, textExp)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp)
+
+ elif op[0] == "extrasub":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ if extra1.classes[0] != extra2.classes[0]:
+ textExp1 = m21.expressions.TextExpression(
+ f"changed to {extra2.classes[0]}")
+ textExp2 = m21.expressions.TextExpression(
+ f"changed from {extra1.classes[0]}")
+ else:
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]}")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]}")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extracontentedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} text")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} text")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extraoffsetedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} offset")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} offset")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extradurationedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} duration")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} duration")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extrastyleedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ sd1 = op[1].styledict
+ sd2 = op[2].styledict
+ changedStr: str = ""
+ for k1, v1 in sd1.items():
+ if k1 not in sd2 or sd2[k1] != v1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k1
+
+ # one last thing: check for keys in sd2 that aren't in sd1
+ for k2 in sd2:
+ if k2 not in sd1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k2
+
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} {changedStr}")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} {changedStr}")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
# note
elif op[0] == "noteins":
assert isinstance(op[2], AnnNote)
# color the inserted score2 general note (note, chord, or rest) using Visualization.INSERTED_COLOR
note2 = score2.recurse().getElementById(op[2].general_note)
note2.style.color = Visualization.INSERTED_COLOR
- if "Rest" in note2.classes:
- textExp = m21.expressions.TextExpression("inserted rest")
- elif "Chord" in note2.classes:
- textExp = m21.expressions.TextExpression("inserted chord")
- else:
- textExp = m21.expressions.TextExpression("inserted note")
- textExp.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression(f"inserted {note2.classes[0]}")
+ textExp.style.color = Visualization.INSERTED_COLOR
note2.activeSite.insert(note2.offset, textExp)
elif op[0] == "notedel":
@@ -203,13 +348,8 @@
# color the deleted score1 general note (note, chord, or rest) using Visualization.DELETED_COLOR
note1 = score1.recurse().getElementById(op[1].general_note)
note1.style.color = Visualization.DELETED_COLOR
- if "Rest" in note1.classes:
- textExp = m21.expressions.TextExpression("deleted rest")
- elif "Chord" in note1.classes:
- textExp = m21.expressions.TextExpression("deleted chord")
- else:
- textExp = m21.expressions.TextExpression("deleted note")
- textExp.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression(f"deleted {note2.classes[0]}")
+ textExp.style.color = Visualization.DELETED_COLOR
note1.activeSite.insert(note1.offset, textExp)
# pitch
@@ -383,6 +523,97 @@
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)
+ elif op[0] == "editnoteshape":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed note shape")
+ 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 note shape")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editnoteheadfill":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed note head fill")
+ 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 note head fill")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editnoteheadparenthesis":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed note head paren")
+ 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 note head paren")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editstemdirection":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed stem direction")
+ 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 stem direction")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editstyle":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ sd1 = op[1].styledict
+ sd2 = op[2].styledict
+ changedStr: str = ""
+ for k1, v1 in sd1.items():
+ if k1 not in sd2 or sd2[k1] != v1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k1
+
+ # one last thing: check for keys in sd2 that aren't in sd1
+ for k2 in sd2:
+ if k2 not in sd1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k2
+
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression(f"changed note {changedStr}")
+ 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(f"changed note {changedStr}")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
# accident
elif op[0] == "accidentins":
assert isinstance(op[1], AnnNote)
@@ -894,19 +1125,164 @@
for el in voice1.recurse().notesAndRests:
el.style.color = Visualization.DELETED_COLOR
+ # extra
+ elif op[0] == "extrains":
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.INSERTED_COLOR, and add a textExpression
+ # describing the insertion.
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp = m21.expressions.TextExpression(f"inserted {extra2.classes[0]}")
+ textExp.style.color = Visualization.INSERTED_COLOR
+ if isinstance(extra2, m21.spanner.Spanner):
+ insertionPoint = extra2.getFirst()
+ insertionPoint.activeSite.insert(insertionPoint.offset, textExp)
+ else:
+ extra2.activeSite.insert(extra2.offset, textExp)
+
+ elif op[0] == "extradel":
+ assert isinstance(op[1], AnnExtra)
+ # color the extra using Visualization.DELETED_COLOR, and add a textExpression
+ # describing the deletion.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ textExp = m21.expressions.TextExpression(f"deleted {extra1.classes[0]}")
+ textExp.style.color = Visualization.DELETED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint = extra1.getFirst()
+ insertionPoint.activeSite.insert(insertionPoint.offset, textExp)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp)
+
+ elif op[0] == "extrasub":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ if extra1.classes[0] != extra2.classes[0]:
+ textExp1 = m21.expressions.TextExpression(
+ f"changed to {extra2.classes[0]}")
+ textExp2 = m21.expressions.TextExpression(
+ f"changed from {extra1.classes[0]}")
+ else:
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]}")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]}")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extracontentedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} text")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} text")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extraoffsetedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} offset")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} offset")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extradurationedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} duration")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} duration")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extrastyleedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ sd1 = op[1].styledict
+ sd2 = op[2].styledict
+ changedStr: str = ""
+ for k1, v1 in sd1.items():
+ if k1 not in sd2 or sd2[k1] != v1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k1
+
+ # one last thing: check for keys in sd2 that aren't in sd1
+ for k2 in sd2:
+ if k2 not in sd1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k2
+
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} {changedStr}")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} {changedStr}")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
# note
elif op[0] == "noteins":
assert isinstance(op[2], AnnNote)
# color the inserted score2 general note (note, chord, or rest) using Visualization.INSERTED_COLOR
note2 = score2.recurse().getElementById(op[2].general_note)
note2.style.color = Visualization.INSERTED_COLOR
- if "Rest" in note2.classes:
- textExp = m21.expressions.TextExpression("inserted rest")
- elif "Chord" in note2.classes:
- textExp = m21.expressions.TextExpression("inserted chord")
- else:
- textExp = m21.expressions.TextExpression("inserted note")
- textExp.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression(f"inserted {note2.classes[0]}")
+ textExp.style.color = Visualization.INSERTED_COLOR
note2.activeSite.insert(note2.offset, textExp)
elif op[0] == "notedel":
@@ -914,13 +1290,8 @@
# color the deleted score1 general note (note, chord, or rest) using Visualization.DELETED_COLOR
note1 = score1.recurse().getElementById(op[1].general_note)
note1.style.color = Visualization.DELETED_COLOR
- if "Rest" in note1.classes:
- textExp = m21.expressions.TextExpression("deleted rest")
- elif "Chord" in note1.classes:
- textExp = m21.expressions.TextExpression("deleted chord")
- else:
- textExp = m21.expressions.TextExpression("deleted note")
- textExp.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression(f"deleted {note2.classes[0]}")
+ textExp.style.color = Visualization.DELETED_COLOR
note1.activeSite.insert(note1.offset, textExp)
# pitch
@@ -1094,6 +1465,97 @@
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)
+ elif op[0] == "editnoteshape":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed note shape")
+ 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 note shape")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editnoteheadfill":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed note head fill")
+ 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 note head fill")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editnoteheadparenthesis":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed note head paren")
+ 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 note head paren")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editstemdirection":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed stem direction")
+ 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 stem direction")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editstyle":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ sd1 = op[1].styledict
+ sd2 = op[2].styledict
+ changedStr: str = ""
+ for k1, v1 in sd1.items():
+ if k1 not in sd2 or sd2[k1] != v1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k1
+
+ # one last thing: check for keys in sd2 that aren't in sd1
+ for k2 in sd2:
+ if k2 not in sd1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k2
+
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression(f"changed note {changedStr}")
+ 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(f"changed note {changedStr}")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
# accident
elif op[0] == "accidentins":
assert isinstance(op[1], AnnNote)
@@ -1639,19 +2101,164 @@
for el in voice1.recurse().notesAndRests:
el.style.color = Visualization.DELETED_COLOR
+ # extra
+ elif op[0] == "extrains":
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.INSERTED_COLOR, and add a textExpression
+ # describing the insertion.
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp = m21.expressions.TextExpression(f"inserted {extra2.classes[0]}")
+ textExp.style.color = Visualization.INSERTED_COLOR
+ if isinstance(extra2, m21.spanner.Spanner):
+ insertionPoint = extra2.getFirst()
+ insertionPoint.activeSite.insert(insertionPoint.offset, textExp)
+ else:
+ extra2.activeSite.insert(extra2.offset, textExp)
+
+ elif op[0] == "extradel":
+ assert isinstance(op[1], AnnExtra)
+ # color the extra using Visualization.DELETED_COLOR, and add a textExpression
+ # describing the deletion.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ textExp = m21.expressions.TextExpression(f"deleted {extra1.classes[0]}")
+ textExp.style.color = Visualization.DELETED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint = extra1.getFirst()
+ insertionPoint.activeSite.insert(insertionPoint.offset, textExp)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp)
+
+ elif op[0] == "extrasub":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ if extra1.classes[0] != extra2.classes[0]:
+ textExp1 = m21.expressions.TextExpression(
+ f"changed to {extra2.classes[0]}")
+ textExp2 = m21.expressions.TextExpression(
+ f"changed from {extra1.classes[0]}")
+ else:
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]}")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]}")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extracontentedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} text")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} text")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extraoffsetedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} offset")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} offset")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extradurationedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} duration")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} duration")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
+ elif op[0] == "extrastyleedit":
+ assert isinstance(op[1], AnnExtra)
+ assert isinstance(op[2], AnnExtra)
+ sd1 = op[1].styledict
+ sd2 = op[2].styledict
+ changedStr: str = ""
+ for k1, v1 in sd1.items():
+ if k1 not in sd2 or sd2[k1] != v1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k1
+
+ # one last thing: check for keys in sd2 that aren't in sd1
+ for k2 in sd2:
+ if k2 not in sd1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k2
+
+ # color the extra using Visualization.CHANGED_COLOR, and add a textExpression
+ # describing the change.
+ extra1 = score1.recurse().getElementById(op[1].extra)
+ extra2 = score2.recurse().getElementById(op[2].extra)
+
+ textExp1 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} {changedStr}")
+ textExp2 = m21.expressions.TextExpression(f"changed {extra1.classes[0]} {changedStr}")
+ textExp1.style.color = Visualization.CHANGED_COLOR
+ textExp2.style.color = Visualization.CHANGED_COLOR
+ if isinstance(extra1, m21.spanner.Spanner):
+ insertionPoint1 = extra1.getFirst()
+ insertionPoint2 = extra2.getFirst()
+ insertionPoint1.activeSite.insert(insertionPoint1.offset, textExp1)
+ insertionPoint2.activeSite.insert(insertionPoint2.offset, textExp2)
+ else:
+ extra1.activeSite.insert(extra1.offset, textExp1)
+ extra2.activeSite.insert(extra2.offset, textExp2)
+
# note
elif op[0] == "noteins":
assert isinstance(op[2], AnnNote)
# color the inserted score2 general note (note, chord, or rest) using Visualization.INSERTED_COLOR
note2 = score2.recurse().getElementById(op[2].general_note)
note2.style.color = Visualization.INSERTED_COLOR
- if "Rest" in note2.classes:
- textExp = m21.expressions.TextExpression("inserted rest")
- elif "Chord" in note2.classes:
- textExp = m21.expressions.TextExpression("inserted chord")
- else:
- textExp = m21.expressions.TextExpression("inserted note")
- textExp.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression(f"inserted {note2.classes[0]}")
+ textExp.style.color = Visualization.INSERTED_COLOR
note2.activeSite.insert(note2.offset, textExp)
elif op[0] == "notedel":
@@ -1659,13 +2266,8 @@
# color the deleted score1 general note (note, chord, or rest) using Visualization.DELETED_COLOR
note1 = score1.recurse().getElementById(op[1].general_note)
note1.style.color = Visualization.DELETED_COLOR
- if "Rest" in note1.classes:
- textExp = m21.expressions.TextExpression("deleted rest")
- elif "Chord" in note1.classes:
- textExp = m21.expressions.TextExpression("deleted chord")
- else:
- textExp = m21.expressions.TextExpression("deleted note")
- textExp.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression(f"deleted {note2.classes[0]}")
+ textExp.style.color = Visualization.DELETED_COLOR
note1.activeSite.insert(note1.offset, textExp)
# pitch
@@ -1839,6 +2441,97 @@
textExp.style.color = Visualization.CHANGED_COLOR
note2.activeSite.insert(note2.offset, textExp)
+ elif op[0] == "editnoteshape":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed note shape")
+ 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 note shape")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editnoteheadfill":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed note head fill")
+ 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 note head fill")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editnoteheadparenthesis":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed note head paren")
+ 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 note head paren")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editstemdirection":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression("changed stem direction")
+ 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 stem direction")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
+ elif op[0] == "editstyle":
+ assert isinstance(op[1], AnnNote)
+ assert isinstance(op[2], AnnNote)
+ sd1 = op[1].styledict
+ sd2 = op[2].styledict
+ changedStr: str = ""
+ for k1, v1 in sd1.items():
+ if k1 not in sd2 or sd2[k1] != v1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k1
+
+ # one last thing: check for keys in sd2 that aren't in sd1
+ for k2 in sd2:
+ if k2 not in sd1:
+ if changedStr:
+ changedStr += ","
+ changedStr += k2
+
+ note1 = score1.recurse().getElementById(op[1].general_note)
+ note1.style.color = Visualization.CHANGED_COLOR
+ textExp = m21.expressions.TextExpression(f"changed note {changedStr}")
+ 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(f"changed note {changedStr}")
+ textExp.style.color = Visualization.CHANGED_COLOR
+ note2.activeSite.insert(note2.offset, textExp)
+
# accident
elif op[0] == "accidentins":
assert isinstance(op[1], AnnNote)
diff --git a/docs/search.js b/docs/search.js
index 5f4b958..da6bffd 100644
--- a/docs/search.js
+++ b/docs/search.js
@@ -1,6 +1,6 @@
window.pdocSearch = (function(){
/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o\n"}, {"fullname": "musicdiff.diff", "modulename": "musicdiff", "qualname": "diff", "type": "function", "doc": "Compare two musical scores and optionally save/display the differences as two marked-up\nrendered PDFs.
\n\nArgs
\n\n\n- score1 (str, Path, music21.stream.Score): The first music score to compare. The score\ncan be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI,\netc), or a music21 Score object.
\n- score2 (str, Path, music21.stream.Score): The second musical score to compare. The score\ncan be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI,\netc), or a music21 Score object.
\n- out_path1 (str, Path): Where to save the first marked-up rendered score PDF.\nIf out_path1 is None, both PDFs will be displayed in the default PDF viewer.\n(default is None)
\n- out_path2 (str, Path): Where to save the second marked-up rendered score PDF.\nIf out_path2 is None, both PDFs will be displayed in the default PDF viewer.\n(default is None)
\n- force_parse (bool): Whether or not to force music21 to re-parse a file it has parsed\npreviously.\n(default is True)
\n- visualize_diffs (bool): Whether or not to render diffs as marked up PDFs. If False,\nthe only result of the call will be the return value (the number of differences).\n(default is True)
\n
\n\nReturns
\n\n\n int: The number of differences found (0 means the scores were identical, None means the diff failed)
\n
\n", "signature": "(\n score1: Union[str, pathlib.Path, music21.stream.base.Score],\n score2: Union[str, pathlib.Path, music21.stream.base.Score],\n out_path1: Union[str, pathlib.Path] = None,\n out_path2: Union[str, pathlib.Path] = None,\n force_parse: bool = True,\n visualize_diffs: bool = True\n) -> int", "funcdef": "def"}, {"fullname": "musicdiff.annotation", "modulename": "musicdiff.annotation", "type": "module", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnNote", "modulename": "musicdiff.annotation", "qualname": "AnnNote", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnNote.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnNote.__init__", "type": "function", "doc": "Extend music21 GeneralNote with some precomputed, easily compared information about it.
\n\nArgs
\n\n\n- general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
\n- enhanced_beam_list (list): A list of beaming information about this GeneralNote.
\n- tuplet_list (list): A list of tuplet info about this GeneralNote.
\n
\n", "signature": "(self, general_note, enhanced_beam_list, tuplet_list)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnNote.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnNote.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnNote
.
\n\nReturns
\n\n\n int: The notation size of the annotated note
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnNote.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnNote.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnNote
. Since there\nis only one GeneralNote here, this will always be a single-element list.
\n\nReturns
\n\n\n [int]: A list containing the single GeneralNote id for this note.
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnVoice", "modulename": "musicdiff.annotation", "qualname": "AnnVoice", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnVoice.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnVoice.__init__", "type": "function", "doc": "Extend music21 Voice with some precomputed, easily compared information about it.
\n\nArgs
\n\n\n- voice (music21.stream.Voice): The music21 voice to extend.
\n
\n", "signature": "(self, voice)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnVoice.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnVoice.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnVoice
.
\n\nReturns
\n\n\n int: The notation size of the annotated voice
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnVoice.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnVoice.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnVoice
.
\n\nReturns
\n\n\n [int]: A list containing the GeneralNote ids contained in this voice
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnMeasure", "modulename": "musicdiff.annotation", "qualname": "AnnMeasure", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnMeasure.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnMeasure.__init__", "type": "function", "doc": "Extend music21 Measure with some precomputed, easily compared information about it.
\n\nArgs
\n\n\n- measure (music21.stream.Measure): The music21 measure to extend.
\n
\n", "signature": "(self, measure)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnMeasure.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnMeasure.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnMeasure
.
\n\nReturns
\n\n\n int: The notation size of the annotated measure
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnMeasure.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnMeasure.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnMeasure
.
\n\nReturns
\n\n\n [int]: A list containing the GeneralNote ids contained in this measure
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnPart", "modulename": "musicdiff.annotation", "qualname": "AnnPart", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnPart.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnPart.__init__", "type": "function", "doc": "Extend music21 Part/PartStaff with some precomputed, easily compared information about it.
\n\nArgs
\n\n\n- part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff to extend.
\n
\n", "signature": "(self, part)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnPart.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnPart.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnPart
.
\n\nReturns
\n\n\n int: The notation size of the annotated part
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnPart.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnPart.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnPart
.
\n\nReturns
\n\n\n [int]: A list containing the GeneralNote ids contained in this part
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnScore", "modulename": "musicdiff.annotation", "qualname": "AnnScore", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnScore.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnScore.__init__", "type": "function", "doc": "Take a music21 score and store it as a sequence of Full Trees.\nThe hierarchy is \"score -> parts -> measures -> voices -> notes\"
\n\nArgs
\n\n\n- score (music21.stream.Score): The music21 score
\n
\n", "signature": "(self, score)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnScore.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnScore.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnScore
.
\n\nReturns
\n\n\n int: The notation size of the annotated score
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnScore.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnScore.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnScore
.
\n\nReturns
\n\n\n [int]: A list containing the GeneralNote ids contained in this score
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.comparison", "modulename": "musicdiff.comparison", "type": "module", "doc": "\n"}, {"fullname": "musicdiff.comparison.Comparison", "modulename": "musicdiff.comparison", "qualname": "Comparison", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.comparison.Comparison.__init__", "modulename": "musicdiff.comparison", "qualname": "Comparison.__init__", "type": "function", "doc": "\n", "signature": "()", "funcdef": "def"}, {"fullname": "musicdiff.comparison.Comparison.annotated_scores_diff", "modulename": "musicdiff.comparison", "qualname": "Comparison.annotated_scores_diff", "type": "function", "doc": "Compare two annotated scores, computing an operations list and the cost of applying those\noperations to the first score to generate the second score.
\n\nArgs
\n\n\n- score1 (
musicdiff.annotation.AnnScore
): The first annotated score to compare. \n- score2 (
musicdiff.annotation.AnnScore
): The second annotated score to compare. \n
\n\nReturns
\n\n\n List[Tuple], int: The operations list and the cost
\n
\n", "signature": "(\n score1: musicdiff.annotation.AnnScore,\n score2: musicdiff.annotation.AnnScore\n) -> Tuple[List[Tuple], int]", "funcdef": "def"}, {"fullname": "musicdiff.visualization", "modulename": "musicdiff.visualization", "type": "module", "doc": "\n"}, {"fullname": "musicdiff.visualization.Visualization", "modulename": "musicdiff.visualization", "qualname": "Visualization", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.visualization.Visualization.__init__", "modulename": "musicdiff.visualization", "qualname": "Visualization.__init__", "type": "function", "doc": "\n", "signature": "()", "funcdef": "def"}, {"fullname": "musicdiff.visualization.Visualization.INSERTED_COLOR", "modulename": "musicdiff.visualization", "qualname": "Visualization.INSERTED_COLOR", "type": "variable", "doc": "INSERTED_COLOR
can be set to customize the rendered score markup that mark_diffs
does.
\n", "default_value": " = 'red'"}, {"fullname": "musicdiff.visualization.Visualization.DELETED_COLOR", "modulename": "musicdiff.visualization", "qualname": "Visualization.DELETED_COLOR", "type": "variable", "doc": "DELETED_COLOR
can be set to customize the rendered score markup that mark_diffs
does.
\n", "default_value": " = 'red'"}, {"fullname": "musicdiff.visualization.Visualization.CHANGED_COLOR", "modulename": "musicdiff.visualization", "qualname": "Visualization.CHANGED_COLOR", "type": "variable", "doc": "CHANGED_COLOR
can be set to customize the rendered score markup that mark_diffs
does.
\n", "default_value": " = 'red'"}, {"fullname": "musicdiff.visualization.Visualization.mark_diffs", "modulename": "musicdiff.visualization", "qualname": "Visualization.mark_diffs", "type": "function", "doc": "Mark up two music21 scores with the differences described by an operations\nlist (e.g. a list returned from musicdiff.Comparison.annotated_scores_diff
).
\n\nArgs
\n\n\n- score1 (music21.stream.Score): The first score to mark up
\n- score2 (music21.stream.Score): The second score to mark up
\n- operations (List[Tuple]): The operations list that describes the difference\nbetween the two scores
\n
\n", "signature": "(\n score1: music21.stream.base.Score,\n score2: music21.stream.base.Score,\n operations: List[Tuple]\n)", "funcdef": "def"}, {"fullname": "musicdiff.visualization.Visualization.show_diffs", "modulename": "musicdiff.visualization", "qualname": "Visualization.show_diffs", "type": "function", "doc": "Render two (presumably marked-up) music21 scores. If both out_path1 and out_path2 are not None,\nsave the rendered PDFs at those two locations, otherwise just display them using the default\nPDF viewer on the system.
\n\nArgs
\n\n\n- score1 (music21.stream.Score): The first score to render
\n- score2 (music21.stream.Score): The second score to render
\n- out_path1 (str, Path): Where to save the first marked-up rendered score PDF.\nIf out_path1 is None, both PDFs will be displayed in the default PDF viewer.\n(default is None)
\n- out_path2 (str, Path): Where to save the second marked-up rendered score PDF.\nIf out_path2 is None, both PDFs will be displayed in the default PDF viewer.\n(default is None)
\n
\n", "signature": "(\n score1: music21.stream.base.Score,\n score2: music21.stream.base.Score,\n out_path1: Union[str, pathlib.Path] = None,\n out_path2: Union[str, pathlib.Path] = None\n)", "funcdef": "def"}];
+ /** pdoc search index */const docs = [{"fullname": "musicdiff", "modulename": "musicdiff", "type": "module", "doc": "\n"}, {"fullname": "musicdiff.diff", "modulename": "musicdiff", "qualname": "diff", "type": "function", "doc": "Compare two musical scores and optionally save/display the differences as two marked-up\nrendered PDFs.
\n\nArgs
\n\n\n- score1 (str, Path, music21.stream.Score): The first music score to compare. The score\ncan be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI,\netc), or a music21 Score object.
\n- score2 (str, Path, music21.stream.Score): The second musical score to compare. The score\ncan be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI,\netc), or a music21 Score object.
\n- out_path1 (str, Path): Where to save the first marked-up rendered score PDF.\nIf out_path1 is None, both PDFs will be displayed in the default PDF viewer.\n(default is None)
\n- out_path2 (str, Path): Where to save the second marked-up rendered score PDF.\nIf out_path2 is None, both PDFs will be displayed in the default PDF viewer.\n(default is None)
\n- force_parse (bool): Whether or not to force music21 to re-parse a file it has parsed\npreviously.\n(default is True)
\n- visualize_diffs (bool): Whether or not to render diffs as marked up PDFs. If False,\nthe only result of the call will be the return value (the number of differences).\n(default is True)
\n- detail (DetailLevel): What level of detail to use during the diff. Can be\nGeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is\ncurrently equivalent to AllObjects).
\n
\n\nReturns
\n\n\n int: The number of differences found (0 means the scores were identical, None means the diff failed)
\n
\n", "signature": "(\n score1: Union[str, pathlib.Path, music21.stream.base.Score],\n score2: Union[str, pathlib.Path, music21.stream.base.Score],\n out_path1: Union[str, pathlib.Path] = None,\n out_path2: Union[str, pathlib.Path] = None,\n force_parse: bool = True,\n visualize_diffs: bool = True,\n detail: musicdiff.m21utils.DetailLevel = \n) -> int", "funcdef": "def"}, {"fullname": "musicdiff.annotation", "modulename": "musicdiff.annotation", "type": "module", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnNote", "modulename": "musicdiff.annotation", "qualname": "AnnNote", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnNote.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnNote.__init__", "type": "function", "doc": "Extend music21 GeneralNote with some precomputed, easily compared information about it.
\n\nArgs
\n\n\n- general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
\n- enhanced_beam_list (list): A list of beaming information about this GeneralNote.
\n- tuplet_list (list): A list of tuplet info about this GeneralNote.
\n- detail (DetailLevel): What level of detail to use during the diff. Can be\nGeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is\ncurrently equivalent to AllObjects).
\n
\n", "signature": "(\n self,\n general_note: music21.note.GeneralNote,\n enhanced_beam_list,\n tuplet_list,\n detail: musicdiff.m21utils.DetailLevel = \n)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnNote.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnNote.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnNote
.
\n\nReturns
\n\n\n int: The notation size of the annotated note
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnNote.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnNote.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnNote
. Since there\nis only one GeneralNote here, this will always be a single-element list.
\n\nReturns
\n\n\n [int]: A list containing the single GeneralNote id for this note.
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnExtra", "modulename": "musicdiff.annotation", "qualname": "AnnExtra", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnExtra.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnExtra.__init__", "type": "function", "doc": "Extend music21 non-GeneralNote and non-Stream objects with some precomputed, easily compared information about it.\nExamples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc.
\n\nArgs
\n\n\n- extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream object to extend.
\n- measure (music21.stream.Measure): The music21 Measure the extra was found in. If the extra\nwas found in a Voice, this is the Measure that the Voice was found in.
\n- detail (DetailLevel): What level of detail to use during the diff. Can be\nGeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is\ncurrently equivalent to AllObjects).
\n
\n", "signature": "(\n self,\n extra: music21.base.Music21Object,\n measure: music21.stream.base.Measure,\n score: music21.stream.base.Score,\n detail: musicdiff.m21utils.DetailLevel = \n)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnExtra.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnExtra.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnExtra
.
\n\nReturns
\n\n\n int: The notation size of the annotated extra
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnVoice", "modulename": "musicdiff.annotation", "qualname": "AnnVoice", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnVoice.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnVoice.__init__", "type": "function", "doc": "Extend music21 Voice with some precomputed, easily compared information about it.
\n\nArgs
\n\n\n- voice (music21.stream.Voice): The music21 voice to extend.
\n- detail (DetailLevel): What level of detail to use during the diff. Can be\nGeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is\ncurrently equivalent to AllObjects).
\n
\n", "signature": "(\n self,\n voice: music21.stream.base.Voice,\n detail: musicdiff.m21utils.DetailLevel = \n)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnVoice.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnVoice.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnVoice
.
\n\nReturns
\n\n\n int: The notation size of the annotated voice
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnVoice.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnVoice.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnVoice
.
\n\nReturns
\n\n\n [int]: A list containing the GeneralNote ids contained in this voice
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnMeasure", "modulename": "musicdiff.annotation", "qualname": "AnnMeasure", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnMeasure.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnMeasure.__init__", "type": "function", "doc": "Extend music21 Measure with some precomputed, easily compared information about it.
\n\nArgs
\n\n\n- measure (music21.stream.Measure): The music21 measure to extend.
\n- score (music21.stream.Score): the enclosing music21 Score.
\n- spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
\n- detail (DetailLevel): What level of detail to use during the diff. Can be\nGeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is\ncurrently equivalent to AllObjects).
\n
\n", "signature": "(\n self,\n measure: music21.stream.base.Measure,\n score: music21.stream.base.Score,\n spannerBundle: music21.spanner.SpannerBundle,\n detail: musicdiff.m21utils.DetailLevel = \n)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnMeasure.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnMeasure.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnMeasure
.
\n\nReturns
\n\n\n int: The notation size of the annotated measure
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnMeasure.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnMeasure.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnMeasure
.
\n\nReturns
\n\n\n [int]: A list containing the GeneralNote ids contained in this measure
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnPart", "modulename": "musicdiff.annotation", "qualname": "AnnPart", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnPart.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnPart.__init__", "type": "function", "doc": "Extend music21 Part/PartStaff with some precomputed, easily compared information about it.
\n\nArgs
\n\n\n- part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff to extend.
\n- score (music21.stream.Score): the enclosing music21 Score.
\n- spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
\n- detail (DetailLevel): What level of detail to use during the diff. Can be\nGeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is\ncurrently equivalent to AllObjects).
\n
\n", "signature": "(\n self,\n part: music21.stream.base.Part,\n score: music21.stream.base.Score,\n spannerBundle: music21.spanner.SpannerBundle,\n detail: musicdiff.m21utils.DetailLevel = \n)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnPart.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnPart.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnPart
.
\n\nReturns
\n\n\n int: The notation size of the annotated part
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnPart.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnPart.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnPart
.
\n\nReturns
\n\n\n [int]: A list containing the GeneralNote ids contained in this part
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnScore", "modulename": "musicdiff.annotation", "qualname": "AnnScore", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.annotation.AnnScore.__init__", "modulename": "musicdiff.annotation", "qualname": "AnnScore.__init__", "type": "function", "doc": "Take a music21 score and store it as a sequence of Full Trees.\nThe hierarchy is \"score -> parts -> measures -> voices -> notes\"
\n\nArgs
\n\n\n- score (music21.stream.Score): The music21 score
\n- detail (DetailLevel): What level of detail to use during the diff. Can be\nGeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is\ncurrently equivalent to AllObjects).
\n
\n", "signature": "(\n self,\n score: music21.stream.base.Score,\n detail: musicdiff.m21utils.DetailLevel = \n)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnScore.notation_size", "modulename": "musicdiff.annotation", "qualname": "AnnScore.notation_size", "type": "function", "doc": "Compute a measure of how many symbols are displayed in the score for this AnnScore
.
\n\nReturns
\n\n\n int: The notation size of the annotated score
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.annotation.AnnScore.get_note_ids", "modulename": "musicdiff.annotation", "qualname": "AnnScore.get_note_ids", "type": "function", "doc": "Computes a list of the GeneralNote ids for this AnnScore
.
\n\nReturns
\n\n\n [int]: A list containing the GeneralNote ids contained in this score
\n
\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "musicdiff.comparison", "modulename": "musicdiff.comparison", "type": "module", "doc": "\n"}, {"fullname": "musicdiff.comparison.Comparison", "modulename": "musicdiff.comparison", "qualname": "Comparison", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.comparison.Comparison.__init__", "modulename": "musicdiff.comparison", "qualname": "Comparison.__init__", "type": "function", "doc": "\n", "signature": "()", "funcdef": "def"}, {"fullname": "musicdiff.comparison.Comparison.annotated_scores_diff", "modulename": "musicdiff.comparison", "qualname": "Comparison.annotated_scores_diff", "type": "function", "doc": "Compare two annotated scores, computing an operations list and the cost of applying those\noperations to the first score to generate the second score.
\n\nArgs
\n\n\n- score1 (
musicdiff.annotation.AnnScore
): The first annotated score to compare. \n- score2 (
musicdiff.annotation.AnnScore
): The second annotated score to compare. \n
\n\nReturns
\n\n\n List[Tuple], int: The operations list and the cost
\n
\n", "signature": "(\n score1: musicdiff.annotation.AnnScore,\n score2: musicdiff.annotation.AnnScore\n) -> Tuple[List[Tuple], int]", "funcdef": "def"}, {"fullname": "musicdiff.visualization", "modulename": "musicdiff.visualization", "type": "module", "doc": "\n"}, {"fullname": "musicdiff.visualization.Visualization", "modulename": "musicdiff.visualization", "qualname": "Visualization", "type": "class", "doc": "\n"}, {"fullname": "musicdiff.visualization.Visualization.__init__", "modulename": "musicdiff.visualization", "qualname": "Visualization.__init__", "type": "function", "doc": "\n", "signature": "()", "funcdef": "def"}, {"fullname": "musicdiff.visualization.Visualization.INSERTED_COLOR", "modulename": "musicdiff.visualization", "qualname": "Visualization.INSERTED_COLOR", "type": "variable", "doc": "INSERTED_COLOR
can be set to customize the rendered score markup that mark_diffs
does.
\n", "default_value": " = 'red'"}, {"fullname": "musicdiff.visualization.Visualization.DELETED_COLOR", "modulename": "musicdiff.visualization", "qualname": "Visualization.DELETED_COLOR", "type": "variable", "doc": "DELETED_COLOR
can be set to customize the rendered score markup that mark_diffs
does.
\n", "default_value": " = 'red'"}, {"fullname": "musicdiff.visualization.Visualization.CHANGED_COLOR", "modulename": "musicdiff.visualization", "qualname": "Visualization.CHANGED_COLOR", "type": "variable", "doc": "CHANGED_COLOR
can be set to customize the rendered score markup that mark_diffs
does.
\n", "default_value": " = 'red'"}, {"fullname": "musicdiff.visualization.Visualization.mark_diffs", "modulename": "musicdiff.visualization", "qualname": "Visualization.mark_diffs", "type": "function", "doc": "Mark up two music21 scores with the differences described by an operations\nlist (e.g. a list returned from musicdiff.Comparison.annotated_scores_diff
).
\n\nArgs
\n\n\n- score1 (music21.stream.Score): The first score to mark up
\n- score2 (music21.stream.Score): The second score to mark up
\n- operations (List[Tuple]): The operations list that describes the difference\nbetween the two scores
\n
\n", "signature": "(\n score1: music21.stream.base.Score,\n score2: music21.stream.base.Score,\n operations: List[Tuple]\n)", "funcdef": "def"}, {"fullname": "musicdiff.visualization.Visualization.show_diffs", "modulename": "musicdiff.visualization", "qualname": "Visualization.show_diffs", "type": "function", "doc": "Render two (presumably marked-up) music21 scores. If both out_path1 and out_path2 are not None,\nsave the rendered PDFs at those two locations, otherwise just display them using the default\nPDF viewer on the system.
\n\nArgs
\n\n\n- score1 (music21.stream.Score): The first score to render
\n- score2 (music21.stream.Score): The second score to render
\n- out_path1 (str, Path): Where to save the first marked-up rendered score PDF.\nIf out_path1 is None, both PDFs will be displayed in the default PDF viewer.\n(default is None)
\n- out_path2 (str, Path): Where to save the second marked-up rendered score PDF.\nIf out_path2 is None, both PDFs will be displayed in the default PDF viewer.\n(default is None)
\n
\n", "signature": "(\n score1: music21.stream.base.Score,\n score2: music21.stream.base.Score,\n out_path1: Union[str, pathlib.Path] = None,\n out_path2: Union[str, pathlib.Path] = None\n)", "funcdef": "def"}];
// mirrored in build-search-index.js (part 1)
// Also split on html tags. this is a cheap heuristic, but good enough.