Start with hooks, drop to core when the graph is yours.
The React layer owns provider ergonomics. The core layer owns reusable playback primitives.
Hooks first for React apps
Use hooks when playback belongs to React UI state. AudioProvider creates the context lazily, connects masterGain -> analyser -> destination, and lets hooks expose stable play, stop, and isPlaying controls.
import { AudioProvider, useTone } from "@webaudio-kit/react";
function CueButton() {
const cue = useTone({
durationMs: 180,
frequency: 880,
gain: 0.12,
type: "square",
});
return (
<button onClick={() => void cue.play()}>
{cue.isPlaying ? "Restart cue" : "Play cue"}
</button>
);
}
export function App() {
return (
<AudioProvider>
<CueButton />
</AudioProvider>
);
}Core first for non-React and custom graphs
Use @webaudio-kit/core directly when React is not involved or when your application owns the destination node, scheduling, and handle lifecycle.
import { playFrequencySweep, playTone } from "@webaudio-kit/core";
const audioContext = new AudioContext();
const masterGain = audioContext.createGain();
const analyser = audioContext.createAnalyser();
masterGain.gain.value = 0.2;
masterGain.connect(analyser);
analyser.connect(audioContext.destination);
await audioContext.resume();
const tone = playTone(
audioContext,
{ durationMs: 240, frequency: 660, gain: 0.12 },
masterGain,
);
const sweep = playFrequencySweep(
audioContext,
{ durationMs: 700, from: 400, gain: 0.08, to: 1600 },
masterGain,
);
tone.stop();
sweep.stop();React + core interop
In React, call ensureAudioContext() from the user action instead of direct audio.audioContext null checks. It returns the same provider runtime that hooks use.
import { playNoise, playTone } from "@webaudio-kit/core";
import { useAudioContext } from "@webaudio-kit/react";
function LayeredCueButton() {
const audio = useAudioContext();
async function playLayeredCue() {
const runtime = await audio.ensureAudioContext();
const tone = playTone(
runtime.audioContext,
{
durationMs: 180,
envelope: { attackMs: 8, releaseMs: 45 },
frequency: 880,
gain: 0.1,
pattern: { repeat: 2, gapMs: 80 },
type: "square",
},
runtime.masterGain,
);
const noise = playNoise(
runtime.audioContext,
{
durationMs: 120,
envelope: { attackMs: 4, releaseMs: 50 },
gain: 0.025,
type: "pink",
},
runtime.masterGain,
);
setTimeout(() => {
tone.stop();
noise.stop();
}, 700);
}
return (
<>
<button onClick={() => void playLayeredCue()}>Play layered cue</button>
<button onClick={() => audio.stopAll()}>Stop hook playback</button>
</>
);
}Passing runtime.masterGain routes core playback through the provider analyser, so WaveformCanvas, SpectrumCanvas, and master volume still react to the custom cue.
The direct call shape is playTone(runtime.audioContext, options, runtime.masterGain) or playNoise(runtime.audioContext, options, runtime.masterGain).
Decision checklist
The playback belongs to React UI state and you want stable controls, hook cleanup, and provider stopAll().
The app is not React, or you own a custom Web Audio graph and will track every returned handle yourself.
A React screen mostly uses hooks, but one advanced action needs direct playTone, playFrequencySweep, or playNoise.