From 27e5c4cacd116b3093af1cb7ed39fb06c9a310f6 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 27 Oct 2022 16:01:15 -1000 Subject: [PATCH 1/4] fix volume difficulties --- music21/note.py | 2 +- music21/volume.py | 44 +++++++++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/music21/note.py b/music21/note.py index 2b09c9347a..85cf14a6dc 100644 --- a/music21/note.py +++ b/music21/note.py @@ -1032,7 +1032,7 @@ def __deepcopy__(self, memo=None): >>> import copy >>> n = note.NotRest() - >>> n.volume = volume.Volume(50) + >>> n.volume = volume.Volume(velocity=50) >>> m = copy.deepcopy(n) >>> m.volume.client is m True diff --git a/music21/volume.py b/music21/volume.py index 6ba4e1ef38..ca592ff500 100644 --- a/music21/volume.py +++ b/music21/volume.py @@ -48,17 +48,23 @@ 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 ''' # CLASS VARIABLES # __slots__ = ( @@ -70,10 +76,11 @@ class Volume(prebase.ProtoM21Object, SlottedObjectMixin): def __init__( self, + *, client: note.NotRest | None = None, - velocity=None, - velocityScalar=None, - velocityIsRelative=True, + velocity: int | float | None = None, + velocityScalar: int | 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 @@ -103,8 +110,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 +126,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 +261,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 +328,7 @@ def realized(self): return self.getRealized() @property - def velocity(self): + def velocity(self) -> int: ''' 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,7 +352,7 @@ def velocity(self): return round(v) @velocity.setter - def velocity(self, value): + def velocity(self, value: int | float): if not common.isNum(value): raise VolumeException(f'value provided for velocity must be a number, not {value}') if value < 0: From f33c018e0f0771cdffae738d7f5cc3e0cb983120 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 3 Jan 2024 10:53:26 -1000 Subject: [PATCH 2/4] stricter type checking --- music21/chord/__init__.py | 7 ++----- music21/note.py | 4 ++-- music21/volume.py | 25 +++++++++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index 7a203bba89..5972a56eb2 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -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}') diff --git a/music21/note.py b/music21/note.py index a1076b223e..fad690d744 100644 --- a/music21/note.py +++ b/music21/note.py @@ -1269,9 +1269,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 2cbc0a5440..9477c934d7 100644 --- a/music21/volume.py +++ b/music21/volume.py @@ -88,11 +88,11 @@ def __init__( # 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 elif velocityScalar is not None: - self.velocityScalar = velocityScalar + self.velocityScalar = float(velocityScalar) self._cachedRealized = None self.velocityIsRelative = velocityIsRelative @@ -331,7 +331,7 @@ def realized(self): return self.getRealized() @property - def velocity(self) -> int: + 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. @@ -355,18 +355,20 @@ def velocity(self) -> int: return round(v) @velocity.setter - def velocity(self, value: int | float): - 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 @@ -401,7 +403,10 @@ 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}') @@ -411,7 +416,7 @@ def velocityScalar(self, value): scalar = 1 else: scalar = value - self._velocityScalar = scalar + self._velocityScalar = float(scalar) # ------------------------------------------------------------------------------ From 2bae1c0db4b0a98c1b7fd64770aa01041d19f193 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 3 Jan 2024 11:18:52 -1000 Subject: [PATCH 3/4] more typing fixes (all? too much to hope) --- music21/chord/__init__.py | 24 ++++++++++++------------ music21/volume.py | 12 ++++-------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index 5972a56eb2..baf2793403 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. @@ -443,6 +443,8 @@ def volume(self) -> 'music21.volume.Volume': # do NOT change to volume.Volume * Changed in v8: setting volume to a list of volumes is no longer supported. See :meth:`~music21.chord.ChordBase.setVolumes` instead + * Improved in v9: if hasComponentVolumes is True, then the velocity object + returned here will be regenerated with each call, and updates live. OMIT_FROM_DOCS @@ -457,28 +459,27 @@ 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 + 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 @@ -525,7 +526,6 @@ def hasComponentVolumes(self) -> bool: >>> c4.hasComponentVolumes() False - ''' count = 0 for c in self._notes: diff --git a/music21/volume.py b/music21/volume.py index 9477c934d7..bceb3c61aa 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,15 +34,11 @@ # ------------------------------------------------------------------------------ - - class VolumeException(exceptions21.Music21Exception): pass # ------------------------------------------------------------------------------ - - class Volume(prebase.ProtoM21Object, SlottedObjectMixin): ''' The Volume object lives on NotRest objects and subclasses. It is not a @@ -81,8 +77,8 @@ def __init__( self, *, client: note.NotRest|None = None, - velocity: int|float|None = None, - velocityScalar: int|float|None = None, + velocity: int|None = None, + velocityScalar: float|None = None, velocityIsRelative: bool = True, ): # store a reference to the client, as we use this to do context @@ -90,7 +86,7 @@ def __init__( self.client = client self._velocityScalar: float|None = None if velocity is not None: - self.velocity = velocity + self.velocity = int(velocity) elif velocityScalar is not None: self.velocityScalar = float(velocityScalar) self._cachedRealized = None From 58d2927322a2c21b501eeb9bb4e96156b48a55e1 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 3 Jan 2024 11:53:11 -1000 Subject: [PATCH 4/4] maybe fixed? --- music21/chord/__init__.py | 7 +++---- music21/volume.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index baf2793403..db050b4391 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -443,8 +443,6 @@ def volume(self) -> 'music21.volume.Volume': # do NOT change to volume.Volume, * Changed in v8: setting volume to a list of volumes is no longer supported. See :meth:`~music21.chord.ChordBase.setVolumes` instead - * Improved in v9: if hasComponentVolumes is True, then the velocity object - returned here will be regenerated with each call, and updates live. OMIT_FROM_DOCS @@ -473,6 +471,7 @@ def volume(self) -> 'music21.volume.Volume': # do NOT change to volume.Volume, if velocities: # avoid division by zero error out_volume.velocity = int(round(sum(velocities) / len(velocities))) + self._volume = out_volume return out_volume @@ -582,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/volume.py b/music21/volume.py index bceb3c61aa..02c7315eb0 100644 --- a/music21/volume.py +++ b/music21/volume.py @@ -406,13 +406,17 @@ def velocityScalar(self, value: int|float|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 - self._velocityScalar = float(scalar) + if t.TYPE_CHECKING: + assert value is not None + scalar = float(value) + self._velocityScalar = scalar # ------------------------------------------------------------------------------