npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@audiorective/clock

v2.1.2

Published

Timing and scheduling engine — transport, tempo, look-ahead tick windows, rulers

Readme

@audiorective/clock

Timing and scheduling engine — transport, tempo, look-ahead tick windows, rulers. The temporal pillar of audiorective: core answers "what sounds and how," clock answers "when."

Install

npm install @audiorective/clock

The mental model

Web Audio's render quantum asks "fill the next 128 samples." The clock asks "commit the next ~100 ms of musical events." Same inversion of control: consumers never ask "what is now?" — they answer "what falls in this window?" (the canonical look-ahead pattern: JS timers jitter 10–50 ms, Web Audio scheduling is sample-accurate, so you schedule ahead against AudioContext time and a late JS tick can't move audio that's already committed).

Everything schedulable takes absolute AudioContext time. The clock's headline feature is conversion: you convert a musical position to a time, then schedule.

Quick start — a metronome

import { Clock, Timeline, LinearBarRuler } from "@audiorective/clock";

const timeline = new Timeline({ audioContext: ctx, bpm: 120 }).addRuler("bar", new LinearBarRuler({ numerator: 4, denominator: 4 }));

const clock = new Clock({
  timeline,
  onTick(window) {
    for (const { time, index } of window.rulers.bar.grid(16)) {
      // `time` is already an absolute AudioContext time, which is what
      // core's Sampler takes as `when`
      if (pattern[index % 16]) sampler.trigger({ when: time });
    }
  },
});

clock.start();

grid(division) is the primary scheduling idiom: it yields only points inside the current window, pre-converted to absolute time. Deriving every event from a grid point handed to you exactly once means you never double-schedule — no manual window-bounds bookkeeping needed.

Timeline — the beat↔time mapping

Timeline owns the transport anchor and the tempo curve — the only two pieces of stored position state in the whole package. Beat position is always derived, never accumulated, so there's no drift over a long session.

timeline.beatToTime(64); // absolute AudioContext time for beat 64
timeline.timeToBeat(ctx.currentTime); // current beat position
timeline.bpm.setValueAtTime(140, timeline.beatToTime(64)); // tempo change scheduled at beat 64

timeline.bpm is a standalone, event-list-backed tempo curve (TempoParam) — not an AudioParam. It mirrors the Web Audio scheduling method names (setValueAtTime, cancelScheduledValues, cancelAndHoldAtTime) plus a valueAtTime(t) query the AudioParam-backed SchedulableParam in @audiorective/core can never support. V1 supports steps only; ramp methods throw (linearRampToValueAtTime/exponentialRampToValueAtTime are V2).

Transport

clock.start(); // or clock.start({ atBeat: 16 })
clock.pause(); // freezes position; committed look-ahead audio keeps sounding (an accepted tail)
clock.resume(); // continues from the frozen position
clock.stop(); // resets to beat 0
clock.seek(64); // jumps to beat 64

pause() only pauses a playing clock and resume() only resumes a paused one; each is otherwise a no-op, so start() is the sole entry into playback.

clock.state is a reactive Param<"stopped" | "playing" | "paused">.

Every jump (seek, start({ atBeat }), stop→start) bumps window.generation and begins a new continuity segment — windows are non-overlapping only within a segment, so beats may legitimately reappear across a backward seek. Consumers holding derived state (step counters, generator positions) should reset when generation changes.

Rulers — reading the beat axis

The clock's only native coordinate is the beat axis — a monotonic float driven by the tempo curve. Every other reading (bars, cycles, seconds, polyrhythm) is a ruler: a stateless interpreter registered on the Timeline.

timeline.addRuler("bar", new LinearBarRuler({ numerator: 4, denominator: 4 }));
timeline.rulers.bar.current.value; // reactive point reading, refreshed every tick — for UI/visuals

Four built-ins, crossed along two axes — unit (bar vs. raw time) × topology (linear, counts forever / cycle, wraps):

| | Linear | Cycle | | -------- | -------------------------------------------- | -------------------------------------------------------- | | Bar | LinearBarRuler({ numerator, denominator }) | CycleBarRuler({ numerator, denominator, bars, from? }) | | Time | LinearTimeRuler() | CycleTimeRuler({ seconds, from? }) |

CycleBarRuler is how looping is expressed: the beat axis never jumps — it's read modulo the cycle region, so scheduling across a loop boundary needs no special case. Its window reading exposes spans (the window's cycle-relative sub-ranges, for note content that isn't grid-snapped) alongside grid().

Its grid points fold too. A linear ruler yields { beat, time, index } counting forever; a cycle ruler yields { beat, time, step, cycle } where step is cycle-relative and division means steps per cycle. Pass a pattern's length and step indexes it directly:

for (const { time, step } of window.rulers.pattern.grid(steps.length)) {
  if (steps[step]) sampler.trigger({ when: time });
}

cycle rides on each point rather than the window, since a window can straddle the wrap; cycle * division + step recovers the global count. The fold is floored, not % — JS remainder would return a negative step for beats before the origin.

Write a custom ruler for anything else (polyrhythm, swing) — implement Ruler<TWindow, TPoint>:

interface Ruler<TWindow, TPoint> {
  read(window: CoreTickWindow, timeline: TimelineLike): TWindow; // window-scoped, grid()/spans close over its bounds
  at(beat: number, timeline: TimelineLike): TPoint; // point reading, feeds `current`
}

Miss detection

If the clock fires late and audio time has moved past the previous window's end, those beats are gone — Web Audio cannot schedule into the past. The clock reports the gap via onMiss and continues; it does not try to recover.

new Clock({
  timeline,
  onTick /* ... */,
  onMiss(gap) {
    console.warn(`missed ${gap.gapDuration}s`, gap);
  },
});

Remedy: raise lookAhead.

Tick sources

Clock defaults to WorkerTickSource (a Web-Worker timer, so ticks continue in background tabs). IntervalTickSource and ManualTickSource are also exported — the latter is how this package's own tests drive ticks deterministically without a real AudioContext.

Design

See docs/superpowers/specs/2026-07-04-clock-design.md (kept in git history) for the full design rationale.