Source code for subsequence.link_clock
"""
Ableton Link clock adapter for Subsequence.
Wraps ``aalink.Link`` and adapts its asyncio-native API to Subsequence's
24 PPQN pulse model. Requires the optional ``link`` extra::
pip install subsequence[link]
Usage::
link_clock = LinkClock(bpm=120, quantum=4.0, loop=asyncio.get_running_loop())
beat_origin = await link_clock.wait_for_bar()
# ... in the pulse loop:
await link_clock.sync(beat_origin + pulse_count / PPQN)
"""
from __future__ import annotations
import asyncio
import math
import typing
def _require_aalink () -> typing.Any:
"""Import aalink or raise a helpful RuntimeError."""
try:
import aalink # type: ignore
return aalink
except ImportError:
raise RuntimeError(
"Ableton Link support requires the 'aalink' package.\n"
"Install it with: pip install subsequence[link]"
) from None
[docs]
class LinkClock:
"""
Thin wrapper around ``aalink.Link`` for Subsequence's pulse-based clock.
Parameters:
bpm: Initial tempo in BPM (proposed to the Link session).
quantum: Beat cycle length — 4.0 means one bar in 4/4 time.
loop: The running asyncio event loop (required by aalink).
"""
def __init__ (self, bpm: float, quantum: float, loop: asyncio.AbstractEventLoop) -> None:
"""
Join the Link session immediately, proposing *bpm* and setting the bar length to *quantum* beats.
"""
aalink = _require_aalink()
self._link = aalink.Link(bpm, loop)
self._link.enabled = True
self._link.quantum = float(quantum)
# ------------------------------------------------------------------
# Properties that mirror the Link session state
# ------------------------------------------------------------------
@property
[docs]
def beat (self) -> float:
"""Current absolute beat position in the Link session timeline."""
return float(self._link.beat)
@property
[docs]
def tempo (self) -> float:
"""Current session tempo in BPM (authoritative from the Link network)."""
return float(self._link.tempo)
@property
[docs]
def quantum (self) -> float:
"""Beat cycle length (e.g. 4.0 for one bar in 4/4)."""
return float(self._link.quantum)
@property
[docs]
def num_peers (self) -> int:
"""Number of connected Link peers (not counting self)."""
return int(self._link.num_peers)
@property
[docs]
def playing (self) -> bool:
"""Whether the Link session transport is playing."""
return bool(self._link.playing)
# ------------------------------------------------------------------
# Sync / control
# ------------------------------------------------------------------
[docs]
async def sync (self, beat: float) -> float:
"""Wait until the Link session beat reaches *beat*, then return *beat*.
This is the primary timing primitive used by the sequencer loop.
Calling ``await link_clock.sync(beat_origin + pulse / PPQN)`` for each
successive pulse gives accurate, Link-synchronised timing.
"""
return float(await self._link.sync(beat))
[docs]
async def wait_for_bar (self) -> float:
"""Wait for the next quantum boundary (bar start) and return it.
Use this to start the sequencer at a musically clean position that is
phase-aligned with all other Link participants.
Returns the beat value at which playback should begin (``beat_origin``).
"""
current = self._link.beat
# Next quantum boundary strictly after the current beat
# math.floor, not int(): Link beats can be negative before transport
# zero, and int() truncates toward zero - skipping the boundary at 0.0
# and delaying the start by a full extra quantum.
next_boundary = (math.floor(current / self._link.quantum) + 1) * self._link.quantum
result = await self._link.sync(next_boundary)
return float(result)
[docs]
def request_tempo (self, bpm: float) -> None:
"""Propose a new tempo to the Link session.
Other peers may accept or reject the change depending on their own
session rules. Subsequence's sequencer will pick up the network-
authoritative tempo on the next pulse.
"""
self._link.tempo = float(bpm)
[docs]
def disable (self) -> None:
"""Disconnect from the Link session."""
self._link.enabled = False