1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: romanText/rtObjects.py 4# Purpose: music21 objects for processing roman numeral analysis text files 5# 6# Authors: Christopher Ariza 7# Michael Scott Cuthbert 8# 9# Copyright: Copyright © 2011-2012, 2019 Michael Scott Cuthbert and the music21 Project 10# License: BSD, see license.txt 11# ------------------------------------------------------------------------------ 12''' 13Translation routines for roman numeral analysis text files, as defined 14and demonstrated by Dmitri Tymoczko, Mark Gotham, Michael Scott Cuthbert, 15and Christopher Ariza in ISMIR 2019. 16''' 17import fractions 18import io 19import re 20import unittest 21 22from music21 import common 23from music21 import exceptions21 24from music21 import environment 25from music21 import key 26from music21 import prebase 27 28_MOD = 'romanText.rtObjects' 29environLocal = environment.Environment(_MOD) 30 31# alternate endings might end with a, b, c for non 32# zero or more for everything after the first number 33reMeasureTag = re.compile(r'm[0-9]+[a-b]*-*[0-9]*[a-b]*') 34reVariant = re.compile(r'var[0-9]+') 35reVariantLetter = re.compile(r'var([A-Z]+)') 36 37reOptKeyOpenAtom = re.compile(r'\?\([A-Ga-g]+[b#]*:') 38reOptKeyCloseAtom = re.compile(r'\?\)[A-Ga-g]+[b#]*:?') 39# ?g:( ? 40reKeyAtom = re.compile('[A-Ga-g]+[b#]*;:') 41reAnalyticKeyAtom = re.compile('[A-Ga-g]+[b#]*:') 42reKeySignatureAtom = re.compile(r'KS-?[0-7]') 43# must distinguish b3 from bVII; there may be b1.66.5 44reBeatAtom = re.compile(r'b[1-9.]+') 45reRepeatStartAtom = re.compile(r'\|\|:') 46reRepeatStopAtom = re.compile(r':\|\|') 47reNoChordAtom = re.compile('(NC|N.C.|nc)') 48 49 50# ------------------------------------------------------------------------------ 51 52class RomanTextException(exceptions21.Music21Exception): 53 pass 54 55 56class RTTokenException(exceptions21.Music21Exception): 57 pass 58 59 60class RTHandlerException(exceptions21.Music21Exception): 61 pass 62 63 64class RTFileException(exceptions21.Music21Exception): 65 pass 66 67 68# ------------------------------------------------------------------------------ 69 70class RTToken(prebase.ProtoM21Object): 71 '''Stores each linear, logical entity of a RomanText. 72 73 A multi-pass parsing procedure is likely necessary, as RomanText permits 74 variety of groupings and markings. 75 76 >>> rtt = romanText.rtObjects.RTToken('||:') 77 >>> rtt 78 <music21.romanText.rtObjects.RTToken '||:'> 79 80 A standard RTToken returns `False` for all of the following. 81 82 >>> rtt.isComposer() or rtt.isTitle() or rtt.isPiece() 83 False 84 >>> rtt.isAnalyst() or rtt.isProofreader() 85 False 86 >>> rtt.isTimeSignature() or rtt.isKeySignature() or rtt.isNote() 87 False 88 >>> rtt.isForm() or rtt.isPedal() or rtt.isMeasure() or rtt.isWork() 89 False 90 >>> rtt.isMovement() or rtt.isVersion() or rtt.isAtom() 91 False 92 ''' 93 94 def __init__(self, src=''): 95 self.src = src # store source character sequence 96 self.lineNumber = 0 97 98 def _reprInternal(self): 99 return repr(self.src) 100 101 def isComposer(self): 102 return False 103 104 def isTitle(self): 105 return False 106 107 def isPiece(self): 108 return False 109 110 def isAnalyst(self): 111 return False 112 113 def isProofreader(self): 114 return False 115 116 def isTimeSignature(self): 117 return False 118 119 def isKeySignature(self): 120 return False 121 122 def isNote(self): 123 return False 124 125 def isForm(self): 126 '''Occasionally found in header. 127 ''' 128 return False 129 130 def isMeasure(self): 131 return False 132 133 def isPedal(self): 134 return False 135 136 def isWork(self): 137 return False 138 139 def isMovement(self): 140 return False 141 142 def isVersion(self): 143 return False 144 145 def isAtom(self): 146 '''Atoms are any untagged data; generally only found inside of a 147 measure definition. 148 ''' 149 return False 150 151 152class RTTagged(RTToken): 153 '''In romanText, some data elements are tags, that is a tag name, a colon, 154 optional whitespace, and data. In non-RTTagged elements, there is just 155 data. 156 157 All tagged tokens are subclasses of this class. Examples are: 158 159 Title: Die Jahrzeiten 160 Composer: Fanny Mendelssohn 161 162 >>> rtTag = romanText.rtObjects.RTTagged('Title: Die Jahrzeiten') 163 >>> rtTag.tag 164 'Title' 165 >>> rtTag.data 166 'Die Jahrzeiten' 167 >>> rtTag.isTitle() 168 True 169 >>> rtTag.isComposer() 170 False 171 ''' 172 173 def __init__(self, src=''): 174 super().__init__(src) 175 # try to split off tag from data 176 self.tag = '' 177 self.data = '' 178 if ':' in src: 179 iFirst = src.find(':') # first index found at 180 self.tag = src[:iFirst].strip() 181 # add one to skip colon 182 self.data = src[iFirst + 1:].strip() 183 else: # we do not have a clear tag; perhaps store all as data 184 self.data = src 185 186 def isComposer(self): 187 '''True is the tag represents a composer. 188 189 >>> rth = romanText.rtObjects.RTTagged('Composer: Claudio Monteverdi') 190 >>> rth.isComposer() 191 True 192 >>> rth.isTitle() 193 False 194 >>> rth.isWork() 195 False 196 >>> rth.data 197 'Claudio Monteverdi' 198 ''' 199 if self.tag.lower() == 'composer': 200 return True 201 return False 202 203 def isTitle(self): 204 '''True if tag represents a title, otherwise False. 205 206 >>> tag = romanText.rtObjects.RTTagged('Title: This is a title.') 207 >>> tag.isTitle() 208 True 209 210 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 211 >>> tag.isTitle() 212 False 213 ''' 214 if self.tag.lower() == 'title': 215 return True 216 return False 217 218 def isPiece(self): 219 ''' 220 True if tag represents a piece, otherwise False. 221 222 >>> tag = romanText.rtObjects.RTTagged('Piece: This is a piece.') 223 >>> tag.isPiece() 224 True 225 226 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 227 >>> tag.isPiece() 228 False 229 ''' 230 if self.tag.lower() == 'piece': 231 return True 232 return False 233 234 def isAnalyst(self): 235 '''True if tag represents a analyst, otherwise False. 236 237 >>> tag = romanText.rtObjects.RTTagged('Analyst: This is an analyst.') 238 >>> tag.isAnalyst() 239 True 240 241 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 242 >>> tag.isAnalyst() 243 False 244 ''' 245 if self.tag.lower() == 'analyst': 246 return True 247 return False 248 249 def isProofreader(self): 250 '''True if tag represents a proofreader, otherwise False. 251 252 >>> tag = romanText.rtObjects.RTTagged('Proofreader: This is a proofreader.') 253 >>> tag.isProofreader() 254 True 255 256 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 257 >>> tag.isProofreader() 258 False 259 ''' 260 if self.tag.lower() in ('proofreader', 'proof reader'): 261 return True 262 return False 263 264 def isTimeSignature(self): 265 '''True if tag represents a time signature, otherwise False. 266 267 >>> tag = romanText.rtObjects.RTTagged('TimeSignature: This is a time signature.') 268 >>> tag.isTimeSignature() 269 True 270 271 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 272 >>> tag.isTimeSignature() 273 False 274 275 TimeSignature header data can be found intermingled with measures. 276 ''' 277 if self.tag.lower() in ('timesignature', 'time signature'): 278 return True 279 return False 280 281 def isKeySignature(self): 282 ''' 283 True if tag represents a key signature, otherwise False. 284 285 >>> tag = romanText.rtObjects.RTTagged('KeySignature: This is a key signature.') 286 >>> tag.isKeySignature() 287 True 288 >>> tag.data 289 'This is a key signature.' 290 291 KeySignatures are a type of tagged data found outside of measures, 292 such as "Key Signature: -1" meaning one flat. 293 294 Key signatures are generally numbers representing the number of sharps (or 295 negative for flats). Non-standard key signatures are not supported. 296 297 >>> tag = romanText.rtObjects.RTTagged('KeySignature: -3') 298 >>> tag.data 299 '-3' 300 301 music21 supports one legacy key signature type: `KeySignature: Bb` which 302 represents a one-flat signature. Important to note: no other key signatures 303 of this type are supported. (For instance, `KeySignature: Ab` has no effect) 304 305 >>> tag = romanText.rtObjects.RTTagged('KeySignature: Bb') 306 >>> tag.data 307 'Bb' 308 309 Testing that `.isKeySignature` returns `False` for non-key signatures: 310 311 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 312 >>> tag.isKeySignature() 313 False 314 315 316 N.B.: this is not the same as a key definition found inside of a 317 Measure. These are represented by RTKey rtObjects, defined below, and are 318 not RTTagged rtObjects, but RTAtom subclasses. 319 ''' 320 if self.tag.lower() in ('keysignature', 'key signature'): 321 return True 322 else: 323 return False 324 325 def isNote(self): 326 '''True if tag represents a note, otherwise False. 327 328 >>> tag = romanText.rtObjects.RTTagged('Note: This is a note.') 329 >>> tag.isNote() 330 True 331 332 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 333 >>> tag.isNote() 334 False 335 ''' 336 if self.tag.lower() == 'note': 337 return True 338 return False 339 340 def isForm(self): 341 '''True if tag represents a form, otherwise False. 342 343 >>> tag = romanText.rtObjects.RTTagged('Form: This is a form.') 344 >>> tag.isForm() 345 True 346 347 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 348 >>> tag.isForm() 349 False 350 ''' 351 if self.tag.lower() == 'form': 352 return True 353 return False 354 355 def isPedal(self): 356 '''True if tag represents a pedal, otherwise False. 357 358 >>> tag = romanText.rtObjects.RTTagged('Pedal: This is a pedal.') 359 >>> tag.isPedal() 360 True 361 362 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 363 >>> tag.isPedal() 364 False 365 ''' 366 if self.tag.lower() in ['pedal']: 367 return True 368 return False 369 370 def isVersion(self): 371 '''True if tag defines the version of RomanText standard used, 372 otherwise False. 373 374 Pieces without the tag are defined to conform to RomanText 1.0, 375 the version described in the ISMIR publication. 376 377 >>> rth = romanText.rtObjects.RTTagged('RTVersion: 1.0') 378 >>> rth.isTitle() 379 False 380 >>> rth.isVersion() 381 True 382 >>> rth.tag 383 'RTVersion' 384 >>> rth.data 385 '1.0' 386 ''' 387 if self.tag.lower() == 'rtversion': 388 return True 389 else: 390 return False 391 392 def isWork(self): 393 '''True if tag represents a work, otherwise False. 394 395 The "work" is not defined as a header tag, but is used to represent 396 all tags, often placed after Composer, for the work or pieces designation. 397 398 >>> rth = romanText.rtObjects.RTTagged('Work: BWV232') 399 >>> rth.isWork() 400 True 401 >>> rth.tag 402 'Work' 403 >>> rth.data 404 'BWV232' 405 406 For historical reasons, the tag 'Madrigal' also designates a work. 407 408 >>> rth = romanText.rtObjects.RTTagged('Madrigal: 4.12') 409 >>> rth.isTitle() 410 False 411 >>> rth.isWork() 412 True 413 >>> rth.tag 414 'Madrigal' 415 >>> rth.data 416 '4.12' 417 ''' 418 if self.tag.lower() in ('work', 'madrigal'): 419 return True 420 else: 421 return False 422 423 def isMovement(self): 424 '''True if tag represents a movement, otherwise False. 425 426 >>> tag = romanText.rtObjects.RTTagged('Movement: This is a movement.') 427 >>> tag.isMovement() 428 True 429 430 >>> tag = romanText.rtObjects.RTTagged('Nothing: Nothing at all.') 431 >>> tag.isMovement() 432 False 433 ''' 434 if self.tag.lower() == 'movement': 435 return True 436 return False 437 438 def isSixthMinor(self): 439 ''' 440 True if tag represents a configuration setting for setting vi/vio/VI in minor 441 442 >>> tag = romanText.rtObjects.RTTagged('Sixth Minor: Flat') 443 >>> tag.isSixthMinor() 444 True 445 >>> tag.data 446 'Flat' 447 ''' 448 return self.tag.lower() in ('sixthminor', 'sixth minor') # e.g. 'Sixth Minor: FLAT' 449 450 def isSeventhMinor(self): 451 ''' 452 True if tag represents a configuration setting for setting vii/viio/VII in minor 453 454 >>> tag = romanText.rtObjects.RTTagged('Seventh Minor: Courtesy') 455 >>> tag.isSeventhMinor() 456 True 457 >>> tag.data 458 'Courtesy' 459 ''' 460 return self.tag.lower() in ('seventhminor', 'seventh minor') 461 462 463class RTMeasure(RTToken): 464 '''In RomanText, measures are given one per line and always start with 'm'. 465 466 For instance: 467 468 m4 i b3 v b4 VI 469 m5 b2 g: IV b4 V 470 m6 i 471 m7 D: V 472 473 Measure ranges can be used and copied, such as: 474 475 m8-m9=m4-m5 476 477 RTMeasure objects can also define variant readings for a measure: 478 479 m1 ii 480 m1var1 ii b2 ii6 b3 IV 481 482 Variants are not part of the tag, but are read into an attribute. 483 484 Endings are indicated by a single letter after the measure number, such as 485 "a" for first ending. 486 487 >>> rtm = romanText.rtObjects.RTMeasure('m15a V6 b1.5 V6/5 b2 I b3 viio6') 488 >>> rtm.data 489 'V6 b1.5 V6/5 b2 I b3 viio6' 490 >>> rtm.number 491 [15] 492 >>> rtm.repeatLetter 493 ['a'] 494 >>> rtm.isMeasure() 495 True 496 497 498 ''' 499 500 def __init__(self, src=''): 501 super().__init__(src) 502 # try to split off tag from data 503 self.tag = '' # the measure number or range 504 self.data = '' # only chord, phrase, and similar definitions 505 self.number = [] # one or more measure numbers 506 self.repeatLetter = [] # one or more repeat letters 507 self.variantNumber = None # a one-measure or short variant 508 # a longer-variant that 509 # defines a different way of reading a large section 510 self.variantLetter = None 511 # store boolean if this measure defines copying another range 512 self.isCopyDefinition = False 513 # store processed tokens associated with this measure 514 self.atoms = [] 515 516 if src: 517 self._parseAttributes(src) 518 519 def _getMeasureNumberData(self, src): 520 '''Return the number or numbers as a list, as well as any repeat 521 indications. 522 523 >>> rtm = romanText.rtObjects.RTMeasure() 524 >>> rtm._getMeasureNumberData('m77') 525 ([77], ['']) 526 >>> rtm._getMeasureNumberData('m123b-432b') 527 ([123, 432], ['b', 'b']) 528 ''' 529 # note: this is separate procedure b/c it is used to get copy 530 # boundaries 531 if '-' in src: # its a range 532 mnStart, mnEnd = src.split('-') 533 proc = [mnStart, mnEnd] 534 else: 535 proc = [src] # treat as one 536 number = [] 537 repeatLetter = [] 538 for mn in proc: 539 # append in order, start, end 540 numStr, alphaStr = common.getNumFromStr(mn) 541 number.append(int(numStr)) 542 # remove all 'm' in alpha 543 alphaStr = alphaStr.replace('m', '') 544 repeatLetter.append(alphaStr) 545 return number, repeatLetter 546 547 def _parseAttributes(self, src): 548 # assume that we have already checked that this is a measure 549 g = reMeasureTag.match(src) 550 if g is None: # not measure tag found 551 raise RTHandlerException(f'found no measure tag: {src}') 552 iEnd = g.end() # get end index 553 rawTag = src[:iEnd].strip() 554 self.tag = rawTag 555 rawData = src[iEnd:].strip() # may have variant 556 557 # get the number list from the tag 558 self.number, self.repeatLetter = self._getMeasureNumberData(rawTag) 559 560 # strip a variant indication off of rawData if found 561 g = reVariant.match(rawData) 562 if g is not None: # there is a variant tag 563 varStr = g.group(0) 564 self.variantNumber = int(common.getNumFromStr(varStr)[0]) 565 self.data = rawData[g.end():].strip() 566 else: 567 self.data = rawData 568 g = reVariantLetter.match(rawData) 569 if g is not None: # there is a variant letter tag 570 varStr = g.group(1) 571 self.variantLetter = varStr 572 self.data = rawData[g.end():].strip() 573 574 if self.data.startswith('='): 575 self.isCopyDefinition = True 576 577 def _reprInternal(self): 578 if len(self.number) == 1: 579 numberStr = str(self.number[0]) 580 else: 581 numberStr = f'{self.number[0]}-{self.number[1]}' 582 return numberStr 583 584 def isMeasure(self): 585 return True 586 587 def getCopyTarget(self): 588 '''If this measure defines a copy operation, return two lists defining 589 the measures to copy; the second list has the repeat data. 590 591 >>> rtm = romanText.rtObjects.RTMeasure('m35-36 = m29-30') 592 >>> rtm.number 593 [35, 36] 594 >>> rtm.getCopyTarget() 595 ([29, 30], ['', '']) 596 597 >>> rtm = romanText.rtObjects.RTMeasure('m4 = m1') 598 >>> rtm.number 599 [4] 600 >>> rtm.getCopyTarget() 601 ([1], ['']) 602 ''' 603 # remove equal sign 604 rawData = self.data.replace('=', '').strip() 605 return self._getMeasureNumberData(rawData) 606 607 608class RTAtom(RTToken): 609 '''In RomanText, definitions of chords, phrases boundaries, open/close 610 parenthesis, beat indicators, etc. appear within measures (RTMeasure 611 objects). These individual elements will be called Atoms, as they are data 612 that is not tagged. 613 614 Each atom store a reference to its container (normally an RTMeasure). 615 616 >>> chordIV = romanText.rtObjects.RTAtom('IV') 617 >>> beat4 = romanText.rtObjects.RTAtom('b4') 618 >>> beat4 619 <music21.romanText.rtObjects.RTAtom 'b4'> 620 >>> beat4.isAtom() 621 True 622 623 However, see RTChord, RTBeat, etc. which are subclasses of RTAtom 624 specifically for storing chords, beats, etc. 625 ''' 626 627 def __init__(self, src='', container=None): 628 # this stores the source 629 super().__init__(src) 630 self.container = container 631 632 def isAtom(self): 633 return True 634 635 # for lower level distinctions, use isinstance(), as each type has its own subclass. 636 637 638class RTChord(RTAtom): 639 r'''An RTAtom subclass that defines a chord. Also contains a reference to 640 the container. 641 642 >>> chordIV = romanText.rtObjects.RTChord('IV') 643 >>> chordIV 644 <music21.romanText.rtObjects.RTChord 'IV'> 645 ''' 646 647 def __init__(self, src='', container=None): 648 super().__init__(src, container) 649 650 # store offset within measure 651 self.offset = None 652 # store a quarterLength duration 653 self.quarterLength = None 654 655 656class RTNoChord(RTAtom): 657 r'''An RTAtom subclass that defines absence of a chord. Also contains a 658 reference to the container. 659 660 >>> chordNC = romanText.rtObjects.RTNoChord('NC') 661 >>> chordNC 662 <music21.romanText.rtObjects.RTNoChord 'NC'> 663 664 >>> rth = romanText.rtObjects.RTHandler() 665 >>> rth.tokenizeAtoms('nc NC N.C.') 666 [<music21.romanText.rtObjects.RTNoChord 'nc'>, 667 <music21.romanText.rtObjects.RTNoChord 'NC'>, 668 <music21.romanText.rtObjects.RTNoChord 'N.C.'>] 669 ''' 670 671 def __init__(self, src='', container=None): 672 super().__init__(src, container) 673 674 # store offset within measure 675 self.offset = None 676 # store a quarterLength duration 677 self.quarterLength = None 678 679 680class RTBeat(RTAtom): 681 r'''An RTAtom subclass that defines a beat definition. Also contains a 682 reference to the container. 683 684 >>> beatFour = romanText.rtObjects.RTBeat('b4') 685 >>> beatFour 686 <music21.romanText.rtObjects.RTBeat 'b4'> 687 ''' 688 689 def getBeatFloatOrFrac(self): 690 ''' 691 Gets the beat number as a float or fraction. Time signature independent 692 693 >>> RTB = romanText.rtObjects.RTBeat 694 695 Simple ones: 696 697 >>> RTB('b1').getBeatFloatOrFrac() 698 1.0 699 >>> RTB('b2').getBeatFloatOrFrac() 700 2.0 701 702 etc. 703 704 with easy float: 705 706 >>> RTB('b1.5').getBeatFloatOrFrac() 707 1.5 708 >>> RTB('b1.25').getBeatFloatOrFrac() 709 1.25 710 711 with harder: 712 713 >>> RTB('b1.33').getBeatFloatOrFrac() 714 Fraction(4, 3) 715 716 >>> RTB('b2.66').getBeatFloatOrFrac() 717 Fraction(8, 3) 718 719 >>> RTB('b1.2').getBeatFloatOrFrac() 720 Fraction(6, 5) 721 722 723 A third digit of 0.5 adds 1/2 of 1/DENOM of before. Here DENOM is 3 (in 5/3) so 724 we add 1/6 to 5/3 to get 11/6: 725 726 727 >>> RTB('b1.66').getBeatFloatOrFrac() 728 Fraction(5, 3) 729 730 >>> RTB('b1.66.5').getBeatFloatOrFrac() 731 Fraction(11, 6) 732 733 734 Similarly 0.25 adds 1/4 of 1/DENOM... to get 21/12 or 7/4 or 1.75 735 736 >>> RTB('b1.66.25').getBeatFloatOrFrac() 737 1.75 738 739 And 0.75 adds 3/4 of 1/DENOM to get 23/12 740 741 >>> RTB('b1.66.75').getBeatFloatOrFrac() 742 Fraction(23, 12) 743 744 745 A weird way of writing 'b1.5' 746 747 >>> RTB('b1.33.5').getBeatFloatOrFrac() 748 1.5 749 ''' 750 beatStr = self.src.replace('b', '') 751 # there may be more than one decimal in the number, such as 752 # 1.66.5, to show halfway through 2/3rd of a beat 753 parts = beatStr.split('.') 754 mainBeat = int(parts[0]) 755 if len(parts) > 1: # 1.66 756 fracPart = common.addFloatPrecision('.' + parts[1]) 757 else: 758 fracPart = 0.0 759 760 if len(parts) > 2: # 1.66.5 761 fracPartDivisor = float('.' + parts[2]) # 0.5 762 if isinstance(fracPart, float): 763 fracPart = fractions.Fraction.from_float(fracPart) 764 denom = fracPart.denominator 765 fracBeatFrac = common.opFrac(1. / (denom / fracPartDivisor)) 766 else: 767 fracBeatFrac = 0.0 768 769 if len(parts) > 3: 770 environLocal.printDebug([f'got unexpected beat: {self.src}']) 771 raise RTTokenException(f'cannot handle specification: {self.src}') 772 773 beat = common.opFrac(mainBeat + fracPart + fracBeatFrac) 774 return beat 775 776 def getOffset(self, timeSignature): 777 '''Given a time signature, return the offset position specified by this 778 beat. 779 780 >>> rtb = romanText.rtObjects.RTBeat('b1.5') 781 >>> rtb.getOffset(meter.TimeSignature('3/4')) 782 0.5 783 >>> rtb.getOffset(meter.TimeSignature('6/8')) 784 0.75 785 >>> rtb.getOffset(meter.TimeSignature('2/2')) 786 1.0 787 788 >>> rtb = romanText.rtObjects.RTBeat('b2') 789 >>> rtb.getOffset(meter.TimeSignature('3/4')) 790 1.0 791 >>> rtb.getOffset(meter.TimeSignature('6/8')) 792 1.5 793 794 >>> rtb = romanText.rtObjects.RTBeat('b1.66') 795 >>> rtb.getOffset(meter.TimeSignature('6/8')) 796 1.0 797 >>> rtc = romanText.rtObjects.RTBeat('b1.66.5') 798 >>> rtc.getOffset(meter.TimeSignature('6/8')) 799 1.25 800 ''' 801 beat = self.getBeatFloatOrFrac() 802 803 # environLocal.printDebug(['using beat value:', beat]) 804 # TODO: check for exceptions/errors if this beat is bad 805 try: 806 post = timeSignature.getOffsetFromBeat(beat) 807 except exceptions21.TimeSignatureException: 808 environLocal.printDebug(['bad beat specification: %s in a meter of %s' % ( 809 self.src, timeSignature)]) 810 post = 0.0 811 812 return post 813 814 815class RTKeyTypeAtom(RTAtom): 816 '''RTKeyTypeAtoms contain utility functions for all Key-type tokens, i.e. 817 RTKey, RTAnalyticKey, but not KeySignature. 818 819 >>> gMinor = romanText.rtObjects.RTKeyTypeAtom('g;:') 820 >>> gMinor 821 <music21.romanText.rtObjects.RTKeyTypeAtom 'g;:'> 822 ''' 823 824 def getKey(self): 825 ''' 826 This returns a Key, not a KeySignature object 827 ''' 828 myKey = self.src.rstrip(self.footerStrip) 829 myKey = key.convertKeyStringToMusic21KeyString(myKey) 830 return key.Key(myKey) 831 832 def getKeySignature(self): 833 '''Get a KeySignature object. 834 ''' 835 myKey = self.getKey() 836 return key.KeySignature(myKey.sharps) 837 838 839class RTKey(RTKeyTypeAtom): 840 '''An RTKey(RTAtom) defines both a change in KeySignature and a change 841 in the analyzed Key. 842 843 They are defined by ";:" after the Key. 844 845 >>> gMinor = romanText.rtObjects.RTKey('g;:') 846 >>> gMinor 847 <music21.romanText.rtObjects.RTKey 'g;:'> 848 >>> gMinor.getKey() 849 <music21.key.Key of g minor> 850 851 >>> bMinor = romanText.rtObjects.RTKey('bb;:') 852 >>> bMinor 853 <music21.romanText.rtObjects.RTKey 'bb;:'> 854 >>> bMinor.getKey() 855 <music21.key.Key of b- minor> 856 >>> bMinor.getKeySignature() 857 <music21.key.KeySignature of 5 flats> 858 859 >>> eFlatMajor = romanText.rtObjects.RTKey('Eb;:') 860 >>> eFlatMajor 861 <music21.romanText.rtObjects.RTKey 'Eb;:'> 862 >>> eFlatMajor.getKey() 863 <music21.key.Key of E- major> 864 ''' 865 footerStrip = ';:' 866 867 868class RTAnalyticKey(RTKeyTypeAtom): 869 '''An RTAnalyticKey(RTKeyTypeAtom) only defines a change in the key 870 being analyzed. It does not in itself create a :class:~'music21.key.Key' 871 object. 872 873 >>> gMinor = romanText.rtObjects.RTAnalyticKey('g:') 874 >>> gMinor 875 <music21.romanText.rtObjects.RTAnalyticKey 'g:'> 876 >>> gMinor.getKey() 877 <music21.key.Key of g minor> 878 879 >>> bMinor = romanText.rtObjects.RTAnalyticKey('bb:') 880 >>> bMinor 881 <music21.romanText.rtObjects.RTAnalyticKey 'bb:'> 882 >>> bMinor.getKey() 883 <music21.key.Key of b- minor> 884 885 ''' 886 footerStrip = ':' 887 888 889class RTKeySignature(RTAtom): 890 ''' 891 An RTKeySignature(RTAtom) only defines a change in the KeySignature. 892 It does not in itself create a :class:~'music21.key.Key' object, nor 893 does it change the analysis taking place. 894 895 The number after KS defines the number of sharps (negative for flats). 896 897 >>> gMinor = romanText.rtObjects.RTKeySignature('KS-2') 898 >>> gMinor 899 <music21.romanText.rtObjects.RTKeySignature 'KS-2'> 900 >>> gMinor.getKeySignature() 901 <music21.key.KeySignature of 2 flats> 902 903 >>> aMajor = romanText.rtObjects.RTKeySignature('KS3') 904 >>> aMajor.getKeySignature() 905 <music21.key.KeySignature of 3 sharps> 906 ''' 907 908 def getKeySignature(self): 909 numSharps = int(self.src[2:]) 910 return key.KeySignature(numSharps) 911 912 913class RTOpenParens(RTAtom): 914 ''' 915 A simple open parenthesis Atom with a sensible default 916 917 >>> romanText.rtObjects.RTOpenParens('(') 918 <music21.romanText.rtObjects.RTOpenParens '('> 919 ''' 920 921 def __init__(self, src='(', container=None): # pylint: disable=useless-super-delegation 922 super().__init__(src, container) 923 924 925class RTCloseParens(RTAtom): 926 ''' 927 A simple close parenthesis Atom with a sensible default 928 929 >>> romanText.rtObjects.RTCloseParens(')') 930 <music21.romanText.rtObjects.RTCloseParens ')'> 931 ''' 932 933 def __init__(self, src=')', container=None): # pylint: disable=useless-super-delegation 934 super().__init__(src, container) 935 936 937class RTOptionalKeyOpen(RTAtom): 938 ''' 939 Marks the beginning of an optional Key area which does not 940 affect the roman numeral analysis. (For instance, it is 941 possible to analyze in Bb major, while remaining in g minor) 942 943 >>> possibleKey = romanText.rtObjects.RTOptionalKeyOpen('?(Bb:') 944 >>> possibleKey 945 <music21.romanText.rtObjects.RTOptionalKeyOpen '?(Bb:'> 946 >>> possibleKey.getKey() 947 <music21.key.Key of B- major> 948 ''' 949 950 def getKey(self): 951 # alter flat symbol 952 if self.src == '?(b:': 953 return key.Key('b') 954 else: 955 keyStr = self.src.replace('b', '-') 956 keyStr = keyStr.replace(':', '') 957 keyStr = keyStr.replace('?', '') 958 keyStr = keyStr.replace('(', '') 959 # environLocal.printDebug(['create a key from:', keyStr]) 960 return key.Key(keyStr) 961 962 963class RTOptionalKeyClose(RTAtom): 964 ''' 965 Marks the end of an optional Key area which does not affect the roman 966 numeral analysis. 967 968 For example, it is possible to analyze in Bb major, while remaining in g 969 minor. 970 971 >>> possibleKey = romanText.rtObjects.RTOptionalKeyClose('?)Bb:') 972 >>> possibleKey 973 <music21.romanText.rtObjects.RTOptionalKeyClose '?)Bb:'> 974 >>> possibleKey.getKey() 975 <music21.key.Key of B- major> 976 ''' 977 978 def getKey(self): 979 # alter flat symbol 980 if self.src == '?)b:' or self.src == '?)b': 981 return key.Key('b') 982 else: 983 keyStr = self.src.replace('b', '-') 984 keyStr = keyStr.replace(':', '') 985 keyStr = keyStr.replace('?', '') 986 keyStr = keyStr.replace(')', '') 987 # environLocal.printDebug(['create a key from:', keyStr]) 988 return key.Key(keyStr) 989 990 991class RTPhraseMarker(RTAtom): 992 ''' 993 A Phrase Marker: 994 995 >>> rtPhraseMarker = romanText.rtObjects.RTPhraseMarker('') 996 >>> rtPhraseMarker 997 <music21.romanText.rtObjects.RTPhraseMarker ''> 998 ''' 999 1000 1001class RTPhraseBoundary(RTPhraseMarker): 1002 ''' 1003 >>> phrase = romanText.rtObjects.RTPhraseBoundary('||') 1004 >>> phrase 1005 <music21.romanText.rtObjects.RTPhraseBoundary '||'> 1006 ''' 1007 1008 def __init__(self, src='||', container=None): # pylint: disable=useless-super-delegation 1009 super().__init__(src, container) 1010 1011 1012class RTEllisonStart(RTPhraseMarker): 1013 ''' 1014 >>> phrase = romanText.rtObjects.RTEllisonStart('|*') 1015 >>> phrase 1016 <music21.romanText.rtObjects.RTEllisonStart '|*'> 1017 ''' 1018 1019 def __init__(self, src='|*', container=None): # pylint: disable=useless-super-delegation 1020 super().__init__(src, container) 1021 1022 1023class RTEllisonStop(RTPhraseMarker): 1024 ''' 1025 >>> phrase = romanText.rtObjects.RTEllisonStop('*|') 1026 >>> phrase 1027 <music21.romanText.rtObjects.RTEllisonStop '*|'> 1028 ''' 1029 1030 def __init__(self, src='*|', container=None): # pylint: disable=useless-super-delegation 1031 super().__init__(src, container) 1032 1033 1034class RTRepeat(RTAtom): 1035 ''' 1036 >>> repeat = romanText.rtObjects.RTRepeat('||:') 1037 >>> repeat 1038 <music21.romanText.rtObjects.RTRepeat '||:'> 1039 ''' 1040 1041 1042class RTRepeatStart(RTRepeat): 1043 ''' 1044 >>> repeat = romanText.rtObjects.RTRepeatStart() 1045 >>> repeat 1046 <music21.romanText.rtObjects.RTRepeatStart ...'||:'> 1047 ''' 1048 1049 def __init__(self, src='||:', container=None): # pylint: disable=useless-super-delegation 1050 super().__init__(src, container) 1051 1052 1053class RTRepeatStop(RTRepeat): 1054 ''' 1055 >>> repeat = romanText.rtObjects.RTRepeatStop() 1056 >>> repeat 1057 <music21.romanText.rtObjects.RTRepeatStop ...':||'> 1058 ''' 1059 1060 def __init__(self, src=':||', container=None): # pylint: disable=useless-super-delegation 1061 super().__init__(src, container) 1062 1063 1064# ------------------------------------------------------------------------------ 1065 1066class RTHandler: 1067 1068 # divide elements of a character stream into rtObjects and handle 1069 # store in a list, and pass global information to components 1070 def __init__(self): 1071 # tokens are ABC rtObjects in a linear stream 1072 # tokens are strongly divided between header and body, so can 1073 # divide here 1074 self._tokens = [] 1075 self.currentLineNumber = 0 1076 1077 def splitAtHeader(self, lines): 1078 '''Divide string into header and non-header; this is done before 1079 tokenization. 1080 1081 >>> rth = romanText.rtObjects.RTHandler() 1082 >>> rth.splitAtHeader(['Title: s', 'Time Signature:', '', 'm1 g: i']) 1083 (['Title: s', 'Time Signature:', ''], ['m1 g: i']) 1084 1085 ''' 1086 # iterate over lines and find the first measure definition 1087 iStartBody = None 1088 for i, l in enumerate(lines): 1089 if reMeasureTag.match(l.strip()) is not None: 1090 # found a measure definition 1091 iStartBody = i 1092 break 1093 if iStartBody is None: 1094 raise RomanTextException('Cannot find the first measure definition in this file. ' 1095 + 'Dumping contexts: %s', lines) 1096 return lines[:iStartBody], lines[iStartBody:] 1097 1098 def tokenizeHeader(self, lines): 1099 '''In the header, we only have :class:`~music21.romanText.base.RTTagged` 1100 tokens. We can this process these all as the same class. 1101 ''' 1102 post = [] 1103 for i, line in enumerate(lines): 1104 line = line.strip() 1105 if line == '': 1106 continue 1107 # wrap each line in a header token 1108 rtt = RTTagged(line) 1109 rtt.lineNumber = i + 1 1110 post.append(rtt) 1111 self.currentLineNumber = len(lines) + 1 1112 return post 1113 1114 def tokenizeBody(self, lines): 1115 '''In the body, we may have measure, time signature, or note 1116 declarations, as well as possible other tagged definitions. 1117 ''' 1118 post = [] 1119 startLineNumber = self.currentLineNumber 1120 for i, line in enumerate(lines): 1121 currentLineNumber = startLineNumber + i 1122 try: 1123 line = line.strip() 1124 if line == '': 1125 continue 1126 # first, see if it is a measure definition, if not, than assume it is tagged data 1127 if reMeasureTag.match(line) is not None: 1128 rtm = RTMeasure(line) 1129 rtm.lineNumber = currentLineNumber 1130 # note: could places these in-line, after post 1131 rtm.atoms = self.tokenizeAtoms(rtm.data, container=rtm) 1132 for a in rtm.atoms: 1133 a.lineNumber = currentLineNumber 1134 post.append(rtm) 1135 else: 1136 # store items in a measure tag outside of the measure 1137 rtt = RTTagged(line) 1138 rtt.lineNumber = currentLineNumber 1139 post.append(rtt) 1140 except Exception: 1141 import traceback 1142 tracebackMessage = traceback.format_exc() 1143 raise RTHandlerException('At line %s (%s) an exception was raised: \n%s' % ( 1144 currentLineNumber, line, tracebackMessage)) 1145 return post 1146 1147 def tokenizeAtoms(self, line, container=None): 1148 '''Given a line of data stored in measure consisting only of Atoms, 1149 tokenize and return a list. 1150 1151 >>> rth = romanText.rtObjects.RTHandler() 1152 >>> rth.tokenizeAtoms('IV b3 ii7 b4 ii') 1153 [<music21.romanText.rtObjects.RTChord 'IV'>, 1154 <music21.romanText.rtObjects.RTBeat 'b3'>, 1155 <music21.romanText.rtObjects.RTChord 'ii7'>, 1156 <music21.romanText.rtObjects.RTBeat 'b4'>, 1157 <music21.romanText.rtObjects.RTChord 'ii'>] 1158 1159 >>> rth.tokenizeAtoms('V7 b2 V13 b3 V7 iio6/5[no5]') 1160 [<music21.romanText.rtObjects.RTChord 'V7'>, 1161 <music21.romanText.rtObjects.RTBeat 'b2'>, 1162 <music21.romanText.rtObjects.RTChord 'V13'>, 1163 <music21.romanText.rtObjects.RTBeat 'b3'>, 1164 <music21.romanText.rtObjects.RTChord 'V7'>, 1165 <music21.romanText.rtObjects.RTChord 'iio6/5[no5]'>] 1166 1167 >>> tokenList = rth.tokenizeAtoms('I b2 I b2.25 V/ii b2.5 bVII b2.75 V g: IV') 1168 >>> tokenList 1169 [<music21.romanText.rtObjects.RTChord 'I'>, 1170 <music21.romanText.rtObjects.RTBeat 'b2'>, 1171 <music21.romanText.rtObjects.RTChord 'I'>, 1172 <music21.romanText.rtObjects.RTBeat 'b2.25'>, 1173 <music21.romanText.rtObjects.RTChord 'V/ii'>, 1174 <music21.romanText.rtObjects.RTBeat 'b2.5'>, 1175 <music21.romanText.rtObjects.RTChord 'bVII'>, 1176 <music21.romanText.rtObjects.RTBeat 'b2.75'>, 1177 <music21.romanText.rtObjects.RTChord 'V'>, 1178 <music21.romanText.rtObjects.RTAnalyticKey 'g:'>, 1179 <music21.romanText.rtObjects.RTChord 'IV'>] 1180 1181 >>> tokenList[-2].getKey() 1182 <music21.key.Key of g minor> 1183 1184 >>> rth.tokenizeAtoms('= m3') 1185 [] 1186 1187 >>> tokenList = rth.tokenizeAtoms('g;: ||: V b2 ?(Bb: VII7 b3 III b4 ?)Bb: i :||') 1188 >>> tokenList 1189 [<music21.romanText.rtObjects.RTKey 'g;:'>, 1190 <music21.romanText.rtObjects.RTRepeatStart '||:'>, 1191 <music21.romanText.rtObjects.RTChord 'V'>, 1192 <music21.romanText.rtObjects.RTBeat 'b2'>, 1193 <music21.romanText.rtObjects.RTOptionalKeyOpen '?(Bb:'>, 1194 <music21.romanText.rtObjects.RTChord 'VII7'>, 1195 <music21.romanText.rtObjects.RTBeat 'b3'>, 1196 <music21.romanText.rtObjects.RTChord 'III'>, 1197 <music21.romanText.rtObjects.RTBeat 'b4'>, 1198 <music21.romanText.rtObjects.RTOptionalKeyClose '?)Bb:'>, 1199 <music21.romanText.rtObjects.RTChord 'i'>, 1200 <music21.romanText.rtObjects.RTRepeatStop ':||'>] 1201 ''' 1202 post = [] 1203 # break by spaces 1204 for word in line.split(' '): 1205 word = word.strip() 1206 if word == '': 1207 continue 1208 elif word == '=': 1209 # if an = is found, this is a copy definition, and no atoms here 1210 break 1211 elif word == '||': 1212 post.append(RTPhraseBoundary(word, container)) 1213 elif word == '(': 1214 post.append(RTOpenParens(word, container)) 1215 elif word == ')': 1216 post.append(RTCloseParens(word, container)) 1217 elif reBeatAtom.match(word) is not None: 1218 post.append(RTBeat(word, container)) 1219 # from here, all that is left is keys or chords 1220 elif reOptKeyOpenAtom.match(word) is not None: 1221 post.append(RTOptionalKeyOpen(word, container)) 1222 elif reOptKeyCloseAtom.match(word) is not None: 1223 post.append(RTOptionalKeyClose(word, container)) 1224 elif reKeyAtom.match(word) is not None: 1225 post.append(RTKey(word, container)) 1226 elif reAnalyticKeyAtom.match(word) is not None: 1227 post.append(RTAnalyticKey(word, container)) 1228 elif reKeySignatureAtom.match(word) is not None: 1229 post.append(RTKeySignature(word, container)) 1230 elif reRepeatStartAtom.match(word) is not None: 1231 post.append(RTRepeatStart(word, container)) 1232 elif reRepeatStopAtom.match(word) is not None: 1233 post.append(RTRepeatStop(word, container)) 1234 elif reNoChordAtom.match(word) is not None: 1235 post.append(RTNoChord(word, container)) 1236 else: # only option is that it is a chord 1237 post.append(RTChord(word, container)) 1238 return post 1239 1240 def tokenize(self, src): 1241 ''' 1242 Walk the RT string, creating RT rtObjects along the way. 1243 ''' 1244 # break into lines 1245 lines = src.split('\n') 1246 linesHeader, linesBody = self.splitAtHeader(lines) 1247 # environLocal.printDebug([linesHeader]) 1248 self._tokens += self.tokenizeHeader(linesHeader) 1249 self._tokens += self.tokenizeBody(linesBody) 1250 1251 def process(self, src): 1252 ''' 1253 Given an entire specification as a single source string, strSrc, tokenize it. 1254 This is usually provided in a file. 1255 ''' 1256 self._tokens = [] 1257 self.tokenize(src) 1258 1259 def definesMovements(self, countRequired=2): 1260 '''Return True if more than one movement is defined in a RT file. 1261 1262 >>> rth = romanText.rtObjects.RTHandler() 1263 >>> rth.process('Movement: 1 \\n Movement: 2 \\n \\n m1') 1264 >>> rth.definesMovements() 1265 True 1266 >>> rth.process('Movement: 1 \\n m1') 1267 >>> rth.definesMovements() 1268 False 1269 ''' 1270 if not self._tokens: 1271 raise RTHandlerException('must create tokens first') 1272 count = 0 1273 for t in self._tokens: 1274 if t.isMovement(): 1275 count += 1 1276 if count >= countRequired: 1277 return True 1278 return False 1279 1280 def definesMovement(self): 1281 '''Return True if this handler has 1 or more movement. 1282 1283 >>> rth = romanText.rtObjects.RTHandler() 1284 >>> rth.process('Movement: 1 \\n \\n m1') 1285 >>> rth.definesMovements() 1286 False 1287 >>> rth.definesMovement() 1288 True 1289 ''' 1290 return self.definesMovements(countRequired=1) 1291 1292 def splitByMovement(self, duplicateHeader=True): 1293 '''If we have movements defined, return a list of RTHandler rtObjects, 1294 representing header information and each movement, in order. 1295 1296 >>> rth = romanText.rtObjects.RTHandler() 1297 >>> rth.process('Title: Test \\n Movement: 1 \\n m1 \\n Movement: 2 \\n m1') 1298 >>> post = rth.splitByMovement(False) 1299 >>> len(post) 1300 3 1301 1302 >>> len(post[0]) 1303 1 1304 1305 >>> post[0].__class__ 1306 <class 'music21.romanText.rtObjects.RTHandler'> 1307 >>> len(post[1]), len(post[2]) 1308 (2, 2) 1309 1310 >>> post = rth.splitByMovement(duplicateHeader=True) 1311 >>> len(post) 1312 2 1313 1314 >>> len(post[0]), len(post[1]) 1315 (3, 3) 1316 ''' 1317 post = [] 1318 sub = [] 1319 for t in self._tokens: 1320 if t.isMovement(): 1321 # when finding a movement, we are ending a previous 1322 # and starting a new; this may just have metadata 1323 rth = RTHandler() 1324 rth.tokens = sub 1325 post.append(rth) 1326 sub = [] 1327 sub.append(t) 1328 1329 if sub: 1330 rth = RTHandler() 1331 rth.tokens = sub 1332 post.append(rth) 1333 1334 if duplicateHeader: 1335 alt = [] 1336 # if no movement in this first handler, assume it is header info 1337 if not post[0].definesMovement(): 1338 handlerHead = post[0] 1339 iStart = 1 1340 else: 1341 handlerHead = None 1342 iStart = 0 1343 for h in post[iStart:]: 1344 if handlerHead is not None: 1345 h = handlerHead + h # add metadata 1346 alt.append(h) 1347 # reassign 1348 post = alt 1349 1350 return post 1351 1352 # -------------------------------------------------------------------------- 1353 # access tokens 1354 1355 def _getTokens(self): 1356 if not self._tokens: 1357 raise RTHandlerException('must process tokens before calling split') 1358 return self._tokens 1359 1360 def _setTokens(self, tokens): 1361 '''Assign tokens to this Handler. 1362 ''' 1363 self._tokens = tokens 1364 1365 tokens = property(_getTokens, _setTokens, 1366 doc='''Get or set tokens for this Handler. 1367 ''') 1368 1369 def __len__(self): 1370 return len(self._tokens) 1371 1372 def __add__(self, other): 1373 '''Return a new handler adding the tokens in both 1374 ''' 1375 rth = self.__class__() # will get the same class type 1376 rth.tokens = self._tokens + other._tokens 1377 return rth 1378 1379 1380# ------------------------------------------------------------------------------ 1381 1382class RTFile(prebase.ProtoM21Object): 1383 ''' 1384 Roman Text File access. 1385 ''' 1386 1387 def __init__(self): 1388 self.file = None 1389 self.filename = None 1390 1391 def open(self, filename): 1392 '''Open a file for reading, trying a variety of encodings and then 1393 trying them again with an ignore if it is not possible. 1394 ''' 1395 for encoding in ('utf-8', 'macintosh', 'latin-1', 'utf-16'): 1396 try: 1397 # pylint: disable=consider-using-with 1398 self.file = io.open(filename, encoding=encoding) 1399 if self.file is not None: 1400 break 1401 except UnicodeDecodeError: 1402 pass 1403 if self.file is None: 1404 for encoding in ('utf-8', 'macintosh', 'latin-1', 'utf-16', None): 1405 try: 1406 # pylint: disable=consider-using-with 1407 self.file = io.open(filename, encoding=encoding, errors='ignore') 1408 if self.file is not None: 1409 break 1410 except UnicodeDecodeError: 1411 pass 1412 if self.file is None: 1413 raise RomanTextException( 1414 f'Cannot parse file {filename}, possibly a broken codec?') 1415 1416 self.filename = filename 1417 1418 def openFileLike(self, fileLike): 1419 '''Assign a file-like object, such as those provided by StringIO, as an 1420 open file object. 1421 ''' 1422 self.file = fileLike # already 'open' 1423 1424 def _reprInternal(self): 1425 return '' 1426 1427 def close(self): 1428 self.file.close() 1429 1430 def read(self): 1431 '''Read a file. Note that this calls readstr, which processes all tokens. 1432 1433 If `number` is given, a work number will be extracted if possible. 1434 ''' 1435 return self.readstr(self.file.read()) 1436 1437 def readstr(self, strSrc): 1438 '''Read a string and process all Tokens. Returns a ABCHandler instance. 1439 ''' 1440 handler = RTHandler() 1441 # return the handler instance 1442 handler.process(strSrc) 1443 return handler 1444 1445 1446# ------------------------------------------------------------------------------ 1447 1448class Test(unittest.TestCase): 1449 1450 def testBasicA(self): 1451 from music21.romanText import testFiles 1452 for fileStr in testFiles.ALL: 1453 f = RTFile() 1454 unused_rth = f.readstr(fileStr) # get a handler from a string 1455 1456 def testReA(self): 1457 # gets the index of the end of the measure indication 1458 g = reMeasureTag.match('m1 g: V b2 i') 1459 self.assertEqual(g.end(), 2) 1460 self.assertEqual(g.group(0), 'm1') 1461 1462 self.assertEqual(reMeasureTag.match('Time Signature: 2/2'), None) 1463 1464 g = reMeasureTag.match('m3-4=m1-2') 1465 self.assertEqual(g.end(), 4) 1466 self.assertEqual(g.start(), 0) 1467 self.assertEqual(g.group(0), 'm3-4') 1468 1469 g = reMeasureTag.match('m123-432=m1120-24234') 1470 self.assertEqual(g.group(0), 'm123-432') 1471 1472 g = reMeasureTag.match('m231a IV6 b4 C: V') 1473 self.assertEqual(g.group(0), 'm231a') 1474 1475 g = reMeasureTag.match('m123b-432b=m1120a-24234a') 1476 self.assertEqual(g.group(0), 'm123b-432b') 1477 1478 g = reMeasureTag.match('m231var1 IV6 b4 C: V') 1479 self.assertEqual(g.group(0), 'm231') 1480 1481 # this only works if it starts the string 1482 g = reVariant.match('var1 IV6 b4 C: V') 1483 self.assertEqual(g.group(0), 'var1') 1484 1485 g = reAnalyticKeyAtom.match('Bb:') 1486 self.assertEqual(g.group(0), 'Bb:') 1487 g = reAnalyticKeyAtom.match('F#:') 1488 self.assertEqual(g.group(0), 'F#:') 1489 g = reAnalyticKeyAtom.match('f#:') 1490 self.assertEqual(g.group(0), 'f#:') 1491 g = reAnalyticKeyAtom.match('b:') 1492 self.assertEqual(g.group(0), 'b:') 1493 g = reAnalyticKeyAtom.match('bb:') 1494 self.assertEqual(g.group(0), 'bb:') 1495 g = reAnalyticKeyAtom.match('g:') 1496 self.assertEqual(g.group(0), 'g:') 1497 1498 # beats do not have a colon 1499 self.assertEqual(reKeyAtom.match('b2'), None) 1500 self.assertEqual(reKeyAtom.match('b2.5'), None) 1501 1502 g = reBeatAtom.match('b2.5') 1503 self.assertEqual(g.group(0), 'b2.5') 1504 1505 g = reBeatAtom.match('bVII') 1506 self.assertEqual(g, None) 1507 1508 g = reBeatAtom.match('b1.66.5') 1509 self.assertEqual(g.group(0), 'b1.66.5') 1510 1511 def testMeasureAttributeProcessing(self): 1512 rtm = RTMeasure('m17var1 vi b2 IV b2.5 viio6/4 b3.5 I') 1513 self.assertEqual(rtm.data, 'vi b2 IV b2.5 viio6/4 b3.5 I') 1514 self.assertEqual(rtm.number, [17]) 1515 self.assertEqual(rtm.tag, 'm17') 1516 self.assertEqual(rtm.variantNumber, 1) 1517 1518 rtm = RTMeasure('m17varC vi b2 IV b2.5 viio6/4 b3.5 I') 1519 self.assertEqual(rtm.data, 'vi b2 IV b2.5 viio6/4 b3.5 I') 1520 self.assertEqual(rtm.variantLetter, 'C') 1521 1522 rtm = RTMeasure('m20 vi b2 ii6/5 b3 V b3.5 V7') 1523 self.assertEqual(rtm.data, 'vi b2 ii6/5 b3 V b3.5 V7') 1524 self.assertEqual(rtm.number, [20]) 1525 self.assertEqual(rtm.tag, 'm20') 1526 self.assertEqual(rtm.variantNumber, None) 1527 self.assertFalse(rtm.isCopyDefinition) 1528 1529 rtm = RTMeasure('m0 b3 G: I') 1530 self.assertEqual(rtm.data, 'b3 G: I') 1531 self.assertEqual(rtm.number, [0]) 1532 self.assertEqual(rtm.tag, 'm0') 1533 self.assertEqual(rtm.variantNumber, None) 1534 self.assertFalse(rtm.isCopyDefinition) 1535 1536 rtm = RTMeasure('m59 = m57') 1537 self.assertEqual(rtm.data, '= m57') 1538 self.assertEqual(rtm.number, [59]) 1539 self.assertEqual(rtm.tag, 'm59') 1540 self.assertEqual(rtm.variantNumber, None) 1541 self.assertTrue(rtm.isCopyDefinition) 1542 1543 rtm = RTMeasure('m3-4 = m1-2') 1544 self.assertEqual(rtm.data, '= m1-2') 1545 self.assertEqual(rtm.number, [3, 4]) 1546 self.assertEqual(rtm.tag, 'm3-4') 1547 self.assertEqual(rtm.variantNumber, None) 1548 self.assertTrue(rtm.isCopyDefinition) 1549 1550 def testTokenDefinition(self): 1551 # test that we are always getting the right number of tokens 1552 from music21.romanText import testFiles 1553 1554 rth = RTHandler() 1555 rth.process(testFiles.mozartK279) 1556 1557 count = 0 1558 for t in rth._tokens: 1559 if t.isMovement(): 1560 count += 1 1561 self.assertEqual(count, 3) 1562 1563 rth.process(testFiles.riemenschneider001) 1564 count = 0 1565 for t in rth._tokens: 1566 if t.isMeasure(): 1567 # print(t.src) 1568 count += 1 1569 # 21, 2 variants, and one pickup 1570 self.assertEqual(count, 21 + 2 + 1) 1571 1572 count = 0 1573 for t in rth._tokens: 1574 if t.isMeasure(): 1575 for a in t.atoms: 1576 if isinstance(a, RTAnalyticKey): 1577 count += 1 1578 self.assertEqual(count, 1) 1579 1580 1581# ------------------------------------------------------------------------------ 1582# define presented order in documentation 1583# _DOC_ORDER = [] 1584 1585if __name__ == '__main__': 1586 import music21 1587 music21.mainTest(Test) 1588 1589