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