Skip to main content

Hooks

React hooks for accessing playlist state and controls.

Import​

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

// Specialized hooks
useAudioTracks,
useZoomControls,
useTimeFormat,
useMasterVolume,

// Drag & drop
useClipDragHandlers,
useDragSensors,

// Clip editing
useClipSplitting,

// Effects
useDynamicEffects,
useTrackDynamicEffects,

// Recording (integrated with playlist)
useIntegratedRecording,

// Keyboard shortcuts
useKeyboardShortcuts,
usePlaybackShortcuts,

// Export
useExportWav,
} from '@waveform-playlist/browser';

// 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 and refs.

function usePlaylistData(): {
// Audio data
sampleRate: number;
duration: number;
audioBuffers: AudioBuffer[];

// Display settings
samplesPerPixel: number;
waveHeight: number;
mono: boolean;
controls: { show: boolean; width: number };

// Refs for direct access
playoutRef: RefObject<TonePlayout>;
scrollContainerRef: RefObject<HTMLDivElement>;
};

usePlaybackAnimation​

Access playback state and timing refs for smooth animations.

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

// Refs for 60fps animation loops
currentTimeRef: RefObject<number>;
playbackStartTimeRef: RefObject<number>;
audioStartPositionRef: RefObject<number>;
};

Example​

function AnimatedPlayhead() {
const { isPlaying, currentTimeRef, playbackStartTimeRef, audioStartPositionRef } = usePlaybackAnimation();
const { samplesPerPixel, sampleRate } = usePlaylistData();
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
let frameId: number;

const animate = () => {
if (ref.current && isPlaying) {
const elapsed = getContext().currentTime - (playbackStartTimeRef.current ?? 0);
const time = (audioStartPositionRef.current ?? 0) + elapsed;
const pixels = (time * sampleRate) / samplesPerPixel;
ref.current.style.transform = `translateX(${pixels}px)`;
}
frameId = requestAnimationFrame(animate);
};

if (isPlaying) frameId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(frameId);
}, [isPlaying]);

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

useAudioTracks​

Load and decode audio files into track objects.

Signature​

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

Parameters​

NameTypeDescription
configsAudioConfig[]Array of audio configurations

AudioConfig​

interface AudioConfig {
src: string; // URL to audio file (required)
name: string; // Display name (required)
startTime?: number; // Start position in seconds
waveformDataUrl?: string; // URL to BBC Peaks waveform data
gain?: number; // Initial volume 0-1
muted?: boolean; // Start muted
soloed?: boolean; // Start soloed
pan?: number; // Pan position -1 to 1
}

Returns​

PropertyTypeDescription
tracksClipTrack[]Loaded track objects
loadingbooleanLoading state
errorstring | nullError message
progressnumberLoading progress 0-1

Example​

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

if (loading) return <div>Loading... {Math.round(progress * 100)}%</div>;
if (error) return <div>Error: {error}</div>;

usePlaylistState​

Access the current playlist state.

Signature​

function usePlaylistState(): PlaylistState;

Returns​

interface PlaylistState {
// Tracks
tracks: ClipTrack[];
selectedTrackIndex: number | null;
selectedClipIndex: number | null;

// Playback
isPlaying: boolean;
isPaused: boolean;
cursorPosition: number;
duration: number;

// Selection
selection: { start: number; end: number };

// Display
samplesPerPixel: number;
waveHeight: number;
sampleRate: number;

// Settings
isContinuousPlay: boolean;
isAutomaticScroll: boolean;
masterVolume: number;
timeFormat: string;
}

Example​

function StatusBar() {
const { isPlaying, cursorPosition, duration, tracks } = usePlaylistState();

return (
<div>
<span>{isPlaying ? 'Playing' : 'Stopped'}</span>
<span>{cursorPosition.toFixed(2)} / {duration.toFixed(2)}</span>
<span>{tracks.length} tracks</span>
</div>
);
}

usePlaylistControls​

Access playlist control functions.

Signature​

function usePlaylistControls(): PlaylistControls;

Returns​

interface PlaylistControls {
// Playback
play: (start?: number, end?: number) => void;
pause: () => void;
stop: () => void;
seek: (position: number) => void;

// Zoom
zoomIn: () => void;
zoomOut: () => void;
setSamplesPerPixel: (spp: number) => void;

// Tracks
addTrack: (track: ClipTrack) => void;
removeTrack: (index: number) => void;
moveTrack: (fromIndex: number, toIndex: number) => void;
selectTrack: (index: number | null) => void;

// Settings
setIsContinuousPlay: (value: boolean) => void;
setIsAutomaticScroll: (value: boolean) => void;
setMasterVolume: (value: number) => void;
setTimeFormat: (format: string) => void;
}

Example​

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

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

usePlaybackControls​

Focused hook for playback state and controls.

Signature​

function usePlaybackControls(): {
play: () => void;
pause: () => void;
stop: () => void;
isPlaying: boolean;
isPaused: boolean;
};

Example​

function PlayPauseButton() {
const { play, pause, isPlaying } = usePlaybackControls();

return (
<button onClick={isPlaying ? pause : play}>
{isPlaying ? 'Pause' : 'Play'}
</button>
);
}

useTrackControls​

Control an individual track.

Signature​

function useTrackControls(trackIndex: number): {
muted: boolean;
soloed: boolean;
volume: number;
pan: number;
setMuted: (value: boolean) => void;
setSoloed: (value: boolean) => void;
setVolume: (value: number) => void;
setPan: (value: number) => void;
};

Parameters​

NameTypeDescription
trackIndexnumberIndex of the track

Example​

function TrackMixer({ trackIndex }: { trackIndex: number }) {
const { volume, pan, setVolume, setPan, muted, setMuted } =
useTrackControls(trackIndex);

return (
<div>
<button onClick={() => setMuted(!muted)}>
{muted ? 'Unmute' : 'Mute'}
</button>
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={(e) => setVolume(parseFloat(e.target.value))}
/>
<input
type="range"
min="-1"
max="1"
step="0.01"
value={pan}
onChange={(e) => setPan(parseFloat(e.target.value))}
/>
</div>
);
}

useZoomControls​

Control zoom level.

Signature​

function useZoomControls(): {
samplesPerPixel: number;
zoomIn: () => void;
zoomOut: () => void;
setSamplesPerPixel: (spp: number) => void;
canZoomIn: boolean;
canZoomOut: boolean;
};

Example​

function ZoomSlider() {
const { samplesPerPixel, setSamplesPerPixel, canZoomIn, canZoomOut } =
useZoomControls();

const zoomLevels = [256, 512, 1024, 2048, 4096];
const currentIndex = zoomLevels.indexOf(samplesPerPixel);

return (
<div>
<button onClick={() => setSamplesPerPixel(zoomLevels[currentIndex - 1])} disabled={!canZoomIn}>
+
</button>
<span>{samplesPerPixel} spp</span>
<button onClick={() => setSamplesPerPixel(zoomLevels[currentIndex + 1])} disabled={!canZoomOut}>
-
</button>
</div>
);
}

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 { cursorPosition } = usePlaylistState();
const { formatTime, setTimeFormat } = useTimeFormat();

return (
<div>
<span>{formatTime(cursorPosition)}</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​

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
}

Returns​

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

// Microphone levels
level: number; // Current RMS level (0-1)
peakLevel: number; // Peak level with decay (0-1)

// Device management
devices: MediaDeviceInfo[];
hasPermission: boolean;
selectedDevice: string | null;

// Controls
startRecording: () => Promise<void>;
stopRecording: () => void;
requestMicAccess: () => Promise<void>;
changeDevice: (deviceId: string) => void;

// Live waveform data
recordingPeaks: number[];

// 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,
level,
peakLevel,
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>}
<VUMeter level={level} peakLevel={peakLevel} />
</div>
);
}

Drag & Drop Hooks​

useClipDragHandlers​

Handles clip dragging (move) and boundary trimming with collision detection.

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

Options​

interface UseClipDragHandlersOptions {
tracks: ClipTrack[];
onTracksChange: (tracks: ClipTrack[]) => void;
samplesPerPixel: number;
sampleRate: number;
}

Example​

import { DndContext } from '@dnd-kit/core';
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';

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

const { onDragStart, onDragMove, onDragEnd, collisionModifier } = useClipDragHandlers({
tracks,
onTracksChange: setTracks,
samplesPerPixel,
sampleRate,
});

return (
<DndContext
sensors={sensors}
onDragStart={onDragStart}
onDragMove={onDragMove}
onDragEnd={onDragEnd}
modifiers={[restrictToHorizontalAxis, collisionModifier]}
>
<Waveform interactiveClips showClipHeaders />
</DndContext>
);
}

useDragSensors​

Pre-configured drag sensors for clip editing.

function useDragSensors(): SensorDescriptor<any>[];

useAnnotationDragHandlers​

Similar to useClipDragHandlers but for annotation boxes.


Clip Editing Hooks​

useClipSplitting​

Split clips at the playhead position.

function useClipSplitting(options: UseClipSplittingOptions): UseClipSplittingResult;

Options​

interface UseClipSplittingOptions {
tracks: ClipTrack[];
onTracksChange: (tracks: ClipTrack[]) => void;
selectedTrackId?: string | null;
selectedClipId?: string | null;
}

Returns​

interface UseClipSplittingResult {
splitClipAtPlayhead: () => void;
canSplit: boolean;
}

Example​

function SplitButton() {
const { tracks, selectedTrackId, selectedClipId } = usePlaylistState();
const [localTracks, setLocalTracks] = useState(tracks);

const { splitClipAtPlayhead, canSplit } = useClipSplitting({
tracks: localTracks,
onTracksChange: setLocalTracks,
selectedTrackId,
selectedClipId,
});

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

Effects Hooks​

useDynamicEffects​

Manage master effects chain with real-time parameter updates.

function useDynamicEffects(): UseDynamicEffectsReturn;

Returns​

interface UseDynamicEffectsReturn {
activeEffects: ActiveEffect[];
addEffect: (effectId: string) => void;
removeEffect: (instanceId: string) => void;
updateParameter: (instanceId: string, paramId: string, value: number) => void;
toggleBypass: (instanceId: string) => void;
reorderEffects: (fromIndex: number, toIndex: number) => void;
createEffectsFunction: () => EffectsFunction;
createOfflineEffectsFunction: () => EffectsFunction;
}

interface ActiveEffect {
instanceId: string;
effectId: string;
parameters: Record<string, number>;
bypassed: boolean;
}

useTrackDynamicEffects​

Per-track effects management.

function useTrackDynamicEffects(): UseTrackDynamicEffectsReturn;

Returns​

interface UseTrackDynamicEffectsReturn {
trackEffects: Map<string, TrackActiveEffect[]>;
addTrackEffect: (trackId: string, effectId: string) => void;
removeTrackEffect: (trackId: string, instanceId: string) => void;
updateTrackParameter: (trackId: string, instanceId: string, paramId: string, value: number) => void;
toggleTrackBypass: (trackId: string, instanceId: string) => void;
createTrackEffectsFunction: (trackId: string) => TrackEffectsFunction;
createOfflineTrackEffectsFunction: (trackId: string) => TrackEffectsFunction;
}

Recording Hooks​

From @waveform-playlist/recording:

useMicrophoneAccess​

function useMicrophoneAccess(options?: {
audioConstraints?: MediaTrackConstraints;
}): {
hasAccess: boolean;
isRequesting: boolean;
error: string | null;
requestAccess: () => Promise<void>;
revokeAccess: () => void;
};

useRecording​

function useRecording(): {
isRecording: boolean;
isPaused: boolean;
duration: number;
recordedBlob: Blob | null;
startRecording: () => void;
stopRecording: () => void;
pauseRecording: () => void;
resumeRecording: () => void;
clearRecording: () => void;
};

useMicrophoneLevel​

function useMicrophoneLevel(): {
level: number; // 0-1 RMS level
peak: number; // 0-1 peak with decay
};

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?: EffectsFunction; // Tone.js effects chain for export
autoDownload?: boolean; // Trigger automatic download (default: true)
onProgress?: (progress: number) => void;
}

Effects Function​

When an effectsFunction is provided and applyEffects is true, export uses Tone.Offline to render through the effects chain. This allows exporting with reverb, delay, and other Tone.js effects.

type EffectsFunction = (
masterVolume: Volume,
destination: ToneAudioNode,
isOffline: boolean // true during export
) => void | (() => void);

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​

Prefer specific hooks over general ones:

// Better - only subscribes to playback state
const { isPlaying, play, pause } = usePlaybackControls();

// Less efficient - subscribes to all state
const { isPlaying } = usePlaylistState();
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​

Buy Me A Coffee