@olilarkin/react-waveform
v0.0.1
Published
React audio waveform and synchronized spectrogram player.
Maintainers
Readme
@olilarkin/react-waveform
React component for an audio file player with a synchronized waveform and reassigned spectrogram display.
It is based on the audio analysis and WebAssembly DSP approach from @counterpoint-studio/audio-file-mcp-app, packaged for normal React applications.
Install
npm install @olilarkin/react-waveformimport {
WaveformSpectrogramPlayer,
type WaveformSpectrogramPlayerHandle,
} from "@olilarkin/react-waveform";
import "@olilarkin/react-waveform/style.css";Usage
import { useRef, useState } from "react";
import {
WaveformSpectrogramPlayer,
type WaveformSpectrogramPlayerHandle,
} from "@olilarkin/react-waveform";
import "@olilarkin/react-waveform/style.css";
export function AudioInspector() {
const [file, setFile] = useState<File | null>(null);
const player = useRef<WaveformSpectrogramPlayerHandle>(null);
return (
<>
<input
type="file"
accept="audio/*"
onChange={(event) => setFile(event.currentTarget.files?.[0] ?? null)}
/>
<WaveformSpectrogramPlayer
ref={player}
source={file}
metadata={{ name: file?.name }}
rangeSelection
loopPlayback
followPlayback={{ enabled: true, edgeFraction: 0.18, resumeDelayMs: 250 }}
snapToMarkers
defaultRange={{ startSeconds: 4, endSeconds: 12 }}
locatorControls
defaultLocators={{ leftSeconds: 4, rightSeconds: 12 }}
markerLayers={[
{
id: "beats",
color: "#00aaff",
lane: "secondary",
markers: [
{ id: "beat-1", seconds: 1.25, label: "1" },
{ id: "beat-2", seconds: 1.75, label: "2" },
],
},
]}
onReady={(info) => console.log("ready", info)}
onRangeChange={(range) => console.log("range", range)}
onError={(error) => console.error(error)}
/>
</>
);
}source accepts a File, Blob, URL string, or URL. URL sources are fetched as a full blob in this first version.
Props
source?: File | Blob | string | URL | nullmetadata?: { name?: string; sizeBytes?: number; mimeType?: string }rangeSelection?: booleandefaults tofalseloopPlayback?: booleandefaults tofalseloopSelection?: booleanlegacy alias for enabling both range selection and loop playbacktimelineUnit?: "seconds" | "samples" | "percent"defaults to"seconds"timelineOptions?: { visible?: boolean; height?: number | string; backgroundColor?: string; textColor?: string; borderColor?: string }overlayOptions?: { hoverTextColor?: string; hoverTextBackgroundColor?: string; hoverTextShadow?: boolean; hoverTextShadowColor?: string; markerTop?: number | string; markerColor?: string; markerLabelColor?: string; markerLabelShadow?: boolean; markerLabelShadowColor?: string; locatorLabelColor?: string; locatorLabelShadow?: boolean; locatorLabelShadowColor?: string; rangeFillColor?: string; rangeFillOpacity?: number; rangeHandleColor?: string }waveformDrawingMode?: "frequency" | "sections" | "solid"defaults to"solid"sections?: Array<{ id: string | number; startSeconds?: number; endSeconds?: number; startNormalized?: number; endNormalized?: number; color: string; label?: string }>used whenwaveformDrawingModeis"sections"spectrogramOptions?: { colorMap?: "inferno" | "jet" | "magma" | "viridis" | "grayscale"; contrast?: number; range?: number }range?: { startSeconds: number; endSeconds: number } | nulldefaultRange?: { startSeconds: number; endSeconds: number } | nulldefaultViewMode?: "waveform" | "spectrum" | "both"defaults to"both"autoPlay?: booleandefaults tofalsefollowPlayback?: boolean | { enabled?: boolean; edgeFraction?: number; resumeDelayMs?: number }defaults tofalsemarkerLayers?: WaveformMarkerLayer[]defaults to[]snapToMarkers?: boolean | { enabled?: boolean; radiusPx?: number }defaults tofalselocators?: { leftSeconds: number; rightSeconds: number } | nulldefaultLocators?: { leftSeconds: number; rightSeconds: number } | nulllocatorControls?: boolean | { enabled?: boolean; color?: string; showLabels?: boolean; top?: number | string }defaults tofalseappearance?: { accentColor?: string; textColor?: string; mutedTextColor?: string; faintTextColor?: string; borderColor?: string; backgroundColor?: string; playheadColor?: string; rangeFillColor?: string; rangeHandleColor?: string; spectrogramBackgroundColor?: string }className?: stringstyle?: React.CSSPropertiesonReady(info)onPlaybackChange(playing)onPositionChange(position)onRangeChange(range)onRegionChange(region)onLocatorsChange(locators)onMarkerMove(layerId, markerId, seconds)onMetricsChange(metrics)onError(error)
The imperative ref exposes play(), pause(), seek(seconds), zoomIn(), zoomOut(), resetZoom(), setRange(start,end), setRangeNormalized(start,end), setRangeCenterSpan(centerSeconds,spanSeconds), clearRange(), setLocators(left,right), clearLocators(), goToLeftLocator(), goToRightLocator(), setLoopRegion(start,end), and clearLoopRegion().
followPlayback is opt-in. When enabled and the user is zoomed in, playback shifts the visible viewport once the playhead leaves the configured edge margin. It is suppressed while the user interacts with the timeline and resumes after resumeDelayMs, which defaults to 250. edgeFraction defaults to 0.18 and is clamped to a practical range.
rangeSelection controls drag-to-select range gestures. Ranges can be controlled with range, initialized with defaultRange, or changed through the imperative ref. Use setRangeNormalized(start,end) for 0..1 file positions and setRangeCenterSpan(centerSeconds,spanSeconds) for centered selections. Existing ranges can be edited by dragging either edge or moved by dragging inside the range. loopPlayback controls whether playback loops the selected/programmatic range. They are independent so hosts can use selection as an analysis range without changing transport behavior.
appearance provides a typed styling surface for common colors. It maps to CSS variables on the root player, so advanced consumers can still override the same variables through style or external CSS.
Locators
locatorControls is opt-in. Locators can be controlled by the host with locators, initialized with defaultLocators, or changed through the imperative ref. The locator overlay is clipped and remapped through zoom like markers and the playhead.
const player = useRef<WaveformSpectrogramPlayerHandle>(null);
<WaveformSpectrogramPlayer
ref={player}
source={file}
locatorControls={{ enabled: true, color: "#f97316" }}
defaultLocators={{ leftSeconds: 8, rightSeconds: 24 }}
onLocatorsChange={(locators) => console.log(locators)}
/>;
player.current?.goToLeftLocator();Marker Layers
markerLayers lets consumers render any number of host-owned timeline overlays, such as detected beats, cue points, or analysis landmarks. Markers can be expressed in seconds or as a normalized 0...1 file position. The component maps them through the same viewport as the waveform, so they stay aligned while users zoom.
When snapToMarkers is enabled, seek taps and range endpoints snap to nearby markers using a screen-space radius. The default snap radius is 12px, so snap behavior remains local as users zoom.
<WaveformSpectrogramPlayer
source={file}
markerLayers={[
{
id: "beats",
color: "#00aaff",
lane: "secondary",
markers: beats.map((seconds, index) => ({
id: `beat-${index}`,
seconds,
label: String(index + 1),
})),
},
{
id: "sections",
color: "#f97316",
lane: "primary",
markers: [{ id: "chorus", normalized: 0.42, label: "Chorus" }],
},
]}
/>Styling
Import @olilarkin/react-waveform/style.css. The component is scoped under .wr-player and defaults to a compact 240px height.
Useful CSS variables:
.wr-player {
--wr-accent: #38bdf8;
--wr-timeline-height: 30px;
--wr-timeline-background: #d6dae1;
--wr-timeline-color: #15171a;
--wr-hover-text-color: #fff;
--wr-hover-text-shadow: none;
--wr-marker-top: 0px;
--wr-marker-label-color: currentColor;
--wr-marker-label-shadow: none;
--wr-locator-label-color: currentColor;
--wr-locator-label-shadow: none;
--wr-range-fill-color: #38bdf8;
--wr-range-fill-opacity: 22%;
--wr-range-handle: #38bdf8;
--wr-spectrogram-height: 60px;
--wr-color-text: #111;
--wr-color-text-muted: #666;
--wr-color-border: #dcdcdc;
--wr-color-background: #ececec;
}WASM DSP
The package includes an inlined WebAssembly DSP module for FFT, loudness, and spectrogram rendering, with a JavaScript fallback for environments that block WebAssembly compilation.
Regenerate DSP artifacts only when the C/vendor DSP sources change:
pnpm run build:dspThis requires an active Emscripten SDK environment.
Development
pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm devLicense
ISC. Portions of the audio engine are adapted from @counterpoint-studio/audio-file-mcp-app; see NOTICE.
