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​
| Name | Type | Description |
|---|---|---|
configs | AudioTrackConfig[] | 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​
| Property | Type | Description |
|---|---|---|
tracks | ClipTrack[] | Loaded track objects |
loading | boolean | Loading state |
error | string | null | Error message |
loadedCount | number | Number of configs that have finished loading |
totalCount | number | Total 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​
| Property | Type | Description |
|---|---|---|
tracks | ClipTrack[] | Current tracks (placeholders + loaded) |
addTracks | (sources: TrackSource[]) => void | Add files or URLs at runtime |
removeTrack | (trackId: string) => void | Remove a track by id. Aborts in-flight fetch if still loading. |
loadingCount | number | Number of sources currently decoding |
isLoading | boolean | true when any source is still decoding |
errors | TrackLoadError[] | 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​
| Format | Example |
|---|---|
seconds | 123.456 |
thousandths | 2:03.456 |
hh:mm:ss | 0:02:03 |
hh:mm:ss.u | 0:02:03.4 |
hh:mm:ss.uu | 0:02:03.45 |
hh:mm:ss.uuu | 0: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​
| Key | Action |
|---|---|
Space | Toggle play/pause |
Escape | Stop playback |
0 | Rewind 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