Skip to main content

Beats & Bars

Waveform Playlist can display a musical timescale with bar and beat markers instead of the default time-based scale. Clips can snap to the beat or bar grid when dragged.

Setup​

Wrap your playlist content with BeatsAndBarsProvider from @waveform-playlist/ui-components:

import { WaveformPlaylistProvider, Waveform } from '@waveform-playlist/browser';
import { BeatsAndBarsProvider } from '@waveform-playlist/ui-components';

<WaveformPlaylistProvider tracks={tracks} timescale>
<BeatsAndBarsProvider bpm={120} timeSignature={[4, 4]} snapTo="beat">
<Waveform showClipHeaders interactiveClips />
</BeatsAndBarsProvider>
</WaveformPlaylistProvider>

When BeatsAndBarsProvider is present, the timescale automatically switches from temporal (minutes:seconds) to bar and beat markers.

Provider Props​

PropTypeDefaultDescription
bpmnumber—Beats per minute
timeSignature[number, number]—Time signature as [beats, noteValue], e.g., [4, 4]
snapTo'bar' | 'beat' | 'off''off'Grid resolution for snap-to-grid

Snap-to-Grid​

Wrap your Waveform with ClipInteractionProvider to enable drag/move/trim with snap-to-grid. Set snap to enable snapping — it auto-detects beats mode from BeatsAndBarsProvider context:

import { WaveformPlaylistProvider, Waveform, ClipInteractionProvider } from '@waveform-playlist/browser';
import { BeatsAndBarsProvider } from '@waveform-playlist/ui-components';

<WaveformPlaylistProvider tracks={tracks} timescale>
<BeatsAndBarsProvider bpm={120} timeSignature={[4, 4]} snapTo="beat">
<ClipInteractionProvider snap>
<Waveform showClipHeaders />
</ClipInteractionProvider>
</BeatsAndBarsProvider>
</WaveformPlaylistProvider>

ClipInteractionProvider handles all the drag sensors, collision detection, snap modifiers, and drag handlers internally. The interactiveClips prop on Waveform is auto-enabled when inside a ClipInteractionProvider.

When snap is enabled, the provider reads from BeatsAndBarsProvider context:

  • If scaleMode="beats" and snapTo is "beat" or "bar" → clips snap to the beat/bar grid
  • Otherwise → clips snap to the timescale grid (derived from zoom level)
PropTypeDefaultDescription
snapbooleanfalseEnable snap-to-grid (auto-detects beats vs timescale from context)
touchOptimizedbooleanfalse250ms delay activation for touch input
Advanced: Manual DragDropProvider Setup

For full control over drag sensors, modifiers, and handlers, you can bypass ClipInteractionProvider and configure DragDropProvider directly with useClipDragHandlers, useDragSensors, ClipCollisionModifier, and SnapToGridModifier. See the LLM API Reference for the complete hook and modifier signatures.

Snap Modes​

  • 'bar' — Clips snap to bar boundaries (e.g., every 4 beats in 4/4 time)
  • 'beat' — Clips snap to individual beat boundaries
  • 'off' — Free positioning, no snapping

The modifier snaps the clip's absolute position to the grid, not the drag delta. This means an off-grid clip will snap to the nearest grid line, not stay at its original offset.

PPQN (Pulses Per Quarter Note)​

All beat/bar math uses 192 PPQN — the same resolution as Tone.js's internal transport. Positions are converted to integer ticks to avoid floating-point errors with non-integer beat durations.

The core math utilities are available from @waveform-playlist/core:

import {
ticksPerBeat, // (ppqn) => ticks per beat
ticksPerBar, // (ppqn, timeSignature) => ticks per bar
samplesToTicks, // (samples, sampleRate, bpm, ppqn) => ticks
ticksToSamples, // (ticks, sampleRate, bpm, ppqn) => samples
snapToGrid, // (ticks, gridTicks) => nearest grid tick
PPQN, // 192
} from '@waveform-playlist/core';

Why integer ticks?​

Millisecond-based modular arithmetic breaks with non-integer beat durations. For example, at 119 BPM a beat is ~504.2 ms — checking counter % 504.2 === 0 fails due to floating-point precision. Integer tick space (192 ticks per beat) eliminates this entirely.

Time Signatures​

Supported time signatures include any [beats, noteValue] combination:

SignatureBeats per barNote valueTicks per bar
4/44Quarter768
3/43Quarter576
6/86Eighth576
2/22Half768
5/45Quarter960
7/87Eighth672

Dynamic BPM and Time Signature​

The provider props are reactive — changing bpm or timeSignature re-renders the timescale and updates the snap grid:

const [bpm, setBpm] = useState(120);
const [timeSig, setTimeSig] = useState<[number, number]>([4, 4]);

<BeatsAndBarsProvider bpm={bpm} timeSignature={timeSig} snapTo="beat">
<input
type="number"
value={bpm}
onChange={(e) => setBpm(Number(e.target.value))}
/>
{/* Waveforms stretch/compress as BPM changes */}
</BeatsAndBarsProvider>

Temporal Mode Fallback​

The SmartScale component (used internally by Waveform) checks for BeatsAndBarsProvider. Without it, the timescale renders in temporal mode (minutes:seconds). You can also let users switch between modes — see the Beats & Bars example for a complete implementation with a mode selector.

See Also​