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