Source code for quasimoto.sampler.frequency

"""
A module implementing a simple frequency interface class.
"""

# built-in
import math
from typing import Callable

# third-party
from runtimepy.primitives import Bool, Double, create

# internal
from quasimoto.enums.wave import WaveShape, WaveShapelike
from quasimoto.sampler.notes import DEFAULT_FREQUENCY


[docs] class HasFrequencyMixin: """A simple mixin class for classes that have some frequency component.""" def __init__( self, frequency: float = DEFAULT_FREQUENCY, shape: WaveShapelike = WaveShape.SINE, ) -> None: """Initialize this instance.""" # Can be changed after initialization. self.shape = create(WaveShape.primitive()) self.shape.value = WaveShape.normalize(shape) self.frequency = Double(value=frequency) self.by_shape: dict[int, Callable[[float], float]] = { WaveShape.SINE: self.sin, WaveShape.TRIANGLE: self.triangle, WaveShape.SQUARE: self.square, WaveShape.SAWTOOTH: self.sawtooth, } # Used by proportion pool. self.proportion: float = 0.0 self.enabled = Bool(value=False)
[docs] def next_shape(self) -> None: """Increment to the next shape.""" self.shape.value = 1 + (int(self.shape.value) % 4)
[docs] def harmonic(self, index: int) -> float: """Get a harmonic frequency based on this instance's frequency.""" return float(2**index) * self.frequency.value
[docs] def sin(self, now: float) -> float: """Get a raw sin-wave value sample.""" return math.sin(math.tau * now * self.frequency.value)
[docs] def period(self) -> float: """Obtain the period for this frequency.""" return 1.0 / self.frequency.value
[docs] def quantize_to_period(self, time: float) -> float: """Return time extended to the next start-of-period.""" period = self.period() return time + (period - divmod(time, period)[1])
[docs] def triangle(self, now: float) -> float: """Get a raw triangle-wave value sample.""" period = self.period() return ( (4.0 / period) * abs(((now - period / 4.0) % period) - (period / 2.0)) ) - 1.0
[docs] def square(self, now: float) -> float: """Get a raw square-wave value sample.""" step = self.frequency.value * now return 2 * (2 * math.floor(step) - math.floor(2 * step)) + 1
[docs] def sawtooth(self, now: float) -> float: """Get a raw sawtooth-wave value sample.""" phase = now / self.period() return 2.0 * (phase - math.floor(0.5 + phase))
[docs] def proportion_pool(step: float, *instances: HasFrequencyMixin) -> None: """Handle updating instance proportions.""" count = len(instances) total = 0.0 for inst in instances: if not inst.enabled: curr = inst.proportion if curr > 0.0: inst.proportion = max(0.0, curr - step) total += inst.proportion count -= 1 if count == 0: return # Factor in bandwidth used by currently-disabled sources. equal = (1.0 - total) / count for inst in instances: if inst.enabled: curr = inst.proportion if not math.isclose(curr, equal): delta = min(abs(curr - equal), step) if curr > equal: curr -= delta else: curr += delta inst.proportion = curr total += curr
# Check for correctness. # assert total <= 1.0, total