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