Source code for clockblocks.utilities

#  ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++  #
#  This file is part of SCAMP (Suite for Computer-Assisted Music in Python)                      #
#  Copyright © 2020 Marc Evanstein <marc@marcevanstein.com>.                                     #
#                                                                                                #
#  This program is free software: you can redistribute it and/or modify it under the terms of    #
#  the GNU General Public License as published by the Free Software Foundation, either version   #
#  3 of the License, or (at your option) any later version.                                      #
#                                                                                                #
#  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;     #
#  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     #
#  See the GNU General Public License for more details.                                          #
#                                                                                                #
#  You should have received a copy of the GNU General Public License along with this program.    #
#  If not, see <http://www.gnu.org/licenses/>.                                                   #
#  ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++  #
"""
Module containing the free functions that make up clockblocks' most convenient interface. Rather than calling
methods on a particular clock, these operate on whichever clock is running the current thread: most importantly
:func:`wait` and :func:`fork`, along with getters and setters for that clock's tempo (:func:`set_tempo`,
:func:`set_tempo_target`, :func:`apply_tempo_envelope`, and their rate/beat-length counterparts).
"""

from __future__ import annotations
import math
import threading
from typing import TYPE_CHECKING, Callable, Sequence, Union
from clockblocks.exceptions import NoActiveClockError
if TYPE_CHECKING:
    from clockblocks import clock, moment
    from clockblocks.moment import ResolvableMoment
    from clockblocks.tempo_envelope import TempoEnvelope


# Default tolerances for near-equality comparisons of beats / times.
# Because of the use of numerical integration, two values representing "the same moment"
# can easily differ by ~1e-12. 1e-9 sits far below any musically meaningful gap, making
# it a safe threshold while still distinguishing genuine (audible) differences.
NEAR_EQUAL_REL_TOL = 1e-9
NEAR_EQUAL_ABS_TOL = 1e-12


[docs] def near_equal(a: float, b: float, *, rel_tol: float = NEAR_EQUAL_REL_TOL, abs_tol: float = NEAR_EQUAL_ABS_TOL) -> bool: """ True if ``a`` and ``b`` are equal to within floating-point reconstruction noise. Thin wrapper over :func:`math.isclose` with project-wide default tolerances plus a small absolute floor: ``math.isclose`` defaults ``abs_tol=0.0``, which is too strict near zero, and several callers compare quantities that can be ~0 (e.g. envelope-segment durations). """ return math.isclose(a, b, rel_tol=rel_tol, abs_tol=abs_tol)
[docs] def meaningfully_less_than(a: float, b: float, *, rel_tol: float = NEAR_EQUAL_REL_TOL, abs_tol: float = NEAR_EQUAL_ABS_TOL) -> bool: """ True if ``a`` is meaningfully less than ``b`` — i.e. ``a < b`` and not merely by rounding noise (see :func:`near_equal`). Use in place of a bare ``<`` when ``a`` and ``b`` are beats/times reconstructed from scheduler time. """ return a < b and not near_equal(a, b, rel_tol=rel_tol, abs_tol=abs_tol)
[docs] def meaningfully_greater_than(a: float, b: float, *, rel_tol: float = NEAR_EQUAL_REL_TOL, abs_tol: float = NEAR_EQUAL_ABS_TOL) -> bool: """ True if ``a`` is meaningfully greater than ``b`` (mirror of :func:`meaningfully_less_than`). """ return a > b and not near_equal(a, b, rel_tol=rel_tol, abs_tol=abs_tol)
# snap_float_to_nice_decimal now lives in expenvelope (the base Envelope needs it for durations rounding); # re-exported here so existing `from clockblocks.utilities import snap_float_to_nice_decimal` importers keep working. from expenvelope import snap_float_to_nice_decimal class _PrintColors: PURPLE = '\033[95m' CYAN = '\033[96m' DARKCYAN = '\033[36m' BLUE = '\033[94m' GREEN = '\033[92m' YELLOW = '\033[93m' RED = '\033[91m' BOLD = '\033[1m' UNDERLINE = '\033[4m' END = '\033[0m'
[docs] def current_clock() -> clock.Clock | None: """ Get the :class:`~clockblocks.clock.Clock` active on the current thread, or None if none is active. """ # The clock is attached to its thread (as __clock__) when the thread is started. return getattr(threading.current_thread(), '__clock__', None)
def _no_active_clock_error(message: str) -> NoActiveClockError: """ Build a NoActiveClockError, adding *why* this thread has no clock when we happen to know. A thread can lose its clock in two ways that are easy to be surprised by: the clock was killed, or ``run_as_server()`` handed ownership to a background thread. Both leave a short note on the thread as they release it, which turns a bare "no active clock" into an explanation of what became of the one that used to be here. """ reason = getattr(threading.current_thread(), '__no_clock_reason__', None) return NoActiveClockError(message if reason is None else f"{message} Note: {reason}.") ################################################################################################################## # Context-inferring wait and fork methods ################################################################################################################## # Module-level forwarders that grab the clock active on the calling thread via current_clock() and # forward to the corresponding Clock method. # ---------------------------------------------------------------------------------------------------------------
[docs] def wait(dt: 'float | ResolvableMoment', units="beats") -> None: """ Block the clock currently active on this thread for ``dt`` beats (or seconds, if ``units="time"``), yielding to the scheduler. Forwards to :meth:`~clockblocks.clock.Clock.wait`. ``dt`` may also be a :class:`~clockblocks.moment.Moment` or :class:`~clockblocks.metric_phase.MetricPhaseTarget`, in which case it is resolved directly and ``units`` is ignored — e.g. ``wait(Moment.at_beat(8))``. On a thread with no active clock, raises NoActiveClockError. :param dt: how long to wait — a number (in beats, or seconds if ``units="time"``), or a Moment / MetricPhaseTarget to wait until. :param units: either ``"beats"`` or ``"time"`` (ignored when ``dt`` is a Moment). """ c = current_clock() if c is not None: c.wait(dt, units=units) else: raise _no_active_clock_error("wait() called on a thread with no active clock.")
[docs] def wait_until(when: 'float | ResolvableMoment', units="beats") -> None: """ Block the clock currently active on this thread until the *absolute* beat (or time, if ``units="time"``) given by ``when``. Forwards to :meth:`~clockblocks.clock.Clock.wait_until`. ``when`` may also be a :class:`~clockblocks.moment.Moment` or :class:`~clockblocks.metric_phase.MetricPhaseTarget`, in which case the units are ignored and behavior is identical to wait(). If ``when`` is already in the past, this returns essentially immediately. On a thread with no active clock, raises NoActiveClockError. :param when: the absolute beat (or time, if ``units="time"``) to wait until, or a Moment / MetricPhaseTarget to resolve. :param units: either ``"beats"`` or ``"time"`` (ignored when ``when`` is a Moment). """ c = current_clock() if c is not None: c.wait_until(when, units=units) else: raise _no_active_clock_error("wait_until() called on a thread with no active clock.")
[docs] def wait_forever() -> None: """ Block forever on the currently active clock (see :meth:`~clockblocks.clock.Clock.wait_forever`) — usually to keep the main script alive while child clocks do the work. Unblocks only if the clock is killed, raising :class:`~clockblocks.exceptions.ClockKilledError`. On a thread with no active clock, raises NoActiveClockError. """ c = current_clock() if c is not None: c.wait_forever() else: raise _no_active_clock_error("wait_forever() called on a thread with no active clock.")
[docs] def wait_for_children_to_finish() -> None: """ Block on the currently active clock until its child clocks have finished (see :meth:`~clockblocks.clock.Clock.wait_for_children_to_finish`). On a thread with no active clock, raises NoActiveClockError. """ c = current_clock() if c is None: raise _no_active_clock_error("wait_for_children_to_finish() called on a thread with no active clock.") c.wait_for_children_to_finish()
[docs] def wait_for_clock_to_finish(clock: 'clock.Clock') -> None: """ Block on the currently active clock until `clock` has finished (see :meth:`~clockblocks.clock.Clock.wait_for_clock_to_finish`). On a thread with no active clock, raises NoActiveClockError. """ c = current_clock() if c is None: raise _no_active_clock_error("wait_for_clock_to_finish() called on a thread with no active clock.") c.wait_for_clock_to_finish(clock)
[docs] def terminate_forked_children() -> None: """ Kill the child clocks of the currently active clock, returning once they have finished unwinding (see :meth:`~clockblocks.clock.Clock.terminate_forked_children`). The deliberate counterpart to :func:`wait_for_children_to_finish`. On a thread with no active clock, raises NoActiveClockError. """ c = current_clock() if c is None: raise _no_active_clock_error("terminate_forked_children() called on a thread with no active clock.") c.terminate_forked_children()
[docs] def fork(forked_function: Callable, args: Sequence = (), kwargs: dict = None, name: str = None, initial_rate: float = None, initial_tempo: float = None, initial_beat_length: float = None, when: Union[float, 'moment.ResolvableMoment'] = None, done_callback: Callable = None) -> 'clock.Clock': """ Run ``forked_function`` on a new child clock of the currently active clock, so it proceeds in parallel while staying coordinated under the same musical time. See :meth:`~clockblocks.clock.Clock.fork`; raises NoActiveClockError if there is no active clock. :param forked_function: the function to run on the child clock. Note that, unlike the original clockblocks, the child clock is *not* injected as an argument — call :func:`current_clock` from inside the function if you need a reference to it. :param args: positional arguments passed to ``forked_function``. :param kwargs: keyword arguments passed to ``forked_function``. :param name: optional name for the child clock, useful for debugging. :param initial_rate: the child's starting rate in beats per second. Give at most one of ``initial_rate``/``initial_tempo``/``initial_beat_length``; if none is given the child starts at rate 1. :param initial_tempo: the child's starting tempo in beats per minute. :param initial_beat_length: the child's starting beat length in seconds per beat. :param when: when to start the fork. ``None`` (the default) starts it immediately; otherwise pass a :class:`~clockblocks.moment.Moment` (e.g. ``Moment.after_beats(4)`` or ``Moment.at_time(10)``) or a :class:`~clockblocks.metric_phase.MetricPhaseTarget`. Unlike :func:`wait`, a bare number is rejected here, since we don't know from context whether it's beats/time, relative/absolute. :param done_callback: an optional function called when the forked function finishes. :return: the newly created child :class:`~clockblocks.clock.Clock`. """ c = current_clock() if c is None: raise _no_active_clock_error("Cannot fork: there is no active clock on this thread.") return c.fork(forked_function, args=args, kwargs=kwargs, name=name, initial_rate=initial_rate, initial_tempo=initial_tempo, initial_beat_length=initial_beat_length, when=when, done_callback=done_callback)
################################################################################################################## # Context-inferring tempo methods ################################################################################################################## # Module-level forwarders that grab the clock active on the calling thread via current_clock() and # forward to the corresponding Clock tempo modifier. Note that these act on the current clock and *not* # on the master clock. set_tempo(120) inside a forked clock changes *that fork's* tempo; to change the # master's tempo, call the method on the master Clock object directly. # --------------------------------------------------------------------------------------------------------------- def _current_clock_or_raise(caller: str) -> 'clock.Clock': """ Returns the clock active on this thread, or raises NoActiveClockError naming `caller`. (Unlike wait(), there is no unsynchronized fallback: an unsynchronized thread has no tempo to change.) """ c = current_clock() if c is None: raise _no_active_clock_error(f"{caller}() called on a thread with no active clock.") return c
[docs] def set_tempo(tempo: float) -> None: """Immediately set the tempo of the current clock (see :attr:`~clockblocks.clock.Clock.tempo`). Raises NoActiveClockError if there is no active clock. :param tempo: the new tempo, in beats per minute.""" _current_clock_or_raise("set_tempo").tempo = tempo
[docs] def set_rate(rate: float) -> None: """Immediately set the rate of the current clock (see :attr:`~clockblocks.clock.Clock.rate`). Raises NoActiveClockError if there is no active clock. :param rate: the new rate, in beats per second.""" _current_clock_or_raise("set_rate").rate = rate
[docs] def set_beat_length(beat_length: float) -> None: """Immediately set the beat length of the current clock (see :attr:`~clockblocks.clock.Clock.beat_length`). Raises NoActiveClockError if there is no active clock. :param beat_length: the new beat length, in seconds per beat.""" _current_clock_or_raise("set_beat_length").beat_length = beat_length
[docs] def get_tempo() -> float: """Return the current tempo (in beats per minute) of the currently active clock (see :attr:`~clockblocks.clock.Clock.tempo`). Raises NoActiveClockError if there is no active clock. :return: the current tempo, in beats per minute.""" return _current_clock_or_raise("get_tempo").tempo
[docs] def get_rate() -> float: """Return the current rate (in beats per second) of the currently active clock (see :attr:`~clockblocks.clock.Clock.rate`). Raises NoActiveClockError if there is no active clock. :return: the current rate, in beats per second.""" return _current_clock_or_raise("get_rate").rate
[docs] def get_beat_length() -> float: """Return the current beat length (in seconds per beat) of the currently active clock (see :attr:`~clockblocks.clock.Clock.beat_length`). Raises NoActiveClockError if there is no active clock. :return: the current beat length, in seconds per beat.""" return _current_clock_or_raise("get_beat_length").beat_length
################################################################################################################## # Context-inferring position readers ################################################################################################################## # Module-level readers of the current clock's position, for user convenience (current_clock().beat is a bit wordy), # and consistency with other context-inferring readers. Note that there are deliberately no # projected_beat()/projected_time() counterparts, since these are intended for callers *outside* the clock family. # ---------------------------------------------------------------------------------------------------------------
[docs] def get_beat() -> float: """Return how many beats have passed on the currently active clock (see :attr:`~clockblocks.clock.Clock.beat`). As with the tempo helpers, this reads *this* clock and not the master; use ``current_clock().master.beat`` for that. Raises NoActiveClockError if there is no active clock. :return: the current beat of the active clock.""" # float() sheds the _CallableFloat that Clock.beat returns, so this new function doesn't inherit the # deprecated beat() call spelling. Drop the cast when that shim goes in 2.0. return float(_current_clock_or_raise("get_beat").beat)
[docs] def get_time() -> float: """Return how much time has passed on the currently active clock (see :attr:`~clockblocks.clock.Clock.time`) — in seconds if it is the master clock, or parent clock beats if it is a forked child clock. As with the tempo helpers, this reads *this* clock and not the master; use ``current_clock().master.time`` for that. Raises NoActiveClockError if there is no active clock. :return: the current time of the active clock.""" # float() as in get_beat: sheds the 1.x-only _CallableFloat shim. return float(_current_clock_or_raise("get_time").time)
[docs] def set_tempo_target(tempo_target: float, when: 'ResolvableMoment', curve_shape: float = None, truncate: bool = True, align_to: 'ResolvableMoment' = None) -> None: """Smoothly change the current clock's tempo to ``tempo_target`` (in beats per minute), arriving at ``when``. Forwards to :meth:`~clockblocks.clock.Clock.set_tempo_target` on the currently active clock. Raises NoActiveClockError if there is no active clock. :param tempo_target: the tempo to arrive at, in beats per minute. :param when: when the target is reached, as a :class:`~clockblocks.moment.Moment` (e.g. ``Moment.after_beats(4)``, ``Moment.at_time(10)``) or a :class:`~clockblocks.metric_phase.MetricPhaseTarget`. :param curve_shape: the bend of the transition: ``0`` is linear, ``> 0`` changes late, ``< 0`` early. :param truncate: if ``True``, discard any tempo curve already scheduled past the current beat first. :param align_to: optional; solve the curvature to land the free axis (the one ``when`` did not pin) on a phase or coordinate — e.g. accelerate over a fixed time, landing on a downbeat. See :meth:`~clockblocks.clock.Clock.set_tempo_target`.""" _current_clock_or_raise("set_tempo_target").set_tempo_target( tempo_target, when, curve_shape=curve_shape, truncate=truncate, align_to=align_to)
[docs] def set_rate_target(rate_target: float, when: 'ResolvableMoment', curve_shape: float = None, truncate: bool = True, align_to: 'ResolvableMoment' = None) -> None: """Smoothly change the current clock's rate to ``rate_target`` (in beats per second), arriving at ``when``. Forwards to :meth:`~clockblocks.clock.Clock.set_rate_target` on the currently active clock. Raises NoActiveClockError if there is no active clock. :param rate_target: the rate to arrive at, in beats per second. :param when: when the target is reached, as a :class:`~clockblocks.moment.Moment` or :class:`~clockblocks.metric_phase.MetricPhaseTarget`. :param curve_shape: the bend of the transition: ``0`` is linear, ``> 0`` changes late, ``< 0`` early. :param truncate: if ``True``, discard any tempo curve already scheduled past the current beat first. :param align_to: optional; solve the curvature to land the free axis on a phase or coordinate. See :meth:`~clockblocks.clock.Clock.set_rate_target`.""" _current_clock_or_raise("set_rate_target").set_rate_target( rate_target, when, curve_shape=curve_shape, truncate=truncate, align_to=align_to)
[docs] def set_beat_length_target(beat_length_target: float, when: 'ResolvableMoment', curve_shape: float = None, truncate: bool = True, align_to: 'ResolvableMoment' = None) -> None: """Smoothly change the current clock's beat length to ``beat_length_target`` (in seconds per beat), arriving at ``when``. Forwards to :meth:`~clockblocks.clock.Clock.set_beat_length_target` on the currently active clock. Raises NoActiveClockError if there is no active clock. :param beat_length_target: the beat length to arrive at, in seconds per beat. :param when: when the target is reached, as a :class:`~clockblocks.moment.Moment` or :class:`~clockblocks.metric_phase.MetricPhaseTarget`. :param curve_shape: the bend of the transition: ``0`` is linear, ``> 0`` changes late, ``< 0`` early. :param truncate: if ``True``, discard any tempo curve already scheduled past the current beat first. :param align_to: optional; solve the curvature to land the free axis on a phase or coordinate. See :meth:`~clockblocks.clock.Clock.set_beat_length_target`.""" _current_clock_or_raise("set_beat_length_target").set_beat_length_target( beat_length_target, when, curve_shape=curve_shape, truncate=truncate, align_to=align_to)
[docs] def set_tempo_targets(tempo_targets: Sequence[float], whens: 'Sequence[ResolvableMoment]', curve_shapes: Sequence[float] = None, truncate: bool = True, align_to: 'ResolvableMoment | Sequence[ResolvableMoment | None]' = None) -> None: """Set several tempo targets at once, building a multi-segment tempo curve on the current clock. Forwards to :meth:`~clockblocks.clock.Clock.set_tempo_targets`; acts on the currently active clock. Raises NoActiveClockError if there is no active clock. :param tempo_targets: the tempo to arrive at for each segment, in beats per minute. :param whens: the arrival moment for each target (same length as ``tempo_targets``). Each is resolved against the clock's *current* position (so an ``after_*`` moment counts from now, not from the previous segment's end), beats and time may be mixed, and the moments must come out strictly increasing in clock-time. :param curve_shapes: optional per-segment curve shapes (same length), or ``None`` for all-linear. :param truncate: if ``True``, discard any tempo curve already scheduled past the current beat first. :param align_to: optional; bend whole runs of segments to land collectively on a phase/coordinate. Pass a single :class:`~clockblocks.moment.Moment`/:class:`~clockblocks.metric_phase.MetricPhaseTarget` to align the whole call at its end, or a per-segment list of ``None``/targets where each non-``None`` entry closes and aligns the run since the previous alignment. See :meth:`~clockblocks.clock.Clock.set_beat_length_targets`. This is one-shot. To loop a tempo shape, build a :class:`~clockblocks.tempo_envelope.TempoEnvelope` and pass it to :func:`apply_tempo_envelope` with ``loop=True``.""" _current_clock_or_raise("set_tempo_targets").set_tempo_targets( tempo_targets, whens, curve_shapes=curve_shapes, truncate=truncate, align_to=align_to)
[docs] def set_rate_targets(rate_targets: Sequence[float], whens: 'Sequence[ResolvableMoment]', curve_shapes: Sequence[float] = None, truncate: bool = True, align_to: 'ResolvableMoment | Sequence[ResolvableMoment | None]' = None) -> None: """Set several rate targets at once, building a multi-segment tempo curve on the current clock. Forwards to :meth:`~clockblocks.clock.Clock.set_rate_targets`; acts on the currently active clock. Raises NoActiveClockError if there is no active clock. :param rate_targets: the rate to arrive at for each segment, in beats per second. :param whens: the arrival moment for each target (same length as ``rate_targets``). :param curve_shapes: optional per-segment curve shapes (same length), or ``None`` for all-linear. :param truncate: if ``True``, discard any tempo curve already scheduled past the current beat first. :param align_to: optional; align whole runs of segments to a phase/coordinate. See :meth:`~clockblocks.clock.Clock.set_beat_length_targets`.""" _current_clock_or_raise("set_rate_targets").set_rate_targets( rate_targets, whens, curve_shapes=curve_shapes, truncate=truncate, align_to=align_to)
[docs] def set_beat_length_targets(beat_length_targets: Sequence[float], whens: 'Sequence[ResolvableMoment]', curve_shapes: Sequence[float] = None, truncate: bool = True, align_to: 'ResolvableMoment | Sequence[ResolvableMoment | None]' = None) -> None: """Set several beat-length targets at once, building a multi-segment tempo curve on the current clock. Forwards to :meth:`~clockblocks.clock.Clock.set_beat_length_targets`; acts on the currently active clock. Raises NoActiveClockError if there is no active clock. :param beat_length_targets: the beat length to arrive at for each segment, in seconds per beat. :param whens: the arrival moment for each target (same length as ``beat_length_targets``). :param curve_shapes: optional per-segment curve shapes (same length), or ``None`` for all-linear. :param truncate: if ``True``, discard any tempo curve already scheduled past the current beat first. :param align_to: optional; align whole runs of segments to a phase/coordinate. See :meth:`~clockblocks.clock.Clock.set_beat_length_targets`.""" _current_clock_or_raise("set_beat_length_targets").set_beat_length_targets( beat_length_targets, whens, curve_shapes=curve_shapes, truncate=truncate, align_to=align_to)
[docs] def apply_tempo_function(function: Callable, domain_start: float = 0, domain_end: float = None, duration_units: str = "beats", truncate: bool = True, loop: bool = False, extension_increment: float = 2.0, **kwargs) -> None: """Drive the current clock's tempo (in beats per minute) by following a function of beats/time. Forwards to :meth:`~clockblocks.clock.Clock.apply_tempo_function`; acts on the currently active clock. Raises NoActiveClockError if there is no active clock. :param function: a callable mapping a position (in the units given by ``duration_units``) to a tempo. :param domain_start: the input value at which to start reading ``function``. :param domain_end: the input value at which to stop, or ``None`` to follow the function open-endedly. :param duration_units: ``"beats"`` or ``"time"`` — whether the function's input is measured in beats or in seconds. :param truncate: if ``True``, discard any tempo curve already scheduled past the current beat first. :param loop: only relevant when ``domain_end`` is set: if ``True``, repeat that finite domain indefinitely (until :func:`stop_tempo_loop_or_function`). :param extension_increment: only relevant when ``domain_end`` is ``None``: how far ahead (in the function's domain) to project at a time, extending again as the clock reaches the projected end. :param kwargs: forwarded to the underlying envelope-from-function sampling (``scanning_step_size``, ``iterations``, etc. — see :meth:`~clockblocks.tempo_envelope.TempoHistory.apply_function`).""" _current_clock_or_raise("apply_tempo_function").apply_tempo_function( function, domain_start=domain_start, domain_end=domain_end, duration_units=duration_units, truncate=truncate, loop=loop, extension_increment=extension_increment, **kwargs)
[docs] def apply_rate_function(function: Callable, domain_start: float = 0, domain_end: float = None, duration_units: str = "beats", truncate: bool = True, loop: bool = False, extension_increment: float = 2.0, **kwargs) -> None: """Drive the current clock's rate (in beats per second) by following a function of beats/time. Forwards to :meth:`~clockblocks.clock.Clock.apply_rate_function`; acts on the currently active clock. Raises NoActiveClockError if there is no active clock. See :func:`apply_tempo_function` for the meaning of every argument (the only difference is that ``function`` returns a rate in beats per second).""" _current_clock_or_raise("apply_rate_function").apply_rate_function( function, domain_start=domain_start, domain_end=domain_end, duration_units=duration_units, truncate=truncate, loop=loop, extension_increment=extension_increment, **kwargs)
[docs] def apply_beat_length_function(function: Callable, domain_start: float = 0, domain_end: float = None, duration_units: str = "beats", truncate: bool = True, loop: bool = False, extension_increment: float = 2.0, **kwargs) -> None: """Drive the current clock's beat length (in seconds per beat) by following a function of beats/time. Forwards to :meth:`~clockblocks.clock.Clock.apply_beat_length_function`; acts on the currently active clock. Raises NoActiveClockError if there is no active clock. See :func:`apply_tempo_function` for the meaning of every argument (the only difference is that ``function`` returns a beat length in seconds per beat).""" _current_clock_or_raise("apply_beat_length_function").apply_beat_length_function( function, domain_start=domain_start, domain_end=domain_end, duration_units=duration_units, truncate=truncate, loop=loop, extension_increment=extension_increment, **kwargs)
[docs] def apply_tempo_envelope(envelope: 'TempoEnvelope', truncate: bool = True, loop: bool = False) -> None: """Append a ready-made :class:`~clockblocks.tempo_envelope.TempoEnvelope` onto the current clock's tempo curve. This is the way to *loop* a tempo shape (a :class:`~clockblocks.tempo_envelope.TempoEnvelope` is defined over beats, so it loops unambiguously). Forwards to :meth:`~clockblocks.clock.Clock.apply_tempo_envelope`; acts on the currently active clock. Raises NoActiveClockError if there is no active clock. :param envelope: the tempo envelope to apply (under the hood a beat-length-over-beats curve). :param truncate: if ``True``, discard any tempo curve already scheduled past the current beat first. :param loop: if ``True``, repeat the envelope indefinitely until stopped with :func:`stop_tempo_loop_or_function`.""" _current_clock_or_raise("apply_tempo_envelope").apply_tempo_envelope( envelope, truncate=truncate, loop=loop)
[docs] def stop_tempo_loop_or_function() -> None: """Stop following any function or looping envelope previously started on the current clock's tempo (via :func:`apply_tempo_function`/:func:`apply_rate_function`/:func:`apply_beat_length_function` or a looping :func:`apply_tempo_envelope`). The tempo holds at its current value. Forwards to :meth:`~clockblocks.clock.Clock.stop_tempo_loop_or_function`; acts on the currently active clock. Raises NoActiveClockError if there is no active clock.""" _current_clock_or_raise("stop_tempo_loop_or_function").stop_tempo_loop_or_function()