Skip to main content

Hooks

React hooks for accessing playlist state and controls.

Import​

import {
// Core context hooks
usePlaylistState,
usePlaylistControls,
usePlaylistData,
usePlaybackAnimation,

// Specialized hooks
useTimeFormat,

// Engine-delegation hooks (internal, used by provider)
useSelectionState,
useLoopState,
useSelectedTrack,
useZoomControls,
useMasterVolume,

// Drag & drop
useClipDragHandlers,
useDragSensors,

// Clip editing
useClipSplitting,

// Keyboard shortcuts
useKeyboardShortcuts,
usePlaybackShortcuts,
} from '@waveform-playlist/browser';

// Tone.js batteries-included hooks (import from /tone to keep core bundle engine-free)
import {
useAudioTracks,
useDynamicTracks,
useDynamicEffects,
useTrackDynamicEffects,
useExportWav,
useOutputMeter,
} from '@waveform-playlist/browser/tone';

// Recording primitives (lower-level hooks)
import {
useMicrophoneAccess,
useRecording,
useMicrophoneLevel,
} from '@waveform-playlist/recording';

// Annotation hooks
import {
useAnnotationControls,
} from '@waveform-playlist/annotations';

Core Context Hooks​

These hooks access the playlist context provided by WaveformPlaylistProvider.

usePlaylistData​

Access static playlist configuration, refs, and loading state.

function usePlaylistData(): {
// Audio data
sampleRate: number;
duration: number;
audioBuffers: AudioBuffer[];
tracks: ClipTrack[];
trackStates: TrackState[];
peaksDataArray: TrackClipPeaks[];

// Display settings
samplesPerPixel: number;
waveHeight: number;
timeScaleHeight: number;
minimumPlaylistHeight: number;
mono: boolean;
masterVolume: number;
canZoomIn: boolean;
canZoomOut: boolean;
controls: { show: boolean; width: number };
timeFormat: TimeFormat;
barWidth: number;
barGap: number;
roundedBars: boolean;
progressBarWidth: number;

// Refs for direct access
playoutRef: RefObject<PlaylistEngine | null>; // from @waveform-playlist/engine
isDraggingRef: MutableRefObject<boolean>; // true during boundary trim drags

// Loading state
isReady: boolean; // True when all tracks are loaded

// Callback
onTracksChange: ((tracks: ClipTrack[]) => void) | undefined;
};

usePlaybackAnimation​

Access playback state, timing refs, and the per-frame animation registry. A single requestAnimationFrame loop inside the provider drives all visual updates; components register callbacks instead of spinning their own loops.

interface FrameData {
/** Raw engine time (use for state/logic — NOT for visual positioning). */
readonly time: number;
/** engine.getAudibleTime() while playing, raw time when resting. Use for any DOM
* positioning that should match the audible output. */
readonly visualTime: number;
readonly sampleRate: number;
readonly samplesPerPixel: number;
}

function usePlaybackAnimation(): {
isPlaying: boolean;
currentTime: number;

// Refs for 60fps animation loops (read inside frame callbacks or rAF)
currentTimeRef: RefObject<number>;
/** Visually-aligned playback time: engine.getAudibleTime() (raw − outputLatency −
* lookAhead) while playing, raw time when resting. Read this for any static
* playhead positioning that should match the audible output. */
visualTimeRef: RefObject<number>;
playbackStartTimeRef: RefObject<number>;
audioStartPositionRef: RefObject<number>;

/** Raw playback time from engine (auto-wraps at loop boundaries). */
getPlaybackTime: () => number;
/** Current adapter scheduler lookahead (Tone ~0.1s, native 0). Use for any
* audible-latency calculation that must match the playhead. */
getLookAhead: () => number;

/** AudioContext.currentTime from the playout adapter (raw hardware clock). */
getAudioContextTime: () => number;
/** AudioContext.outputLatency from the playout adapter. */
getOutputLatency: () => number;
/** Register a per-frame callback driven by the shared animation loop. */
registerFrameCallback: (id: string, cb: (data: FrameData) => void) => void;
unregisterFrameCallback: (id: string) => void;
};

Example​

Use registerFrameCallback rather than your own requestAnimationFrame loop — it shares the provider's single rAF, runs only while playing, and hands you a fresh FrameData so closure values can't go stale during zoom changes.

function AnimatedPlayhead() {
const { registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
registerFrameCallback('playhead', ({ visualTime, sampleRate, samplesPerPixel }) => {
if (!ref.current) return;
const pixels = (visualTime * sampleRate) / samplesPerPixel;
ref.current.style.transform = `translateX(${pixels}px)`;
});
return () => unregisterFrameCallback('playhead');
}, [registerFrameCallback, unregisterFrameCallback]);

return <div ref={ref} className="playhead" />;
}

For static positioning when playback is stopped (initial render, after pause/seek), read visualTimeRef.current instead — the loop keeps it current outside of playback too.


useAudioTracks​

Load and decode audio files into track objects.

Signature​

function useAudioTracks(configs: AudioTrackConfig[]): {
tracks: ClipTrack[];
loading: boolean;
error: string | null;
loadedCount: number;
totalCount: number;
};

Parameters​

NameTypeDescription
configsAudioTrackConfig[]Array of audio configurations

AudioTrackConfig​

interface AudioTrackConfig {
src?: string; // URL to audio file
audioBuffer?: AudioBuffer; // Pre-loaded AudioBuffer (skips fetch/decode)
name?: string; // Display name
startTime?: number; // Start position in seconds
duration?: number; // Clip duration in seconds
offset?: number; // Offset into source audio in seconds
waveformData?: WaveformDataObject; // Pre-computed BBC audiowaveform data
volume?: number; // Initial volume 0-1
muted?: boolean; // Start muted
soloed?: boolean; // Start soloed
pan?: number; // Pan position -1 to 1
color?: string; // Waveform color
fadeIn?: Fade; // Fade in configuration
fadeOut?: Fade; // Fade out configuration
}

Returns​

PropertyTypeDescription
tracksClipTrack[]Loaded track objects
loadingbooleanLoading state
errorstring | nullError message
loadedCountnumberNumber of configs that have finished loading
totalCountnumberTotal number of configs

Example​

const { tracks, loading, error, loadedCount, totalCount } = useAudioTracks([
{ src: '/audio/track1.mp3', name: 'Track 1' },
{ src: '/audio/track2.mp3', name: 'Track 2', startTime: 5 },
]);

if (loading) return <div>Loading {loadedCount} / {totalCount}…</div>;
if (error) return <div>Error: {error}</div>;

useDynamicTracks​

Imperative hook for adding tracks at runtime (drag-and-drop, file picker). Complements useAudioTracks which is declarative (configs-driven).

Placeholder tracks with clips: [] appear instantly while audio decodes in parallel. Each placeholder is atomically replaced with the loaded track on success, or removed on error.

Signature​

function useDynamicTracks(): UseDynamicTracksReturn;

TrackSource​

type TrackSource =
| File // Drag-and-drop / file input
| Blob // Raw audio blob
| string // URL shorthand
| { src: string; name?: string }; // URL with optional name

Returns​

PropertyTypeDescription
tracksClipTrack[]Current tracks (placeholders + loaded)
addTracks(sources: TrackSource[]) => voidAdd files or URLs at runtime
removeTrack(trackId: string) => voidRemove a track by id. Aborts in-flight fetch if still loading.
loadingCountnumberNumber of sources currently decoding
isLoadingbooleantrue when any source is still decoding
errorsTrackLoadError[]Tracks that failed to load (removed from tracks automatically)

TrackLoadError​

interface TrackLoadError {
name: string; // Display name of the source that failed
error: Error; // The underlying error
}

Example​

function DragDropPlaylist() {
const { tracks, addTracks, removeTrack, loadingCount, isLoading, errors } = useDynamicTracks();

const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('audio/'));
if (files.length > 0) addTracks(files);
};

return (
<div>
<div onDrop={handleDrop} onDragOver={(e) => e.preventDefault()}>
{isLoading
? `Decoding ${loadingCount} file(s)...`
: 'Drop audio files here'}
</div>

{tracks.length > 0 && (
<WaveformPlaylistProvider tracks={tracks}>
<Waveform onRemoveTrack={(index) => removeTrack(tracks[index].id)} />
</WaveformPlaylistProvider>
)}
</div>
);
}

usePlaylistState​

Access the current playlist state.

Signature​

function usePlaylistState(): PlaylistState;

Returns​

interface PlaylistStateContextValue {
continuousPlay: boolean;
linkEndpoints: boolean;
annotationsEditable: boolean;
isAutomaticScroll: boolean;
isLoopEnabled: boolean;
annotations: AnnotationData[];
activeAnnotationId: string | null;
selectionStart: number;
selectionEnd: number;
selectedTrackId: string | null;
loopStart: number;
loopEnd: number;
/** Whether playback rolls past the end instead of auto-stopping (implies fillViewport) */
indefinitePlayback: boolean;
/** Whether the timeline visually fills the scroll container (layout only) */
fillViewport: boolean;
/** Whether undo is available */
canUndo: boolean;
/** Whether redo is available */
canRedo: boolean;
}

Example​

function StatusBar() {
const { continuousPlay, selectedTrackId } = usePlaylistState();
const { isPlaying } = usePlaybackAnimation();
const { duration } = usePlaylistData();

return (
<div>
<span>{isPlaying ? 'Playing' : 'Stopped'}</span>
<span>Duration: {duration.toFixed(2)}s</span>
<span>Continuous: {continuousPlay ? 'On' : 'Off'}</span>
</div>
);
}

usePlaylistControls​

Access playlist control functions.

Signature​

function usePlaylistControls(): PlaylistControls;

Returns​

interface PlaylistControlsContextValue {
// Playback
play: (startTime?: number, playDuration?: number) => Promise<void>;
pause: () => void;
stop: () => void;
seekTo: (time: number) => void;
setCurrentTime: (time: number) => void;

// Track controls
setTrackMute: (trackIndex: number, muted: boolean) => void;
setTrackSolo: (trackIndex: number, soloed: boolean) => void;
setTrackVolume: (trackIndex: number, volume: number) => void;
setTrackPan: (trackIndex: number, pan: number) => void;

// Selection
setSelection: (start: number, end: number) => void;
setSelectedTrackId: (trackId: string | null) => void;

// Time format
setTimeFormat: (format: TimeFormat) => void;
formatTime: (seconds: number) => string;

// Zoom
zoomIn: () => void;
zoomOut: () => void;

// Master volume
setMasterVolume: (volume: number) => void;

// Scroll
setAutomaticScroll: (enabled: boolean) => void;
setScrollContainer: (element: HTMLDivElement | null) => void;
scrollContainerRef: RefObject<HTMLDivElement | null>;

// Annotation controls
setContinuousPlay: (enabled: boolean) => void;
setLinkEndpoints: (enabled: boolean) => void;
setAnnotationsEditable: (enabled: boolean) => void;
setAnnotations: Dispatch<SetStateAction<AnnotationData[]>>;
setActiveAnnotationId: (id: string | null) => void;

// Loop controls
setLoopEnabled: (enabled: boolean) => void;
setLoopRegion: (start: number, end: number) => void;
setLoopRegionFromSelection: () => void;
clearLoopRegion: () => void;

// Undo/redo
undo: () => void;
redo: () => void;

// Recording
/** Mark a recording session active/inactive. While active: (1) the
* end-of-audio auto-stop is suppressed so overdub playback runs past the
* end of existing material, and (2) with an `armedTrackId`, that track's
* existing content is transiently muted — punch-in replaces whatever the
* take overlaps — and its previous mute state is restored when the
* session ends (audio-only, the UI mute control is untouched). Auto-wired
* from the Waveform `recordingState` prop; overdub flows should also call
* it eagerly (before `play()`). */
setRecordingActive: (active: boolean, armedTrackId?: string | null) => void;
}

Example​

function CustomControls() {
const { play, pause, stop, seekTo } = usePlaylistControls();

return (
<div>
<button onClick={() => play()}>Play</button>
<button onClick={() => pause()}>Pause</button>
<button onClick={() => stop()}>Stop</button>
<button onClick={() => seekTo(0)}>Go to Start</button>
</div>
);
}

useZoomControls​

Zoom state management with engine delegation. Internal hook used by the provider — delegates zoomIn() / zoomOut() to the engine and mirrors state via onEngineState().

Signature​

function useZoomControls(props: {
engineRef: RefObject<PlaylistEngine | null>;
initialSamplesPerPixel: number;
}): {
samplesPerPixel: number;
zoomIn: () => void;
zoomOut: () => void;
canZoomIn: boolean;
canZoomOut: boolean;
onEngineState: (state: EngineState) => void;
};

Note: For zooming in consumer components, use zoomIn() and zoomOut() from usePlaylistControls(), or the ZoomInButton / ZoomOutButton components. The canZoomIn / canZoomOut / samplesPerPixel values are also available from usePlaylistData().

Example​

function ZoomControls() {
const { zoomIn, zoomOut } = usePlaylistControls();
const { canZoomIn, canZoomOut, samplesPerPixel } = usePlaylistData();

return (
<div>
<button onClick={zoomIn} disabled={!canZoomIn}>+</button>
<span>{samplesPerPixel} spp</span>
<button onClick={zoomOut} disabled={!canZoomOut}>-</button>
</div>
);
}

useMasterVolume​

Master volume state with engine delegation. Internal hook used by the provider.

Signature​

function useMasterVolume(props: {
engineRef: RefObject<PlaylistEngine | null>;
initialVolume?: number; // Default: 1.0
}): {
masterVolume: number;
setMasterVolume: (volume: number) => void;
masterVolumeRef: RefObject<number>;
onEngineState: (state: EngineState) => void;
};

Note: For consumer components, use setMasterVolume() from usePlaylistControls() and masterVolume from usePlaylistData(), or the MasterVolumeControl component.


Engine-Delegation Hooks​

These hooks follow the onEngineState pattern: they delegate mutations to PlaylistEngine methods and mirror engine state into React via an onEngineState() callback. They are used internally by WaveformPlaylistProvider but can be useful for advanced custom providers.

:::tip Using the engine without React? For Svelte, Vue, vanilla JS, or headless editing, see Using the Engine Directly — the PlaylistEngine class works without any framework. :::

useSelectionState​

Selection state (start/end) with engine delegation.

function useSelectionState(props: {
engineRef: RefObject<PlaylistEngine | null>;
}): {
selectionStart: number;
selectionEnd: number;
setSelection: (start: number, end: number) => void;
selectionStartRef: RefObject<number>;
selectionEndRef: RefObject<number>;
onEngineState: (state: EngineState) => void;
};

Note: For consumer components, use setSelection() from usePlaylistControls() and selectionStart / selectionEnd from usePlaylistState().

useLoopState​

Loop region state with engine delegation.

function useLoopState(props: {
engineRef: RefObject<PlaylistEngine | null>;
}): {
isLoopEnabled: boolean;
loopStart: number;
loopEnd: number;
setLoopEnabled: (enabled: boolean) => void;
setLoopRegion: (start: number, end: number) => void;
isLoopEnabledRef: RefObject<boolean>;
loopStartRef: RefObject<number>;
loopEndRef: RefObject<number>;
onEngineState: (state: EngineState) => void;
};

Note: For consumer components, use setLoopEnabled() / setLoopRegion() / clearLoopRegion() from usePlaylistControls() and loop state from usePlaylistState().

useSelectedTrack​

Selected track ID with engine delegation.

function useSelectedTrack(props: {
engineRef: RefObject<PlaylistEngine | null>;
}): {
selectedTrackId: string | null;
setSelectedTrackId: (trackId: string | null) => void;
selectedTrackIdRef: RefObject<string | null>;
onEngineState: (state: EngineState) => void;
};

Note: For consumer components, use setSelectedTrackId() from usePlaylistControls() and selectedTrackId from usePlaylistState().


useTimeFormat​

Control time format display.

Signature​

function useTimeFormat(): {
timeFormat: string;
setTimeFormat: (format: string) => void;
formatTime: (seconds: number) => string;
};

Available Formats​

FormatExample
seconds123.456
thousandths2:03.456
hh:mm:ss0:02:03
hh:mm:ss.u0:02:03.4
hh:mm:ss.uu0:02:03.45
hh:mm:ss.uuu0:02:03.456

Example​

function TimeDisplay() {
const { currentTime } = usePlaybackAnimation();
const { formatTime, setTimeFormat } = useTimeFormat();

return (
<div>
<span>{formatTime(currentTime)}</span>
<select onChange={(e) => setTimeFormat(e.target.value)}>
<option value="thousandths">0:00.000</option>
<option value="hh:mm:ss">0:00:00</option>
<option value="seconds">Seconds</option>
</select>
</div>
);
}

Integrated Recording​

useIntegratedRecording​

From @waveform-playlist/recording

Full-featured recording hook that integrates with the playlist - handles microphone access, recording, live peaks, and automatic track/clip creation.

function useIntegratedRecording(
tracks: ClipTrack[],
setTracks: (tracks: ClipTrack[]) => void,
selectedTrackId: string | null,
options?: IntegratedRecordingOptions
): UseIntegratedRecordingReturn;

Options​

interface IntegratedRecordingOptions {
currentTime?: number; // Current playhead position for recording start
audioConstraints?: MediaTrackConstraints; // Override recording-optimized defaults
channelCount?: number; // Default: 1 (auto-detected from stream; fallback)
samplesPerPixel?: number; // Default: 1024
bits?: 8 | 16; // Default: 16
}

Returns​

interface UseIntegratedRecordingReturn {
// Recording state
isRecording: boolean;
isPaused: boolean;
duration: number;

// Microphone levels (scalar — max across channels)
level: number; // Current peak level (0-1)
peakLevel: number; // Held peak level with decay (0-1)

// Per-channel levels (for multi-channel VU meters)
levels: number[]; // Per-channel peak levels (0-1)
peakLevels: number[]; // Per-channel held peak levels (0-1)

// Device management
stream: MediaStream | null;
devices: MicrophoneDevice[];
hasPermission: boolean;
selectedDevice: string | null;

// Controls
/** Resolves true when the capture pipeline actually started. */
startRecording: () => Promise<boolean>;
stopRecording: () => void;
pauseRecording: () => void;
resumeRecording: () => void;
requestMicAccess: () => Promise<void>;
changeDevice: (deviceId: string) => Promise<void>;

// Live waveform data
recordingPeaks: (Int8Array | Int16Array)[]; // Per-channel live peaks

// Error handling
error: Error | null;
}

Example​

function RecordingControls() {
const [tracks, setTracks] = useState<ClipTrack[]>([]);
const [selectedTrackId, setSelectedTrackId] = useState<string | null>(null);
const { currentTime } = usePlaybackAnimation();

const {
isRecording,
duration,
levels,
peakLevels,
devices,
hasPermission,
startRecording,
stopRecording,
requestMicAccess,
changeDevice,
recordingPeaks,
error,
} = useIntegratedRecording(tracks, setTracks, selectedTrackId, { currentTime });

if (!hasPermission) {
return <button onClick={requestMicAccess}>Enable Microphone</button>;
}

return (
<div>
<select onChange={(e) => changeDevice(e.target.value)}>
{devices.map(d => <option key={d.deviceId} value={d.deviceId}>{d.label}</option>)}
</select>
<button onClick={isRecording ? stopRecording : startRecording}>
{isRecording ? 'Stop' : 'Record'}
</button>
{isRecording && <span>Recording: {duration.toFixed(1)}s</span>}
<SegmentedVUMeter levels={levels} peakLevels={peakLevels} />
</div>
);
}

Drag & Drop Hooks​

useClipDragHandlers​

Handles clip dragging (move) and boundary trimming with collision detection. Delegates move operations to engine.moveClip() and trim operations to engine.trimClip().

function useClipDragHandlers(options: UseClipDragHandlersOptions): {
onDragStart: (event: DragStartEvent) => void;
onDragMove: (event: DragMoveEvent) => void;
onDragEnd: (event: DragEndEvent) => void;
onDragCancel: (event: DragCancelEvent) => void;
collisionModifier: Modifier;
};

Options​

interface UseClipDragHandlersOptions {
tracks: ClipTrack[];
onTracksChange: (tracks: ClipTrack[]) => void;
samplesPerPixel: number;
engineRef: RefObject<PlaylistEngine | null>;
/** Ref toggled during boundary trim drags. Obtain from usePlaylistData(). */
isDraggingRef: MutableRefObject<boolean>;
}

Example​

import { DragDropProvider } from '@dnd-kit/react';
import { RestrictToHorizontalAxis } from '@dnd-kit/abstract/modifiers';
import { ClipCollisionModifier, noDropAnimationPlugins, useDragSensors } from '@waveform-playlist/browser';

function EditablePlaylist() {
const [tracks, setTracks] = useState<ClipTrack[]>(initialTracks);
const { samplesPerPixel, playoutRef, isDraggingRef } = usePlaylistData();
const sensors = useDragSensors();

const { onDragStart, onDragMove, onDragEnd } = useClipDragHandlers({
tracks,
onTracksChange: setTracks,
samplesPerPixel,
engineRef: playoutRef,
isDraggingRef,
});

return (
<DragDropProvider
sensors={sensors}
onDragStart={onDragStart}
onDragMove={onDragMove}
onDragEnd={onDragEnd}
modifiers={[RestrictToHorizontalAxis, ClipCollisionModifier.configure({ tracks, samplesPerPixel })]}
plugins={noDropAnimationPlugins}
>
<Waveform interactiveClips showClipHeaders />
</DragDropProvider>
);
}

useDragSensors​

Pre-configured drag sensors for clip editing.

function useDragSensors(options?: DragSensorOptions): PluginDescriptor[];

useAnnotationDragHandlers​

Similar to useClipDragHandlers but for annotation boxes.


Clip Editing Hooks​

useClipSplitting​

Split clips at the playhead position. Delegates to engine.splitClip() — the engine handles clip creation, adapter sync, and statechange emission.

function useClipSplitting(options: UseClipSplittingOptions): UseClipSplittingResult;

Options​

interface UseClipSplittingOptions {
tracks: ClipTrack[];
samplesPerPixel: number;
engineRef: RefObject<PlaylistEngine | null>;
}

Returns​

interface UseClipSplittingResult {
splitClipAtPlayhead: () => boolean;
splitClipAt: (trackIndex: number, clipIndex: number, splitTime: number) => boolean;
}

Example​

function SplitButton() {
const { tracks, samplesPerPixel, playoutRef } = usePlaylistData();

const { splitClipAtPlayhead } = useClipSplitting({
tracks,
samplesPerPixel,
engineRef: playoutRef,
});

return (
<button onClick={splitClipAtPlayhead}>
Split Clip (S)
</button>
);
}

Effects Hooks​

Both effects hooks also host third-party WAM 2.0 plugins alongside the built-in Tone.js effects — see the WAM Plugins guide for native-context setup, bypass semantics, WamEffectGui, and plugin discovery.

useDynamicEffects​

Manage master effects chain with real-time parameter updates.

function useDynamicEffects(fftSize?: number): UseDynamicEffectsReturn; // default: 256

Returns​

interface UseDynamicEffectsReturn {
activeEffects: ActiveEffect[];
availableEffects: EffectDefinition[];
addEffect: (effectId: string) => void;
/**
* Hosts a WAM plugin from a module URL and appends it to the master chain.
* Requires native-context mode — call configureGlobalContext({ nativeAudioContext: true })
* from @waveform-playlist/playout before any audio initialization.
* WAM entries render in offline WAV export (re-instantiated on the offline context).
*/
addWamEffect: (url: string, initialState?: unknown) => Promise<string>;
/** Live plugin handle for a hosted WAM entry (for GUI mounting via WamEffectGui). */
getWamPlugin: (instanceId: string) => WamPluginInstance | undefined;
removeEffect: (instanceId: string) => void;
updateParameter: (instanceId: string, paramName: string, value: number | string | boolean) => void;
toggleBypass: (instanceId: string) => void;
reorderEffects: (fromIndex: number, toIndex: number) => void;
clearAllEffects: () => void;
masterEffects: EffectsFunction;
/**
* Creates a fresh effects function for offline rendering. Native effects are
* re-created on the offline context; WAM entries are re-instantiated from
* their URL-cached factories with the live instance's state transferred.
* The returned function may be async and may reject — a WAV export never
* silently renders without an effect the live chain has.
*/
createOfflineEffectsFunction: () => OfflineEffectsFunction | undefined;
analyserRef: RefObject<Analyser | null>;
}

interface ActiveEffect {
instanceId: string;
effectId: string;
/** 'native' = built-in Tone effect; 'wam' = hosted WAM plugin. */
kind: 'native' | 'wam';
/** Module URL for wam entries. */
url?: string;
definition: EffectDefinition;
params: Record<string, number | string | boolean>;
bypassed: boolean;
}

useTrackDynamicEffects​

Per-track effects management.

function useTrackDynamicEffects(): UseTrackDynamicEffectsReturn;

Returns​

interface UseTrackDynamicEffectsReturn {
trackEffectsState: Map<string, TrackActiveEffect[]>;
addEffectToTrack: (trackId: string, effectId: string) => void;
/**
* Hosts a WAM plugin from a module URL and appends it to a track's effect chain.
* Requires native-context mode — call configureGlobalContext({ nativeAudioContext: true })
* from @waveform-playlist/playout before any audio initialization.
* WAM entries render in offline WAV export (re-instantiated on the offline context).
*/
addWamEffectToTrack: (trackId: string, url: string, initialState?: unknown) => Promise<string>;
/** Live plugin handle for a hosted WAM entry on a track (for GUI mounting via WamEffectGui). */
getTrackWamPlugin: (trackId: string, instanceId: string) => WamPluginInstance | undefined;
removeEffectFromTrack: (trackId: string, instanceId: string) => void;
updateTrackEffectParameter: (
trackId: string,
instanceId: string,
paramName: string,
value: number | string | boolean
) => void;
toggleBypass: (trackId: string, instanceId: string) => void;
clearTrackEffects: (trackId: string) => void;
getTrackEffectsFunction: (trackId: string) => TrackEffectsFunction | undefined;
/**
* Creates a fresh effects function for a track for offline rendering. Native
* effects are re-created on the offline context; WAM entries are
* re-instantiated with the live instance's state transferred. May reject —
* a WAV export never silently renders without an effect the live chain has.
*/
createOfflineTrackEffectsFunction: (trackId: string) => OfflineTrackEffectsFunction | undefined;
availableEffects: EffectDefinition[];
}

interface TrackActiveEffect {
instanceId: string;
effectId: string;
/** 'native' = built-in Tone effect; 'wam' = hosted WAM plugin. */
kind: 'native' | 'wam';
/** Module URL for wam entries. */
url?: string;
definition: EffectDefinition;
params: Record<string, number | string | boolean>;
bypassed: boolean;
}

Recording Hooks​

From @waveform-playlist/recording:

useMicrophoneAccess​

function useMicrophoneAccess(): {
stream: MediaStream | null;
devices: MicrophoneDevice[];
hasPermission: boolean;
isLoading: boolean;
error: Error | null;
requestAccess: (deviceId?: string, audioConstraints?: MediaTrackConstraints) => Promise<void>;
stopStream: () => void;
};

useRecording​

function useRecording(
stream: MediaStream | null,
options?: RecordingOptions
): {
isRecording: boolean;
isPaused: boolean;
duration: number;
peaks: (Int8Array | Int16Array)[];
audioBuffer: AudioBuffer | null;
level: number;
peakLevel: number;
/** Resolves true when the capture pipeline actually started. */
startRecording: () => Promise<boolean>;
stopRecording: () => Promise<AudioBuffer | null>;
pauseRecording: () => void;
resumeRecording: () => void;
error: Error | null;
};

useMicrophoneLevel​

function useMicrophoneLevel(
stream: MediaStream | null,
options?: {
channelCount?: number; // Default: 1 (mono). Set to 2 for stereo.
updateRate?: number; // Default: 60 (Hz)
}
): {
level: number; // 0-1 peak level (max across channels, for backwards compatibility)
peakLevel: number; // 0-1 held peak with decay (max across channels)
levels: number[]; // Per-channel peak levels (0-1)
peakLevels: number[]; // Per-channel held peak levels with decay (0-1)
rmsLevels: number[]; // Per-channel RMS levels (0-1)
resetPeak: () => void; // Reset all held peak levels
};

useOutputMeter​

From @waveform-playlist/browser/tone

Monitor the master output level of the playlist. Must be used inside WaveformPlaylistProvider.

function useOutputMeter(options?: UseOutputMeterOptions): UseOutputMeterReturn;

Options​

interface UseOutputMeterOptions {
channelCount?: number; // Default: 2
updateRate?: number; // Default: 60 (Hz)
isPlaying?: boolean; // Reset levels when false (prevents frozen meters)
}

Returns​

interface UseOutputMeterReturn {
levels: number[]; // Per-channel peak output levels (0-1)
peakLevels: number[]; // Per-channel held peak levels with decay (0-1)
rmsLevels: number[]; // Per-channel RMS output levels (0-1)
resetPeak: () => void; // Reset all peak hold indicators
}

Example​

import { useOutputMeter } from '@waveform-playlist/browser/tone';

function OutputMeter() {
const { levels, peakLevels, resetPeak } = useOutputMeter({ channelCount: 2 });

return (
<div>
{levels.map((level, ch) => (
<div key={ch}>
<span>{ch === 0 ? 'L' : 'R'}: {(level * 100).toFixed(0)}%</span>
<span>Peak: {(peakLevels[ch] * 100).toFixed(0)}%</span>
</div>
))}
<button onClick={resetPeak}>Reset Peak</button>
</div>
);
}

Export Hooks​

useExportWav​

Export the playlist to WAV format using offline rendering.

function useExportWav(): {
exportWav: (tracks: ClipTrack[], trackStates: TrackState[], options?: ExportOptions) => Promise<ExportResult>;
isExporting: boolean;
progress: number;
error: string | null;
};

ExportOptions​

interface ExportOptions {
filename?: string; // Filename for download (default: 'export')
mode?: 'master' | 'individual'; // Export all tracks mixed or single track
trackIndex?: number; // Track index for individual export
bitDepth?: 16 | 32; // WAV bit depth (default: 16)
applyEffects?: boolean; // Apply fades and effects (default: true)
effectsFunction?: OfflineEffectsFunction; // Effects chain for export (native + WAM)
autoDownload?: boolean; // Trigger automatic download (default: true)
onProgress?: (progress: number) => void;
}

Effects Function​

When an effectsFunction is provided and applyEffects is true, export renders through the effects chain — native Tone.js effects and hosted WAM plugins alike. WAM entries are re-instantiated on the offline context from their URL-cached factories with the live instance's state transferred; a WAM plugin that fails to re-instantiate fails the export. The function may return a cleanup callback synchronously or asynchronously.

type OfflineEffectsCleanup = void | (() => void);

type OfflineEffectsFunction = (
masterVolume: Volume,
destination: ToneAudioNode,
isOffline: boolean // true during export
) => OfflineEffectsCleanup | Promise<OfflineEffectsCleanup>;

ExportResult​

interface ExportResult {
audioBuffer: AudioBuffer; // Rendered audio buffer
blob: Blob; // WAV file as Blob
duration: number; // Duration in seconds
}

Example​

function ExportButton() {
const { tracks, trackStates } = usePlaylistData();
const { exportWav, isExporting, progress } = useExportWav();

const handleExport = async () => {
try {
const result = await exportWav(tracks, trackStates, {
filename: 'my-mix',
mode: 'master',
bitDepth: 16,
});
console.log('Exported:', result.duration, 'seconds');
} catch (error) {
console.error('Export failed:', error);
}
};

return (
<button onClick={handleExport} disabled={isExporting}>
{isExporting ? `Exporting ${Math.round(progress * 100)}%` : 'Export WAV'}
</button>
);
}

Annotations Hooks​

From @waveform-playlist/annotations:

useAnnotationControls​

function useAnnotationControls(): {
annotations: Annotation[];
selectedAnnotationId: string | null;
addAnnotation: (annotation: Annotation) => void;
updateAnnotation: (id: string, updates: Partial<Annotation>) => void;
deleteAnnotation: (id: string) => void;
selectAnnotation: (id: string | null) => void;
setAnnotations: (annotations: Annotation[]) => void;
};

Keyboard Shortcuts​

usePlaybackShortcuts​

Enable common playback keyboard shortcuts.

Default Shortcuts​

KeyAction
SpaceToggle play/pause
EscapeStop playback
0Rewind to start

Signature​

function usePlaybackShortcuts(options?: UsePlaybackShortcutsOptions): {
togglePlayPause: () => void;
stopPlayback: () => void;
rewindToStart: () => void;
shortcuts: KeyboardShortcut[];
};

Options​

interface UsePlaybackShortcutsOptions {
enabled?: boolean; // Enable shortcuts (default: true)
additionalShortcuts?: KeyboardShortcut[]; // Add custom shortcuts
shortcuts?: KeyboardShortcut[]; // Override all shortcuts
}

Example​

// Basic usage - enables default shortcuts
usePlaybackShortcuts();

// With additional custom shortcuts
usePlaybackShortcuts({
additionalShortcuts: [
{ key: 's', action: splitClipAtPlayhead, description: 'Split clip' },
{ key: ' ', action: togglePlay, description: 'Play/Pause' },
],
});

// Override defaults completely
usePlaybackShortcuts({
shortcuts: [
{ key: 'Home', action: rewindToStart, description: 'Go to start' },
],
});

useKeyboardShortcuts​

Low-level hook for custom keyboard shortcuts.

Signature​

function useKeyboardShortcuts(options: UseKeyboardShortcutsOptions): void;

KeyboardShortcut​

interface KeyboardShortcut {
key: string; // Key to listen for
action: () => void; // Function to call
ctrlKey?: boolean; // Require Ctrl modifier
shiftKey?: boolean; // Require Shift modifier
metaKey?: boolean; // Require Meta/Cmd modifier
altKey?: boolean; // Require Alt modifier
description?: string; // Human-readable description
preventDefault?: boolean; // Prevent default behavior (default: true)
}

Example​

useKeyboardShortcuts({
shortcuts: [
{
key: 's',
action: () => splitClip(),
description: 'Split clip at playhead',
},
{
key: 'z',
metaKey: true,
action: () => undo(),
description: 'Undo',
},
{
key: 'z',
metaKey: true,
shiftKey: true,
action: () => redo(),
description: 'Redo',
},
],
enabled: !isInputFocused,
});

getShortcutLabel​

Get a human-readable label for a shortcut:

import { getShortcutLabel } from '@waveform-playlist/browser';

const shortcut = { key: 's', metaKey: true, shiftKey: true };
const label = getShortcutLabel(shortcut);
// On Mac: "Cmd+Shift+S"
// On Windows: "Ctrl+Shift+S"

Best Practices​

1. Use Specific Hooks​

Use the split context hooks for optimal performance:

// Only subscribes to playback animation state
const { isPlaying } = usePlaybackAnimation();
const { play, pause } = usePlaylistControls();

2. Memoize Callbacks​

When passing to child components:

const handleVolumeChange = useCallback((value: number) => {
setVolume(value);
}, [setVolume]);

3. Context Boundaries​

Hooks must be used within their providers:

// Correct
<WaveformPlaylistProvider>
<ComponentUsingHooks />
</WaveformPlaylistProvider>

// Error
<ComponentUsingHooks /> // Outside provider

See Also​

In Partnership With

Télécom Paris