1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: romanText/translate.py 4# Purpose: Translation routines for roman numeral analysis text files 5# 6# Authors: Christopher Ariza 7# Michael Scott Cuthbert 8# 9# Copyright: Copyright © 2011-2012, 2016, 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. Also used for the ClercqTemperley 15format which is similar but a little different. 16 17This module is really only needed for people extending the parser, 18for others it's simple to get Harmony, RomanNumeral, Key (or KeySignature) 19and other objects out of an rntxt file by running this: 20 21 22>>> monteverdi = corpus.parse('monteverdi/madrigal.3.1.rntxt') 23>>> monteverdi.show('text') 24{0.0} <music21.metadata.Metadata object at 0x...> 25{0.0} <music21.stream.Part ...> 26 {0.0} <music21.stream.Measure 1 offset=0.0> 27 {0.0} <music21.key.KeySignature of 1 flat> 28 {0.0} <music21.meter.TimeSignature 4/4> 29 {0.0} <music21.roman.RomanNumeral vi in F major> 30 {3.0} <music21.roman.RomanNumeral V[no3] in F major> 31 {4.0} <music21.stream.Measure 2 offset=4.0> 32 {0.0} <music21.roman.RomanNumeral I in F major> 33 {3.0} <music21.roman.RomanNumeral IV in F major> 34 ... 35 36Then the stream can be analyzed with something like this, storing 37the data to make a histogram of scale degree usage within a key: 38 39>>> degreeDictionary = {} 40>>> for el in monteverdi.recurse(): 41... if isinstance(el, roman.RomanNumeral): 42... print(f'{el.figure} {el.key}') 43... for p in el.pitches: 44... degree, accidental = el.key.getScaleDegreeAndAccidentalFromPitch(p) 45... if accidental is None: 46... degreeString = str(degree) 47... else: 48... degreeString = str(degree) + str(accidental.modifier) 49... if degreeString not in degreeDictionary: 50... degreeDictionary[degreeString] = 1 51... else: 52... degreeDictionary[degreeString] += 1 53... degTuple = (str(p), degreeString) 54... print(degTuple) 55 vi F major 56 ('D5', '6') 57 ('F5', '1') 58 ('A5', '3') 59 V[no3] F major 60 ('C5', '5') 61 ('G5', '2') 62 I F major 63 ('F4', '1') 64 ('A4', '3') 65 ('C5', '5') 66 ... 67 V6 g minor 68 ('F#5', '7#') 69 ('A5', '2') 70 ('D6', '5') 71 i g minor 72 ('G4', '1') 73 ('B-4', '3') 74 ('D5', '5') 75 ... 76 77Now if we'd like we can get a Histogram of the data. 78It's a little complex, but worth seeing in full: 79 80>>> import operator 81>>> histogram = graph.primitives.GraphHistogram() 82>>> i = 0 83>>> data = [] 84>>> xLabels = [] 85>>> values = [] 86>>> ddList = list(degreeDictionary.items()) 87>>> for deg,value in sorted(ddList, key=operator.itemgetter(1), reverse=True): 88... data.append((i, degreeDictionary[deg]), ) 89... xLabels.append((i+.5, deg), ) 90... values.append(degreeDictionary[deg]) 91... i += 1 92>>> histogram.data = data 93 94 95These commands give nice labels for the data; optional: 96 97>>> histogram.setIntegerTicksFromData(values, 'y') 98>>> histogram.setTicks('x', xLabels) 99>>> histogram.setAxisLabel('x', 'ScaleDegree') 100 101Now generate the histogram: 102 103>>> #_DOCS_HIDE histogram.process() 104 105.. image:: images/romanTranslatePitchDistribution.* 106 :width: 600 107 108 109OMIT_FROM_DOCS 110 111>>> x = converter.parse('romantext: m1 a: VI') 112>>> [str(p) for p in x[roman.RomanNumeral].first().pitches] 113['F5', 'A5', 'C6'] 114 115>>> x = converter.parse('romantext: m1 a: vi') 116>>> [str(p) for p in x[roman.RomanNumeral].first().pitches] 117['F#5', 'A5', 'C#6'] 118 119>>> [str(p) for p in 120... converter.parse('romantext: m1 a: vio' 121... )[roman.RomanNumeral].first().pitches] 122['F#5', 'A5', 'C6'] 123''' 124import copy 125import traceback 126import unittest 127 128from music21 import bar 129from music21 import base 130from music21 import common 131from music21 import exceptions21 132from music21 import harmony 133from music21 import key 134from music21 import metadata 135from music21 import meter 136from music21 import note 137from music21 import roman 138from music21 import stream 139from music21 import tie 140from music21.romanText import rtObjects 141 142from music21 import environment 143_MOD = 'romanText.translate' 144environLocal = environment.Environment(_MOD) 145 146ROMANTEXT_VERSION = 1.0 147 148 149USE_RN_CACHE = False 150# Not currently using rnCache because of problems with PivotChords, 151# See mail from Dmitri, 30 September 2014 152 153# ------------------------------------------------------------------------------ 154 155 156class RomanTextTranslateException(exceptions21.Music21Exception): 157 pass 158 159 160class RomanTextUnprocessedToken(base.ElementWrapper): 161 pass 162 163 164class RomanTextUnprocessedMetadata(base.Music21Object): 165 def __init__(self, tag='', data=''): 166 super().__init__() 167 self.tag = tag 168 self.data = data 169 170 def _reprInternal(self) -> str: 171 return f'{self.tag}: {self.data}' 172 173 174def _copySingleMeasure(t, p, kCurrent): 175 ''' 176 Given a RomanText token, a Part used as the current container, 177 and the current Key, return a Measure copied from the past of the Part. 178 179 This is used in cases of definitions such as: 180 m23=m21 181 ''' 182 m = None 183 # copy from a past location; need to change key 184 # environLocal.printDebug(['calling _copySingleMeasure()']) 185 targetNumber, unused_targetRepeat = t.getCopyTarget() 186 if len(targetNumber) > 1: # pragma: no cover 187 # this is an encoding error 188 raise RomanTextTranslateException( 189 'a single measure cannot define a copy operation for multiple measures') 190 # TODO: ignoring repeat letters 191 target = targetNumber[0] 192 for mPast in p.getElementsByClass('Measure'): 193 if mPast.number == target: 194 try: 195 m = copy.deepcopy(mPast) 196 except TypeError: # pragma: no cover 197 raise RomanTextTranslateException( 198 f'Failed to copy measure {mPast.number}:' 199 + ' did you perhaps parse an RTOpus object with romanTextToStreamScore ' 200 + 'instead of romanTextToStreamOpus?') 201 m.number = t.number[0] 202 # update all keys 203 for rnPast in m.getElementsByClass('RomanNumeral'): 204 if kCurrent is None: # pragma: no cover 205 # should not happen 206 raise RomanTextTranslateException( 207 'attempting to copy a measure but no past key definitions are found') 208 if rnPast.editorial.get('followsKeyChange'): 209 kCurrent = rnPast.key 210 elif rnPast.pivotChord is not None: 211 kCurrent = rnPast.pivotChord.key 212 else: 213 rnPast.key = kCurrent 214 if rnPast.secondaryRomanNumeral is not None: 215 newRN = roman.RomanNumeral(rnPast.figure, kCurrent) 216 newRN.duration = copy.deepcopy(rnPast.duration) 217 newRN.lyrics = copy.deepcopy(rnPast.lyrics) 218 m.replace(rnPast, newRN) 219 220 break 221 return m, kCurrent 222 223 224def _copyMultipleMeasures(t, p, kCurrent): 225 ''' 226 Given a RomanText token for a RTMeasure, a 227 Part used as the current container, and the current Key, 228 return a Measure range copied from the past of the Part. 229 230 This is used for cases such as: 231 m23-25 = m20-22 232 ''' 233 # the key provided needs to be the current key 234 # environLocal.printDebug(['calling _copyMultipleMeasures()']) 235 236 targetNumbers, unused_targetRepeat = t.getCopyTarget() 237 if len(targetNumbers) == 1: # pragma: no cover 238 # this is an encoding error 239 raise RomanTextTranslateException('a multiple measure range cannot copy a single measure') 240 # TODO: ignoring repeat letters 241 targetStart = targetNumbers[0] 242 targetEnd = targetNumbers[1] 243 244 if t.number[1] - t.number[0] != targetEnd - targetStart: # pragma: no cover 245 raise RomanTextTranslateException( 246 'both the source and destination sections need to have the same number of measures') 247 if t.number[0] < targetEnd: # pragma: no cover 248 raise RomanTextTranslateException( 249 'the source section cannot overlap with the destination section') 250 251 measures = [] 252 for mPast in p.getElementsByClass('Measure'): 253 if mPast.number in range(targetStart, targetEnd + 1): 254 try: 255 m = copy.deepcopy(mPast) 256 except TypeError: # pragma: no cover 257 raise RomanTextTranslateException( 258 'Failed to copy measure {0} to measure range {1}-{2}: '.format( 259 mPast.number, targetStart, targetEnd) 260 + 'did you perhaps parse an RTOpus object with romanTextToStreamScore ' 261 + 'instead of romanTextToStreamOpus?') 262 263 m.number = t.number[0] + mPast.number - targetStart 264 measures.append(m) 265 # update all keys 266 allRNs = list(m.getElementsByClass('RomanNumeral')) 267 for rnPast in allRNs: 268 if kCurrent is None: # pragma: no cover 269 # should not happen 270 raise RomanTextTranslateException( 271 'attempting to copy a measure but no past key definitions are found') 272 if rnPast.editorial.get('followsKeyChange'): 273 kCurrent = rnPast.key 274 elif rnPast.pivotChord is not None: 275 kCurrent = rnPast.pivotChord.key 276 else: 277 rnPast.key = kCurrent 278 if rnPast.secondaryRomanNumeral is not None: 279 newRN = roman.RomanNumeral(rnPast.figure, kCurrent) 280 newRN.duration = copy.deepcopy(rnPast.duration) 281 newRN.lyrics = copy.deepcopy(rnPast.lyrics) 282 m.replace(rnPast, newRN) 283 284 if mPast.number == targetEnd: 285 break 286 return measures, kCurrent 287 288 289def _getKeyAndPrefix(rtKeyOrString): 290 '''Given an RTKey specification, return the Key and a string prefix based 291 on the tonic: 292 293 >>> romanText.translate._getKeyAndPrefix('c') 294 (<music21.key.Key of c minor>, 'c: ') 295 >>> romanText.translate._getKeyAndPrefix('F#') 296 (<music21.key.Key of F# major>, 'F#: ') 297 >>> romanText.translate._getKeyAndPrefix('Eb') 298 (<music21.key.Key of E- major>, 'E-: ') 299 >>> romanText.translate._getKeyAndPrefix('Bb') 300 (<music21.key.Key of B- major>, 'B-: ') 301 >>> romanText.translate._getKeyAndPrefix('bb') 302 (<music21.key.Key of b- minor>, 'b-: ') 303 >>> romanText.translate._getKeyAndPrefix('b#') 304 (<music21.key.Key of b# minor>, 'b#: ') 305 ''' 306 if isinstance(rtKeyOrString, str): 307 rtKeyOrString = key.convertKeyStringToMusic21KeyString(rtKeyOrString) 308 k = key.Key(rtKeyOrString) 309 else: 310 k = rtKeyOrString.getKey() 311 tonicName = k.tonic.name 312 if k.mode == 'minor': 313 tonicName = tonicName.lower() 314 prefix = tonicName + ': ' 315 return k, prefix 316 317 318# Cache each of the created keys so that we don't recreate them. 319_rnKeyCache = {} 320 321 322class PartTranslator: 323 ''' 324 A refactoring of the previously massive romanTextToStreamScore function 325 to allow for more fine-grained testing (eventually), and to 326 get past the absurdly high number of nested blocks (the previous translator 327 was written under severe time constraints). 328 ''' 329 330 def __init__(self, md=None): 331 if md is None: 332 md = metadata.Metadata() 333 self.md = md # global metadata object 334 self.p = stream.Part() 335 336 self.romanTextVersion = ROMANTEXT_VERSION 337 338 # ts indication are found in header, and also found elsewhere 339 self.tsCurrent = meter.TimeSignature('4/4') # create default 4/4 340 self.tsAtTimeOfLastChord = self.tsCurrent 341 self.tsSet = False # store if set to a measure 342 self.lastMeasureToken = None 343 self.lastMeasureNumber = 0 344 self.previousRn = None 345 self.keySigCurrent = None 346 self.setKeySigFromFirstKeyToken = True # set a keySignature 347 self.foundAKeySignatureSoFar = False 348 self.kCurrent, unused_prefixLyric = _getKeyAndPrefix('C') # default if none defined 349 self.prefixLyric = '' 350 351 self.sixthMinor = roman.Minor67Default.CAUTIONARY 352 self.seventhMinor = roman.Minor67Default.CAUTIONARY 353 354 self.repeatEndings = {} 355 356 # reset for each measure 357 self.currentMeasureToken = None 358 self.previousChordInMeasure = None 359 self.pivotChordPossible = False 360 self.numberOfAtomsInCurrentMeasure = 0 361 self.setKeyChangeToken = False 362 self.currentOffsetInMeasure = 0.0 363 364 def translateTokens(self, tokens): 365 for t in tokens: 366 try: 367 self.translateOneLineToken(t) 368 except Exception: # pylint: disable=broad-except 369 tracebackMessage = traceback.format_exc() 370 raise RomanTextTranslateException( 371 f'At line {t.lineNumber} for token {t}, ' 372 + f'an exception was raised: \n{tracebackMessage}') 373 374 p = self.p 375 p.coreElementsChanged() 376 fixPickupMeasure(p) 377 p.makeBeams(inPlace=True) 378 p.makeAccidentals(inPlace=True) 379 _addRepeatsFromRepeatEndings(p, self.repeatEndings) # 1st and second endings... 380 return p 381 382 def translateOneLineToken(self, t): 383 # noinspection SpellCheckingInspection 384 ''' 385 Translates one token t and set the current settings. 386 387 A token in this case consists of an entire line's worth. 388 It might be a token such as 'Title: Neko Funjatta' or 389 a composite token such as 'm23 b4 IV6' 390 ''' 391 md = self.md 392 # environLocal.printDebug(['token', t]) 393 394 # most common case first... 395 if t.isMeasure(): 396 self.translateMeasureLineToken(t) 397 398 elif t.isTitle(): 399 md.title = t.data 400 401 elif t.isWork(): 402 md.alternativeTitle = t.data 403 404 elif t.isPiece(): 405 md.alternativeTitle = t.data 406 407 elif t.isComposer(): 408 md.composer = t.data 409 410 elif t.isMovement(): 411 md.movementNumber = t.data 412 413 elif t.isTimeSignature(): 414 try: 415 self.tsCurrent = meter.TimeSignature(t.data) 416 self.tsSet = False 417 except exceptions21.Music21Exception: # pragma: no cover 418 environLocal.warn(f'Could not parse TimeSignature tag: {t.data!r}') 419 420 # environLocal.printDebug(['tsCurrent:', tsCurrent]) 421 422 elif t.isKeySignature(): 423 self.parseKeySignatureTag(t) 424 425 elif t.isSixthMinor() or t.isSeventhMinor(): 426 self.setMinorRootParse(t) 427 428 elif t.isVersion(): 429 try: 430 self.romanTextVersion = float(t.data) 431 except ValueError: # pragma: no cover 432 environLocal.warn(f'Could not parse RTVersion tag: {t.data!r}') 433 434 elif isinstance(t, rtObjects.RTTagged): 435 otherMetadata = RomanTextUnprocessedMetadata(t.tag, t.data) 436 self.p.append(otherMetadata) 437 438 else: # pragma: no cover 439 unprocessed = RomanTextUnprocessedToken(t) 440 self.p.append(unprocessed) 441 442 def setMinorRootParse(self, t): 443 ''' 444 Set Roman Numeral parsing standards from a token. 445 446 >>> pt = romanText.translate.PartTranslator() 447 >>> pt.sixthMinor 448 <Minor67Default.CAUTIONARY: 2> 449 450 >>> tag = romanText.rtObjects.RTTagged('SixthMinor: Flat') 451 >>> tag.isSixthMinor() 452 True 453 >>> pt.setMinorRootParse(tag) 454 >>> pt.sixthMinor 455 <Minor67Default.FLAT: 4> 456 457 Harmonic sets to FLAT for sixth and SHARP for seventh 458 459 >>> for config in 'flat sharp quality cautionary harmonic'.split(): 460 ... tag = romanText.rtObjects.RTTagged('Seventh Minor: ' + config) 461 ... pt.setMinorRootParse(tag) 462 ... print(pt.seventhMinor) 463 Minor67Default.FLAT 464 Minor67Default.SHARP 465 Minor67Default.QUALITY 466 Minor67Default.CAUTIONARY 467 Minor67Default.SHARP 468 469 >>> tag = romanText.rtObjects.RTTagged('Sixth Minor: harmonic') 470 >>> pt.setMinorRootParse(tag) 471 >>> print(pt.sixthMinor) 472 Minor67Default.FLAT 473 474 475 Unknown settings raise a `RomanTextTranslateException` 476 477 >>> tag = romanText.rtObjects.RTTagged('Seventh Minor: asdf') 478 >>> pt.setMinorRootParse(tag) 479 Traceback (most recent call last): 480 music21.romanText.translate.RomanTextTranslateException: 481 Cannot parse setting vi or vii parsing: 'asdf' 482 ''' 483 tData = t.data.lower() 484 if tData == 'flat': 485 tEnum = roman.Minor67Default.FLAT 486 elif tData == 'sharp': 487 tEnum = roman.Minor67Default.SHARP 488 elif tData == 'quality': 489 tEnum = roman.Minor67Default.QUALITY 490 elif tData in ('courtesy', 'cautionary'): 491 tEnum = roman.Minor67Default.CAUTIONARY 492 elif tData == 'harmonic': 493 if t.isSixthMinor(): 494 tEnum = roman.Minor67Default.FLAT 495 else: 496 tEnum = roman.Minor67Default.SHARP 497 else: 498 raise RomanTextTranslateException( 499 f'Cannot parse setting vi or vii parsing: {tData!r}') 500 501 if t.isSixthMinor(): 502 self.sixthMinor = tEnum 503 else: 504 self.seventhMinor = tEnum 505 506 def translateMeasureLineToken(self, t): 507 ''' 508 Translate a measure token consisting of a single line such as:: 509 510 m21 b3 V b4 C: IV 511 512 Or it might be a variant measure, or a copy instruction. 513 ''' 514 p = self.p 515 skipsPriorMeasures = ((t.number[0] > self.lastMeasureNumber + 1) 516 and (self.previousRn is not None)) 517 isSingleMeasureCopy = (len(t.number) == 1 and t.isCopyDefinition) 518 isMultipleMeasureCopy = (len(t.number) > 1) 519 520 # environLocal.printDebug(['handling measure token:', t]) 521 # if t.number[0] % 10 == 0: 522 # print('at number ' + str(t.number[0])) 523 if t.variantNumber is not None: 524 # TODO(msc): parse variant numbers 525 # environLocal.printDebug([f' skipping variant: {t}']) 526 return 527 if t.variantLetter is not None: 528 # TODO(msc): parse variant letters 529 # environLocal.printDebug([f' skipping variant: {t}']) 530 return 531 532 # if this measure number is more than 1 greater than the last 533 # defined measure number, and the previous chord is not None, 534 # then fill with copies of the last-defined measure 535 if skipsPriorMeasures: 536 self.fillToMeasureToken(t) 537 538 # create a new measure or copy a past measure 539 if isSingleMeasureCopy: # if not a range 540 p.coreElementsChanged() 541 m, self.kCurrent = _copySingleMeasure(t, p, self.kCurrent) 542 p.coreAppend(m) 543 self.lastMeasureNumber = m.number 544 self.lastMeasureToken = t 545 romans = m.getElementsByClass(roman.RomanNumeral) 546 if romans: 547 self.previousRn = romans[-1] 548 549 elif isMultipleMeasureCopy: 550 p.coreElementsChanged() 551 measures, self.kCurrent = _copyMultipleMeasures(t, p, self.kCurrent) 552 p.append(measures) # appendCore does not work with list 553 self.lastMeasureNumber = measures[-1].number 554 self.lastMeasureToken = t 555 romans = measures[-1].getElementsByClass(roman.RomanNumeral) 556 if romans: 557 self.previousRn = romans[-1] 558 559 else: 560 m = self.translateSingleMeasure(t) 561 p.coreAppend(m) 562 563 def fillToMeasureToken(self, t): 564 ''' 565 Create a series of measures which extend the previous RN until the measure number 566 implied by t. 567 ''' 568 p = self.p 569 for i in range(self.lastMeasureNumber + 1, t.number[0]): 570 mFill = stream.Measure() 571 mFill.number = i 572 if self.previousRn is not None: 573 newRn = copy.deepcopy(self.previousRn) 574 newRn.lyric = '' 575 # set to entire bar duration and tie 576 newRn.duration = copy.deepcopy(self.tsAtTimeOfLastChord.barDuration) 577 if self.previousRn.tie is None: 578 self.previousRn.tie = tie.Tie('start') 579 else: 580 self.previousRn.tie.type = 'continue' 581 # set to stop for now; may extend on next iteration 582 newRn.tie = tie.Tie('stop') 583 self.previousRn = newRn 584 mFill.append(newRn) 585 appendMeasureToRepeatEndingsDict(self.lastMeasureToken, 586 mFill, 587 self.repeatEndings, i) 588 p.coreAppend(mFill) 589 self.lastMeasureNumber = t.number[0] - 1 590 self.lastMeasureToken = t 591 592 def parseKeySignatureTag(self, t): 593 ''' 594 Parse a key signature tag which has already been determined to 595 be a key signature. 596 597 >>> tag = romanText.rtObjects.RTTagged('KeySignature: -4') 598 >>> tag.isKeySignature() 599 True 600 >>> tag.data 601 '-4' 602 603 >>> pt = romanText.translate.PartTranslator() 604 >>> pt.keySigCurrent is None 605 True 606 >>> pt.setKeySigFromFirstKeyToken 607 True 608 >>> pt.foundAKeySignatureSoFar 609 False 610 611 >>> pt.parseKeySignatureTag(tag) 612 >>> pt.keySigCurrent 613 <music21.key.KeySignature of 4 flats> 614 >>> pt.setKeySigFromFirstKeyToken 615 False 616 >>> pt.foundAKeySignatureSoFar 617 True 618 619 >>> tag = romanText.rtObjects.RTTagged('KeySignature: xyz') 620 >>> pt.parseKeySignatureTag(tag) 621 Traceback (most recent call last): 622 music21.romanText.translate.RomanTextTranslateException: 623 Cannot parse key signature: 'xyz' 624 ''' 625 data = t.data 626 if data == '': 627 self.keySigCurrent = key.KeySignature(0) 628 elif data == 'Bb': 629 self.keySigCurrent = key.KeySignature(-1) 630 else: 631 try: 632 dataVal = int(data) 633 self.keySigCurrent = key.KeySignature(dataVal) 634 except ValueError: 635 raise RomanTextTranslateException(f'Cannot parse key signature: {data!r}') 636 self.setKeySigFromFirstKeyToken = False 637 # environLocal.printDebug(['keySigCurrent:', keySigCurrent]) 638 self.foundAKeySignatureSoFar = True 639 640 def translateSingleMeasure(self, measureToken): 641 ''' 642 Given a measureToken, return a `stream.Measure` object with 643 the appropriate atoms set. 644 ''' 645 self.currentMeasureToken = measureToken 646 m = stream.Measure() 647 m.number = measureToken.number[0] 648 appendMeasureToRepeatEndingsDict(measureToken, m, self.repeatEndings) 649 self.lastMeasureNumber = measureToken.number[0] 650 self.lastMeasureToken = measureToken 651 652 if not self.tsSet: 653 m.timeSignature = self.tsCurrent 654 self.tsSet = True # only set when changed 655 if not self.setKeySigFromFirstKeyToken and self.keySigCurrent is not None: 656 m.insert(0, self.keySigCurrent) 657 self.setKeySigFromFirstKeyToken = True # only set when changed 658 659 self.currentOffsetInMeasure = 0.0 # start offsets at zero 660 self.previousChordInMeasure = None 661 self.pivotChordPossible = False 662 self.numberOfAtomsInCurrentMeasure = len(measureToken.atoms) 663 # first RomanNumeral object after a key change should have this set to True 664 self.setKeyChangeToken = False 665 666 for i, a in enumerate(measureToken.atoms): 667 isLastAtomInMeasure = (i == self.numberOfAtomsInCurrentMeasure - 1) 668 self.translateSingleMeasureAtom(a, m, isLastAtomInMeasure=isLastAtomInMeasure) 669 670 # may need to adjust duration of last chord added 671 if self.tsCurrent is not None: 672 self.previousRn.quarterLength = (self.tsCurrent.barDuration.quarterLength 673 - self.currentOffsetInMeasure) 674 m.coreElementsChanged() 675 return m 676 677 def translateSingleMeasureAtom(self, a, m, *, isLastAtomInMeasure=False): 678 ''' 679 Translate a single atom in a measure token. 680 681 a is the Atom 682 m is a `stream.Measure` object. 683 684 Uses coreInsert and coreAppend methods, so must have `m.coreElementsChanged()` 685 called afterwards. 686 ''' 687 if (isinstance(a, rtObjects.RTKey) 688 or (self.foundAKeySignatureSoFar is False 689 and isinstance(a, rtObjects.RTAnalyticKey))): 690 self.setAnalyticKey(a) 691 # insert at beginning of measure if at beginning 692 # -- for things like pickups. 693 if m.number <= 1: 694 m.coreInsert(0, self.kCurrent) 695 else: 696 m.coreInsert(self.currentOffsetInMeasure, self.kCurrent) 697 self.foundAKeySignatureSoFar = True 698 699 elif isinstance(a, rtObjects.RTKeySignature): 700 try: # this sets the keysignature but not the prefix text 701 thisSig = a.getKeySignature() 702 except (exceptions21.Music21Exception, ValueError): # pragma: no cover 703 raise RomanTextTranslateException( 704 f'cannot get key from {a.src} in line {self.currentMeasureToken.src}') 705 # insert at beginning of measure if at beginning 706 # -- for things like pickups. 707 if m.number <= 1: 708 m.coreInsert(0, thisSig) 709 else: 710 m.coreInsert(self.currentOffsetInMeasure, thisSig) 711 self.foundAKeySignatureSoFar = True 712 713 elif isinstance(a, rtObjects.RTAnalyticKey): 714 self.setAnalyticKey(a) 715 716 elif isinstance(a, rtObjects.RTBeat): 717 # set new offset based on beat 718 try: 719 newOffset = a.getOffset(self.tsCurrent) 720 except ValueError: # pragma: no cover 721 raise RomanTextTranslateException( 722 'cannot properly get an offset from ' 723 + f'beat data {a.src}' 724 + 'under timeSignature {0} in line {1}'.format( 725 self.tsCurrent, 726 self.currentMeasureToken.src)) 727 if (self.previousChordInMeasure is None 728 and self.previousRn is not None 729 and newOffset > 0): 730 # setting a new beat before giving any chords 731 firstChord = copy.deepcopy(self.previousRn) 732 firstChord.quarterLength = newOffset 733 firstChord.lyric = '' 734 if self.previousRn.tie is None: 735 self.previousRn.tie = tie.Tie('start') 736 else: 737 self.previousRn.tie.type = 'continue' 738 firstChord.tie = tie.Tie('stop') 739 self.previousRn = firstChord 740 self.previousChordInMeasure = firstChord 741 m.coreInsert(0, firstChord) 742 self.pivotChordPossible = False 743 self.currentOffsetInMeasure = newOffset 744 745 elif isinstance(a, rtObjects.RTNoChord): 746 # use source to evaluation roman 747 self.tsAtTimeOfLastChord = self.tsCurrent 748 cs = harmony.NoChord() 749 m.coreInsert(self.currentOffsetInMeasure, cs) 750 751 rn = note.Rest() 752 if self.pivotChordPossible is False: 753 # probably best to find duration 754 if self.previousChordInMeasure is None: 755 pass # use default duration 756 else: # update duration of previous chord in Measure 757 oPrevious = self.previousChordInMeasure.getOffsetBySite(m) 758 newQL = self.currentOffsetInMeasure - oPrevious 759 if newQL <= 0: # pragma: no cover 760 raise RomanTextTranslateException( 761 f'too many notes in this measure: {self.currentMeasureToken.src}') 762 self.previousChordInMeasure.quarterLength = newQL 763 self.prefixLyric = '' 764 m.coreInsert(self.currentOffsetInMeasure, rn) 765 self.previousChordInMeasure = rn 766 self.previousRn = rn 767 self.pivotChordPossible = False 768 769 elif isinstance(a, rtObjects.RTChord): 770 self.processRTChord(a, m, self.currentOffsetInMeasure) 771 elif isinstance(a, rtObjects.RTRepeat): 772 if self.currentOffsetInMeasure == 0: 773 if isinstance(a, rtObjects.RTRepeatStart): 774 m.leftBarline = bar.Repeat(direction='start') 775 else: 776 rtt = RomanTextUnprocessedToken(a) 777 m.coreInsert(self.currentOffsetInMeasure, rtt) 778 elif (self.tsCurrent is not None 779 and (self.tsCurrent.barDuration.quarterLength == self.currentOffsetInMeasure 780 or isLastAtomInMeasure)): 781 if isinstance(a, rtObjects.RTRepeatStop): 782 m.rightBarline = bar.Repeat(direction='end') 783 else: 784 rtt = RomanTextUnprocessedToken(a) 785 m.coreInsert(self.currentOffsetInMeasure, rtt) 786 else: # mid measure repeat signs 787 rtt = RomanTextUnprocessedToken(a) 788 m.coreInsert(self.currentOffsetInMeasure, rtt) 789 790 else: 791 rtt = RomanTextUnprocessedToken(a) 792 m.coreInsert(self.currentOffsetInMeasure, rtt) 793 # environLocal.warn(f' Got an unknown token: {a}') 794 795 def processRTChord(self, a, m, currentOffset): 796 ''' 797 Process a single RTChord atom. 798 ''' 799 # use source to evaluation roman 800 self.tsAtTimeOfLastChord = self.tsCurrent 801 try: 802 aSrc = a.src 803 # if kCurrent.mode == 'minor': 804 # if aSrc.lower().startswith('vi'): # vi or vii w/ or w/o o 805 # if aSrc.upper() == a.src: # VI or VII to bVI or bVII 806 # aSrc = 'b' + aSrc 807 cacheTuple = (aSrc, self.kCurrent.tonicPitchNameWithCase) 808 if USE_RN_CACHE and cacheTuple in _rnKeyCache: # pragma: no cover 809 # print('Got a match: ' + str(cacheTuple)) 810 # Problems with Caches not picking up pivot chords... 811 # Not faster, see below. 812 rn = copy.deepcopy(_rnKeyCache[cacheTuple]) 813 else: 814 # print('No match for: ' + str(cacheTuple)) 815 rn = roman.RomanNumeral(aSrc, 816 copy.deepcopy(self.kCurrent), 817 sixthMinor=self.sixthMinor, 818 seventhMinor=self.seventhMinor, 819 ) 820 _rnKeyCache[cacheTuple] = rn 821 # surprisingly, not faster... and more dangerous 822 # rn = roman.RomanNumeral(aSrc, kCurrent) 823 # # SLOWEST!!! 824 # rn = roman.RomanNumeral(aSrc, kCurrent.tonicPitchNameWithCase) 825 826 # >>> from timeit import timeit as t 827 # >>> t('roman.RomanNumeral("IV", "c#")', 828 # ... 'from music21 import roman', number=1000) 829 # 45.75 830 # >>> t('roman.RomanNumeral("IV", k)', 831 # ... 'from music21 import roman, key; k = key.Key("c#")', 832 # ... number=1000) 833 # 16.09 834 # >>> t('roman.RomanNumeral("IV", copy.deepcopy(k))', 835 # ... 'from music21 import roman, key; import copy; 836 # ... k = key.Key("c#")', number=1000) 837 # 22.49 838 # # key cache, does not help much... 839 # >>> t('copy.deepcopy(r)', 'from music21 import roman; import copy; 840 # ... r = roman.RomanNumeral("IV", "c#")', number=1000) 841 # 19.01 842 843 if self.setKeyChangeToken is True: 844 rn.editorial.followsKeyChange = True 845 self.setKeyChangeToken = False 846 else: 847 rn.editorial.followsKeyChange = False 848 except (roman.RomanNumeralException, 849 exceptions21.Music21CommonException): # pragma: no cover 850 # environLocal.printDebug(f' cannot create RN from: {a.src}') 851 rn = note.Note() # create placeholder 852 853 if self.pivotChordPossible is False: 854 # probably best to find duration 855 if self.previousChordInMeasure is None: 856 pass # use default duration 857 else: # update duration of previous chord in Measure 858 oPrevious = self.previousChordInMeasure.getOffsetBySite(m) 859 newQL = currentOffset - oPrevious 860 if newQL <= 0: # pragma: no cover 861 raise RomanTextTranslateException( 862 f'too many notes in this measure: {self.currentMeasureToken.src}') 863 self.previousChordInMeasure.quarterLength = newQL 864 865 rn.addLyric(self.prefixLyric + a.src) 866 self.prefixLyric = '' 867 m.coreInsert(currentOffset, rn) 868 self.previousChordInMeasure = rn 869 self.previousRn = rn 870 self.pivotChordPossible = True 871 else: 872 self.previousChordInMeasure.lyric += '//' + self.prefixLyric + a.src 873 self.previousChordInMeasure.pivotChord = rn 874 self.prefixLyric = '' 875 self.pivotChordPossible = False 876 877 def setAnalyticKey(self, a): 878 ''' 879 Indicates a change in the analyzed key, not a change in anything 880 else, such as the keySignature. 881 ''' 882 try: # this sets the key and the keysignature 883 self.kCurrent, pl = _getKeyAndPrefix(a) 884 self.prefixLyric += pl 885 except: # pragma: no cover 886 raise RomanTextTranslateException( 887 f'cannot get analytic key from {a.src} in line {self.currentMeasureToken.src}') 888 self.setKeyChangeToken = True 889 890 891def romanTextToStreamScore(rtHandler, inputM21=None): 892 ''' 893 The main processing module for single-movement RomanText works. 894 895 Given a romanText handler or string, return or fill a Score Stream. 896 ''' 897 # accept a string directly; mostly for testing 898 if isinstance(rtHandler, str): 899 rtf = rtObjects.RTFile() 900 tokenedRtHandler = rtf.readstr(rtHandler) # return handler, processes tokens 901 else: 902 tokenedRtHandler = rtHandler 903 904 # this could be just a Stream, but b/c we are creating metadata, 905 # perhaps better to match presentation of other scores. 906 if inputM21 is None: 907 s = stream.Score() 908 else: 909 s = inputM21 910 911 # metadata can be first 912 md = metadata.Metadata() 913 s.insert(0, md) 914 915 partTrans = PartTranslator(md) 916 p = partTrans.translateTokens(tokenedRtHandler.tokens) 917 s.insert(0, p) 918 919 return s 920 921 922letterToNumDict = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8} 923 924 925def appendMeasureToRepeatEndingsDict(t, m, repeatEndings, measureNumber=None): 926 '''Takes an RTMeasure object (t), (which might represent one or more 927 measures; but currently only one) and a music21 stream.Measure object and 928 store it as a tuple in the repeatEndings dictionary to mark where the 929 translator should later mark for adding endings. 930 931 If the optional measureNumber is specified, we use that rather than the 932 token number to add to the dict. 933 934 This does not yet work for skipped measures. 935 936 >>> rtm = romanText.rtObjects.RTMeasure('m15a V6 b1.5 V6/5 b2 I b3 viio6') 937 >>> rtm.repeatLetter 938 ['a'] 939 >>> rtm2 = romanText.rtObjects.RTMeasure('m15b V6 b1.5 V6/5 b2 I') 940 >>> rtm2.repeatLetter 941 ['b'] 942 >>> repeatEndings = {} 943 >>> m1 = stream.Measure() 944 >>> m2 = stream.Measure() 945 >>> romanText.translate.appendMeasureToRepeatEndingsDict(rtm, m1, repeatEndings) 946 >>> repeatEndings 947 {1: [(15, <music21.stream.Measure 0a offset=0.0>)]} 948 >>> romanText.translate.appendMeasureToRepeatEndingsDict(rtm2, m2, repeatEndings) 949 >>> repeatEndings[1], repeatEndings[2] 950 ([(15, <music21.stream.Measure 0a offset=0.0>)], 951 [(15, <music21.stream.Measure 0b offset=0.0>)]) 952 >>> repeatEndings[2][0][1] is m2 953 True 954 ''' 955 if not t.repeatLetter: 956 return 957 958 m.numberSuffix = t.repeatLetter[0] 959 960 for rl in t.repeatLetter: 961 if rl is None or rl == '': 962 continue 963 if rl not in letterToNumDict: # pragma: no cover 964 raise RomanTextTranslateException(f'Improper repeat letter: {rl}') 965 repeatNumber = letterToNumDict[rl] 966 if repeatNumber not in repeatEndings: 967 repeatEndings[repeatNumber] = [] 968 if measureNumber is None: 969 measureTuple = (t.number[0], m) 970 else: 971 measureTuple = (measureNumber, m) 972 repeatEndings[repeatNumber].append(measureTuple) 973 974 975def _consolidateRepeatEndings(repeatEndings): 976 ''' 977 take repeatEndings, which is a dict of integers (repeat ending numbers) each 978 holding a list of tuples of measure numbers and measure objects that get this ending, 979 and return a list where contiguous endings should appear. Each element of the list is a 980 two-element tuple, where the first element is a list of measure objects that should have 981 a bracket and the second element is the repeat number. 982 983 Assumes that the list of measure numbers in each repeatEndings array is sorted. 984 985 For the sake of demo and testing, we will use strings instead of measure objects. 986 987 988 >>> repeatEndings = {1: [(5, 'm5a'), (6, 'm6a'), (17, 'm17'), (18, 'm18'), 989 ... (19, 'm19'), (23, 'm23a')], 990 ... 2: [(5, 'm5b'), (6, 'm6b'), (20, 'm20'), (21, 'm21'), (23, 'm23b')], 991 ... 3: [(23, 'm23c')]} 992 >>> print(romanText.translate._consolidateRepeatEndings(repeatEndings)) 993 [(['m5a', 'm6a'], 1), (['m17', 'm18', 'm19'], 1), (['m23a'], 1), 994 (['m5b', 'm6b'], 2), (['m20', 'm21'], 2), (['m23b'], 2), (['m23c'], 3)] 995 ''' 996 returnList = [] 997 998 for endingNumber in repeatEndings: 999 startMeasureNumber = None 1000 lastMeasureNumber = None 1001 measureList = [] 1002 for measureNumberUnderEnding, measureObject in repeatEndings[endingNumber]: 1003 if startMeasureNumber is None: 1004 startMeasureNumber = measureNumberUnderEnding 1005 lastMeasureNumber = measureNumberUnderEnding 1006 measureList.append(measureObject) 1007 elif measureNumberUnderEnding > lastMeasureNumber + 1: 1008 myTuple = (measureList, endingNumber) 1009 returnList.append(myTuple) 1010 startMeasureNumber = measureNumberUnderEnding 1011 lastMeasureNumber = measureNumberUnderEnding 1012 measureList = [measureObject] 1013 else: 1014 measureList.append(measureObject) 1015 lastMeasureNumber = measureNumberUnderEnding 1016 if startMeasureNumber is not None: 1017 myTuple = (measureList, endingNumber) 1018 returnList.append(myTuple) 1019 1020 return returnList 1021 1022 1023def _addRepeatsFromRepeatEndings(s, repeatEndings): 1024 ''' 1025 given a Stream and the repeatEndings dict, add repeats to the stream... 1026 ''' 1027 from music21 import spanner 1028 consolidatedRepeats = _consolidateRepeatEndings(repeatEndings) 1029 for repeatEndingTuple in consolidatedRepeats: 1030 measureList, endingNumber = repeatEndingTuple[0], repeatEndingTuple[1] 1031 rb = spanner.RepeatBracket(measureList, number=endingNumber) 1032 rbOffset = measureList[0].getOffsetBySite(s) 1033 # Adding repeat bracket to stream at beginning of repeated section. 1034 # Maybe better at end? 1035 s.insert(rbOffset, rb) 1036 # should be 'if not max(endingNumbers)', but we can't tell that for each repeat. 1037 if endingNumber == 1: 1038 if measureList[-1].rightBarline is None: 1039 measureList[-1].rightBarline = bar.Repeat(direction='end') 1040 1041 1042def fixPickupMeasure(partObject): 1043 '''Fix a pickup measure if any. 1044 1045 We determine a pickup measure by being measure 0 and not having an RN 1046 object at the beginning. 1047 1048 Demonstration: an otherwise incorrect part 1049 1050 >>> p = stream.Part() 1051 >>> m0 = stream.Measure() 1052 >>> m0.number = 0 1053 >>> k0 = key.Key('G') 1054 >>> m0.insert(0, k0) 1055 >>> m0.insert(0, meter.TimeSignature('4/4')) 1056 >>> m0.insert(2, roman.RomanNumeral('V', k0)) 1057 >>> m1 = stream.Measure() 1058 >>> m1.number = 1 1059 >>> m2 = stream.Measure() 1060 >>> m2.number = 2 1061 >>> p.insert(0, m0) 1062 >>> p.insert(4, m1) 1063 >>> p.insert(8, m2) 1064 1065 After running fixPickupMeasure() 1066 1067 >>> romanText.translate.fixPickupMeasure(p) 1068 >>> p.show('text') 1069 {0.0} <music21.stream.Measure 0 offset=0.0> 1070 {0.0} <music21.key.Key of G major> 1071 {0.0} <music21.meter.TimeSignature 4/4> 1072 {0.0} <music21.roman.RomanNumeral V in G major> 1073 {2.0} <music21.stream.Measure 1 offset=2.0> 1074 <BLANKLINE> 1075 {6.0} <music21.stream.Measure 2 offset=6.0> 1076 <BLANKLINE> 1077 >>> m0.paddingLeft 1078 2.0 1079 ''' 1080 m0 = partObject.measure(0) 1081 if m0 is None: 1082 return 1083 rnObjects = m0.getElementsByClass('RomanNumeral') 1084 if not rnObjects: 1085 return 1086 if rnObjects[0].offset == 0: 1087 return 1088 newPadding = rnObjects[0].offset 1089 for el in m0: 1090 if el.offset < newPadding: # should be zero for Clefs, etc. 1091 pass 1092 else: 1093 el.offset = el.offset - newPadding 1094 m0.paddingLeft = newPadding 1095 for el in partObject: # adjust all other measures backwards 1096 if el.offset > 0: 1097 el.offset -= newPadding 1098 1099 1100def romanTextToStreamOpus(rtHandler, inputM21=None): 1101 '''The main processing routine for RomanText objects that may or may not 1102 be multi movement. 1103 1104 Takes in a romanText.rtObjects.RTFile() object, or a string as rtHandler. 1105 1106 Runs `romanTextToStreamScore()` as its main work. 1107 1108 If inputM21 is None then it will create a Score or Opus object. 1109 1110 Return either a Score object, or, if a multi-movement work is defined, an 1111 Opus object. 1112 ''' 1113 if isinstance(rtHandler, str): 1114 rtf = rtObjects.RTFile() 1115 rtHandler = rtf.readstr(rtHandler) # return handler, processes tokens 1116 1117 if rtHandler.definesMovements(): # create an opus 1118 if inputM21 is None: 1119 s = stream.Opus() 1120 else: 1121 s = inputM21 1122 # copy the common header to each of the sub-handlers 1123 handlerBundles = rtHandler.splitByMovement(duplicateHeader=True) 1124 # see if we have header information 1125 for h in handlerBundles: 1126 # print(h, len(h)) 1127 # append to opus 1128 s.append(romanTextToStreamScore(h)) 1129 return s # an opus 1130 else: # create a Score 1131 return romanTextToStreamScore(rtHandler, inputM21=inputM21) 1132 1133 1134# ------------------------------------------------------------------------------ 1135 1136 1137class TestSlow(unittest.TestCase): # pragma: no cover 1138 ''' 1139 These tests are currently too slow to run every time. 1140 ''' 1141 def testExternalA(self): 1142 from music21.romanText import testFiles 1143 1144 for tf in testFiles.ALL: 1145 rtf = rtObjects.RTFile() 1146 rth = rtf.readstr(tf) # return handler, processes tokens 1147 s = romanTextToStreamScore(rth) 1148 s.show() 1149 1150 # noinspection SpellCheckingInspection 1151 def testBasicA(self): 1152 from music21.romanText import testFiles 1153 1154 for tf in testFiles.ALL: 1155 rtf = rtObjects.RTFile() 1156 rth = rtf.readstr(tf) # return handler, processes tokens 1157 # will run romanTextToStreamScore on all but k273 1158 s = romanTextToStreamOpus(rth) 1159 s.show() 1160 1161 s = romanTextToStreamScore(testFiles.swv23) 1162 self.assertEqual(s.metadata.composer, 'Heinrich Schutz') 1163 # this is defined as a Piece tag, but shows up here as a title, after 1164 # being set as an alternate title 1165 self.assertEqual(s.metadata.title, 'Warum toben die Heiden, Psalmen Davids no. 2, SWV 23') 1166 1167 s = romanTextToStreamScore(testFiles.riemenschneider001) 1168 self.assertEqual(s.metadata.composer, 'J. S. Bach') 1169 self.assertEqual(s.metadata.title, 'Aus meines Herzens Grunde') 1170 1171 s = romanTextToStreamScore(testFiles.monteverdi_3_13) 1172 self.assertEqual(s.metadata.composer, 'Claudio Monteverdi') 1173 1174 def testMeasureCopyingA(self): 1175 from music21.romanText import testFiles 1176 1177 s = romanTextToStreamScore(testFiles.swv23) 1178 mStream = s.parts[0].getElementsByClass('Measure') 1179 # the first four measures should all have the same content 1180 rn1 = mStream[1].getElementsByClass('RomanNumeral').first() 1181 self.assertEqual([str(x) for x in rn1.pitches], ['D5', 'F#5', 'A5']) 1182 self.assertEqual(str(rn1.figure), 'V') 1183 rn2 = mStream[1].getElementsByClass('RomanNumeral')[1] 1184 self.assertEqual(str(rn2.figure), 'i') 1185 1186 # make sure that m2, m3, m4 have the same values 1187 rn1 = mStream[2].getElementsByClass('RomanNumeral')[0] 1188 self.assertEqual(str(rn1.figure), 'V') 1189 rn2 = mStream[2].getElementsByClass('RomanNumeral')[1] 1190 self.assertEqual(str(rn2.figure), 'i') 1191 1192 rn1 = mStream[3].getElementsByClass('RomanNumeral')[0] 1193 self.assertEqual(str(rn1.figure), 'V') 1194 rn2 = mStream[3].getElementsByClass('RomanNumeral')[1] 1195 self.assertEqual(str(rn2.figure), 'i') 1196 1197 # test multiple measure copying 1198 s = romanTextToStreamScore(testFiles.monteverdi_3_13) 1199 mStream = s.parts[0].getElementsByClass('Measure') 1200 1201 m1a = None 1202 m2a = None 1203 m3a = None 1204 m1b = None 1205 m2b = None 1206 m3b = None 1207 1208 for m in mStream: 1209 if m.number == 41: # m49-51 = m41-43 1210 m1a = m 1211 elif m.number == 42: # m49-51 = m41-43 1212 m2a = m 1213 elif m.number == 43: # m49-51 = m41-43 1214 m3a = m 1215 elif m.number == 49: # m49-51 = m41-43 1216 m1b = m 1217 elif m.number == 50: # m49-51 = m41-43 1218 m2b = m 1219 elif m.number == 51: # m49-51 = m41-43 1220 m3b = m 1221 1222 rn = m1a.getElementsByClass('RomanNumeral')[0] 1223 self.assertEqual(str(rn.figure), 'IV') 1224 rn = m1a.getElementsByClass('RomanNumeral')[1] 1225 self.assertEqual(str(rn.figure), 'I') 1226 1227 rn = m1b.getElementsByClass('RomanNumeral')[0] 1228 self.assertEqual(str(rn.figure), 'IV') 1229 rn = m1b.getElementsByClass('RomanNumeral')[1] 1230 self.assertEqual(str(rn.figure), 'I') 1231 1232 rn = m2a.getElementsByClass('RomanNumeral')[0] 1233 self.assertEqual(str(rn.figure), 'I') 1234 rn = m2a.getElementsByClass('RomanNumeral')[1] 1235 self.assertEqual(str(rn.figure), 'ii') 1236 1237 rn = m2b.getElementsByClass('RomanNumeral')[0] 1238 self.assertEqual(str(rn.figure), 'I') 1239 rn = m2b.getElementsByClass('RomanNumeral')[1] 1240 self.assertEqual(str(rn.figure), 'ii') 1241 1242 rn = m3a.getElementsByClass('RomanNumeral').first() 1243 self.assertEqual(str(rn.figure), 'V/ii') 1244 rn = m3b.getElementsByClass('RomanNumeral').first() 1245 self.assertEqual(str(rn.figure), 'V/ii') 1246 1247 def testMeasureCopyingB(self): 1248 from music21 import converter 1249 from music21.romanText import testFiles 1250 s = converter.parse(testFiles.monteverdi_3_13) 1251 m25 = s.measure(25) 1252 rn = m25.flatten().getElementsByClass('RomanNumeral') 1253 self.assertEqual(rn[1].figure, 'III') 1254 self.assertEqual(str(rn[1].key), 'd minor') 1255 1256 # TODO: this is getting the F#m even though the key and figure are 1257 # correct 1258 # self.assertEqual(str(rn[1].pitches), '[F4, A4, C5]') 1259 1260 # s.show() 1261 1262 def testOpus(self): 1263 from music21.romanText import testFiles 1264 1265 o = romanTextToStreamOpus(testFiles.mozartK279) 1266 self.assertEqual(o.scores[0].metadata.movementNumber, '1') 1267 self.assertEqual(o.scores[0].metadata.composer, 'Mozart') 1268 self.assertEqual(o.scores[1].metadata.movementNumber, '2') 1269 self.assertEqual(o.scores[1].metadata.composer, 'Mozart') 1270 self.assertEqual(o.scores[2].metadata.movementNumber, '3') 1271 self.assertEqual(o.scores[2].metadata.composer, 'Mozart') 1272 1273 # test using converter. 1274 from music21 import converter 1275 s = converter.parse(testFiles.mozartK279) 1276 self.assertTrue('Opus' in s.classes) 1277 self.assertEqual(len(s.scores), 3) 1278 1279 # make sure a normal file is still a Score 1280 s = converter.parse(testFiles.riemenschneider001) 1281 self.assertTrue(isinstance(s, stream.Score)) 1282 1283 1284class Test(unittest.TestCase): 1285 def testMinor67set(self): 1286 from music21.romanText import testFiles 1287 s = romanTextToStreamScore(testFiles.testSetMinorRootParse) 1288 chords = list(s.recurse().getElementsByClass('RomanNumeral')) 1289 1290 def pitchEqual(index, pitchStr): 1291 ch = chords[index] 1292 chPitches = ch.pitches 1293 self.assertEqual(' '.join(p.name for p in chPitches), pitchStr) 1294 1295 pitchEqual(0, 'C E- G') 1296 pitchEqual(1, 'B D F') 1297 pitchEqual(3, 'G B D') 1298 pitchEqual(4, 'A- C E-') 1299 pitchEqual(7, 'B- D F') 1300 pitchEqual(10, 'A C E') 1301 1302 def testPivotInCopyMultiple(self): 1303 from music21 import converter 1304 testCase = ''' 1305m1 G: I 1306m2 I 1307m3 V D: I 1308m4 V 1309m5 G: I 1310m6-7 = m3-4 1311m8 I 1312''' 1313 s = converter.parse(testCase, format='romanText') 1314 m = s.measure(7).flatten() 1315 self.assertEqual(m.getElementsByClass('RomanNumeral').first().key.name, 'D major') 1316 m = s.measure(8).flatten() 1317 self.assertEqual(m.getElementsByClass('RomanNumeral').first().key.name, 'D major') 1318 1319 def testPivotInCopyMultiple2(self): 1320 ''' 1321 test whether a chord in a pivot situation outside of copying affects copying 1322 ''' 1323 1324 from music21 import converter 1325 testCase = ''' 1326m1 G: I 1327m2 V D: I 1328m3 G: IV 1329m4 V 1330m5 I 1331m6-7 = m4-5 1332m8 I 1333''' 1334 s = converter.parse(testCase, format='romanText') 1335 m = s.measure(5).flatten() 1336 self.assertEqual(m.getElementsByClass('RomanNumeral').first().key.name, 'G major') 1337 1338 def testPivotInCopySingle(self): 1339 from music21 import converter 1340 testCase = ''' 1341m1 G: I 1342m2 I 1343m3 V D: I 1344m4 G: I 1345m5 = m3 1346m6 I 1347''' 1348 s = converter.parse(testCase, format='romanText') 1349 m = s.measure(6).flatten() 1350 self.assertEqual(m.getElementsByClass('RomanNumeral').first().key.name, 'D major') 1351 1352 def testSecondaryInCopyMultiple(self): 1353 ''' 1354 test secondary dominants after copy 1355 ''' 1356 1357 testSecondaryInCopy = ''' 1358Time Signature: 4/4 1359m1 g: i 1360m2 i6 1361m3 V7/v 1362m4 d: i 1363m5-6 = m2-3 1364m7 = m3 1365''' 1366 1367 s = romanTextToStreamScore(testSecondaryInCopy) 1368 m = s.measure(6).flatten() 1369 self.assertEqual(m.getElementsByClass('RomanNumeral').first().pitchedCommonName, 1370 'E-dominant seventh chord') 1371 m = s.measure(7).flatten() 1372 self.assertEqual(m.getElementsByClass('RomanNumeral').first().pitchedCommonName, 1373 'E-dominant seventh chord') 1374 # s.show() 1375 1376 def testBasicB(self): 1377 from music21.romanText import testFiles 1378 1379 unused_s = romanTextToStreamScore(testFiles.riemenschneider001) 1380 # unused_s.show() 1381 1382 def testRomanTextString(self): 1383 from music21 import converter 1384 s = converter.parse('m1 KS1 I \n m2 V6/5 \n m3 I b3 V7 \n' 1385 + 'm4 KS-3 vi \n m5 a: i b3 V4/2 \n m6 I', 1386 format='romantext') 1387 1388 rnStream = s.flatten().getElementsByClass('RomanNumeral').stream() 1389 self.assertEqual(rnStream[0].figure, 'I') 1390 self.assertEqual(rnStream[1].figure, 'V6/5') 1391 self.assertEqual(rnStream[2].figure, 'I') 1392 self.assertEqual(rnStream[3].figure, 'V7') 1393 self.assertEqual(rnStream[4].figure, 'vi') 1394 self.assertEqual(rnStream[5].figure, 'i') 1395 self.assertEqual(rnStream[6].figure, 'V4/2') 1396 self.assertEqual(rnStream[7].figure, 'I') 1397 1398 rnStreamKey = s.flatten().getElementsByClass('KeySignature') 1399 self.assertEqual(rnStreamKey[0].sharps, 1) 1400 self.assertEqual(rnStreamKey[1].sharps, -3) 1401 1402 # s.show() 1403 1404 def testMeasureCopyingB(self): 1405 from music21 import converter 1406 from music21 import pitch 1407 1408 src = '''m1 G: IV || b3 d: III b4 ii 1409m2 v b2 III6 b3 iv6 b4 ii/o6/5 1410m3 i6/4 b3 V 1411m4-5 = m2-3 1412m6-7 = m4-5 1413''' 1414 s = converter.parse(src, format='romantext') 1415 rnStream = s.flatten().getElementsByClass('RomanNumeral') 1416 1417 for elementNumber in [0, 6, 12]: 1418 self.assertEqual(rnStream[elementNumber + 4].figure, 'III6') 1419 self.assertEqual(str([str(p) for p in rnStream[elementNumber + 4].pitches]), 1420 "['A4', 'C5', 'F5']") 1421 1422 x = rnStream[elementNumber + 4].pitches[2].accidental 1423 if x is None: 1424 x = pitch.Accidental('natural') 1425 self.assertEqual(x.alter, 0) 1426 1427 self.assertEqual(rnStream[elementNumber + 5].figure, 'iv6') 1428 self.assertEqual(str([str(p) for p in rnStream[elementNumber + 5].pitches]), 1429 "['B-4', 'D5', 'G5']") 1430 1431 self.assertTrue(rnStream[elementNumber + 5].pitches[0].accidental.displayStatus) 1432 1433 def testNoChord(self): 1434 from music21 import converter 1435 from music21.harmony import NoChord 1436 1437 src = '''m1 G: IV || b3 d: III b4 NC 1438m2 b2 III6 b3 iv6 b4 ii/o6/5 1439m3 NC b3 G: V 1440''' 1441 s = converter.parse(src, format='romantext') 1442 p = s.parts[0] 1443 m1 = p.getElementsByClass('Measure').first() 1444 r1 = m1.notesAndRests[-1] 1445 self.assertIn('Rest', r1.classes) 1446 self.assertEqual(r1.quarterLength, 1.0) 1447 noChordObj = m1.getElementsByClass('Harmony').last() 1448 self.assertIsInstance(noChordObj, NoChord) 1449 1450 m2 = p.getElementsByClass('Measure')[1] 1451 r2 = m2.notesAndRests[0] 1452 self.assertIn('Rest', r2.classes) 1453 self.assertEqual(r1.quarterLength, 1.0) 1454 rn1 = m2.notesAndRests[1] 1455 self.assertIn('RomanNumeral', rn1.classes) 1456 # s.show() 1457 1458 def testUnProcessed(self): 1459 from music21 import converter 1460 1461 src = '''Note: Hello 1462m1 G: IV || b3 d: III b4 NC 1463varM1 I 1464Note: Hi 1465''' 1466 s = converter.parse(src, format='romantext') 1467 p = s.parts[0] 1468 unprocessedElements = p.recurse().getElementsByClass('RomanTextUnprocessedMetadata') 1469 self.assertEqual(len(unprocessedElements), 3) 1470 note1, var1, note2 = unprocessedElements 1471 self.assertEqual(note1.tag, 'Note') 1472 self.assertEqual(note2.tag, 'Note') 1473 self.assertEqual(note1.data, 'Hello') 1474 self.assertEqual(note2.data, 'Hi') 1475 self.assertFalse(var1.tag) 1476 self.assertIn(' I', var1.data) 1477 1478 def testSixthMinorParse(self): 1479 from music21 import converter 1480 1481 src = '''SixthMinor: flat 1482m1 c: vi 1483''' 1484 s = converter.parse(src, format='romantext') 1485 p = s.parts[0] 1486 ch0 = p.recurse().notes[0] 1487 self.assertEqual(ch0.root().name, 'A-') 1488 1489 def testSetRTVersion(self): 1490 src = '''RTVersion: 2.5 1491m1 C: I''' 1492 rtf = rtObjects.RTFile() 1493 rtHandler = rtf.readstr(src) 1494 pt = PartTranslator() 1495 pt.translateTokens(rtHandler.tokens) 1496 self.assertEqual(pt.romanTextVersion, 2.5) 1497 1498 # gives warning, not raises... 1499 # src = '''RTVersion: XYZ 1500 # m1 C: I''' 1501 # rtf = rtObjects.RTFile() 1502 # rtHandler = rtf.readstr(src) 1503 # pt = PartTranslator() 1504 # with self.assertRaises(RomanTextTranslateException): 1505 # pt.translateTokens(rtHandler.tokens) 1506 1507 def testPivotChord(self): 1508 from music21 import converter 1509 1510 src = '''m1 G: I b3 v d: i b4 V''' 1511 s = converter.parse(src, format='romantext') 1512 p = s.parts[0] 1513 m1 = p.getElementsByClass('Measure').first() 1514 allRNs = m1.getElementsByClass('RomanNumeral') 1515 notPChord = allRNs[0] 1516 pChord = allRNs[1] 1517 self.assertEqual(pChord.key.tonic.step, 'G') 1518 self.assertEqual(pChord.figure, 'v') 1519 pivot = pChord.pivotChord 1520 self.assertEqual(pivot.key.tonic.step, 'D') 1521 self.assertEqual(pivot.figure, 'i') 1522 1523 self.assertIsNone(notPChord.pivotChord) 1524 # s.show('text') 1525 1526 def testTimeSigChanges(self): 1527 from music21 import converter 1528 src = '''Time Signature: 4/4 1529 m1 C: I 1530 Time Signature: 2/4 1531 m10 V 1532 Time Signature: 4/4 1533 m12 I 1534 m14-25 = m1-12 1535 ''' 1536 s = converter.parse(src, format='romantext') 1537 p = s.parts[0] 1538 m3 = p.getElementsByClass('Measure')[2] 1539 self.assertEqual(m3.getOffsetBySite(p), 8.0) 1540 m10 = p.getElementsByClass('Measure')[9] 1541 self.assertEqual(m10.getOffsetBySite(p), 36.0) 1542 m11 = p.getElementsByClass('Measure')[10] 1543 self.assertEqual(m11.getOffsetBySite(p), 38.0) 1544 m12 = p.getElementsByClass('Measure')[11] 1545 self.assertEqual(m12.getOffsetBySite(p), 40.0) 1546 m13 = p.getElementsByClass('Measure')[12] 1547 self.assertEqual(m13.getOffsetBySite(p), 44.0) 1548 1549 m16 = p.getElementsByClass('Measure')[15] 1550 self.assertEqual(m16.getOffsetBySite(p), 56.0) 1551 m23 = p.getElementsByClass('Measure')[22] 1552 self.assertEqual(m23.getOffsetBySite(p), 84.0) 1553 m24 = p.getElementsByClass('Measure')[23] 1554 self.assertEqual(m24.getOffsetBySite(p), 86.0) 1555 m25 = p.getElementsByClass('Measure')[24] 1556 self.assertEqual(m25.getOffsetBySite(p), 88.0) 1557 1558 def testEndings(self): 1559 # has first and second endings... 1560 1561 from music21.romanText import testFiles 1562 from music21 import converter 1563 unused_s = converter.parse(testFiles.mozartK283_2_opening, format='romanText') 1564 # s.show('text') 1565 1566 def testTuplets(self): 1567 from music21 import converter 1568 c = converter.parse('m1 C: I b2.66 V', format='romantext') 1569 n1 = c.flatten().notes[0] 1570 n2 = c.flatten().notes[1] 1571 self.assertEqual(n1.duration.quarterLength, common.opFrac(5 / 3)) 1572 self.assertEqual(n2.offset, common.opFrac(5 / 3)) 1573 self.assertEqual(n2.duration.quarterLength, common.opFrac(7 / 3)) 1574 1575 c = converter.parse('TimeSignature: 6/8\nm1 C: I b2.66 V', format='romantext') 1576 n1 = c.flatten().notes[0] 1577 n2 = c.flatten().notes[1] 1578 self.assertEqual(n1.duration.quarterLength, 5 / 2) 1579 self.assertEqual(n2.offset, 5 / 2) 1580 self.assertEqual(n2.duration.quarterLength, 1 / 2) 1581 1582 c = converter.parse('m1 C: I b2.66.5 V', format='romantext') 1583 n1 = c.flatten().notes[0] 1584 n2 = c.flatten().notes[1] 1585 self.assertEqual(n1.duration.quarterLength, common.opFrac(11 / 6)) 1586 self.assertEqual(n2.offset, common.opFrac(11 / 6)) 1587 self.assertEqual(n2.duration.quarterLength, common.opFrac(13 / 6)) 1588 1589 1590# ------------------------------------------------------------------------------ 1591 1592# define presented order in documentation 1593_DOC_ORDER = [] 1594 1595 1596if __name__ == '__main__': 1597 import music21 1598 music21.mainTest(Test) # , TestSlow) 1599 1600