# ------------------------------------------------------------------------------
# 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')