Source code for pico_synth_sandbox.timer

# pico_synth_sandbox/timer.py
# 2023 Cooper Dalrymple - me@dcdalrymple.com
# GPL v3 License

from pico_synth_sandbox.tasks import Task
from pico_synth_sandbox import clamp
import time, asyncio

[docs]class Timer(Task): """An abstract class to help handle timing functionality of the :class:`pico_synth_sandbox.arpeggiator.Arpeggiator` and :class:`pico_synth_sandbox.sequencer.Sequencer` classes. Note press and release timing is managed by bpm (beats per minute), steps (divisions of a beat), and gate (note duration during step). :param bpm: The beats per minute of timer. :type bpm: int :param steps: The number of steps to divide a single beat. The minimum value allowed is 0.25, or a whole note. :type steps: float :param gate: The duration of each pressed note per step to play before releasing. This value is a ratio from 0.0 to 1.0. :type gate: float """ STEP_WHOLE = 0.25 #: Whole note beat division STEP_HALF = 0.5 #: Half note beat division STEP_QUARTER = 1.0 #: Quarter note beat division STEP_DOTTED_QUARTER = 1.5 #: Dotted quarter note beat division STEP_EIGHTH = 2.0 #: Eighth note beat division STEP_TRIPLET = 3.0 #: Triplet note beat division STEP_SIXTEENTH = 4.0 #: Sixteenth note beat division STEP_THIRTYSECOND = 8.0 #: Thirtysecond note beat division STEPS = [ #: All of the available beat divisions in a list from longest to shortest STEP_WHOLE, STEP_HALF, STEP_QUARTER, STEP_DOTTED_QUARTER, STEP_EIGHTH, STEP_TRIPLET, STEP_SIXTEENTH, STEP_THIRTYSECOND ] def __init__(self, bpm=120, steps=2.0, gate=0.5): self._enabled = False self._gate = clamp(gate) self._reset(False) self._update_timing( bpm=bpm, steps=max(float(steps), self.STEP_WHOLE) ) self._step = None self._press = None self._release = None self._last_press = [] Task.__init__(self) def _update_timing(self, bpm=None, steps=None): if bpm: self._bpm = bpm if steps: self._steps = steps self._step_time = 60.0 / self._bpm / self._steps self._gate_duration = self._gate * self._step_time def _reset(self, immediate=True): self._now = time.monotonic() if immediate: self._now -= self._step_time
[docs] def set_bpm(self, value): """Set the beats per minute. :param value: The desired beats per minute. :type value: int """ self._update_timing(bpm=value)
[docs] def get_bpm(self): """Get the beats per minute. :return: Beats per minute :rtype: int """ return self._bpm
[docs] def set_steps(self, value): """Set number of steps per beat (or the beat division). The pre-defined `pico_synth_sandbox.Timer.STEP_...` constants can be used here. :param value: The number of steps to divide a single beat. The minimum value allowed is 0.25, or a whole note. :type value: float """ value = max(float(value), self.STEP_WHOLE) self._update_timing(steps=value)
[docs] def get_steps(self): """Get the number of steps per beat (or the beat division). :return: Steps per beat :rtype: float """ return self._steps
[docs] def set_gate(self, value): """Set the note gate within a step of a beat. :param value: The duration of each pressed note per step to play before releasing. This value is a ratio from 0.0 to 1.0. :type value: float """ self._gate = clamp(value) self._update_timing()
[docs] def get_gate(self): """Get the note gate within a step of a beat. This value is a ratio from 0.0 to 1.0. :return: gate :rtype: float """ return self._gate
[docs] def is_enabled(self): """Whether or not the timer object is enabled (running). :return: enabled state :rtype: bool """ return self._enabled
[docs] def set_enabled(self, value:bool): """Directly set whether or not the timer object is enabled (running). :param value: The state of the timer. :type value: bool """ if value and not self._enabled: self.enable() elif not value and self._enabled: self.disable()
[docs] def enable(self): """Enable the timer object to start timing beat steps and triggering note press and release callbacks. The first step will immediately trigger. """ self._enabled = True self._now = time.monotonic() - self._step_time self._enable()
def _enable(self): pass
[docs] def disable(self): """Disable the timer object and immediately release any pressed notes. """ self._enabled = False self._do_release() self._disable()
def _disable(self): pass
[docs] def toggle(self): """Toggle between the enabled and disabled timer states. Any relevant actions may occur during this process (note press and release callbacks). """ if self.is_enabled(): self.disable() else: self.enable()
[docs] def set_step(self, callback): """Set the callback method you would like to be called when a step is triggered. This callback will fire whether or not the step has pressed any notes. However, any pressed notes will occur before this callback is called. :param callback: The callback method without any parameters. Ie: `def step():`. :type callback: function """ self._step = callback
[docs] def set_press(self, callback): """Set the callback method you would like to be called when a timed step note is pressed. :param callback: The callback method. Must have 2 parameters for note value and velocity (0.0-1.0). Ie: `def press(notenum, velocity):`. :type callback: function """ self._press = callback
[docs] def set_release(self, callback): """Set the callback method you would like to be called when a timed step note is released. :param callback: The callback method. Must have 1 parameter for note value. Velocity is always assumed to be 0.0. Ie: `def release(notenum):`. :type callback: function """ self._release = callback
def _is_active(self): return self._enabled
[docs] async def update(self): """Update the timer object and call any relevant callbacks if a new beat step or the end of the gate of a step is reached. The actual functionality of this method will depend on the child class that utilizes the :class:`pico_synth_sandbox.timer.Timer` parent class. """ while True: if not self._is_active(): break self._update() self._do_step() if self._last_press: await self.sleep(self._gate_duration) self._do_release() await self.sleep(self._step_time - self._gate_duration) else: await self.sleep(self._step_time)
async def sleep(self, delay:float): self._now += delay await asyncio.sleep(self._now - time.monotonic()) def _update(self): pass def _do_step(self): if self._step: self._step() def _do_press(self, notenum, velocity): if self._press: self._press(notenum, velocity) self._last_press.append(notenum) def _do_release(self): if self._release and self._last_press: for notenum in self._last_press: self._release(notenum) self._last_press.clear()