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
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
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.
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
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
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
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.
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
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.
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
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
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
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.
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
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
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]
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.
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
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
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
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
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.
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
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
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
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.
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
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
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
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.
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 ""
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
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
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
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
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
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.
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
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