# ------------------------------------------------------------------------------
# Name: dynamics.py
# Purpose: Module for dealing with dynamics changes.
#
# Authors: Michael Scott Asato Cuthbert
# Christopher Ariza
#
# Copyright: Copyright © 2009-2023 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Classes and functions for creating and manipulating dynamic symbols. Rather than
subclasses, the :class:`~music21.dynamics.Dynamic` object is often specialized by parameters.
'''
from __future__ import annotations
import unittest
from music21 import base
from music21 import common
from music21 import environment
from music21 import exceptions21
from music21 import spanner
from music21 import style
environLocal = environment.Environment('dynamics')
shortNames = ['pppppp', 'ppppp', 'pppp', 'ppp', 'pp', 'p', 'mp',
'mf', 'f', 'fp', 'sf', 'ff', 'fff', 'ffff', 'fffff', 'ffffff']
longNames = {'ppp': 'pianississimo',
'pp': 'pianissimo',
'p': 'piano',
'mp': 'mezzopiano',
'mf': 'mezzoforte',
'f': 'forte',
'fp': 'fortepiano',
'sf': 'sforzando',
'ff': 'fortissimo',
'fff': 'fortississimo'}
# could be really useful for automatic description of musical events
englishNames = {'ppp': 'extremely soft',
'pp': 'very soft',
'p': 'soft',
'mp': 'moderately soft',
'mf': 'moderately loud',
'f': 'loud',
'ff': 'very loud',
'fff': 'extremely loud'}
[docs]
def dynamicStrFromDecimal(n):
'''
Given a decimal from 0 to 1, return a string representing a dynamic
with 0 being the softest (0.01 = 'ppp') and 1 being the loudest (0.9+ = 'fff').
0 returns "n" (niente), while ppp and fff are the loudest dynamics used.
>>> dynamics.dynamicStrFromDecimal(0.25)
'pp'
>>> dynamics.dynamicStrFromDecimal(1)
'fff'
'''
if n is None or n <= 0:
return 'n'
elif n < 0.11:
return 'pppp'
elif n < 0.16:
return 'ppp'
elif n < 0.26:
return 'pp'
elif n < 0.36:
return 'p'
elif n < 0.5:
return 'mp'
elif n < 0.65:
return 'mf'
elif n < 0.8:
return 'f'
elif n < 0.9:
return 'ff'
else:
return 'fff'
# defaults used for volume scalar
dynamicStrToScalar = {
None: 0.5, # default value
'n': 0.0,
'pppp': 0.1,
'ppp': 0.15,
'pp': 0.25,
'p': 0.35,
'mp': 0.45,
'mf': 0.55,
'f': 0.7,
'fp': 0.75,
'sf': 0.85,
'ff': 0.85,
'fff': 0.9,
'ffff': 0.95,
}
# ------------------------------------------------------------------------------
class DynamicException(exceptions21.Music21Exception):
pass
# ------------------------------------------------------------------------------
[docs]
class Dynamic(base.Music21Object):
'''
Object representation of Dynamics.
>>> pp1 = dynamics.Dynamic('pp')
>>> pp1.value
'pp'
>>> pp1.longName
'pianissimo'
>>> pp1.englishName
'very soft'
Dynamics can also be specified on a 0 to 1 scale with 1 being the
loudest (see dynamicStrFromDecimal() above).
>>> ppp = dynamics.Dynamic(0.15) # on 0 to 1 scale
>>> ppp.value
'ppp'
>>> print(f'{ppp.volumeScalar:.2f}')
0.15
Note that we got lucky last time because the dynamic 0.15 exactly corresponds
to what we've considered the default for 'ppp'. Here we assign 0.98 which
is close to the 0.9 that is the default for 'fff' -- but the 0.98 will
be retained in the .volumeScalar
>>> loud = dynamics.Dynamic(0.98) # on 0 to 1 scale
>>> loud.value
'fff'
>>> print(f'{loud.volumeScalar:.2f}')
0.98
Transferring the .value ('fff') to a new Dynamic object will set the volumeScalar
back to 0.9.
>>> loud2 = dynamics.Dynamic(loud.value)
>>> loud2.value
'fff'
>>> print(f'{loud2.volumeScalar:.2f}')
0.90
Custom dynamics are possible:
>>> myDyn = dynamics.Dynamic('rfzsfmp')
>>> myDyn.value
'rfzsfmp'
>>> print(myDyn.volumeScalar)
0.5
>>> myDyn.volumeScalar = 0.87
>>> myDyn.volumeScalar
0.87
Dynamics can be placed anywhere in a stream.
>>> s = stream.Stream()
>>> s.insert(0, note.Note('E-4', type='half'))
>>> s.insert(2, note.Note('F#5', type='half'))
>>> s.insert(0, dynamics.Dynamic('pp'))
>>> s.insert(1, dynamics.Dynamic('mf'))
>>> s.insert(3, dynamics.Dynamic('fff'))
>>> #_DOCS_SHOW s.show()
.. image:: images/dynamics_simple.*
:width: 344
'''
classSortOrder = 10
_styleClass = style.TextStyle
_DOC_ORDER = ['longName', 'englishName']
_DOC_ATTR: dict[str, str] = {
'longName': r'''
the name of this dynamic in Italian.
>>> d = dynamics.Dynamic('pp')
>>> d.longName
'pianissimo'
''',
'englishName': r'''
the name of this dynamic in English.
>>> d = dynamics.Dynamic('pp')
>>> d.englishName
'very soft'
''',
'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 dynamics,
the placement above or below an object has semantic
meaning and is not purely presentational. For instance, a dynamic
placed between two staves in a piano part implies that it applies
to both hands, while one placed below the lower staff would apply
only to the left hand.
''',
}
def __init__(self, value=None, **keywords):
super().__init__(**keywords)
# the scalar is used to calculate the final output of a note
# under this dynamic. if this property is set, it will override
# use of a default.
self._volumeScalar = None
self.longName = None
self.englishName = None
self._value = None
if not isinstance(value, str):
# assume it is a number, try to convert
self._volumeScalar = value
self.value = dynamicStrFromDecimal(value)
else:
self.value = value # will use property
# for position, as musicxml, all units are in tenths of interline space
# position is needed as default positions are often incorrect
self.style.absoluteX = -36
self.style.absoluteY = -80 # below top line
# this value provides good 16th note alignment
self.placement = None
def _reprInternal(self):
return str(self.value)
def _getValue(self):
return self._value
def _setValue(self, value):
self._value = value
if self._value in longNames:
self.longName = longNames[self._value]
else:
self.longName = None
if self._value in englishNames:
self.englishName = englishNames[self._value]
else:
self.englishName = None
value = property(_getValue, _setValue,
doc='''
Get or set the value of this dynamic, which sets the long and
English names of this Dynamic. The value is a string specification.
>>> p = dynamics.Dynamic('p')
>>> p.value
'p'
>>> p.englishName
'soft'
>>> p.longName
'piano'
>>> p.value = 'f'
>>> p.value
'f'
>>> p.englishName
'loud'
>>> p.longName
'forte'
''')
def _getVolumeScalar(self):
if self._volumeScalar is not None:
return self._volumeScalar
# use default
elif self._value in dynamicStrToScalar:
return dynamicStrToScalar[self._value]
else:
thisDynamic = self._value
# ignore leading s like in sf
if 's' in thisDynamic:
thisDynamic = thisDynamic[1:]
# ignore closing z like in fz
if thisDynamic[-1] == 'z':
thisDynamic = thisDynamic[:-1]
if thisDynamic in dynamicStrToScalar:
return dynamicStrToScalar[thisDynamic]
else:
return dynamicStrToScalar[None]
def _setVolumeScalar(self, value):
# we can manually set this to be anything, overriding defaults
if common.isNum(value) and 0 <= value <= 1:
self._volumeScalar = value
else:
raise DynamicException(f'cannot set as volume scalar to: {value}')
volumeScalar = property(_getVolumeScalar, _setVolumeScalar, doc=r'''
Get or set the volume scalar for this dynamic. If not explicitly set, a
default volume scalar will be provided. Any number between 0 and 1 can be
used to set the volume scalar, overriding the expected behavior.
As mezzo is at 0.5, the unit interval range is doubled for
generating final output. The default output is 0.5.
>>> d = dynamics.Dynamic('mf')
>>> d.volumeScalar
0.55...
>>> d.volumeScalar = 0.1
>>> d.volumeScalar
0.1
>>> d.value
'mf'
int(volumeScalar \* 127) gives the MusicXML <sound dynamics="x"/> tag
>>> xmlOut = musicxml.m21ToXml.GeneralObjectExporter().parse(d).decode('utf-8')
>>> print(xmlOut)
<?xml...
<direction>
<direction-type>
<dynamics default-x="-36" default-y="-80">
<mf />
</dynamics>
</direction-type>
<sound dynamics="12" />
</direction>...
''')
# ------------------------------------------------------------------------------
[docs]
class DynamicWedge(spanner.Spanner):
'''
Common base-class for Crescendo and Diminuendo.
'''
def __init__(self, *spannedElements, **keywords):
super().__init__(*spannedElements, **keywords)
# from music21 import note
# self.fillElementTypes = [note.GeneralNote]
self.type = None # crescendo or diminuendo
self.placement = 'below' # can be above or below, after musicxml
self.spread = 15 # this unit is in tenths
self.niente = False
[docs]
class Crescendo(DynamicWedge):
'''
A spanner crescendo wedge.
>>> d = dynamics.Crescendo()
>>> d.spread
15
>>> d.spread = 20
>>> d.spread
20
>>> d.type
'crescendo'
'''
def __init__(self, *spannedElements, **keywords):
super().__init__(*spannedElements, **keywords)
self.type = 'crescendo'
[docs]
class Diminuendo(DynamicWedge):
'''
A spanner diminuendo wedge.
>>> d = dynamics.Diminuendo()
>>> d.spread = 20
>>> d.spread
20
'''
def __init__(self, *spannedElements, **keywords):
super().__init__(*spannedElements, **keywords)
self.type = 'diminuendo'
# ------------------------------------------------------------------------------
class TestExternal(unittest.TestCase):
show = True
def testSingle(self):
a = Dynamic('ffff')
if self.show:
a.show()
def testBasic(self):
'''
present each dynamic in a single measure
'''
from music21 import stream
a = stream.Stream()
o = 0
for dynStr in shortNames:
b = Dynamic(dynStr)
a.insert(o, b)
o += 4 # increment
if self.show:
a.show()
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
def testCopyAndDeepcopy(self):
from music21.test.commonTest import testCopyAll
testCopyAll(self, globals())
def testBasic(self):
noDyn = Dynamic()
assert noDyn.longName is None
pp = Dynamic('pp')
self.assertEqual(pp.value, 'pp')
self.assertEqual(pp.longName, 'pianissimo')
self.assertEqual(pp.englishName, 'very soft')
def testCorpusDynamicsWedge(self):
from music21 import corpus
from music21 import dynamics
a = corpus.parse('opus41no1/movement2') # has dynamics!
b = a.parts[0].flatten().getElementsByClass(dynamics.Dynamic)
self.assertEqual(len(b), 35)
b = a.parts[0].flatten().getElementsByClass(dynamics.DynamicWedge)
self.assertEqual(len(b), 2)
def testMusicxmlOutput(self):
# test direct rendering of musicxml
from music21.musicxml import m21ToXml
d = Dynamic('p')
xmlOut = m21ToXml.GeneralObjectExporter().parse(d).decode('utf-8')
match = '<p />'
self.assertNotEqual(xmlOut.find(match), -1, xmlOut)
def testDynamicsPositionA(self):
from music21 import stream
from music21 import note
s = stream.Stream()
selections = ['pp', 'f', 'mf', 'fff']
# positions = [-20, 0, 20]
for i in range(10):
d = Dynamic(selections[i % len(selections)])
s.append(d)
s.append(note.Note('c1'))
# s.show()
def testDynamicsPositionB(self):
import random
from music21 import stream
from music21 import note
from music21 import layout
s = stream.Stream()
for i in range(6):
m = stream.Measure(number=i + 1)
m.append(layout.SystemLayout(isNew=True))
m.append(note.Rest(type='whole'))
s.append(m)
stream_iterator = s.getElementsByClass(stream.Measure)
for m in stream_iterator:
offsets = [x * 0.25 for x in range(16)]
random.shuffle(offsets)
offsets = offsets[:4]
for o in offsets:
d = Dynamic('mf')
d.style.absoluteY = 20
m.insert(o, d)
# s.show()
# ------------------------------------------------------------------------------
# define presented order in documentation
_DOC_ORDER = [Dynamic, dynamicStrFromDecimal]
if __name__ == '__main__':
import music21
music21.mainTest(Test)