@juandinella/audio-bands 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -15
- package/dist/{chunk-33JHLQZJ.js → chunk-BA6JOXL4.js} +66 -19
- package/dist/chunk-BA6JOXL4.js.map +1 -0
- package/dist/core-entry.cjs +65 -18
- package/dist/core-entry.cjs.map +1 -1
- package/dist/core-entry.d.cts +12 -4
- package/dist/core-entry.d.ts +12 -4
- package/dist/core-entry.js +1 -1
- package/dist/index.cjs +65 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react-entry.cjs +170 -49
- package/dist/react-entry.cjs.map +1 -1
- package/dist/react-entry.d.cts +12 -4
- package/dist/react-entry.d.ts +12 -4
- package/dist/react-entry.js +106 -32
- package/dist/react-entry.js.map +1 -1
- package/dist/{types-CiYwsfgy.d.cts → types-0KQJLMV2.d.cts} +10 -2
- package/dist/{types-CiYwsfgy.d.ts → types-0KQJLMV2.d.ts} +10 -2
- package/package.json +5 -1
- package/dist/chunk-33JHLQZJ.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,18 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
**Demo**: [audio-bands.juandinella.com](https://audio-bands.juandinella.com)
|
|
6
6
|
|
|
7
|
-
Headless audio analysis for the browser.
|
|
7
|
+
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.
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
10
|
+
const frame = audio.snapshot();
|
|
11
|
+
const { bass, mid, high } = frame.bands;
|
|
12
|
+
const custom = frame.customBands;
|
|
13
|
+
const fft = frame.fft;
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
## Why
|
|
16
17
|
|
|
17
18
|
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.
|
|
18
19
|
|
|
20
|
+
The intended center of the API is `snapshot()`: one call, one coherent analysis frame.
|
|
21
|
+
|
|
19
22
|
## Install
|
|
20
23
|
|
|
21
24
|
```bash
|
|
@@ -51,12 +54,14 @@ const audio = new AudioBands({
|
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
await audio.load('/track.mp3');
|
|
57
|
+
await audio.play();
|
|
54
58
|
|
|
55
59
|
function loop() {
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
+
const frame = audio.snapshot();
|
|
61
|
+
const { bass, mid, high, overall } = frame.bands;
|
|
62
|
+
const custom = frame.customBands;
|
|
63
|
+
const fft = frame.fft;
|
|
64
|
+
const waveform = frame.waveform;
|
|
60
65
|
|
|
61
66
|
requestAnimationFrame(loop);
|
|
62
67
|
}
|
|
@@ -74,8 +79,16 @@ function Visualizer() {
|
|
|
74
79
|
isPlaying,
|
|
75
80
|
hasTrack,
|
|
76
81
|
loadError,
|
|
82
|
+
playbackError,
|
|
77
83
|
micError,
|
|
78
84
|
loadTrack,
|
|
85
|
+
play,
|
|
86
|
+
pause,
|
|
87
|
+
setLoop,
|
|
88
|
+
seek,
|
|
89
|
+
getDuration,
|
|
90
|
+
getCurrentTime,
|
|
91
|
+
snapshot,
|
|
79
92
|
togglePlayPause,
|
|
80
93
|
toggleMic,
|
|
81
94
|
getBands,
|
|
@@ -86,12 +99,27 @@ function Visualizer() {
|
|
|
86
99
|
},
|
|
87
100
|
});
|
|
88
101
|
|
|
102
|
+
const frame = snapshot();
|
|
103
|
+
|
|
89
104
|
return (
|
|
90
105
|
<>
|
|
91
106
|
<button onClick={() => loadTrack('/track.mp3')}>load</button>
|
|
92
|
-
<button onClick={
|
|
107
|
+
<button onClick={play}>play</button>
|
|
108
|
+
<button onClick={pause}>pause</button>
|
|
109
|
+
<button onClick={() => setLoop(true)}>loop</button>
|
|
110
|
+
<button onClick={() => seek(30)}>seek 0:30</button>
|
|
111
|
+
<button onClick={togglePlayPause}>toggle</button>
|
|
93
112
|
<button onClick={toggleMic}>Toggle mic</button>
|
|
94
|
-
<pre>{JSON.stringify({
|
|
113
|
+
<pre>{JSON.stringify({
|
|
114
|
+
hasTrack,
|
|
115
|
+
loadError,
|
|
116
|
+
playbackError,
|
|
117
|
+
micError,
|
|
118
|
+
duration: getDuration(),
|
|
119
|
+
currentTime: getCurrentTime(),
|
|
120
|
+
...frame.bands,
|
|
121
|
+
...frame.customBands,
|
|
122
|
+
}, null, 2)}</pre>
|
|
95
123
|
</>
|
|
96
124
|
);
|
|
97
125
|
}
|
|
@@ -109,7 +137,24 @@ const waveform = audio.getWaveform('mic');
|
|
|
109
137
|
|
|
110
138
|
## When To Use Bands Vs FFT
|
|
111
139
|
|
|
112
|
-
|
|
140
|
+
## What `bass`, `mid`, `high`, and `overall` Mean
|
|
141
|
+
|
|
142
|
+
`getBands()` returns three coarse analyser regions plus a convenience summary value:
|
|
143
|
+
|
|
144
|
+
- `bass`, `mid`, and `high` are normalized slices of the analyser spectrum, not fixed acoustic bands in Hz
|
|
145
|
+
- the default split is percentage-based (`0-0.08`, `0.08-0.4`, `0.4-1`) over the available FFT bins
|
|
146
|
+
- those regions therefore depend on analyser resolution and the underlying audio context sample rate
|
|
147
|
+
- `overall` is a UI-oriented weighted summary (`bass * 0.5 + mid * 0.3 + high * 0.2`), not a perceptual loudness metric
|
|
148
|
+
|
|
149
|
+
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()`.
|
|
150
|
+
|
|
151
|
+
Use `snapshot()` first when you need a full analysis frame:
|
|
152
|
+
|
|
153
|
+
- read `bands`, `customBands`, `fft`, and `waveform` together
|
|
154
|
+
- avoid multiple analyser reads in one render loop
|
|
155
|
+
- keep derived values synchronized
|
|
156
|
+
|
|
157
|
+
Use `getBands()` when you only want stable, simple control signals:
|
|
113
158
|
|
|
114
159
|
- pulsing a blob with low-end energy
|
|
115
160
|
- scaling UI based on overall intensity
|
|
@@ -147,11 +192,18 @@ new AudioBands(options?: AudioBandsOptions)
|
|
|
147
192
|
|
|
148
193
|
| Method | Description |
|
|
149
194
|
| ----------------------- | ----------- |
|
|
150
|
-
| `load(url)` | Load and
|
|
151
|
-
| `
|
|
195
|
+
| `load(url)` | Load a track and connect it to the analyser. Rejects with `AudioBandsError` on failure. |
|
|
196
|
+
| `play()` | Start playback for the current track. Rejects with `AudioBandsError` on failure. |
|
|
197
|
+
| `pause()` | Pause the current track. |
|
|
198
|
+
| `setLoop(loop)` | Set whether the current and future loaded tracks should loop. |
|
|
199
|
+
| `seek(seconds)` | Seek the current track to a given time in seconds. |
|
|
200
|
+
| `getDuration()` | Returns the current track duration in seconds, or `null` when unavailable. |
|
|
201
|
+
| `getCurrentTime()` | Returns the current playback time in seconds, or `null` when unavailable. |
|
|
202
|
+
| `togglePlayPause()` | Toggle the current track. Returns a promise and propagates playback errors when toggling into play. |
|
|
152
203
|
| `enableMic()` | Request microphone access and start mic analysis. Rejects with `AudioBandsError` on failure. |
|
|
153
204
|
| `disableMic()` | Stop mic input and clean up the stream. |
|
|
154
|
-
| `
|
|
205
|
+
| `snapshot(source?)` | Returns `{ bands, customBands, fft, waveform }` from a single analyser read. |
|
|
206
|
+
| `getBands(source?)` | Returns normalized analyser-region energy `{ bass, mid, high, overall }`. |
|
|
155
207
|
| `getCustomBands(source?)` | Returns normalized values for configured custom bands. |
|
|
156
208
|
| `getFftData(source?)` | Returns raw `Uint8Array` frequency bins. |
|
|
157
209
|
| `getWaveform(source?)` | Returns raw time-domain data for `'music'` or `'mic'`. |
|
|
@@ -167,11 +219,19 @@ const {
|
|
|
167
219
|
hasTrack,
|
|
168
220
|
audioError,
|
|
169
221
|
loadError,
|
|
222
|
+
playbackError,
|
|
170
223
|
micError,
|
|
171
224
|
state,
|
|
172
225
|
loadTrack,
|
|
226
|
+
play,
|
|
227
|
+
pause,
|
|
228
|
+
setLoop,
|
|
229
|
+
seek,
|
|
230
|
+
getDuration,
|
|
231
|
+
getCurrentTime,
|
|
173
232
|
togglePlayPause,
|
|
174
233
|
toggleMic,
|
|
234
|
+
snapshot,
|
|
175
235
|
getBands,
|
|
176
236
|
getCustomBands,
|
|
177
237
|
getFftData,
|
|
@@ -199,6 +259,7 @@ type AudioBandsOptions = {
|
|
|
199
259
|
customBands?: Record<string, { from: number; to: number }>;
|
|
200
260
|
onError?: (error: AudioBandsError) => void;
|
|
201
261
|
onLoadError?: (error: AudioBandsError) => void;
|
|
262
|
+
onPlaybackError?: (error: AudioBandsError) => void;
|
|
202
263
|
onMicError?: (error: AudioBandsError) => void;
|
|
203
264
|
onStateChange?: (state: AudioBandsState) => void;
|
|
204
265
|
onPlay?: () => void;
|
|
@@ -216,6 +277,7 @@ type AudioBandsState = {
|
|
|
216
277
|
micActive: boolean;
|
|
217
278
|
hasTrack: boolean; // a track source is assigned, even if playback later fails
|
|
218
279
|
loadError: AudioBandsError | null;
|
|
280
|
+
playbackError: AudioBandsError | null;
|
|
219
281
|
micError: AudioBandsError | null;
|
|
220
282
|
};
|
|
221
283
|
```
|
|
@@ -223,12 +285,20 @@ type AudioBandsState = {
|
|
|
223
285
|
## Notes
|
|
224
286
|
|
|
225
287
|
- `AudioContext` is created lazily on the first call to `load()` or `enableMic()`.
|
|
288
|
+
- `load()` prepares the current track but does not start playback. Call `play()` or `togglePlayPause()` after loading.
|
|
289
|
+
- `togglePlayPause()` follows the same playback error contract as `play()`: if toggling into play fails, the returned promise rejects.
|
|
226
290
|
- `hasTrack` means a track source is currently assigned to the instance. It can still be `true` if `play()` fails due to autoplay policy or another playback error.
|
|
291
|
+
- `loadError` stores track loading failures only.
|
|
292
|
+
- `playbackError` stores playback failures for the current track, such as autoplay-policy rejections.
|
|
293
|
+
- In the React hook, changing `music`, `mic`, `bandRanges`, or `customBands` recreates the underlying `AudioBands` instance.
|
|
227
294
|
- The mic analyser is not connected to `AudioContext.destination`, so it will not feed back into the speakers.
|
|
228
|
-
- `
|
|
295
|
+
- `snapshot()` is the preferred way to read analysis inside `requestAnimationFrame`.
|
|
296
|
+
- `getBands()`, `getCustomBands()`, `getFftData()`, and `getWaveform()` are convenience reads when you only need one view of the current frame.
|
|
229
297
|
- `getFftData()` returns the same underlying buffer on each call. Copy it if you need frame-to-frame comparisons.
|
|
230
298
|
- `fftSize` must be a power of two between `32` and `32768`.
|
|
231
299
|
- Band ranges are normalized from `0` to `1`, where `0` is the start of the analyser spectrum and `1` is the end.
|
|
300
|
+
- The default `bass` / `mid` / `high` labels are convenience names for analyser regions, not fixed Hz buckets.
|
|
301
|
+
- `overall` is intended as a simple UI summary, not as an acoustically weighted loudness value.
|
|
232
302
|
|
|
233
303
|
## License
|
|
234
304
|
|
|
@@ -114,6 +114,7 @@ var AudioBands = class {
|
|
|
114
114
|
micActive: false,
|
|
115
115
|
hasTrack: false,
|
|
116
116
|
loadError: null,
|
|
117
|
+
playbackError: null,
|
|
117
118
|
micError: null
|
|
118
119
|
};
|
|
119
120
|
this.ctx = null;
|
|
@@ -127,6 +128,7 @@ var AudioBands = class {
|
|
|
127
128
|
this.musicSource = null;
|
|
128
129
|
this.micSource = null;
|
|
129
130
|
this.micStream = null;
|
|
131
|
+
this.trackLoop = false;
|
|
130
132
|
this.destroyed = false;
|
|
131
133
|
this.options = options;
|
|
132
134
|
this.musicConfig = normalizeAnalyserConfig(options.music, DEFAULT_MUSIC_ANALYSER);
|
|
@@ -147,41 +149,69 @@ var AudioBands = class {
|
|
|
147
149
|
try {
|
|
148
150
|
ctx = this.ensureCtx();
|
|
149
151
|
} catch (error) {
|
|
150
|
-
throw this.handleError("load", error);
|
|
152
|
+
throw this.handleError("load", error, "load_error");
|
|
151
153
|
}
|
|
152
154
|
this.teardownMusic();
|
|
153
155
|
const audio = new Audio();
|
|
154
156
|
audio.crossOrigin = "anonymous";
|
|
155
157
|
audio.src = url;
|
|
156
|
-
audio.loop =
|
|
158
|
+
audio.loop = this.trackLoop;
|
|
157
159
|
this.audioEl = audio;
|
|
158
|
-
this.setState({ hasTrack: true, loadError: null });
|
|
160
|
+
this.setState({ hasTrack: true, loadError: null, playbackError: null });
|
|
159
161
|
const source = ctx.createMediaElementSource(audio);
|
|
160
162
|
source.connect(this.musicAnalyser);
|
|
161
163
|
this.musicSource = source;
|
|
164
|
+
}
|
|
165
|
+
async play() {
|
|
166
|
+
const audio = this.audioEl;
|
|
167
|
+
if (!audio) return;
|
|
162
168
|
try {
|
|
163
169
|
await audio.play();
|
|
164
|
-
this.setState({ isPlaying: true,
|
|
170
|
+
this.setState({ isPlaying: true, playbackError: null });
|
|
165
171
|
this.options.onPlay?.();
|
|
166
172
|
} catch (error) {
|
|
167
|
-
throw this.handleError("
|
|
173
|
+
throw this.handleError("playback", error, "playback_error");
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
|
-
|
|
176
|
+
pause() {
|
|
177
|
+
const audio = this.audioEl;
|
|
178
|
+
if (!audio || audio.paused) return;
|
|
179
|
+
audio.pause();
|
|
180
|
+
this.setState({ isPlaying: false });
|
|
181
|
+
this.options.onPause?.();
|
|
182
|
+
}
|
|
183
|
+
async togglePlayPause() {
|
|
171
184
|
const audio = this.audioEl;
|
|
172
185
|
if (!audio) return;
|
|
173
186
|
if (audio.paused) {
|
|
174
|
-
|
|
175
|
-
this.setState({ isPlaying: true, loadError: null });
|
|
176
|
-
this.options.onPlay?.();
|
|
177
|
-
}).catch((error) => {
|
|
178
|
-
this.handleError("load", error, "playback_error");
|
|
179
|
-
});
|
|
187
|
+
await this.play();
|
|
180
188
|
return;
|
|
181
189
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
this.pause();
|
|
191
|
+
}
|
|
192
|
+
setLoop(loop) {
|
|
193
|
+
this.trackLoop = loop;
|
|
194
|
+
if (this.audioEl) this.audioEl.loop = loop;
|
|
195
|
+
}
|
|
196
|
+
seek(seconds) {
|
|
197
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
198
|
+
throw new AudioBandsError(
|
|
199
|
+
"config",
|
|
200
|
+
"invalid_config",
|
|
201
|
+
"seek time must be a finite number greater than or equal to 0"
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (!this.audioEl) return;
|
|
205
|
+
const duration = Number.isFinite(this.audioEl.duration) ? this.audioEl.duration : null;
|
|
206
|
+
this.audioEl.currentTime = duration === null ? seconds : Math.min(seconds, duration);
|
|
207
|
+
}
|
|
208
|
+
getDuration() {
|
|
209
|
+
if (!this.audioEl) return null;
|
|
210
|
+
return Number.isFinite(this.audioEl.duration) ? this.audioEl.duration : null;
|
|
211
|
+
}
|
|
212
|
+
getCurrentTime() {
|
|
213
|
+
if (!this.audioEl) return null;
|
|
214
|
+
return Number.isFinite(this.audioEl.currentTime) ? this.audioEl.currentTime : null;
|
|
185
215
|
}
|
|
186
216
|
async enableMic() {
|
|
187
217
|
let ctx;
|
|
@@ -240,6 +270,16 @@ var AudioBands = class {
|
|
|
240
270
|
getWaveform(source = "music") {
|
|
241
271
|
return this.readWaveformData(source);
|
|
242
272
|
}
|
|
273
|
+
snapshot(source = "music") {
|
|
274
|
+
const fft = this.readFrequencyData(source);
|
|
275
|
+
const waveform = this.readWaveformData(source);
|
|
276
|
+
return {
|
|
277
|
+
bands: fft ? computeBands(fft, this.classicRanges) : { ...ZERO },
|
|
278
|
+
customBands: fft ? computeCustomBands(fft, this.customBandRanges) : computeCustomBands(new Uint8Array(1), this.customBandRanges),
|
|
279
|
+
fft,
|
|
280
|
+
waveform
|
|
281
|
+
};
|
|
282
|
+
}
|
|
243
283
|
destroy() {
|
|
244
284
|
if (this.destroyed) return;
|
|
245
285
|
this.teardownMusic();
|
|
@@ -307,16 +347,19 @@ var AudioBands = class {
|
|
|
307
347
|
analyser.smoothingTimeConstant = config.smoothingTimeConstant;
|
|
308
348
|
return analyser;
|
|
309
349
|
}
|
|
310
|
-
handleError(kind, error, fallbackCode = kind === "mic" ? "mic_error" : "load_error") {
|
|
350
|
+
handleError(kind, error, fallbackCode = kind === "mic" ? "mic_error" : kind === "playback" ? "playback_error" : "load_error") {
|
|
311
351
|
const wrapped = error instanceof AudioBandsError ? error : new AudioBandsError(
|
|
312
352
|
kind,
|
|
313
353
|
fallbackCode,
|
|
314
|
-
kind === "mic" ? "Failed to access microphone input" : "Failed to
|
|
354
|
+
kind === "mic" ? "Failed to access microphone input" : kind === "playback" ? "Failed to play audio track" : "Failed to load audio track",
|
|
315
355
|
error
|
|
316
356
|
);
|
|
317
357
|
if (kind === "load") {
|
|
318
358
|
this.setState({ isPlaying: false, loadError: wrapped });
|
|
319
359
|
this.options.onLoadError?.(wrapped);
|
|
360
|
+
} else if (kind === "playback") {
|
|
361
|
+
this.setState({ isPlaying: false, playbackError: wrapped });
|
|
362
|
+
this.options.onPlaybackError?.(wrapped);
|
|
320
363
|
} else {
|
|
321
364
|
this.setState({ micActive: false, micError: wrapped });
|
|
322
365
|
this.options.onMicError?.(wrapped);
|
|
@@ -347,7 +390,11 @@ var AudioBands = class {
|
|
|
347
390
|
}
|
|
348
391
|
this.musicSource = null;
|
|
349
392
|
this.musicWaveformData = this.musicAnalyser ? new Uint8Array(this.musicAnalyser.fftSize) : null;
|
|
350
|
-
this.setState({
|
|
393
|
+
this.setState({
|
|
394
|
+
isPlaying: false,
|
|
395
|
+
hasTrack: false,
|
|
396
|
+
playbackError: null
|
|
397
|
+
});
|
|
351
398
|
}
|
|
352
399
|
};
|
|
353
400
|
|
|
@@ -355,4 +402,4 @@ export {
|
|
|
355
402
|
AudioBandsError,
|
|
356
403
|
AudioBands
|
|
357
404
|
};
|
|
358
|
-
//# sourceMappingURL=chunk-
|
|
405
|
+
//# sourceMappingURL=chunk-BA6JOXL4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/core.ts"],"sourcesContent":["import type { AudioBandsErrorCode, AudioBandsErrorKind } from './types';\n\nexport class AudioBandsError extends Error {\n readonly kind: AudioBandsErrorKind;\n readonly code: AudioBandsErrorCode;\n readonly cause?: unknown;\n\n constructor(\n kind: AudioBandsErrorKind,\n code: AudioBandsErrorCode,\n message: string,\n cause?: unknown,\n ) {\n super(message);\n this.name = 'AudioBandsError';\n this.kind = kind;\n this.code = code;\n this.cause = cause;\n }\n}\n","import { AudioBandsError } from './errors';\nimport type {\n AudioAnalyserConfig,\n AudioBandsOptions,\n AudioBandsSnapshot,\n AudioBandsState,\n AudioSource,\n BandRange,\n Bands,\n ClassicBandRanges,\n CustomBandRanges,\n} from './types';\n\nconst DEFAULT_MUSIC_ANALYSER: Required<AudioAnalyserConfig> = {\n fftSize: 256,\n smoothingTimeConstant: 0.85,\n};\n\nconst DEFAULT_MIC_ANALYSER: Required<AudioAnalyserConfig> = {\n fftSize: 256,\n smoothingTimeConstant: 0.8,\n};\n\nconst DEFAULT_CLASSIC_RANGES: Record<keyof Omit<Bands, 'overall'>, BandRange> = {\n bass: { from: 0, to: 0.08 },\n mid: { from: 0.08, to: 0.4 },\n high: { from: 0.4, to: 1 },\n};\n\nconst ZERO: Bands = { bass: 0, mid: 0, high: 0, overall: 0 };\n\nfunction avg(arr: Uint8Array<ArrayBuffer>, from: number, to: number): number {\n let sum = 0;\n for (let i = from; i < to; i++) sum += arr[i];\n return sum / (to - from);\n}\n\nfunction isPowerOfTwo(value: number): boolean {\n return (value & (value - 1)) === 0;\n}\n\nfunction normalizeAnalyserConfig(\n config: AudioAnalyserConfig | undefined,\n fallback: Required<AudioAnalyserConfig>,\n): Required<AudioAnalyserConfig> {\n const fftSize = config?.fftSize ?? fallback.fftSize;\n const smoothingTimeConstant =\n config?.smoothingTimeConstant ?? fallback.smoothingTimeConstant;\n\n if (\n !Number.isInteger(fftSize) ||\n fftSize < 32 ||\n fftSize > 32768 ||\n !isPowerOfTwo(fftSize)\n ) {\n throw new AudioBandsError(\n 'config',\n 'invalid_config',\n 'fftSize must be a power of two between 32 and 32768',\n );\n }\n\n if (\n typeof smoothingTimeConstant !== 'number' ||\n smoothingTimeConstant < 0 ||\n smoothingTimeConstant > 1\n ) {\n throw new AudioBandsError(\n 'config',\n 'invalid_config',\n 'smoothingTimeConstant must be between 0 and 1',\n );\n }\n\n return { fftSize, smoothingTimeConstant };\n}\n\nfunction normalizeRange(name: string, range: BandRange | undefined): BandRange {\n const normalized = range ?? DEFAULT_CLASSIC_RANGES[name as keyof typeof DEFAULT_CLASSIC_RANGES];\n\n if (\n typeof normalized?.from !== 'number' ||\n typeof normalized?.to !== 'number' ||\n normalized.from < 0 ||\n normalized.to > 1 ||\n normalized.from >= normalized.to\n ) {\n throw new AudioBandsError(\n 'config',\n 'invalid_config',\n `Band range \"${name}\" must satisfy 0 <= from < to <= 1`,\n );\n }\n\n return normalized;\n}\n\nfunction normalizeClassicRanges(\n ranges: ClassicBandRanges | undefined,\n): Record<keyof Omit<Bands, 'overall'>, BandRange> {\n return {\n bass: normalizeRange('bass', ranges?.bass),\n mid: normalizeRange('mid', ranges?.mid),\n high: normalizeRange('high', ranges?.high),\n };\n}\n\nfunction normalizeCustomBands(customBands: CustomBandRanges | undefined): CustomBandRanges {\n if (!customBands) return {};\n\n return Object.fromEntries(\n Object.entries(customBands).map(([name, range]) => [name, normalizeRange(name, range)]),\n );\n}\n\nfunction getIndexes(len: number, range: BandRange): [number, number] {\n const from = Math.max(0, Math.min(len - 1, Math.floor(len * range.from)));\n const to = Math.max(from + 1, Math.min(len, Math.floor(len * range.to)));\n return [from, to];\n}\n\nfunction getRangeValue(data: Uint8Array<ArrayBuffer>, range: BandRange): number {\n const [from, to] = getIndexes(data.length, range);\n return avg(data, from, to) / 255;\n}\n\nfunction fillFrequencyData(\n analyser: AnalyserNode,\n data: Uint8Array<ArrayBuffer>,\n): Uint8Array<ArrayBuffer> {\n analyser.getByteFrequencyData(data);\n return data;\n}\n\nfunction computeBands(\n data: Uint8Array<ArrayBuffer>,\n ranges: Record<keyof Omit<Bands, 'overall'>, BandRange>,\n): Bands {\n const bass = getRangeValue(data, ranges.bass);\n const mid = getRangeValue(data, ranges.mid);\n const high = getRangeValue(data, ranges.high);\n\n return {\n bass,\n mid,\n high,\n overall: bass * 0.5 + mid * 0.3 + high * 0.2,\n };\n}\n\nfunction computeCustomBands(\n data: Uint8Array<ArrayBuffer>,\n ranges: CustomBandRanges,\n): Record<string, number> {\n return Object.fromEntries(\n Object.entries(ranges).map(([name, range]) => [name, getRangeValue(data, range)]),\n );\n}\n\nfunction cloneState(state: AudioBandsState): AudioBandsState {\n return { ...state };\n}\n\n/**\n * Vanilla JS class — no framework dependency.\n * Works in React, Vue, Svelte, or plain HTML.\n */\nexport class AudioBands {\n private options: AudioBandsOptions;\n private readonly musicConfig: Required<AudioAnalyserConfig>;\n private readonly micConfig: Required<AudioAnalyserConfig>;\n private readonly classicRanges: Record<keyof Omit<Bands, 'overall'>, BandRange>;\n private readonly customBandRanges: CustomBandRanges;\n\n private readonly state: AudioBandsState = {\n isPlaying: false,\n micActive: false,\n hasTrack: false,\n loadError: null,\n playbackError: null,\n micError: null,\n };\n\n private ctx: AudioContext | null = null;\n private musicAnalyser: AnalyserNode | null = null;\n private musicData: Uint8Array<ArrayBuffer> | null = null;\n private musicWaveformData: Uint8Array<ArrayBuffer> | null = null;\n private micAnalyser: AnalyserNode | null = null;\n private micData: Uint8Array<ArrayBuffer> | null = null;\n private micWaveformData: Uint8Array<ArrayBuffer> | null = null;\n private audioEl: HTMLAudioElement | null = null;\n private musicSource: MediaElementAudioSourceNode | null = null;\n private micSource: MediaStreamAudioSourceNode | null = null;\n private micStream: MediaStream | null = null;\n private trackLoop = false;\n private destroyed = false;\n\n constructor(options: AudioBandsOptions = {}) {\n this.options = options;\n this.musicConfig = normalizeAnalyserConfig(options.music, DEFAULT_MUSIC_ANALYSER);\n this.micConfig = normalizeAnalyserConfig(options.mic, DEFAULT_MIC_ANALYSER);\n this.classicRanges = normalizeClassicRanges(options.bandRanges);\n this.customBandRanges = normalizeCustomBands(options.customBands);\n }\n\n getState(): AudioBandsState {\n return cloneState(this.state);\n }\n\n getCustomBands(source: AudioSource = 'music'): Record<string, number> {\n const data = this.readFrequencyData(source);\n if (!data) return computeCustomBands(new Uint8Array(1) as Uint8Array<ArrayBuffer>, this.customBandRanges);\n return computeCustomBands(data, this.customBandRanges);\n }\n\n async load(url: string): Promise<void> {\n let ctx: AudioContext;\n try {\n ctx = this.ensureCtx();\n } catch (error) {\n throw this.handleError('load', error, 'load_error');\n }\n\n this.teardownMusic();\n\n const audio = new Audio();\n audio.crossOrigin = 'anonymous';\n audio.src = url;\n audio.loop = this.trackLoop;\n this.audioEl = audio;\n this.setState({ hasTrack: true, loadError: null, playbackError: null });\n\n const source = ctx.createMediaElementSource(audio);\n source.connect(this.musicAnalyser!);\n this.musicSource = source;\n }\n\n async play(): Promise<void> {\n const audio = this.audioEl;\n if (!audio) return;\n\n try {\n await audio.play();\n this.setState({ isPlaying: true, playbackError: null });\n this.options.onPlay?.();\n } catch (error) {\n throw this.handleError('playback', error, 'playback_error');\n }\n }\n\n pause(): void {\n const audio = this.audioEl;\n if (!audio || audio.paused) return;\n\n audio.pause();\n this.setState({ isPlaying: false });\n this.options.onPause?.();\n }\n\n async togglePlayPause(): Promise<void> {\n const audio = this.audioEl;\n if (!audio) return;\n\n if (audio.paused) {\n await this.play();\n return;\n }\n\n this.pause();\n }\n\n setLoop(loop: boolean): void {\n this.trackLoop = loop;\n if (this.audioEl) this.audioEl.loop = loop;\n }\n\n seek(seconds: number): void {\n if (!Number.isFinite(seconds) || seconds < 0) {\n throw new AudioBandsError(\n 'config',\n 'invalid_config',\n 'seek time must be a finite number greater than or equal to 0',\n );\n }\n\n if (!this.audioEl) return;\n\n const duration = Number.isFinite(this.audioEl.duration) ? this.audioEl.duration : null;\n this.audioEl.currentTime = duration === null ? seconds : Math.min(seconds, duration);\n }\n\n getDuration(): number | null {\n if (!this.audioEl) return null;\n return Number.isFinite(this.audioEl.duration) ? this.audioEl.duration : null;\n }\n\n getCurrentTime(): number | null {\n if (!this.audioEl) return null;\n return Number.isFinite(this.audioEl.currentTime) ? this.audioEl.currentTime : null;\n }\n\n async enableMic(): Promise<void> {\n let ctx: AudioContext;\n try {\n ctx = this.ensureCtx();\n } catch (error) {\n throw this.handleError('mic', error);\n }\n\n if (this.micStream) return;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: true,\n video: false,\n });\n this.micStream = stream;\n\n const analyser = this.createAnalyser(ctx, this.micConfig);\n this.micAnalyser = analyser;\n this.micData = new Uint8Array(\n analyser.frequencyBinCount,\n ) as Uint8Array<ArrayBuffer>;\n this.micWaveformData = new Uint8Array(\n analyser.fftSize,\n ) as Uint8Array<ArrayBuffer>;\n\n const source = ctx.createMediaStreamSource(stream);\n source.connect(analyser);\n this.micSource = source;\n\n this.setState({ micActive: true, micError: null });\n this.options.onMicStart?.();\n } catch (error) {\n throw this.handleError('mic', error, 'mic_error');\n }\n }\n\n disableMic(): void {\n const hadMic = Boolean(this.micStream || this.micSource || this.micAnalyser);\n this.micStream?.getTracks().forEach((track) => track.stop());\n this.micStream = null;\n\n try {\n this.micSource?.disconnect();\n } catch {\n /* already disconnected */\n }\n\n this.micSource = null;\n this.micAnalyser = null;\n this.micData = null;\n this.micWaveformData = null;\n this.setState({ micActive: false });\n\n if (hadMic) this.options.onMicStop?.();\n }\n\n getBands(source: AudioSource = 'music'): Bands {\n const data = this.readFrequencyData(source);\n if (!data) return { ...ZERO };\n return computeBands(data, this.classicRanges);\n }\n\n getFftData(source: AudioSource = 'music'): Uint8Array<ArrayBuffer> | null {\n return this.readFrequencyData(source);\n }\n\n getWaveform(source: AudioSource = 'music'): Uint8Array<ArrayBuffer> | null {\n return this.readWaveformData(source);\n }\n\n snapshot(source: AudioSource = 'music'): AudioBandsSnapshot {\n const fft = this.readFrequencyData(source);\n const waveform = this.readWaveformData(source);\n\n return {\n bands: fft ? computeBands(fft, this.classicRanges) : { ...ZERO },\n customBands: fft\n ? computeCustomBands(fft, this.customBandRanges)\n : computeCustomBands(new Uint8Array(1) as Uint8Array<ArrayBuffer>, this.customBandRanges),\n fft,\n waveform,\n };\n }\n\n destroy(): void {\n if (this.destroyed) return;\n\n this.teardownMusic();\n this.disableMic();\n void this.ctx?.close();\n this.ctx = null;\n this.musicAnalyser = null;\n this.musicData = null;\n this.musicWaveformData = null;\n this.setState({ isPlaying: false, micActive: false, hasTrack: false });\n this.options = {};\n this.destroyed = true;\n }\n\n private readFrequencyData(source: AudioSource): Uint8Array<ArrayBuffer> | null {\n if (source === 'mic') {\n if (!this.micAnalyser || !this.micData) return null;\n return fillFrequencyData(this.micAnalyser, this.micData);\n }\n\n if (!this.musicAnalyser || !this.musicData) return null;\n return fillFrequencyData(this.musicAnalyser, this.musicData);\n }\n\n private readWaveformData(source: AudioSource): Uint8Array<ArrayBuffer> | null {\n if (source === 'mic') {\n if (!this.micAnalyser || !this.micWaveformData) return null;\n this.micAnalyser.getByteTimeDomainData(this.micWaveformData);\n return this.micWaveformData;\n }\n\n if (!this.musicAnalyser || !this.musicWaveformData) return null;\n this.musicAnalyser.getByteTimeDomainData(this.musicWaveformData);\n return this.musicWaveformData;\n }\n\n private ensureCtx(): AudioContext {\n if (this.destroyed) {\n throw new AudioBandsError(\n 'lifecycle',\n 'destroyed',\n 'This AudioBands instance was destroyed',\n );\n }\n\n if (this.ctx) return this.ctx;\n\n const Ctx =\n window.AudioContext ||\n (window as unknown as { webkitAudioContext?: typeof AudioContext })\n .webkitAudioContext;\n\n if (!Ctx) {\n throw new AudioBandsError(\n 'lifecycle',\n 'unsupported_audio_context',\n 'AudioContext is not supported in this environment',\n );\n }\n\n const ctx = new Ctx();\n const analyser = this.createAnalyser(ctx, this.musicConfig);\n analyser.connect(ctx.destination);\n\n this.ctx = ctx;\n this.musicAnalyser = analyser;\n this.musicData = new Uint8Array(\n analyser.frequencyBinCount,\n ) as Uint8Array<ArrayBuffer>;\n this.musicWaveformData = new Uint8Array(\n analyser.fftSize,\n ) as Uint8Array<ArrayBuffer>;\n\n return ctx;\n }\n\n private createAnalyser(\n ctx: AudioContext,\n config: Required<AudioAnalyserConfig>,\n ): AnalyserNode {\n const analyser = ctx.createAnalyser();\n analyser.fftSize = config.fftSize;\n analyser.smoothingTimeConstant = config.smoothingTimeConstant;\n return analyser;\n }\n\n private handleError(\n kind: 'load' | 'playback' | 'mic',\n error: unknown,\n fallbackCode: 'load_error' | 'playback_error' | 'mic_error' = kind === 'mic'\n ? 'mic_error'\n : kind === 'playback'\n ? 'playback_error'\n : 'load_error',\n ): AudioBandsError {\n const wrapped =\n error instanceof AudioBandsError\n ? error\n : new AudioBandsError(\n kind,\n fallbackCode,\n kind === 'mic'\n ? 'Failed to access microphone input'\n : kind === 'playback'\n ? 'Failed to play audio track'\n : 'Failed to load audio track',\n error,\n );\n\n if (kind === 'load') {\n this.setState({ isPlaying: false, loadError: wrapped });\n this.options.onLoadError?.(wrapped);\n } else if (kind === 'playback') {\n this.setState({ isPlaying: false, playbackError: wrapped });\n this.options.onPlaybackError?.(wrapped);\n } else {\n this.setState({ micActive: false, micError: wrapped });\n this.options.onMicError?.(wrapped);\n }\n\n this.options.onError?.(wrapped);\n return wrapped;\n }\n\n private setState(patch: Partial<AudioBandsState>): void {\n let changed = false;\n\n for (const [key, value] of Object.entries(patch) as Array<\n [keyof AudioBandsState, AudioBandsState[keyof AudioBandsState]]\n >) {\n if (this.state[key] !== value) {\n this.state[key] = value as never;\n changed = true;\n }\n }\n\n if (changed) this.options.onStateChange?.(this.getState());\n }\n\n private teardownMusic(): void {\n this.audioEl?.pause();\n if (this.audioEl) {\n this.audioEl.src = '';\n this.audioEl.load();\n }\n this.audioEl = null;\n\n try {\n this.musicSource?.disconnect();\n } catch {\n /* already disconnected */\n }\n\n this.musicSource = null;\n this.musicWaveformData = this.musicAnalyser\n ? new Uint8Array(this.musicAnalyser.fftSize) as Uint8Array<ArrayBuffer>\n : null;\n this.setState({\n isPlaying: false,\n hasTrack: false,\n playbackError: null,\n });\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/core-entry.cjs
CHANGED
|
@@ -141,6 +141,7 @@ var AudioBands = class {
|
|
|
141
141
|
micActive: false,
|
|
142
142
|
hasTrack: false,
|
|
143
143
|
loadError: null,
|
|
144
|
+
playbackError: null,
|
|
144
145
|
micError: null
|
|
145
146
|
};
|
|
146
147
|
this.ctx = null;
|
|
@@ -154,6 +155,7 @@ var AudioBands = class {
|
|
|
154
155
|
this.musicSource = null;
|
|
155
156
|
this.micSource = null;
|
|
156
157
|
this.micStream = null;
|
|
158
|
+
this.trackLoop = false;
|
|
157
159
|
this.destroyed = false;
|
|
158
160
|
this.options = options;
|
|
159
161
|
this.musicConfig = normalizeAnalyserConfig(options.music, DEFAULT_MUSIC_ANALYSER);
|
|
@@ -174,41 +176,69 @@ var AudioBands = class {
|
|
|
174
176
|
try {
|
|
175
177
|
ctx = this.ensureCtx();
|
|
176
178
|
} catch (error) {
|
|
177
|
-
throw this.handleError("load", error);
|
|
179
|
+
throw this.handleError("load", error, "load_error");
|
|
178
180
|
}
|
|
179
181
|
this.teardownMusic();
|
|
180
182
|
const audio = new Audio();
|
|
181
183
|
audio.crossOrigin = "anonymous";
|
|
182
184
|
audio.src = url;
|
|
183
|
-
audio.loop =
|
|
185
|
+
audio.loop = this.trackLoop;
|
|
184
186
|
this.audioEl = audio;
|
|
185
|
-
this.setState({ hasTrack: true, loadError: null });
|
|
187
|
+
this.setState({ hasTrack: true, loadError: null, playbackError: null });
|
|
186
188
|
const source = ctx.createMediaElementSource(audio);
|
|
187
189
|
source.connect(this.musicAnalyser);
|
|
188
190
|
this.musicSource = source;
|
|
191
|
+
}
|
|
192
|
+
async play() {
|
|
193
|
+
const audio = this.audioEl;
|
|
194
|
+
if (!audio) return;
|
|
189
195
|
try {
|
|
190
196
|
await audio.play();
|
|
191
|
-
this.setState({ isPlaying: true,
|
|
197
|
+
this.setState({ isPlaying: true, playbackError: null });
|
|
192
198
|
this.options.onPlay?.();
|
|
193
199
|
} catch (error) {
|
|
194
|
-
throw this.handleError("
|
|
200
|
+
throw this.handleError("playback", error, "playback_error");
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
|
-
|
|
203
|
+
pause() {
|
|
204
|
+
const audio = this.audioEl;
|
|
205
|
+
if (!audio || audio.paused) return;
|
|
206
|
+
audio.pause();
|
|
207
|
+
this.setState({ isPlaying: false });
|
|
208
|
+
this.options.onPause?.();
|
|
209
|
+
}
|
|
210
|
+
async togglePlayPause() {
|
|
198
211
|
const audio = this.audioEl;
|
|
199
212
|
if (!audio) return;
|
|
200
213
|
if (audio.paused) {
|
|
201
|
-
|
|
202
|
-
this.setState({ isPlaying: true, loadError: null });
|
|
203
|
-
this.options.onPlay?.();
|
|
204
|
-
}).catch((error) => {
|
|
205
|
-
this.handleError("load", error, "playback_error");
|
|
206
|
-
});
|
|
214
|
+
await this.play();
|
|
207
215
|
return;
|
|
208
216
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
this.pause();
|
|
218
|
+
}
|
|
219
|
+
setLoop(loop) {
|
|
220
|
+
this.trackLoop = loop;
|
|
221
|
+
if (this.audioEl) this.audioEl.loop = loop;
|
|
222
|
+
}
|
|
223
|
+
seek(seconds) {
|
|
224
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
225
|
+
throw new AudioBandsError(
|
|
226
|
+
"config",
|
|
227
|
+
"invalid_config",
|
|
228
|
+
"seek time must be a finite number greater than or equal to 0"
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (!this.audioEl) return;
|
|
232
|
+
const duration = Number.isFinite(this.audioEl.duration) ? this.audioEl.duration : null;
|
|
233
|
+
this.audioEl.currentTime = duration === null ? seconds : Math.min(seconds, duration);
|
|
234
|
+
}
|
|
235
|
+
getDuration() {
|
|
236
|
+
if (!this.audioEl) return null;
|
|
237
|
+
return Number.isFinite(this.audioEl.duration) ? this.audioEl.duration : null;
|
|
238
|
+
}
|
|
239
|
+
getCurrentTime() {
|
|
240
|
+
if (!this.audioEl) return null;
|
|
241
|
+
return Number.isFinite(this.audioEl.currentTime) ? this.audioEl.currentTime : null;
|
|
212
242
|
}
|
|
213
243
|
async enableMic() {
|
|
214
244
|
let ctx;
|
|
@@ -267,6 +297,16 @@ var AudioBands = class {
|
|
|
267
297
|
getWaveform(source = "music") {
|
|
268
298
|
return this.readWaveformData(source);
|
|
269
299
|
}
|
|
300
|
+
snapshot(source = "music") {
|
|
301
|
+
const fft = this.readFrequencyData(source);
|
|
302
|
+
const waveform = this.readWaveformData(source);
|
|
303
|
+
return {
|
|
304
|
+
bands: fft ? computeBands(fft, this.classicRanges) : { ...ZERO },
|
|
305
|
+
customBands: fft ? computeCustomBands(fft, this.customBandRanges) : computeCustomBands(new Uint8Array(1), this.customBandRanges),
|
|
306
|
+
fft,
|
|
307
|
+
waveform
|
|
308
|
+
};
|
|
309
|
+
}
|
|
270
310
|
destroy() {
|
|
271
311
|
if (this.destroyed) return;
|
|
272
312
|
this.teardownMusic();
|
|
@@ -334,16 +374,19 @@ var AudioBands = class {
|
|
|
334
374
|
analyser.smoothingTimeConstant = config.smoothingTimeConstant;
|
|
335
375
|
return analyser;
|
|
336
376
|
}
|
|
337
|
-
handleError(kind, error, fallbackCode = kind === "mic" ? "mic_error" : "load_error") {
|
|
377
|
+
handleError(kind, error, fallbackCode = kind === "mic" ? "mic_error" : kind === "playback" ? "playback_error" : "load_error") {
|
|
338
378
|
const wrapped = error instanceof AudioBandsError ? error : new AudioBandsError(
|
|
339
379
|
kind,
|
|
340
380
|
fallbackCode,
|
|
341
|
-
kind === "mic" ? "Failed to access microphone input" : "Failed to
|
|
381
|
+
kind === "mic" ? "Failed to access microphone input" : kind === "playback" ? "Failed to play audio track" : "Failed to load audio track",
|
|
342
382
|
error
|
|
343
383
|
);
|
|
344
384
|
if (kind === "load") {
|
|
345
385
|
this.setState({ isPlaying: false, loadError: wrapped });
|
|
346
386
|
this.options.onLoadError?.(wrapped);
|
|
387
|
+
} else if (kind === "playback") {
|
|
388
|
+
this.setState({ isPlaying: false, playbackError: wrapped });
|
|
389
|
+
this.options.onPlaybackError?.(wrapped);
|
|
347
390
|
} else {
|
|
348
391
|
this.setState({ micActive: false, micError: wrapped });
|
|
349
392
|
this.options.onMicError?.(wrapped);
|
|
@@ -374,7 +417,11 @@ var AudioBands = class {
|
|
|
374
417
|
}
|
|
375
418
|
this.musicSource = null;
|
|
376
419
|
this.musicWaveformData = this.musicAnalyser ? new Uint8Array(this.musicAnalyser.fftSize) : null;
|
|
377
|
-
this.setState({
|
|
420
|
+
this.setState({
|
|
421
|
+
isPlaying: false,
|
|
422
|
+
hasTrack: false,
|
|
423
|
+
playbackError: null
|
|
424
|
+
});
|
|
378
425
|
}
|
|
379
426
|
};
|
|
380
427
|
// Annotate the CommonJS export names for ESM import in node:
|