@juandinella/audio-bands
v0.6.2
Published
Headless browser audio analysis. Get bass/mid/high bands from music or mic, with an optional React hook.
Maintainers
Readme
audio-bands
Demo: audio-bands.juandinella.com
Headless audio analysis for the browser. Read a consistent frame of low/mid/high energy regions, custom named bands, raw FFT bins, and waveform data without shipping a renderer.
const frame = audio.snapshot();
const { bass, mid, high } = frame.bands;
const custom = frame.customBands;
const fft = frame.fft;Why
Most audio libraries either only play audio or immediately draw a canvas for you. This one stays lower level: it gives you usable analysis data and lets you decide how to render it.
The intended center of the API is snapshot(): one call, one coherent analysis frame.
Install
npm install @juandinella/audio-bandsFor repository development, npm test covers the mocked unit suite and npm run test:browser runs a small browser smoke test against the built bundles.
Entry points
@juandinella/audio-bands: main framework-agnostic export@juandinella/audio-bands/core: explicit core-only entry@juandinella/audio-bands/react: React hook
If you use the React hook, install react as well.
Minimal reference examples live in examples/README.md. The Vite app in examples/src/App.tsx is a showcase demo, while examples/snippets/ contains the smallest copyable integrations.
Usage
Vanilla JS
import { AudioBands } from '@juandinella/audio-bands';
const audio = new AudioBands({
music: {
fftSize: 512,
smoothingTimeConstant: 0.7,
},
customBands: {
presence: { from: 0.25, to: 0.5 },
air: { from: 0.5, to: 1 },
},
onLoadError: (error) => console.error('track error', error),
onMicError: (error) => console.error('mic error', error),
});
await audio.load('/track.mp3');
await audio.play();
function loop() {
const frame = audio.snapshot();
const { bass, mid, high, overall } = frame.bands;
const custom = frame.customBands;
const fft = frame.fft;
const waveform = frame.waveform;
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);React hook
import { useAudioBands } from '@juandinella/audio-bands/react';
function Visualizer() {
const {
isPlaying,
hasTrack,
loadError,
playbackError,
micError,
loadTrack,
play,
pause,
setLoop,
seek,
getDuration,
getCurrentTime,
snapshot,
togglePlayPause,
toggleMic,
getBands,
getCustomBands,
} = useAudioBands({
customBands: {
presence: { from: 0.25, to: 0.5 },
},
});
const frame = snapshot();
return (
<>
<button onClick={() => loadTrack('/track.mp3')}>load</button>
<button onClick={play}>play</button>
<button onClick={pause}>pause</button>
<button onClick={() => setLoop(true)}>loop</button>
<button onClick={() => seek(30)}>seek 0:30</button>
<button onClick={togglePlayPause}>toggle</button>
<button onClick={toggleMic}>Toggle mic</button>
<pre>{JSON.stringify({
hasTrack,
loadError,
playbackError,
micError,
duration: getDuration(),
currentTime: getCurrentTime(),
...frame.bands,
...frame.customBands,
}, null, 2)}</pre>
</>
);
}Mic input
await audio.enableMic();
const micBands = audio.getBands('mic');
const micCustomBands = audio.getCustomBands('mic');
const waveform = audio.getWaveform('mic');When To Use Bands Vs FFT
What bass, mid, high, and overall Mean
getBands() returns three coarse analyser regions plus a convenience summary value:
bass,mid, andhighare normalized slices of the analyser spectrum, not fixed acoustic bands in Hz- the default split is percentage-based (
0-0.08,0.08-0.4,0.4-1) over the available FFT bins - those regions therefore depend on analyser resolution and the underlying audio context sample rate
overallis a UI-oriented weighted summary (bass * 0.5 + mid * 0.3 + high * 0.2), not a perceptual loudness metric
Use these values as stable control signals for interaction and motion. If you need tighter semantic control, define customBands. If you need physically meaningful bin-level data, use getFftData() or snapshot().
Use snapshot() first when you need a full analysis frame:
- read
bands,customBands,fft, andwaveformtogether - avoid multiple analyser reads in one render loop
- keep derived values synchronized
Use getBands() when you only want stable, simple control signals:
- pulsing a blob with low-end energy
- scaling UI based on overall intensity
- animating typography or CSS variables
- driving scenes where three broad zones are enough
Use getCustomBands() when the default bass/mid/high split is too coarse, but you still want named, high-level buckets:
- separate
presence,air, orsub - tune bands to your own design system or animation logic
- keep your render code semantic instead of index-based
Use getFftData() when you need bin-level detail:
- bar visualizers
- line spectrums
- log interpolation
- any renderer that maps directly over bins
Rule of thumb:
getBands()for product UIgetCustomBands()for art directiongetFftData()for visualizers
API
AudioBands
new AudioBands(options?: AudioBandsOptions)Methods
| Method | Description |
| ----------------------- | ----------- |
| load(url) | Load a track, connect it to the analyser, and resolve when the media is ready. Rejects with AudioBandsError on load failure. |
| play() | Start playback for the current track. Rejects with AudioBandsError on failure. |
| pause() | Pause the current track. |
| setLoop(loop) | Set whether the current and future loaded tracks should loop. |
| seek(seconds) | Seek the current track to a given time in seconds. |
| getDuration() | Returns the current track duration in seconds, or null when unavailable. |
| getCurrentTime() | Returns the current playback time in seconds, or null when unavailable. |
| togglePlayPause() | Toggle the current track. Returns a promise and propagates playback errors when toggling into play. |
| enableMic() | Request microphone access and start mic analysis. Rejects with AudioBandsError on failure. |
| disableMic() | Stop mic input and clean up the stream. |
| snapshot(source?) | Returns { bands, customBands, fft, waveform } from a single analyser read. |
| getBands(source?) | Returns normalized analyser-region energy { bass, mid, high, overall }. |
| getCustomBands(source?) | Returns normalized values for configured custom bands. |
| getFftData(source?) | Returns raw Uint8Array frequency bins. |
| getWaveform(source?) | Returns raw time-domain data for 'music' or 'mic'. |
| getState() | Returns the current playback/mic/error state. |
| destroy() | Stop playback, release the mic and close the AudioContext. |
useAudioBands()
const {
isPlaying,
micActive,
hasTrack,
audioError,
loadError,
playbackError,
micError,
state,
loadTrack,
play,
pause,
setLoop,
seek,
getDuration,
getCurrentTime,
togglePlayPause,
toggleMic,
snapshot,
getBands,
getCustomBands,
getFftData,
getWaveform,
} = useAudioBands(options);AudioBandsOptions
type AudioBandsOptions = {
music?: {
fftSize?: number;
smoothingTimeConstant?: number;
};
mic?: {
fftSize?: number;
smoothingTimeConstant?: number;
};
bandRanges?: {
bass?: { from: number; to: number };
mid?: { from: number; to: number };
high?: { from: number; to: number };
};
customBands?: Record<string, { from: number; to: number }>;
onError?: (error: AudioBandsError) => void;
onLoadError?: (error: AudioBandsError) => void;
onPlaybackError?: (error: AudioBandsError) => void;
onMicError?: (error: AudioBandsError) => void;
onStateChange?: (state: AudioBandsState) => void;
onPlay?: () => void;
onPause?: () => void;
onMicStart?: () => void;
onMicStop?: () => void;
};AudioBandsState
type AudioBandsState = {
isPlaying: boolean;
micActive: boolean;
hasTrack: boolean; // a track source is assigned, even if playback later fails
loadError: AudioBandsError | null;
playbackError: AudioBandsError | null;
micError: AudioBandsError | null;
};Notes
AudioContextis created lazily on the first call toload()orenableMic().load()prepares the current track but does not start playback. It resolves only after the media is ready enough for duration/seek reads to be meaningful, then you can callplay()ortogglePlayPause().togglePlayPause()follows the same playback error contract asplay(): if toggling into play fails, the returned promise rejects.hasTrackmeans the current track finished loading and is ready on the instance. It can still betrueifplay()fails later due to autoplay policy or another playback error.isPlayingfollows the underlying media element events, so it falls back tofalsewhen the track pauses or reachesended.loadErrorstores track loading failures only.playbackErrorstores playback failures for the current track, such as autoplay-policy rejections.- In the React hook, changing
music,mic,bandRanges, orcustomBandsrecreates the underlyingAudioBandsinstance. - The mic analyser is not connected to
AudioContext.destination, so it will not feed back into the speakers. snapshot()is the preferred way to read analysis insiderequestAnimationFrame.getBands(),getCustomBands(),getFftData(), andgetWaveform()are convenience reads when you only need one view of the current frame.getFftData()returns the same underlying buffer on each call. Copy it if you need frame-to-frame comparisons.fftSizemust be a power of two between32and32768.- Band ranges are normalized from
0to1, where0is the start of the analyser spectrum and1is the end. - The default
bass/mid/highlabels are convenience names for analyser regions, not fixed Hz buckets. overallis intended as a simple UI summary, not as an acoustically weighted loudness value.
Development
npm testbuilds the package and runs the unit suite.- Releases are published from GitHub Actions when a
v*tag is pushed.
License
MIT
