@fieldui/core
v0.2.0
Published
Headless React hooks for audio interfaces — transport, audio graph, peaks, regions, recording, analysis, export. The engine behind @fieldui/react.
Downloads
676
Maintainers
Readme
@fieldui/core
Headless React hooks for Field, a headless-first React component library for audio interfaces (waveforms, transports, meters, spectrograms, scrubbers, regions, knobs, faders).
This package is the engine. It builds on the Web Audio API and provides hooks for audio context lifecycle, transport state, peaks, regions, recording, analysis, and export. Everything the visual layer of Field is made of, with zero rendering opinions. If you want a custom UI, or no UI at all and just the audio plumbing, depend on this package alone.
┌──────────────────────────────────────────────────────────────┐
│ @fieldui/react components (canvas + radix + tailwind) │
│ @fieldui/core ← headless hooks (audio graph, RAF, etc.)│
│ @fieldui/tokens CSS variables + TS token map │
└──────────────────────────────────────────────────────────────┘Highlights
- Two providers, one shared loop.
AudioGraphProviderowns a singleAudioContextplus ananalyser → gain → destinationmaster chain.RenderLoopProviderruns one sharedrequestAnimationFramethat all 60 fps consumers join, so a 10-component dashboard only ticks once per frame. - Refs-not-state for live data. Hooks that update at audio rate (peaks,
level meter, playback time) return
RefObjects the consumer reads inside its own RAF callback. Zero React re-renders during playback. - Type-safe source adapter.
useAudioSourceaccepts exactly one of{ buffer | element | stream }and bridges all three to the audio graph. Stream and element sources auto-connect to the master chain. Passconnect: false(or a specificAudioNode) to override. Per-context caches handle React 18 strict-mode remounts and thecreateMediaElementSource"called twice" footgun. - Bring your own
AudioContext.AudioGraphProvideraccepts an existingAudioContext(which it will not close on unmount) orAudioContextOptionsfor the default-create path. Drops into apps that already own their context (Tone.js, custom shells) cleanly. - Auto-resume on user gesture. Browsers require activation before audio
can play. The provider attaches one-shot listeners for
pointerdown,click,touchstart, andkeydown, then resumes the context on the first one. No "click to start" overlay required. - Worker-offloaded peaks.
usePeaksdecimates buffers for waveform rendering. Large buffers (≳10 s mono at 48 kHz) run on a Blob-URL Worker; smaller ones inline so the postMessage round-trip doesn't dominate.
Install
npm install @fieldui/core react react-dom
# or pnpm / yarn / bunreact and react-dom are peer deps. Works on React 18 or 19.
Quickstart
A minimal player that decodes a clip, draws a waveform you supply, and exposes transport controls:
import {
AudioGraphProvider,
RenderLoopProvider,
useAudioBuffer,
useAudioGraph,
useAudioSource,
usePlaybackTime,
useTransport,
} from "@fieldui/core";
const App = () => (
<RenderLoopProvider>
<AudioGraphProvider>
<Player src="/clip.wav" />
</AudioGraphProvider>
</RenderLoopProvider>
);
const Player = ({ src }: { src: string }) => {
const { masterAnalyser } = useAudioGraph();
const { buffer } = useAudioBuffer(src);
const audioSource = useAudioSource(buffer ? { buffer } : null);
const transport = useTransport({
buffer,
createBufferSource: audioSource.createBufferSource,
connectSource: (node) => masterAnalyser && node.connect(masterAnalyser),
});
const time = usePlaybackTime(transport); // time.timeRef.current is RAF-precise
return (
<div>
<button onClick={transport.play} disabled={transport.state === "playing"}>
Play
</button>
<button onClick={transport.pause}>Pause</button>
<button onClick={transport.stop}>Stop</button>
<span>{transport.currentTime.toFixed(2)}s / {transport.duration.toFixed(2)}s</span>
</div>
);
};The two providers belong as high in the tree as you can put them,
typically right under your app shell. useTransport doesn't know about
your effect chain. It asks for a fresh source via createBufferSource()
and lets you connect it via connectSource, so the same hook fits
chainless rigs, multi-bus rigs, and send/return rigs.
For stream and element sources, useAudioSource auto-connects to
masterAnalyser by default. No boilerplate:
useAudioSource({ stream: micStream }); // wired to masterAnalyser
useAudioSource({ stream, connect: "destination" }); // bypass master chain
useAudioSource({ stream, connect: myCustomNode }); // wire to your node
useAudioSource({ stream, connect: false }); // wire it yourselfBring your own AudioContext
If your app already owns an AudioContext, hand it to the provider:
const ctx = new AudioContext({ latencyHint: "interactive" });
<AudioGraphProvider context={ctx}>{children}</AudioGraphProvider>The provider builds its master chain on your context and does not call
close() on unmount. You keep ownership of the lifecycle.
To customize the default-create path instead:
<AudioGraphProvider contextOptions={{ latencyHint: "playback", sampleRate: 48000 }}>
{children}
</AudioGraphProvider>Memoize or hoist contextOptions. The master chain rebuilds when
latencyHint or sampleRate change.
Hook surface
Grouped by what you have and what you want:
| You have… | You want… | Use |
|-----------------------------|----------------------------------|------------------------------------|
| string URL or File | decoded AudioBuffer | useAudioBuffer |
| string URL | streaming <audio> element | useMediaElement |
| nothing | mic MediaStream | useMediaStream |
| any source | adapt into the audio graph | useAudioSource |
| any source | play / pause / seek state | useTransport |
| transport | precise per-frame time ref | usePlaybackTime |
| AudioBuffer | peak data for a waveform | usePeaks |
| URL of pre-computed peaks | peak data | useRemotePeaks |
| AnalyserNode | freq + time-domain refs | useAnalyser |
| AnalyserNode | peak / RMS / hold refs | useLevelMeter |
| AnalyserNode (live) | peaks built progressively | useWaveformBuilder |
| MediaStream | recorded Blob | useRecorder |
| AudioBuffer | rendered through a chain | useOfflineRender |
| AudioBuffer | WAV / WebM blob | useExport |
| nothing | gain / pan / filter nodes | useGainNode · usePanNode · useFilterNode |
| an array of nodes | connected in series | useNodeChain |
| transport | ARIA + live-region announcements | useAccessibleAudio |
| any | document-level keyboard shortcuts | useAudioKeyboardShortcuts |
| element ref | drag handler with axis lock | usePointerDrag |
| element ref | observed size | useResizeObserver |
| nothing | current device pixel ratio | useDevicePixelRatio |
| input ref + value/min/max | arrow / page / home / end keys | useKeyboardStep |
| region + duration | drag / resize handlers | useRegionDrag |
| nothing | region list state | useRegions |
Plus utilities: formatTime / parseTime (mm:ss, mm:ss.ms, hh:mm:ss,
SMPTE, bars:beats, samples), encodeWav / encodeWavFromChannels /
sliceAudioBuffer, encodePeaks / decodePeaks / resamplePeaks,
downloadBlob.
Architecture in one diagram
useAudioBuffer ──► AudioBuffer
│
▼
useAudioSource ──► createBufferSource() (factory)
│ each play()
▼
useTransport ──► AudioBufferSourceNode ──► your chain (gain→pan→filter…)
│
▼
masterAnalyser ──► masterGain ──► destinationThe master analyser sits before the gain so muting the master output (e.g. for mic-monitor feedback prevention) doesn't silence level meters or spectrograms. Standard pre-fader-listen topology.
For the full architectural narrative (provider lifecycles, hook tiers,
composition walkthroughs for the file player, streaming player, live mic,
and record-then-playback patterns) see
docs/architecture.md.
Sibling packages
| Package | Role |
|---------|------|
| @fieldui/react | Visual components (waveform, transport, meter, spectrogram, scrubber, knob, fader, regions, …) built on this package's hooks. |
| @fieldui/tokens | Design tokens. CSS variables and TypeScript token map. Independent of this package. |
Compatibility
- React 18 or 19 (peer dep on both).
- Modern evergreen browsers. Uses
AudioContext,AnalyserNode,MediaRecorder,OfflineAudioContext,ResizeObserver,matchMedia, Pointer Events, and Web Workers. Worker-offloaded peaks fall back to the main thread whenWorkeris unavailable. - ES2022 / Node ≥ 20 for development.
- SSR-safe. Every hook guards its
window/document/navigatoraccess; nothing executes at module scope. The hooks no-op until they reach a real DOM environment.
License
MIT. © 2026 fieldui.
