musicdiff.annotation

   1# ------------------------------------------------------------------------------
   2# Purpose:       notation is a set of annotated music21 notation wrappers for use
   3#                by musicdiff.
   4#                musicdiff is a package for comparing music scores using music21.
   5#
   6# Authors:       Greg Chapman <gregc@mac.com>
   7#                musicdiff is derived from:
   8#                   https://github.com/fosfrancesco/music-score-diff.git
   9#                   by Francesco Foscarin <foscarin.francesco@gmail.com>
  10#
  11# Copyright:     (c) 2022-2025 Francesco Foscarin, Greg Chapman
  12# License:       MIT, see LICENSE
  13# ------------------------------------------------------------------------------
  14
  15__docformat__ = "google"
  16
  17import html
  18from fractions import Fraction
  19import typing as t
  20
  21import music21 as m21
  22from music21.common import OffsetQL, opFrac
  23
  24from musicdiff import M21Utils
  25from musicdiff import DetailLevel
  26
  27class AnnNote:
  28    def __init__(
  29        self,
  30        general_note: m21.note.GeneralNote,
  31        gap_dur: OffsetQL,
  32        enhanced_beam_list: list[str],
  33        tuplet_list: list[str],
  34        tuplet_info: list[str],
  35        parent_chord: m21.chord.ChordBase | None = None,
  36        chord_offset: OffsetQL | None = None,  # only set if this note is inside a chord
  37        detail: DetailLevel | int = DetailLevel.Default,
  38    ) -> None:
  39        """
  40        Extend music21 GeneralNote with some precomputed, easily compared information about it.
  41
  42        Args:
  43            general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
  44            gap_dur (OffsetQL): gap since end of last note (or since start of measure, if
  45                first note in measure).  Usually zero.
  46            enhanced_beam_list (list): A list of beaming information about this GeneralNote.
  47            tuplet_list (list): A list of basic tuplet info about this GeneralNote.
  48            tuplet_info (list): A list of detailed tuplet info about this GeneralNote.
  49            detail (DetailLevel | int): What level of detail to use during the diff.
  50                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
  51                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
  52                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
  53                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
  54                Style, Metadata, or Voicing.
  55        """
  56        self.general_note: int | str = general_note.id
  57        self.is_in_chord: bool = False
  58        self.note_idx_in_chord: int | None = None
  59        if parent_chord is not None:
  60            # This is what visualization uses to color the note red (chord id and note idx)
  61            self.general_note = parent_chord.id
  62            self.is_in_chord = True
  63            self.note_idx_in_chord = parent_chord.notes.index(general_note)
  64
  65        # A lot of stuff is carried by the parent_chord (if present) or the
  66        # general_note (if parent_chord not present); we call that the carrier
  67        carrier: m21.note.GeneralNote = parent_chord or general_note
  68
  69        self.gap_dur: OffsetQL = gap_dur
  70        self.beamings: list[str] = enhanced_beam_list
  71        self.tuplets: list[str] = tuplet_list
  72        self.tuplet_info: list[str] = tuplet_info
  73
  74        self.note_offset: OffsetQL = 0.
  75        self.note_dur_type: str = ''
  76        self.note_dur_dots: int = 0
  77        self.note_is_grace: bool = False
  78
  79        # fullNameSuffix is only for text output, it is not involved in comparison at all.
  80        # It is of the form "Dotted Quarter Rest", etc.
  81        self.fullNameSuffix: str = general_note.duration.fullName
  82        if isinstance(general_note, m21.note.Rest):
  83            self.fullNameSuffix += " Rest"
  84        elif isinstance(general_note, m21.chord.ChordBase):
  85            if parent_chord is None:
  86                self.fullNameSuffix += " Chord"
  87            else:
  88                # we're actually annotating one of the notes in the chord
  89                self.fullNameSuffix += " Note"
  90        elif isinstance(general_note, (m21.note.Note, m21.note.Unpitched)):
  91            self.fullNameSuffix += " Note"
  92        else:
  93            self.fullNameSuffix += " Note"
  94        self.fullNameSuffix = self.fullNameSuffix.lower()
  95
  96        if not DetailLevel.includesVoicing(detail):
  97            # if we're comparing the individual notes, we need to make a note of
  98            # offset and visual duration to be used later when searching for matching
  99            # notes in the measures being compared.
 100
 101            # offset
 102            if chord_offset is None:
 103                self.note_offset = general_note.offset
 104            else:
 105                self.note_offset = chord_offset
 106
 107            # visual duration and graceness
 108            self.note_dur_type = carrier.duration.type
 109            self.note_dur_dots = carrier.duration.dots
 110            self.note_is_grace = carrier.duration.isGrace
 111
 112        self.styledict: dict = {}
 113
 114        if DetailLevel.includesStyle(detail):
 115            # we will take style from the individual note, and then override with
 116            # style from the chord (following music21's MusicXML exporter).
 117            if M21Utils.has_style(general_note):
 118                self.styledict = M21Utils.obj_to_styledict(general_note, detail)
 119
 120            if parent_chord is not None:
 121                if M21Utils.has_style(parent_chord):
 122                    parentstyledict = M21Utils.obj_to_styledict(parent_chord, detail)
 123                    for k, v in parentstyledict.items():
 124                        self.styledict[k] = v
 125
 126        self.noteshape: str = 'normal'
 127        self.noteheadFill: bool | None = None
 128        self.noteheadParenthesis: bool = False
 129        self.stemDirection: str = 'unspecified'
 130        if DetailLevel.includesStyle(detail) and isinstance(general_note, m21.note.NotRest):
 131            # stemDirection is different.  It might be on the parent chord, or
 132            # it might be on one of the notes in the parent chord (and applies
 133            # to all the notes in the chord, of course).
 134            if parent_chord is None:
 135                self.stemDirection = general_note.stemDirection
 136            else:
 137                if parent_chord.stemDirection != 'unspecified':
 138                    self.stemDirection = parent_chord.stemDirection
 139                else:
 140                    for n in parent_chord.notes:
 141                        if n.stemDirection != 'unspecified':
 142                            self.stemDirection = n.stemDirection
 143                            break
 144
 145            if parent_chord is None:
 146                self.noteshape = general_note.notehead
 147                self.noteheadFill = general_note.noteheadFill
 148                self.noteheadParenthesis = general_note.noteheadParenthesis
 149            else:
 150                # try general_note first, but if nothing about note head is specified,
 151                # go with whatever parent_chord says.
 152                if (general_note.notehead != 'normal'
 153                        or general_note.noteheadParenthesis
 154                        or general_note.noteheadFill is not None):
 155                    self.noteheadParenthesis = general_note.noteheadParenthesis
 156                    self.noteshape = general_note.notehead
 157                    self.noteheadFill = general_note.noteheadFill
 158                else:
 159                    self.noteshape = parent_chord.notehead
 160                    self.noteheadFill = parent_chord.noteheadFill
 161                    self.noteheadParenthesis = parent_chord.noteheadParenthesis
 162
 163        # compute the representation of NoteNode as in the paper
 164        # pitches is a list  of elements, each one is (pitchposition, accidental, tied)
 165        self.pitches: list[tuple[str, str, bool]]
 166        if isinstance(general_note, m21.chord.ChordBase):
 167            notes: tuple[m21.note.NotRest, ...] = general_note.notes
 168            if hasattr(general_note, "sortDiatonicAscending"):
 169                # PercussionChords don't have this, Chords do
 170                notes = general_note.sortDiatonicAscending().notes
 171            self.pitches = []
 172            for p in notes:
 173                if not isinstance(p, (m21.note.Note, m21.note.Unpitched)):
 174                    raise TypeError("The chord must contain only Note or Unpitched")
 175                self.pitches.append(M21Utils.note2tuple(p, detail))
 176
 177        elif isinstance(general_note, (m21.note.Note, m21.note.Unpitched, m21.note.Rest)):
 178            self.pitches = [M21Utils.note2tuple(general_note, detail)]
 179        else:
 180            raise TypeError("The generalNote must be a Chord, a Rest, a Note, or an Unpitched")
 181
 182        dur: m21.duration.Duration = carrier.duration
 183        # note head
 184        type_number = Fraction(
 185            M21Utils.get_type_num(dur)
 186        )
 187        self.note_head: int | Fraction
 188        if type_number >= 4:
 189            self.note_head = 4
 190        else:
 191            self.note_head = type_number
 192        # dots
 193        self.dots: int = dur.dots
 194        # graceness
 195        self.graceType: str = ''
 196        self.graceSlash: bool | None = False
 197        if isinstance(dur, m21.duration.AppoggiaturaDuration):
 198            self.graceType = 'acc'
 199            self.graceSlash = dur.slash
 200        elif isinstance(dur, m21.duration.GraceDuration):
 201            # might be accented or unaccented.  duration.slash isn't always reliable
 202            # (historically), but we can use it as a fallback.
 203            # Check duration.stealTimePrevious and duration.stealTimeFollowing first.
 204            if dur.stealTimePrevious is not None:
 205                self.graceType = 'unacc'
 206            elif dur.stealTimeFollowing is not None:
 207                self.graceType = 'acc'
 208            elif dur.slash is True:
 209                self.graceType = 'unacc'
 210            elif dur.slash is False:
 211                self.graceType = 'acc'
 212            else:
 213                # by default, GraceDuration with no other indications (slash is None)
 214                # is assumed to be unaccented.
 215                self.graceType = 'unacc'
 216            self.graceSlash = dur.slash
 217
 218        # The following (articulations, expressions) only occur once per chord
 219        # or standalone note, so we only want to annotate them once.  We annotate them
 220        # on standalone notes (of course), and on the first note of a parent_chord.
 221        self.articulations: list[str] = []
 222        self.expressions: list[str] = []
 223
 224        if self.note_idx_in_chord is None or self.note_idx_in_chord == 0:
 225            # articulations
 226            if DetailLevel.includesArticulations(detail):
 227                self.articulations = [
 228                    M21Utils.articulation_to_string(a, detail) for a in carrier.articulations
 229                ]
 230                if self.articulations:
 231                    self.articulations.sort()
 232
 233            if DetailLevel.includesOrnaments(detail):
 234                # expressions (tremolo, arpeggio, textexp have their own detail bits, though)
 235                for a in carrier.expressions:
 236                    if not DetailLevel.includesTremolos(detail):
 237                        if isinstance(a, m21.expressions.Tremolo):
 238                            continue
 239                    if not DetailLevel.includesArpeggios(detail):
 240                        if isinstance(a, m21.expressions.ArpeggioMark):
 241                            continue
 242                    if not DetailLevel.includesDirections(detail):
 243                        if isinstance(a, m21.expressions.TextExpression):
 244                            continue
 245                    self.expressions.append(
 246                        M21Utils.expression_to_string(a, detail)
 247                    )
 248                if self.expressions:
 249                    self.expressions.sort()
 250
 251        # precomputed/cached representations for faster comparison
 252        self.precomputed_str: str = self.__str__()
 253        self._cached_notation_size: int | None = None
 254
 255    def notation_size(self) -> int:
 256        """
 257        Compute a measure of how many symbols are displayed in the score for this `AnnNote`.
 258
 259        Returns:
 260            int: The notation size of the annotated note
 261        """
 262        if self._cached_notation_size is None:
 263            size: int = 0
 264            # add for the pitches
 265            for pitch in self.pitches:
 266                size += M21Utils.pitch_size(pitch)
 267            # add for the notehead (quarter, half, semibreve, breve, etc)
 268            size += 1
 269            # add for the dots
 270            size += self.dots * len(self.pitches)  # one dot for each note if it's a chord
 271            # add for the beams/flags
 272            size += len(self.beamings)
 273            # add for the tuplets
 274            size += len(self.tuplets)
 275            size += len(self.tuplet_info)
 276            # add for the articulations
 277            size += len(self.articulations)
 278            # add for the expressions
 279            size += len(self.expressions)
 280            # add 1 if it's a gracenote, and 1 more if there's a grace slash
 281            if self.graceType:
 282                size += 1
 283                if self.graceSlash is True:
 284                    size += 1
 285            # add 1 for abnormal note shape (diamond, etc)
 286            if self.noteshape != 'normal':
 287                size += 1
 288            # add 1 for abnormal note fill
 289            if self.noteheadFill is not None:
 290                size += 1
 291            # add 1 if there's a parenthesis around the note
 292            if self.noteheadParenthesis:
 293                size += 1
 294            # add 1 if stem direction is specified
 295            if self.stemDirection != 'unspecified':
 296                size += 1
 297            # add 1 if there is an empty space before this note
 298            if self.gap_dur != 0:
 299                size += 1
 300            # add 1 for any other style info (in future might count the style entries)
 301            if self.styledict:
 302                size += 1
 303
 304            self._cached_notation_size = size
 305
 306        return self._cached_notation_size
 307
 308    def get_identifying_string(self, name: str = "") -> str:
 309        string: str = ""
 310        if self.fullNameSuffix.endswith("rest"):
 311            string = self.fullNameSuffix
 312        elif self.fullNameSuffix.endswith("note"):
 313            string = self.pitches[0][0]
 314            if self.pitches[0][1] != "None":
 315                string += " " + self.pitches[0][1]
 316            string += " (" + self.fullNameSuffix + ")"
 317        elif self.fullNameSuffix.endswith("chord"):
 318            string = "["
 319            for p in self.pitches:  # add for pitches
 320                string += p[0]  # pitch name and octave
 321                if p[1] != "None":
 322                    string += " " + p[1]  # pitch accidental
 323                string += ","
 324            string = string[:-1]  # delete the last comma
 325            string += "] (" + self.fullNameSuffix + ")"
 326        return string
 327
 328    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
 329        string: str = self.get_identifying_string(name)
 330        if name == "pitch":
 331            # this is only for "pitch", not for "" (pitches are in identifying string)
 332            if self.fullNameSuffix.endswith("chord"):
 333                string += f", pitch[{idx}]={self.pitches[idx][0]}"
 334            return string
 335
 336        if name == "accid":
 337            # this is only for "accid" (indexed in a chord), not for "", or for "accid" on a note
 338            # (accidental is in identifying string)
 339            if self.fullNameSuffix.endswith("chord"):
 340                string += f", accid[{idx}]={self.pitches[idx][1]}"
 341            return string
 342
 343        if name == "head":
 344            # this is only for "head", not for "" (head is implied by identifying string)
 345            if self.note_head == 4:
 346                string += ", head=normal"
 347            else:
 348                string += f", head={m21.duration.typeFromNumDict[float(self.note_head)]}"
 349            if name:
 350                return string
 351
 352        if name == "dots":
 353            # this is only for "dots", not for "" (dots is in identifying string)
 354            string += f", dots={self.dots}"
 355            return string
 356
 357        if not name or name == "flagsbeams":
 358            numBeams: int = len(self.beamings)
 359            # Flags are implied by identifying string, so do not belong when name=="".
 360            # And "no beams" is boring for name=="".  Non-zero beams, though, we always
 361            # want to see.
 362            if numBeams == 0:
 363                if name:
 364                    string += ", no flags/beams"
 365                    return string
 366            elif all(b == "partial" for b in self.beamings):
 367                if name:
 368                    if numBeams == 1:
 369                        string += f", {numBeams} flag"
 370                    else:
 371                        string += f", {numBeams} flags"
 372                    return string
 373            else:
 374                # it's beams, not flags
 375                if numBeams == 1:
 376                    string += f", {numBeams} beam="
 377                else:
 378                    string += f", {numBeams} beams=["
 379                for i, b in enumerate(self.beamings):
 380                    if i > 0:
 381                        string += ", "
 382                    string += b
 383                if numBeams > 1:
 384                    string += "]"
 385                if name:
 386                    return string
 387
 388        if not name or name == "tuplet":
 389            if name or self.tuplets:
 390                string += ", tuplets=["
 391                for i, (tup, ti) in enumerate(zip(self.tuplets, self.tuplet_info)):
 392                    if i > 0:
 393                        string += ", "
 394                    if ti != "":
 395                        ti = "(" + ti + ")"
 396                    string += tup + ti
 397
 398                string += "]"
 399                if name:
 400                    return string
 401
 402        if not name or name == "tie":
 403            if self.pitches[idx][2]:
 404                string += ", tied"
 405            elif name:
 406                string += ", not tied"
 407            if name:
 408                return string
 409
 410
 411        if not name or name == "grace":
 412            if not name:
 413                if self.graceType:
 414                    string += f", grace={self.graceType}"
 415            else:
 416                string += f", grace={self.graceType}"
 417            if name:
 418                return string
 419
 420        if not name or name == "graceslash":
 421            if self.graceType:
 422                if self.graceSlash:
 423                    string += ", with grace slash"
 424                else:
 425                    string += ", with no grace slash"
 426            if name:
 427                return string
 428
 429        if not name or name == "noteshape":
 430            if not name:
 431                if self.noteshape != "normal":
 432                    string += f", noteshape={self.noteshape}"
 433            else:
 434                string += f", noteshape={self.noteshape}"
 435            if name:
 436                return string
 437
 438        if not name or name == "notefill":
 439            if not name:
 440                if self.noteheadFill is not None:
 441                    string += f", noteheadFill={self.noteheadFill}"
 442            else:
 443                string += f", noteheadFill={self.noteheadFill}"
 444            if name:
 445                return string
 446
 447        if not name or name == "noteparen":
 448            if not name:
 449                if self.noteheadParenthesis:
 450                    string += f", noteheadParenthesis={self.noteheadParenthesis}"
 451            else:
 452                string += f", noteheadParenthesis={self.noteheadParenthesis}"
 453            if name:
 454                return string
 455
 456        if not name or name == "stemdir":
 457            if not name:
 458                if self.stemDirection != "unspecified":
 459                    string += f", stemDirection={self.stemDirection}"
 460            else:
 461                string += f", stemDirection={self.stemDirection}"
 462            if name:
 463                return string
 464
 465        if not name or name == "spacebefore":
 466            if not name:
 467                if self.gap_dur != 0:
 468                    string += f", spacebefore={self.gap_dur}"
 469            else:
 470                string += f", spacebefore={self.gap_dur}"
 471            if name:
 472                return string
 473
 474        if not name or name == "artic":
 475            if name or self.articulations:
 476                string += ", articulations=["
 477                for i, artic in enumerate(self.articulations):
 478                    if i > 0:
 479                        string += ", "
 480                    string += artic
 481                string += "]"
 482            if name:
 483                return string
 484
 485        if not name or name == "expression":
 486            if name or self.expressions:
 487                string += ", expressions=["
 488                for i, exp in enumerate(self.expressions):
 489                    if i > 0:
 490                        string += ", "
 491                    string += exp
 492                string += "]"
 493            if name:
 494                return string
 495
 496        if not name or name == "style":
 497            if name or self.styledict:
 498                allOfThem: bool = False
 499                changedKeys: list[str] = []
 500                if changedStr:
 501                    changedKeys = changedStr.split(",")
 502                else:
 503                    changedKeys = [str(k) for k in self.styledict]
 504                    allOfThem = True
 505
 506                if allOfThem:
 507                    string += ", style={"
 508                else:
 509                    string += ", changedStyle={"
 510
 511                needsComma: bool = False
 512                for i, k in enumerate(changedKeys):
 513                    if k in self.styledict:
 514                        if needsComma:
 515                            string += ", "
 516                        string += f"{k}:{self.styledict[k]}"
 517                        needsComma = True
 518                string += "}"
 519            if name:
 520                return string
 521
 522        return string
 523
 524    def __repr__(self) -> str:
 525        # must include a unique id for memoization!
 526        # we use the music21 id of the general note.
 527        return (
 528            f"GeneralNote({self.general_note}),G:{self.gap_dur},"
 529            + f"P:{self.pitches},H:{self.note_head},D:{self.dots},"
 530            + f"B:{self.beamings},T:{self.tuplets},TI:{self.tuplet_info},"
 531            + f"A:{self.articulations},E:{self.expressions},"
 532            + f"S:{self.styledict}"
 533        )
 534
 535    def __str__(self) -> str:
 536        """
 537        Returns:
 538            str: the representation of the Annotated note. Does not consider MEI id
 539        """
 540        string: str = "["
 541        for p in self.pitches:  # add for pitches
 542            string += p[0]
 543            if p[1] != "None":
 544                string += p[1]
 545            if p[2]:
 546                string += "T"
 547            string += ","
 548        string = string[:-1]  # delete the last comma
 549        string += "]"
 550        string += str(self.note_head)  # add for notehead
 551        for _ in range(self.dots):  # add for dots
 552            string += "*"
 553        if self.graceType:
 554            string += self.graceType
 555            if self.graceSlash:
 556                string += "/"
 557        if len(self.beamings) > 0:  # add for beaming
 558            string += "B"
 559            for b in self.beamings:
 560                if b == "start":
 561                    string += "sr"
 562                elif b == "continue":
 563                    string += "co"
 564                elif b == "stop":
 565                    string += "sp"
 566                elif b == "partial":
 567                    string += "pa"
 568                else:
 569                    raise ValueError(f"Incorrect beaming type: {b}")
 570
 571        if len(self.tuplets) > 0:  # add for tuplets
 572            string += "T"
 573            for tup, ti in zip(self.tuplets, self.tuplet_info):
 574                if ti != "":
 575                    ti = "(" + ti + ")"
 576                if tup == "start":
 577                    string += "sr" + ti
 578                elif tup == "continue":
 579                    string += "co" + ti
 580                elif tup == "stop":
 581                    string += "sp" + ti
 582                elif tup == "startStop":
 583                    string += "ss" + ti
 584                else:
 585                    raise ValueError(f"Incorrect tuplet type: {tup}")
 586
 587        if len(self.articulations) > 0:  # add for articulations
 588            for a in self.articulations:
 589                string += " " + a
 590        if len(self.expressions) > 0:  # add for expressions
 591            for e in self.expressions:
 592                string += " " + e
 593
 594        if self.noteshape != "normal":
 595            string += f" noteshape={self.noteshape}"
 596        if self.noteheadFill is not None:
 597            string += f" noteheadFill={self.noteheadFill}"
 598        if self.noteheadParenthesis:
 599            string += f" noteheadParenthesis={self.noteheadParenthesis}"
 600        if self.stemDirection != "unspecified":
 601            string += f" stemDirection={self.stemDirection}"
 602
 603        # gap_dur
 604        if self.gap_dur != 0:
 605            string += f" spaceBefore={self.gap_dur}"
 606
 607        # and then the style fields
 608        for i, (k, v) in enumerate(self.styledict.items()):
 609            if i == 0:
 610                string += " "
 611            if i > 0:
 612                string += ","
 613            string += f"{k}={v}"
 614
 615        return string
 616
 617    def get_note_ids(self) -> list[str | int]:
 618        """
 619        Computes a list of the GeneralNote ids for this `AnnNote`.  Since there
 620        is only one GeneralNote here, this will always be a single-element list.
 621
 622        Returns:
 623            [int]: A list containing the single GeneralNote id for this note.
 624        """
 625        return [self.general_note]
 626
 627    def __eq__(self, other) -> bool:
 628        # equality does not consider the MEI id!
 629        return self.precomputed_str == other.precomputed_str
 630
 631
 632class AnnExtra:
 633    def __init__(
 634        self,
 635        extra: m21.base.Music21Object,
 636        measure: m21.stream.Measure,
 637        score: m21.stream.Score,
 638        detail: DetailLevel | int = DetailLevel.Default
 639    ) -> None:
 640        """
 641        Extend music21 non-GeneralNote and non-Stream objects with some precomputed,
 642        easily compared information about it.
 643
 644        Examples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc.
 645
 646        Args:
 647            extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream
 648                object to extend.
 649            measure (music21.stream.Measure): The music21 Measure the extra was found in.
 650                If the extra was found in a Voice, this is the Measure that the Voice was
 651                found in.
 652            detail (DetailLevel | int): What level of detail to use during the diff.
 653                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
 654                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
 655                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
 656                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
 657                Style, Metadata, or Voicing.
 658        """
 659        self.extra = extra.id
 660        self.kind: str = M21Utils.extra_to_kind(extra)
 661        self.styledict: dict = {}
 662
 663        # kind-specific fields (set to None if not relevant)
 664
 665        # content is a string that (if not None) should be counted as 1 symbol per character
 666        # (e.g. "con fiero")
 667        self.content: str | None = M21Utils.extra_to_string(extra, self.kind, detail)
 668
 669        # symbolic is a string that (if not None) should be counted as 1 symbol (e.g. "G2+8")
 670        self.symbolic: str | None = M21Utils.extra_to_symbolic(extra, self.kind, detail)
 671
 672        # offset and/or duration are sometimes relevant
 673        self.offset: OffsetQL | None = None
 674        self.duration: OffsetQL | None = None
 675        self.offset, self.duration = M21Utils.extra_to_offset_and_duration(
 676            extra, self.kind, measure, score, detail
 677        )
 678
 679        # infodict (kind-specific elements; each element is worth one musical symbol)
 680        self.infodict: dict[str, str] = M21Utils.extra_to_infodict(extra, self.kind, detail)
 681
 682        # styledict
 683        if DetailLevel.includesStyle(detail):
 684            if not isinstance(extra, m21.harmony.ChordSymbol):
 685                # We don't (yet) compare style of ChordSymbols, because Humdrum has no way (yet)
 686                # of storing that.
 687                if M21Utils.has_style(extra):
 688                    # includes extra.placement if present
 689
 690                    # special case: MM with text='SMUFLNote = nnn" is being annotated as if there is
 691                    # no text, so none of the text style stuff should be added.
 692                    smuflTextSuppressed: bool = False
 693                    if (isinstance(extra, m21.tempo.MetronomeMark)
 694                            and not extra.textImplicit
 695                            and M21Utils.parse_note_equal_num(extra.text) != (None, None)):
 696                        smuflTextSuppressed = True
 697
 698                    self.styledict = M21Utils.obj_to_styledict(
 699                        extra,
 700                        detail,
 701                        smuflTextSuppressed=smuflTextSuppressed
 702                    )
 703
 704        # precomputed/cached representations for faster comparison
 705        self.precomputed_str: str = self.__str__()
 706        self._cached_notation_size: int | None = None
 707
 708    def notation_size(self) -> int:
 709        """
 710        Compute a measure of how many symbols are displayed in the score for this `AnnExtra`.
 711
 712        Returns:
 713            int: The notation size of the annotated extra
 714        """
 715        if self._cached_notation_size is None:
 716            cost: int = 0
 717            if self.content is not None:
 718                cost += len(self.content)
 719            if self.symbolic is not None:
 720                cost += 1
 721            if self.duration is not None:
 722                cost += 1
 723            cost += len(self.infodict)
 724            if self.styledict:
 725                cost += 1  # someday we might add len(styledict) instead of 1
 726            self._cached_notation_size = cost
 727
 728        return self._cached_notation_size
 729
 730    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
 731        string: str = self.content or ""
 732        if self.symbolic:
 733            if string:
 734                string += " "
 735            string += self.symbolic
 736        if self.infodict and name != "info":
 737            for i, k in enumerate(self.infodict):
 738                if string:
 739                    string += " "
 740                string += f"{k}:{self.infodict[k]}"
 741
 742        if name == "":
 743            if self.duration is not None:
 744                if string:
 745                    string += " "
 746                string += f"dur={M21Utils.ql_to_string(self.duration)}"
 747            return string
 748
 749        if name == "content":
 750            if self.content is None:
 751                return ""
 752            return self.content
 753
 754        if name == "symbolic":
 755            if self.symbolic is None:
 756                return ""
 757            return self.symbolic
 758
 759        if name == "offset":
 760            if self.offset is None:
 761                return ""
 762            if string:
 763                string += " "
 764            string += f"offset={M21Utils.ql_to_string(self.offset)}"
 765            return string
 766
 767        if name == "duration":
 768            if self.duration is None:
 769                return ""
 770            if string:
 771                string += " "
 772            string += f"dur={M21Utils.ql_to_string(self.duration)}"
 773            return string
 774
 775        if name == "info":
 776            changedKeys: list[str] = changedStr.split(',')
 777            if not changedKeys:
 778                if string:
 779                    string += " "
 780                string += "changedInfo={}"
 781                return string
 782
 783            if string:
 784                string += " "
 785            string += "changedInfo={"
 786
 787            needsComma: bool = False
 788            for i, k in enumerate(changedKeys):
 789                if k in self.infodict:
 790                    if needsComma:
 791                        string += ", "
 792                    string += f"{k}:{self.infodict[k]}"
 793                    needsComma = True
 794            string += "}"
 795            return string
 796
 797        if name == "style":
 798            changedKeys = changedStr.split(',')
 799            if not changedKeys:
 800                if string:
 801                    string += " "
 802                string += "changedStyle={}"
 803                return string
 804
 805            if string:
 806                string += " "
 807            string += "changedStyle={"
 808
 809            needsComma = False
 810            for i, k in enumerate(changedKeys):
 811                if k in self.styledict:
 812                    if needsComma:
 813                        string += ", "
 814                    string += f"{k}:{self.styledict[k]}"
 815                    needsComma = True
 816            string += "}"
 817            return string
 818
 819        return ""  # should never get here
 820
 821    def __repr__(self) -> str:
 822        # must include a unique id for memoization!
 823        # we use the music21 id of the extra.
 824        output: str = f"Extra({self.extra}):"
 825        output += str(self)
 826        return output
 827
 828    def __str__(self) -> str:
 829        """
 830        Returns:
 831            str: the compared representation of the AnnExtra. Does not consider music21 id.
 832        """
 833        string = f'{self.kind}'
 834        if self.content:
 835            string += f',content={self.content}'
 836        if self.symbolic:
 837            string += f',symbol={self.symbolic}'
 838        if self.offset is not None:
 839            string += f',off={self.offset}'
 840        if self.duration is not None:
 841            string += f',dur={self.duration}'
 842        # then any info fields
 843        if self.infodict:
 844            string += ',info:'
 845        for k, v in self.infodict.items():
 846            string += f',{k}={v}'
 847        # and then any style fields
 848        if self.styledict:
 849            string += ',style:'
 850        for k, v in self.styledict.items():
 851            string += f',{k}={v}'
 852        return string
 853
 854    def __eq__(self, other) -> bool:
 855        # equality does not consider the MEI id!
 856        return self.precomputed_str == other.precomputed_str
 857
 858
 859class AnnLyric:
 860    def __init__(
 861        self,
 862        lyric_holder: m21.note.GeneralNote,  # note containing the lyric
 863        lyric: m21.note.Lyric,  # the lyric itself
 864        measure: m21.stream.Measure,
 865        detail: DetailLevel | int = DetailLevel.Default
 866    ) -> None:
 867        """
 868        Extend a lyric from a music21 GeneralNote with some precomputed, easily
 869        compared information about it.
 870
 871        Args:
 872            lyric_holder (music21.note.GeneralNote): The note/chord/rest containing the lyric.
 873            lyric (music21.note.Lyric): The music21 Lyric object to extend.
 874            measure (music21.stream.Measure): The music21 Measure the lyric was found in.
 875                If the lyric was found in a Voice, this is the Measure that the lyric was
 876                found in.
 877            detail (DetailLevel | int): What level of detail to use during the diff.
 878                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
 879                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
 880                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
 881                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
 882                Style, Metadata, or Voicing.
 883        """
 884        self.lyric_holder = lyric_holder.id
 885
 886        # for comparison: lyric, number, identifier, offset, styledict
 887        self.lyric: str = ""
 888        self.number: int = 0
 889        self.identifier: str = ""
 890        self.offset = lyric_holder.getOffsetInHierarchy(measure)
 891        self.styledict: dict[str, str] = {}
 892
 893        # ignore .syllabic and .text, what is visible is .rawText (and there
 894        # are several .syllabic/.text combos that create the same .rawText).
 895        self.lyric = lyric.rawText
 896
 897        if lyric.number is not None:
 898            self.number = lyric.number
 899
 900        if (lyric._identifier is not None
 901                and lyric._identifier != lyric.number
 902                and lyric._identifier != str(lyric.number)):
 903            self.identifier = lyric._identifier
 904
 905        if DetailLevel.includesStyle(detail) and M21Utils.has_style(lyric):
 906            self.styledict = M21Utils.obj_to_styledict(lyric, detail)
 907            if self.styledict:
 908                # sort styleDict before converting to string so we can compare strings
 909                self.styledict = dict(sorted(self.styledict.items()))
 910
 911        # precomputed/cached representations for faster comparison
 912        self.precomputed_str: str = self.__str__()
 913        self._cached_notation_size: int | None = None
 914
 915    def notation_size(self) -> int:
 916        """
 917        Compute a measure of how many symbols are displayed in the score for this `AnnLyric`.
 918
 919        Returns:
 920            int: The notation size of the annotated lyric
 921        """
 922        if self._cached_notation_size is None:
 923            size: int = len(self.lyric)
 924            size += 1  # for offset
 925            if self.number:
 926                size += 1
 927            if self.identifier:
 928                size += 1
 929            if self.styledict:
 930                size += 1  # maybe someday we'll count items in styledict?
 931            self._cached_notation_size = size
 932
 933        return self._cached_notation_size
 934
 935    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
 936        string: str = f'"{self.lyric}"'
 937        if name == "":
 938            if self.number is not None:
 939                string += f", num={self.number}"
 940            if self.identifier:  # not None and != ""
 941                string += f", id={self.identifier}"
 942            if self.styledict:
 943                string += f" style={self.styledict}"
 944            return string
 945
 946        if name == "rawtext":
 947            return string
 948
 949        if name == "offset":
 950            string += f" offset={M21Utils.ql_to_string(self.offset)}"
 951            return string
 952
 953        if name == "num":
 954            string += f", num={self.number}"
 955            return string
 956
 957        if name == "id":
 958            string += f", id={self.identifier}"
 959            return string
 960
 961        if name == "style":
 962            string += f" style={self.styledict}"
 963            return string
 964
 965        return ""  # should never get here
 966
 967    def __repr__(self) -> str:
 968        # must include a unique id for memoization!
 969        # we use the music21 id of the general note
 970        # that holds the lyric, plus the lyric
 971        # number within that general note.
 972        output: str = f"Lyric({self.lyric_holder}[{self.number}]):"
 973        output += str(self)
 974        return output
 975
 976    def __str__(self) -> str:
 977        """
 978        Returns:
 979            str: the compared representation of the AnnLyric. Does not consider music21 id.
 980        """
 981        string = (
 982            f"{self.lyric},num={self.number},id={self.identifier}"
 983            + f",off={self.offset},style={self.styledict}"
 984        )
 985        return string
 986
 987    def __eq__(self, other) -> bool:
 988        # equality does not consider the MEI id!
 989        return self.precomputed_str == other.precomputed_str
 990
 991
 992class AnnVoice:
 993    def __init__(
 994        self,
 995        voice: m21.stream.Voice | m21.stream.Measure,
 996        enclosingMeasure: m21.stream.Measure,
 997        detail: DetailLevel | int = DetailLevel.Default
 998    ) -> None:
 999        """
1000        Extend music21 Voice with some precomputed, easily compared information about it.
1001        Only ever called if detail includes Voicing.
1002
1003        Args:
1004            voice (music21.stream.Voice or Measure): The music21 voice to extend. This
1005                can be a Measure, but only if it contains no Voices.
1006            detail (DetailLevel | int): What level of detail to use during the diff.
1007                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1008                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1009                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1010                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1011                Style, Metadata, or Voicing.
1012        """
1013        self.voice: int | str = voice.id
1014        note_list: list[m21.note.GeneralNote] = []
1015
1016        if DetailLevel.includesNotesAndRests(detail):
1017            note_list = M21Utils.get_notes_and_gracenotes(voice)
1018
1019        self.en_beam_list: list[list[str]] = []
1020        self.tuplet_list: list[list[str]] = []
1021        self.tuplet_info: list[list[str]] = []
1022        self.annot_notes: list[AnnNote] = []
1023
1024        if note_list:
1025            self.en_beam_list = M21Utils.get_enhance_beamings(
1026                note_list,
1027                detail
1028            )  # beams ("partial" can mean partial beam or just a flag)
1029            self.tuplet_list = M21Utils.get_tuplets_type(
1030                note_list
1031            )  # corrected tuplets (with "start" and "continue")
1032            self.tuplet_info = M21Utils.get_tuplets_info(note_list)
1033            # create a list of notes with beaming and tuplets information attached
1034            self.annot_notes = []
1035            for i, n in enumerate(note_list):
1036                expectedOffsetInMeas: OffsetQL = 0
1037                if i > 0:
1038                    prevNoteStart: OffsetQL = (
1039                        note_list[i - 1].getOffsetInHierarchy(enclosingMeasure)
1040                    )
1041                    prevNoteDurQL: OffsetQL = (
1042                        note_list[i - 1].duration.quarterLength
1043                    )
1044                    expectedOffsetInMeas = opFrac(prevNoteStart + prevNoteDurQL)
1045
1046                gapDurQL: OffsetQL = (
1047                    n.getOffsetInHierarchy(enclosingMeasure) - expectedOffsetInMeas
1048                )
1049                self.annot_notes.append(
1050                    AnnNote(
1051                        n,
1052                        gapDurQL,
1053                        self.en_beam_list[i],
1054                        self.tuplet_list[i],
1055                        self.tuplet_info[i],
1056                        detail=detail
1057                    )
1058                )
1059
1060        self.n_of_notes: int = len(self.annot_notes)
1061        self.precomputed_str: str = self.__str__()
1062        self._cached_notation_size: int | None = None
1063
1064    def __eq__(self, other) -> bool:
1065        # equality does not consider MEI id!
1066        if not isinstance(other, AnnVoice):
1067            return False
1068
1069        if len(self.annot_notes) != len(other.annot_notes):
1070            return False
1071
1072        return self.precomputed_str == other.precomputed_str
1073
1074    def notation_size(self) -> int:
1075        """
1076        Compute a measure of how many symbols are displayed in the score for this `AnnVoice`.
1077
1078        Returns:
1079            int: The notation size of the annotated voice
1080        """
1081        if self._cached_notation_size is None:
1082            self._cached_notation_size = sum([an.notation_size() for an in self.annot_notes])
1083        return self._cached_notation_size
1084
1085    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1086        string: str = "["
1087        for an in self.annot_notes:
1088            string += an.readable_str()
1089            string += ","
1090
1091        if string[-1] == ",":
1092            # delete the last comma
1093            string = string[:-1]
1094
1095        string += "]"
1096        return string
1097
1098    def __repr__(self) -> str:
1099        # must include a unique id for memoization!
1100        # we use the music21 id of the voice.
1101        string: str = f"Voice({self.voice}):"
1102        string += "["
1103        for an in self.annot_notes:
1104            string += repr(an)
1105            string += ","
1106
1107        if string[-1] == ",":
1108            # delete the last comma
1109            string = string[:-1]
1110
1111        string += "]"
1112        return string
1113
1114    def __str__(self) -> str:
1115        string = "["
1116        for an in self.annot_notes:
1117            string += str(an)
1118            string += ","
1119
1120        if string[-1] == ",":
1121            # delete the last comma
1122            string = string[:-1]
1123
1124        string += "]"
1125        return string
1126
1127    def get_note_ids(self) -> list[str | int]:
1128        """
1129        Computes a list of the GeneralNote ids for this `AnnVoice`.
1130
1131        Returns:
1132            [int]: A list containing the GeneralNote ids contained in this voice
1133        """
1134        return [an.general_note for an in self.annot_notes]
1135
1136
1137class AnnMeasure:
1138    def __init__(
1139        self,
1140        measure: m21.stream.Measure,
1141        part: m21.stream.Part,
1142        score: m21.stream.Score,
1143        spannerBundle: m21.spanner.SpannerBundle,
1144        detail: DetailLevel | int = DetailLevel.Default
1145    ) -> None:
1146        """
1147        Extend music21 Measure with some precomputed, easily compared information about it.
1148
1149        Args:
1150            measure (music21.stream.Measure): The music21 Measure to extend.
1151            part (music21.stream.Part): the enclosing music21 Part
1152            score (music21.stream.Score): the enclosing music21 Score.
1153            spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners
1154                in the score.
1155            detail (DetailLevel | int): What level of detail to use during the diff.
1156                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1157                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1158                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1159                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1160                Style, Metadata, or Voicing.
1161        """
1162        self.measure: int | str = measure.id
1163        self.includes_voicing: bool = DetailLevel.includesVoicing(detail)
1164        self.n_of_elements: int = 0
1165
1166        # for text output only (see self.readable_str())
1167        self.measureNumber: str = M21Utils.get_measure_number_with_suffix(measure, part)
1168        # if self.measureNumber == 135:
1169        #     print('135')
1170
1171        if self.includes_voicing:
1172            # we make an AnnVoice for each voice in the measure
1173            self.voices_list: list[AnnVoice] = []
1174            if len(measure.voices) == 0:
1175                # there is a single AnnVoice (i.e. in the music21 Measure there are no voices)
1176                ann_voice = AnnVoice(measure, measure, detail)
1177                if ann_voice.n_of_notes > 0:
1178                    self.voices_list.append(ann_voice)
1179            else:  # there are multiple voices (or an array with just one voice)
1180                for voice in measure.voices:
1181                    ann_voice = AnnVoice(voice, measure, detail)
1182                    if ann_voice.n_of_notes > 0:
1183                        self.voices_list.append(ann_voice)
1184            self.n_of_elements = len(self.voices_list)
1185        else:
1186            # we pull up all the notes in all the voices (and split any chords into
1187            # individual notes)
1188            self.annot_notes: list[AnnNote] = []
1189
1190            note_list: list[m21.note.GeneralNote] = []
1191            if DetailLevel.includesNotesAndRests(detail):
1192                note_list = M21Utils.get_notes_and_gracenotes(measure, recurse=True)
1193
1194            if note_list:
1195                en_beam_list = M21Utils.get_enhance_beamings(
1196                    note_list,
1197                    detail
1198                )  # beams ("partial" can mean partial beam or just a flag)
1199                tuplet_list = M21Utils.get_tuplets_type(
1200                    note_list
1201                )  # corrected tuplets (with "start" and "continue")
1202                tuplet_info = M21Utils.get_tuplets_info(note_list)
1203
1204                # create a list of notes with beaming and tuplets information attached
1205                self.annot_notes = []
1206                for i, n in enumerate(note_list):
1207                    if isinstance(n, m21.chord.ChordBase):
1208                        if isinstance(n, m21.chord.Chord):
1209                            n.sortDiatonicAscending(inPlace=True)
1210                        chord_offset: OffsetQL = n.getOffsetInHierarchy(measure)
1211                        for n1 in n.notes:
1212                            self.annot_notes.append(
1213                                AnnNote(
1214                                    n1,
1215                                    0.,
1216                                    en_beam_list[i],
1217                                    tuplet_list[i],
1218                                    tuplet_info[i],
1219                                    parent_chord=n,
1220                                    chord_offset=chord_offset,
1221                                    detail=detail
1222                                )
1223                            )
1224                    else:
1225                        self.annot_notes.append(
1226                            AnnNote(
1227                                n,
1228                                0.,
1229                                en_beam_list[i],
1230                                tuplet_list[i],
1231                                tuplet_info[i],
1232                                detail=detail
1233                            )
1234                        )
1235
1236            self.n_of_elements = len(self.annot_notes)
1237
1238        self.extras_list: list[AnnExtra] = []
1239        for extra in M21Utils.get_extras(measure, part, score, spannerBundle, detail):
1240            self.extras_list.append(AnnExtra(extra, measure, score, detail))
1241        self.n_of_elements += len(self.extras_list)
1242
1243        # For correct comparison, sort the extras_list, so that any extras
1244        # that all have the same offset are sorted alphabetically.
1245        # 888 need to sort by class here?  Or not at all?
1246        self.extras_list.sort(key=lambda e: (e.kind, e.offset))
1247
1248        self.lyrics_list: list[AnnLyric] = []
1249        if DetailLevel.includesLyrics(detail):
1250            for lyric_holder in M21Utils.get_lyrics_holders(measure):
1251                for lyric in lyric_holder.lyrics:
1252                    if lyric.rawText:
1253                        # we ignore lyrics with no visible text
1254                        self.lyrics_list.append(AnnLyric(lyric_holder, lyric, measure, detail))
1255            self.n_of_elements += len(self.lyrics_list)
1256
1257            # For correct comparison, sort the lyrics_list, so that any lyrics
1258            # that all have the same offset are sorted by verse number.
1259            if self.lyrics_list:
1260                self.lyrics_list.sort(key=lambda lyr: (lyr.offset, lyr.number))
1261
1262        # precomputed/cached values to speed up the computation.
1263        # As they start to be long, they are hashed
1264        self.precomputed_str: int = hash(self.__str__())
1265        self.precomputed_repr: int = hash(self.__repr__())
1266        self._cached_notation_size: int | None = None
1267
1268    def __str__(self) -> str:
1269        output: str = ''
1270        if self.includes_voicing:
1271            output += str([str(v) for v in self.voices_list])
1272        else:
1273            output += str([str(n) for n in self.annot_notes])
1274        if self.extras_list:
1275            output += ' Extras:' + str([str(e) for e in self.extras_list])
1276        if self.lyrics_list:
1277            output += ' Lyrics:' + str([str(lyr) for lyr in self.lyrics_list])
1278        return output
1279
1280    def __repr__(self) -> str:
1281        # must include a unique id for memoization!
1282        # we use the music21 id of the measure.
1283        output: str = f"Measure({self.measure}):"
1284        if self.includes_voicing:
1285            output += str([repr(v) for v in self.voices_list])
1286        else:
1287            output += str([repr(n) for n in self.annot_notes])
1288        if self.extras_list:
1289            output += ' Extras:' + str([repr(e) for e in self.extras_list])
1290        if self.lyrics_list:
1291            output += ' Lyrics:' + str([repr(lyr) for lyr in self.lyrics_list])
1292        return output
1293
1294    def __eq__(self, other) -> bool:
1295        # equality does not consider MEI id!
1296        if not isinstance(other, AnnMeasure):
1297            return False
1298
1299        if self.includes_voicing and other.includes_voicing:
1300            if len(self.voices_list) != len(other.voices_list):
1301                return False
1302        elif not self.includes_voicing and not other.includes_voicing:
1303            if len(self.annot_notes) != len(other.annot_notes):
1304                return False
1305        else:
1306            # shouldn't ever happen, but I guess it could if the client does weird stuff
1307            return False
1308
1309        if len(self.extras_list) != len(other.extras_list):
1310            return False
1311
1312        if len(self.lyrics_list) != len(other.lyrics_list):
1313            return False
1314
1315        return self.precomputed_str == other.precomputed_str
1316        # return all([v[0] == v[1] for v in zip(self.voices_list, other.voices_list)])
1317
1318    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1319        string: str = f"measure {self.measureNumber}"
1320        return string
1321
1322    def notation_size(self) -> int:
1323        """
1324        Compute a measure of how many symbols are displayed in the score for this `AnnMeasure`.
1325
1326        Returns:
1327            int: The notation size of the annotated measure
1328        """
1329        if self._cached_notation_size is None:
1330            if self.includes_voicing:
1331                self._cached_notation_size = (
1332                    sum([v.notation_size() for v in self.voices_list])
1333                    + sum([e.notation_size() for e in self.extras_list])
1334                    + sum([lyr.notation_size() for lyr in self.lyrics_list])
1335                )
1336            else:
1337                self._cached_notation_size = (
1338                    sum([n.notation_size() for n in self.annot_notes])
1339                    + sum([e.notation_size() for e in self.extras_list])
1340                    + sum([lyr.notation_size() for lyr in self.lyrics_list])
1341                )
1342        return self._cached_notation_size
1343
1344    def get_note_ids(self) -> list[str | int]:
1345        """
1346        Computes a list of the GeneralNote ids for this `AnnMeasure`.
1347
1348        Returns:
1349            [int]: A list containing the GeneralNote ids contained in this measure
1350        """
1351        notes_id = []
1352        if self.includes_voicing:
1353            for v in self.voices_list:
1354                notes_id.extend(v.get_note_ids())
1355        else:
1356            for n in self.annot_notes:
1357                notes_id.extend(n.get_note_ids())
1358        return notes_id
1359
1360
1361class AnnPart:
1362    def __init__(
1363        self,
1364        part: m21.stream.Part,
1365        score: m21.stream.Score,
1366        part_idx: int,
1367        spannerBundle: m21.spanner.SpannerBundle,
1368        detail: DetailLevel | int = DetailLevel.Default
1369    ):
1370        """
1371        Extend music21 Part/PartStaff with some precomputed, easily compared information about it.
1372
1373        Args:
1374            part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff
1375                to extend.
1376            score (music21.stream.Score): the enclosing music21 Score.
1377            spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in
1378                the score.
1379            detail (DetailLevel | int): What level of detail to use during the diff.
1380                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1381                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1382                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1383                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1384                Style, Metadata, or Voicing.
1385        """
1386        self.part: int | str = part.id
1387        self.part_idx: int = part_idx
1388        self.bar_list: list[AnnMeasure] = []
1389        for measure in part.getElementsByClass("Measure"):
1390            # create the bar objects
1391            ann_bar = AnnMeasure(measure, part, score, spannerBundle, detail)
1392            if ann_bar.n_of_elements > 0:
1393                self.bar_list.append(ann_bar)
1394        self.n_of_bars: int = len(self.bar_list)
1395        # Precomputed str to speed up the computation.
1396        # String itself is pretty long, so it is hashed
1397        self.precomputed_str: int = hash(self.__str__())
1398        self._cached_notation_size: int | None = None
1399
1400    def __str__(self) -> str:
1401        output: str = 'Part: '
1402        output += str([str(b) for b in self.bar_list])
1403        return output
1404
1405    def __eq__(self, other) -> bool:
1406        # equality does not consider MEI id!
1407        if not isinstance(other, AnnPart):
1408            return False
1409
1410        if len(self.bar_list) != len(other.bar_list):
1411            return False
1412
1413        return all(b[0] == b[1] for b in zip(self.bar_list, other.bar_list))
1414
1415    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1416        string: str = f"part {self.part_idx}"
1417        return string
1418
1419    def notation_size(self) -> int:
1420        """
1421        Compute a measure of how many symbols are displayed in the score for this `AnnPart`.
1422
1423        Returns:
1424            int: The notation size of the annotated part
1425        """
1426        if self._cached_notation_size is None:
1427            self._cached_notation_size = sum([b.notation_size() for b in self.bar_list])
1428        return self._cached_notation_size
1429
1430    def __repr__(self) -> str:
1431        # must include a unique id for memoization!
1432        # we use the music21 id of the part.
1433        output: str = f"Part({self.part}):"
1434        output += str([repr(b) for b in self.bar_list])
1435        return output
1436
1437    def get_note_ids(self) -> list[str | int]:
1438        """
1439        Computes a list of the GeneralNote ids for this `AnnPart`.
1440
1441        Returns:
1442            [int]: A list containing the GeneralNote ids contained in this part
1443        """
1444        notes_id = []
1445        for b in self.bar_list:
1446            notes_id.extend(b.get_note_ids())
1447        return notes_id
1448
1449
1450class AnnStaffGroup:
1451    def __init__(
1452        self,
1453        staff_group: m21.layout.StaffGroup,
1454        part_to_index: dict[m21.stream.Part, int],
1455        detail: DetailLevel | int = DetailLevel.Default
1456    ) -> None:
1457        """
1458        Take a StaffGroup and store it as an annotated object.
1459        """
1460        self.staff_group: int | str = staff_group.id
1461        self.name: str = staff_group.name or ''
1462        self.abbreviation: str = staff_group.abbreviation or ''
1463        self.symbol: str | None = None
1464        self.barTogether: bool | str | None = staff_group.barTogether
1465
1466        if DetailLevel.includesStyle(detail):
1467            # symbol (brace, bracket, line, etc) is considered to be style
1468            self.symbol = staff_group.symbol
1469
1470        self.part_indices: list[int] = []
1471        for part in staff_group:
1472            self.part_indices.append(part_to_index.get(part, -1))
1473
1474        # sort so simple list comparison can work
1475        self.part_indices.sort()
1476
1477        self.n_of_parts: int = len(self.part_indices)
1478
1479        # precomputed representations for faster comparison
1480        self.precomputed_str: str = self.__str__()
1481        self._cached_notation_size: int | None = None
1482
1483    def __str__(self) -> str:
1484        output: str = "StaffGroup"
1485        if self.name and self.abbreviation:
1486            output += f"({self.name},{self.abbreviation})"
1487        elif self.name:
1488            output += f"({self.name})"
1489        elif self.abbreviation:
1490            output += f"(,{self.abbreviation})"
1491        else:
1492            output += "(,)"
1493
1494        output += f", partIndices={self.part_indices}"
1495        if self.symbol is not None:
1496            output += f", symbol={self.symbol}"
1497        if self.barTogether is not None:
1498            output += f", barTogether={self.barTogether}"
1499        return output
1500
1501    def __eq__(self, other) -> bool:
1502        # equality does not consider MEI id (or MEI ids of parts included in the group)
1503        if not isinstance(other, AnnStaffGroup):
1504            return False
1505
1506        if self.name != other.name:
1507            return False
1508
1509        if self.abbreviation != other.abbreviation:
1510            return False
1511
1512        if self.symbol != other.symbol:
1513            return False
1514
1515        if self.barTogether != other.barTogether:
1516            return False
1517
1518        if self.n_of_parts != other.n_of_parts:
1519            # trying to avoid the more expensive part_indices array comparison
1520            return False
1521
1522        if self.part_indices != other.part_indices:
1523            return False
1524
1525        return True
1526
1527    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1528        string: str = f"StaffGroup{self.part_indices}"
1529        if name == "":
1530            return string
1531
1532        if name == "name":
1533            string += f" name={self.name}"
1534            return string
1535
1536        if name == "abbr":
1537            string += f" abbr={self.abbreviation}"
1538            return string
1539
1540        if name == "sym":
1541            string += f" sym={self.symbol}"
1542            return string
1543
1544        if name == "barline":
1545            string += f" barTogether={self.barTogether}"
1546            return string
1547
1548        if name == "parts":
1549            # main string already has parts in it
1550            return string
1551
1552        return ""
1553
1554    def notation_size(self) -> int:
1555        """
1556        Compute a measure of how many symbols are displayed in the score for this `AnnStaffGroup`.
1557
1558        Returns:
1559            int: The notation size of the annotated staff group
1560        """
1561        # There are 5 main visible things about a StaffGroup:
1562        #   name, abbreviation, symbol shape, barline type, and which staves it encloses
1563        if self._cached_notation_size is None:
1564            size: int = len(self.name)
1565            size += len(self.abbreviation)
1566            size += 1  # for symbol shape
1567            size += 1  # for barline type
1568            size += 1  # for lowest staff index (vertical start)
1569            size += 1  # for highest staff index (vertical height)
1570            self._cached_notation_size = size
1571        return self._cached_notation_size
1572
1573    def __repr__(self) -> str:
1574        # must include a unique id for memoization!
1575        # we use the music21 id of the staff group.
1576        output: str = f"StaffGroup({self.staff_group}):"
1577        output += f" name={self.name}, abbrev={self.abbreviation},"
1578        output += f" symbol={self.symbol}, barTogether={self.barTogether}"
1579        output += f", partIndices={self.part_indices}"
1580        return output
1581
1582
1583class AnnMetadataItem:
1584    def __init__(
1585        self,
1586        key: str,
1587        value: t.Any
1588    ) -> None:
1589        # Normally this would be the id of the Music21Object, but we just have a key/value
1590        # pair, so we just make up an id, by using our own address.  In this case, we will
1591        # not be looking this id up in the score, but only using it as a memo-ization key.
1592        self.metadata_item = id(self)
1593        self.key = key
1594        if isinstance(value, m21.metadata.Text):
1595            # Create a string representing both the text and the language, but not isTranslated,
1596            # since isTranslated cannot be represented in many file formats.
1597            self.value = (
1598                self.make_value_string(value)
1599                + f'(language={value.language})'
1600            )
1601            if isinstance(value, m21.metadata.Copyright):
1602                self.value += f' role={value.role}'
1603        elif isinstance(value, m21.metadata.Contributor):
1604            # Create a string (same thing: value.name.isTranslated will differ randomly)
1605            # Currently I am also ignoring more than one name, and birth/death.
1606            if not value._names:
1607                # ignore this metadata item
1608                self.key = ''
1609                self.value = ''
1610                return
1611
1612            self.value = self.make_value_string(value)
1613            roleEmitted: bool = False
1614            if value.role:
1615                if value.role == 'poet':
1616                    # special case: many MusicXML files have the lyricist listed as the poet.
1617                    # We compare them as equivalent here.
1618                    lyr: str = 'lyricist'
1619                    self.key = lyr
1620                    self.value += f'(role={lyr}'
1621                else:
1622                    self.value += f'(role={value.role}'
1623                roleEmitted = True
1624            if value._names:
1625                if roleEmitted:
1626                    self.value += ', '
1627                self.value += f'language={value._names[0].language}'
1628            if roleEmitted:
1629                self.value += ')'
1630        else:
1631            # Date types
1632            self.value = str(value)
1633
1634        self._cached_notation_size: int | None = None
1635
1636    def __eq__(self, other) -> bool:
1637        if not isinstance(other, AnnMetadataItem):
1638            return False
1639
1640        if self.key != other.key:
1641            return False
1642
1643        if self.value != other.value:
1644            return False
1645
1646        return True
1647
1648    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1649        return str(self)
1650
1651    def __str__(self) -> str:
1652        return self.key + ':' + str(self.value)
1653
1654
1655    def __repr__(self) -> str:
1656        # must include a unique id for memoization!
1657        # We use id(self), because there is no music21 object here.
1658        output: str = f"MetadataItem({self.metadata_item}):"
1659        output += self.key + ':' + str(self.value)
1660        return output
1661
1662    def notation_size(self) -> int:
1663        """
1664        Compute a measure of how many symbols are displayed in the score for this `AnnMetadataItem`.
1665
1666        Returns:
1667            int: The notation size of the annotated metadata item
1668        """
1669        if self._cached_notation_size is None:
1670            size: int = len(self.key)
1671            size += len(self.value)
1672            self._cached_notation_size = size
1673        return self._cached_notation_size
1674
1675    def make_value_string(self, value: m21.metadata.Contributor | m21.metadata.Text) -> str:
1676        # Unescapes a bunch of stuff (and strips off leading/trailing whitespace)
1677        output: str = str(value)
1678        output = output.strip()
1679        output = html.unescape(output)
1680        return output
1681
1682
1683class AnnScore:
1684    def __init__(
1685        self,
1686        score: m21.stream.Score,
1687        detail: DetailLevel | int = DetailLevel.Default
1688    ) -> None:
1689        """
1690        Take a music21 score and store it as a sequence of Full Trees.
1691        The hierarchy is "score -> parts -> measures -> voices -> notes"
1692        Args:
1693            score (music21.stream.Score): The music21 score
1694            detail (DetailLevel | int): What level of detail to use during the diff.
1695                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1696                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1697                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1698                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1699                Style, Metadata, or Voicing.
1700        """
1701        self.score: int | str = score.id
1702        self.part_list: list[AnnPart] = []
1703        self.staff_group_list: list[AnnStaffGroup] = []
1704        self.metadata_items_list: list[AnnMetadataItem] = []
1705        self.num_syntax_errors_fixed: int = 0
1706
1707        if hasattr(score, "c21_syntax_errors_fixed"):
1708            self.num_syntax_errors_fixed = score.c21_syntax_errors_fixed  # type: ignore
1709
1710        spannerBundle: m21.spanner.SpannerBundle = score.spannerBundle
1711        part_to_index: dict[m21.stream.Part, int] = {}
1712
1713        # Before we start, transpose all notes to written pitch, both for transposing
1714        # instruments and Ottavas. Be careful to preserve accidental.displayStatus
1715        # during transposition, since we use that visibility indicator when comparing
1716        # accidentals.
1717        score.toWrittenPitch(inPlace=True, preserveAccidentalDisplay=True)
1718
1719        for idx, part in enumerate(score.parts):
1720            # create and add the AnnPart object to part_list
1721            # and to part_to_index dict
1722            part_to_index[part] = idx
1723            ann_part = AnnPart(part, score, idx, spannerBundle, detail)
1724            self.part_list.append(ann_part)
1725
1726        self.n_of_parts: int = len(self.part_list)
1727
1728        if DetailLevel.includesStaffDetails(detail):
1729            for staffGroup in score[m21.layout.StaffGroup]:
1730                # ignore any StaffGroup that contains all the parts, and has no symbol
1731                # and has no barthru (this is just a placeholder generated by some
1732                # file formats, and has the same meaning if it is missing).
1733                if len(staffGroup) == len(part_to_index):
1734                    if not staffGroup.symbol and not staffGroup.barTogether:
1735                        continue
1736
1737                ann_staff_group = AnnStaffGroup(staffGroup, part_to_index, detail)
1738                if ann_staff_group.n_of_parts > 0:
1739                    self.staff_group_list.append(ann_staff_group)
1740
1741            # now sort the staff_group_list in increasing order of first part index
1742            # (secondary sort in decreasing order of last part index)
1743            self.staff_group_list.sort(
1744                key=lambda each: (each.part_indices[0], -each.part_indices[-1])
1745            )
1746
1747        if DetailLevel.includesMetadata(detail) and score.metadata:
1748            # m21 metadata.all() can't sort primitives, so we'll have to sort by hand.
1749            # Note: we sort metadata_items_list after the fact, because sometimes
1750            # (e.g. otherContributor:poet) we substitute names (e.g. lyricist:)
1751            allItems: list[tuple[str, t.Any]] = list(
1752                score.metadata.all(returnPrimitives=True, returnSorted=False)
1753            )
1754            for key, value in allItems:
1755                if key in ('fileFormat', 'filePath', 'software'):
1756                    # Don't compare metadata items that are uninterestingly different.
1757                    continue
1758                if (key.startswith('raw:')
1759                        or key.startswith('meiraw:')
1760                        or key.startswith('humdrumraw:')):
1761                    # Don't compare verbatim/raw metadata ('meiraw:meihead',
1762                    # 'raw:freeform', 'humdrumraw:XXX'), it's often deleted
1763                    # when made obsolete by conversions/edits.
1764                    continue
1765                if key in ('humdrum:EMD', 'humdrum:EST', 'humdrum:VTS',
1766                        'humdrum:RLN', 'humdrum:PUB'):
1767                    # Don't compare metadata items that should never be transferred
1768                    # from one file to another.  'humdrum:EMD' is a modification
1769                    # description entry, humdrum:EST is "current encoding status"
1770                    # (i.e. complete or some value of not complete), 'humdrum:VTS'
1771                    # is a checksum of the Humdrum file, 'humdrum:RLN' is the
1772                    # extended ASCII encoding of the Humdrum file, 'humdrum:PUB'
1773                    # is the publication status of the file (published or not?).
1774                    continue
1775                ami: AnnMetadataItem = AnnMetadataItem(key, value)
1776                if ami.key and ami.value:
1777                    self.metadata_items_list.append(ami)
1778
1779            self.metadata_items_list.sort(key=lambda each: (each.key, str(each.value)))
1780
1781        # cached notation size
1782        self._cached_notation_size: int | None = None
1783
1784    def __eq__(self, other) -> bool:
1785        # equality does not consider MEI id!
1786        if not isinstance(other, AnnScore):
1787            return False
1788
1789        if len(self.part_list) != len(other.part_list):
1790            return False
1791
1792        return all(p[0] == p[1] for p in zip(self.part_list, other.part_list))
1793
1794    def notation_size(self) -> int:
1795        """
1796        Compute a measure of how many symbols are displayed in the score for this `AnnScore`.
1797
1798        Returns:
1799            int: The notation size of the annotated score
1800        """
1801        if self._cached_notation_size is None:
1802            size: int = sum([p.notation_size() for p in self.part_list])
1803            size += sum([sg.notation_size() for sg in self.staff_group_list])
1804            size += sum([md.notation_size() for md in self.metadata_items_list])
1805            self._cached_notation_size = size
1806        return self._cached_notation_size
1807
1808    def __repr__(self) -> str:
1809        # must include a unique id for memoization!
1810        # we use the music21 id of the score.
1811        output: str = f"Score({self.score}):"
1812        output += str(repr(p) for p in self.part_list)
1813        return output
1814
1815    def get_note_ids(self) -> list[str | int]:
1816        """
1817        Computes a list of the GeneralNote ids for this `AnnScore`.
1818
1819        Returns:
1820            [int]: A list containing the GeneralNote ids contained in this score
1821        """
1822        notes_id = []
1823        for p in self.part_list:
1824            notes_id.extend(p.get_note_ids())
1825        return notes_id
1826
1827    # return the sequences of measures for a specified part
1828    def _measures_from_part(self, part_number) -> list[AnnMeasure]:
1829        # only used by tests/test_scl.py
1830        if part_number not in range(0, len(self.part_list)):
1831            raise ValueError(
1832                f"parameter 'part_number' should be between 0 and {len(self.part_list) - 1}"
1833            )
1834        return self.part_list[part_number].bar_list
class AnnNote:
 28class AnnNote:
 29    def __init__(
 30        self,
 31        general_note: m21.note.GeneralNote,
 32        gap_dur: OffsetQL,
 33        enhanced_beam_list: list[str],
 34        tuplet_list: list[str],
 35        tuplet_info: list[str],
 36        parent_chord: m21.chord.ChordBase | None = None,
 37        chord_offset: OffsetQL | None = None,  # only set if this note is inside a chord
 38        detail: DetailLevel | int = DetailLevel.Default,
 39    ) -> None:
 40        """
 41        Extend music21 GeneralNote with some precomputed, easily compared information about it.
 42
 43        Args:
 44            general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
 45            gap_dur (OffsetQL): gap since end of last note (or since start of measure, if
 46                first note in measure).  Usually zero.
 47            enhanced_beam_list (list): A list of beaming information about this GeneralNote.
 48            tuplet_list (list): A list of basic tuplet info about this GeneralNote.
 49            tuplet_info (list): A list of detailed tuplet info about this GeneralNote.
 50            detail (DetailLevel | int): What level of detail to use during the diff.
 51                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
 52                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
 53                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
 54                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
 55                Style, Metadata, or Voicing.
 56        """
 57        self.general_note: int | str = general_note.id
 58        self.is_in_chord: bool = False
 59        self.note_idx_in_chord: int | None = None
 60        if parent_chord is not None:
 61            # This is what visualization uses to color the note red (chord id and note idx)
 62            self.general_note = parent_chord.id
 63            self.is_in_chord = True
 64            self.note_idx_in_chord = parent_chord.notes.index(general_note)
 65
 66        # A lot of stuff is carried by the parent_chord (if present) or the
 67        # general_note (if parent_chord not present); we call that the carrier
 68        carrier: m21.note.GeneralNote = parent_chord or general_note
 69
 70        self.gap_dur: OffsetQL = gap_dur
 71        self.beamings: list[str] = enhanced_beam_list
 72        self.tuplets: list[str] = tuplet_list
 73        self.tuplet_info: list[str] = tuplet_info
 74
 75        self.note_offset: OffsetQL = 0.
 76        self.note_dur_type: str = ''
 77        self.note_dur_dots: int = 0
 78        self.note_is_grace: bool = False
 79
 80        # fullNameSuffix is only for text output, it is not involved in comparison at all.
 81        # It is of the form "Dotted Quarter Rest", etc.
 82        self.fullNameSuffix: str = general_note.duration.fullName
 83        if isinstance(general_note, m21.note.Rest):
 84            self.fullNameSuffix += " Rest"
 85        elif isinstance(general_note, m21.chord.ChordBase):
 86            if parent_chord is None:
 87                self.fullNameSuffix += " Chord"
 88            else:
 89                # we're actually annotating one of the notes in the chord
 90                self.fullNameSuffix += " Note"
 91        elif isinstance(general_note, (m21.note.Note, m21.note.Unpitched)):
 92            self.fullNameSuffix += " Note"
 93        else:
 94            self.fullNameSuffix += " Note"
 95        self.fullNameSuffix = self.fullNameSuffix.lower()
 96
 97        if not DetailLevel.includesVoicing(detail):
 98            # if we're comparing the individual notes, we need to make a note of
 99            # offset and visual duration to be used later when searching for matching
100            # notes in the measures being compared.
101
102            # offset
103            if chord_offset is None:
104                self.note_offset = general_note.offset
105            else:
106                self.note_offset = chord_offset
107
108            # visual duration and graceness
109            self.note_dur_type = carrier.duration.type
110            self.note_dur_dots = carrier.duration.dots
111            self.note_is_grace = carrier.duration.isGrace
112
113        self.styledict: dict = {}
114
115        if DetailLevel.includesStyle(detail):
116            # we will take style from the individual note, and then override with
117            # style from the chord (following music21's MusicXML exporter).
118            if M21Utils.has_style(general_note):
119                self.styledict = M21Utils.obj_to_styledict(general_note, detail)
120
121            if parent_chord is not None:
122                if M21Utils.has_style(parent_chord):
123                    parentstyledict = M21Utils.obj_to_styledict(parent_chord, detail)
124                    for k, v in parentstyledict.items():
125                        self.styledict[k] = v
126
127        self.noteshape: str = 'normal'
128        self.noteheadFill: bool | None = None
129        self.noteheadParenthesis: bool = False
130        self.stemDirection: str = 'unspecified'
131        if DetailLevel.includesStyle(detail) and isinstance(general_note, m21.note.NotRest):
132            # stemDirection is different.  It might be on the parent chord, or
133            # it might be on one of the notes in the parent chord (and applies
134            # to all the notes in the chord, of course).
135            if parent_chord is None:
136                self.stemDirection = general_note.stemDirection
137            else:
138                if parent_chord.stemDirection != 'unspecified':
139                    self.stemDirection = parent_chord.stemDirection
140                else:
141                    for n in parent_chord.notes:
142                        if n.stemDirection != 'unspecified':
143                            self.stemDirection = n.stemDirection
144                            break
145
146            if parent_chord is None:
147                self.noteshape = general_note.notehead
148                self.noteheadFill = general_note.noteheadFill
149                self.noteheadParenthesis = general_note.noteheadParenthesis
150            else:
151                # try general_note first, but if nothing about note head is specified,
152                # go with whatever parent_chord says.
153                if (general_note.notehead != 'normal'
154                        or general_note.noteheadParenthesis
155                        or general_note.noteheadFill is not None):
156                    self.noteheadParenthesis = general_note.noteheadParenthesis
157                    self.noteshape = general_note.notehead
158                    self.noteheadFill = general_note.noteheadFill
159                else:
160                    self.noteshape = parent_chord.notehead
161                    self.noteheadFill = parent_chord.noteheadFill
162                    self.noteheadParenthesis = parent_chord.noteheadParenthesis
163
164        # compute the representation of NoteNode as in the paper
165        # pitches is a list  of elements, each one is (pitchposition, accidental, tied)
166        self.pitches: list[tuple[str, str, bool]]
167        if isinstance(general_note, m21.chord.ChordBase):
168            notes: tuple[m21.note.NotRest, ...] = general_note.notes
169            if hasattr(general_note, "sortDiatonicAscending"):
170                # PercussionChords don't have this, Chords do
171                notes = general_note.sortDiatonicAscending().notes
172            self.pitches = []
173            for p in notes:
174                if not isinstance(p, (m21.note.Note, m21.note.Unpitched)):
175                    raise TypeError("The chord must contain only Note or Unpitched")
176                self.pitches.append(M21Utils.note2tuple(p, detail))
177
178        elif isinstance(general_note, (m21.note.Note, m21.note.Unpitched, m21.note.Rest)):
179            self.pitches = [M21Utils.note2tuple(general_note, detail)]
180        else:
181            raise TypeError("The generalNote must be a Chord, a Rest, a Note, or an Unpitched")
182
183        dur: m21.duration.Duration = carrier.duration
184        # note head
185        type_number = Fraction(
186            M21Utils.get_type_num(dur)
187        )
188        self.note_head: int | Fraction
189        if type_number >= 4:
190            self.note_head = 4
191        else:
192            self.note_head = type_number
193        # dots
194        self.dots: int = dur.dots
195        # graceness
196        self.graceType: str = ''
197        self.graceSlash: bool | None = False
198        if isinstance(dur, m21.duration.AppoggiaturaDuration):
199            self.graceType = 'acc'
200            self.graceSlash = dur.slash
201        elif isinstance(dur, m21.duration.GraceDuration):
202            # might be accented or unaccented.  duration.slash isn't always reliable
203            # (historically), but we can use it as a fallback.
204            # Check duration.stealTimePrevious and duration.stealTimeFollowing first.
205            if dur.stealTimePrevious is not None:
206                self.graceType = 'unacc'
207            elif dur.stealTimeFollowing is not None:
208                self.graceType = 'acc'
209            elif dur.slash is True:
210                self.graceType = 'unacc'
211            elif dur.slash is False:
212                self.graceType = 'acc'
213            else:
214                # by default, GraceDuration with no other indications (slash is None)
215                # is assumed to be unaccented.
216                self.graceType = 'unacc'
217            self.graceSlash = dur.slash
218
219        # The following (articulations, expressions) only occur once per chord
220        # or standalone note, so we only want to annotate them once.  We annotate them
221        # on standalone notes (of course), and on the first note of a parent_chord.
222        self.articulations: list[str] = []
223        self.expressions: list[str] = []
224
225        if self.note_idx_in_chord is None or self.note_idx_in_chord == 0:
226            # articulations
227            if DetailLevel.includesArticulations(detail):
228                self.articulations = [
229                    M21Utils.articulation_to_string(a, detail) for a in carrier.articulations
230                ]
231                if self.articulations:
232                    self.articulations.sort()
233
234            if DetailLevel.includesOrnaments(detail):
235                # expressions (tremolo, arpeggio, textexp have their own detail bits, though)
236                for a in carrier.expressions:
237                    if not DetailLevel.includesTremolos(detail):
238                        if isinstance(a, m21.expressions.Tremolo):
239                            continue
240                    if not DetailLevel.includesArpeggios(detail):
241                        if isinstance(a, m21.expressions.ArpeggioMark):
242                            continue
243                    if not DetailLevel.includesDirections(detail):
244                        if isinstance(a, m21.expressions.TextExpression):
245                            continue
246                    self.expressions.append(
247                        M21Utils.expression_to_string(a, detail)
248                    )
249                if self.expressions:
250                    self.expressions.sort()
251
252        # precomputed/cached representations for faster comparison
253        self.precomputed_str: str = self.__str__()
254        self._cached_notation_size: int | None = None
255
256    def notation_size(self) -> int:
257        """
258        Compute a measure of how many symbols are displayed in the score for this `AnnNote`.
259
260        Returns:
261            int: The notation size of the annotated note
262        """
263        if self._cached_notation_size is None:
264            size: int = 0
265            # add for the pitches
266            for pitch in self.pitches:
267                size += M21Utils.pitch_size(pitch)
268            # add for the notehead (quarter, half, semibreve, breve, etc)
269            size += 1
270            # add for the dots
271            size += self.dots * len(self.pitches)  # one dot for each note if it's a chord
272            # add for the beams/flags
273            size += len(self.beamings)
274            # add for the tuplets
275            size += len(self.tuplets)
276            size += len(self.tuplet_info)
277            # add for the articulations
278            size += len(self.articulations)
279            # add for the expressions
280            size += len(self.expressions)
281            # add 1 if it's a gracenote, and 1 more if there's a grace slash
282            if self.graceType:
283                size += 1
284                if self.graceSlash is True:
285                    size += 1
286            # add 1 for abnormal note shape (diamond, etc)
287            if self.noteshape != 'normal':
288                size += 1
289            # add 1 for abnormal note fill
290            if self.noteheadFill is not None:
291                size += 1
292            # add 1 if there's a parenthesis around the note
293            if self.noteheadParenthesis:
294                size += 1
295            # add 1 if stem direction is specified
296            if self.stemDirection != 'unspecified':
297                size += 1
298            # add 1 if there is an empty space before this note
299            if self.gap_dur != 0:
300                size += 1
301            # add 1 for any other style info (in future might count the style entries)
302            if self.styledict:
303                size += 1
304
305            self._cached_notation_size = size
306
307        return self._cached_notation_size
308
309    def get_identifying_string(self, name: str = "") -> str:
310        string: str = ""
311        if self.fullNameSuffix.endswith("rest"):
312            string = self.fullNameSuffix
313        elif self.fullNameSuffix.endswith("note"):
314            string = self.pitches[0][0]
315            if self.pitches[0][1] != "None":
316                string += " " + self.pitches[0][1]
317            string += " (" + self.fullNameSuffix + ")"
318        elif self.fullNameSuffix.endswith("chord"):
319            string = "["
320            for p in self.pitches:  # add for pitches
321                string += p[0]  # pitch name and octave
322                if p[1] != "None":
323                    string += " " + p[1]  # pitch accidental
324                string += ","
325            string = string[:-1]  # delete the last comma
326            string += "] (" + self.fullNameSuffix + ")"
327        return string
328
329    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
330        string: str = self.get_identifying_string(name)
331        if name == "pitch":
332            # this is only for "pitch", not for "" (pitches are in identifying string)
333            if self.fullNameSuffix.endswith("chord"):
334                string += f", pitch[{idx}]={self.pitches[idx][0]}"
335            return string
336
337        if name == "accid":
338            # this is only for "accid" (indexed in a chord), not for "", or for "accid" on a note
339            # (accidental is in identifying string)
340            if self.fullNameSuffix.endswith("chord"):
341                string += f", accid[{idx}]={self.pitches[idx][1]}"
342            return string
343
344        if name == "head":
345            # this is only for "head", not for "" (head is implied by identifying string)
346            if self.note_head == 4:
347                string += ", head=normal"
348            else:
349                string += f", head={m21.duration.typeFromNumDict[float(self.note_head)]}"
350            if name:
351                return string
352
353        if name == "dots":
354            # this is only for "dots", not for "" (dots is in identifying string)
355            string += f", dots={self.dots}"
356            return string
357
358        if not name or name == "flagsbeams":
359            numBeams: int = len(self.beamings)
360            # Flags are implied by identifying string, so do not belong when name=="".
361            # And "no beams" is boring for name=="".  Non-zero beams, though, we always
362            # want to see.
363            if numBeams == 0:
364                if name:
365                    string += ", no flags/beams"
366                    return string
367            elif all(b == "partial" for b in self.beamings):
368                if name:
369                    if numBeams == 1:
370                        string += f", {numBeams} flag"
371                    else:
372                        string += f", {numBeams} flags"
373                    return string
374            else:
375                # it's beams, not flags
376                if numBeams == 1:
377                    string += f", {numBeams} beam="
378                else:
379                    string += f", {numBeams} beams=["
380                for i, b in enumerate(self.beamings):
381                    if i > 0:
382                        string += ", "
383                    string += b
384                if numBeams > 1:
385                    string += "]"
386                if name:
387                    return string
388
389        if not name or name == "tuplet":
390            if name or self.tuplets:
391                string += ", tuplets=["
392                for i, (tup, ti) in enumerate(zip(self.tuplets, self.tuplet_info)):
393                    if i > 0:
394                        string += ", "
395                    if ti != "":
396                        ti = "(" + ti + ")"
397                    string += tup + ti
398
399                string += "]"
400                if name:
401                    return string
402
403        if not name or name == "tie":
404            if self.pitches[idx][2]:
405                string += ", tied"
406            elif name:
407                string += ", not tied"
408            if name:
409                return string
410
411
412        if not name or name == "grace":
413            if not name:
414                if self.graceType:
415                    string += f", grace={self.graceType}"
416            else:
417                string += f", grace={self.graceType}"
418            if name:
419                return string
420
421        if not name or name == "graceslash":
422            if self.graceType:
423                if self.graceSlash:
424                    string += ", with grace slash"
425                else:
426                    string += ", with no grace slash"
427            if name:
428                return string
429
430        if not name or name == "noteshape":
431            if not name:
432                if self.noteshape != "normal":
433                    string += f", noteshape={self.noteshape}"
434            else:
435                string += f", noteshape={self.noteshape}"
436            if name:
437                return string
438
439        if not name or name == "notefill":
440            if not name:
441                if self.noteheadFill is not None:
442                    string += f", noteheadFill={self.noteheadFill}"
443            else:
444                string += f", noteheadFill={self.noteheadFill}"
445            if name:
446                return string
447
448        if not name or name == "noteparen":
449            if not name:
450                if self.noteheadParenthesis:
451                    string += f", noteheadParenthesis={self.noteheadParenthesis}"
452            else:
453                string += f", noteheadParenthesis={self.noteheadParenthesis}"
454            if name:
455                return string
456
457        if not name or name == "stemdir":
458            if not name:
459                if self.stemDirection != "unspecified":
460                    string += f", stemDirection={self.stemDirection}"
461            else:
462                string += f", stemDirection={self.stemDirection}"
463            if name:
464                return string
465
466        if not name or name == "spacebefore":
467            if not name:
468                if self.gap_dur != 0:
469                    string += f", spacebefore={self.gap_dur}"
470            else:
471                string += f", spacebefore={self.gap_dur}"
472            if name:
473                return string
474
475        if not name or name == "artic":
476            if name or self.articulations:
477                string += ", articulations=["
478                for i, artic in enumerate(self.articulations):
479                    if i > 0:
480                        string += ", "
481                    string += artic
482                string += "]"
483            if name:
484                return string
485
486        if not name or name == "expression":
487            if name or self.expressions:
488                string += ", expressions=["
489                for i, exp in enumerate(self.expressions):
490                    if i > 0:
491                        string += ", "
492                    string += exp
493                string += "]"
494            if name:
495                return string
496
497        if not name or name == "style":
498            if name or self.styledict:
499                allOfThem: bool = False
500                changedKeys: list[str] = []
501                if changedStr:
502                    changedKeys = changedStr.split(",")
503                else:
504                    changedKeys = [str(k) for k in self.styledict]
505                    allOfThem = True
506
507                if allOfThem:
508                    string += ", style={"
509                else:
510                    string += ", changedStyle={"
511
512                needsComma: bool = False
513                for i, k in enumerate(changedKeys):
514                    if k in self.styledict:
515                        if needsComma:
516                            string += ", "
517                        string += f"{k}:{self.styledict[k]}"
518                        needsComma = True
519                string += "}"
520            if name:
521                return string
522
523        return string
524
525    def __repr__(self) -> str:
526        # must include a unique id for memoization!
527        # we use the music21 id of the general note.
528        return (
529            f"GeneralNote({self.general_note}),G:{self.gap_dur},"
530            + f"P:{self.pitches},H:{self.note_head},D:{self.dots},"
531            + f"B:{self.beamings},T:{self.tuplets},TI:{self.tuplet_info},"
532            + f"A:{self.articulations},E:{self.expressions},"
533            + f"S:{self.styledict}"
534        )
535
536    def __str__(self) -> str:
537        """
538        Returns:
539            str: the representation of the Annotated note. Does not consider MEI id
540        """
541        string: str = "["
542        for p in self.pitches:  # add for pitches
543            string += p[0]
544            if p[1] != "None":
545                string += p[1]
546            if p[2]:
547                string += "T"
548            string += ","
549        string = string[:-1]  # delete the last comma
550        string += "]"
551        string += str(self.note_head)  # add for notehead
552        for _ in range(self.dots):  # add for dots
553            string += "*"
554        if self.graceType:
555            string += self.graceType
556            if self.graceSlash:
557                string += "/"
558        if len(self.beamings) > 0:  # add for beaming
559            string += "B"
560            for b in self.beamings:
561                if b == "start":
562                    string += "sr"
563                elif b == "continue":
564                    string += "co"
565                elif b == "stop":
566                    string += "sp"
567                elif b == "partial":
568                    string += "pa"
569                else:
570                    raise ValueError(f"Incorrect beaming type: {b}")
571
572        if len(self.tuplets) > 0:  # add for tuplets
573            string += "T"
574            for tup, ti in zip(self.tuplets, self.tuplet_info):
575                if ti != "":
576                    ti = "(" + ti + ")"
577                if tup == "start":
578                    string += "sr" + ti
579                elif tup == "continue":
580                    string += "co" + ti
581                elif tup == "stop":
582                    string += "sp" + ti
583                elif tup == "startStop":
584                    string += "ss" + ti
585                else:
586                    raise ValueError(f"Incorrect tuplet type: {tup}")
587
588        if len(self.articulations) > 0:  # add for articulations
589            for a in self.articulations:
590                string += " " + a
591        if len(self.expressions) > 0:  # add for expressions
592            for e in self.expressions:
593                string += " " + e
594
595        if self.noteshape != "normal":
596            string += f" noteshape={self.noteshape}"
597        if self.noteheadFill is not None:
598            string += f" noteheadFill={self.noteheadFill}"
599        if self.noteheadParenthesis:
600            string += f" noteheadParenthesis={self.noteheadParenthesis}"
601        if self.stemDirection != "unspecified":
602            string += f" stemDirection={self.stemDirection}"
603
604        # gap_dur
605        if self.gap_dur != 0:
606            string += f" spaceBefore={self.gap_dur}"
607
608        # and then the style fields
609        for i, (k, v) in enumerate(self.styledict.items()):
610            if i == 0:
611                string += " "
612            if i > 0:
613                string += ","
614            string += f"{k}={v}"
615
616        return string
617
618    def get_note_ids(self) -> list[str | int]:
619        """
620        Computes a list of the GeneralNote ids for this `AnnNote`.  Since there
621        is only one GeneralNote here, this will always be a single-element list.
622
623        Returns:
624            [int]: A list containing the single GeneralNote id for this note.
625        """
626        return [self.general_note]
627
628    def __eq__(self, other) -> bool:
629        # equality does not consider the MEI id!
630        return self.precomputed_str == other.precomputed_str
AnnNote( general_note: music21.note.GeneralNote, gap_dur: float | fractions.Fraction, enhanced_beam_list: list[str], tuplet_list: list[str], tuplet_info: list[str], parent_chord: music21.chord.ChordBase | None = None, chord_offset: float | fractions.Fraction | None = None, detail: musicdiff.detaillevel.DetailLevel | int = <DetailLevel.AllObjects: 32767>)
 29    def __init__(
 30        self,
 31        general_note: m21.note.GeneralNote,
 32        gap_dur: OffsetQL,
 33        enhanced_beam_list: list[str],
 34        tuplet_list: list[str],
 35        tuplet_info: list[str],
 36        parent_chord: m21.chord.ChordBase | None = None,
 37        chord_offset: OffsetQL | None = None,  # only set if this note is inside a chord
 38        detail: DetailLevel | int = DetailLevel.Default,
 39    ) -> None:
 40        """
 41        Extend music21 GeneralNote with some precomputed, easily compared information about it.
 42
 43        Args:
 44            general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
 45            gap_dur (OffsetQL): gap since end of last note (or since start of measure, if
 46                first note in measure).  Usually zero.
 47            enhanced_beam_list (list): A list of beaming information about this GeneralNote.
 48            tuplet_list (list): A list of basic tuplet info about this GeneralNote.
 49            tuplet_info (list): A list of detailed tuplet info about this GeneralNote.
 50            detail (DetailLevel | int): What level of detail to use during the diff.
 51                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
 52                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
 53                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
 54                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
 55                Style, Metadata, or Voicing.
 56        """
 57        self.general_note: int | str = general_note.id
 58        self.is_in_chord: bool = False
 59        self.note_idx_in_chord: int | None = None
 60        if parent_chord is not None:
 61            # This is what visualization uses to color the note red (chord id and note idx)
 62            self.general_note = parent_chord.id
 63            self.is_in_chord = True
 64            self.note_idx_in_chord = parent_chord.notes.index(general_note)
 65
 66        # A lot of stuff is carried by the parent_chord (if present) or the
 67        # general_note (if parent_chord not present); we call that the carrier
 68        carrier: m21.note.GeneralNote = parent_chord or general_note
 69
 70        self.gap_dur: OffsetQL = gap_dur
 71        self.beamings: list[str] = enhanced_beam_list
 72        self.tuplets: list[str] = tuplet_list
 73        self.tuplet_info: list[str] = tuplet_info
 74
 75        self.note_offset: OffsetQL = 0.
 76        self.note_dur_type: str = ''
 77        self.note_dur_dots: int = 0
 78        self.note_is_grace: bool = False
 79
 80        # fullNameSuffix is only for text output, it is not involved in comparison at all.
 81        # It is of the form "Dotted Quarter Rest", etc.
 82        self.fullNameSuffix: str = general_note.duration.fullName
 83        if isinstance(general_note, m21.note.Rest):
 84            self.fullNameSuffix += " Rest"
 85        elif isinstance(general_note, m21.chord.ChordBase):
 86            if parent_chord is None:
 87                self.fullNameSuffix += " Chord"
 88            else:
 89                # we're actually annotating one of the notes in the chord
 90                self.fullNameSuffix += " Note"
 91        elif isinstance(general_note, (m21.note.Note, m21.note.Unpitched)):
 92            self.fullNameSuffix += " Note"
 93        else:
 94            self.fullNameSuffix += " Note"
 95        self.fullNameSuffix = self.fullNameSuffix.lower()
 96
 97        if not DetailLevel.includesVoicing(detail):
 98            # if we're comparing the individual notes, we need to make a note of
 99            # offset and visual duration to be used later when searching for matching
100            # notes in the measures being compared.
101
102            # offset
103            if chord_offset is None:
104                self.note_offset = general_note.offset
105            else:
106                self.note_offset = chord_offset
107
108            # visual duration and graceness
109            self.note_dur_type = carrier.duration.type
110            self.note_dur_dots = carrier.duration.dots
111            self.note_is_grace = carrier.duration.isGrace
112
113        self.styledict: dict = {}
114
115        if DetailLevel.includesStyle(detail):
116            # we will take style from the individual note, and then override with
117            # style from the chord (following music21's MusicXML exporter).
118            if M21Utils.has_style(general_note):
119                self.styledict = M21Utils.obj_to_styledict(general_note, detail)
120
121            if parent_chord is not None:
122                if M21Utils.has_style(parent_chord):
123                    parentstyledict = M21Utils.obj_to_styledict(parent_chord, detail)
124                    for k, v in parentstyledict.items():
125                        self.styledict[k] = v
126
127        self.noteshape: str = 'normal'
128        self.noteheadFill: bool | None = None
129        self.noteheadParenthesis: bool = False
130        self.stemDirection: str = 'unspecified'
131        if DetailLevel.includesStyle(detail) and isinstance(general_note, m21.note.NotRest):
132            # stemDirection is different.  It might be on the parent chord, or
133            # it might be on one of the notes in the parent chord (and applies
134            # to all the notes in the chord, of course).
135            if parent_chord is None:
136                self.stemDirection = general_note.stemDirection
137            else:
138                if parent_chord.stemDirection != 'unspecified':
139                    self.stemDirection = parent_chord.stemDirection
140                else:
141                    for n in parent_chord.notes:
142                        if n.stemDirection != 'unspecified':
143                            self.stemDirection = n.stemDirection
144                            break
145
146            if parent_chord is None:
147                self.noteshape = general_note.notehead
148                self.noteheadFill = general_note.noteheadFill
149                self.noteheadParenthesis = general_note.noteheadParenthesis
150            else:
151                # try general_note first, but if nothing about note head is specified,
152                # go with whatever parent_chord says.
153                if (general_note.notehead != 'normal'
154                        or general_note.noteheadParenthesis
155                        or general_note.noteheadFill is not None):
156                    self.noteheadParenthesis = general_note.noteheadParenthesis
157                    self.noteshape = general_note.notehead
158                    self.noteheadFill = general_note.noteheadFill
159                else:
160                    self.noteshape = parent_chord.notehead
161                    self.noteheadFill = parent_chord.noteheadFill
162                    self.noteheadParenthesis = parent_chord.noteheadParenthesis
163
164        # compute the representation of NoteNode as in the paper
165        # pitches is a list  of elements, each one is (pitchposition, accidental, tied)
166        self.pitches: list[tuple[str, str, bool]]
167        if isinstance(general_note, m21.chord.ChordBase):
168            notes: tuple[m21.note.NotRest, ...] = general_note.notes
169            if hasattr(general_note, "sortDiatonicAscending"):
170                # PercussionChords don't have this, Chords do
171                notes = general_note.sortDiatonicAscending().notes
172            self.pitches = []
173            for p in notes:
174                if not isinstance(p, (m21.note.Note, m21.note.Unpitched)):
175                    raise TypeError("The chord must contain only Note or Unpitched")
176                self.pitches.append(M21Utils.note2tuple(p, detail))
177
178        elif isinstance(general_note, (m21.note.Note, m21.note.Unpitched, m21.note.Rest)):
179            self.pitches = [M21Utils.note2tuple(general_note, detail)]
180        else:
181            raise TypeError("The generalNote must be a Chord, a Rest, a Note, or an Unpitched")
182
183        dur: m21.duration.Duration = carrier.duration
184        # note head
185        type_number = Fraction(
186            M21Utils.get_type_num(dur)
187        )
188        self.note_head: int | Fraction
189        if type_number >= 4:
190            self.note_head = 4
191        else:
192            self.note_head = type_number
193        # dots
194        self.dots: int = dur.dots
195        # graceness
196        self.graceType: str = ''
197        self.graceSlash: bool | None = False
198        if isinstance(dur, m21.duration.AppoggiaturaDuration):
199            self.graceType = 'acc'
200            self.graceSlash = dur.slash
201        elif isinstance(dur, m21.duration.GraceDuration):
202            # might be accented or unaccented.  duration.slash isn't always reliable
203            # (historically), but we can use it as a fallback.
204            # Check duration.stealTimePrevious and duration.stealTimeFollowing first.
205            if dur.stealTimePrevious is not None:
206                self.graceType = 'unacc'
207            elif dur.stealTimeFollowing is not None:
208                self.graceType = 'acc'
209            elif dur.slash is True:
210                self.graceType = 'unacc'
211            elif dur.slash is False:
212                self.graceType = 'acc'
213            else:
214                # by default, GraceDuration with no other indications (slash is None)
215                # is assumed to be unaccented.
216                self.graceType = 'unacc'
217            self.graceSlash = dur.slash
218
219        # The following (articulations, expressions) only occur once per chord
220        # or standalone note, so we only want to annotate them once.  We annotate them
221        # on standalone notes (of course), and on the first note of a parent_chord.
222        self.articulations: list[str] = []
223        self.expressions: list[str] = []
224
225        if self.note_idx_in_chord is None or self.note_idx_in_chord == 0:
226            # articulations
227            if DetailLevel.includesArticulations(detail):
228                self.articulations = [
229                    M21Utils.articulation_to_string(a, detail) for a in carrier.articulations
230                ]
231                if self.articulations:
232                    self.articulations.sort()
233
234            if DetailLevel.includesOrnaments(detail):
235                # expressions (tremolo, arpeggio, textexp have their own detail bits, though)
236                for a in carrier.expressions:
237                    if not DetailLevel.includesTremolos(detail):
238                        if isinstance(a, m21.expressions.Tremolo):
239                            continue
240                    if not DetailLevel.includesArpeggios(detail):
241                        if isinstance(a, m21.expressions.ArpeggioMark):
242                            continue
243                    if not DetailLevel.includesDirections(detail):
244                        if isinstance(a, m21.expressions.TextExpression):
245                            continue
246                    self.expressions.append(
247                        M21Utils.expression_to_string(a, detail)
248                    )
249                if self.expressions:
250                    self.expressions.sort()
251
252        # precomputed/cached representations for faster comparison
253        self.precomputed_str: str = self.__str__()
254        self._cached_notation_size: int | None = None

Extend music21 GeneralNote with some precomputed, easily compared information about it.

Arguments:
  • general_note (music21.note.GeneralNote): The music21 note/chord/rest to extend.
  • gap_dur (OffsetQL): gap since end of last note (or since start of measure, if first note in measure). Usually zero.
  • enhanced_beam_list (list): A list of beaming information about this GeneralNote.
  • tuplet_list (list): A list of basic tuplet info about this GeneralNote.
  • tuplet_info (list): A list of detailed tuplet info about this GeneralNote.
  • detail (DetailLevel | int): What level of detail to use during the diff. Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently AllObjects), or any combination (with | or &~) of those or NotesAndRests, Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures, Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics, Style, Metadata, or Voicing.
general_note: int | str
is_in_chord: bool
note_idx_in_chord: int | None
gap_dur: float | fractions.Fraction
beamings: list[str]
tuplets: list[str]
tuplet_info: list[str]
note_offset: float | fractions.Fraction
note_dur_type: str
note_dur_dots: int
note_is_grace: bool
fullNameSuffix: str
styledict: dict
noteshape: str
noteheadFill: bool | None
noteheadParenthesis: bool
stemDirection: str
pitches: list[tuple[str, str, bool]]
note_head: int | fractions.Fraction
dots: int
graceType: str
graceSlash: bool | None
articulations: list[str]
expressions: list[str]
precomputed_str: str
def notation_size(self) -> int:
256    def notation_size(self) -> int:
257        """
258        Compute a measure of how many symbols are displayed in the score for this `AnnNote`.
259
260        Returns:
261            int: The notation size of the annotated note
262        """
263        if self._cached_notation_size is None:
264            size: int = 0
265            # add for the pitches
266            for pitch in self.pitches:
267                size += M21Utils.pitch_size(pitch)
268            # add for the notehead (quarter, half, semibreve, breve, etc)
269            size += 1
270            # add for the dots
271            size += self.dots * len(self.pitches)  # one dot for each note if it's a chord
272            # add for the beams/flags
273            size += len(self.beamings)
274            # add for the tuplets
275            size += len(self.tuplets)
276            size += len(self.tuplet_info)
277            # add for the articulations
278            size += len(self.articulations)
279            # add for the expressions
280            size += len(self.expressions)
281            # add 1 if it's a gracenote, and 1 more if there's a grace slash
282            if self.graceType:
283                size += 1
284                if self.graceSlash is True:
285                    size += 1
286            # add 1 for abnormal note shape (diamond, etc)
287            if self.noteshape != 'normal':
288                size += 1
289            # add 1 for abnormal note fill
290            if self.noteheadFill is not None:
291                size += 1
292            # add 1 if there's a parenthesis around the note
293            if self.noteheadParenthesis:
294                size += 1
295            # add 1 if stem direction is specified
296            if self.stemDirection != 'unspecified':
297                size += 1
298            # add 1 if there is an empty space before this note
299            if self.gap_dur != 0:
300                size += 1
301            # add 1 for any other style info (in future might count the style entries)
302            if self.styledict:
303                size += 1
304
305            self._cached_notation_size = size
306
307        return self._cached_notation_size

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

Returns:

int: The notation size of the annotated note

def get_identifying_string(self, name: str = '') -> str:
309    def get_identifying_string(self, name: str = "") -> str:
310        string: str = ""
311        if self.fullNameSuffix.endswith("rest"):
312            string = self.fullNameSuffix
313        elif self.fullNameSuffix.endswith("note"):
314            string = self.pitches[0][0]
315            if self.pitches[0][1] != "None":
316                string += " " + self.pitches[0][1]
317            string += " (" + self.fullNameSuffix + ")"
318        elif self.fullNameSuffix.endswith("chord"):
319            string = "["
320            for p in self.pitches:  # add for pitches
321                string += p[0]  # pitch name and octave
322                if p[1] != "None":
323                    string += " " + p[1]  # pitch accidental
324                string += ","
325            string = string[:-1]  # delete the last comma
326            string += "] (" + self.fullNameSuffix + ")"
327        return string
def readable_str(self, name: str = '', idx: int = 0, changedStr: str = '') -> str:
329    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
330        string: str = self.get_identifying_string(name)
331        if name == "pitch":
332            # this is only for "pitch", not for "" (pitches are in identifying string)
333            if self.fullNameSuffix.endswith("chord"):
334                string += f", pitch[{idx}]={self.pitches[idx][0]}"
335            return string
336
337        if name == "accid":
338            # this is only for "accid" (indexed in a chord), not for "", or for "accid" on a note
339            # (accidental is in identifying string)
340            if self.fullNameSuffix.endswith("chord"):
341                string += f", accid[{idx}]={self.pitches[idx][1]}"
342            return string
343
344        if name == "head":
345            # this is only for "head", not for "" (head is implied by identifying string)
346            if self.note_head == 4:
347                string += ", head=normal"
348            else:
349                string += f", head={m21.duration.typeFromNumDict[float(self.note_head)]}"
350            if name:
351                return string
352
353        if name == "dots":
354            # this is only for "dots", not for "" (dots is in identifying string)
355            string += f", dots={self.dots}"
356            return string
357
358        if not name or name == "flagsbeams":
359            numBeams: int = len(self.beamings)
360            # Flags are implied by identifying string, so do not belong when name=="".
361            # And "no beams" is boring for name=="".  Non-zero beams, though, we always
362            # want to see.
363            if numBeams == 0:
364                if name:
365                    string += ", no flags/beams"
366                    return string
367            elif all(b == "partial" for b in self.beamings):
368                if name:
369                    if numBeams == 1:
370                        string += f", {numBeams} flag"
371                    else:
372                        string += f", {numBeams} flags"
373                    return string
374            else:
375                # it's beams, not flags
376                if numBeams == 1:
377                    string += f", {numBeams} beam="
378                else:
379                    string += f", {numBeams} beams=["
380                for i, b in enumerate(self.beamings):
381                    if i > 0:
382                        string += ", "
383                    string += b
384                if numBeams > 1:
385                    string += "]"
386                if name:
387                    return string
388
389        if not name or name == "tuplet":
390            if name or self.tuplets:
391                string += ", tuplets=["
392                for i, (tup, ti) in enumerate(zip(self.tuplets, self.tuplet_info)):
393                    if i > 0:
394                        string += ", "
395                    if ti != "":
396                        ti = "(" + ti + ")"
397                    string += tup + ti
398
399                string += "]"
400                if name:
401                    return string
402
403        if not name or name == "tie":
404            if self.pitches[idx][2]:
405                string += ", tied"
406            elif name:
407                string += ", not tied"
408            if name:
409                return string
410
411
412        if not name or name == "grace":
413            if not name:
414                if self.graceType:
415                    string += f", grace={self.graceType}"
416            else:
417                string += f", grace={self.graceType}"
418            if name:
419                return string
420
421        if not name or name == "graceslash":
422            if self.graceType:
423                if self.graceSlash:
424                    string += ", with grace slash"
425                else:
426                    string += ", with no grace slash"
427            if name:
428                return string
429
430        if not name or name == "noteshape":
431            if not name:
432                if self.noteshape != "normal":
433                    string += f", noteshape={self.noteshape}"
434            else:
435                string += f", noteshape={self.noteshape}"
436            if name:
437                return string
438
439        if not name or name == "notefill":
440            if not name:
441                if self.noteheadFill is not None:
442                    string += f", noteheadFill={self.noteheadFill}"
443            else:
444                string += f", noteheadFill={self.noteheadFill}"
445            if name:
446                return string
447
448        if not name or name == "noteparen":
449            if not name:
450                if self.noteheadParenthesis:
451                    string += f", noteheadParenthesis={self.noteheadParenthesis}"
452            else:
453                string += f", noteheadParenthesis={self.noteheadParenthesis}"
454            if name:
455                return string
456
457        if not name or name == "stemdir":
458            if not name:
459                if self.stemDirection != "unspecified":
460                    string += f", stemDirection={self.stemDirection}"
461            else:
462                string += f", stemDirection={self.stemDirection}"
463            if name:
464                return string
465
466        if not name or name == "spacebefore":
467            if not name:
468                if self.gap_dur != 0:
469                    string += f", spacebefore={self.gap_dur}"
470            else:
471                string += f", spacebefore={self.gap_dur}"
472            if name:
473                return string
474
475        if not name or name == "artic":
476            if name or self.articulations:
477                string += ", articulations=["
478                for i, artic in enumerate(self.articulations):
479                    if i > 0:
480                        string += ", "
481                    string += artic
482                string += "]"
483            if name:
484                return string
485
486        if not name or name == "expression":
487            if name or self.expressions:
488                string += ", expressions=["
489                for i, exp in enumerate(self.expressions):
490                    if i > 0:
491                        string += ", "
492                    string += exp
493                string += "]"
494            if name:
495                return string
496
497        if not name or name == "style":
498            if name or self.styledict:
499                allOfThem: bool = False
500                changedKeys: list[str] = []
501                if changedStr:
502                    changedKeys = changedStr.split(",")
503                else:
504                    changedKeys = [str(k) for k in self.styledict]
505                    allOfThem = True
506
507                if allOfThem:
508                    string += ", style={"
509                else:
510                    string += ", changedStyle={"
511
512                needsComma: bool = False
513                for i, k in enumerate(changedKeys):
514                    if k in self.styledict:
515                        if needsComma:
516                            string += ", "
517                        string += f"{k}:{self.styledict[k]}"
518                        needsComma = True
519                string += "}"
520            if name:
521                return string
522
523        return string
def get_note_ids(self) -> list[str | int]:
618    def get_note_ids(self) -> list[str | int]:
619        """
620        Computes a list of the GeneralNote ids for this `AnnNote`.  Since there
621        is only one GeneralNote here, this will always be a single-element list.
622
623        Returns:
624            [int]: A list containing the single GeneralNote id for this note.
625        """
626        return [self.general_note]

Computes a list of the GeneralNote ids for this AnnNote. Since there is only one GeneralNote here, this will always be a single-element list.

Returns:

[int]: A list containing the single GeneralNote id for this note.

class AnnExtra:
633class AnnExtra:
634    def __init__(
635        self,
636        extra: m21.base.Music21Object,
637        measure: m21.stream.Measure,
638        score: m21.stream.Score,
639        detail: DetailLevel | int = DetailLevel.Default
640    ) -> None:
641        """
642        Extend music21 non-GeneralNote and non-Stream objects with some precomputed,
643        easily compared information about it.
644
645        Examples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc.
646
647        Args:
648            extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream
649                object to extend.
650            measure (music21.stream.Measure): The music21 Measure the extra was found in.
651                If the extra was found in a Voice, this is the Measure that the Voice was
652                found in.
653            detail (DetailLevel | int): What level of detail to use during the diff.
654                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
655                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
656                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
657                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
658                Style, Metadata, or Voicing.
659        """
660        self.extra = extra.id
661        self.kind: str = M21Utils.extra_to_kind(extra)
662        self.styledict: dict = {}
663
664        # kind-specific fields (set to None if not relevant)
665
666        # content is a string that (if not None) should be counted as 1 symbol per character
667        # (e.g. "con fiero")
668        self.content: str | None = M21Utils.extra_to_string(extra, self.kind, detail)
669
670        # symbolic is a string that (if not None) should be counted as 1 symbol (e.g. "G2+8")
671        self.symbolic: str | None = M21Utils.extra_to_symbolic(extra, self.kind, detail)
672
673        # offset and/or duration are sometimes relevant
674        self.offset: OffsetQL | None = None
675        self.duration: OffsetQL | None = None
676        self.offset, self.duration = M21Utils.extra_to_offset_and_duration(
677            extra, self.kind, measure, score, detail
678        )
679
680        # infodict (kind-specific elements; each element is worth one musical symbol)
681        self.infodict: dict[str, str] = M21Utils.extra_to_infodict(extra, self.kind, detail)
682
683        # styledict
684        if DetailLevel.includesStyle(detail):
685            if not isinstance(extra, m21.harmony.ChordSymbol):
686                # We don't (yet) compare style of ChordSymbols, because Humdrum has no way (yet)
687                # of storing that.
688                if M21Utils.has_style(extra):
689                    # includes extra.placement if present
690
691                    # special case: MM with text='SMUFLNote = nnn" is being annotated as if there is
692                    # no text, so none of the text style stuff should be added.
693                    smuflTextSuppressed: bool = False
694                    if (isinstance(extra, m21.tempo.MetronomeMark)
695                            and not extra.textImplicit
696                            and M21Utils.parse_note_equal_num(extra.text) != (None, None)):
697                        smuflTextSuppressed = True
698
699                    self.styledict = M21Utils.obj_to_styledict(
700                        extra,
701                        detail,
702                        smuflTextSuppressed=smuflTextSuppressed
703                    )
704
705        # precomputed/cached representations for faster comparison
706        self.precomputed_str: str = self.__str__()
707        self._cached_notation_size: int | None = None
708
709    def notation_size(self) -> int:
710        """
711        Compute a measure of how many symbols are displayed in the score for this `AnnExtra`.
712
713        Returns:
714            int: The notation size of the annotated extra
715        """
716        if self._cached_notation_size is None:
717            cost: int = 0
718            if self.content is not None:
719                cost += len(self.content)
720            if self.symbolic is not None:
721                cost += 1
722            if self.duration is not None:
723                cost += 1
724            cost += len(self.infodict)
725            if self.styledict:
726                cost += 1  # someday we might add len(styledict) instead of 1
727            self._cached_notation_size = cost
728
729        return self._cached_notation_size
730
731    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
732        string: str = self.content or ""
733        if self.symbolic:
734            if string:
735                string += " "
736            string += self.symbolic
737        if self.infodict and name != "info":
738            for i, k in enumerate(self.infodict):
739                if string:
740                    string += " "
741                string += f"{k}:{self.infodict[k]}"
742
743        if name == "":
744            if self.duration is not None:
745                if string:
746                    string += " "
747                string += f"dur={M21Utils.ql_to_string(self.duration)}"
748            return string
749
750        if name == "content":
751            if self.content is None:
752                return ""
753            return self.content
754
755        if name == "symbolic":
756            if self.symbolic is None:
757                return ""
758            return self.symbolic
759
760        if name == "offset":
761            if self.offset is None:
762                return ""
763            if string:
764                string += " "
765            string += f"offset={M21Utils.ql_to_string(self.offset)}"
766            return string
767
768        if name == "duration":
769            if self.duration is None:
770                return ""
771            if string:
772                string += " "
773            string += f"dur={M21Utils.ql_to_string(self.duration)}"
774            return string
775
776        if name == "info":
777            changedKeys: list[str] = changedStr.split(',')
778            if not changedKeys:
779                if string:
780                    string += " "
781                string += "changedInfo={}"
782                return string
783
784            if string:
785                string += " "
786            string += "changedInfo={"
787
788            needsComma: bool = False
789            for i, k in enumerate(changedKeys):
790                if k in self.infodict:
791                    if needsComma:
792                        string += ", "
793                    string += f"{k}:{self.infodict[k]}"
794                    needsComma = True
795            string += "}"
796            return string
797
798        if name == "style":
799            changedKeys = changedStr.split(',')
800            if not changedKeys:
801                if string:
802                    string += " "
803                string += "changedStyle={}"
804                return string
805
806            if string:
807                string += " "
808            string += "changedStyle={"
809
810            needsComma = False
811            for i, k in enumerate(changedKeys):
812                if k in self.styledict:
813                    if needsComma:
814                        string += ", "
815                    string += f"{k}:{self.styledict[k]}"
816                    needsComma = True
817            string += "}"
818            return string
819
820        return ""  # should never get here
821
822    def __repr__(self) -> str:
823        # must include a unique id for memoization!
824        # we use the music21 id of the extra.
825        output: str = f"Extra({self.extra}):"
826        output += str(self)
827        return output
828
829    def __str__(self) -> str:
830        """
831        Returns:
832            str: the compared representation of the AnnExtra. Does not consider music21 id.
833        """
834        string = f'{self.kind}'
835        if self.content:
836            string += f',content={self.content}'
837        if self.symbolic:
838            string += f',symbol={self.symbolic}'
839        if self.offset is not None:
840            string += f',off={self.offset}'
841        if self.duration is not None:
842            string += f',dur={self.duration}'
843        # then any info fields
844        if self.infodict:
845            string += ',info:'
846        for k, v in self.infodict.items():
847            string += f',{k}={v}'
848        # and then any style fields
849        if self.styledict:
850            string += ',style:'
851        for k, v in self.styledict.items():
852            string += f',{k}={v}'
853        return string
854
855    def __eq__(self, other) -> bool:
856        # equality does not consider the MEI id!
857        return self.precomputed_str == other.precomputed_str
AnnExtra( extra: music21.base.Music21Object, measure: music21.stream.base.Measure, score: music21.stream.base.Score, detail: musicdiff.detaillevel.DetailLevel | int = <DetailLevel.AllObjects: 32767>)
634    def __init__(
635        self,
636        extra: m21.base.Music21Object,
637        measure: m21.stream.Measure,
638        score: m21.stream.Score,
639        detail: DetailLevel | int = DetailLevel.Default
640    ) -> None:
641        """
642        Extend music21 non-GeneralNote and non-Stream objects with some precomputed,
643        easily compared information about it.
644
645        Examples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc.
646
647        Args:
648            extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream
649                object to extend.
650            measure (music21.stream.Measure): The music21 Measure the extra was found in.
651                If the extra was found in a Voice, this is the Measure that the Voice was
652                found in.
653            detail (DetailLevel | int): What level of detail to use during the diff.
654                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
655                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
656                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
657                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
658                Style, Metadata, or Voicing.
659        """
660        self.extra = extra.id
661        self.kind: str = M21Utils.extra_to_kind(extra)
662        self.styledict: dict = {}
663
664        # kind-specific fields (set to None if not relevant)
665
666        # content is a string that (if not None) should be counted as 1 symbol per character
667        # (e.g. "con fiero")
668        self.content: str | None = M21Utils.extra_to_string(extra, self.kind, detail)
669
670        # symbolic is a string that (if not None) should be counted as 1 symbol (e.g. "G2+8")
671        self.symbolic: str | None = M21Utils.extra_to_symbolic(extra, self.kind, detail)
672
673        # offset and/or duration are sometimes relevant
674        self.offset: OffsetQL | None = None
675        self.duration: OffsetQL | None = None
676        self.offset, self.duration = M21Utils.extra_to_offset_and_duration(
677            extra, self.kind, measure, score, detail
678        )
679
680        # infodict (kind-specific elements; each element is worth one musical symbol)
681        self.infodict: dict[str, str] = M21Utils.extra_to_infodict(extra, self.kind, detail)
682
683        # styledict
684        if DetailLevel.includesStyle(detail):
685            if not isinstance(extra, m21.harmony.ChordSymbol):
686                # We don't (yet) compare style of ChordSymbols, because Humdrum has no way (yet)
687                # of storing that.
688                if M21Utils.has_style(extra):
689                    # includes extra.placement if present
690
691                    # special case: MM with text='SMUFLNote = nnn" is being annotated as if there is
692                    # no text, so none of the text style stuff should be added.
693                    smuflTextSuppressed: bool = False
694                    if (isinstance(extra, m21.tempo.MetronomeMark)
695                            and not extra.textImplicit
696                            and M21Utils.parse_note_equal_num(extra.text) != (None, None)):
697                        smuflTextSuppressed = True
698
699                    self.styledict = M21Utils.obj_to_styledict(
700                        extra,
701                        detail,
702                        smuflTextSuppressed=smuflTextSuppressed
703                    )
704
705        # precomputed/cached representations for faster comparison
706        self.precomputed_str: str = self.__str__()
707        self._cached_notation_size: int | None = None

Extend music21 non-GeneralNote and non-Stream objects with some precomputed, easily compared information about it.

Examples: TextExpression, Dynamic, Clef, Key, TimeSignature, MetronomeMark, etc.

Arguments:
  • extra (music21.base.Music21Object): The music21 non-GeneralNote/non-Stream object to extend.
  • measure (music21.stream.Measure): The music21 Measure the extra was found in. If the extra was found in a Voice, this is the Measure that the Voice was found in.
  • detail (DetailLevel | int): What level of detail to use during the diff. Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently AllObjects), or any combination (with | or &~) of those or NotesAndRests, Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures, Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics, Style, Metadata, or Voicing.
extra
kind: str
styledict: dict
content: str | None
symbolic: str | None
offset: float | fractions.Fraction | None
duration: float | fractions.Fraction | None
infodict: dict[str, str]
precomputed_str: str
def notation_size(self) -> int:
709    def notation_size(self) -> int:
710        """
711        Compute a measure of how many symbols are displayed in the score for this `AnnExtra`.
712
713        Returns:
714            int: The notation size of the annotated extra
715        """
716        if self._cached_notation_size is None:
717            cost: int = 0
718            if self.content is not None:
719                cost += len(self.content)
720            if self.symbolic is not None:
721                cost += 1
722            if self.duration is not None:
723                cost += 1
724            cost += len(self.infodict)
725            if self.styledict:
726                cost += 1  # someday we might add len(styledict) instead of 1
727            self._cached_notation_size = cost
728
729        return self._cached_notation_size

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

Returns:

int: The notation size of the annotated extra

def readable_str(self, name: str = '', idx: int = 0, changedStr: str = '') -> str:
731    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
732        string: str = self.content or ""
733        if self.symbolic:
734            if string:
735                string += " "
736            string += self.symbolic
737        if self.infodict and name != "info":
738            for i, k in enumerate(self.infodict):
739                if string:
740                    string += " "
741                string += f"{k}:{self.infodict[k]}"
742
743        if name == "":
744            if self.duration is not None:
745                if string:
746                    string += " "
747                string += f"dur={M21Utils.ql_to_string(self.duration)}"
748            return string
749
750        if name == "content":
751            if self.content is None:
752                return ""
753            return self.content
754
755        if name == "symbolic":
756            if self.symbolic is None:
757                return ""
758            return self.symbolic
759
760        if name == "offset":
761            if self.offset is None:
762                return ""
763            if string:
764                string += " "
765            string += f"offset={M21Utils.ql_to_string(self.offset)}"
766            return string
767
768        if name == "duration":
769            if self.duration is None:
770                return ""
771            if string:
772                string += " "
773            string += f"dur={M21Utils.ql_to_string(self.duration)}"
774            return string
775
776        if name == "info":
777            changedKeys: list[str] = changedStr.split(',')
778            if not changedKeys:
779                if string:
780                    string += " "
781                string += "changedInfo={}"
782                return string
783
784            if string:
785                string += " "
786            string += "changedInfo={"
787
788            needsComma: bool = False
789            for i, k in enumerate(changedKeys):
790                if k in self.infodict:
791                    if needsComma:
792                        string += ", "
793                    string += f"{k}:{self.infodict[k]}"
794                    needsComma = True
795            string += "}"
796            return string
797
798        if name == "style":
799            changedKeys = changedStr.split(',')
800            if not changedKeys:
801                if string:
802                    string += " "
803                string += "changedStyle={}"
804                return string
805
806            if string:
807                string += " "
808            string += "changedStyle={"
809
810            needsComma = False
811            for i, k in enumerate(changedKeys):
812                if k in self.styledict:
813                    if needsComma:
814                        string += ", "
815                    string += f"{k}:{self.styledict[k]}"
816                    needsComma = True
817            string += "}"
818            return string
819
820        return ""  # should never get here
class AnnLyric:
860class AnnLyric:
861    def __init__(
862        self,
863        lyric_holder: m21.note.GeneralNote,  # note containing the lyric
864        lyric: m21.note.Lyric,  # the lyric itself
865        measure: m21.stream.Measure,
866        detail: DetailLevel | int = DetailLevel.Default
867    ) -> None:
868        """
869        Extend a lyric from a music21 GeneralNote with some precomputed, easily
870        compared information about it.
871
872        Args:
873            lyric_holder (music21.note.GeneralNote): The note/chord/rest containing the lyric.
874            lyric (music21.note.Lyric): The music21 Lyric object to extend.
875            measure (music21.stream.Measure): The music21 Measure the lyric was found in.
876                If the lyric was found in a Voice, this is the Measure that the lyric was
877                found in.
878            detail (DetailLevel | int): What level of detail to use during the diff.
879                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
880                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
881                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
882                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
883                Style, Metadata, or Voicing.
884        """
885        self.lyric_holder = lyric_holder.id
886
887        # for comparison: lyric, number, identifier, offset, styledict
888        self.lyric: str = ""
889        self.number: int = 0
890        self.identifier: str = ""
891        self.offset = lyric_holder.getOffsetInHierarchy(measure)
892        self.styledict: dict[str, str] = {}
893
894        # ignore .syllabic and .text, what is visible is .rawText (and there
895        # are several .syllabic/.text combos that create the same .rawText).
896        self.lyric = lyric.rawText
897
898        if lyric.number is not None:
899            self.number = lyric.number
900
901        if (lyric._identifier is not None
902                and lyric._identifier != lyric.number
903                and lyric._identifier != str(lyric.number)):
904            self.identifier = lyric._identifier
905
906        if DetailLevel.includesStyle(detail) and M21Utils.has_style(lyric):
907            self.styledict = M21Utils.obj_to_styledict(lyric, detail)
908            if self.styledict:
909                # sort styleDict before converting to string so we can compare strings
910                self.styledict = dict(sorted(self.styledict.items()))
911
912        # precomputed/cached representations for faster comparison
913        self.precomputed_str: str = self.__str__()
914        self._cached_notation_size: int | None = None
915
916    def notation_size(self) -> int:
917        """
918        Compute a measure of how many symbols are displayed in the score for this `AnnLyric`.
919
920        Returns:
921            int: The notation size of the annotated lyric
922        """
923        if self._cached_notation_size is None:
924            size: int = len(self.lyric)
925            size += 1  # for offset
926            if self.number:
927                size += 1
928            if self.identifier:
929                size += 1
930            if self.styledict:
931                size += 1  # maybe someday we'll count items in styledict?
932            self._cached_notation_size = size
933
934        return self._cached_notation_size
935
936    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
937        string: str = f'"{self.lyric}"'
938        if name == "":
939            if self.number is not None:
940                string += f", num={self.number}"
941            if self.identifier:  # not None and != ""
942                string += f", id={self.identifier}"
943            if self.styledict:
944                string += f" style={self.styledict}"
945            return string
946
947        if name == "rawtext":
948            return string
949
950        if name == "offset":
951            string += f" offset={M21Utils.ql_to_string(self.offset)}"
952            return string
953
954        if name == "num":
955            string += f", num={self.number}"
956            return string
957
958        if name == "id":
959            string += f", id={self.identifier}"
960            return string
961
962        if name == "style":
963            string += f" style={self.styledict}"
964            return string
965
966        return ""  # should never get here
967
968    def __repr__(self) -> str:
969        # must include a unique id for memoization!
970        # we use the music21 id of the general note
971        # that holds the lyric, plus the lyric
972        # number within that general note.
973        output: str = f"Lyric({self.lyric_holder}[{self.number}]):"
974        output += str(self)
975        return output
976
977    def __str__(self) -> str:
978        """
979        Returns:
980            str: the compared representation of the AnnLyric. Does not consider music21 id.
981        """
982        string = (
983            f"{self.lyric},num={self.number},id={self.identifier}"
984            + f",off={self.offset},style={self.styledict}"
985        )
986        return string
987
988    def __eq__(self, other) -> bool:
989        # equality does not consider the MEI id!
990        return self.precomputed_str == other.precomputed_str
AnnLyric( lyric_holder: music21.note.GeneralNote, lyric: music21.note.Lyric, measure: music21.stream.base.Measure, detail: musicdiff.detaillevel.DetailLevel | int = <DetailLevel.AllObjects: 32767>)
861    def __init__(
862        self,
863        lyric_holder: m21.note.GeneralNote,  # note containing the lyric
864        lyric: m21.note.Lyric,  # the lyric itself
865        measure: m21.stream.Measure,
866        detail: DetailLevel | int = DetailLevel.Default
867    ) -> None:
868        """
869        Extend a lyric from a music21 GeneralNote with some precomputed, easily
870        compared information about it.
871
872        Args:
873            lyric_holder (music21.note.GeneralNote): The note/chord/rest containing the lyric.
874            lyric (music21.note.Lyric): The music21 Lyric object to extend.
875            measure (music21.stream.Measure): The music21 Measure the lyric was found in.
876                If the lyric was found in a Voice, this is the Measure that the lyric was
877                found in.
878            detail (DetailLevel | int): What level of detail to use during the diff.
879                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
880                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
881                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
882                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
883                Style, Metadata, or Voicing.
884        """
885        self.lyric_holder = lyric_holder.id
886
887        # for comparison: lyric, number, identifier, offset, styledict
888        self.lyric: str = ""
889        self.number: int = 0
890        self.identifier: str = ""
891        self.offset = lyric_holder.getOffsetInHierarchy(measure)
892        self.styledict: dict[str, str] = {}
893
894        # ignore .syllabic and .text, what is visible is .rawText (and there
895        # are several .syllabic/.text combos that create the same .rawText).
896        self.lyric = lyric.rawText
897
898        if lyric.number is not None:
899            self.number = lyric.number
900
901        if (lyric._identifier is not None
902                and lyric._identifier != lyric.number
903                and lyric._identifier != str(lyric.number)):
904            self.identifier = lyric._identifier
905
906        if DetailLevel.includesStyle(detail) and M21Utils.has_style(lyric):
907            self.styledict = M21Utils.obj_to_styledict(lyric, detail)
908            if self.styledict:
909                # sort styleDict before converting to string so we can compare strings
910                self.styledict = dict(sorted(self.styledict.items()))
911
912        # precomputed/cached representations for faster comparison
913        self.precomputed_str: str = self.__str__()
914        self._cached_notation_size: int | None = None

Extend a lyric from a music21 GeneralNote with some precomputed, easily compared information about it.

Arguments:
  • lyric_holder (music21.note.GeneralNote): The note/chord/rest containing the lyric.
  • lyric (music21.note.Lyric): The music21 Lyric object to extend.
  • measure (music21.stream.Measure): The music21 Measure the lyric was found in. If the lyric was found in a Voice, this is the Measure that the lyric was found in.
  • detail (DetailLevel | int): What level of detail to use during the diff. Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently AllObjects), or any combination (with | or &~) of those or NotesAndRests, Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures, Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics, Style, Metadata, or Voicing.
lyric_holder
lyric: str
number: int
identifier: str
offset
styledict: dict[str, str]
precomputed_str: str
def notation_size(self) -> int:
916    def notation_size(self) -> int:
917        """
918        Compute a measure of how many symbols are displayed in the score for this `AnnLyric`.
919
920        Returns:
921            int: The notation size of the annotated lyric
922        """
923        if self._cached_notation_size is None:
924            size: int = len(self.lyric)
925            size += 1  # for offset
926            if self.number:
927                size += 1
928            if self.identifier:
929                size += 1
930            if self.styledict:
931                size += 1  # maybe someday we'll count items in styledict?
932            self._cached_notation_size = size
933
934        return self._cached_notation_size

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

Returns:

int: The notation size of the annotated lyric

def readable_str(self, name: str = '', idx: int = 0, changedStr: str = '') -> str:
936    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
937        string: str = f'"{self.lyric}"'
938        if name == "":
939            if self.number is not None:
940                string += f", num={self.number}"
941            if self.identifier:  # not None and != ""
942                string += f", id={self.identifier}"
943            if self.styledict:
944                string += f" style={self.styledict}"
945            return string
946
947        if name == "rawtext":
948            return string
949
950        if name == "offset":
951            string += f" offset={M21Utils.ql_to_string(self.offset)}"
952            return string
953
954        if name == "num":
955            string += f", num={self.number}"
956            return string
957
958        if name == "id":
959            string += f", id={self.identifier}"
960            return string
961
962        if name == "style":
963            string += f" style={self.styledict}"
964            return string
965
966        return ""  # should never get here
class AnnVoice:
 993class AnnVoice:
 994    def __init__(
 995        self,
 996        voice: m21.stream.Voice | m21.stream.Measure,
 997        enclosingMeasure: m21.stream.Measure,
 998        detail: DetailLevel | int = DetailLevel.Default
 999    ) -> None:
1000        """
1001        Extend music21 Voice with some precomputed, easily compared information about it.
1002        Only ever called if detail includes Voicing.
1003
1004        Args:
1005            voice (music21.stream.Voice or Measure): The music21 voice to extend. This
1006                can be a Measure, but only if it contains no Voices.
1007            detail (DetailLevel | int): What level of detail to use during the diff.
1008                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1009                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1010                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1011                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1012                Style, Metadata, or Voicing.
1013        """
1014        self.voice: int | str = voice.id
1015        note_list: list[m21.note.GeneralNote] = []
1016
1017        if DetailLevel.includesNotesAndRests(detail):
1018            note_list = M21Utils.get_notes_and_gracenotes(voice)
1019
1020        self.en_beam_list: list[list[str]] = []
1021        self.tuplet_list: list[list[str]] = []
1022        self.tuplet_info: list[list[str]] = []
1023        self.annot_notes: list[AnnNote] = []
1024
1025        if note_list:
1026            self.en_beam_list = M21Utils.get_enhance_beamings(
1027                note_list,
1028                detail
1029            )  # beams ("partial" can mean partial beam or just a flag)
1030            self.tuplet_list = M21Utils.get_tuplets_type(
1031                note_list
1032            )  # corrected tuplets (with "start" and "continue")
1033            self.tuplet_info = M21Utils.get_tuplets_info(note_list)
1034            # create a list of notes with beaming and tuplets information attached
1035            self.annot_notes = []
1036            for i, n in enumerate(note_list):
1037                expectedOffsetInMeas: OffsetQL = 0
1038                if i > 0:
1039                    prevNoteStart: OffsetQL = (
1040                        note_list[i - 1].getOffsetInHierarchy(enclosingMeasure)
1041                    )
1042                    prevNoteDurQL: OffsetQL = (
1043                        note_list[i - 1].duration.quarterLength
1044                    )
1045                    expectedOffsetInMeas = opFrac(prevNoteStart + prevNoteDurQL)
1046
1047                gapDurQL: OffsetQL = (
1048                    n.getOffsetInHierarchy(enclosingMeasure) - expectedOffsetInMeas
1049                )
1050                self.annot_notes.append(
1051                    AnnNote(
1052                        n,
1053                        gapDurQL,
1054                        self.en_beam_list[i],
1055                        self.tuplet_list[i],
1056                        self.tuplet_info[i],
1057                        detail=detail
1058                    )
1059                )
1060
1061        self.n_of_notes: int = len(self.annot_notes)
1062        self.precomputed_str: str = self.__str__()
1063        self._cached_notation_size: int | None = None
1064
1065    def __eq__(self, other) -> bool:
1066        # equality does not consider MEI id!
1067        if not isinstance(other, AnnVoice):
1068            return False
1069
1070        if len(self.annot_notes) != len(other.annot_notes):
1071            return False
1072
1073        return self.precomputed_str == other.precomputed_str
1074
1075    def notation_size(self) -> int:
1076        """
1077        Compute a measure of how many symbols are displayed in the score for this `AnnVoice`.
1078
1079        Returns:
1080            int: The notation size of the annotated voice
1081        """
1082        if self._cached_notation_size is None:
1083            self._cached_notation_size = sum([an.notation_size() for an in self.annot_notes])
1084        return self._cached_notation_size
1085
1086    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1087        string: str = "["
1088        for an in self.annot_notes:
1089            string += an.readable_str()
1090            string += ","
1091
1092        if string[-1] == ",":
1093            # delete the last comma
1094            string = string[:-1]
1095
1096        string += "]"
1097        return string
1098
1099    def __repr__(self) -> str:
1100        # must include a unique id for memoization!
1101        # we use the music21 id of the voice.
1102        string: str = f"Voice({self.voice}):"
1103        string += "["
1104        for an in self.annot_notes:
1105            string += repr(an)
1106            string += ","
1107
1108        if string[-1] == ",":
1109            # delete the last comma
1110            string = string[:-1]
1111
1112        string += "]"
1113        return string
1114
1115    def __str__(self) -> str:
1116        string = "["
1117        for an in self.annot_notes:
1118            string += str(an)
1119            string += ","
1120
1121        if string[-1] == ",":
1122            # delete the last comma
1123            string = string[:-1]
1124
1125        string += "]"
1126        return string
1127
1128    def get_note_ids(self) -> list[str | int]:
1129        """
1130        Computes a list of the GeneralNote ids for this `AnnVoice`.
1131
1132        Returns:
1133            [int]: A list containing the GeneralNote ids contained in this voice
1134        """
1135        return [an.general_note for an in self.annot_notes]
AnnVoice( voice: music21.stream.base.Voice | music21.stream.base.Measure, enclosingMeasure: music21.stream.base.Measure, detail: musicdiff.detaillevel.DetailLevel | int = <DetailLevel.AllObjects: 32767>)
 994    def __init__(
 995        self,
 996        voice: m21.stream.Voice | m21.stream.Measure,
 997        enclosingMeasure: m21.stream.Measure,
 998        detail: DetailLevel | int = DetailLevel.Default
 999    ) -> None:
1000        """
1001        Extend music21 Voice with some precomputed, easily compared information about it.
1002        Only ever called if detail includes Voicing.
1003
1004        Args:
1005            voice (music21.stream.Voice or Measure): The music21 voice to extend. This
1006                can be a Measure, but only if it contains no Voices.
1007            detail (DetailLevel | int): What level of detail to use during the diff.
1008                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1009                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1010                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1011                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1012                Style, Metadata, or Voicing.
1013        """
1014        self.voice: int | str = voice.id
1015        note_list: list[m21.note.GeneralNote] = []
1016
1017        if DetailLevel.includesNotesAndRests(detail):
1018            note_list = M21Utils.get_notes_and_gracenotes(voice)
1019
1020        self.en_beam_list: list[list[str]] = []
1021        self.tuplet_list: list[list[str]] = []
1022        self.tuplet_info: list[list[str]] = []
1023        self.annot_notes: list[AnnNote] = []
1024
1025        if note_list:
1026            self.en_beam_list = M21Utils.get_enhance_beamings(
1027                note_list,
1028                detail
1029            )  # beams ("partial" can mean partial beam or just a flag)
1030            self.tuplet_list = M21Utils.get_tuplets_type(
1031                note_list
1032            )  # corrected tuplets (with "start" and "continue")
1033            self.tuplet_info = M21Utils.get_tuplets_info(note_list)
1034            # create a list of notes with beaming and tuplets information attached
1035            self.annot_notes = []
1036            for i, n in enumerate(note_list):
1037                expectedOffsetInMeas: OffsetQL = 0
1038                if i > 0:
1039                    prevNoteStart: OffsetQL = (
1040                        note_list[i - 1].getOffsetInHierarchy(enclosingMeasure)
1041                    )
1042                    prevNoteDurQL: OffsetQL = (
1043                        note_list[i - 1].duration.quarterLength
1044                    )
1045                    expectedOffsetInMeas = opFrac(prevNoteStart + prevNoteDurQL)
1046
1047                gapDurQL: OffsetQL = (
1048                    n.getOffsetInHierarchy(enclosingMeasure) - expectedOffsetInMeas
1049                )
1050                self.annot_notes.append(
1051                    AnnNote(
1052                        n,
1053                        gapDurQL,
1054                        self.en_beam_list[i],
1055                        self.tuplet_list[i],
1056                        self.tuplet_info[i],
1057                        detail=detail
1058                    )
1059                )
1060
1061        self.n_of_notes: int = len(self.annot_notes)
1062        self.precomputed_str: str = self.__str__()
1063        self._cached_notation_size: int | None = None

Extend music21 Voice with some precomputed, easily compared information about it. Only ever called if detail includes Voicing.

Arguments:
  • voice (music21.stream.Voice or Measure): The music21 voice to extend. This can be a Measure, but only if it contains no Voices.
  • detail (DetailLevel | int): What level of detail to use during the diff. Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently AllObjects), or any combination (with | or &~) of those or NotesAndRests, Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures, Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics, Style, Metadata, or Voicing.
voice: int | str
en_beam_list: list[list[str]]
tuplet_list: list[list[str]]
tuplet_info: list[list[str]]
annot_notes: list[AnnNote]
n_of_notes: int
precomputed_str: str
def notation_size(self) -> int:
1075    def notation_size(self) -> int:
1076        """
1077        Compute a measure of how many symbols are displayed in the score for this `AnnVoice`.
1078
1079        Returns:
1080            int: The notation size of the annotated voice
1081        """
1082        if self._cached_notation_size is None:
1083            self._cached_notation_size = sum([an.notation_size() for an in self.annot_notes])
1084        return self._cached_notation_size

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

Returns:

int: The notation size of the annotated voice

def readable_str(self, name: str = '', idx: int = 0, changedStr: str = '') -> str:
1086    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1087        string: str = "["
1088        for an in self.annot_notes:
1089            string += an.readable_str()
1090            string += ","
1091
1092        if string[-1] == ",":
1093            # delete the last comma
1094            string = string[:-1]
1095
1096        string += "]"
1097        return string
def get_note_ids(self) -> list[str | int]:
1128    def get_note_ids(self) -> list[str | int]:
1129        """
1130        Computes a list of the GeneralNote ids for this `AnnVoice`.
1131
1132        Returns:
1133            [int]: A list containing the GeneralNote ids contained in this voice
1134        """
1135        return [an.general_note for an in self.annot_notes]

Computes a list of the GeneralNote ids for this AnnVoice.

Returns:

[int]: A list containing the GeneralNote ids contained in this voice

class AnnMeasure:
1138class AnnMeasure:
1139    def __init__(
1140        self,
1141        measure: m21.stream.Measure,
1142        part: m21.stream.Part,
1143        score: m21.stream.Score,
1144        spannerBundle: m21.spanner.SpannerBundle,
1145        detail: DetailLevel | int = DetailLevel.Default
1146    ) -> None:
1147        """
1148        Extend music21 Measure with some precomputed, easily compared information about it.
1149
1150        Args:
1151            measure (music21.stream.Measure): The music21 Measure to extend.
1152            part (music21.stream.Part): the enclosing music21 Part
1153            score (music21.stream.Score): the enclosing music21 Score.
1154            spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners
1155                in the score.
1156            detail (DetailLevel | int): What level of detail to use during the diff.
1157                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1158                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1159                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1160                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1161                Style, Metadata, or Voicing.
1162        """
1163        self.measure: int | str = measure.id
1164        self.includes_voicing: bool = DetailLevel.includesVoicing(detail)
1165        self.n_of_elements: int = 0
1166
1167        # for text output only (see self.readable_str())
1168        self.measureNumber: str = M21Utils.get_measure_number_with_suffix(measure, part)
1169        # if self.measureNumber == 135:
1170        #     print('135')
1171
1172        if self.includes_voicing:
1173            # we make an AnnVoice for each voice in the measure
1174            self.voices_list: list[AnnVoice] = []
1175            if len(measure.voices) == 0:
1176                # there is a single AnnVoice (i.e. in the music21 Measure there are no voices)
1177                ann_voice = AnnVoice(measure, measure, detail)
1178                if ann_voice.n_of_notes > 0:
1179                    self.voices_list.append(ann_voice)
1180            else:  # there are multiple voices (or an array with just one voice)
1181                for voice in measure.voices:
1182                    ann_voice = AnnVoice(voice, measure, detail)
1183                    if ann_voice.n_of_notes > 0:
1184                        self.voices_list.append(ann_voice)
1185            self.n_of_elements = len(self.voices_list)
1186        else:
1187            # we pull up all the notes in all the voices (and split any chords into
1188            # individual notes)
1189            self.annot_notes: list[AnnNote] = []
1190
1191            note_list: list[m21.note.GeneralNote] = []
1192            if DetailLevel.includesNotesAndRests(detail):
1193                note_list = M21Utils.get_notes_and_gracenotes(measure, recurse=True)
1194
1195            if note_list:
1196                en_beam_list = M21Utils.get_enhance_beamings(
1197                    note_list,
1198                    detail
1199                )  # beams ("partial" can mean partial beam or just a flag)
1200                tuplet_list = M21Utils.get_tuplets_type(
1201                    note_list
1202                )  # corrected tuplets (with "start" and "continue")
1203                tuplet_info = M21Utils.get_tuplets_info(note_list)
1204
1205                # create a list of notes with beaming and tuplets information attached
1206                self.annot_notes = []
1207                for i, n in enumerate(note_list):
1208                    if isinstance(n, m21.chord.ChordBase):
1209                        if isinstance(n, m21.chord.Chord):
1210                            n.sortDiatonicAscending(inPlace=True)
1211                        chord_offset: OffsetQL = n.getOffsetInHierarchy(measure)
1212                        for n1 in n.notes:
1213                            self.annot_notes.append(
1214                                AnnNote(
1215                                    n1,
1216                                    0.,
1217                                    en_beam_list[i],
1218                                    tuplet_list[i],
1219                                    tuplet_info[i],
1220                                    parent_chord=n,
1221                                    chord_offset=chord_offset,
1222                                    detail=detail
1223                                )
1224                            )
1225                    else:
1226                        self.annot_notes.append(
1227                            AnnNote(
1228                                n,
1229                                0.,
1230                                en_beam_list[i],
1231                                tuplet_list[i],
1232                                tuplet_info[i],
1233                                detail=detail
1234                            )
1235                        )
1236
1237            self.n_of_elements = len(self.annot_notes)
1238
1239        self.extras_list: list[AnnExtra] = []
1240        for extra in M21Utils.get_extras(measure, part, score, spannerBundle, detail):
1241            self.extras_list.append(AnnExtra(extra, measure, score, detail))
1242        self.n_of_elements += len(self.extras_list)
1243
1244        # For correct comparison, sort the extras_list, so that any extras
1245        # that all have the same offset are sorted alphabetically.
1246        # 888 need to sort by class here?  Or not at all?
1247        self.extras_list.sort(key=lambda e: (e.kind, e.offset))
1248
1249        self.lyrics_list: list[AnnLyric] = []
1250        if DetailLevel.includesLyrics(detail):
1251            for lyric_holder in M21Utils.get_lyrics_holders(measure):
1252                for lyric in lyric_holder.lyrics:
1253                    if lyric.rawText:
1254                        # we ignore lyrics with no visible text
1255                        self.lyrics_list.append(AnnLyric(lyric_holder, lyric, measure, detail))
1256            self.n_of_elements += len(self.lyrics_list)
1257
1258            # For correct comparison, sort the lyrics_list, so that any lyrics
1259            # that all have the same offset are sorted by verse number.
1260            if self.lyrics_list:
1261                self.lyrics_list.sort(key=lambda lyr: (lyr.offset, lyr.number))
1262
1263        # precomputed/cached values to speed up the computation.
1264        # As they start to be long, they are hashed
1265        self.precomputed_str: int = hash(self.__str__())
1266        self.precomputed_repr: int = hash(self.__repr__())
1267        self._cached_notation_size: int | None = None
1268
1269    def __str__(self) -> str:
1270        output: str = ''
1271        if self.includes_voicing:
1272            output += str([str(v) for v in self.voices_list])
1273        else:
1274            output += str([str(n) for n in self.annot_notes])
1275        if self.extras_list:
1276            output += ' Extras:' + str([str(e) for e in self.extras_list])
1277        if self.lyrics_list:
1278            output += ' Lyrics:' + str([str(lyr) for lyr in self.lyrics_list])
1279        return output
1280
1281    def __repr__(self) -> str:
1282        # must include a unique id for memoization!
1283        # we use the music21 id of the measure.
1284        output: str = f"Measure({self.measure}):"
1285        if self.includes_voicing:
1286            output += str([repr(v) for v in self.voices_list])
1287        else:
1288            output += str([repr(n) for n in self.annot_notes])
1289        if self.extras_list:
1290            output += ' Extras:' + str([repr(e) for e in self.extras_list])
1291        if self.lyrics_list:
1292            output += ' Lyrics:' + str([repr(lyr) for lyr in self.lyrics_list])
1293        return output
1294
1295    def __eq__(self, other) -> bool:
1296        # equality does not consider MEI id!
1297        if not isinstance(other, AnnMeasure):
1298            return False
1299
1300        if self.includes_voicing and other.includes_voicing:
1301            if len(self.voices_list) != len(other.voices_list):
1302                return False
1303        elif not self.includes_voicing and not other.includes_voicing:
1304            if len(self.annot_notes) != len(other.annot_notes):
1305                return False
1306        else:
1307            # shouldn't ever happen, but I guess it could if the client does weird stuff
1308            return False
1309
1310        if len(self.extras_list) != len(other.extras_list):
1311            return False
1312
1313        if len(self.lyrics_list) != len(other.lyrics_list):
1314            return False
1315
1316        return self.precomputed_str == other.precomputed_str
1317        # return all([v[0] == v[1] for v in zip(self.voices_list, other.voices_list)])
1318
1319    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1320        string: str = f"measure {self.measureNumber}"
1321        return string
1322
1323    def notation_size(self) -> int:
1324        """
1325        Compute a measure of how many symbols are displayed in the score for this `AnnMeasure`.
1326
1327        Returns:
1328            int: The notation size of the annotated measure
1329        """
1330        if self._cached_notation_size is None:
1331            if self.includes_voicing:
1332                self._cached_notation_size = (
1333                    sum([v.notation_size() for v in self.voices_list])
1334                    + sum([e.notation_size() for e in self.extras_list])
1335                    + sum([lyr.notation_size() for lyr in self.lyrics_list])
1336                )
1337            else:
1338                self._cached_notation_size = (
1339                    sum([n.notation_size() for n in self.annot_notes])
1340                    + sum([e.notation_size() for e in self.extras_list])
1341                    + sum([lyr.notation_size() for lyr in self.lyrics_list])
1342                )
1343        return self._cached_notation_size
1344
1345    def get_note_ids(self) -> list[str | int]:
1346        """
1347        Computes a list of the GeneralNote ids for this `AnnMeasure`.
1348
1349        Returns:
1350            [int]: A list containing the GeneralNote ids contained in this measure
1351        """
1352        notes_id = []
1353        if self.includes_voicing:
1354            for v in self.voices_list:
1355                notes_id.extend(v.get_note_ids())
1356        else:
1357            for n in self.annot_notes:
1358                notes_id.extend(n.get_note_ids())
1359        return notes_id
AnnMeasure( measure: music21.stream.base.Measure, part: music21.stream.base.Part, score: music21.stream.base.Score, spannerBundle: music21.spanner.SpannerBundle, detail: musicdiff.detaillevel.DetailLevel | int = <DetailLevel.AllObjects: 32767>)
1139    def __init__(
1140        self,
1141        measure: m21.stream.Measure,
1142        part: m21.stream.Part,
1143        score: m21.stream.Score,
1144        spannerBundle: m21.spanner.SpannerBundle,
1145        detail: DetailLevel | int = DetailLevel.Default
1146    ) -> None:
1147        """
1148        Extend music21 Measure with some precomputed, easily compared information about it.
1149
1150        Args:
1151            measure (music21.stream.Measure): The music21 Measure to extend.
1152            part (music21.stream.Part): the enclosing music21 Part
1153            score (music21.stream.Score): the enclosing music21 Score.
1154            spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners
1155                in the score.
1156            detail (DetailLevel | int): What level of detail to use during the diff.
1157                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1158                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1159                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1160                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1161                Style, Metadata, or Voicing.
1162        """
1163        self.measure: int | str = measure.id
1164        self.includes_voicing: bool = DetailLevel.includesVoicing(detail)
1165        self.n_of_elements: int = 0
1166
1167        # for text output only (see self.readable_str())
1168        self.measureNumber: str = M21Utils.get_measure_number_with_suffix(measure, part)
1169        # if self.measureNumber == 135:
1170        #     print('135')
1171
1172        if self.includes_voicing:
1173            # we make an AnnVoice for each voice in the measure
1174            self.voices_list: list[AnnVoice] = []
1175            if len(measure.voices) == 0:
1176                # there is a single AnnVoice (i.e. in the music21 Measure there are no voices)
1177                ann_voice = AnnVoice(measure, measure, detail)
1178                if ann_voice.n_of_notes > 0:
1179                    self.voices_list.append(ann_voice)
1180            else:  # there are multiple voices (or an array with just one voice)
1181                for voice in measure.voices:
1182                    ann_voice = AnnVoice(voice, measure, detail)
1183                    if ann_voice.n_of_notes > 0:
1184                        self.voices_list.append(ann_voice)
1185            self.n_of_elements = len(self.voices_list)
1186        else:
1187            # we pull up all the notes in all the voices (and split any chords into
1188            # individual notes)
1189            self.annot_notes: list[AnnNote] = []
1190
1191            note_list: list[m21.note.GeneralNote] = []
1192            if DetailLevel.includesNotesAndRests(detail):
1193                note_list = M21Utils.get_notes_and_gracenotes(measure, recurse=True)
1194
1195            if note_list:
1196                en_beam_list = M21Utils.get_enhance_beamings(
1197                    note_list,
1198                    detail
1199                )  # beams ("partial" can mean partial beam or just a flag)
1200                tuplet_list = M21Utils.get_tuplets_type(
1201                    note_list
1202                )  # corrected tuplets (with "start" and "continue")
1203                tuplet_info = M21Utils.get_tuplets_info(note_list)
1204
1205                # create a list of notes with beaming and tuplets information attached
1206                self.annot_notes = []
1207                for i, n in enumerate(note_list):
1208                    if isinstance(n, m21.chord.ChordBase):
1209                        if isinstance(n, m21.chord.Chord):
1210                            n.sortDiatonicAscending(inPlace=True)
1211                        chord_offset: OffsetQL = n.getOffsetInHierarchy(measure)
1212                        for n1 in n.notes:
1213                            self.annot_notes.append(
1214                                AnnNote(
1215                                    n1,
1216                                    0.,
1217                                    en_beam_list[i],
1218                                    tuplet_list[i],
1219                                    tuplet_info[i],
1220                                    parent_chord=n,
1221                                    chord_offset=chord_offset,
1222                                    detail=detail
1223                                )
1224                            )
1225                    else:
1226                        self.annot_notes.append(
1227                            AnnNote(
1228                                n,
1229                                0.,
1230                                en_beam_list[i],
1231                                tuplet_list[i],
1232                                tuplet_info[i],
1233                                detail=detail
1234                            )
1235                        )
1236
1237            self.n_of_elements = len(self.annot_notes)
1238
1239        self.extras_list: list[AnnExtra] = []
1240        for extra in M21Utils.get_extras(measure, part, score, spannerBundle, detail):
1241            self.extras_list.append(AnnExtra(extra, measure, score, detail))
1242        self.n_of_elements += len(self.extras_list)
1243
1244        # For correct comparison, sort the extras_list, so that any extras
1245        # that all have the same offset are sorted alphabetically.
1246        # 888 need to sort by class here?  Or not at all?
1247        self.extras_list.sort(key=lambda e: (e.kind, e.offset))
1248
1249        self.lyrics_list: list[AnnLyric] = []
1250        if DetailLevel.includesLyrics(detail):
1251            for lyric_holder in M21Utils.get_lyrics_holders(measure):
1252                for lyric in lyric_holder.lyrics:
1253                    if lyric.rawText:
1254                        # we ignore lyrics with no visible text
1255                        self.lyrics_list.append(AnnLyric(lyric_holder, lyric, measure, detail))
1256            self.n_of_elements += len(self.lyrics_list)
1257
1258            # For correct comparison, sort the lyrics_list, so that any lyrics
1259            # that all have the same offset are sorted by verse number.
1260            if self.lyrics_list:
1261                self.lyrics_list.sort(key=lambda lyr: (lyr.offset, lyr.number))
1262
1263        # precomputed/cached values to speed up the computation.
1264        # As they start to be long, they are hashed
1265        self.precomputed_str: int = hash(self.__str__())
1266        self.precomputed_repr: int = hash(self.__repr__())
1267        self._cached_notation_size: int | None = None

Extend music21 Measure with some precomputed, easily compared information about it.

Arguments:
  • measure (music21.stream.Measure): The music21 Measure to extend.
  • part (music21.stream.Part): the enclosing music21 Part
  • score (music21.stream.Score): the enclosing music21 Score.
  • spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
  • detail (DetailLevel | int): What level of detail to use during the diff. Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently AllObjects), or any combination (with | or &~) of those or NotesAndRests, Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures, Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics, Style, Metadata, or Voicing.
measure: int | str
includes_voicing: bool
n_of_elements: int
measureNumber: str
extras_list: list[AnnExtra]
lyrics_list: list[AnnLyric]
precomputed_str: int
precomputed_repr: int
def readable_str(self, name: str = '', idx: int = 0, changedStr: str = '') -> str:
1319    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1320        string: str = f"measure {self.measureNumber}"
1321        return string
def notation_size(self) -> int:
1323    def notation_size(self) -> int:
1324        """
1325        Compute a measure of how many symbols are displayed in the score for this `AnnMeasure`.
1326
1327        Returns:
1328            int: The notation size of the annotated measure
1329        """
1330        if self._cached_notation_size is None:
1331            if self.includes_voicing:
1332                self._cached_notation_size = (
1333                    sum([v.notation_size() for v in self.voices_list])
1334                    + sum([e.notation_size() for e in self.extras_list])
1335                    + sum([lyr.notation_size() for lyr in self.lyrics_list])
1336                )
1337            else:
1338                self._cached_notation_size = (
1339                    sum([n.notation_size() for n in self.annot_notes])
1340                    + sum([e.notation_size() for e in self.extras_list])
1341                    + sum([lyr.notation_size() for lyr in self.lyrics_list])
1342                )
1343        return self._cached_notation_size

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

Returns:

int: The notation size of the annotated measure

def get_note_ids(self) -> list[str | int]:
1345    def get_note_ids(self) -> list[str | int]:
1346        """
1347        Computes a list of the GeneralNote ids for this `AnnMeasure`.
1348
1349        Returns:
1350            [int]: A list containing the GeneralNote ids contained in this measure
1351        """
1352        notes_id = []
1353        if self.includes_voicing:
1354            for v in self.voices_list:
1355                notes_id.extend(v.get_note_ids())
1356        else:
1357            for n in self.annot_notes:
1358                notes_id.extend(n.get_note_ids())
1359        return notes_id

Computes a list of the GeneralNote ids for this AnnMeasure.

Returns:

[int]: A list containing the GeneralNote ids contained in this measure

class AnnPart:
1362class AnnPart:
1363    def __init__(
1364        self,
1365        part: m21.stream.Part,
1366        score: m21.stream.Score,
1367        part_idx: int,
1368        spannerBundle: m21.spanner.SpannerBundle,
1369        detail: DetailLevel | int = DetailLevel.Default
1370    ):
1371        """
1372        Extend music21 Part/PartStaff with some precomputed, easily compared information about it.
1373
1374        Args:
1375            part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff
1376                to extend.
1377            score (music21.stream.Score): the enclosing music21 Score.
1378            spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in
1379                the score.
1380            detail (DetailLevel | int): What level of detail to use during the diff.
1381                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1382                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1383                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1384                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1385                Style, Metadata, or Voicing.
1386        """
1387        self.part: int | str = part.id
1388        self.part_idx: int = part_idx
1389        self.bar_list: list[AnnMeasure] = []
1390        for measure in part.getElementsByClass("Measure"):
1391            # create the bar objects
1392            ann_bar = AnnMeasure(measure, part, score, spannerBundle, detail)
1393            if ann_bar.n_of_elements > 0:
1394                self.bar_list.append(ann_bar)
1395        self.n_of_bars: int = len(self.bar_list)
1396        # Precomputed str to speed up the computation.
1397        # String itself is pretty long, so it is hashed
1398        self.precomputed_str: int = hash(self.__str__())
1399        self._cached_notation_size: int | None = None
1400
1401    def __str__(self) -> str:
1402        output: str = 'Part: '
1403        output += str([str(b) for b in self.bar_list])
1404        return output
1405
1406    def __eq__(self, other) -> bool:
1407        # equality does not consider MEI id!
1408        if not isinstance(other, AnnPart):
1409            return False
1410
1411        if len(self.bar_list) != len(other.bar_list):
1412            return False
1413
1414        return all(b[0] == b[1] for b in zip(self.bar_list, other.bar_list))
1415
1416    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1417        string: str = f"part {self.part_idx}"
1418        return string
1419
1420    def notation_size(self) -> int:
1421        """
1422        Compute a measure of how many symbols are displayed in the score for this `AnnPart`.
1423
1424        Returns:
1425            int: The notation size of the annotated part
1426        """
1427        if self._cached_notation_size is None:
1428            self._cached_notation_size = sum([b.notation_size() for b in self.bar_list])
1429        return self._cached_notation_size
1430
1431    def __repr__(self) -> str:
1432        # must include a unique id for memoization!
1433        # we use the music21 id of the part.
1434        output: str = f"Part({self.part}):"
1435        output += str([repr(b) for b in self.bar_list])
1436        return output
1437
1438    def get_note_ids(self) -> list[str | int]:
1439        """
1440        Computes a list of the GeneralNote ids for this `AnnPart`.
1441
1442        Returns:
1443            [int]: A list containing the GeneralNote ids contained in this part
1444        """
1445        notes_id = []
1446        for b in self.bar_list:
1447            notes_id.extend(b.get_note_ids())
1448        return notes_id
AnnPart( part: music21.stream.base.Part, score: music21.stream.base.Score, part_idx: int, spannerBundle: music21.spanner.SpannerBundle, detail: musicdiff.detaillevel.DetailLevel | int = <DetailLevel.AllObjects: 32767>)
1363    def __init__(
1364        self,
1365        part: m21.stream.Part,
1366        score: m21.stream.Score,
1367        part_idx: int,
1368        spannerBundle: m21.spanner.SpannerBundle,
1369        detail: DetailLevel | int = DetailLevel.Default
1370    ):
1371        """
1372        Extend music21 Part/PartStaff with some precomputed, easily compared information about it.
1373
1374        Args:
1375            part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff
1376                to extend.
1377            score (music21.stream.Score): the enclosing music21 Score.
1378            spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in
1379                the score.
1380            detail (DetailLevel | int): What level of detail to use during the diff.
1381                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1382                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1383                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1384                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1385                Style, Metadata, or Voicing.
1386        """
1387        self.part: int | str = part.id
1388        self.part_idx: int = part_idx
1389        self.bar_list: list[AnnMeasure] = []
1390        for measure in part.getElementsByClass("Measure"):
1391            # create the bar objects
1392            ann_bar = AnnMeasure(measure, part, score, spannerBundle, detail)
1393            if ann_bar.n_of_elements > 0:
1394                self.bar_list.append(ann_bar)
1395        self.n_of_bars: int = len(self.bar_list)
1396        # Precomputed str to speed up the computation.
1397        # String itself is pretty long, so it is hashed
1398        self.precomputed_str: int = hash(self.__str__())
1399        self._cached_notation_size: int | None = None

Extend music21 Part/PartStaff with some precomputed, easily compared information about it.

Arguments:
  • part (music21.stream.Part, music21.stream.PartStaff): The music21 Part/PartStaff to extend.
  • score (music21.stream.Score): the enclosing music21 Score.
  • spannerBundle (music21.spanner.SpannerBundle): a bundle of all the spanners in the score.
  • detail (DetailLevel | int): What level of detail to use during the diff. Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently AllObjects), or any combination (with | or &~) of those or NotesAndRests, Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures, Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics, Style, Metadata, or Voicing.
part: int | str
part_idx: int
bar_list: list[AnnMeasure]
n_of_bars: int
precomputed_str: int
def readable_str(self, name: str = '', idx: int = 0, changedStr: str = '') -> str:
1416    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1417        string: str = f"part {self.part_idx}"
1418        return string
def notation_size(self) -> int:
1420    def notation_size(self) -> int:
1421        """
1422        Compute a measure of how many symbols are displayed in the score for this `AnnPart`.
1423
1424        Returns:
1425            int: The notation size of the annotated part
1426        """
1427        if self._cached_notation_size is None:
1428            self._cached_notation_size = sum([b.notation_size() for b in self.bar_list])
1429        return self._cached_notation_size

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

Returns:

int: The notation size of the annotated part

def get_note_ids(self) -> list[str | int]:
1438    def get_note_ids(self) -> list[str | int]:
1439        """
1440        Computes a list of the GeneralNote ids for this `AnnPart`.
1441
1442        Returns:
1443            [int]: A list containing the GeneralNote ids contained in this part
1444        """
1445        notes_id = []
1446        for b in self.bar_list:
1447            notes_id.extend(b.get_note_ids())
1448        return notes_id

Computes a list of the GeneralNote ids for this AnnPart.

Returns:

[int]: A list containing the GeneralNote ids contained in this part

class AnnStaffGroup:
1451class AnnStaffGroup:
1452    def __init__(
1453        self,
1454        staff_group: m21.layout.StaffGroup,
1455        part_to_index: dict[m21.stream.Part, int],
1456        detail: DetailLevel | int = DetailLevel.Default
1457    ) -> None:
1458        """
1459        Take a StaffGroup and store it as an annotated object.
1460        """
1461        self.staff_group: int | str = staff_group.id
1462        self.name: str = staff_group.name or ''
1463        self.abbreviation: str = staff_group.abbreviation or ''
1464        self.symbol: str | None = None
1465        self.barTogether: bool | str | None = staff_group.barTogether
1466
1467        if DetailLevel.includesStyle(detail):
1468            # symbol (brace, bracket, line, etc) is considered to be style
1469            self.symbol = staff_group.symbol
1470
1471        self.part_indices: list[int] = []
1472        for part in staff_group:
1473            self.part_indices.append(part_to_index.get(part, -1))
1474
1475        # sort so simple list comparison can work
1476        self.part_indices.sort()
1477
1478        self.n_of_parts: int = len(self.part_indices)
1479
1480        # precomputed representations for faster comparison
1481        self.precomputed_str: str = self.__str__()
1482        self._cached_notation_size: int | None = None
1483
1484    def __str__(self) -> str:
1485        output: str = "StaffGroup"
1486        if self.name and self.abbreviation:
1487            output += f"({self.name},{self.abbreviation})"
1488        elif self.name:
1489            output += f"({self.name})"
1490        elif self.abbreviation:
1491            output += f"(,{self.abbreviation})"
1492        else:
1493            output += "(,)"
1494
1495        output += f", partIndices={self.part_indices}"
1496        if self.symbol is not None:
1497            output += f", symbol={self.symbol}"
1498        if self.barTogether is not None:
1499            output += f", barTogether={self.barTogether}"
1500        return output
1501
1502    def __eq__(self, other) -> bool:
1503        # equality does not consider MEI id (or MEI ids of parts included in the group)
1504        if not isinstance(other, AnnStaffGroup):
1505            return False
1506
1507        if self.name != other.name:
1508            return False
1509
1510        if self.abbreviation != other.abbreviation:
1511            return False
1512
1513        if self.symbol != other.symbol:
1514            return False
1515
1516        if self.barTogether != other.barTogether:
1517            return False
1518
1519        if self.n_of_parts != other.n_of_parts:
1520            # trying to avoid the more expensive part_indices array comparison
1521            return False
1522
1523        if self.part_indices != other.part_indices:
1524            return False
1525
1526        return True
1527
1528    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1529        string: str = f"StaffGroup{self.part_indices}"
1530        if name == "":
1531            return string
1532
1533        if name == "name":
1534            string += f" name={self.name}"
1535            return string
1536
1537        if name == "abbr":
1538            string += f" abbr={self.abbreviation}"
1539            return string
1540
1541        if name == "sym":
1542            string += f" sym={self.symbol}"
1543            return string
1544
1545        if name == "barline":
1546            string += f" barTogether={self.barTogether}"
1547            return string
1548
1549        if name == "parts":
1550            # main string already has parts in it
1551            return string
1552
1553        return ""
1554
1555    def notation_size(self) -> int:
1556        """
1557        Compute a measure of how many symbols are displayed in the score for this `AnnStaffGroup`.
1558
1559        Returns:
1560            int: The notation size of the annotated staff group
1561        """
1562        # There are 5 main visible things about a StaffGroup:
1563        #   name, abbreviation, symbol shape, barline type, and which staves it encloses
1564        if self._cached_notation_size is None:
1565            size: int = len(self.name)
1566            size += len(self.abbreviation)
1567            size += 1  # for symbol shape
1568            size += 1  # for barline type
1569            size += 1  # for lowest staff index (vertical start)
1570            size += 1  # for highest staff index (vertical height)
1571            self._cached_notation_size = size
1572        return self._cached_notation_size
1573
1574    def __repr__(self) -> str:
1575        # must include a unique id for memoization!
1576        # we use the music21 id of the staff group.
1577        output: str = f"StaffGroup({self.staff_group}):"
1578        output += f" name={self.name}, abbrev={self.abbreviation},"
1579        output += f" symbol={self.symbol}, barTogether={self.barTogether}"
1580        output += f", partIndices={self.part_indices}"
1581        return output
AnnStaffGroup( staff_group: music21.layout.StaffGroup, part_to_index: dict[music21.stream.base.Part, int], detail: musicdiff.detaillevel.DetailLevel | int = <DetailLevel.AllObjects: 32767>)
1452    def __init__(
1453        self,
1454        staff_group: m21.layout.StaffGroup,
1455        part_to_index: dict[m21.stream.Part, int],
1456        detail: DetailLevel | int = DetailLevel.Default
1457    ) -> None:
1458        """
1459        Take a StaffGroup and store it as an annotated object.
1460        """
1461        self.staff_group: int | str = staff_group.id
1462        self.name: str = staff_group.name or ''
1463        self.abbreviation: str = staff_group.abbreviation or ''
1464        self.symbol: str | None = None
1465        self.barTogether: bool | str | None = staff_group.barTogether
1466
1467        if DetailLevel.includesStyle(detail):
1468            # symbol (brace, bracket, line, etc) is considered to be style
1469            self.symbol = staff_group.symbol
1470
1471        self.part_indices: list[int] = []
1472        for part in staff_group:
1473            self.part_indices.append(part_to_index.get(part, -1))
1474
1475        # sort so simple list comparison can work
1476        self.part_indices.sort()
1477
1478        self.n_of_parts: int = len(self.part_indices)
1479
1480        # precomputed representations for faster comparison
1481        self.precomputed_str: str = self.__str__()
1482        self._cached_notation_size: int | None = None

Take a StaffGroup and store it as an annotated object.

staff_group: int | str
name: str
abbreviation: str
symbol: str | None
barTogether: bool | str | None
part_indices: list[int]
n_of_parts: int
precomputed_str: str
def readable_str(self, name: str = '', idx: int = 0, changedStr: str = '') -> str:
1528    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1529        string: str = f"StaffGroup{self.part_indices}"
1530        if name == "":
1531            return string
1532
1533        if name == "name":
1534            string += f" name={self.name}"
1535            return string
1536
1537        if name == "abbr":
1538            string += f" abbr={self.abbreviation}"
1539            return string
1540
1541        if name == "sym":
1542            string += f" sym={self.symbol}"
1543            return string
1544
1545        if name == "barline":
1546            string += f" barTogether={self.barTogether}"
1547            return string
1548
1549        if name == "parts":
1550            # main string already has parts in it
1551            return string
1552
1553        return ""
def notation_size(self) -> int:
1555    def notation_size(self) -> int:
1556        """
1557        Compute a measure of how many symbols are displayed in the score for this `AnnStaffGroup`.
1558
1559        Returns:
1560            int: The notation size of the annotated staff group
1561        """
1562        # There are 5 main visible things about a StaffGroup:
1563        #   name, abbreviation, symbol shape, barline type, and which staves it encloses
1564        if self._cached_notation_size is None:
1565            size: int = len(self.name)
1566            size += len(self.abbreviation)
1567            size += 1  # for symbol shape
1568            size += 1  # for barline type
1569            size += 1  # for lowest staff index (vertical start)
1570            size += 1  # for highest staff index (vertical height)
1571            self._cached_notation_size = size
1572        return self._cached_notation_size

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

Returns:

int: The notation size of the annotated staff group

class AnnMetadataItem:
1584class AnnMetadataItem:
1585    def __init__(
1586        self,
1587        key: str,
1588        value: t.Any
1589    ) -> None:
1590        # Normally this would be the id of the Music21Object, but we just have a key/value
1591        # pair, so we just make up an id, by using our own address.  In this case, we will
1592        # not be looking this id up in the score, but only using it as a memo-ization key.
1593        self.metadata_item = id(self)
1594        self.key = key
1595        if isinstance(value, m21.metadata.Text):
1596            # Create a string representing both the text and the language, but not isTranslated,
1597            # since isTranslated cannot be represented in many file formats.
1598            self.value = (
1599                self.make_value_string(value)
1600                + f'(language={value.language})'
1601            )
1602            if isinstance(value, m21.metadata.Copyright):
1603                self.value += f' role={value.role}'
1604        elif isinstance(value, m21.metadata.Contributor):
1605            # Create a string (same thing: value.name.isTranslated will differ randomly)
1606            # Currently I am also ignoring more than one name, and birth/death.
1607            if not value._names:
1608                # ignore this metadata item
1609                self.key = ''
1610                self.value = ''
1611                return
1612
1613            self.value = self.make_value_string(value)
1614            roleEmitted: bool = False
1615            if value.role:
1616                if value.role == 'poet':
1617                    # special case: many MusicXML files have the lyricist listed as the poet.
1618                    # We compare them as equivalent here.
1619                    lyr: str = 'lyricist'
1620                    self.key = lyr
1621                    self.value += f'(role={lyr}'
1622                else:
1623                    self.value += f'(role={value.role}'
1624                roleEmitted = True
1625            if value._names:
1626                if roleEmitted:
1627                    self.value += ', '
1628                self.value += f'language={value._names[0].language}'
1629            if roleEmitted:
1630                self.value += ')'
1631        else:
1632            # Date types
1633            self.value = str(value)
1634
1635        self._cached_notation_size: int | None = None
1636
1637    def __eq__(self, other) -> bool:
1638        if not isinstance(other, AnnMetadataItem):
1639            return False
1640
1641        if self.key != other.key:
1642            return False
1643
1644        if self.value != other.value:
1645            return False
1646
1647        return True
1648
1649    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1650        return str(self)
1651
1652    def __str__(self) -> str:
1653        return self.key + ':' + str(self.value)
1654
1655
1656    def __repr__(self) -> str:
1657        # must include a unique id for memoization!
1658        # We use id(self), because there is no music21 object here.
1659        output: str = f"MetadataItem({self.metadata_item}):"
1660        output += self.key + ':' + str(self.value)
1661        return output
1662
1663    def notation_size(self) -> int:
1664        """
1665        Compute a measure of how many symbols are displayed in the score for this `AnnMetadataItem`.
1666
1667        Returns:
1668            int: The notation size of the annotated metadata item
1669        """
1670        if self._cached_notation_size is None:
1671            size: int = len(self.key)
1672            size += len(self.value)
1673            self._cached_notation_size = size
1674        return self._cached_notation_size
1675
1676    def make_value_string(self, value: m21.metadata.Contributor | m21.metadata.Text) -> str:
1677        # Unescapes a bunch of stuff (and strips off leading/trailing whitespace)
1678        output: str = str(value)
1679        output = output.strip()
1680        output = html.unescape(output)
1681        return output
AnnMetadataItem(key: str, value: Any)
1585    def __init__(
1586        self,
1587        key: str,
1588        value: t.Any
1589    ) -> None:
1590        # Normally this would be the id of the Music21Object, but we just have a key/value
1591        # pair, so we just make up an id, by using our own address.  In this case, we will
1592        # not be looking this id up in the score, but only using it as a memo-ization key.
1593        self.metadata_item = id(self)
1594        self.key = key
1595        if isinstance(value, m21.metadata.Text):
1596            # Create a string representing both the text and the language, but not isTranslated,
1597            # since isTranslated cannot be represented in many file formats.
1598            self.value = (
1599                self.make_value_string(value)
1600                + f'(language={value.language})'
1601            )
1602            if isinstance(value, m21.metadata.Copyright):
1603                self.value += f' role={value.role}'
1604        elif isinstance(value, m21.metadata.Contributor):
1605            # Create a string (same thing: value.name.isTranslated will differ randomly)
1606            # Currently I am also ignoring more than one name, and birth/death.
1607            if not value._names:
1608                # ignore this metadata item
1609                self.key = ''
1610                self.value = ''
1611                return
1612
1613            self.value = self.make_value_string(value)
1614            roleEmitted: bool = False
1615            if value.role:
1616                if value.role == 'poet':
1617                    # special case: many MusicXML files have the lyricist listed as the poet.
1618                    # We compare them as equivalent here.
1619                    lyr: str = 'lyricist'
1620                    self.key = lyr
1621                    self.value += f'(role={lyr}'
1622                else:
1623                    self.value += f'(role={value.role}'
1624                roleEmitted = True
1625            if value._names:
1626                if roleEmitted:
1627                    self.value += ', '
1628                self.value += f'language={value._names[0].language}'
1629            if roleEmitted:
1630                self.value += ')'
1631        else:
1632            # Date types
1633            self.value = str(value)
1634
1635        self._cached_notation_size: int | None = None
metadata_item
key
def readable_str(self, name: str = '', idx: int = 0, changedStr: str = '') -> str:
1649    def readable_str(self, name: str = "", idx: int = 0, changedStr: str = "") -> str:
1650        return str(self)
def notation_size(self) -> int:
1663    def notation_size(self) -> int:
1664        """
1665        Compute a measure of how many symbols are displayed in the score for this `AnnMetadataItem`.
1666
1667        Returns:
1668            int: The notation size of the annotated metadata item
1669        """
1670        if self._cached_notation_size is None:
1671            size: int = len(self.key)
1672            size += len(self.value)
1673            self._cached_notation_size = size
1674        return self._cached_notation_size

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

Returns:

int: The notation size of the annotated metadata item

def make_value_string( self, value: music21.metadata.primitives.Contributor | music21.metadata.primitives.Text) -> str:
1676    def make_value_string(self, value: m21.metadata.Contributor | m21.metadata.Text) -> str:
1677        # Unescapes a bunch of stuff (and strips off leading/trailing whitespace)
1678        output: str = str(value)
1679        output = output.strip()
1680        output = html.unescape(output)
1681        return output
class AnnScore:
1684class AnnScore:
1685    def __init__(
1686        self,
1687        score: m21.stream.Score,
1688        detail: DetailLevel | int = DetailLevel.Default
1689    ) -> None:
1690        """
1691        Take a music21 score and store it as a sequence of Full Trees.
1692        The hierarchy is "score -> parts -> measures -> voices -> notes"
1693        Args:
1694            score (music21.stream.Score): The music21 score
1695            detail (DetailLevel | int): What level of detail to use during the diff.
1696                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1697                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1698                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1699                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1700                Style, Metadata, or Voicing.
1701        """
1702        self.score: int | str = score.id
1703        self.part_list: list[AnnPart] = []
1704        self.staff_group_list: list[AnnStaffGroup] = []
1705        self.metadata_items_list: list[AnnMetadataItem] = []
1706        self.num_syntax_errors_fixed: int = 0
1707
1708        if hasattr(score, "c21_syntax_errors_fixed"):
1709            self.num_syntax_errors_fixed = score.c21_syntax_errors_fixed  # type: ignore
1710
1711        spannerBundle: m21.spanner.SpannerBundle = score.spannerBundle
1712        part_to_index: dict[m21.stream.Part, int] = {}
1713
1714        # Before we start, transpose all notes to written pitch, both for transposing
1715        # instruments and Ottavas. Be careful to preserve accidental.displayStatus
1716        # during transposition, since we use that visibility indicator when comparing
1717        # accidentals.
1718        score.toWrittenPitch(inPlace=True, preserveAccidentalDisplay=True)
1719
1720        for idx, part in enumerate(score.parts):
1721            # create and add the AnnPart object to part_list
1722            # and to part_to_index dict
1723            part_to_index[part] = idx
1724            ann_part = AnnPart(part, score, idx, spannerBundle, detail)
1725            self.part_list.append(ann_part)
1726
1727        self.n_of_parts: int = len(self.part_list)
1728
1729        if DetailLevel.includesStaffDetails(detail):
1730            for staffGroup in score[m21.layout.StaffGroup]:
1731                # ignore any StaffGroup that contains all the parts, and has no symbol
1732                # and has no barthru (this is just a placeholder generated by some
1733                # file formats, and has the same meaning if it is missing).
1734                if len(staffGroup) == len(part_to_index):
1735                    if not staffGroup.symbol and not staffGroup.barTogether:
1736                        continue
1737
1738                ann_staff_group = AnnStaffGroup(staffGroup, part_to_index, detail)
1739                if ann_staff_group.n_of_parts > 0:
1740                    self.staff_group_list.append(ann_staff_group)
1741
1742            # now sort the staff_group_list in increasing order of first part index
1743            # (secondary sort in decreasing order of last part index)
1744            self.staff_group_list.sort(
1745                key=lambda each: (each.part_indices[0], -each.part_indices[-1])
1746            )
1747
1748        if DetailLevel.includesMetadata(detail) and score.metadata:
1749            # m21 metadata.all() can't sort primitives, so we'll have to sort by hand.
1750            # Note: we sort metadata_items_list after the fact, because sometimes
1751            # (e.g. otherContributor:poet) we substitute names (e.g. lyricist:)
1752            allItems: list[tuple[str, t.Any]] = list(
1753                score.metadata.all(returnPrimitives=True, returnSorted=False)
1754            )
1755            for key, value in allItems:
1756                if key in ('fileFormat', 'filePath', 'software'):
1757                    # Don't compare metadata items that are uninterestingly different.
1758                    continue
1759                if (key.startswith('raw:')
1760                        or key.startswith('meiraw:')
1761                        or key.startswith('humdrumraw:')):
1762                    # Don't compare verbatim/raw metadata ('meiraw:meihead',
1763                    # 'raw:freeform', 'humdrumraw:XXX'), it's often deleted
1764                    # when made obsolete by conversions/edits.
1765                    continue
1766                if key in ('humdrum:EMD', 'humdrum:EST', 'humdrum:VTS',
1767                        'humdrum:RLN', 'humdrum:PUB'):
1768                    # Don't compare metadata items that should never be transferred
1769                    # from one file to another.  'humdrum:EMD' is a modification
1770                    # description entry, humdrum:EST is "current encoding status"
1771                    # (i.e. complete or some value of not complete), 'humdrum:VTS'
1772                    # is a checksum of the Humdrum file, 'humdrum:RLN' is the
1773                    # extended ASCII encoding of the Humdrum file, 'humdrum:PUB'
1774                    # is the publication status of the file (published or not?).
1775                    continue
1776                ami: AnnMetadataItem = AnnMetadataItem(key, value)
1777                if ami.key and ami.value:
1778                    self.metadata_items_list.append(ami)
1779
1780            self.metadata_items_list.sort(key=lambda each: (each.key, str(each.value)))
1781
1782        # cached notation size
1783        self._cached_notation_size: int | None = None
1784
1785    def __eq__(self, other) -> bool:
1786        # equality does not consider MEI id!
1787        if not isinstance(other, AnnScore):
1788            return False
1789
1790        if len(self.part_list) != len(other.part_list):
1791            return False
1792
1793        return all(p[0] == p[1] for p in zip(self.part_list, other.part_list))
1794
1795    def notation_size(self) -> int:
1796        """
1797        Compute a measure of how many symbols are displayed in the score for this `AnnScore`.
1798
1799        Returns:
1800            int: The notation size of the annotated score
1801        """
1802        if self._cached_notation_size is None:
1803            size: int = sum([p.notation_size() for p in self.part_list])
1804            size += sum([sg.notation_size() for sg in self.staff_group_list])
1805            size += sum([md.notation_size() for md in self.metadata_items_list])
1806            self._cached_notation_size = size
1807        return self._cached_notation_size
1808
1809    def __repr__(self) -> str:
1810        # must include a unique id for memoization!
1811        # we use the music21 id of the score.
1812        output: str = f"Score({self.score}):"
1813        output += str(repr(p) for p in self.part_list)
1814        return output
1815
1816    def get_note_ids(self) -> list[str | int]:
1817        """
1818        Computes a list of the GeneralNote ids for this `AnnScore`.
1819
1820        Returns:
1821            [int]: A list containing the GeneralNote ids contained in this score
1822        """
1823        notes_id = []
1824        for p in self.part_list:
1825            notes_id.extend(p.get_note_ids())
1826        return notes_id
1827
1828    # return the sequences of measures for a specified part
1829    def _measures_from_part(self, part_number) -> list[AnnMeasure]:
1830        # only used by tests/test_scl.py
1831        if part_number not in range(0, len(self.part_list)):
1832            raise ValueError(
1833                f"parameter 'part_number' should be between 0 and {len(self.part_list) - 1}"
1834            )
1835        return self.part_list[part_number].bar_list
AnnScore( score: music21.stream.base.Score, detail: musicdiff.detaillevel.DetailLevel | int = <DetailLevel.AllObjects: 32767>)
1685    def __init__(
1686        self,
1687        score: m21.stream.Score,
1688        detail: DetailLevel | int = DetailLevel.Default
1689    ) -> None:
1690        """
1691        Take a music21 score and store it as a sequence of Full Trees.
1692        The hierarchy is "score -> parts -> measures -> voices -> notes"
1693        Args:
1694            score (music21.stream.Score): The music21 score
1695            detail (DetailLevel | int): What level of detail to use during the diff.
1696                Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently
1697                AllObjects), or any combination (with | or &~) of those or NotesAndRests,
1698                Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures,
1699                Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics,
1700                Style, Metadata, or Voicing.
1701        """
1702        self.score: int | str = score.id
1703        self.part_list: list[AnnPart] = []
1704        self.staff_group_list: list[AnnStaffGroup] = []
1705        self.metadata_items_list: list[AnnMetadataItem] = []
1706        self.num_syntax_errors_fixed: int = 0
1707
1708        if hasattr(score, "c21_syntax_errors_fixed"):
1709            self.num_syntax_errors_fixed = score.c21_syntax_errors_fixed  # type: ignore
1710
1711        spannerBundle: m21.spanner.SpannerBundle = score.spannerBundle
1712        part_to_index: dict[m21.stream.Part, int] = {}
1713
1714        # Before we start, transpose all notes to written pitch, both for transposing
1715        # instruments and Ottavas. Be careful to preserve accidental.displayStatus
1716        # during transposition, since we use that visibility indicator when comparing
1717        # accidentals.
1718        score.toWrittenPitch(inPlace=True, preserveAccidentalDisplay=True)
1719
1720        for idx, part in enumerate(score.parts):
1721            # create and add the AnnPart object to part_list
1722            # and to part_to_index dict
1723            part_to_index[part] = idx
1724            ann_part = AnnPart(part, score, idx, spannerBundle, detail)
1725            self.part_list.append(ann_part)
1726
1727        self.n_of_parts: int = len(self.part_list)
1728
1729        if DetailLevel.includesStaffDetails(detail):
1730            for staffGroup in score[m21.layout.StaffGroup]:
1731                # ignore any StaffGroup that contains all the parts, and has no symbol
1732                # and has no barthru (this is just a placeholder generated by some
1733                # file formats, and has the same meaning if it is missing).
1734                if len(staffGroup) == len(part_to_index):
1735                    if not staffGroup.symbol and not staffGroup.barTogether:
1736                        continue
1737
1738                ann_staff_group = AnnStaffGroup(staffGroup, part_to_index, detail)
1739                if ann_staff_group.n_of_parts > 0:
1740                    self.staff_group_list.append(ann_staff_group)
1741
1742            # now sort the staff_group_list in increasing order of first part index
1743            # (secondary sort in decreasing order of last part index)
1744            self.staff_group_list.sort(
1745                key=lambda each: (each.part_indices[0], -each.part_indices[-1])
1746            )
1747
1748        if DetailLevel.includesMetadata(detail) and score.metadata:
1749            # m21 metadata.all() can't sort primitives, so we'll have to sort by hand.
1750            # Note: we sort metadata_items_list after the fact, because sometimes
1751            # (e.g. otherContributor:poet) we substitute names (e.g. lyricist:)
1752            allItems: list[tuple[str, t.Any]] = list(
1753                score.metadata.all(returnPrimitives=True, returnSorted=False)
1754            )
1755            for key, value in allItems:
1756                if key in ('fileFormat', 'filePath', 'software'):
1757                    # Don't compare metadata items that are uninterestingly different.
1758                    continue
1759                if (key.startswith('raw:')
1760                        or key.startswith('meiraw:')
1761                        or key.startswith('humdrumraw:')):
1762                    # Don't compare verbatim/raw metadata ('meiraw:meihead',
1763                    # 'raw:freeform', 'humdrumraw:XXX'), it's often deleted
1764                    # when made obsolete by conversions/edits.
1765                    continue
1766                if key in ('humdrum:EMD', 'humdrum:EST', 'humdrum:VTS',
1767                        'humdrum:RLN', 'humdrum:PUB'):
1768                    # Don't compare metadata items that should never be transferred
1769                    # from one file to another.  'humdrum:EMD' is a modification
1770                    # description entry, humdrum:EST is "current encoding status"
1771                    # (i.e. complete or some value of not complete), 'humdrum:VTS'
1772                    # is a checksum of the Humdrum file, 'humdrum:RLN' is the
1773                    # extended ASCII encoding of the Humdrum file, 'humdrum:PUB'
1774                    # is the publication status of the file (published or not?).
1775                    continue
1776                ami: AnnMetadataItem = AnnMetadataItem(key, value)
1777                if ami.key and ami.value:
1778                    self.metadata_items_list.append(ami)
1779
1780            self.metadata_items_list.sort(key=lambda each: (each.key, str(each.value)))
1781
1782        # cached notation size
1783        self._cached_notation_size: int | None = None

Take a music21 score and store it as a sequence of Full Trees. The hierarchy is "score -> parts -> measures -> voices -> notes"

Arguments:
  • score (music21.stream.Score): The music21 score
  • detail (DetailLevel | int): What level of detail to use during the diff. Can be DecoratedNotesAndRests, OtherObjects, AllObjects, Default (currently AllObjects), or any combination (with | or &~) of those or NotesAndRests, Beams, Tremolos, Ornaments, Articulations, Ties, Slurs, Signatures, Directions, Barlines, StaffDetails, ChordSymbols, Ottavas, Arpeggios, Lyrics, Style, Metadata, or Voicing.
score: int | str
part_list: list[AnnPart]
staff_group_list: list[AnnStaffGroup]
metadata_items_list: list[AnnMetadataItem]
num_syntax_errors_fixed: int
n_of_parts: int
def notation_size(self) -> int:
1795    def notation_size(self) -> int:
1796        """
1797        Compute a measure of how many symbols are displayed in the score for this `AnnScore`.
1798
1799        Returns:
1800            int: The notation size of the annotated score
1801        """
1802        if self._cached_notation_size is None:
1803            size: int = sum([p.notation_size() for p in self.part_list])
1804            size += sum([sg.notation_size() for sg in self.staff_group_list])
1805            size += sum([md.notation_size() for md in self.metadata_items_list])
1806            self._cached_notation_size = size
1807        return self._cached_notation_size

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

Returns:

int: The notation size of the annotated score

def get_note_ids(self) -> list[str | int]:
1816    def get_note_ids(self) -> list[str | int]:
1817        """
1818        Computes a list of the GeneralNote ids for this `AnnScore`.
1819
1820        Returns:
1821            [int]: A list containing the GeneralNote ids contained in this score
1822        """
1823        notes_id = []
1824        for p in self.part_list:
1825            notes_id.extend(p.get_note_ids())
1826        return notes_id

Computes a list of the GeneralNote ids for this AnnScore.

Returns:

[int]: A list containing the GeneralNote ids contained in this score