Source code for music21.tempo

# ------------------------------------------------------------------------------
# Name:         tempo.py
# Purpose:      Classes and tools relating to tempo
#
# Authors:      Christopher Ariza
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2009-22 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
This module defines objects for describing tempo and changes in tempo.
'''
from __future__ import annotations

import copy
import typing as t
import unittest

from music21 import base
from music21 import common
from music21 import duration
from music21 import environment
from music21 import exceptions21
from music21 import expressions
from music21 import note
from music21 import spanner
from music21 import style

if t.TYPE_CHECKING:
    from music21.common.types import OffsetQLIn


environLocal = environment.Environment('tempo')


# all lowercase, even german, for string comparison
defaultTempoValues = {
    'larghissimo': 16,
    'largamente': 32,
    'grave': 40,
    'molto adagio': 40,
    'largo': 46,
    'lento': 52,
    'adagio': 56,
    'slow': 56,
    'langsam': 56,
    'larghetto': 60,
    'adagietto': 66,
    'andante': 72,
    'andantino': 80,
    'andante moderato': 83,  # need number
    'maestoso': 88,
    'moderato': 92,
    'moderate': 92,
    'allegretto': 108,
    'animato': 120,
    'allegro moderato': 128,  # need number
    'allegro': 132,
    'fast': 132,
    'schnell': 132,
    'allegrissimo': 140,  # need number
    'molto allegro': 144,
    'très vite': 144,
    'vivace': 160,
    'vivacissimo': 168,
    'presto': 184,
    'prestissimo': 208,
}


[docs] def convertTempoByReferent( numberSrc: int|float, quarterLengthBeatSrc: int|float, quarterLengthBeatDst=1.0 ) -> float: ''' Convert between equivalent tempi, where the speed stays the same but the beat referent and number change. 60 bpm at quarter, going to half >>> tempo.convertTempoByReferent(60, 1, 2) 30.0 60 bpm at quarter, going to 16th >>> tempo.convertTempoByReferent(60, 1, 0.25) 240.0 60 at dotted quarter, get quarter >>> tempo.convertTempoByReferent(60, 1.5, 1) 90.0 60 at dotted quarter, get half >>> tempo.convertTempoByReferent(60, 1.5, 2) 45.0 60 at dotted quarter, get trip >>> tempo.convertTempoByReferent(60, 1.5, 1/3) 270.0 A Fraction instance can also be used: >>> tempo.convertTempoByReferent(60, 1.5, common.opFrac(1/3)) 270.0 ''' # find duration in seconds of quarter length srcDurPerBeat = 60 / numberSrc # convert to dur for one quarter length dur = srcDurPerBeat / quarterLengthBeatSrc # multiply dur by dst quarter dstDurPerBeat = dur * float(quarterLengthBeatDst) # environLocal.printDebug(['dur', dur, 'dstDurPerBeat', dstDurPerBeat]) # find tempo return float(60 / dstDurPerBeat)
# ------------------------------------------------------------------------------ class TempoException(exceptions21.Music21Exception): pass # ------------------------------------------------------------------------------
[docs] class TempoIndication(base.Music21Object): ''' A generic base class for all tempo indications to inherit. Can be used to filter out all types of tempo indications. ''' classSortOrder = 1 _styleClass = style.TextStyle # def __init__(self, **keywords): # super().__init__(**keywords) # # self.style.justify = 'left' # creates a style object to share.
[docs] def getSoundingMetronomeMark(self, found=None): ''' Get the appropriate MetronomeMark from any sort of TempoIndication, regardless of class. ''' if found is None: found = self if isinstance(found, MetricModulation): return found.newMetronome elif isinstance(found, MetronomeMark): return found elif 'TempoText' in found.classes: return found.getMetronomeMark() else: raise TempoException( f'cannot derive a MetronomeMark from this TempoIndication: {found}')
[docs] def getPreviousMetronomeMark(self): ''' Do activeSite and context searches to try to find the last relevant MetronomeMark or MetricModulation object. If a MetricModulation mark is found, return the new MetronomeMark, or the last relevant. >>> s = stream.Stream() >>> s.insert(0, tempo.MetronomeMark(number=120)) >>> mm1 = tempo.MetronomeMark(number=90) >>> s.insert(20, mm1) >>> mm1.getPreviousMetronomeMark() <music21.tempo.MetronomeMark animato Quarter=120> ''' # environLocal.printDebug(['getPreviousMetronomeMark']) # search for TempoIndication objects, not just MetronomeMark objects # must provide getElementBefore, as will otherwise return self obj = self.getContextByClass('TempoIndication', getElementMethod=common.enums.ElementSearch.BEFORE_OFFSET) if obj is None: return None # nothing to do return self.getSoundingMetronomeMark(obj)
# ------------------------------------------------------------------------------
[docs] class TempoText(TempoIndication): ''' >>> import music21 >>> tm = music21.tempo.TempoText('adagio') >>> tm <music21.tempo.TempoText 'adagio'> >>> print(tm.text) adagio ''' def __init__(self, text=None, **keywords): super().__init__(**keywords) # store text in a TextExpression instance self._textExpression = None # a stored object if text is not None: self.text = str(text) def _reprInternal(self): return repr(self.text) def _getText(self): ''' Get the text used for this expression. ''' return self._textExpression.content def _setText(self, value): ''' Set the text of this repeat expression. This is also the primary way that the stored TextExpression object is created. ''' if self._textExpression is None: self._textExpression = expressions.TextExpression(value) if self.hasStyleInformation: self._textExpression.style = self.style # link styles else: # link the styles and set default style self.style = self._textExpression.style self.applyTextFormatting() else: self._textExpression.content = value text = property(_getText, _setText, doc=''' Get or set the text as a string. >>> import music21 >>> tm = music21.tempo.TempoText('adagio') >>> tm.text 'adagio' >>> tm.getTextExpression() <music21.expressions.TextExpression 'adagio'> ''')
[docs] def getMetronomeMark(self): # noinspection PyShadowingNames ''' Return a MetronomeMark object that is configured from this object's Text. >>> tt = tempo.TempoText('slow') >>> mm = tt.getMetronomeMark() >>> mm.number 56 ''' mm = MetronomeMark(text=self.text) if self.hasStyleInformation: mm.style = self.style else: self.style = mm.style return mm
[docs] def getTextExpression(self, numberImplicit=False): ''' Return a TextExpression object for this text. What is this a deepcopy and not the actual one? ''' if self._textExpression is None: return None else: return copy.deepcopy(self._textExpression)
[docs] def setTextExpression(self, value): ''' Given a TextExpression, set it in this object. ''' self._textExpression = value if self._textExpression.hasStyleInformation: # link styles (use the new _textExpression's style) self.style = self._textExpression.style elif self.hasStyleInformation: # link styles (use self.style) self._textExpression.style = self.style else: # no styles at all: link the styles and set default style self.style = self._textExpression.style self.applyTextFormatting()
[docs] def applyTextFormatting(self, te=None, numberImplicit=False): ''' Apply the default text formatting to the text expression version of this tempo mark. ''' if te is None: # use the stored version if possible te = self._textExpression te.style.fontStyle = 'bold' if numberImplicit: te.style.absoluteY = 20 # if not showing number else: te.style.absoluteY = 45 # 4.5 staff lines above return te
[docs] def isCommonTempoText(self, value=None): ''' Return True or False if the supplied text seems like a plausible Tempo indications be used for this TempoText. >>> tt = tempo.TempoText('adagio') >>> tt.isCommonTempoText() True >>> tt = tempo.TempoText('Largo e piano') >>> tt.isCommonTempoText() True >>> tt = tempo.TempoText('undulating') >>> tt.isCommonTempoText() False ''' def stripText(s): # remove all spaces, punctuation, and make lower s = s.strip() s = s.replace(' ', '') s = s.replace('.', '') s = s.lower() return s # if not provided, use stored text if value is None: value = self._textExpression.content for candidate in defaultTempoValues: candidate = stripText(candidate) value = stripText(value) # simply look for membership, not a complete match if value in candidate or candidate in value: return True return False
# ------------------------------------------------------------------------------ class MetronomeMarkException(TempoException): pass # TODO: define if tempo applies only to part # ------------------------------------------------------------------------------
[docs] class MetronomeMark(TempoIndication): ''' A way of specifying a particular tempo with a text string, a referent (a duration) and a number. The `referent` attribute is a Duration object, or a string duration type or a floating-point quarter-length value used to create a Duration. MetronomeMarks, as Music21Object subclasses, also have .duration object property independent of the `referent`. >>> a = tempo.MetronomeMark('slow', 40, note.Note(type='half')) >>> a.number 40 >>> a.referent <music21.duration.Duration 2.0> >>> a.referent.type 'half' >>> print(a.text) slow Some text marks will automatically suggest a number. >>> mm = tempo.MetronomeMark('adagio') >>> mm.number 56 >>> mm.numberImplicit True For certain numbers, a text value can be set implicitly. >>> tm2 = tempo.MetronomeMark(number=208) >>> print(tm2.text) prestissimo >>> tm2.referent <music21.duration.Duration 1.0> Unicode values work fine thanks to Python 3: >>> marking = 'très vite' >>> marking 'très vite' >>> print(tempo.defaultTempoValues[marking]) 144 >>> tm2 = tempo.MetronomeMark(marking) >>> tm2.text.endswith('vite') True >>> tm2.number 144 For playback only (no score output) set numberSounding but no number: >>> fast = tempo.MetronomeMark(numberSounding=168) >>> fast <music21.tempo.MetronomeMark Quarter=168 (playback only)> ''' _DOC_ATTR: dict[str, str] = { 'placement': ''' Staff placement: 'above', 'below', or None. A setting of None implies that the placement will be determined by notation software and no particular placement is demanded. This is not placed in the `.style` property, since for some expressions, the placement above or below an object has semantic meaning and is not purely presentational. ''', } def __init__( self, text: str|int|None = None, number: OffsetQLIn|None = None, referent: OffsetQLIn|str|duration.Duration|base.Music21Object|None = None, *, parentheses: bool = False, playbackOnly: bool = False, numberSounding: OffsetQLIn|None = None, numberImplicit: bool|None = None, **keywords ) -> None: super().__init__(**keywords) if number is None and isinstance(text, int): number = text text = None self._number: int|float|None = ( common.numToIntOrFloat(number) if number is not None else None ) self.numberImplicit = None if numberImplicit is not None: self.numberImplicit = numberImplicit elif self._number is not None: self.numberImplicit = False self._tempoText = None # set with property text self.textImplicit = None if text is not None: # use property to create object if necessary self.text = text # TODO: style?? self.parentheses: bool = parentheses self.placement = None self._referent = None # set with property if referent is None: # if referent is None, set a default quarter note duration referent = duration.Duration(type='quarter') self.referent = referent # use property # set implicit values if necessary self._updateNumberFromText() self._updateTextFromNumber() # need to store a sounding value for the case where # a sounding different is different from the number given in the MM self._numberSounding = numberSounding def _reprInternal(self): sounding = '' number = self.number if self._numberSounding is not None and self.number is None: sounding = ' (playback only)' number = self._numberSounding if self.text is None: return f'{self.referent.fullName}={number}{sounding}' else: return f'{self.text} {self.referent.fullName}={number}{sounding}' def _updateTextFromNumber(self): ''' Update text if number is given and text is not defined. ''' if self._tempoText is None and self._number is not None: # PyCharm inspection does not like using attributes on functions that become properties # noinspection PyArgumentList self._setText(self._getDefaultText(self._number), updateNumberFromText=False) if self.text is not None: self.textImplicit = True def _updateNumberFromText(self): ''' Update number if text is given and number is not defined. ''' if self._number is None and self._tempoText is not None: self._number = self._getDefaultNumber(self._tempoText) if self._number is not None: # only if set self.numberImplicit = True # ------------------------------------------------------------------------- def _getReferent(self): return self._referent def _setReferent(self, value): if value is None: # this may be better not here # if referent is None, set a default quarter note duration self._referent = duration.Duration(type='quarter') # assume ql value or a type string elif common.isNum(value) or isinstance(value, str): self._referent = duration.Duration(value) elif isinstance(value, base.Music21Object): # try to get duration object, like from Note self._referent = value.duration elif isinstance(value, duration.Duration): self._referent = value # should be a music21.duration.Duration object or a # Music21Object with a duration or None else: raise TempoException(f'Cannot get a Duration from the supplied object: {value}') referent = property(_getReferent, _setReferent, doc=''' Get or set the referent, or the Duration object that is the reference for the tempo value in BPM. ''') # properties and conversions def _getText(self): if self._tempoText is None: return None return self._tempoText.text def _setText(self, value, updateNumberFromText=True): if value is None: self._tempoText = None elif isinstance(value, TempoText): self._tempoText = value self.textImplicit = False # must set here else: self._tempoText = TempoText(value) if self.hasStyleInformation: self._tempoText.style = self.style # link style information else: self.style = self._tempoText.style self.textImplicit = False # must set here if updateNumberFromText: self._updateNumberFromText() text = property(_getText, _setText, doc=''' Get or set a text string for this MetronomeMark. Internally implemented as a :class:`~music21.tempo.TempoText` object, which stores the text in a :class:`~music21.expression.TextExpression` object. >>> mm = tempo.MetronomeMark(number=123) >>> mm.text == None True >>> mm.text = 'medium fast' >>> print(mm.text) medium fast ''') def _getNumber(self) -> int|float|None: return self._number # may be None def _setNumber(self, value: int|float|None, updateTextFromNumber=True): # do not replace with a @property since _setNumber has an # optional second attribute. if not common.isNum(value): raise TempoException('cannot set number to a string') self._number = common.numToIntOrFloat(value) self.numberImplicit = False if updateTextFromNumber: self._updateTextFromNumber() number = property(_getNumber, _setNumber, doc=''' Get and set the number, or the numerical value of the Metronome. >>> mm = tempo.MetronomeMark('slow') >>> mm.number 56 >>> mm.numberImplicit True >>> mm.number = 52.5 >>> mm.number 52.5 >>> mm.numberImplicit False ''') def _getNumberSounding(self): return self._numberSounding # may be None def _setNumberSounding(self, value): if not common.isNum(value) and value is not None: raise TempoException('cannot set numberSounding to a string') self._numberSounding = value numberSounding = property(_getNumberSounding, _setNumberSounding, doc=''' Get and set the numberSounding, or the numerical value of the Metronome that is used for playback independent of display. If numberSounding is None, number is assumed to be numberSounding. >>> mm = tempo.MetronomeMark('slow') >>> mm.number 56 >>> mm.numberImplicit True >>> mm.numberSounding is None True >>> mm.numberSounding = 120 >>> mm.numberSounding 120 ''') # -------------------------------------------------------------------------
[docs] def getQuarterBPM(self, useNumberSounding=True) -> float|None: ''' Get a BPM value where the beat is a quarter; must convert from the defined beat to a quarter beat. Will return None if no beat number is defined. This mostly used for generating MusicXML <sound> tags when necessary. >>> mm = tempo.MetronomeMark(number=60, referent='half') >>> mm.getQuarterBPM() 120.0 >>> mm.referent = 'quarter' >>> mm.getQuarterBPM() 60.0 ''' if useNumberSounding and self.numberSounding is not None: return convertTempoByReferent(self.numberSounding, self.referent.quarterLength, 1.0) if self.number is not None: # target quarter length is always 1.0 return convertTempoByReferent(self.number, self.referent.quarterLength, 1.0) return None
[docs] def setQuarterBPM(self, value, setNumber=True): ''' Given a value in BPM, use it to set the value of this MetronomeMark. BPM values are assumed to refer only to quarter notes; different beat values, if defined here, will be scaled. >>> mm = tempo.MetronomeMark(number=60, referent='half') >>> mm.setQuarterBPM(240) # set to 240 for a quarter >>> mm.number # a half is half as fast 120 ''' # assuming a quarter value coming in, what is with our current beat value = convertTempoByReferent(value, 1.0, self.referent.quarterLength) if not setNumber: # convert this to a quarter bpm self._numberSounding = value else: # go through property to set implicit status self.number = value
def _getDefaultNumber(self, tempoText): ''' Given a tempo text expression or a TempoText, get the default number. >>> mm = tempo.MetronomeMark() >>> mm._getDefaultNumber('schnell') 132 >>> mm._getDefaultNumber('adagio') 56 >>> mm._getDefaultNumber('Largo e piano') 46 ''' if isinstance(tempoText, TempoText): tempoStr = tempoText.text else: tempoStr = tempoText post = None # returned if no match if tempoStr.lower() in defaultTempoValues: post = defaultTempoValues[tempoStr.lower()] # an exact match elif tempoStr in defaultTempoValues: post = defaultTempoValues[tempoStr] # look for partial matches if post is None: for word in tempoStr.split(' '): for key in defaultTempoValues: if key == word.lower(): post = defaultTempoValues[key] return post def _getDefaultText(self, number, spread=2): ''' Given a tempo number try to get a text expression; presently only looks for approximate matches The `spread` value is a +/- shift around the default tempo indications defined in defaultTempoValues. >>> mm = tempo.MetronomeMark() >>> mm._getDefaultText(92) 'moderate' >>> mm._getDefaultText(208) 'prestissimo' ''' if common.isNum(number): tempoNumber = number else: # try to convert tempoNumber = float(number) # get items and sort matches = [] for tempoStr, tempoValue in defaultTempoValues.items(): matches.append([tempoValue, tempoStr]) matches.sort() # environLocal.printDebug(['matches', matches]) post = None for tempoValue, tempoStr in matches: if (tempoValue - spread) <= tempoNumber <= (tempoValue + spread): # found a match post = tempoStr break return post
[docs] def getTextExpression(self, returnImplicit=False): ''' If there is a TextExpression available that is not implicit, return it; otherwise, return None. >>> mm = tempo.MetronomeMark('presto') >>> mm.number 184 >>> mm.numberImplicit True >>> mm.getTextExpression() <music21.expressions.TextExpression 'presto'> >>> mm.textImplicit False >>> mm = tempo.MetronomeMark(number=90) >>> mm.numberImplicit False >>> mm.textImplicit True >>> mm.getTextExpression() is None True >>> mm.getTextExpression(returnImplicit=True) <music21.expressions.TextExpression 'maestoso'> ''' if self._tempoText is None: return None # if explicit, always return; if implicit, return if returnImplicit true if not self.textImplicit or (self.textImplicit and returnImplicit): # adjust position if number is implicit; pass number implicit return self._tempoText.getTextExpression( numberImplicit=self.numberImplicit)
# -------------------------------------------------------------------------
[docs] def getEquivalentByReferent(self, referent): ''' Return a new MetronomeMark object that has an equivalent speed but different number and referent values based on a supplied referent (given as a Duration type, quarterLength, or Duration object). >>> mm1 = tempo.MetronomeMark(number=60, referent=1.0) >>> mm1.getEquivalentByReferent(0.5) <music21.tempo.MetronomeMark larghetto Eighth=120> >>> mm1.getEquivalentByReferent(duration.Duration('half')) <music21.tempo.MetronomeMark larghetto Half=30> >>> mm1.getEquivalentByReferent('longa') <music21.tempo.MetronomeMark larghetto Imperfect Longa=3.75> ''' if common.isNum(referent): # assume quarter length quarterLength = referent elif isinstance(referent, str): # try to get quarter len d = duration.Duration(referent) quarterLength = d.quarterLength else: # TODO: test if a Duration quarterLength = referent.quarterLength if self.number is not None: newNumber = convertTempoByReferent(self.number, self.referent.quarterLength, quarterLength) else: newNumber = None return MetronomeMark(text=self.text, number=newNumber, referent=duration.Duration(quarterLength))
# def getEquivalentByNumber(self, number): # ''' # Return a new MetronomeMark object that has an equivalent speed but different number and # referent values based on a supplied tempo number. # ''' # pass
[docs] def getMaintainedNumberWithReferent(self, referent): ''' Return a new MetronomeMark object that has an equivalent number but a new referent. ''' return MetronomeMark(text=self.text, number=self.number, referent=referent)
# -------------------------------------------------------------------------- # real-time realization
[docs] def secondsPerQuarter(self): ''' Return the duration in seconds for each quarter length (not necessarily the referent) of this MetronomeMark. >>> mm1 = tempo.MetronomeMark(referent=1.0, number=60.0) >>> mm1.secondsPerQuarter() 1.0 >>> mm1 = tempo.MetronomeMark(referent=2.0, number=60.0) >>> mm1.secondsPerQuarter() 0.5 >>> mm1 = tempo.MetronomeMark(referent=2.0, number=30.0) >>> mm1.secondsPerQuarter() 1.0 ''' qbpm = self.getQuarterBPM() if qbpm is not None: return 60.0 / self.getQuarterBPM() else: raise MetronomeMarkException('cannot derive seconds as getQuarterBPM() returns None')
[docs] def durationToSeconds(self, durationOrQuarterLength): ''' Given a duration specified as a :class:`~music21.duration.Duration` object or a quarter length, return the resultant time in seconds at the tempo specified by this MetronomeMark. >>> mm1 = tempo.MetronomeMark(referent=1.0, number=60.0) >>> mm1.durationToSeconds(60) 60.0 >>> mm1.durationToSeconds(duration.Duration('16th')) 0.25 ''' if common.isNum(durationOrQuarterLength): ql = durationOrQuarterLength else: # assume a duration object ql = durationOrQuarterLength.quarterLength # get time per quarter return self.secondsPerQuarter() * ql
[docs] def secondsToDuration(self, seconds): ''' Given a duration in seconds, return a :class:`~music21.duration.Duration` object equal to that time. >>> mm1 = tempo.MetronomeMark(referent=1.0, number=60.0) >>> mm1.secondsToDuration(0.25) <music21.duration.Duration 0.25> >>> mm1.secondsToDuration(0.5).type 'eighth' >>> mm1.secondsToDuration(1) <music21.duration.Duration 1.0> ''' if not common.isNum(seconds) or seconds <= 0.0: raise MetronomeMarkException('seconds must be a number greater than zero') ql = seconds / self.secondsPerQuarter() return duration.Duration(quarterLength=ql)
# ------------------------------------------------------------------------------ class MetricModulationException(TempoException): pass # ------------------------------------------------------------------------------
[docs] class MetricModulation(TempoIndication): ''' A class for representing the relationship between two MetronomeMarks. Generally this relationship is one of equality, where the number is maintained but the referent that number is applied to each change. The basic definition of a MetricModulation is given by supplying two MetronomeMarks, one for the oldMetronome, the other for the newMetronome. High level properties, oldReferent and newReferent, and convenience methods permit only setting the referent. The `classicalStyle` attribute determines if the first MetronomeMark describes the new tempo, not the old (the reverse of expected usage). The `maintainBeat` attribute determines if, after an equality statement, the beat is maintained. This is relevant for moving from 3/4 to 6/8, for example. >>> s = stream.Stream() >>> mm1 = tempo.MetronomeMark(number=60) >>> s.append(mm1) >>> s.repeatAppend(note.Note(quarterLength=1), 2) >>> s.repeatAppend(note.Note(quarterLength=0.5), 4) >>> mmod1 = tempo.MetricModulation() >>> mmod1.oldReferent = 0.5 # can use Duration objects >>> mmod1.newReferent = 'quarter' # can use Duration objects >>> s.append(mmod1) >>> mmod1.updateByContext() # get number from last MetronomeMark on Stream >>> mmod1.newMetronome <music21.tempo.MetronomeMark animato Quarter=120> >>> s.append(note.Note()) >>> s.repeatAppend(note.Note(quarterLength=1.5), 2) >>> mmod2 = tempo.MetricModulation() >>> s.append(mmod2) # if the obj is added to Stream, can set referents >>> mmod2.oldReferent = 1.5 # will get number from previous MetronomeMark >>> mmod2.newReferent = 'quarter' >>> mmod2.newMetronome <music21.tempo.MetronomeMark animato Quarter=80> Note that an initial metric modulation can set old and new referents and get None as tempo numbers: >>> mmod3 = tempo.MetricModulation() >>> mmod3.oldReferent = 'half' >>> mmod3.newReferent = '16th' >>> mmod3 <music21.tempo.MetricModulation <music21.tempo.MetronomeMark Half=None>=<music21.tempo.MetronomeMark 16th=None>> test w/ more sane referents that either the old or the new can change without a tempo number >>> mmod3.oldReferent = 'quarter' >>> mmod3.newReferent = 'eighth' >>> mmod3 <music21.tempo.MetricModulation <music21.tempo.MetronomeMark Quarter=None>=<music21.tempo.MetronomeMark Eighth=None>> >>> mmod3.oldMetronome <music21.tempo.MetronomeMark Quarter=None> >>> mmod3.oldMetronome.number = 60 New number automatically updates: >>> mmod3 <music21.tempo.MetricModulation <music21.tempo.MetronomeMark larghetto Quarter=60>=<music21.tempo.MetronomeMark larghetto Eighth=60>> ''' def __init__(self, **keywords): super().__init__(**keywords) self.classicalStyle = False self.maintainBeat = False self.transitionSymbol = '=' # accept different symbols # some old formats use arrows self.arrowDirection = None # can be left, right, or None # showing parens or not self.parentheses = False # store two MetronomeMark objects self._oldMetronome = None self._newMetronome = None def _reprInternal(self): return f'{self.oldMetronome}={self.newMetronome}' # -------------------------------------------------------------------------- # core properties def _setOldMetronome(self, value): if value is None: pass # allow setting as None elif not hasattr(value, 'classes') or 'MetronomeMark' not in value.classes: raise MetricModulationException( 'oldMetronome property must be set with a MetronomeMark instance') self._oldMetronome = value def _getOldMetronome(self): if self._oldMetronome is not None: if self._oldMetronome.number is None: self.updateByContext() return self._oldMetronome oldMetronome = property(_getOldMetronome, _setOldMetronome, doc=''' Get or set the left :class:`~music21.tempo.MetronomeMark` object for the old, or previous value. >>> mm1 = tempo.MetronomeMark(number=60, referent=1) >>> mm1 <music21.tempo.MetronomeMark larghetto Quarter=60> >>> mmod1 = tempo.MetricModulation() >>> mmod1.oldMetronome = mm1 Note that we do need to have a proper MetronomeMark instance to figure this out: >>> mmod1.oldMetronome = 'junk' Traceback (most recent call last): music21.tempo.MetricModulationException: oldMetronome property must be set with a MetronomeMark instance ''') def _setOldReferent(self, value): if value is None: raise MetricModulationException('cannot set old referent to None') # try to get and reassign equivalent if self._oldMetronome is not None: mm = self._oldMetronome.getEquivalentByReferent(value) else: # try to do a context search and get the last MetronomeMark mm = self.getPreviousMetronomeMark() if mm is not None: # replace with an equivalent based on a provided value mm = mm.getEquivalentByReferent(value) if mm is not None: self._oldMetronome = mm else: # create a new metronome mark with a referent, but not w/ a value self._oldMetronome = MetronomeMark(referent=value) # raise MetricModulationException('cannot set old MetronomeMark from provided value.') def _getOldReferent(self): if self._oldMetronome is not None: return self._oldMetronome.referent oldReferent = property(_getOldReferent, _setOldReferent, doc=''' Get or set the referent of the old MetronomeMark. >>> mm1 = tempo.MetronomeMark(number=60, referent=1) >>> mmod1 = tempo.MetricModulation() >>> mmod1.oldMetronome = mm1 >>> mmod1.oldMetronome <music21.tempo.MetronomeMark larghetto Quarter=60> >>> mmod1.oldReferent = 0.25 >>> mmod1.oldMetronome <music21.tempo.MetronomeMark larghetto 16th=240> ''') def _setNewMetronome(self, value): if value is None: pass # allow setting as None elif not hasattr(value, 'classes') or 'MetronomeMark' not in value.classes: raise MetricModulationException( 'newMetronome property must be set with a MetronomeMark instance') self._newMetronome = value def _getNewMetronome(self): # before returning the referent, see if we can update the number if self._newMetronome is not None: if self._newMetronome.number is None: self.updateByContext() return self._newMetronome newMetronome = property(_getNewMetronome, _setNewMetronome, doc=''' Get or set the right :class:`~music21.tempo.MetronomeMark` object for the new, or following value. >>> mm1 = tempo.MetronomeMark(number=60, referent=1) >>> mm1 <music21.tempo.MetronomeMark larghetto Quarter=60> >>> mmod1 = tempo.MetricModulation() >>> mmod1.newMetronome = mm1 >>> mmod1.newMetronome = 'junk' Traceback (most recent call last): music21.tempo.MetricModulationException: newMetronome property must be set with a MetronomeMark instance ''') def _setNewReferent(self, value): if value is None: raise MetricModulationException('cannot set new referent to None') # if oldMetronome is defined, get new metronome from old mm = None if self._newMetronome is not None: # if metro defined, get based on new referent mm = self._newMetronome.getEquivalentByReferent(value) elif self._oldMetronome is not None: # get a new mm with the same number but a new referent mm = self._oldMetronome.getMaintainedNumberWithReferent(value) else: # create a new metronome mark with a referent, but not w/ a value mm = MetronomeMark(referent=value) # raise MetricModulationException('cannot set old MetronomeMark from provided value.') self._newMetronome = mm def _getNewReferent(self): if self._newMetronome is not None: return self._newMetronome.referent newReferent = property(_getNewReferent, _setNewReferent, doc=''' Get or set the referent of the new MetronomeMark. >>> mm1 = tempo.MetronomeMark(number=60, referent=1) >>> mmod1 = tempo.MetricModulation() >>> mmod1.newMetronome = mm1 >>> mmod1.newMetronome <music21.tempo.MetronomeMark larghetto Quarter=60> >>> mmod1.newReferent = 0.25 >>> mmod1.newMetronome <music21.tempo.MetronomeMark larghetto 16th=240> ''') @property def number(self): ''' Get the number of the MetricModulation, or the number assigned to the new MetronomeMark. >>> s = stream.Stream() >>> mm1 = tempo.MetronomeMark(number=60) >>> s.append(mm1) >>> s.repeatAppend(note.Note(quarterLength=1), 2) >>> s.repeatAppend(note.Note(quarterLength=0.5), 4) >>> mmod1 = tempo.MetricModulation() >>> mmod1.oldReferent = 0.5 # can use Duration objects >>> mmod1.newReferent = 'quarter' >>> s.append(mmod1) >>> mmod1.updateByContext() >>> mmod1.newMetronome <music21.tempo.MetronomeMark animato Quarter=120> >>> mmod1.number 120 ''' if self._newMetronome is not None: return self._newMetronome.number # def _setNumber(self, value, updateTextFromNumber=True): # if not common.isNum(value): # raise MetricModulationException('cannot set number to a string') # self._newMetronome.number = value # self._oldMetronome.number = value # -------------------------------------------------------------------------- # high-level configuration methods
[docs] def updateByContext(self): ''' Update this metric modulation based on the context, or the surrounding MetronomeMarks or MetricModulations. The object needs to reside in a Stream for this to be effective. ''' # try to set old number from last; there must be a partially # defined metronome mark already assigned; or create one at quarter? mmLast = self.getPreviousMetronomeMark() mmOld = None if mmLast is not None: # replace with an equivalent based on a provided value if (self._oldMetronome is not None and self._oldMetronome.referent is not None): mmOld = mmLast.getEquivalentByReferent( self._oldMetronome.referent) else: mmOld = MetronomeMark(referent=mmLast.referent, number=mmLast.number) if mmOld is not None: self._oldMetronome = mmOld # if we have a new referent, then update number if (self._newMetronome is not None and self._newMetronome.referent is not None and self._oldMetronome.number is not None): self._newMetronome.number = self._oldMetronome.number
[docs] def setEqualityByReferent(self, side=None, referent=1.0): ''' Set the other side of the metric modulation to an equality; side can be specified, or if one side is None, that side will be set. >>> mm1 = tempo.MetronomeMark(number=60, referent=1) >>> mmod1 = tempo.MetricModulation() >>> mmod1.newMetronome = mm1 >>> mmod1.setEqualityByReferent(None, 2) >>> mmod1 <music21.tempo.MetricModulation <music21.tempo.MetronomeMark larghetto Half=30>=<music21.tempo.MetronomeMark larghetto Quarter=60>> ''' if side is None: if self._oldMetronome is None: side = 'left' elif self._newMetronome is None: side = 'right' if side not in ['left', 'right']: raise TempoException(f'cannot set equality for a side of {side}') if side == 'right': self._newMetronome = self._oldMetronome.getEquivalentByReferent( referent) elif side == 'left': self._oldMetronome = self._newMetronome.getEquivalentByReferent( referent)
[docs] def setOtherByReferent( self, side: str|None = None, referent: str|int|float = 1.0 ): ''' Set the other side of the metric modulation not based on equality, but on a direct translation of the tempo value. referent can be a string type or an int/float quarter length ''' if side is None: if self._oldMetronome is None: side = 'left' elif self._newMetronome is None: side = 'right' if side not in ['left', 'right']: raise TempoException(f'cannot set equality for a side of {side}') if side == 'right': self._newMetronome = self._oldMetronome.getMaintainedNumberWithReferent(referent) elif side == 'left': self._oldMetronome = self._newMetronome.getMaintainedNumberWithReferent(referent)
# ------------------------------------------------------------------------------
[docs] def interpolateElements(element1, element2, sourceStream, destinationStream, autoAdd=True): # noinspection PyShadowingNames ''' Assume that element1 and element2 are two elements in sourceStream and destinationStream with other elements (say eA, eB, eC) between them. For instance, element1 could be the downbeat at offset 10 in sourceStream (a Stream representing a score) and offset 20.5 in destinationStream (which might be a Stream representing the timing of notes in particular recording at approximately but not exactly qtr = 30). Element2 could be the following downbeat in 4/4, at offset 14 in source but offset 25.0 in the recording: >>> sourceStream = stream.Stream() >>> destinationStream = stream.Stream() >>> element1 = note.Note('C4', type='quarter') >>> element2 = note.Note('G4', type='quarter') >>> sourceStream.insert(10, element1) >>> destinationStream.insert(20.5, element1) >>> sourceStream.insert(14, element2) >>> destinationStream.insert(25.0, element2) Suppose eA, eB, and eC are three quarter notes that lie between element1 and element2 in sourceStream and destinationStream, as in: >>> eA = note.Note('D4', type='quarter') >>> eB = note.Note('E4', type='quarter') >>> eC = note.Note('F4', type='quarter') >>> sourceStream.insert(11, eA) >>> sourceStream.insert(12, eB) >>> sourceStream.insert(13, eC) >>> destinationStream.append([eA, eB, eC]) # not needed if autoAdd were true then running this function will cause eA, eB, and eC to have offsets 21.625, 22.75, and 23.875 respectively in destinationStream: >>> tempo.interpolateElements(element1, element2, ... sourceStream, destinationStream, autoAdd=False) >>> for el in [eA, eB, eC]: ... print(el.getOffsetBySite(destinationStream)) 21.625 22.75 23.875 if the elements between element1 and element2 do not yet appear in destinationStream, they are automatically added unless autoAdd is False. (with the default autoAdd, elements are automatically added to new streams): >>> destStream2 = stream.Stream() >>> destStream2.insert(10.1, element1) >>> destStream2.insert(50.5, element2) >>> tempo.interpolateElements(element1, element2, sourceStream, destStream2) >>> for el in [eA, eB, eC]: ... offset = float(el.getOffsetBySite(destStream2)) ... print(f'{offset:.1f}') 20.2 30.3 40.4 (unless autoAdd is set to False, in which case a Tempo Exception arises:) >>> destStream3 = stream.Stream() >>> destStream3.insert(100, element1) >>> destStream3.insert(500, element2) >>> eA.id = 'blah' >>> tempo.interpolateElements(element1, element2, sourceStream, destStream3, autoAdd=False) Traceback (most recent call last): music21.tempo.TempoException: Could not find element <music21.note.Note D> with id ... ''' # TODO: when python 3.13 is the minimum, get rid of the float around el.getOffsetBySite # in the doctest above try: startOffsetSrc = element1.getOffsetBySite(sourceStream) except exceptions21.Music21Exception as e: raise TempoException('could not find element1 in sourceStream') from e try: startOffsetDest = element1.getOffsetBySite(destinationStream) except exceptions21.Music21Exception as e: raise TempoException('could not find element1 in destinationStream') from e try: endOffsetSrc = element2.getOffsetBySite(sourceStream) except exceptions21.Music21Exception as e: raise TempoException('could not find element2 in sourceStream') from e try: endOffsetDest = element2.getOffsetBySite(destinationStream) except exceptions21.Music21Exception as e: raise TempoException('could not find element2 in destinationStream') from e scaleAmount = ((endOffsetDest - startOffsetDest) / (endOffsetSrc - startOffsetSrc)) interpolatedElements = sourceStream.getElementsByOffset( offsetStart=startOffsetSrc, offsetEnd=endOffsetSrc ) for el in interpolatedElements: elOffsetSrc = el.getOffsetBySite(sourceStream) try: el.getOffsetBySite(destinationStream) # dummy except base.SitesException as e: if autoAdd is True: destinationOffset = (scaleAmount * (elOffsetSrc - startOffsetSrc)) + startOffsetDest destinationStream.insert(destinationOffset, el) else: raise TempoException( 'Could not find element ' f'{el!r} with id {el.id!r} ' 'in destinationStream and autoAdd is false' ) from e else: destinationOffset = (scaleAmount * (elOffsetSrc - startOffsetSrc)) + startOffsetDest el.setOffsetBySite(destinationStream, destinationOffset)
# ------------------------------------------------------------------------------
[docs] class TempoChangeSpanner(spanner.Spanner): ''' Spanners showing tempo-change. They do nothing right now. ''' pass
[docs] class RitardandoSpanner(TempoChangeSpanner): ''' Spanner representing a slowing down. ''' pass
[docs] class AccelerandoSpanner(TempoChangeSpanner): ''' Spanner representing a speeding up. ''' pass
# ------------------------------------------------------------------------------ class Test(unittest.TestCase): def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) def testSetup(self): mm1 = MetronomeMark(number=60, referent=note.Note(type='quarter')) self.assertEqual(mm1.number, 60) tm1 = TempoText('Lebhaft') self.assertEqual(tm1.text, 'Lebhaft') def testUnicode(self): # test with no arguments TempoText() # test with arguments TempoText('adagio') # environLocal.printDebug(['testing tempo instantiation', tm]) mm = MetronomeMark('adagio') self.assertEqual(mm.number, 56) self.assertTrue(mm.numberImplicit) self.assertEqual(mm.number, 56) tm2 = TempoText('très vite') self.assertEqual(tm2.text, 'très vite') mm = tm2.getMetronomeMark() self.assertEqual(mm.number, 144) def testTempoTextStyle(self): from music21 import tempo tm = tempo.TempoText('adagio') self.assertEqual(tm.style.absoluteY, 45) self.assertEqual(tm.style.fontStyle, 'bold') tm.style.absoluteY = 33 self.assertEqual(tm.style.absoluteY, 33) tm.style.fontStyle = 'italic' tm.style.fontWeight = None # check that tm.getTextExpression()/tm.text does not modify style tx = tm.text self.assertEqual(tx, 'adagio') te1 = tm.getTextExpression() self.assertEqual(te1.content, 'adagio') self.assertEqual(tm.style.absoluteY, 33) self.assertEqual(tm.style.fontStyle, 'italic') # check that tm.setTextExpression sets tm.style to the te's style (if there is one), # and links the two styles te2 = expressions.TextExpression('andante') te2.style.absoluteY = 38 te2.style.fontStyle = 'bolditalic' self.assertEqual(te2.style.absoluteY, 38) self.assertEqual(te2.style.fontStyle, 'bolditalic') tm.setTextExpression(te2) self.assertEqual(tm.style.absoluteY, 38) self.assertEqual(tm.style.fontStyle, 'bolditalic') self.assertIs(tm.style, te2.style) # check for linked styles # check again that calling tm.getTextExpression/tm.text doesn't modify style tm.getTextExpression() tx = tm.text self.assertEqual(tx, 'andante') self.assertEqual(tm.style.absoluteY, 38) self.assertEqual(tm.style.fontStyle, 'bolditalic') # check that tm.setTextExpression (to a textExpression with no # style) leaves tm.style in place and links the two styles. te3 = expressions.TextExpression('andante with no style') self.assertFalse(te3.hasStyleInformation) self.assertTrue(tm.hasStyleInformation) tm.setTextExpression(te3) self.assertEqual(tm.style.absoluteY, 38) # same as before self.assertEqual(tm.style.fontStyle, 'bolditalic') # same as before self.assertIs(tm.style, te2.style) # check for linked styles # check again that calling tm.getTextExpression/tm.text doesn't modify style tm.getTextExpression() tx = tm.text self.assertEqual(tx, 'andante with no style') self.assertEqual(tm.style.absoluteY, 38) self.assertEqual(tm.style.fontStyle, 'bolditalic') # check that tm.setTextExpression(te4) (with tm and te4 with no style) links the # two styles, with default style set in place. tm.style = None te4 = expressions.TextExpression('andante with no style') self.assertFalse(te4.hasStyleInformation) self.assertFalse(tm.hasStyleInformation) tm.setTextExpression(te4) self.assertEqual(tm.style.absoluteY, 45) # default self.assertEqual(tm.style.fontStyle, 'bold') # default self.assertIs(tm.style, te4.style) # check for linked styles def testMetronomeMarkA(self): from music21 import tempo mm = tempo.MetronomeMark() mm.number = 56 # should implicitly set text self.assertEqual(mm.text, 'adagio') self.assertTrue(mm.textImplicit) mm.text = 'slowish' self.assertEqual(mm.text, 'slowish') self.assertFalse(mm.textImplicit) # default self.assertEqual(mm.referent.quarterLength, 1.0) # setting the text first mm = tempo.MetronomeMark() mm.text = 'presto' mm.referent = duration.Duration(3.0) self.assertEqual(mm.text, 'presto') self.assertEqual(mm.number, 184) self.assertTrue(mm.numberImplicit) mm.number = 200 self.assertEqual(mm.number, 200) self.assertFalse(mm.numberImplicit) # still have default self.assertEqual(mm.referent.quarterLength, 3.0) self.assertEqual(repr(mm), '<music21.tempo.MetronomeMark presto Dotted Half=200>') def testMetronomeMarkB(self): mm = MetronomeMark() # with no args these are set to None self.assertEqual(mm.numberImplicit, None) self.assertEqual(mm.textImplicit, None) mm = MetronomeMark(number=100) self.assertEqual(mm.number, 100) self.assertFalse(mm.numberImplicit) self.assertEqual(mm.text, None) # not set self.assertEqual(mm.textImplicit, None) mm = MetronomeMark(number=101, text='rapido') self.assertEqual(mm.number, 101) self.assertFalse(mm.numberImplicit) self.assertEqual(mm.text, 'rapido') self.assertFalse(mm.textImplicit) def testMetronomeModulationA(self): from music21 import tempo # need to create a mm without a speed # want to say that an eighth is becoming the speed of a sixteenth mm1 = tempo.MetronomeMark(referent=0.5, number=120) mm2 = tempo.MetronomeMark(referent='16th') mmod1 = tempo.MetricModulation() mmod1.oldMetronome = mm1 mmod1.newMetronome = mm2 # this works and new value is updated: self.assertEqual(str(mmod1), '<music21.tempo.MetricModulation ' + '<music21.tempo.MetronomeMark animato Eighth=120>=' + '<music21.tempo.MetronomeMark animato 16th=120>>') # we can get the same result by using setEqualityByReferent() mm1 = tempo.MetronomeMark(referent=0.5, number=120) mmod1 = tempo.MetricModulation() mmod1.oldMetronome = mm1 # will automatically set right mm, as presently is None mmod1.setOtherByReferent(referent='16th') # should get the same result as above, but with defined value self.assertEqual(str(mmod1), '<music21.tempo.MetricModulation ' + '<music21.tempo.MetronomeMark animato Eighth=120>=' + '<music21.tempo.MetronomeMark animato 16th=120>>') # the effective speed has been slowed by this modulation self.assertEqual(mmod1.oldMetronome.getQuarterBPM(), 60.0) self.assertEqual(mmod1.newMetronome.getQuarterBPM(), 30.0) def testGetPreviousMetronomeMarkA(self): from music21 import tempo from music21 import stream # test getting basic metronome marks p = stream.Part() m1 = stream.Measure() m1.repeatAppend(note.Note(quarterLength=1), 4) m2 = copy.deepcopy(m1) mm1 = tempo.MetronomeMark(number=56, referent=0.25) m1.insert(0, mm1) mm2 = tempo.MetronomeMark(number=150, referent=0.5) m2.insert(0, mm2) p.append([m1, m2]) self.assertEqual(str(mm2.getPreviousMetronomeMark()), '<music21.tempo.MetronomeMark adagio 16th=56>') # p.show() def testGetPreviousMetronomeMarkB(self): from music21 import tempo from music21 import stream # test using a tempo text, will return a default metronome mark if possible p = stream.Part() m1 = stream.Measure() m1.repeatAppend(note.Note(quarterLength=1), 4) m2 = copy.deepcopy(m1) mm1 = tempo.TempoText('slow') m1.insert(0, mm1) mm2 = tempo.MetronomeMark(number=150, referent=0.5) m2.insert(0, mm2) p.append([m1, m2]) self.assertEqual(str(mm2.getPreviousMetronomeMark()), '<music21.tempo.MetronomeMark slow Quarter=56>') # p.show() def testGetPreviousMetronomeMarkC(self): from music21 import tempo from music21 import stream # test using a metric modulation p = stream.Part() m1 = stream.Measure() m1.repeatAppend(note.Note(quarterLength=1), 4) m2 = copy.deepcopy(m1) m3 = copy.deepcopy(m2) mm1 = tempo.MetronomeMark('slow') m1.insert(0, mm1) mm2 = tempo.MetricModulation() mm2.oldMetronome = tempo.MetronomeMark(referent=1, number=52) mm2.setOtherByReferent(referent='16th') m2.insert(0, mm2) mm3 = tempo.MetronomeMark(number=150, referent=0.5) m3.insert(0, mm3) p.append([m1, m2, m3]) # p.show() self.assertEqual(str(mm3.getPreviousMetronomeMark()), '<music21.tempo.MetronomeMark lento 16th=52>') def testSetReferentA(self): ''' Test setting referents directly via context searches. ''' from music21 import stream from music21 import tempo p = stream.Part() m1 = stream.Measure() m1.repeatAppend(note.Note(quarterLength=1), 4) m2 = copy.deepcopy(m1) m3 = copy.deepcopy(m2) mm1 = tempo.MetronomeMark(number=92) m1.insert(0, mm1) mm2 = tempo.MetricModulation() m2.insert(0, mm2) p.append([m1, m2, m3]) mm2.oldReferent = 0.25 self.assertEqual(str(mm2.oldMetronome), '<music21.tempo.MetronomeMark moderate 16th=368>') mm2.setOtherByReferent(referent=2) self.assertEqual(str(mm2.newMetronome), '<music21.tempo.MetronomeMark moderate Half=368>') # p.show() def testSetReferentB(self): from music21 import tempo from music21 import stream s = stream.Stream() mm1 = tempo.MetronomeMark(number=60) s.append(mm1) s.repeatAppend(note.Note(quarterLength=1), 2) s.repeatAppend(note.Note(quarterLength=0.5), 4) mmod1 = tempo.MetricModulation() mmod1.oldReferent = 0.5 # can use Duration objects mmod1.newReferent = 'quarter' # can use Duration objects s.append(mmod1) mmod1.updateByContext() self.assertEqual(str(mmod1.oldMetronome.referent), '<music21.duration.Duration 0.5>') self.assertEqual(mmod1.oldMetronome.number, 120) self.assertEqual(str(mmod1.newMetronome), '<music21.tempo.MetronomeMark animato Quarter=120>') s.append(note.Note()) s.repeatAppend(note.Note(quarterLength=1.5), 2) mmod2 = tempo.MetricModulation() mmod2.oldReferent = 1.5 mmod2.newReferent = 'quarter' # can use Duration objects s.append(mmod2) mmod2.updateByContext() self.assertEqual(str(mmod2.oldMetronome), '<music21.tempo.MetronomeMark animato Dotted Quarter=80>') self.assertEqual(str(mmod2.newMetronome), '<music21.tempo.MetronomeMark andantino Quarter=80>') # s.repeatAppend(note.Note(), 4) # s.show() def testSetReferentC(self): from music21 import tempo from music21 import stream s = stream.Stream() mm1 = tempo.MetronomeMark(number=60) s.append(mm1) s.repeatAppend(note.Note(quarterLength=1), 2) s.repeatAppend(note.Note(quarterLength=0.5), 4) mmod1 = tempo.MetricModulation() s.append(mmod1) mmod1.oldReferent = 0.5 # can use Duration objects mmod1.newReferent = 'quarter' # can use Duration objects self.assertEqual(str(mmod1.oldMetronome.referent), '<music21.duration.Duration 0.5>') self.assertEqual(mmod1.oldMetronome.number, 120) self.assertEqual(str(mmod1.newMetronome), '<music21.tempo.MetronomeMark larghetto Quarter=120>') s.append(note.Note()) s.repeatAppend(note.Note(quarterLength=1.5), 2) mmod2 = tempo.MetricModulation() s.append(mmod2) mmod2.oldReferent = 1.5 mmod2.newReferent = 'quarter' # can use Duration objects self.assertEqual(str(mmod2.oldMetronome), '<music21.tempo.MetronomeMark larghetto Dotted Quarter=80>') self.assertEqual(str(mmod2.newMetronome), '<music21.tempo.MetronomeMark larghetto Quarter=80>') # s.repeatAppend(note.Note(), 4) # s.show() def testSetReferentD(self): from music21 import tempo from music21 import stream s = stream.Stream() mm1 = tempo.MetronomeMark(number=60) s.append(mm1) s.repeatAppend(note.Note(quarterLength=1), 2) s.repeatAppend(note.Note(quarterLength=0.5), 4) mmod1 = tempo.MetricModulation() s.append(mmod1) # even with we have no assigned metronome, update context will create mmod1.updateByContext() self.assertEqual(str(mmod1.oldMetronome.referent), '<music21.duration.Duration 1.0>') self.assertEqual(mmod1.oldMetronome.number, 60) # value from last mm # still have not set new self.assertEqual(mmod1.newMetronome, None) mmod1.newReferent = 0.25 self.assertEqual(str(mmod1.newMetronome), '<music21.tempo.MetronomeMark larghetto 16th=60>') # s.append(note.Note()) # s.repeatAppend(note.Note(quarterLength=1.5), 2) def testSetReferentE(self): from music21 import stream s = stream.Stream() mm1 = MetronomeMark(number=70) s.append(mm1) s.repeatAppend(note.Note(quarterLength=1), 2) s.repeatAppend(note.Note(quarterLength=0.5), 4) mmod1 = MetricModulation() mmod1.oldReferent = 'eighth' mmod1.newReferent = 'half' s.append(mmod1) self.assertEqual(mmod1.oldMetronome.number, 140) self.assertEqual(mmod1.newMetronome.number, 140) s = stream.Stream() mm1 = MetronomeMark(number=70) s.append(mm1) s.repeatAppend(note.Note(quarterLength=1), 2) s.repeatAppend(note.Note(quarterLength=0.5), 4) # make sure it works in reverse too mmod1 = MetricModulation() mmod1.oldReferent = 'eighth' mmod1.newReferent = 'half' s.append(mmod1) self.assertEqual(mmod1.newMetronome.number, 140) self.assertEqual(mmod1.oldMetronome.number, 140) self.assertEqual(mmod1.number, 140) def testSecondsPerQuarterA(self): mm = MetronomeMark(referent=1.0, number=120.0) self.assertEqual(mm.secondsPerQuarter(), 0.5) self.assertEqual(mm.durationToSeconds(120), 60.0) self.assertEqual(mm.secondsToDuration(60.0).quarterLength, 120.0) mm = MetronomeMark(referent=0.5, number=120.0) self.assertEqual(mm.secondsPerQuarter(), 1.0) self.assertEqual(mm.durationToSeconds(60), 60.0) self.assertEqual(mm.secondsToDuration(60.0).quarterLength, 60.0) mm = MetronomeMark(referent=2.0, number=120.0) self.assertEqual(mm.secondsPerQuarter(), 0.25) self.assertEqual(mm.durationToSeconds(240), 60.0) self.assertEqual(mm.secondsToDuration(60.0).quarterLength, 240.0) mm = MetronomeMark(referent=1.5, number=120.0) self.assertAlmostEqual(mm.secondsPerQuarter(), 1 / 3) self.assertEqual(mm.durationToSeconds(180), 60.0) self.assertEqual(mm.secondsToDuration(60.0).quarterLength, 180.0) def testStylesAreShared(self): halfNote = note.Note(type='half') mm = MetronomeMark('slow', 40, halfNote) mm.style.justify = 'left' self.assertIs(mm._tempoText.style, mm.style) self.assertIs(mm._tempoText._textExpression.style, mm.style) # ------------------------------------------------------------------------------ # define presented order in documentation _DOC_ORDER = [MetronomeMark, TempoText, MetricModulation, TempoIndication, AccelerandoSpanner, RitardandoSpanner, TempoChangeSpanner, interpolateElements] if __name__ == '__main__': import music21 music21.mainTest(Test) # , runTest='testStylesAreShared')