Lab

time

Musical time from @audio-ui/engine: tempos and signatures, bars and beats, grid divisions and their labels, tempo-synced note values, warp markers, the metronome, and the look-ahead scheduler a transport clock runs on.

pnpm add @audio-ui/engine
import { adjacentDivision, barLabelInterval, DOTTED_FACTOR, divisionAt, divisionSeconds, /* … */ } from "@audio-ui/engine/time";
Used byarrangement, arrangement-engine, dawproject, demo-song, demo-sound, synced-rate, use-arrangement, use-piano-roll-playback, use-timeline-grid, use-transport-clock
ExportKindWhat it is
adjacentDivisionfunctionThe fixed division next to division: a finer one (direction 1, twice as dense) or a coarser one (-1), held at the ends of the list.
barLabelIntervalfunctionSeconds between labelled bars on a ruler at zoom px per second: always a whole number of bars, doubled until labels are at least minSpacing px apart, so they never collide or fall mid-bar.
DOTTED_FACTORconstThree halves: a dotted note is its straight one and half again.
divisionAtfunctionThe fixed division whose step is nearest seconds at tempo: what a grid that follows the zoom becomes when it is fixed where it is.
divisionSecondsfunctionHow long one fixed division lasts at tempo, in seconds.
FixedGridDivisiontypeThe id of a fixed grid division: "4-bars" to "1/32".
GRID_DIVISIONSconstThe fixed grid divisions, coarsest first: a number of bars, or a note value.
GridDivisiontypeadaptive follows the zoom; a fixed division stays put however far you zoom.
GridIntervalOptionsinterfaceHow close adaptive grid lines may be, and whether the grid is in triplets.
gridIntervalfunctionSeconds between grid lines at zoom px per second: the lines a timeline draws and the step it snaps to, so what you see is where you land.
gridLabelfunctionNames an interval the way a grid readout does: "1 bar", "4 bars", "1/16", "1/8T".
METRONOME_RHYTHMSconstHow often a metronome clicks: with the beat, or a note value, straight or triplet.
MetronomeRhythmtypeThe id of a metronome rhythm: "auto" (the beat), a bar, or a note value, straight or triplet.
metronomeRhythmSecondsfunctionSeconds between clicks for a rhythm, or null for auto, which has no interval of its own: it is the beat, whatever the signature makes one.
SYNCED_DIVISIONSconstThe note values a time parameter that follows the tempo can take, shortest first: 1/16 is a sixteenth note, 1/8t a triplet eighth (two thirds of it) and 1/8d a dotted eighth (one and a half times it).
SyncedDivisiontypeA note value as syncedSeconds reads it: "1/8", its triplet "1/8t" or its dotted "1/8d".
syncedSecondsfunctionHow long a synced note value lasts at tempo, in seconds: 1/8 an eighth, 1/8t its triplet, 1/8d its dotted one.
TRIPLET_FACTORconstThree in the space of two: a triplet division is 2/3 of its straight one.
BarinterfaceA bar as a ruler draws it.
barsBeatsWithfunctionWhere seconds falls in bars, beats and sixteenths through the signature changes, each counted from one.
barsOffunctionThe bars from 0 to until seconds, through the signature changes: whole bars of each signature, and a partial bar where a change cuts one short.
isSignaturefunctionWhether a signature is one a bar can have: 1 to 99 beats of a whole, half, quarter, eighth or sixteenth note.
MetronomeClickinterfaceA click of a metronome.
metronomeClicksfunctionThe clicks of a metronome from 0 to until seconds, through the signature changes.
SignatureChangeinterfaceA new signature from at on, in seconds: beatsPerBar beats of a beatUnit note.
advancefunctionAdvances cursor, emitting every step boundary now within SCHEDULE_AHEAD_SECONDS of now.
anchoredCursorfunctionA cursor that emits step at now, the seed for start() and for every re-anchor (seek, region commit).
anchorForTempoChangefunctionThe anchor to carry on with when the tempo changes while playing.
MAX_CATCH_UP_SECONDSconstHow late a boundary may be and still be played, half a second.
positionAtfunctionThe continuous playhead position, in fractional full-grid steps, at now.
SCHEDULE_AHEAD_SECONDSconstHow far ahead of now boundaries are committed, in seconds: the article's 100 ms.
SCHEDULER_POLL_MSconstHow often to poll the scheduler, in milliseconds: the article's 25 ms.
SchedulerAnchorinterfaceThe audio-time / step pair a running clock is anchored to, for deriving the display position.
SchedulerConfiginterfaceWhat the scheduler steps through: the window of steps it loops over and how long a step lasts, read on every call.
SchedulerCursorinterfaceWhere the scheduler is: the next step it will emit and when.
StepBoundaryinterfaceA step to play and the audio time it starts at.
BarsBeatsinterfaceA position in bars, beats and sixteenths, each counted from one.
barsBeatsAtfunctionWhere seconds falls in bars, beats and sixteenths, each counted from one.
bpmToMicrosecondsPerQuarterNotefunctionA tempo in BPM as microseconds per quarter note, the unit a MIDI file stores: 120 BPM is 500000.
DEFAULT_MICROSECONDS_PER_QUARTER_NOTEconstWhat a file with no Set Tempo event plays at: 120 BPM, 500,000 microseconds a quarter note.
DEFAULT_TEMPOconst120 BPM in 4/4.
describeBarsBeatsfunction"Bar 3, beat 2", the way a position is read out to a screen reader.
microsecondsPerQuarterNoteToBpmfunctionThe BPM of a Set Tempo's microseconds per quarter note: 500000 is 120.
secondsPerBarfunctionSeconds per bar: a beat times the beats in a bar.
secondsPerBeatfunctionSeconds per beat, a beat being one beatUnit note: in 6/8, half a quarter.
secondsPerQuarterfunctionSeconds per quarter note: BPM counts quarters whatever the signature says.
TempointerfaceTempo and time signature: how long a quarter note, a beat and a bar last in seconds, and where a position in seconds falls in bars and beats.
addWarpMarkerfunctionAdds a marker at a time on the clip, pinning what plays there; nothing changes in how it plays until it moves.
moveWarpMarkerfunctionMoves the marker at index to a new time on the clip, stretching the recording on either side of it: it stops short of its neighbours.
removeWarpMarkerfunctionTakes the marker at index away, as long as two are left to make a map.
scaleWarpfunctionPlays the whole map factor times as long: 2 is half the speed, an octave down; 0.5 twice the speed.
unwarpedTimefunctionWhat time in the source plays at a time on the clip.
WarpMarkerinterfaceA time in the source, pinned to a time on the clip.
warpedTimefunctionWhere on the clip a time in the source plays.
warpMarkersfunctionThe markers that make a map: finite ones, in order, each later than the one before in both times.
warpRatefunctionHow fast the source plays at a time on the clip: 1 as recorded, 2 twice as fast.

Usage

A Tempo is a BPM and a signature. Every function that turns seconds into music, or music into seconds, reads one:

import { barsBeatsAt, secondsPerBar, secondsPerBeat, syncedSeconds } from "@audio-ui/engine/time";

const tempo = { bpm: 120, beatsPerBar: 6, beatUnit: 8 };

secondsPerBeat(tempo); // 0.25: an eighth note at 120 BPM
secondsPerBar(tempo); // 1.5
barsBeatsAt(2, tempo); // { bar: 2, beat: 3, sixteenth: 1 }
syncedSeconds("1/8d", tempo); // a dotted eighth, for a delay that follows the tempo

BPM counts quarter notes whatever the signature says, so a 6/8 bar at 120 BPM lasts three quarters. describeBarsBeats gives the same position as text for a screen reader, "Bar 2, beat 3".

The grid

gridInterval(division, zoom, tempo) is the step between two grid lines at a zoom in pixels per second, and the step an edit snaps to, so what is drawn is where a drag lands. "adaptive" follows the zoom and keeps lines at least 24 px apart; a fixed division (an id of GRID_DIVISIONS, from "4-bars" to "1/32") stays put. gridLabel names the interval as a readout does, "1 bar" or "1/8T", and barLabelInterval spaces the bar numbers of a ruler.

Signature changes and the metronome

barsOf(tempo, changes, until) lists the bars up to a time through SignatureChanges, with a partial bar where a change cuts one short, and barsBeatsWith(seconds, tempo, changes) reads a position through them. metronomeClicks(tempo, changes, until, rhythm) lists the clicks over the same changes, each with accent on the first of its bar; METRONOME_RHYTHMS holds the rhythms it can click at, "auto" (the beat) by default.

The scheduler

A transport clock that plays sound ahead of time calls advance on a timer: it returns every step boundary within the next 100 ms, each with the audio time to give to a node's start(), and the cursor to call it with next time.

import { advance, anchoredCursor, SCHEDULER_POLL_MS } from "@audio-ui/engine/time";

const config = { regionStart: 0, regionEnd: 16, secondsPerStep: 60 / 120 / 4 };
let cursor = anchoredCursor(ctx.currentTime, 0);

setInterval(() => {
  const next = advance(cursor, ctx.currentTime, config);
  for (const { step, time } of next.boundaries) {
    playStep(step, time);
  }
  cursor = next.cursor;
}, SCHEDULER_POLL_MS);

config is read fresh on every call, so a tempo change takes effect at the next boundary. positionAt(now, anchor, config) is the playhead between boundaries, in fractional steps, and anchorForTempoChange keeps it continuous when the tempo moves while playing. A boundary later than half a second is skipped rather than played late.

The registry's useTransportClock runs this loop for a React page.

Warp markers

A WarpMarker pins a time in a recording to a time on a clip. warpMarkers keeps the markers that make a valid map, warpedTime and unwarpedTime read it both ways, warpRate is how fast the recording plays at a point, and addWarpMarker, moveWarpMarker, removeWarpMarker and scaleWarp edit it.

MIDI tempos

A MIDI file stores a tempo as microseconds per quarter note: bpmToMicrosecondsPerQuarterNote(120) is 500000, and microsecondsPerQuarterNoteToBpm goes back.

Built by Ouest Labs, the source code is available on GitHub.
RSS