diff --git a/docs/musicdiff.html b/docs/musicdiff.html index 3144195..174b4be 100644 --- a/docs/musicdiff.html +++ b/docs/musicdiff.html @@ -75,6 +75,7 @@

import music21 as m21 from musicdiff.m21utils import M21Utils +from musicdiff.m21utils import DetailLevel from musicdiff.annotation import AnnScore from musicdiff.comparison import Comparison from musicdiff.visualization import Visualization @@ -103,6 +104,7 @@

out_path2: Union[str, Path] = None, force_parse: bool = True, visualize_diffs: bool = True, + detail: DetailLevel = DetailLevel.Default ) -> int: ''' Compare two musical scores and optionally save/display the differences as two marked-up @@ -127,6 +129,9 @@

visualize_diffs (bool): Whether or not to render diffs as marked up PDFs. If False, the only result of the call will be the return value (the number of differences). (default is True) + detail (DetailLevel): What level of detail to use during the diff. Can be + GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is + currently equivalent to AllObjects). Returns: int: The number of differences found (0 means the scores were identical, None means the diff failed) @@ -192,8 +197,8 @@

return None # scan each score, producing an annotated wrapper - annotated_score1: AnnScore = AnnScore(score1) - annotated_score2: AnnScore = AnnScore(score2) + annotated_score1: AnnScore = AnnScore(score1, detail) + annotated_score2: AnnScore = AnnScore(score2, detail) diff_list: List = None _cost: int = None @@ -230,7 +235,8 @@

out_path1: Union[str, pathlib.Path] = None, out_path2: Union[str, pathlib.Path] = None, force_parse: bool = True, - visualize_diffs: bool = True + visualize_diffs: bool = True, + detail: musicdiff.m21utils.DetailLevel = <DetailLevel.AllObjects: 2> ) -> int: @@ -242,6 +248,7 @@

out_path2: Union[str, Path] = None, force_parse: bool = True, visualize_diffs: bool = True, + detail: DetailLevel = DetailLevel.Default ) -> int: ''' Compare two musical scores and optionally save/display the differences as two marked-up @@ -266,6 +273,9 @@

visualize_diffs (bool): Whether or not to render diffs as marked up PDFs. If False, the only result of the call will be the return value (the number of differences). (default is True) + detail (DetailLevel): What level of detail to use during the diff. Can be + GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is + currently equivalent to AllObjects). Returns: int: The number of differences found (0 means the scores were identical, None means the diff failed) @@ -331,8 +341,8 @@

return None # scan each score, producing an annotated wrapper - annotated_score1: AnnScore = AnnScore(score1) - annotated_score2: AnnScore = AnnScore(score2) + annotated_score1: AnnScore = AnnScore(score1, detail) + annotated_score2: AnnScore = AnnScore(score2, detail) diff_list: List = None _cost: int = None @@ -381,6 +391,9 @@

Args
  • visualize_diffs (bool): Whether or not to render diffs as marked up PDFs. If False, the only result of the call will be the return value (the number of differences). (default is True)
  • +
  • detail (DetailLevel): What level of detail to use during the diff. Can be +GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is +currently equivalent to AllObjects).
  • Returns
    diff --git a/docs/musicdiff/__main__.html b/docs/musicdiff/__main__.html new file mode 100644 index 0000000..afbdd1c --- /dev/null +++ b/docs/musicdiff/__main__.html @@ -0,0 +1,298 @@ + + + + + + + musicdiff.__main__ API documentation + + + + + + + + +
    +
    +

    +musicdiff.__main__

    + + +
    + View Source +
    # ------------------------------------------------------------------------------
    +# Purpose:       __main__.py is a music file comparison tool built on musicdiff.
    +#                musicdiff is a package for comparing music scores using music21.
    +#                Usage:
    +#                   python3 -m musicdiff filePath1 filePath2
    +#
    +# Authors:       Greg Chapman <gregc@mac.com>
    +#                musicdiff is derived from:
    +#                   https://github.com/fosfrancesco/music-score-diff.git
    +#                   by Francesco Foscarin <foscarin.francesco@gmail.com>
    +#
    +# Copyright:     (c) 2022 Francesco Foscarin, Greg Chapman
    +# License:       MIT, see LICENSE
    +# ------------------------------------------------------------------------------
    +import sys
    +import argparse
    +
    +from musicdiff import diff
    +from musicdiff import DetailLevel
    +
    +# To use the new Humdrum importer from converter21 in place of the one in music21:
    +# pip install converter21
    +# Then uncomment all lines in this file marked "# c21"
    +# import music21 as m21 # c21
    +# from converter21 import HumdrumConverter # c21
    +
    +# ------------------------------------------------------------------------------
    +
    +'''
    +    main entry point (parse arguments and do conversion)
    +'''
    +if __name__ == "__main__":
    +
    +    # to use the new Humdrum importer from converter21 in place of the one in music21...
    +    # m21.converter.unregisterSubconverter(m21.converter.subConverters.ConverterHumdrum) # c21
    +    # m21.converter.registerSubconverter(HumdrumConverter)                               # c21
    +    # print('registered converter21 humdrum importer', file=sys.stderr)                  # c21
    +
    +    parser = argparse.ArgumentParser(
    +                prog='python3 -m musicdiff',
    +                description='Music score notation diff (MusicXML, MEI, Humdrum, etc)')
    +    parser.add_argument("file1",
    +                        help="first music score file to compare (any format music21 can parse)")
    +    parser.add_argument("file2",
    +                        help="second music score file to compare (any format music21 can parse)")
    +    parser.add_argument("-d", "--detail", default="Default",
    +                        choices=["GeneralNotesOnly", "AllObjects", "AllObjectsWithStyle", "Default"],
    +                        help="set detail level")
    +    args = parser.parse_args()
    +
    +    detail: DetailLevel = DetailLevel.Default
    +    if args.detail == "GeneralNotesOnly":
    +        detail = DetailLevel.GeneralNotesOnly
    +    elif args.detail == "AllObjects":
    +        detail = DetailLevel.AllObjects
    +    elif args.detail == "AllObjectsWithStyle":
    +        detail = DetailLevel.AllObjectsWithStyle
    +    elif args.detail == "Default":
    +        detail = DetailLevel.Default
    +
    +    # Note that diff() can take a music21 Score instead of a file, for either
    +    # or both arguments.
    +    # Note also that diff() can take str or pathlib.Path for files.
    +    numDiffs: int = diff(args.file1, args.file2, detail=detail)
    +    if numDiffs is not None and numDiffs == 0:
    +        print(f'Scores in {args.file1} and {args.file2} are identical.', file=sys.stderr)
    +
    + +
    + +
    +
    + + \ No newline at end of file diff --git a/docs/musicdiff/annotation.html b/docs/musicdiff/annotation.html index b7157f4..bb930c1 100644 --- a/docs/musicdiff/annotation.html +++ b/docs/musicdiff/annotation.html @@ -44,6 +44,18 @@

    API Documentation

    + +
  • + AnnExtra + +
  • AnnVoice @@ -141,14 +153,15 @@

    __docformat__ = "google" from fractions import Fraction +from typing import Optional import music21 as m21 from musicdiff import M21Utils - +from musicdiff import DetailLevel class AnnNote: - def __init__(self, general_note, enhanced_beam_list, tuplet_list): + def __init__(self, general_note: m21.note.GeneralNote, enhanced_beam_list, tuplet_list, detail: DetailLevel = DetailLevel.Default): """ Extend music21 GeneralNote with some precomputed, easily compared information about it. @@ -156,27 +169,51 @@

    general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend. enhanced_beam_list (list): A list of beaming information about this GeneralNote. tuplet_list (list): A list of tuplet info about this GeneralNote. + detail (DetailLevel): What level of detail to use during the diff. Can be + GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is + currently equivalent to AllObjects). + """ self.general_note = general_note.id self.beamings = enhanced_beam_list self.tuplets = tuplet_list - ##compute the representaiton of NoteNode as in the paper + + self.stylestr: str = '' + self.styledict: dict = {} + if M21Utils.has_style(general_note): + self.styledict = M21Utils.obj_to_styledict(general_note, detail) + self.noteshape: str = 'normal' + self.noteheadFill: Optional[bool] = None + self.noteheadParenthesis: bool = False + self.stemDirection: str = 'unspecified' + if detail >= DetailLevel.AllObjectsWithStyle and isinstance(general_note, m21.note.NotRest): + self.noteshape = general_note.notehead + self.noteheadFill = general_note.noteheadFill + self.noteheadParenthesis = general_note.noteheadParenthesis + self.stemDirection = general_note.stemDirection + + # compute the representation of NoteNode as in the paper # pitches is a list of elements, each one is (pitchposition, accidental, tie) if general_note.isRest: self.pitches = [ ("R", "None", False) ] # accidental and tie are automaticaly set for rests - elif general_note.isChord: + elif general_note.isChord or "ChordBase" in general_note.classSet: + # ChordBase/PercussionChord is new in v7, so I am being careful to use + # it only as a string so v6 will still work. + noteList: [m21.note.GeneralNote] = general_note.notes + if hasattr(general_note, "sortDiatonicAscending"): # PercussionChords don't have this + noteList = general_note.sortDiatonicAscending().notes self.pitches = [ - M21Utils.note2tuple(p) for p in general_note.sortDiatonicAscending().notes + M21Utils.note2tuple(p) for p in noteList ] - elif general_note.isNote: + elif general_note.isNote or isinstance(general_note, m21.note.Unpitched): self.pitches = [M21Utils.note2tuple(general_note)] else: raise TypeError("The generalNote must be a Chord, a Rest or a Note") # note head type_number = Fraction( - m21.duration.convertTypeToNumber(general_note.duration.type) + M21Utils.get_type_num(general_note.duration) ) if type_number >= 4: self.note_head = 4 @@ -222,7 +259,8 @@

    def __repr__(self): # does consider the MEI id! return (f"{self.pitches},{self.note_head},{self.dots},{self.beamings}," + - f"{self.tuplets},{self.general_note},{self.articulations},{self.expressions}") + f"{self.tuplets},{self.general_note},{self.articulations},{self.expressions}" + + f"{self.styledict}") def __str__(self): """ @@ -272,6 +310,22 @@

    if len(self.expressions) > 0: # add for articulations for e in self.expressions: string += e + + if self.noteshape != 'normal': + string += f"noteshape={self.noteshape}" + if self.noteheadFill is not None: + string += f"noteheadFill={self.noteheadFill}" + if self.noteheadParenthesis: + string += f"noteheadParenthesis={self.noteheadParenthesis}" + if self.stemDirection != 'unspecified': + string += f"stemDirection={self.stemDirection}" + + # and then the style fields + for i, (k, v) in enumerate(self.styledict.items()): + if i > 0: + string += "," + string += f"{k}={v}" + return string def get_note_ids(self): @@ -308,34 +362,102 @@

    # return True +class AnnExtra: + def __init__(self, extra: m21.base.Music21Object, measure: m21.stream.Measure, score: m21.stream.Score, detail: DetailLevel = DetailLevel.Default): + """ + Extend music21 non-GeneralNote and non-Stream objects with some precomputed, easily compared information about it. + Examples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc. + + Args: + extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream object to extend. + measure (music21.stream.Measure): The music21 Measure the extra was found in. If the extra + was found in a Voice, this is the Measure that the Voice was found in. + 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.extra = extra.id + self.offset: float + self.duration: float + if isinstance(extra, m21.spanner.Spanner): + firstNote: m21.note.GeneralNote = extra.getFirst() + lastNote: m21.note.GeneralNote = extra.getLast() + self.offset = float(firstNote.getOffsetInHierarchy(measure)) + # to compute duration we need to use offset-in-score, since the end note might be in another Measure + startOffsetInScore: float = float(firstNote.getOffsetInHierarchy(score)) + endOffsetInScore: float = float(lastNote.getOffsetInHierarchy(score) + lastNote.duration.quarterLength) + self.duration = endOffsetInScore - startOffsetInScore + else: + self.offset = float(extra.getOffsetInHierarchy(measure)) + self.duration = float(extra.duration.quarterLength) + self.content: str = M21Utils.extra_to_string(extra) + self.styledict: str = {} + if M21Utils.has_style(extra): + self.styledict = M21Utils.obj_to_styledict(extra, detail) # includes extra.placement if present + self._notation_size: int = 1 # so far, always 1, but maybe some extra will be bigger someday + + # precomputed representations for faster comparison + self.precomputed_str = self.__str__() + + def notation_size(self): + """ + Compute a measure of how many symbols are displayed in the score for this `AnnExtra`. + + Returns: + int: The notation size of the annotated extra + """ + return self._notation_size + + def __repr__(self): + return str(self) + + def __str__(self): + """ + Returns: + str: the compared representation of the AnnExtra. Does not consider music21 id. + """ + string = f'{self.content},off={self.offset},dur={self.duration}' + # and then any style fields + for k, v in self.styledict.items(): + string += f",{k}={v}" + return string + + def __eq__(self, other): + # equality does not consider the MEI id! + return self.precomputed_str == other.precomputed_str + + 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) @@ -389,34 +511,54 @@

    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! @@ -426,6 +568,9 @@

    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)]) @@ -436,10 +581,7 @@

    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): """ @@ -455,17 +597,25 @@

    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) @@ -511,18 +661,22 @@

    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) @@ -586,7 +740,7 @@

    View Source
    class AnnNote:
    -    def __init__(self, general_note, enhanced_beam_list, tuplet_list):
    +    def __init__(self, general_note: m21.note.GeneralNote, enhanced_beam_list, tuplet_list, detail: DetailLevel = DetailLevel.Default):
             """
             Extend music21 GeneralNote with some precomputed, easily compared information about it.
     
    @@ -594,27 +748,51 @@ 

    general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend. enhanced_beam_list (list): A list of beaming information about this GeneralNote. tuplet_list (list): A list of tuplet info about this GeneralNote. + detail (DetailLevel): What level of detail to use during the diff. Can be + GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is + currently equivalent to AllObjects). + """ self.general_note = general_note.id self.beamings = enhanced_beam_list self.tuplets = tuplet_list - ##compute the representaiton of NoteNode as in the paper + + self.stylestr: str = '' + self.styledict: dict = {} + if M21Utils.has_style(general_note): + self.styledict = M21Utils.obj_to_styledict(general_note, detail) + self.noteshape: str = 'normal' + self.noteheadFill: Optional[bool] = None + self.noteheadParenthesis: bool = False + self.stemDirection: str = 'unspecified' + if detail >= DetailLevel.AllObjectsWithStyle and isinstance(general_note, m21.note.NotRest): + self.noteshape = general_note.notehead + self.noteheadFill = general_note.noteheadFill + self.noteheadParenthesis = general_note.noteheadParenthesis + self.stemDirection = general_note.stemDirection + + # compute the representation of NoteNode as in the paper # pitches is a list of elements, each one is (pitchposition, accidental, tie) if general_note.isRest: self.pitches = [ ("R", "None", False) ] # accidental and tie are automaticaly set for rests - elif general_note.isChord: + elif general_note.isChord or "ChordBase" in general_note.classSet: + # ChordBase/PercussionChord is new in v7, so I am being careful to use + # it only as a string so v6 will still work. + noteList: [m21.note.GeneralNote] = general_note.notes + if hasattr(general_note, "sortDiatonicAscending"): # PercussionChords don't have this + noteList = general_note.sortDiatonicAscending().notes self.pitches = [ - M21Utils.note2tuple(p) for p in general_note.sortDiatonicAscending().notes + M21Utils.note2tuple(p) for p in noteList ] - elif general_note.isNote: + elif general_note.isNote or isinstance(general_note, m21.note.Unpitched): self.pitches = [M21Utils.note2tuple(general_note)] else: raise TypeError("The generalNote must be a Chord, a Rest or a Note") # note head type_number = Fraction( - m21.duration.convertTypeToNumber(general_note.duration.type) + M21Utils.get_type_num(general_note.duration) ) if type_number >= 4: self.note_head = 4 @@ -660,7 +838,8 @@

    def __repr__(self): # does consider the MEI id! return (f"{self.pitches},{self.note_head},{self.dots},{self.beamings}," + - f"{self.tuplets},{self.general_note},{self.articulations},{self.expressions}") + f"{self.tuplets},{self.general_note},{self.articulations},{self.expressions}" + + f"{self.styledict}") def __str__(self): """ @@ -710,6 +889,22 @@

    if len(self.expressions) > 0: # add for articulations for e in self.expressions: string += e + + if self.noteshape != 'normal': + string += f"noteshape={self.noteshape}" + if self.noteheadFill is not None: + string += f"noteheadFill={self.noteheadFill}" + if self.noteheadParenthesis: + string += f"noteheadParenthesis={self.noteheadParenthesis}" + if self.stemDirection != 'unspecified': + string += f"stemDirection={self.stemDirection}" + + # and then the style fields + for i, (k, v) in enumerate(self.styledict.items()): + if i > 0: + string += "," + string += f"{k}={v}" + return string def get_note_ids(self): @@ -754,12 +949,17 @@

    #   - AnnNote(general_note, enhanced_beam_list, tuplet_list) + AnnNote( + general_note: music21.note.GeneralNote, + enhanced_beam_list, + tuplet_list, + detail: musicdiff.m21utils.DetailLevel = <DetailLevel.AllObjects: 2> +)
    View Source -
        def __init__(self, general_note, enhanced_beam_list, tuplet_list):
    +            
        def __init__(self, general_note: m21.note.GeneralNote, enhanced_beam_list, tuplet_list, detail: DetailLevel = DetailLevel.Default):
             """
             Extend music21 GeneralNote with some precomputed, easily compared information about it.
     
    @@ -767,27 +967,51 @@ 

    general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend. enhanced_beam_list (list): A list of beaming information about this GeneralNote. tuplet_list (list): A list of tuplet info about this GeneralNote. + detail (DetailLevel): What level of detail to use during the diff. Can be + GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is + currently equivalent to AllObjects). + """ self.general_note = general_note.id self.beamings = enhanced_beam_list self.tuplets = tuplet_list - ##compute the representaiton of NoteNode as in the paper + + self.stylestr: str = '' + self.styledict: dict = {} + if M21Utils.has_style(general_note): + self.styledict = M21Utils.obj_to_styledict(general_note, detail) + self.noteshape: str = 'normal' + self.noteheadFill: Optional[bool] = None + self.noteheadParenthesis: bool = False + self.stemDirection: str = 'unspecified' + if detail >= DetailLevel.AllObjectsWithStyle and isinstance(general_note, m21.note.NotRest): + self.noteshape = general_note.notehead + self.noteheadFill = general_note.noteheadFill + self.noteheadParenthesis = general_note.noteheadParenthesis + self.stemDirection = general_note.stemDirection + + # compute the representation of NoteNode as in the paper # pitches is a list of elements, each one is (pitchposition, accidental, tie) if general_note.isRest: self.pitches = [ ("R", "None", False) ] # accidental and tie are automaticaly set for rests - elif general_note.isChord: + elif general_note.isChord or "ChordBase" in general_note.classSet: + # ChordBase/PercussionChord is new in v7, so I am being careful to use + # it only as a string so v6 will still work. + noteList: [m21.note.GeneralNote] = general_note.notes + if hasattr(general_note, "sortDiatonicAscending"): # PercussionChords don't have this + noteList = general_note.sortDiatonicAscending().notes self.pitches = [ - M21Utils.note2tuple(p) for p in general_note.sortDiatonicAscending().notes + M21Utils.note2tuple(p) for p in noteList ] - elif general_note.isNote: + elif general_note.isNote or isinstance(general_note, m21.note.Unpitched): self.pitches = [M21Utils.note2tuple(general_note)] else: raise TypeError("The generalNote must be a Chord, a Rest or a Note") # note head type_number = Fraction( - m21.duration.convertTypeToNumber(general_note.duration.type) + M21Utils.get_type_num(general_note.duration) ) if type_number >= 4: self.note_head = 4 @@ -818,6 +1042,9 @@

    Args
  • general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
  • enhanced_beam_list (list): A list of beaming information about this GeneralNote.
  • tuplet_list (list): A list of tuplet info about this GeneralNote.
  • +
  • detail (DetailLevel): What level of detail to use during the diff. Can be +GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is +currently equivalent to AllObjects).
  • @@ -904,6 +1131,190 @@
    Returns
    +

    + +
    +
    + #   + + + class + AnnExtra: +
    + +
    + View Source +
    class AnnExtra:
    +    def __init__(self, extra: m21.base.Music21Object, measure: m21.stream.Measure, score: m21.stream.Score, detail: DetailLevel = DetailLevel.Default):
    +        """
    +        Extend music21 non-GeneralNote and non-Stream objects with some precomputed, easily compared information about it.
    +        Examples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc.
    +
    +        Args:
    +            extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream object to extend.
    +            measure (music21.stream.Measure): The music21 Measure the extra was found in.  If the extra
    +                was found in a Voice, this is the Measure that the Voice was found in.
    +            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.extra = extra.id
    +        self.offset: float
    +        self.duration: float
    +        if isinstance(extra, m21.spanner.Spanner):
    +            firstNote: m21.note.GeneralNote = extra.getFirst()
    +            lastNote: m21.note.GeneralNote = extra.getLast()
    +            self.offset = float(firstNote.getOffsetInHierarchy(measure))
    +            # to compute duration we need to use offset-in-score, since the end note might be in another Measure
    +            startOffsetInScore: float = float(firstNote.getOffsetInHierarchy(score))
    +            endOffsetInScore: float = float(lastNote.getOffsetInHierarchy(score) + lastNote.duration.quarterLength)
    +            self.duration = endOffsetInScore - startOffsetInScore
    +        else:
    +            self.offset = float(extra.getOffsetInHierarchy(measure))
    +            self.duration = float(extra.duration.quarterLength)
    +        self.content: str = M21Utils.extra_to_string(extra)
    +        self.styledict: str = {}
    +        if M21Utils.has_style(extra):
    +            self.styledict = M21Utils.obj_to_styledict(extra, detail) # includes extra.placement if present
    +        self._notation_size: int = 1 # so far, always 1, but maybe some extra will be bigger someday
    +
    +        # precomputed representations for faster comparison
    +        self.precomputed_str = self.__str__()
    +
    +    def notation_size(self):
    +        """
    +        Compute a measure of how many symbols are displayed in the score for this `AnnExtra`.
    +
    +        Returns:
    +            int: The notation size of the annotated extra
    +        """
    +        return self._notation_size
    +
    +    def __repr__(self):
    +        return str(self)
    +
    +    def __str__(self):
    +        """
    +        Returns:
    +            str: the compared representation of the AnnExtra. Does not consider music21 id.
    +        """
    +        string = f'{self.content},off={self.offset},dur={self.duration}'
    +        # and then any style fields
    +        for k, v in self.styledict.items():
    +            string += f",{k}={v}"
    +        return string
    +
    +    def __eq__(self, other):
    +        # equality does not consider the MEI id!
    +        return self.precomputed_str == other.precomputed_str
    +
    + +
    + + + +
    +
    #   + + + AnnExtra( + extra: music21.base.Music21Object, + measure: music21.stream.base.Measure, + score: music21.stream.base.Score, + detail: musicdiff.m21utils.DetailLevel = <DetailLevel.AllObjects: 2> +) +
    + +
    + View Source +
        def __init__(self, extra: m21.base.Music21Object, measure: m21.stream.Measure, score: m21.stream.Score, detail: DetailLevel = DetailLevel.Default):
    +        """
    +        Extend music21 non-GeneralNote and non-Stream objects with some precomputed, easily compared information about it.
    +        Examples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc.
    +
    +        Args:
    +            extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream object to extend.
    +            measure (music21.stream.Measure): The music21 Measure the extra was found in.  If the extra
    +                was found in a Voice, this is the Measure that the Voice was found in.
    +            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.extra = extra.id
    +        self.offset: float
    +        self.duration: float
    +        if isinstance(extra, m21.spanner.Spanner):
    +            firstNote: m21.note.GeneralNote = extra.getFirst()
    +            lastNote: m21.note.GeneralNote = extra.getLast()
    +            self.offset = float(firstNote.getOffsetInHierarchy(measure))
    +            # to compute duration we need to use offset-in-score, since the end note might be in another Measure
    +            startOffsetInScore: float = float(firstNote.getOffsetInHierarchy(score))
    +            endOffsetInScore: float = float(lastNote.getOffsetInHierarchy(score) + lastNote.duration.quarterLength)
    +            self.duration = endOffsetInScore - startOffsetInScore
    +        else:
    +            self.offset = float(extra.getOffsetInHierarchy(measure))
    +            self.duration = float(extra.duration.quarterLength)
    +        self.content: str = M21Utils.extra_to_string(extra)
    +        self.styledict: str = {}
    +        if M21Utils.has_style(extra):
    +            self.styledict = M21Utils.obj_to_styledict(extra, detail) # includes extra.placement if present
    +        self._notation_size: int = 1 # so far, always 1, but maybe some extra will be bigger someday
    +
    +        # precomputed representations for faster comparison
    +        self.precomputed_str = self.__str__()
    +
    + +
    + +

    Extend music21 non-GeneralNote and non-Stream objects with some precomputed, easily compared information about it. +Examples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc.

    + +
    Args
    + +
      +
    • extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream object to extend.
    • +
    • measure (music21.stream.Measure): The music21 Measure the extra was found in. If the extra +was found in a Voice, this is the Measure that the Voice was found in.
    • +
    • detail (DetailLevel): What level of detail to use during the diff. Can be +GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is +currently equivalent to AllObjects).
    • +
    +
    + + +
    +
    +
    #   + + + def + notation_size(self): +
    + +
    + View Source +
        def notation_size(self):
    +        """
    +        Compute a measure of how many symbols are displayed in the score for this `AnnExtra`.
    +
    +        Returns:
    +            int: The notation size of the annotated extra
    +        """
    +        return self._notation_size
    +
    + +
    + +

    Compute a measure of how many symbols are displayed in the score for this AnnExtra.

    + +
    Returns
    + +
    +

    int: The notation size of the annotated extra

    +
    +
    + +
    @@ -918,33 +1329,36 @@
    Returns
    View Source
    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
    #   - AnnVoice(voice) + AnnVoice( + voice: music21.stream.base.Voice, + detail: musicdiff.m21utils.DetailLevel = <DetailLevel.AllObjects: 2> +)
    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
    #   - AnnScore(score) + AnnScore( + score: music21.stream.base.Score, + detail: musicdiff.m21utils.DetailLevel = <DetailLevel.AllObjects: 2> +)
    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\n
    Args
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Args
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Returns
    \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\n
    Args
    \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\n
    Args
    \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.