musicdiff.visualization
View Source
# ------------------------------------------------------------------------------ # Purpose: visualization is a diff visualization package for use by musicdiff. # musicdiff is a package for comparing music scores using music21. # # 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 # ------------------------------------------------------------------------------ __docformat__ = "google" from typing import List, Tuple, Union from pathlib import Path import sys import music21 as m21 from musicdiff.annotation import AnnMeasure, AnnVoice, AnnNote, AnnExtra class Visualization: # These can be set by the client to different colors INSERTED_COLOR = "red" """ `INSERTED_COLOR` can be set to customize the rendered score markup that `mark_diffs` does. """ DELETED_COLOR = "red" """ `DELETED_COLOR` can be set to customize the rendered score markup that `mark_diffs` does. """ CHANGED_COLOR = "red" """ `CHANGED_COLOR` can be set to customize the rendered score markup that `mark_diffs` does. """ @staticmethod def mark_diffs( score1: m21.stream.Score, score2: m21.stream.Score, operations: List[Tuple] ): """ Mark up two music21 scores with the differences described by an operations list (e.g. a list returned from `musicdiff.Comparison.annotated_scores_diff`). Args: score1 (music21.stream.Score): The first score to mark up score2 (music21.stream.Score): The second score to mark up operations (List[Tuple]): The operations list that describes the difference between the two scores """ for op in operations: # bar if op[0] == "insbar": assert isinstance(op[2], AnnMeasure) # color all the notes in the inserted score2 measure using Visualization.INSERTED_COLOR measure2 = score2.recurse().getElementById(op[2].measure) textExp = m21.expressions.TextExpression("inserted measure") textExp.style.color = Visualization.INSERTED_COLOR measure2.insert(0, textExp) measure2.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing for el in measure2.recurse().notesAndRests: el.style.color = Visualization.INSERTED_COLOR elif op[0] == "delbar": assert isinstance(op[1], AnnMeasure) # color all the notes in the deleted score1 measure using Visualization.DELETED_COLOR measure1 = score1.recurse().getElementById(op[1].measure) textExp = m21.expressions.TextExpression("deleted measure") textExp.style.color = Visualization.DELETED_COLOR measure1.insert(0, textExp) measure1.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing for el in measure1.recurse().notesAndRests: el.style.color = Visualization.DELETED_COLOR # voices elif op[0] == "voiceins": assert isinstance(op[2], AnnVoice) # color all the notes in the inserted score2 voice using Visualization.INSERTED_COLOR voice2 = score2.recurse().getElementById(op[2].voice) textExp = m21.expressions.TextExpression("inserted voice") textExp.style.color = Visualization.INSERTED_COLOR voice2.insert(0, textExp) voice2.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing for el in voice2.recurse().notesAndRests: el.style.color = Visualization.INSERTED_COLOR elif op[0] == "voicedel": assert isinstance(op[1], AnnVoice) # color all the notes in the deleted score1 voice using Visualization.DELETED_COLOR voice1 = score1.recurse().getElementById(op[1].voice) textExp = m21.expressions.TextExpression("deleted voice") textExp.style.color = Visualization.DELETED_COLOR voice1.insert(0, textExp) voice1.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing 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 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": assert isinstance(op[1], AnnNote) # 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 textExp = m21.expressions.TextExpression(f"deleted {note2.classes[0]}") textExp.style.color = Visualization.DELETED_COLOR note1.activeSite.insert(note1.offset, textExp) # pitch elif op[0] == "pitchnameedit": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the changed note (in both scores) using Visualization.CHANGED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed pitch") textExp.style.color = Visualization.CHANGED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed pitch") textExp.style.color = Visualization.CHANGED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "inspitch": assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the inserted note in score2 using Visualization.INSERTED_COLOR chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.INSERTED_COLOR if "Rest" in note2.classes: textExp = m21.expressions.TextExpression("inserted rest") else: textExp = m21.expressions.TextExpression("inserted note") textExp.style.color = Visualization.INSERTED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "delpitch": assert isinstance(op[1], AnnNote) assert len(op) == 5 # the indices must be there # color the deleted note in score1 using Visualization.DELETED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.DELETED_COLOR if "Rest" in note1.classes: textExp = m21.expressions.TextExpression("deleted rest") else: textExp = m21.expressions.TextExpression("deleted note") textExp.style.color = Visualization.DELETED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) elif op[0] == "headedit": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the changed note/rest/chord (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed note head") 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") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # beam elif op[0] == "insbeam": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.INSERTED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.INSERTED_COLOR if hasattr(note1, "beams"): for beam in note1.beams: beam.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("increased flags") textExp.style.color = Visualization.INSERTED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.INSERTED_COLOR if hasattr(note2, "beams"): for beam in note2.beams: beam.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("increased flags") textExp.style.color = Visualization.INSERTED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "delbeam": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.DELETED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.DELETED_COLOR if hasattr(note1, "beams"): for beam in note1.beams: beam.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("decreased flags") textExp.style.color = Visualization.CHANGED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.DELETED_COLOR if hasattr(note2, "beams"): for beam in note2.beams: beam.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("decreased flags") textExp.style.color = Visualization.DELETED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "editbeam": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the changed beam (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR if hasattr(note1, "beams"): for beam in note1.beams: beam.style.color = ( Visualization.CHANGED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("changed flags") 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 if hasattr(note2, "beams"): for beam in note2.beams: beam.style.color = ( Visualization.CHANGED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("changed flags") 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) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the modified note in both scores using Visualization.INSERTED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color only the indexed note's accidental in the chord idx = op[4][0] note1 = note1.notes[idx] if note1.pitch.accidental: note1.pitch.accidental.style.color = Visualization.INSERTED_COLOR note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted accidental") textExp.style.color = Visualization.INSERTED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color only the indexed note's accidental in the chord idx = op[4][1] note2 = note2.notes[idx] if note2.pitch.accidental: note2.pitch.accidental.style.color = Visualization.INSERTED_COLOR note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted accidental") textExp.style.color = Visualization.INSERTED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "accidentdel": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the modified note in both scores using Visualization.DELETED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color only the indexed note's accidental in the chord idx = op[4][0] note1 = note1.notes[idx] if note1.pitch.accidental: note1.pitch.accidental.style.color = Visualization.DELETED_COLOR note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted accidental") textExp.style.color = Visualization.DELETED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color only the indexed note's accidental in the chord idx = op[4][1] note2 = note2.notes[idx] if note2.pitch.accidental: note2.pitch.accidental.style.color = Visualization.DELETED_COLOR note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted accidental") textExp.style.color = Visualization.DELETED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "accidentedit": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the changed accidental (in both scores) using Visualization.CHANGED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] if note1.pitch.accidental: note1.pitch.accidental.style.color = Visualization.CHANGED_COLOR note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed accidental") textExp.style.color = Visualization.CHANGED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] if note2.pitch.accidental: note2.pitch.accidental.style.color = Visualization.CHANGED_COLOR note2.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed accidental") textExp.style.color = Visualization.CHANGED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "dotins": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # In music21, the dots are not separately colorable from the note, # so we will just color the modified note here in both scores, using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("inserted dot") 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("inserted dot") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "dotdel": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # In music21, the dots are not separately colorable from the note, # so we will just color the modified note here in both scores, using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("deleted dot") 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("deleted dot") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # tuplets elif op[0] == "instuplet": 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("inserted tuplet") 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("inserted tuplet") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "deltuplet": 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("deleted tuplet") 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("deleted tuplet") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "edittuplet": 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 tuplet") 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 tuplet") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # ties elif op[0] == "tieins": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # Color the modified note here in both scores, using Visualization.INSERTED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted tie") textExp.style.color = Visualization.INSERTED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted tie") textExp.style.color = Visualization.INSERTED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "tiedel": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # Color the modified note in both scores, using Visualization.DELETED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted tie") textExp.style.color = Visualization.DELETED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted tie") textExp.style.color = Visualization.DELETED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) # expressions elif op[0] == "insexpression": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the note in both scores using Visualization.INSERTED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted expression") textExp.style.color = Visualization.INSERTED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted expression") textExp.style.color = Visualization.INSERTED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "delexpression": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the deleted expression in score1 using Visualization.DELETED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted expression") textExp.style.color = Visualization.DELETED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted expression") textExp.style.color = Visualization.DELETED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "editexpression": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the changed beam (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed expression") 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 expression") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # articulations elif op[0] == "insarticulation": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.INSERTED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted articulation") textExp.style.color = Visualization.INSERTED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted articulation") textExp.style.color = Visualization.INSERTED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "delarticulation": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.DELETED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted articulation") textExp.style.color = Visualization.DELETED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted articulation") textExp.style.color = Visualization.DELETED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "editarticulation": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed articulation") 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 articulation") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) else: print(f"Annotation type {op[0]} not yet supported for visualization", file=sys.stderr) @staticmethod def show_diffs(score1: m21.stream.Score, score2: m21.stream.Score, out_path1: Union[str, Path] = None, out_path2: Union[str, Path] = None): """ Render two (presumably marked-up) music21 scores. If both out_path1 and out_path2 are not None, save the rendered PDFs at those two locations, otherwise just display them using the default PDF viewer on the system. Args: score1 (music21.stream.Score): The first score to render score2 (music21.stream.Score): The second score to render out_path1 (str, Path): Where to save the first marked-up rendered score PDF. If out_path1 is None, both PDFs will be displayed in the default PDF viewer. (default is None) out_path2 (str, Path): Where to save the second marked-up rendered score PDF. If out_path2 is None, both PDFs will be displayed in the default PDF viewer. (default is None) """ # display the two (presumably annotated) scores originalComposer1: str = None originalComposer2: str = None if score1.metadata is None: score1.metadata = m21.metadata.Metadata() if score2.metadata is None: score2.metadata = m21.metadata.Metadata() originalComposer1 = score1.metadata.composer if originalComposer1 is None: score1.metadata.composer = "score1" else: score1.metadata.composer = "score1 " + originalComposer1 originalComposer2 = score2.metadata.composer if originalComposer2 is None: score2.metadata.composer = "score2" else: score2.metadata.composer = "score2 " + originalComposer2 #save files if requested if (out_path1 is not None) and (out_path2 is not None): score1.write("musicxml.pdf", makeNotation=False, fp=out_path1) score2.write("musicxml.pdf", makeNotation=False, fp=out_path2) print(f"Annotated scores saved in {out_path1} and {out_path2}.", file=sys.stderr) else: # just display the scores score1.show("musicxml.pdf", makeNotation=False) score2.show("musicxml.pdf", makeNotation=False)
View Source
class Visualization: # These can be set by the client to different colors INSERTED_COLOR = "red" """ `INSERTED_COLOR` can be set to customize the rendered score markup that `mark_diffs` does. """ DELETED_COLOR = "red" """ `DELETED_COLOR` can be set to customize the rendered score markup that `mark_diffs` does. """ CHANGED_COLOR = "red" """ `CHANGED_COLOR` can be set to customize the rendered score markup that `mark_diffs` does. """ @staticmethod def mark_diffs( score1: m21.stream.Score, score2: m21.stream.Score, operations: List[Tuple] ): """ Mark up two music21 scores with the differences described by an operations list (e.g. a list returned from `musicdiff.Comparison.annotated_scores_diff`). Args: score1 (music21.stream.Score): The first score to mark up score2 (music21.stream.Score): The second score to mark up operations (List[Tuple]): The operations list that describes the difference between the two scores """ for op in operations: # bar if op[0] == "insbar": assert isinstance(op[2], AnnMeasure) # color all the notes in the inserted score2 measure using Visualization.INSERTED_COLOR measure2 = score2.recurse().getElementById(op[2].measure) textExp = m21.expressions.TextExpression("inserted measure") textExp.style.color = Visualization.INSERTED_COLOR measure2.insert(0, textExp) measure2.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing for el in measure2.recurse().notesAndRests: el.style.color = Visualization.INSERTED_COLOR elif op[0] == "delbar": assert isinstance(op[1], AnnMeasure) # color all the notes in the deleted score1 measure using Visualization.DELETED_COLOR measure1 = score1.recurse().getElementById(op[1].measure) textExp = m21.expressions.TextExpression("deleted measure") textExp.style.color = Visualization.DELETED_COLOR measure1.insert(0, textExp) measure1.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing for el in measure1.recurse().notesAndRests: el.style.color = Visualization.DELETED_COLOR # voices elif op[0] == "voiceins": assert isinstance(op[2], AnnVoice) # color all the notes in the inserted score2 voice using Visualization.INSERTED_COLOR voice2 = score2.recurse().getElementById(op[2].voice) textExp = m21.expressions.TextExpression("inserted voice") textExp.style.color = Visualization.INSERTED_COLOR voice2.insert(0, textExp) voice2.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing for el in voice2.recurse().notesAndRests: el.style.color = Visualization.INSERTED_COLOR elif op[0] == "voicedel": assert isinstance(op[1], AnnVoice) # color all the notes in the deleted score1 voice using Visualization.DELETED_COLOR voice1 = score1.recurse().getElementById(op[1].voice) textExp = m21.expressions.TextExpression("deleted voice") textExp.style.color = Visualization.DELETED_COLOR voice1.insert(0, textExp) voice1.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing 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 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": assert isinstance(op[1], AnnNote) # 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 textExp = m21.expressions.TextExpression(f"deleted {note2.classes[0]}") textExp.style.color = Visualization.DELETED_COLOR note1.activeSite.insert(note1.offset, textExp) # pitch elif op[0] == "pitchnameedit": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the changed note (in both scores) using Visualization.CHANGED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed pitch") textExp.style.color = Visualization.CHANGED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed pitch") textExp.style.color = Visualization.CHANGED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "inspitch": assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the inserted note in score2 using Visualization.INSERTED_COLOR chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.INSERTED_COLOR if "Rest" in note2.classes: textExp = m21.expressions.TextExpression("inserted rest") else: textExp = m21.expressions.TextExpression("inserted note") textExp.style.color = Visualization.INSERTED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "delpitch": assert isinstance(op[1], AnnNote) assert len(op) == 5 # the indices must be there # color the deleted note in score1 using Visualization.DELETED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.DELETED_COLOR if "Rest" in note1.classes: textExp = m21.expressions.TextExpression("deleted rest") else: textExp = m21.expressions.TextExpression("deleted note") textExp.style.color = Visualization.DELETED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) elif op[0] == "headedit": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the changed note/rest/chord (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed note head") 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") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # beam elif op[0] == "insbeam": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.INSERTED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.INSERTED_COLOR if hasattr(note1, "beams"): for beam in note1.beams: beam.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("increased flags") textExp.style.color = Visualization.INSERTED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.INSERTED_COLOR if hasattr(note2, "beams"): for beam in note2.beams: beam.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("increased flags") textExp.style.color = Visualization.INSERTED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "delbeam": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.DELETED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.DELETED_COLOR if hasattr(note1, "beams"): for beam in note1.beams: beam.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("decreased flags") textExp.style.color = Visualization.CHANGED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.DELETED_COLOR if hasattr(note2, "beams"): for beam in note2.beams: beam.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("decreased flags") textExp.style.color = Visualization.DELETED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "editbeam": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the changed beam (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR if hasattr(note1, "beams"): for beam in note1.beams: beam.style.color = ( Visualization.CHANGED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("changed flags") 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 if hasattr(note2, "beams"): for beam in note2.beams: beam.style.color = ( Visualization.CHANGED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("changed flags") 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) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the modified note in both scores using Visualization.INSERTED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color only the indexed note's accidental in the chord idx = op[4][0] note1 = note1.notes[idx] if note1.pitch.accidental: note1.pitch.accidental.style.color = Visualization.INSERTED_COLOR note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted accidental") textExp.style.color = Visualization.INSERTED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color only the indexed note's accidental in the chord idx = op[4][1] note2 = note2.notes[idx] if note2.pitch.accidental: note2.pitch.accidental.style.color = Visualization.INSERTED_COLOR note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted accidental") textExp.style.color = Visualization.INSERTED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "accidentdel": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the modified note in both scores using Visualization.DELETED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color only the indexed note's accidental in the chord idx = op[4][0] note1 = note1.notes[idx] if note1.pitch.accidental: note1.pitch.accidental.style.color = Visualization.DELETED_COLOR note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted accidental") textExp.style.color = Visualization.DELETED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color only the indexed note's accidental in the chord idx = op[4][1] note2 = note2.notes[idx] if note2.pitch.accidental: note2.pitch.accidental.style.color = Visualization.DELETED_COLOR note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted accidental") textExp.style.color = Visualization.DELETED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "accidentedit": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the changed accidental (in both scores) using Visualization.CHANGED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] if note1.pitch.accidental: note1.pitch.accidental.style.color = Visualization.CHANGED_COLOR note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed accidental") textExp.style.color = Visualization.CHANGED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] if note2.pitch.accidental: note2.pitch.accidental.style.color = Visualization.CHANGED_COLOR note2.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed accidental") textExp.style.color = Visualization.CHANGED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "dotins": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # In music21, the dots are not separately colorable from the note, # so we will just color the modified note here in both scores, using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("inserted dot") 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("inserted dot") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "dotdel": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # In music21, the dots are not separately colorable from the note, # so we will just color the modified note here in both scores, using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("deleted dot") 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("deleted dot") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # tuplets elif op[0] == "instuplet": 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("inserted tuplet") 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("inserted tuplet") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "deltuplet": 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("deleted tuplet") 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("deleted tuplet") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "edittuplet": 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 tuplet") 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 tuplet") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # ties elif op[0] == "tieins": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # Color the modified note here in both scores, using Visualization.INSERTED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted tie") textExp.style.color = Visualization.INSERTED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted tie") textExp.style.color = Visualization.INSERTED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "tiedel": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # Color the modified note in both scores, using Visualization.DELETED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted tie") textExp.style.color = Visualization.DELETED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted tie") textExp.style.color = Visualization.DELETED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) # expressions elif op[0] == "insexpression": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the note in both scores using Visualization.INSERTED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted expression") textExp.style.color = Visualization.INSERTED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted expression") textExp.style.color = Visualization.INSERTED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "delexpression": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the deleted expression in score1 using Visualization.DELETED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted expression") textExp.style.color = Visualization.DELETED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted expression") textExp.style.color = Visualization.DELETED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "editexpression": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the changed beam (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed expression") 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 expression") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # articulations elif op[0] == "insarticulation": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.INSERTED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted articulation") textExp.style.color = Visualization.INSERTED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted articulation") textExp.style.color = Visualization.INSERTED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "delarticulation": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.DELETED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted articulation") textExp.style.color = Visualization.DELETED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted articulation") textExp.style.color = Visualization.DELETED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "editarticulation": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed articulation") 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 articulation") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) else: print(f"Annotation type {op[0]} not yet supported for visualization", file=sys.stderr) @staticmethod def show_diffs(score1: m21.stream.Score, score2: m21.stream.Score, out_path1: Union[str, Path] = None, out_path2: Union[str, Path] = None): """ Render two (presumably marked-up) music21 scores. If both out_path1 and out_path2 are not None, save the rendered PDFs at those two locations, otherwise just display them using the default PDF viewer on the system. Args: score1 (music21.stream.Score): The first score to render score2 (music21.stream.Score): The second score to render out_path1 (str, Path): Where to save the first marked-up rendered score PDF. If out_path1 is None, both PDFs will be displayed in the default PDF viewer. (default is None) out_path2 (str, Path): Where to save the second marked-up rendered score PDF. If out_path2 is None, both PDFs will be displayed in the default PDF viewer. (default is None) """ # display the two (presumably annotated) scores originalComposer1: str = None originalComposer2: str = None if score1.metadata is None: score1.metadata = m21.metadata.Metadata() if score2.metadata is None: score2.metadata = m21.metadata.Metadata() originalComposer1 = score1.metadata.composer if originalComposer1 is None: score1.metadata.composer = "score1" else: score1.metadata.composer = "score1 " + originalComposer1 originalComposer2 = score2.metadata.composer if originalComposer2 is None: score2.metadata.composer = "score2" else: score2.metadata.composer = "score2 " + originalComposer2 #save files if requested if (out_path1 is not None) and (out_path2 is not None): score1.write("musicxml.pdf", makeNotation=False, fp=out_path1) score2.write("musicxml.pdf", makeNotation=False, fp=out_path2) print(f"Annotated scores saved in {out_path1} and {out_path2}.", file=sys.stderr) else: # just display the scores score1.show("musicxml.pdf", makeNotation=False) score2.show("musicxml.pdf", makeNotation=False)
INSERTED_COLOR
can be set to customize the rendered score markup that mark_diffs
does.
DELETED_COLOR
can be set to customize the rendered score markup that mark_diffs
does.
CHANGED_COLOR
can be set to customize the rendered score markup that mark_diffs
does.
#  
@staticmethod
def
mark_diffs(
score1: music21.stream.base.Score,
score2: music21.stream.base.Score,
operations: List[Tuple]
):
View Source
@staticmethod def mark_diffs( score1: m21.stream.Score, score2: m21.stream.Score, operations: List[Tuple] ): """ Mark up two music21 scores with the differences described by an operations list (e.g. a list returned from `musicdiff.Comparison.annotated_scores_diff`). Args: score1 (music21.stream.Score): The first score to mark up score2 (music21.stream.Score): The second score to mark up operations (List[Tuple]): The operations list that describes the difference between the two scores """ for op in operations: # bar if op[0] == "insbar": assert isinstance(op[2], AnnMeasure) # color all the notes in the inserted score2 measure using Visualization.INSERTED_COLOR measure2 = score2.recurse().getElementById(op[2].measure) textExp = m21.expressions.TextExpression("inserted measure") textExp.style.color = Visualization.INSERTED_COLOR measure2.insert(0, textExp) measure2.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing for el in measure2.recurse().notesAndRests: el.style.color = Visualization.INSERTED_COLOR elif op[0] == "delbar": assert isinstance(op[1], AnnMeasure) # color all the notes in the deleted score1 measure using Visualization.DELETED_COLOR measure1 = score1.recurse().getElementById(op[1].measure) textExp = m21.expressions.TextExpression("deleted measure") textExp.style.color = Visualization.DELETED_COLOR measure1.insert(0, textExp) measure1.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing for el in measure1.recurse().notesAndRests: el.style.color = Visualization.DELETED_COLOR # voices elif op[0] == "voiceins": assert isinstance(op[2], AnnVoice) # color all the notes in the inserted score2 voice using Visualization.INSERTED_COLOR voice2 = score2.recurse().getElementById(op[2].voice) textExp = m21.expressions.TextExpression("inserted voice") textExp.style.color = Visualization.INSERTED_COLOR voice2.insert(0, textExp) voice2.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing for el in voice2.recurse().notesAndRests: el.style.color = Visualization.INSERTED_COLOR elif op[0] == "voicedel": assert isinstance(op[1], AnnVoice) # color all the notes in the deleted score1 voice using Visualization.DELETED_COLOR voice1 = score1.recurse().getElementById(op[1].voice) textExp = m21.expressions.TextExpression("deleted voice") textExp.style.color = Visualization.DELETED_COLOR voice1.insert(0, textExp) voice1.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing 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 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": assert isinstance(op[1], AnnNote) # 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 textExp = m21.expressions.TextExpression(f"deleted {note2.classes[0]}") textExp.style.color = Visualization.DELETED_COLOR note1.activeSite.insert(note1.offset, textExp) # pitch elif op[0] == "pitchnameedit": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the changed note (in both scores) using Visualization.CHANGED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed pitch") textExp.style.color = Visualization.CHANGED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed pitch") textExp.style.color = Visualization.CHANGED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "inspitch": assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the inserted note in score2 using Visualization.INSERTED_COLOR chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.INSERTED_COLOR if "Rest" in note2.classes: textExp = m21.expressions.TextExpression("inserted rest") else: textExp = m21.expressions.TextExpression("inserted note") textExp.style.color = Visualization.INSERTED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "delpitch": assert isinstance(op[1], AnnNote) assert len(op) == 5 # the indices must be there # color the deleted note in score1 using Visualization.DELETED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.DELETED_COLOR if "Rest" in note1.classes: textExp = m21.expressions.TextExpression("deleted rest") else: textExp = m21.expressions.TextExpression("deleted note") textExp.style.color = Visualization.DELETED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) elif op[0] == "headedit": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the changed note/rest/chord (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed note head") 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") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # beam elif op[0] == "insbeam": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.INSERTED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.INSERTED_COLOR if hasattr(note1, "beams"): for beam in note1.beams: beam.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("increased flags") textExp.style.color = Visualization.INSERTED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.INSERTED_COLOR if hasattr(note2, "beams"): for beam in note2.beams: beam.style.color = ( Visualization.INSERTED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("increased flags") textExp.style.color = Visualization.INSERTED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "delbeam": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.DELETED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.DELETED_COLOR if hasattr(note1, "beams"): for beam in note1.beams: beam.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("decreased flags") textExp.style.color = Visualization.CHANGED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.DELETED_COLOR if hasattr(note2, "beams"): for beam in note2.beams: beam.style.color = ( Visualization.DELETED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("decreased flags") textExp.style.color = Visualization.DELETED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "editbeam": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the changed beam (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR if hasattr(note1, "beams"): for beam in note1.beams: beam.style.color = ( Visualization.CHANGED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("changed flags") 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 if hasattr(note2, "beams"): for beam in note2.beams: beam.style.color = ( Visualization.CHANGED_COLOR ) # this apparently does nothing textExp = m21.expressions.TextExpression("changed flags") 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) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the modified note in both scores using Visualization.INSERTED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color only the indexed note's accidental in the chord idx = op[4][0] note1 = note1.notes[idx] if note1.pitch.accidental: note1.pitch.accidental.style.color = Visualization.INSERTED_COLOR note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted accidental") textExp.style.color = Visualization.INSERTED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color only the indexed note's accidental in the chord idx = op[4][1] note2 = note2.notes[idx] if note2.pitch.accidental: note2.pitch.accidental.style.color = Visualization.INSERTED_COLOR note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted accidental") textExp.style.color = Visualization.INSERTED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "accidentdel": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the modified note in both scores using Visualization.DELETED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color only the indexed note's accidental in the chord idx = op[4][0] note1 = note1.notes[idx] if note1.pitch.accidental: note1.pitch.accidental.style.color = Visualization.DELETED_COLOR note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted accidental") textExp.style.color = Visualization.DELETED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color only the indexed note's accidental in the chord idx = op[4][1] note2 = note2.notes[idx] if note2.pitch.accidental: note2.pitch.accidental.style.color = Visualization.DELETED_COLOR note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted accidental") textExp.style.color = Visualization.DELETED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "accidentedit": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # color the changed accidental (in both scores) using Visualization.CHANGED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] if note1.pitch.accidental: note1.pitch.accidental.style.color = Visualization.CHANGED_COLOR note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed accidental") textExp.style.color = Visualization.CHANGED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] if note2.pitch.accidental: note2.pitch.accidental.style.color = Visualization.CHANGED_COLOR note2.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed accidental") textExp.style.color = Visualization.CHANGED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "dotins": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # In music21, the dots are not separately colorable from the note, # so we will just color the modified note here in both scores, using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("inserted dot") 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("inserted dot") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "dotdel": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # In music21, the dots are not separately colorable from the note, # so we will just color the modified note here in both scores, using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("deleted dot") 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("deleted dot") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # tuplets elif op[0] == "instuplet": 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("inserted tuplet") 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("inserted tuplet") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "deltuplet": 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("deleted tuplet") 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("deleted tuplet") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "edittuplet": 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 tuplet") 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 tuplet") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # ties elif op[0] == "tieins": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # Color the modified note here in both scores, using Visualization.INSERTED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted tie") textExp.style.color = Visualization.INSERTED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted tie") textExp.style.color = Visualization.INSERTED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) elif op[0] == "tiedel": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) assert len(op) == 5 # the indices must be there # Color the modified note in both scores, using Visualization.DELETED_COLOR chord1 = score1.recurse().getElementById(op[1].general_note) note1 = chord1 if "Chord" in note1.classes: # color just the indexed note in the chord idx = op[4][0] note1 = note1.notes[idx] note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted tie") textExp.style.color = Visualization.DELETED_COLOR if note1.activeSite is not None: note1.activeSite.insert(note1.offset, textExp) else: chord1.activeSite.insert(chord1.offset, textExp) chord2 = score2.recurse().getElementById(op[2].general_note) note2 = chord2 if "Chord" in note2.classes: # color just the indexed note in the chord idx = op[4][1] note2 = note2.notes[idx] note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted tie") textExp.style.color = Visualization.DELETED_COLOR if note2.activeSite is not None: note2.activeSite.insert(note2.offset, textExp) else: chord2.activeSite.insert(chord2.offset, textExp) # expressions elif op[0] == "insexpression": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the note in both scores using Visualization.INSERTED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted expression") textExp.style.color = Visualization.INSERTED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted expression") textExp.style.color = Visualization.INSERTED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "delexpression": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the deleted expression in score1 using Visualization.DELETED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted expression") textExp.style.color = Visualization.DELETED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted expression") textExp.style.color = Visualization.DELETED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "editexpression": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the changed beam (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed expression") 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 expression") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) # articulations elif op[0] == "insarticulation": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.INSERTED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted articulation") textExp.style.color = Visualization.INSERTED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.INSERTED_COLOR textExp = m21.expressions.TextExpression("inserted articulation") textExp.style.color = Visualization.INSERTED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "delarticulation": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note in both scores using Visualization.DELETED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted articulation") textExp.style.color = Visualization.DELETED_COLOR note1.activeSite.insert(note1.offset, textExp) note2 = score2.recurse().getElementById(op[2].general_note) note2.style.color = Visualization.DELETED_COLOR textExp = m21.expressions.TextExpression("deleted articulation") textExp.style.color = Visualization.DELETED_COLOR note2.activeSite.insert(note2.offset, textExp) elif op[0] == "editarticulation": assert isinstance(op[1], AnnNote) assert isinstance(op[2], AnnNote) # color the modified note (in both scores) using Visualization.CHANGED_COLOR note1 = score1.recurse().getElementById(op[1].general_note) note1.style.color = Visualization.CHANGED_COLOR textExp = m21.expressions.TextExpression("changed articulation") 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 articulation") textExp.style.color = Visualization.CHANGED_COLOR note2.activeSite.insert(note2.offset, textExp) else: print(f"Annotation type {op[0]} not yet supported for visualization", file=sys.stderr)
Mark up two music21 scores with the differences described by an operations
list (e.g. a list returned from musicdiff.Comparison.annotated_scores_diff
).
Args
- score1 (music21.stream.Score): The first score to mark up
- score2 (music21.stream.Score): The second score to mark up
- operations (List[Tuple]): The operations list that describes the difference between the two scores
#  
@staticmethod
def
show_diffs(
score1: music21.stream.base.Score,
score2: music21.stream.base.Score,
out_path1: Union[str, pathlib.Path] = None,
out_path2: Union[str, pathlib.Path] = None
):
View Source
@staticmethod def show_diffs(score1: m21.stream.Score, score2: m21.stream.Score, out_path1: Union[str, Path] = None, out_path2: Union[str, Path] = None): """ Render two (presumably marked-up) music21 scores. If both out_path1 and out_path2 are not None, save the rendered PDFs at those two locations, otherwise just display them using the default PDF viewer on the system. Args: score1 (music21.stream.Score): The first score to render score2 (music21.stream.Score): The second score to render out_path1 (str, Path): Where to save the first marked-up rendered score PDF. If out_path1 is None, both PDFs will be displayed in the default PDF viewer. (default is None) out_path2 (str, Path): Where to save the second marked-up rendered score PDF. If out_path2 is None, both PDFs will be displayed in the default PDF viewer. (default is None) """ # display the two (presumably annotated) scores originalComposer1: str = None originalComposer2: str = None if score1.metadata is None: score1.metadata = m21.metadata.Metadata() if score2.metadata is None: score2.metadata = m21.metadata.Metadata() originalComposer1 = score1.metadata.composer if originalComposer1 is None: score1.metadata.composer = "score1" else: score1.metadata.composer = "score1 " + originalComposer1 originalComposer2 = score2.metadata.composer if originalComposer2 is None: score2.metadata.composer = "score2" else: score2.metadata.composer = "score2 " + originalComposer2 #save files if requested if (out_path1 is not None) and (out_path2 is not None): score1.write("musicxml.pdf", makeNotation=False, fp=out_path1) score2.write("musicxml.pdf", makeNotation=False, fp=out_path2) print(f"Annotated scores saved in {out_path1} and {out_path2}.", file=sys.stderr) else: # just display the scores score1.show("musicxml.pdf", makeNotation=False) score2.show("musicxml.pdf", makeNotation=False)
Render two (presumably marked-up) music21 scores. If both out_path1 and out_path2 are not None, save the rendered PDFs at those two locations, otherwise just display them using the default PDF viewer on the system.
Args
- score1 (music21.stream.Score): The first score to render
- score2 (music21.stream.Score): The second score to render
- out_path1 (str, Path): Where to save the first marked-up rendered score PDF. If out_path1 is None, both PDFs will be displayed in the default PDF viewer. (default is None)
- out_path2 (str, Path): Where to save the second marked-up rendered score PDF. If out_path2 is None, both PDFs will be displayed in the default PDF viewer. (default is None)