Source code for quasimoto.sampler.source

"""
A module implementing a sound-source interface base class.
"""

# built-in
from abc import ABC, abstractmethod
from collections.abc import Iterable, Iterator
from copy import copy
from pathlib import Path
from typing import Any, Callable, TypeVar

# third-party
import matplotlib.pyplot as plt
from runtimepy.primitives import Double

# internal
from quasimoto.sampler.frequency import HasFrequencyMixin
from quasimoto.sampler.parameters import DEFAULT, SourceParameters
from quasimoto.sampler.time import TimeKeeper
from quasimoto.wave.writer import WaveWriter

T = TypeVar("T", bound="SourceInterface")
DEFAULT_AMPLITUDE = 1.0


[docs] class SourceInterface(HasFrequencyMixin, Iterable[int], ABC): """A class implementing a sound-source interface.""" def __init__( self, time_keeper: TimeKeeper, params: SourceParameters = DEFAULT ) -> None: """Initialize this instance.""" self.time_keeper = time_keeper self.params = params super().__init__( frequency=self.params.frequency, shape=self.params.shape ) self.stop_time = self.params.stop_time self.set_duration(self.params.duration) # Amplitude should be applied by the iterator/caller. self.enabled.value = params.enabled self.amplitude = Double(value=self.params.amplitude) def _enable_event(_: float) -> None: """A simple method for enabling this source.""" self.enabled.value = True self.enable_event = _enable_event def _disable_event(_: float) -> None: """A simple method for disabling this source.""" self.enabled.value = False self.disable_event = _disable_event
[docs] def set_duration(self, duration: float = None) -> None: """Set a duration for this source.""" if duration is not None: self.stop_time = self.time_keeper.time + duration
def __iter__(self) -> Iterator[int]: """Return an iterator.""" return self @abstractmethod def __copy__(self: T) -> T: """Create a copy of this instance."""
[docs] def copy(self: T) -> T: """Create a copy of this instance.""" return copy(self)
[docs] def clone(self: T, harmonic: int = None, stop_time: float = None) -> T: """Get a copy of this instance.""" result = self.copy() if stop_time is not None: result.stop_time = stop_time if harmonic is not None: result.frequency.value = result.harmonic(harmonic) return result
[docs] @abstractmethod def value(self, now: float) -> int: """Get the next value."""
[docs] @classmethod def create( cls: type[T], time_keeper: TimeKeeper, data: dict[str, Any] = None ) -> T: """Create a source instance from dictionary data.""" return cls( time_keeper, params=( SourceParameters.from_dict(data) if data is not None else DEFAULT ), )
def __next__(self) -> int: """Get the next value from this sampler.""" # Determine if we should stop. if ( self.stop_time is not None and self.time_keeper.time >= self.stop_time ): raise StopIteration return self.value(self.time_keeper.time)
[docs] def sample_for( self, duration_s: float, handler: Callable[[int], None] ) -> None: """Sample this source for the specified duration.""" # Copy self and our time keeper to create equivalent but independent # results. inst = self.copy() inst.time_keeper = self.time_keeper.copy() for _ in range(inst.time_keeper.num_samples(duration_s)): handler(next(inst)) inst.time_keeper.advance()
[docs] def get_samples(self, duration_s: float) -> list[int]: """Collect samples.""" data: list[int] = [] self.sample_for(duration_s, data.append) return data
[docs] def plot(self, path: Path | str, duration_s: float) -> None: """Create a plot.""" # Plot data. plt.plot(self.get_samples(duration_s)) plt.savefig(str(path), bbox_inches="tight")
[docs] def to_wave(self, path: Path, duration_s: float) -> None: """Write this source to a wave file.""" with WaveWriter.from_path(path) as writer: writer.write([(x, x) for x in self.get_samples(duration_s)])