diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index 7a203bba8..db050b439 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -424,7 +424,7 @@ def tie(self, value: tie.Tie|None): # d['tie'] = value @property - def volume(self) -> 'music21.volume.Volume': # do NOT change to volume.Volume + def volume(self) -> 'music21.volume.Volume': # do NOT change to volume.Volume, see setter... ''' Get or set the :class:`~music21.volume.Volume` object for this Chord. @@ -457,28 +457,28 @@ def volume(self) -> 'music21.volume.Volume': # do NOT change to volume.Volume if not self.hasComponentVolumes(): # create a single new Volume object for the chord - self._volume = note.NotRest._getVolume(self, forceClient=self) + self._volume = volume.Volume(client=self) return self._volume # if we have components and _volume is None, create a volume from # components velocities = [] - for d in self._notes: - velocities.append(d.volume.velocity) + for inner_n in self._notes: + if inner_n.volume.velocity is not None: + velocities.append(inner_n.volume.velocity) # create new local object - self._volume = volume.Volume(client=self) + out_volume = volume.Volume(client=self) if velocities: # avoid division by zero error - self._volume.velocity = int(round(sum(velocities) / len(velocities))) + out_volume.velocity = int(round(sum(velocities) / len(velocities))) - if t.TYPE_CHECKING: - assert self._volume is not None - return self._volume + self._volume = out_volume + return out_volume @volume.setter def volume(self, expr: None|'music21.volume.Volume'|int|float): - # Do NOT change typing to volume.Volume because it will take the property as - # its name + # Do NOT change typing to volume.Volume w/o quotes because it will take the property as + # its name and be really confused. if isinstance(expr, volume.Volume): expr.client = self # remove any component volumes @@ -487,13 +487,10 @@ def volume(self, expr: None|'music21.volume.Volume'|int|float): note.NotRest._setVolume(self, expr, setClient=False) elif common.isNum(expr): vol = self._getVolume() - if t.TYPE_CHECKING: - assert isinstance(expr, (int, float)) - if expr < 1: # assume a scalar - vol.velocityScalar = expr + vol.velocityScalar = float(expr) else: # assume velocity - vol.velocity = expr + vol.velocity = int(expr) else: raise ChordException(f'unhandled setting expr: {expr}') @@ -528,7 +525,6 @@ def hasComponentVolumes(self) -> bool: >>> c4.hasComponentVolumes() False - ''' count = 0 for c in self._notes: @@ -585,9 +581,9 @@ def setVolumes(self, volumes: Sequence['music21.volume.Volume'|int|float]): v = v_entry else: # create a new Volume if v_entry < 1: # assume a scalar - v = volume.Volume(velocityScalar=v_entry) + v = volume.Volume(velocityScalar=float(v_entry)) else: # assume velocity - v = volume.Volume(velocity=v_entry) + v = volume.Volume(velocity=int(v_entry)) v.client = self c._setVolume(v, setClient=False) diff --git a/music21/note.py b/music21/note.py index 3df3dc047..11946ab51 100644 --- a/music21/note.py +++ b/music21/note.py @@ -1279,9 +1279,9 @@ def _setVolume(self, value: None|volume.Volume|int|float, setClient=True): # call local getVolume will set client appropriately vol = self._getVolume() if value < 1: # assume a scalar - vol.velocityScalar = value + vol.velocityScalar = float(value) else: # assume velocity - vol.velocity = value + vol.velocity = int(value) else: raise TypeError(f'this must be a Volume object, not {value}') diff --git a/music21/volume.py b/music21/volume.py index c61b326aa..02c7315eb 100644 --- a/music21/volume.py +++ b/music21/volume.py @@ -7,7 +7,7 @@ # Authors: Christopher Ariza # Michael Scott Asato Cuthbert # -# Copyright: Copyright © 2011-2012, 2015, 2017 +# Copyright: Copyright © 2011-2012, 2015, 2017, 2024 # Michael Scott Asato Cuthbert # License: BSD, see license.txt # ------------------------------------------------------------------------------ @@ -34,31 +34,36 @@ # ------------------------------------------------------------------------------ - - class VolumeException(exceptions21.Music21Exception): pass # ------------------------------------------------------------------------------ - - class Volume(prebase.ProtoM21Object, SlottedObjectMixin): ''' The Volume object lives on NotRest objects and subclasses. It is not a Music21Object subclass. - >>> v = volume.Volume(velocity=90) - >>> v - - >>> v.velocity - 90 + Generally, just assume that a Note has a volume object and don't worry + about creating this class directly: >>> n = note.Note('C5') >>> v = n.volume >>> v.velocity = 20 >>> v.client is n True + + But if you want to create it yourself, you can specify the client, velocity, + velocityScalar, and + + >>> v = volume.Volume(velocity=90) + >>> v + + >>> v.velocity + 90 + + * Changed in v9: all constructor attributes are keyword only. + (client as first attribute was confusing) ''' # CLASS VARIABLES # __slots__ = ( @@ -70,19 +75,20 @@ class Volume(prebase.ProtoM21Object, SlottedObjectMixin): def __init__( self, + *, client: note.NotRest|None = None, - velocity=None, - velocityScalar=None, - velocityIsRelative=True, + velocity: int|None = None, + velocityScalar: float|None = None, + velocityIsRelative: bool = True, ): # store a reference to the client, as we use this to do context # will use property; if None will leave as None self.client = client - self._velocityScalar = None + self._velocityScalar: float|None = None if velocity is not None: - self.velocity = velocity + self.velocity = int(velocity) elif velocityScalar is not None: - self.velocityScalar = velocityScalar + self.velocityScalar = float(velocityScalar) self._cachedRealized = None self.velocityIsRelative = velocityIsRelative @@ -103,8 +109,14 @@ def getDynamicContext(self): ''' Return the dynamic context of this Volume, based on the position of the client of this object. + + >>> n = note.Note() + >>> n.volume.velocityScalar = 0.9 + >>> s = stream.Measure([dynamics.Dynamic('ff'), n]) + >>> n.volume.getDynamicContext() + ''' - # TODO: find wedges and crescendi too and demo/test. + # TODO: find wedges and crescendi too and demo/test. return self.client.getContextByClass('Dynamic') def mergeAttributes(self, other): @@ -113,9 +125,8 @@ def mergeAttributes(self, other): Values are always copied, not passed by reference. >>> n1 = note.Note() - >>> v1 = volume.Volume() + >>> v1 = n1.volume >>> v1.velocity = 111 - >>> v1.client = n1 >>> v2 = volume.Volume() >>> v2.mergeAttributes(v1) @@ -249,8 +260,10 @@ def getRealized( elif self.client is not None: dm = self.getDynamicContext() # dm may be None else: - environLocal.printDebug(['getRealized():', - 'useDynamicContext is True but no dynamic supplied or found in context']) + environLocal.printDebug([ + 'getRealized():', + 'useDynamicContext is True but no dynamic supplied or found in context', + ]) if dm is not None: # double scalar (so range is between 0 and 1) and scale # the current val (around the base) @@ -314,7 +327,7 @@ def realized(self): return self.getRealized() @property - def velocity(self): + def velocity(self) -> int|None: ''' Get or set the velocity value, a numerical value between 0 and 127 and available setting amplitude on each Note or Pitch in chord. @@ -338,18 +351,20 @@ def velocity(self): return round(v) @velocity.setter - def velocity(self, value): - if not common.isNum(value): + def velocity(self, value: int|float|None): + if value is None: + self._velocityScalar = None + elif not common.isNum(value): raise VolumeException(f'value provided for velocity must be a number, not {value}') - if value < 0: + elif value <= 0: self._velocityScalar = 0.0 - elif value > 127: + elif value >= 127: self._velocityScalar = 1.0 else: self._velocityScalar = value / 127.0 @property - def velocityScalar(self): + def velocityScalar(self) -> float|None: ''' Get or set the velocityScalar value, a numerical value between 0 and 1 and available setting amplitude on each Note or Pitch in @@ -384,16 +399,23 @@ def velocityScalar(self): return v @velocityScalar.setter - def velocityScalar(self, value): + def velocityScalar(self, value: int|float|None): + if value is None: + self._velocityScalar = None + if not common.isNum(value): raise VolumeException('value provided for velocityScalar must be a number, ' + f'not {value}') + + scalar: float if value < 0: - scalar = 0 + scalar = 0.0 elif value > 1: - scalar = 1 + scalar = 1.0 else: - scalar = value + if t.TYPE_CHECKING: + assert value is not None + scalar = float(value) self._velocityScalar = scalar