1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: tempo.py 4# Purpose: Classes and tools relating to tempo 5# 6# Authors: Christopher Ariza 7# Michael Scott Cuthbert 8# 9# Copyright: Copyright © 2009-11, '15 Michael Scott Cuthbert and the music21 Project 10# License: BSD, see license.txt 11# ------------------------------------------------------------------------------ 12''' 13This module defines objects for describing tempo and changes in tempo. 14''' 15import copy 16import unittest 17from typing import Union, Optional 18 19from music21 import base 20from music21 import common 21from music21 import duration 22from music21 import exceptions21 23from music21 import expressions 24from music21 import note 25from music21 import spanner 26from music21 import style 27 28from music21 import environment 29_MOD = 'tempo' 30environLocal = environment.Environment(_MOD) 31 32 33# all lowercase, even german, for string comparison 34defaultTempoValues = { 35 'larghissimo': 16, 36 'largamente': 32, 37 'grave': 40, 38 'molto adagio': 40, 39 'largo': 46, 40 'lento': 52, 41 'adagio': 56, 42 'slow': 56, 43 'langsam': 56, 44 'larghetto': 60, 45 'adagietto': 66, 46 'andante': 72, 47 'andantino': 80, 48 'andante moderato': 83, # need number 49 'maestoso': 88, 50 'moderato': 92, 51 'moderate': 92, 52 'allegretto': 108, 53 'animato': 120, 54 'allegro moderato': 128, # need number 55 'allegro': 132, 56 'fast': 132, 57 'schnell': 132, 58 'allegrissimo': 140, # need number 59 'molto allegro': 144, 60 'très vite': 144, 61 'vivace': 160, 62 'vivacissimo': 168, 63 'presto': 184, 64 'prestissimo': 208, 65} 66 67 68def convertTempoByReferent( 69 numberSrc, 70 quarterLengthBeatSrc, 71 quarterLengthBeatDst=1.0 72): 73 ''' 74 Convert between equivalent tempi, where the speed stays the 75 same but the beat referent and number change. 76 77 78 60 bpm at quarter, going to half 79 80 >>> tempo.convertTempoByReferent(60, 1, 2) 81 30.0 82 83 60 bpm at quarter, going to 16th 84 85 >>> tempo.convertTempoByReferent(60, 1, 0.25) 86 240.0 87 88 60 at dotted quarter, get quarter 89 90 >>> tempo.convertTempoByReferent(60, 1.5, 1) 91 90.0 92 93 60 at dotted quarter, get half 94 95 >>> tempo.convertTempoByReferent(60, 1.5, 2) 96 45.0 97 98 60 at dotted quarter, get trip 99 100 >>> tempo.convertTempoByReferent(60, 1.5, 1/3) 101 270.0 102 103 A Fraction instance can also be used: 104 105 >>> tempo.convertTempoByReferent(60, 1.5, common.opFrac(1/3)) 106 270.0 107 108 ''' 109 # find duration in seconds of of quarter length 110 srcDurPerBeat = 60 / numberSrc 111 # convert to dur for one quarter length 112 dur = srcDurPerBeat / quarterLengthBeatSrc 113 # multiply dur by dst quarter 114 dstDurPerBeat = dur * float(quarterLengthBeatDst) 115 # environLocal.printDebug(['dur', dur, 'dstDurPerBeat', dstDurPerBeat]) 116 # find tempo 117 return float(60 / dstDurPerBeat) 118 119 120# ------------------------------------------------------------------------------ 121class TempoException(exceptions21.Music21Exception): 122 pass 123 124 125# ------------------------------------------------------------------------------ 126class TempoIndication(base.Music21Object): 127 ''' 128 A generic base class for all tempo indications to inherit. 129 Can be used to filter out all types of tempo indications. 130 ''' 131 classSortOrder = 1 132 _styleClass = style.TextStyle 133 134 # def __init__(self): 135 # super().__init__() 136 # # self.style.justify = 'left' # creates a style object to share. 137 138 def getSoundingMetronomeMark(self, found=None): 139 '''Get the appropriate MetronomeMark from any sort of TempoIndication, regardless of class. 140 ''' 141 if found is None: 142 found = self 143 144 if isinstance(found, MetricModulation): 145 return found.newMetronome 146 elif isinstance(found, MetronomeMark): 147 return found 148 elif 'TempoText' in found.classes: 149 return found.getMetronomeMark() 150 else: 151 raise TempoException( 152 f'cannot derive a MetronomeMark from this TempoIndication: {found}') 153 154 def getPreviousMetronomeMark(self): 155 ''' 156 Do activeSite and context searches to try to find the last relevant 157 MetronomeMark or MetricModulation object. If a MetricModulation mark is found, 158 return the new MetronomeMark, or the last relevant. 159 160 >>> s = stream.Stream() 161 >>> s.insert(0, tempo.MetronomeMark(number=120)) 162 >>> mm1 = tempo.MetronomeMark(number=90) 163 >>> s.insert(20, mm1) 164 >>> mm1.getPreviousMetronomeMark() 165 <music21.tempo.MetronomeMark animato Quarter=120> 166 ''' 167 # environLocal.printDebug(['getPreviousMetronomeMark']) 168 # search for TempoIndication objects, not just MetronomeMark objects 169 # must provide getElementBefore, as will otherwise return self 170 obj = self.getContextByClass('TempoIndication', 171 getElementMethod=common.enums.ElementSearch.BEFORE_OFFSET) 172 if obj is None: 173 return None # nothing to do 174 return self.getSoundingMetronomeMark(obj) 175 176 177# ------------------------------------------------------------------------------ 178class TempoText(TempoIndication): 179 ''' 180 >>> import music21 181 >>> tm = music21.tempo.TempoText('adagio') 182 >>> tm 183 <music21.tempo.TempoText 'adagio'> 184 >>> print(tm.text) 185 adagio 186 ''' 187 188 def __init__(self, text=None): 189 super().__init__() 190 191 # store text in a TextExpression instance 192 self._textExpression = None # a stored object 193 194 if text is not None: 195 self.text = str(text) 196 197 def _reprInternal(self): 198 return repr(self.text) 199 200 def _getText(self): 201 ''' 202 Get the text used for this expression. 203 ''' 204 return self._textExpression.content 205 206 def _setText(self, value): 207 ''' 208 Set the text of this repeat expression. This is also the primary way 209 that the stored TextExpression object is created. 210 ''' 211 if self._textExpression is None: 212 self._textExpression = expressions.TextExpression(value) 213 if self.hasStyleInformation: 214 self._textExpression.style = self.style # link styles 215 else: 216 self.style = self._textExpression.style 217 self.applyTextFormatting() 218 else: 219 self._textExpression.content = value 220 221 text = property(_getText, _setText, doc=''' 222 Get or set the text as a string. 223 224 >>> import music21 225 >>> tm = music21.tempo.TempoText('adagio') 226 >>> tm.text 227 'adagio' 228 >>> tm.getTextExpression() 229 <music21.expressions.TextExpression 'adagio'> 230 ''') 231 232 def getMetronomeMark(self): 233 # noinspection PyShadowingNames 234 ''' 235 Return a MetronomeMark object that is configured from this objects Text. 236 237 >>> tt = tempo.TempoText('slow') 238 >>> mm = tt.getMetronomeMark() 239 >>> mm.number 240 56 241 ''' 242 mm = MetronomeMark(text=self.text) 243 if self.hasStyleInformation: 244 mm.style = self.style 245 else: 246 self.style = mm.style 247 return mm 248 249 def getTextExpression(self, numberImplicit=False): 250 ''' 251 Return a TextExpression object for this text. 252 253 What is this a deepcopy and not the actual one? 254 ''' 255 if self._textExpression is None: 256 return None 257 else: 258 self.applyTextFormatting(numberImplicit=numberImplicit) 259 return copy.deepcopy(self._textExpression) 260 261 def setTextExpression(self, value): 262 ''' 263 Given a TextExpression, set it in this object. 264 ''' 265 self._textExpression = value 266 self.applyTextFormatting() 267 268 def applyTextFormatting(self, te=None, numberImplicit=False): 269 ''' 270 Apply the default text formatting to the text expression version of of this tempo mark 271 ''' 272 if te is None: # use the stored version if possible 273 te = self._textExpression 274 te.style.fontStyle = 'bold' 275 if numberImplicit: 276 te.style.absoluteY = 20 # if not showing number 277 else: 278 te.style.absoluteY = 45 # 4.5 staff lines above 279 return te 280 281 def isCommonTempoText(self, value=None): 282 ''' 283 Return True or False if the supplied text seems like a 284 plausible Tempo indications be used for this TempoText. 285 286 287 >>> tt = tempo.TempoText('adagio') 288 >>> tt.isCommonTempoText() 289 True 290 291 >>> tt = tempo.TempoText('Largo e piano') 292 >>> tt.isCommonTempoText() 293 True 294 295 >>> tt = tempo.TempoText('undulating') 296 >>> tt.isCommonTempoText() 297 False 298 ''' 299 def stripText(s): 300 # remove all spaces, punctuation, and make lower 301 s = s.strip() 302 s = s.replace(' ', '') 303 s = s.replace('.', '') 304 s = s.lower() 305 return s 306 # if not provided, use stored text 307 if value is None: 308 value = self._textExpression.content 309 310 for candidate in defaultTempoValues: 311 candidate = stripText(candidate) 312 value = stripText(value) 313 # simply look for membership, not a complete match 314 if value in candidate or candidate in value: 315 return True 316 return False 317 318 319# ------------------------------------------------------------------------------ 320class MetronomeMarkException(TempoException): 321 pass 322 323 324# TODO: define if tempo applies only to part 325# ------------------------------------------------------------------------------ 326class MetronomeMark(TempoIndication): 327 ''' 328 A way of specifying a particular tempo with a text string, 329 a referent (a duration) and a number. 330 331 The `referent` attribute is a Duration object, or a string duration type or 332 a floating-point quarter-length value used to create a Duration. 333 334 MetronomeMarks, as Music21Object subclasses, also have .duration object 335 property independent of the `referent`. 336 337 >>> a = tempo.MetronomeMark('slow', 40, note.Note(type='half')) 338 >>> a.number 339 40 340 >>> a.referent 341 <music21.duration.Duration 2.0> 342 >>> a.referent.type 343 'half' 344 >>> print(a.text) 345 slow 346 347 348 Some text marks will automatically suggest a number. 349 350 351 >>> mm = tempo.MetronomeMark('adagio') 352 >>> mm.number 353 56 354 >>> mm.numberImplicit 355 True 356 357 358 For certain numbers, a text value can be set implicitly 359 360 361 >>> tm2 = tempo.MetronomeMark(number=208) 362 >>> print(tm2.text) 363 prestissimo 364 >>> tm2.referent 365 <music21.duration.Duration 1.0> 366 367 Unicode values work fine thanks to Python 3: 368 369 >>> marking = 'très vite' 370 >>> marking 371 'très vite' 372 >>> print(tempo.defaultTempoValues[marking]) 373 144 374 >>> tm2 = tempo.MetronomeMark(marking) 375 >>> tm2.text.endswith('vite') 376 True 377 >>> tm2.number 378 144 379 ''' 380 _DOC_ATTR = { 381 'placement': "Staff placement: 'above', 'below', or None.", 382 } 383 384 def __init__(self, text=None, number=None, referent=None, parentheses=False): 385 super().__init__() 386 387 if number is None and isinstance(text, int): 388 number = text 389 text = None 390 391 self._number = number # may be None 392 self.numberImplicit = None 393 if self._number is not None: 394 self.numberImplicit = False 395 396 self._tempoText = None # set with property text 397 self.textImplicit = None 398 if text is not None: # use property to create object if necessary 399 self.text = text 400 401 # TODO: style?? 402 self.parentheses = parentheses 403 404 self.placement = None 405 406 self._referent = None # set with property 407 if referent is None: 408 # if referent is None, set a default quarter note duration 409 referent = duration.Duration(type='quarter') 410 self.referent = referent # use property 411 412 # set implicit values if necessary 413 self._updateNumberFromText() 414 self._updateTextFromNumber() 415 416 # need to store a sounding value for the case where where 417 # a sounding different is different than the number given in the MM 418 self._numberSounding = None 419 420 def _reprInternal(self): 421 if self.text is None: 422 return f'{self.referent.fullName}={self.number}' 423 else: 424 return f'{self.text} {self.referent.fullName}={self.number}' 425 426 def _updateTextFromNumber(self): 427 '''Update text if number is given and text is not defined 428 ''' 429 if self._tempoText is None and self._number is not None: 430 # PyCharm inspection does not like using attributes on functions that become properties 431 # noinspection PyArgumentList 432 self._setText(self._getDefaultText(self._number), 433 updateNumberFromText=False) 434 if self.text is not None: 435 self.textImplicit = True 436 437 def _updateNumberFromText(self): 438 '''Update number if text is given and number is not defined 439 ''' 440 if self._number is None and self._tempoText is not None: 441 self._number = self._getDefaultNumber(self._tempoText) 442 if self._number is not None: # only if set 443 self.numberImplicit = True 444 445 # ------------------------------------------------------------------------- 446 def _getReferent(self): 447 return self._referent 448 449 def _setReferent(self, value): 450 if value is None: # this may be better not here 451 # if referent is None, set a default quarter note duration 452 self._referent = duration.Duration(type='quarter') 453 # assume ql value or a type string 454 elif common.isNum(value) or isinstance(value, str): 455 self._referent = duration.Duration(value) 456 elif not isinstance(value, duration.Duration): 457 # try get duration object, like from Note 458 self._referent = value.duration 459 elif 'Duration' in value.classes: 460 self._referent = value 461 # should be a music21.duration.Duration object or a 462 # Music21Object with a duration or None 463 else: 464 raise TempoException(f'Cannot get a Duration from the supplied object: {value}') 465 466 referent = property(_getReferent, _setReferent, doc=''' 467 Get or set the referent, or the Duration object that is the 468 reference for the tempo value in BPM. 469 ''') 470 471 # properties and conversions 472 def _getText(self): 473 if self._tempoText is None: 474 return None 475 return self._tempoText.text 476 477 def _setText(self, value, updateNumberFromText=True): 478 if value is None: 479 self._tempoText = None 480 elif isinstance(value, TempoText): 481 self._tempoText = value 482 self.textImplicit = False # must set here 483 else: 484 self._tempoText = TempoText(value) 485 if self.hasStyleInformation: 486 self._tempoText.style = self.style # link style information 487 else: 488 self.style = self._tempoText.style 489 self.textImplicit = False # must set here 490 491 if updateNumberFromText: 492 self._updateNumberFromText() 493 494 text = property(_getText, _setText, doc=''' 495 Get or set a text string for this MetronomeMark. Internally implemented as a 496 :class:`~music21.tempo.TempoText` object, which stores the text in 497 a :class:`~music21.expression.TextExpression` object. 498 499 >>> mm = tempo.MetronomeMark(number=123) 500 >>> mm.text == None 501 True 502 >>> mm.text = 'medium fast' 503 >>> print(mm.text) 504 medium fast 505 ''') 506 507 def _getNumber(self): 508 return self._number # may be None 509 510 def _setNumber(self, value, updateTextFromNumber=True): 511 if not common.isNum(value): 512 raise TempoException('cannot set number to a string') 513 self._number = value 514 self.numberImplicit = False 515 if updateTextFromNumber: 516 self._updateTextFromNumber() 517 518 number = property(_getNumber, _setNumber, doc=''' 519 Get and set the number, or the numerical value of the Metronome. 520 521 >>> mm = tempo.MetronomeMark('slow') 522 >>> mm.number 523 56 524 >>> mm.numberImplicit 525 True 526 >>> mm.number = 52.5 527 >>> mm.number 528 52.5 529 >>> mm.numberImplicit 530 False 531 ''') 532 533 def _getNumberSounding(self): 534 return self._numberSounding # may be None 535 536 def _setNumberSounding(self, value): 537 if not common.isNum(value): 538 raise TempoException('cannot set numberSounding to a string') 539 self._numberSounding = value 540 541 numberSounding = property(_getNumberSounding, _setNumberSounding, doc=''' 542 Get and set the numberSounding, or the numerical value of the Metronome that 543 is used for playback independent of display. If numberSounding is None, number is 544 assumed to be numberSounding. 545 546 547 >>> mm = tempo.MetronomeMark('slow') 548 >>> mm.number 549 56 550 >>> mm.numberImplicit 551 True 552 >>> mm.numberSounding == None 553 True 554 >>> mm.numberSounding = 120 555 >>> mm.numberSounding 556 120 557 ''') 558 559 # ------------------------------------------------------------------------- 560 def getQuarterBPM(self, useNumberSounding=True): 561 ''' 562 Get a BPM value where the beat is a quarter; must convert from the 563 defined beat to a quarter beat. Will return None if no beat number is defined. 564 565 This mostly used for generating MusicXML <sound> tags when necessary. 566 567 568 >>> mm = tempo.MetronomeMark(number=60, referent='half') 569 >>> mm.getQuarterBPM() 570 120.0 571 >>> mm.referent = 'quarter' 572 >>> mm.getQuarterBPM() 573 60.0 574 ''' 575 if useNumberSounding and self.numberSounding is not None: 576 return convertTempoByReferent(self.numberSounding, 577 self.referent.quarterLength, 578 1.0) 579 if self.number is not None: 580 # target quarter length is always 1.0 581 return convertTempoByReferent(self.number, 582 self.referent.quarterLength, 583 1.0) 584 return None 585 586 def setQuarterBPM(self, value, setNumber=True): 587 ''' 588 Given a value in BPM, use it to set the value of this MetronomeMark. 589 BPM values are assumed to be refer only to quarter notes; different beat values, 590 if defined here, will be scaled 591 592 593 >>> mm = tempo.MetronomeMark(number=60, referent='half') 594 >>> mm.setQuarterBPM(240) # set to 240 for a quarter 595 >>> mm.number # a half is half as fast 596 120.0 597 ''' 598 # assuming a quarter value coming in, what is with our current beat 599 value = convertTempoByReferent(value, 1.0, self.referent.quarterLength) 600 if not setNumber: 601 # convert this to a quarter bpm 602 self._numberSounding = value 603 else: # go through property so as to set implicit status 604 self.number = value 605 606 def _getDefaultNumber(self, tempoText): 607 ''' 608 Given a tempo text expression or an TempoText, get the default number. 609 610 >>> mm = tempo.MetronomeMark() 611 >>> mm._getDefaultNumber('schnell') 612 132 613 >>> mm._getDefaultNumber('adagio') 614 56 615 >>> mm._getDefaultNumber('Largo e piano') 616 46 617 ''' 618 if isinstance(tempoText, TempoText): 619 tempoStr = tempoText.text 620 else: 621 tempoStr = tempoText 622 post = None # returned if no match 623 if tempoStr.lower() in defaultTempoValues: 624 post = defaultTempoValues[tempoStr.lower()] 625 # an exact match 626 elif tempoStr in defaultTempoValues: 627 post = defaultTempoValues[tempoStr] 628 # look for partial matches 629 if post is None: 630 for word in tempoStr.split(' '): 631 for key in defaultTempoValues: 632 if key == word.lower(): 633 post = defaultTempoValues[key] 634 return post 635 636 def _getDefaultText(self, number, spread=2): 637 ''' 638 Given a tempo number try to get a text expression; 639 presently only looks for approximate matches 640 641 The `spread` value is a +/- shift around the default tempo 642 indications defined in defaultTempoValues 643 644 645 >>> mm = tempo.MetronomeMark() 646 >>> mm._getDefaultText(92) 647 'moderate' 648 >>> mm._getDefaultText(208) 649 'prestissimo' 650 ''' 651 if common.isNum(number): 652 tempoNumber = number 653 else: # try to convert 654 tempoNumber = float(number) 655 # get a items and sort 656 matches = [] 657 for tempoStr, tempoValue in defaultTempoValues.items(): 658 matches.append([tempoValue, tempoStr]) 659 matches.sort() 660 # environLocal.printDebug(['matches', matches]) 661 post = None 662 for tempoValue, tempoStr in matches: 663 if (tempoValue - spread) <= tempoNumber <= (tempoValue + spread): 664 # found a match 665 post = tempoStr 666 break 667 return post 668 669 def getTextExpression(self, returnImplicit=False): 670 ''' 671 If there is a TextExpression available that is not implicit, return it; 672 otherwise, return None. 673 674 >>> mm = tempo.MetronomeMark('presto') 675 >>> mm.number 676 184 677 >>> mm.numberImplicit 678 True 679 >>> mm.getTextExpression() 680 <music21.expressions.TextExpression 'presto'> 681 >>> mm.textImplicit 682 False 683 684 >>> mm = tempo.MetronomeMark(number=90) 685 >>> mm.numberImplicit 686 False 687 >>> mm.textImplicit 688 True 689 >>> mm.getTextExpression() is None 690 True 691 >>> mm.getTextExpression(returnImplicit=True) 692 <music21.expressions.TextExpression 'maestoso'> 693 ''' 694 if self._tempoText is None: 695 return None 696 # if explicit, always return; if implicit, return if returnImplicit true 697 if not self.textImplicit or (self.textImplicit and returnImplicit): 698 # adjust position if number is implicit; pass number implicit 699 return self._tempoText.getTextExpression( 700 numberImplicit=self.numberImplicit) 701 702 # ------------------------------------------------------------------------- 703 def getEquivalentByReferent(self, referent): 704 ''' 705 Return a new MetronomeMark object that has an equivalent speed but 706 different number and referent values based on a supplied referent 707 (given as a Duration type, quarterLength, or Duration object). 708 709 710 >>> mm1 = tempo.MetronomeMark(number=60, referent=1.0) 711 >>> mm1.getEquivalentByReferent(0.5) 712 <music21.tempo.MetronomeMark larghetto Eighth=120.0> 713 >>> mm1.getEquivalentByReferent(duration.Duration('half')) 714 <music21.tempo.MetronomeMark larghetto Half=30.0> 715 716 >>> mm1.getEquivalentByReferent('longa') 717 <music21.tempo.MetronomeMark larghetto Imperfect Longa=3.75> 718 719 ''' 720 if common.isNum(referent): # assume quarter length 721 quarterLength = referent 722 elif isinstance(referent, str): # try to get quarter len 723 d = duration.Duration(referent) 724 quarterLength = d.quarterLength 725 else: # TODO: test if a Duration 726 quarterLength = referent.quarterLength 727 728 if self.number is not None: 729 newNumber = convertTempoByReferent(self.number, 730 self.referent.quarterLength, 731 quarterLength) 732 else: 733 newNumber = None 734 735 return MetronomeMark(text=self.text, number=newNumber, 736 referent=duration.Duration(quarterLength)) 737 738 # def getEquivalentByNumber(self, number): 739 # ''' 740 # Return a new MetronomeMark object that has an equivalent speed but different number and 741 # referent values based on a supplied tempo number. 742 # ''' 743 # pass 744 745 def getMaintainedNumberWithReferent(self, referent): 746 ''' 747 Return a new MetronomeMark object that has an equivalent number but a new referent. 748 ''' 749 return MetronomeMark(text=self.text, number=self.number, 750 referent=referent) 751 752 # -------------------------------------------------------------------------- 753 # real-time realization 754 def secondsPerQuarter(self): 755 ''' 756 Return the duration in seconds for each quarter length 757 (not necessarily the referent) of this MetronomeMark. 758 759 >>> from music21 import tempo 760 >>> mm1 = tempo.MetronomeMark(referent=1.0, number=60.0) 761 >>> mm1.secondsPerQuarter() 762 1.0 763 >>> mm1 = tempo.MetronomeMark(referent=2.0, number=60.0) 764 >>> mm1.secondsPerQuarter() 765 0.5 766 >>> mm1 = tempo.MetronomeMark(referent=2.0, number=30.0) 767 >>> mm1.secondsPerQuarter() 768 1.0 769 ''' 770 qbpm = self.getQuarterBPM() 771 if qbpm is not None: 772 return 60.0 / self.getQuarterBPM() 773 else: 774 raise MetronomeMarkException('cannot derive seconds as getQuarterBPM() returns None') 775 776 def durationToSeconds(self, durationOrQuarterLength): 777 '''Given a duration specified as a :class:`~music21.duration.Duration` object or a 778 quarter length, return the resultant time in seconds at the tempo specified by 779 this MetronomeMark. 780 781 >>> from music21 import tempo 782 >>> mm1 = tempo.MetronomeMark(referent=1.0, number=60.0) 783 >>> mm1.durationToSeconds(60) 784 60.0 785 >>> mm1.durationToSeconds(duration.Duration('16th')) 786 0.25 787 ''' 788 if common.isNum(durationOrQuarterLength): 789 ql = durationOrQuarterLength 790 else: # assume a duration object 791 ql = durationOrQuarterLength.quarterLength 792 # get time per quarter 793 return self.secondsPerQuarter() * ql 794 795 def secondsToDuration(self, seconds): 796 ''' 797 Given a duration in seconds, 798 return a :class:`~music21.duration.Duration` object equal to that time. 799 800 >>> from music21 import tempo 801 >>> mm1 = tempo.MetronomeMark(referent=1.0, number=60.0) 802 >>> mm1.secondsToDuration(0.25) 803 <music21.duration.Duration 0.25> 804 >>> mm1.secondsToDuration(0.5).type 805 'eighth' 806 >>> mm1.secondsToDuration(1) 807 <music21.duration.Duration 1.0> 808 ''' 809 if not common.isNum(seconds) or seconds <= 0.0: 810 raise MetronomeMarkException('seconds must be a number greater than zero') 811 ql = seconds / self.secondsPerQuarter() 812 return duration.Duration(quarterLength=ql) 813 814 815# ------------------------------------------------------------------------------ 816class MetricModulationException(TempoException): 817 pass 818 819 820# ------------------------------------------------------------------------------ 821class MetricModulation(TempoIndication): 822 ''' 823 A class for representing the relationship between two MetronomeMarks. 824 Generally this relationship is one of equality, where the number is maintained but 825 the referent that number is applied to changes. 826 827 The basic definition of a MetricModulation is given by supplying two MetronomeMarks, 828 one for the oldMetronome, the other for the newMetronome. High level properties, 829 oldReferent and newReferent, and convenience methods permit only setting the referent. 830 831 The `classicalStyle` attribute determines of the first MetronomeMark describes the 832 new tempo, not the old (the reverse of expected usage). 833 834 The `maintainBeat` attribute determines if, after an equality statement, 835 the beat is maintained. This is relevant for moving from 3/4 to 6/8, for example. 836 837 >>> s = stream.Stream() 838 >>> mm1 = tempo.MetronomeMark(number=60) 839 >>> s.append(mm1) 840 >>> s.repeatAppend(note.Note(quarterLength=1), 2) 841 >>> s.repeatAppend(note.Note(quarterLength=0.5), 4) 842 843 >>> mmod1 = tempo.MetricModulation() 844 >>> mmod1.oldReferent = 0.5 # can use Duration objects 845 >>> mmod1.newReferent = 'quarter' # can use Duration objects 846 >>> s.append(mmod1) 847 >>> mmod1.updateByContext() # get number from last MetronomeMark on Stream 848 >>> mmod1.newMetronome 849 <music21.tempo.MetronomeMark animato Quarter=120.0> 850 851 >>> s.append(note.Note()) 852 >>> s.repeatAppend(note.Note(quarterLength=1.5), 2) 853 854 >>> mmod2 = tempo.MetricModulation() 855 >>> s.append(mmod2) # if the obj is added to Stream, can set referents 856 >>> mmod2.oldReferent = 1.5 # will get number from previous MetronomeMark 857 >>> mmod2.newReferent = 'quarter' 858 >>> mmod2.newMetronome 859 <music21.tempo.MetronomeMark animato Quarter=80.0> 860 861 Note that an initial metric modulation can set old and new referents and get None as 862 tempo numbers: 863 864 >>> mmod3 = tempo.MetricModulation() 865 >>> mmod3.oldReferent = 'half' 866 >>> mmod3.newReferent = '16th' 867 >>> mmod3 868 <music21.tempo.MetricModulation 869 <music21.tempo.MetronomeMark 870 Half=None>=<music21.tempo.MetronomeMark 16th=None>> 871 872 test w/ more sane referents that either the old or the new can change without a tempo number 873 874 >>> mmod3.oldReferent = 'quarter' 875 >>> mmod3.newReferent = 'eighth' 876 >>> mmod3 877 <music21.tempo.MetricModulation 878 <music21.tempo.MetronomeMark 879 Quarter=None>=<music21.tempo.MetronomeMark Eighth=None>> 880 >>> mmod3.oldMetronome 881 <music21.tempo.MetronomeMark Quarter=None> 882 >>> mmod3.oldMetronome.number = 60 883 884 New number automatically updates: 885 886 >>> mmod3 887 <music21.tempo.MetricModulation 888 <music21.tempo.MetronomeMark larghetto 889 Quarter=60>=<music21.tempo.MetronomeMark larghetto Eighth=60>> 890 ''' 891 892 def __init__(self): 893 super().__init__() 894 895 self.classicalStyle = False 896 self.maintainBeat = False 897 self.transitionSymbol = '=' # accept different symbols 898 # some old formats use arrows 899 self.arrowDirection = None # can be left, right, or None 900 901 # showing parens or not 902 self.parentheses = False 903 904 # store two MetronomeMark objects 905 self._oldMetronome = None 906 self._newMetronome = None 907 908 def _reprInternal(self): 909 return f'{self.oldMetronome}={self.newMetronome}' 910 911 # -------------------------------------------------------------------------- 912 # core properties 913 def _setOldMetronome(self, value): 914 if value is None: 915 pass # allow setting as None 916 elif not hasattr(value, 'classes') or 'MetronomeMark' not in value.classes: 917 raise MetricModulationException( 918 'oldMetronome property must be set with a MetronomeMark instance') 919 self._oldMetronome = value 920 921 def _getOldMetronome(self): 922 if self._oldMetronome is not None: 923 if self._oldMetronome.number is None: 924 self.updateByContext() 925 return self._oldMetronome 926 927 oldMetronome = property(_getOldMetronome, _setOldMetronome, doc=''' 928 Get or set the left :class:`~music21.tempo.MetronomeMark` object 929 for the old, or previous value. 930 931 >>> mm1 = tempo.MetronomeMark(number=60, referent=1) 932 >>> mm1 933 <music21.tempo.MetronomeMark larghetto Quarter=60> 934 >>> mmod1 = tempo.MetricModulation() 935 >>> mmod1.oldMetronome = mm1 936 937 Note that we do need to have a proper MetronomeMark instance to figure this out: 938 939 >>> mmod1.oldMetronome = 'junk' 940 Traceback (most recent call last): 941 music21.tempo.MetricModulationException: oldMetronome property 942 must be set with a MetronomeMark instance 943 ''') 944 945 def _setOldReferent(self, value): 946 if value is None: 947 raise MetricModulationException('cannot set old referent to None') 948 # try to get and reassign equivalent 949 if self._oldMetronome is not None: 950 mm = self._oldMetronome.getEquivalentByReferent(value) 951 else: 952 # try to do a context search and get the last MetronomeMark 953 mm = self.getPreviousMetronomeMark() 954 if mm is not None: 955 # replace with an equivalent based on a provided value 956 mm = mm.getEquivalentByReferent(value) 957 if mm is not None: 958 self._oldMetronome = mm 959 else: 960 # create a new metronome mark with a referent, but not w/ a value 961 self._oldMetronome = MetronomeMark(referent=value) 962 # raise MetricModulationException('cannot set old MetronomeMark from provided value.') 963 964 def _getOldReferent(self): 965 if self._oldMetronome is not None: 966 return self._oldMetronome.referent 967 968 oldReferent = property(_getOldReferent, _setOldReferent, doc=''' 969 Get or set the referent of the old MetronomeMark. 970 971 >>> mm1 = tempo.MetronomeMark(number=60, referent=1) 972 >>> mmod1 = tempo.MetricModulation() 973 >>> mmod1.oldMetronome = mm1 974 >>> mmod1.oldMetronome 975 <music21.tempo.MetronomeMark larghetto Quarter=60> 976 >>> mmod1.oldReferent = 0.25 977 >>> mmod1.oldMetronome 978 <music21.tempo.MetronomeMark larghetto 16th=240.0> 979 980 ''') 981 982 def _setNewMetronome(self, value): 983 if value is None: 984 pass # allow setting as None 985 elif not hasattr(value, 'classes') or 'MetronomeMark' not in value.classes: 986 raise MetricModulationException( 987 'newMetronome property must be set with a MetronomeMark instance') 988 self._newMetronome = value 989 990 def _getNewMetronome(self): 991 # before returning the referent, see if we can update the number 992 if self._newMetronome is not None: 993 if self._newMetronome.number is None: 994 self.updateByContext() 995 return self._newMetronome 996 997 newMetronome = property(_getNewMetronome, _setNewMetronome, doc=''' 998 Get or set the right :class:`~music21.tempo.MetronomeMark` 999 object for the new, or following value. 1000 1001 >>> mm1 = tempo.MetronomeMark(number=60, referent=1) 1002 >>> mm1 1003 <music21.tempo.MetronomeMark larghetto Quarter=60> 1004 >>> mmod1 = tempo.MetricModulation() 1005 >>> mmod1.newMetronome = mm1 1006 >>> mmod1.newMetronome = 'junk' 1007 Traceback (most recent call last): 1008 music21.tempo.MetricModulationException: newMetronome property must be 1009 set with a MetronomeMark instance 1010 ''') 1011 1012 def _setNewReferent(self, value): 1013 if value is None: 1014 raise MetricModulationException('cannot set new referent to None') 1015 # of oldMetronome is defined, get new metronome from old 1016 mm = None 1017 if self._newMetronome is not None: 1018 # if metro defined, get based on new referent 1019 mm = self._newMetronome.getEquivalentByReferent(value) 1020 elif self._oldMetronome is not None: 1021 # get a new mm with the same number but a new referent 1022 mm = self._oldMetronome.getMaintainedNumberWithReferent(value) 1023 else: 1024 # create a new metronome mark with a referent, but not w/ a value 1025 mm = MetronomeMark(referent=value) 1026 # raise MetricModulationException('cannot set old MetronomeMark from provided value.') 1027 self._newMetronome = mm 1028 1029 def _getNewReferent(self): 1030 if self._newMetronome is not None: 1031 return self._newMetronome.referent 1032 1033 newReferent = property(_getNewReferent, _setNewReferent, doc=''' 1034 Get or set the referent of the new MetronomeMark. 1035 1036 >>> mm1 = tempo.MetronomeMark(number=60, referent=1) 1037 >>> mmod1 = tempo.MetricModulation() 1038 >>> mmod1.newMetronome = mm1 1039 >>> mmod1.newMetronome 1040 <music21.tempo.MetronomeMark larghetto Quarter=60> 1041 >>> mmod1.newReferent = 0.25 1042 >>> mmod1.newMetronome 1043 <music21.tempo.MetronomeMark larghetto 16th=240.0> 1044 ''') 1045 1046 @property 1047 def number(self): 1048 ''' 1049 Get and the number of the MetricModulation, or the number 1050 assigned to the new MetronomeMark. 1051 1052 >>> s = stream.Stream() 1053 >>> mm1 = tempo.MetronomeMark(number=60) 1054 >>> s.append(mm1) 1055 >>> s.repeatAppend(note.Note(quarterLength=1), 2) 1056 >>> s.repeatAppend(note.Note(quarterLength=0.5), 4) 1057 1058 >>> mmod1 = tempo.MetricModulation() 1059 >>> mmod1.oldReferent = 0.5 # can use Duration objects 1060 >>> mmod1.newReferent = 'quarter' 1061 >>> s.append(mmod1) 1062 >>> mmod1.updateByContext() 1063 >>> mmod1.newMetronome 1064 <music21.tempo.MetronomeMark animato Quarter=120.0> 1065 >>> mmod1.number 1066 120.0 1067 ''' 1068 if self._newMetronome is not None: 1069 return self._newMetronome.number 1070 1071 # def _setNumber(self, value, updateTextFromNumber=True): 1072 # if not common.isNum(value): 1073 # raise MetricModulationException('cannot set number to a string') 1074 # self._newMetronome.number = value 1075 # self._oldMetronome.number = value 1076 1077 # -------------------------------------------------------------------------- 1078 # high-level configuration methods 1079 def updateByContext(self): 1080 '''Update this metric modulation based on the context, 1081 or the surrounding MetronomeMarks or MetricModulations. 1082 The object needs to reside in a Stream for this to be effective. 1083 ''' 1084 # try to set old number from last; there must be a partially 1085 # defined metronome mark already assigned; or create one at quarter? 1086 mmLast = self.getPreviousMetronomeMark() 1087 mmOld = None 1088 if mmLast is not None: 1089 # replace with an equivalent based on a provided value 1090 if (self._oldMetronome is not None 1091 and self._oldMetronome.referent is not None): 1092 mmOld = mmLast.getEquivalentByReferent( 1093 self._oldMetronome.referent) 1094 else: 1095 mmOld = MetronomeMark(referent=mmLast.referent, 1096 number=mmLast.number) 1097 if mmOld is not None: 1098 self._oldMetronome = mmOld 1099 # if we have an a new referent, then update number 1100 if (self._newMetronome is not None 1101 and self._newMetronome.referent is not None 1102 and self._oldMetronome.number is not None): 1103 self._newMetronome.number = self._oldMetronome.number 1104 1105 def setEqualityByReferent(self, side=None, referent=1.0): 1106 '''Set the other side of the metric modulation to 1107 an equality; side can be specified, or if one side 1108 is None, that side will be set. 1109 1110 1111 >>> mm1 = tempo.MetronomeMark(number=60, referent=1) 1112 >>> mmod1 = tempo.MetricModulation() 1113 >>> mmod1.newMetronome = mm1 1114 >>> mmod1.setEqualityByReferent(None, 2) 1115 >>> mmod1 1116 <music21.tempo.MetricModulation 1117 <music21.tempo.MetronomeMark larghetto 1118 Half=30.0>=<music21.tempo.MetronomeMark larghetto Quarter=60>> 1119 1120 ''' 1121 if side is None: 1122 if self._oldMetronome is None: 1123 side = 'left' 1124 elif self._newMetronome is None: 1125 side = 'right' 1126 if side not in ['left', 'right']: 1127 raise TempoException(f'cannot set equality for a side of {side}') 1128 1129 if side == 'right': 1130 self._newMetronome = self._oldMetronome.getEquivalentByReferent( 1131 referent) 1132 elif side == 'left': 1133 self._oldMetronome = self._newMetronome.getEquivalentByReferent( 1134 referent) 1135 1136 def setOtherByReferent( 1137 self, 1138 side: Optional[str] = None, 1139 referent: Union[str, int, float] = 1.0 1140 ): 1141 ''' 1142 Set the other side of the metric modulation not based on equality, 1143 but on a direct translation of the tempo value. 1144 1145 referent can be a string type or a int/float quarter length 1146 ''' 1147 if side is None: 1148 if self._oldMetronome is None: 1149 side = 'left' 1150 elif self._newMetronome is None: 1151 side = 'right' 1152 if side not in ['left', 'right']: 1153 raise TempoException(f'cannot set equality for a side of {side}') 1154 if side == 'right': 1155 self._newMetronome = self._oldMetronome.getMaintainedNumberWithReferent(referent) 1156 elif side == 'left': 1157 self._oldMetronome = self._newMetronome.getMaintainedNumberWithReferent(referent) 1158 1159 1160# ------------------------------------------------------------------------------ 1161def interpolateElements(element1, element2, sourceStream, 1162 destinationStream, autoAdd=True): 1163 # noinspection PyShadowingNames 1164 ''' 1165 Assume that element1 and element2 are two elements in sourceStream 1166 and destinationStream with other elements (say eA, eB, eC) between 1167 them. For instance, element1 could be the downbeat at offset 10 1168 in sourceStream (a Stream representing a score) and offset 20.5 1169 in destinationStream (which might be a Stream representing the 1170 timing of notes in particular recording at approximately but not 1171 exactly qtr = 30). Element2 could be the following downbeat in 4/4, 1172 at offset 14 in source but offset 25.0 in the recording: 1173 1174 >>> sourceStream = stream.Stream() 1175 >>> destinationStream = stream.Stream() 1176 >>> element1 = note.Note('C4', type='quarter') 1177 >>> element2 = note.Note('G4', type='quarter') 1178 >>> sourceStream.insert(10, element1) 1179 >>> destinationStream.insert(20.5, element1) 1180 >>> sourceStream.insert(14, element2) 1181 >>> destinationStream.insert(25.0, element2) 1182 1183 Suppose eA, eB, and eC are three quarter notes that lie 1184 between element1 and element2 in sourceStream 1185 and destinationStream, as in: 1186 1187 >>> eA = note.Note('D4', type='quarter') 1188 >>> eB = note.Note('E4', type='quarter') 1189 >>> eC = note.Note('F4', type='quarter') 1190 >>> sourceStream.insert(11, eA) 1191 >>> sourceStream.insert(12, eB) 1192 >>> sourceStream.insert(13, eC) 1193 >>> destinationStream.append([eA, eB, eC]) # not needed if autoAdd were true 1194 1195 then running this function will cause eA, eB, and eC 1196 to have offsets 21.625, 22.75, and 23.875 respectively 1197 in destinationStream: 1198 1199 >>> tempo.interpolateElements(element1, element2, 1200 ... sourceStream, destinationStream, autoAdd=False) 1201 >>> for el in [eA, eB, eC]: 1202 ... print(el.getOffsetBySite(destinationStream)) 1203 21.625 1204 22.75 1205 23.875 1206 1207 if the elements between element1 and element2 do not yet 1208 appear in destinationStream, they are automatically added 1209 unless autoAdd is False. 1210 1211 (with the default autoAdd, elements are automatically added to new streams): 1212 1213 >>> destStream2 = stream.Stream() 1214 >>> destStream2.insert(10.1, element1) 1215 >>> destStream2.insert(50.5, element2) 1216 >>> tempo.interpolateElements(element1, element2, sourceStream, destStream2) 1217 >>> for el in [eA, eB, eC]: 1218 ... print('%.1f' % (el.getOffsetBySite(destStream2),)) 1219 20.2 1220 30.3 1221 40.4 1222 1223 (unless autoAdd is set to False, in which case a Tempo Exception arises:) 1224 1225 >>> destStream3 = stream.Stream() 1226 >>> destStream3.insert(100, element1) 1227 >>> destStream3.insert(500, element2) 1228 >>> eA.id = 'blah' 1229 >>> tempo.interpolateElements(element1, element2, sourceStream, destStream3, autoAdd=False) 1230 Traceback (most recent call last): 1231 music21.tempo.TempoException: Could not find element <music21.note.Note D> with id ... 1232 ''' 1233 try: 1234 startOffsetSrc = element1.getOffsetBySite(sourceStream) 1235 except exceptions21.Music21Exception as e: 1236 raise TempoException('could not find element1 in sourceStream') from e 1237 try: 1238 startOffsetDest = element1.getOffsetBySite(destinationStream) 1239 except exceptions21.Music21Exception as e: 1240 raise TempoException('could not find element1 in destinationStream') from e 1241 1242 try: 1243 endOffsetSrc = element2.getOffsetBySite(sourceStream) 1244 except exceptions21.Music21Exception as e: 1245 raise TempoException('could not find element2 in sourceStream') from e 1246 try: 1247 endOffsetDest = element2.getOffsetBySite(destinationStream) 1248 except exceptions21.Music21Exception as e: 1249 raise TempoException('could not find element2 in destinationStream') from e 1250 1251 scaleAmount = ((endOffsetDest - startOffsetDest) / (endOffsetSrc - startOffsetSrc)) 1252 1253 interpolatedElements = sourceStream.getElementsByOffset( 1254 offsetStart=startOffsetSrc, 1255 offsetEnd=endOffsetSrc 1256 ) 1257 for el in interpolatedElements: 1258 elOffsetSrc = el.getOffsetBySite(sourceStream) 1259 try: 1260 el.getOffsetBySite(destinationStream) # dummy 1261 except base.SitesException as e: 1262 if autoAdd is True: 1263 destinationOffset = (scaleAmount * (elOffsetSrc - startOffsetSrc)) + startOffsetDest 1264 destinationStream.insert(destinationOffset, el) 1265 else: 1266 raise TempoException( 1267 'Could not find element ' 1268 + f'{el!r} with id {el.id!r} ' 1269 + 'in destinationStream and autoAdd is false') from e 1270 else: 1271 destinationOffset = (scaleAmount * (elOffsetSrc - startOffsetSrc)) + startOffsetDest 1272 el.setOffsetBySite(destinationStream, destinationOffset) 1273 1274 1275# ------------------------------------------------------------------------------ 1276class TempoChangeSpanner(spanner.Spanner): 1277 ''' 1278 Spanners showing tempo-change. They do nothing right now. 1279 ''' 1280 pass 1281 1282 1283class RitardandoSpanner(TempoChangeSpanner): 1284 ''' 1285 Spanner representing a slowing down. 1286 ''' 1287 pass 1288 1289 1290class AccelerandoSpanner(TempoChangeSpanner): 1291 ''' 1292 Spanner representing a speeding up. 1293 ''' 1294 pass 1295 1296 1297# ------------------------------------------------------------------------------ 1298class Test(unittest.TestCase): 1299 def testCopyAndDeepcopy(self): 1300 '''Test copying all objects defined in this module 1301 ''' 1302 import sys 1303 import types 1304 for part in sys.modules[self.__module__].__dict__: 1305 match = False 1306 for skip in ['_', '__', 'Test', 'Exception']: 1307 if part.startswith(skip) or part.endswith(skip): 1308 match = True 1309 if match: 1310 continue 1311 obj = getattr(sys.modules[self.__module__], part) 1312 # noinspection PyTypeChecker 1313 if callable(obj) and not isinstance(obj, types.FunctionType): 1314 copy.copy(obj) 1315 copy.deepcopy(obj) 1316 1317 def testSetup(self): 1318 mm1 = MetronomeMark(number=60, referent=note.Note(type='quarter')) 1319 self.assertEqual(mm1.number, 60) 1320 1321 tm1 = TempoText('Lebhaft') 1322 self.assertEqual(tm1.text, 'Lebhaft') 1323 1324 def testUnicode(self): 1325 # test with no arguments 1326 TempoText() 1327 # test with arguments 1328 TempoText('adagio') 1329 1330 # environLocal.printDebug(['testing tempo instantiation', tm]) 1331 mm = MetronomeMark('adagio') 1332 self.assertEqual(mm.number, 56) 1333 self.assertTrue(mm.numberImplicit) 1334 1335 self.assertEqual(mm.number, 56) 1336 tm2 = TempoText('très vite') 1337 1338 self.assertEqual(tm2.text, 'très vite') 1339 mm = tm2.getMetronomeMark() 1340 self.assertEqual(mm.number, 144) 1341 1342 def testMetronomeMarkA(self): 1343 from music21 import tempo 1344 mm = tempo.MetronomeMark() 1345 mm.number = 56 # should implicitly set text 1346 self.assertEqual(mm.text, 'adagio') 1347 self.assertTrue(mm.textImplicit) 1348 mm.text = 'slowish' 1349 self.assertEqual(mm.text, 'slowish') 1350 self.assertFalse(mm.textImplicit) 1351 # default 1352 self.assertEqual(mm.referent.quarterLength, 1.0) 1353 1354 # setting the text first 1355 mm = tempo.MetronomeMark() 1356 mm.text = 'presto' 1357 mm.referent = duration.Duration(3.0) 1358 self.assertEqual(mm.text, 'presto') 1359 self.assertEqual(mm.number, 184) 1360 self.assertTrue(mm.numberImplicit) 1361 mm.number = 200 1362 self.assertEqual(mm.number, 200) 1363 self.assertFalse(mm.numberImplicit) 1364 # still have default 1365 self.assertEqual(mm.referent.quarterLength, 3.0) 1366 self.assertEqual(repr(mm), '<music21.tempo.MetronomeMark presto Dotted Half=200>') 1367 1368 def testMetronomeMarkB(self): 1369 mm = MetronomeMark() 1370 # with no args these are set to None 1371 self.assertEqual(mm.numberImplicit, None) 1372 self.assertEqual(mm.textImplicit, None) 1373 1374 mm = MetronomeMark(number=100) 1375 self.assertEqual(mm.number, 100) 1376 self.assertFalse(mm.numberImplicit) 1377 self.assertEqual(mm.text, None) 1378 # not set 1379 self.assertEqual(mm.textImplicit, None) 1380 1381 mm = MetronomeMark(number=101, text='rapido') 1382 self.assertEqual(mm.number, 101) 1383 self.assertFalse(mm.numberImplicit) 1384 self.assertEqual(mm.text, 'rapido') 1385 self.assertFalse(mm.textImplicit) 1386 1387 def testMetronomeModulationA(self): 1388 from music21 import tempo 1389 # need to create a mm without a speed 1390 # want to say that an eighth is becoming the speed of a sixteenth 1391 mm1 = tempo.MetronomeMark(referent=0.5, number=120) 1392 mm2 = tempo.MetronomeMark(referent='16th') 1393 1394 mmod1 = tempo.MetricModulation() 1395 mmod1.oldMetronome = mm1 1396 mmod1.newMetronome = mm2 1397 1398 # this works, and new value is updated: 1399 self.assertEqual(str(mmod1), 1400 '<music21.tempo.MetricModulation ' 1401 + '<music21.tempo.MetronomeMark animato Eighth=120>=' 1402 + '<music21.tempo.MetronomeMark animato 16th=120>>') 1403 1404 # we can get the same result by using setEqualityByReferent() 1405 mm1 = tempo.MetronomeMark(referent=0.5, number=120) 1406 mmod1 = tempo.MetricModulation() 1407 mmod1.oldMetronome = mm1 1408 # will automatically set right mm, as presently is None 1409 mmod1.setOtherByReferent(referent='16th') 1410 # should get the same result as above, but with defined value 1411 self.assertEqual(str(mmod1), 1412 '<music21.tempo.MetricModulation ' 1413 + '<music21.tempo.MetronomeMark animato Eighth=120>=' 1414 + '<music21.tempo.MetronomeMark animato 16th=120>>') 1415 # the effective speed as been slowed by this modulation 1416 self.assertEqual(mmod1.oldMetronome.getQuarterBPM(), 60.0) 1417 self.assertEqual(mmod1.newMetronome.getQuarterBPM(), 30.0) 1418 1419 def testGetPreviousMetronomeMarkA(self): 1420 from music21 import tempo 1421 from music21 import stream 1422 1423 # test getting basic metronome marks 1424 p = stream.Part() 1425 m1 = stream.Measure() 1426 m1.repeatAppend(note.Note(quarterLength=1), 4) 1427 m2 = copy.deepcopy(m1) 1428 mm1 = tempo.MetronomeMark(number=56, referent=0.25) 1429 m1.insert(0, mm1) 1430 mm2 = tempo.MetronomeMark(number=150, referent=0.5) 1431 m2.insert(0, mm2) 1432 p.append([m1, m2]) 1433 self.assertEqual(str(mm2.getPreviousMetronomeMark()), 1434 '<music21.tempo.MetronomeMark adagio 16th=56>') 1435 # p.show() 1436 1437 def testGetPreviousMetronomeMarkB(self): 1438 from music21 import tempo 1439 from music21 import stream 1440 1441 # test using a tempo text, will return a default metronome mark if possible 1442 p = stream.Part() 1443 m1 = stream.Measure() 1444 m1.repeatAppend(note.Note(quarterLength=1), 4) 1445 m2 = copy.deepcopy(m1) 1446 mm1 = tempo.TempoText('slow') 1447 m1.insert(0, mm1) 1448 mm2 = tempo.MetronomeMark(number=150, referent=0.5) 1449 m2.insert(0, mm2) 1450 p.append([m1, m2]) 1451 self.assertEqual(str(mm2.getPreviousMetronomeMark()), 1452 '<music21.tempo.MetronomeMark slow Quarter=56>') 1453 # p.show() 1454 1455 def testGetPreviousMetronomeMarkC(self): 1456 from music21 import tempo 1457 from music21 import stream 1458 1459 # test using a metric modulation 1460 p = stream.Part() 1461 m1 = stream.Measure() 1462 m1.repeatAppend(note.Note(quarterLength=1), 4) 1463 m2 = copy.deepcopy(m1) 1464 m3 = copy.deepcopy(m2) 1465 1466 mm1 = tempo.MetronomeMark('slow') 1467 m1.insert(0, mm1) 1468 1469 mm2 = tempo.MetricModulation() 1470 mm2.oldMetronome = tempo.MetronomeMark(referent=1, number=52) 1471 mm2.setOtherByReferent(referent='16th') 1472 m2.insert(0, mm2) 1473 1474 mm3 = tempo.MetronomeMark(number=150, referent=0.5) 1475 m3.insert(0, mm3) 1476 1477 p.append([m1, m2, m3]) 1478 # p.show() 1479 1480 self.assertEqual(str(mm3.getPreviousMetronomeMark()), 1481 '<music21.tempo.MetronomeMark lento 16th=52>') 1482 1483 def testSetReferentA(self): 1484 '''Test setting referents directly via context searches. 1485 ''' 1486 from music21 import tempo 1487 from music21 import stream 1488 p = stream.Part() 1489 m1 = stream.Measure() 1490 m1.repeatAppend(note.Note(quarterLength=1), 4) 1491 m2 = copy.deepcopy(m1) 1492 m3 = copy.deepcopy(m2) 1493 1494 mm1 = tempo.MetronomeMark(number=92) 1495 m1.insert(0, mm1) 1496 1497 mm2 = tempo.MetricModulation() 1498 m2.insert(0, mm2) 1499 1500 p.append([m1, m2, m3]) 1501 1502 mm2.oldReferent = 0.25 1503 self.assertEqual(str(mm2.oldMetronome), 1504 '<music21.tempo.MetronomeMark moderate 16th=368.0>') 1505 mm2.setOtherByReferent(referent=2) 1506 self.assertEqual(str(mm2.newMetronome), 1507 '<music21.tempo.MetronomeMark moderate Half=368.0>') 1508 # p.show() 1509 1510 def testSetReferentB(self): 1511 from music21 import tempo 1512 from music21 import stream 1513 s = stream.Stream() 1514 mm1 = tempo.MetronomeMark(number=60) 1515 s.append(mm1) 1516 s.repeatAppend(note.Note(quarterLength=1), 2) 1517 s.repeatAppend(note.Note(quarterLength=0.5), 4) 1518 1519 mmod1 = tempo.MetricModulation() 1520 mmod1.oldReferent = 0.5 # can use Duration objects 1521 mmod1.newReferent = 'quarter' # can use Duration objects 1522 s.append(mmod1) 1523 mmod1.updateByContext() 1524 1525 self.assertEqual(str(mmod1.oldMetronome.referent), '<music21.duration.Duration 0.5>') 1526 self.assertEqual(mmod1.oldMetronome.number, 120) 1527 self.assertEqual(str(mmod1.newMetronome), 1528 '<music21.tempo.MetronomeMark animato Quarter=120.0>') 1529 1530 s.append(note.Note()) 1531 s.repeatAppend(note.Note(quarterLength=1.5), 2) 1532 1533 mmod2 = tempo.MetricModulation() 1534 mmod2.oldReferent = 1.5 1535 mmod2.newReferent = 'quarter' # can use Duration objects 1536 s.append(mmod2) 1537 mmod2.updateByContext() 1538 self.assertEqual(str(mmod2.oldMetronome), 1539 '<music21.tempo.MetronomeMark animato Dotted Quarter=80.0>') 1540 self.assertEqual(str(mmod2.newMetronome), 1541 '<music21.tempo.MetronomeMark andantino Quarter=80.0>') 1542 1543 # s.repeatAppend(note.Note(), 4) 1544 # s.show() 1545 1546 def testSetReferentC(self): 1547 from music21 import tempo 1548 from music21 import stream 1549 s = stream.Stream() 1550 mm1 = tempo.MetronomeMark(number=60) 1551 s.append(mm1) 1552 s.repeatAppend(note.Note(quarterLength=1), 2) 1553 s.repeatAppend(note.Note(quarterLength=0.5), 4) 1554 1555 mmod1 = tempo.MetricModulation() 1556 s.append(mmod1) 1557 mmod1.oldReferent = 0.5 # can use Duration objects 1558 mmod1.newReferent = 'quarter' # can use Duration objects 1559 1560 self.assertEqual(str(mmod1.oldMetronome.referent), '<music21.duration.Duration 0.5>') 1561 self.assertEqual(mmod1.oldMetronome.number, 120) 1562 self.assertEqual(str(mmod1.newMetronome), 1563 '<music21.tempo.MetronomeMark larghetto Quarter=120.0>') 1564 1565 s.append(note.Note()) 1566 s.repeatAppend(note.Note(quarterLength=1.5), 2) 1567 1568 mmod2 = tempo.MetricModulation() 1569 s.append(mmod2) 1570 mmod2.oldReferent = 1.5 1571 mmod2.newReferent = 'quarter' # can use Duration objects 1572 1573 self.assertEqual(str(mmod2.oldMetronome), 1574 '<music21.tempo.MetronomeMark larghetto Dotted Quarter=80.0>') 1575 self.assertEqual(str(mmod2.newMetronome), 1576 '<music21.tempo.MetronomeMark larghetto Quarter=80.0>') 1577 # s.repeatAppend(note.Note(), 4) 1578 # s.show() 1579 1580 def testSetReferentD(self): 1581 from music21 import tempo 1582 from music21 import stream 1583 s = stream.Stream() 1584 mm1 = tempo.MetronomeMark(number=60) 1585 s.append(mm1) 1586 s.repeatAppend(note.Note(quarterLength=1), 2) 1587 s.repeatAppend(note.Note(quarterLength=0.5), 4) 1588 1589 mmod1 = tempo.MetricModulation() 1590 s.append(mmod1) 1591 # even with we have no assigned metronome, update context will create 1592 mmod1.updateByContext() 1593 1594 self.assertEqual(str(mmod1.oldMetronome.referent), '<music21.duration.Duration 1.0>') 1595 self.assertEqual(mmod1.oldMetronome.number, 60) # value form last mm 1596 # still have not set new 1597 self.assertEqual(mmod1.newMetronome, None) 1598 1599 mmod1.newReferent = 0.25 1600 self.assertEqual(str(mmod1.newMetronome), '<music21.tempo.MetronomeMark larghetto 16th=60>') 1601 # s.append(note.Note()) 1602 # s.repeatAppend(note.Note(quarterLength=1.5), 2) 1603 1604 def testSetReferentE(self): 1605 from music21 import stream 1606 1607 s = stream.Stream() 1608 mm1 = MetronomeMark(number=70) 1609 s.append(mm1) 1610 s.repeatAppend(note.Note(quarterLength=1), 2) 1611 s.repeatAppend(note.Note(quarterLength=0.5), 4) 1612 1613 mmod1 = MetricModulation() 1614 mmod1.oldReferent = 'eighth' 1615 mmod1.newReferent = 'half' 1616 s.append(mmod1) 1617 self.assertEqual(mmod1.oldMetronome.number, 140) 1618 self.assertEqual(mmod1.newMetronome.number, 140) 1619 1620 s = stream.Stream() 1621 mm1 = MetronomeMark(number=70) 1622 s.append(mm1) 1623 s.repeatAppend(note.Note(quarterLength=1), 2) 1624 s.repeatAppend(note.Note(quarterLength=0.5), 4) 1625 1626 # make sure it works in reverse too 1627 mmod1 = MetricModulation() 1628 mmod1.oldReferent = 'eighth' 1629 mmod1.newReferent = 'half' 1630 s.append(mmod1) 1631 self.assertEqual(mmod1.newMetronome.number, 140) 1632 self.assertEqual(mmod1.oldMetronome.number, 140) 1633 self.assertEqual(mmod1.number, 140) 1634 1635 def testSecondsPerQuarterA(self): 1636 mm = MetronomeMark(referent=1.0, number=120.0) 1637 self.assertEqual(mm.secondsPerQuarter(), 0.5) 1638 self.assertEqual(mm.durationToSeconds(120), 60.0) 1639 self.assertEqual(mm.secondsToDuration(60.0).quarterLength, 120.0) 1640 1641 mm = MetronomeMark(referent=0.5, number=120.0) 1642 self.assertEqual(mm.secondsPerQuarter(), 1.0) 1643 self.assertEqual(mm.durationToSeconds(60), 60.0) 1644 self.assertEqual(mm.secondsToDuration(60.0).quarterLength, 60.0) 1645 1646 mm = MetronomeMark(referent=2.0, number=120.0) 1647 self.assertEqual(mm.secondsPerQuarter(), 0.25) 1648 self.assertEqual(mm.durationToSeconds(240), 60.0) 1649 self.assertEqual(mm.secondsToDuration(60.0).quarterLength, 240.0) 1650 1651 mm = MetronomeMark(referent=1.5, number=120.0) 1652 self.assertAlmostEqual(mm.secondsPerQuarter(), 1 / 3) 1653 self.assertEqual(mm.durationToSeconds(180), 60.0) 1654 self.assertEqual(mm.secondsToDuration(60.0).quarterLength, 180.0) 1655 1656 def testStylesAreShared(self): 1657 halfNote = note.Note(type='half') 1658 mm = MetronomeMark('slow', 40, halfNote) 1659 mm.style.justify = 'left' 1660 self.assertIs(mm._tempoText.style, mm.style) 1661 self.assertIs(mm._tempoText._textExpression.style, mm.style) 1662 1663 1664# ------------------------------------------------------------------------------ 1665# define presented order in documentation 1666_DOC_ORDER = [MetronomeMark, TempoText, MetricModulation, TempoIndication, 1667 AccelerandoSpanner, RitardandoSpanner, TempoChangeSpanner, 1668 interpolateElements] 1669 1670 1671if __name__ == '__main__': 1672 import music21 1673 music21.mainTest(Test) # , runTest='testStylesAreShared') 1674