@juandinella/audio-bands 0.4.7 → 0.6.2

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 CHANGED
@@ -4,24 +4,29 @@
4
4
 
5
5
  **Demo**: [audio-bands.juandinella.com](https://audio-bands.juandinella.com)
6
6
 
7
- Headless audio analysis for the browser. Get normalized `bass`, `mid`, `high`, custom named bands, raw FFT bins, or time-domain waveform data without shipping a renderer.
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 { bass, mid, high } = audio.getBands();
11
- const custom = audio.getCustomBands();
12
- const fft = audio.getFftData();
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
22
25
  npm install @juandinella/audio-bands
23
26
  ```
24
27
 
28
+ For repository development, `npm test` covers the mocked unit suite and `npm run test:browser` runs a small browser smoke test against the built bundles.
29
+
25
30
  ### Entry points
26
31
 
27
32
  - `@juandinella/audio-bands`: main framework-agnostic export
@@ -30,6 +35,8 @@ npm install @juandinella/audio-bands
30
35
 
31
36
  If you use the React hook, install `react` as well.
32
37
 
38
+ Minimal reference examples live in [`examples/README.md`](./examples/README.md). The Vite app in `examples/src/App.tsx` is a showcase demo, while `examples/snippets/` contains the smallest copyable integrations.
39
+
33
40
  ## Usage
34
41
 
35
42
  ### Vanilla JS
@@ -51,12 +58,14 @@ const audio = new AudioBands({
51
58
  });
52
59
 
53
60
  await audio.load('/track.mp3');
61
+ await audio.play();
54
62
 
55
63
  function loop() {
56
- const { bass, mid, high, overall } = audio.getBands();
57
- const custom = audio.getCustomBands();
58
- const fft = audio.getFftData();
59
- const waveform = audio.getWaveform();
64
+ const frame = audio.snapshot();
65
+ const { bass, mid, high, overall } = frame.bands;
66
+ const custom = frame.customBands;
67
+ const fft = frame.fft;
68
+ const waveform = frame.waveform;
60
69
 
61
70
  requestAnimationFrame(loop);
62
71
  }
@@ -74,8 +83,16 @@ function Visualizer() {
74
83
  isPlaying,
75
84
  hasTrack,
76
85
  loadError,
86
+ playbackError,
77
87
  micError,
78
88
  loadTrack,
89
+ play,
90
+ pause,
91
+ setLoop,
92
+ seek,
93
+ getDuration,
94
+ getCurrentTime,
95
+ snapshot,
79
96
  togglePlayPause,
80
97
  toggleMic,
81
98
  getBands,
@@ -86,12 +103,27 @@ function Visualizer() {
86
103
  },
87
104
  });
88
105
 
106
+ const frame = snapshot();
107
+
89
108
  return (
90
109
  <>
91
110
  <button onClick={() => loadTrack('/track.mp3')}>load</button>
92
- <button onClick={togglePlayPause}>{isPlaying ? 'Pause' : 'Play'}</button>
111
+ <button onClick={play}>play</button>
112
+ <button onClick={pause}>pause</button>
113
+ <button onClick={() => setLoop(true)}>loop</button>
114
+ <button onClick={() => seek(30)}>seek 0:30</button>
115
+ <button onClick={togglePlayPause}>toggle</button>
93
116
  <button onClick={toggleMic}>Toggle mic</button>
94
- <pre>{JSON.stringify({ hasTrack, loadError, micError, ...getBands(), ...getCustomBands() }, null, 2)}</pre>
117
+ <pre>{JSON.stringify({
118
+ hasTrack,
119
+ loadError,
120
+ playbackError,
121
+ micError,
122
+ duration: getDuration(),
123
+ currentTime: getCurrentTime(),
124
+ ...frame.bands,
125
+ ...frame.customBands,
126
+ }, null, 2)}</pre>
95
127
  </>
96
128
  );
97
129
  }
@@ -109,7 +141,24 @@ const waveform = audio.getWaveform('mic');
109
141
 
110
142
  ## When To Use Bands Vs FFT
111
143
 
112
- Use `getBands()` when you want stable, simple control signals:
144
+ ## What `bass`, `mid`, `high`, and `overall` Mean
145
+
146
+ `getBands()` returns three coarse analyser regions plus a convenience summary value:
147
+
148
+ - `bass`, `mid`, and `high` are normalized slices of the analyser spectrum, not fixed acoustic bands in Hz
149
+ - the default split is percentage-based (`0-0.08`, `0.08-0.4`, `0.4-1`) over the available FFT bins
150
+ - those regions therefore depend on analyser resolution and the underlying audio context sample rate
151
+ - `overall` is a UI-oriented weighted summary (`bass * 0.5 + mid * 0.3 + high * 0.2`), not a perceptual loudness metric
152
+
153
+ 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()`.
154
+
155
+ Use `snapshot()` first when you need a full analysis frame:
156
+
157
+ - read `bands`, `customBands`, `fft`, and `waveform` together
158
+ - avoid multiple analyser reads in one render loop
159
+ - keep derived values synchronized
160
+
161
+ Use `getBands()` when you only want stable, simple control signals:
113
162
 
114
163
  - pulsing a blob with low-end energy
115
164
  - scaling UI based on overall intensity
@@ -147,11 +196,18 @@ new AudioBands(options?: AudioBandsOptions)
147
196
 
148
197
  | Method | Description |
149
198
  | ----------------------- | ----------- |
150
- | `load(url)` | Load and play a track. Rejects with `AudioBandsError` on failure. |
151
- | `togglePlayPause()` | Toggle the current track. |
199
+ | `load(url)` | Load a track, connect it to the analyser, and resolve when the media is ready. Rejects with `AudioBandsError` on load failure. |
200
+ | `play()` | Start playback for the current track. Rejects with `AudioBandsError` on failure. |
201
+ | `pause()` | Pause the current track. |
202
+ | `setLoop(loop)` | Set whether the current and future loaded tracks should loop. |
203
+ | `seek(seconds)` | Seek the current track to a given time in seconds. |
204
+ | `getDuration()` | Returns the current track duration in seconds, or `null` when unavailable. |
205
+ | `getCurrentTime()` | Returns the current playback time in seconds, or `null` when unavailable. |
206
+ | `togglePlayPause()` | Toggle the current track. Returns a promise and propagates playback errors when toggling into play. |
152
207
  | `enableMic()` | Request microphone access and start mic analysis. Rejects with `AudioBandsError` on failure. |
153
208
  | `disableMic()` | Stop mic input and clean up the stream. |
154
- | `getBands(source?)` | Returns normalized `{ bass, mid, high, overall }`. |
209
+ | `snapshot(source?)` | Returns `{ bands, customBands, fft, waveform }` from a single analyser read. |
210
+ | `getBands(source?)` | Returns normalized analyser-region energy `{ bass, mid, high, overall }`. |
155
211
  | `getCustomBands(source?)` | Returns normalized values for configured custom bands. |
156
212
  | `getFftData(source?)` | Returns raw `Uint8Array` frequency bins. |
157
213
  | `getWaveform(source?)` | Returns raw time-domain data for `'music'` or `'mic'`. |
@@ -167,11 +223,19 @@ const {
167
223
  hasTrack,
168
224
  audioError,
169
225
  loadError,
226
+ playbackError,
170
227
  micError,
171
228
  state,
172
229
  loadTrack,
230
+ play,
231
+ pause,
232
+ setLoop,
233
+ seek,
234
+ getDuration,
235
+ getCurrentTime,
173
236
  togglePlayPause,
174
237
  toggleMic,
238
+ snapshot,
175
239
  getBands,
176
240
  getCustomBands,
177
241
  getFftData,
@@ -199,6 +263,7 @@ type AudioBandsOptions = {
199
263
  customBands?: Record<string, { from: number; to: number }>;
200
264
  onError?: (error: AudioBandsError) => void;
201
265
  onLoadError?: (error: AudioBandsError) => void;
266
+ onPlaybackError?: (error: AudioBandsError) => void;
202
267
  onMicError?: (error: AudioBandsError) => void;
203
268
  onStateChange?: (state: AudioBandsState) => void;
204
269
  onPlay?: () => void;
@@ -216,6 +281,7 @@ type AudioBandsState = {
216
281
  micActive: boolean;
217
282
  hasTrack: boolean; // a track source is assigned, even if playback later fails
218
283
  loadError: AudioBandsError | null;
284
+ playbackError: AudioBandsError | null;
219
285
  micError: AudioBandsError | null;
220
286
  };
221
287
  ```
@@ -223,12 +289,26 @@ type AudioBandsState = {
223
289
  ## Notes
224
290
 
225
291
  - `AudioContext` is created lazily on the first call to `load()` or `enableMic()`.
226
- - `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.
292
+ - `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 call `play()` or `togglePlayPause()`.
293
+ - `togglePlayPause()` follows the same playback error contract as `play()`: if toggling into play fails, the returned promise rejects.
294
+ - `hasTrack` means the current track finished loading and is ready on the instance. It can still be `true` if `play()` fails later due to autoplay policy or another playback error.
295
+ - `isPlaying` follows the underlying media element events, so it falls back to `false` when the track pauses or reaches `ended`.
296
+ - `loadError` stores track loading failures only.
297
+ - `playbackError` stores playback failures for the current track, such as autoplay-policy rejections.
298
+ - In the React hook, changing `music`, `mic`, `bandRanges`, or `customBands` recreates the underlying `AudioBands` instance.
227
299
  - The mic analyser is not connected to `AudioContext.destination`, so it will not feed back into the speakers.
228
- - `getBands()`, `getCustomBands()`, `getFftData()`, and `getWaveform()` read live data. Call them inside `requestAnimationFrame`, not from React state updates.
300
+ - `snapshot()` is the preferred way to read analysis inside `requestAnimationFrame`.
301
+ - `getBands()`, `getCustomBands()`, `getFftData()`, and `getWaveform()` are convenience reads when you only need one view of the current frame.
229
302
  - `getFftData()` returns the same underlying buffer on each call. Copy it if you need frame-to-frame comparisons.
230
303
  - `fftSize` must be a power of two between `32` and `32768`.
231
304
  - Band ranges are normalized from `0` to `1`, where `0` is the start of the analyser spectrum and `1` is the end.
305
+ - The default `bass` / `mid` / `high` labels are convenience names for analyser regions, not fixed Hz buckets.
306
+ - `overall` is intended as a simple UI summary, not as an acoustically weighted loudness value.
307
+
308
+ ## Development
309
+
310
+ - `npm test` builds the package and runs the unit suite.
311
+ - Releases are published from GitHub Actions when a `v*` tag is pushed.
232
312
 
233
313
  ## License
234
314
 
@@ -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,10 @@ var AudioBands = class {
127
128
  this.musicSource = null;
128
129
  this.micSource = null;
129
130
  this.micStream = null;
131
+ this.musicEventCleanup = null;
132
+ this.pendingLoadCleanup = null;
133
+ this.pendingLoadReject = null;
134
+ this.trackLoop = false;
130
135
  this.destroyed = false;
131
136
  this.options = options;
132
137
  this.musicConfig = normalizeAnalyserConfig(options.music, DEFAULT_MUSIC_ANALYSER);
@@ -147,41 +152,96 @@ var AudioBands = class {
147
152
  try {
148
153
  ctx = this.ensureCtx();
149
154
  } catch (error) {
150
- throw this.handleError("load", error);
155
+ throw this.handleError("load", error, "load_error");
151
156
  }
152
157
  this.teardownMusic();
153
158
  const audio = new Audio();
154
159
  audio.crossOrigin = "anonymous";
160
+ audio.preload = "auto";
155
161
  audio.src = url;
156
- audio.loop = true;
162
+ audio.loop = this.trackLoop;
163
+ this.bindMusicElementEvents(audio);
157
164
  this.audioEl = audio;
158
- this.setState({ hasTrack: true, loadError: null });
165
+ this.setState({ hasTrack: false, loadError: null, playbackError: null });
159
166
  const source = ctx.createMediaElementSource(audio);
160
167
  source.connect(this.musicAnalyser);
161
168
  this.musicSource = source;
169
+ await new Promise((resolve, reject) => {
170
+ const finalize = () => {
171
+ cleanup();
172
+ this.pendingLoadCleanup = null;
173
+ this.pendingLoadReject = null;
174
+ };
175
+ const handleReady = () => {
176
+ finalize();
177
+ this.setState({ hasTrack: true, loadError: null });
178
+ resolve();
179
+ };
180
+ const handleFailure = (event) => {
181
+ finalize();
182
+ const mediaError = event?.target?.error ?? audio.error ?? event;
183
+ reject(this.handleLoadFailure(mediaError));
184
+ };
185
+ const cleanup = () => {
186
+ audio.removeEventListener("loadedmetadata", handleReady);
187
+ audio.removeEventListener("canplay", handleReady);
188
+ audio.removeEventListener("error", handleFailure);
189
+ };
190
+ this.pendingLoadCleanup = cleanup;
191
+ this.pendingLoadReject = reject;
192
+ audio.addEventListener("loadedmetadata", handleReady, { once: true });
193
+ audio.addEventListener("canplay", handleReady, { once: true });
194
+ audio.addEventListener("error", handleFailure, { once: true });
195
+ audio.load();
196
+ });
197
+ }
198
+ async play() {
199
+ const audio = this.audioEl;
200
+ if (!audio) return;
162
201
  try {
163
202
  await audio.play();
164
- this.setState({ isPlaying: true, loadError: null });
165
- this.options.onPlay?.();
203
+ this.setState({ playbackError: null });
166
204
  } catch (error) {
167
- throw this.handleError("load", error, "load_error");
205
+ throw this.handleError("playback", error, "playback_error");
168
206
  }
169
207
  }
170
- togglePlayPause() {
208
+ pause() {
209
+ const audio = this.audioEl;
210
+ if (!audio || audio.paused) return;
211
+ audio.pause();
212
+ }
213
+ async togglePlayPause() {
171
214
  const audio = this.audioEl;
172
215
  if (!audio) return;
173
216
  if (audio.paused) {
174
- void audio.play().then(() => {
175
- this.setState({ isPlaying: true, loadError: null });
176
- this.options.onPlay?.();
177
- }).catch((error) => {
178
- this.handleError("load", error, "playback_error");
179
- });
217
+ await this.play();
180
218
  return;
181
219
  }
182
- audio.pause();
183
- this.setState({ isPlaying: false });
184
- this.options.onPause?.();
220
+ this.pause();
221
+ }
222
+ setLoop(loop) {
223
+ this.trackLoop = loop;
224
+ if (this.audioEl) this.audioEl.loop = loop;
225
+ }
226
+ seek(seconds) {
227
+ if (!Number.isFinite(seconds) || seconds < 0) {
228
+ throw new AudioBandsError(
229
+ "config",
230
+ "invalid_config",
231
+ "seek time must be a finite number greater than or equal to 0"
232
+ );
233
+ }
234
+ if (!this.audioEl) return;
235
+ const duration = Number.isFinite(this.audioEl.duration) ? this.audioEl.duration : null;
236
+ this.audioEl.currentTime = duration === null ? seconds : Math.min(seconds, duration);
237
+ }
238
+ getDuration() {
239
+ if (!this.audioEl) return null;
240
+ return Number.isFinite(this.audioEl.duration) ? this.audioEl.duration : null;
241
+ }
242
+ getCurrentTime() {
243
+ if (!this.audioEl) return null;
244
+ return Number.isFinite(this.audioEl.currentTime) ? this.audioEl.currentTime : null;
185
245
  }
186
246
  async enableMic() {
187
247
  let ctx;
@@ -240,6 +300,16 @@ var AudioBands = class {
240
300
  getWaveform(source = "music") {
241
301
  return this.readWaveformData(source);
242
302
  }
303
+ snapshot(source = "music") {
304
+ const fft = this.readFrequencyData(source);
305
+ const waveform = this.readWaveformData(source);
306
+ return {
307
+ bands: fft ? computeBands(fft, this.classicRanges) : { ...ZERO },
308
+ customBands: fft ? computeCustomBands(fft, this.customBandRanges) : computeCustomBands(new Uint8Array(1), this.customBandRanges),
309
+ fft,
310
+ waveform
311
+ };
312
+ }
243
313
  destroy() {
244
314
  if (this.destroyed) return;
245
315
  this.teardownMusic();
@@ -307,16 +377,19 @@ var AudioBands = class {
307
377
  analyser.smoothingTimeConstant = config.smoothingTimeConstant;
308
378
  return analyser;
309
379
  }
310
- handleError(kind, error, fallbackCode = kind === "mic" ? "mic_error" : "load_error") {
380
+ handleError(kind, error, fallbackCode = kind === "mic" ? "mic_error" : kind === "playback" ? "playback_error" : "load_error") {
311
381
  const wrapped = error instanceof AudioBandsError ? error : new AudioBandsError(
312
382
  kind,
313
383
  fallbackCode,
314
- kind === "mic" ? "Failed to access microphone input" : "Failed to load or play audio track",
384
+ kind === "mic" ? "Failed to access microphone input" : kind === "playback" ? "Failed to play audio track" : "Failed to load audio track",
315
385
  error
316
386
  );
317
387
  if (kind === "load") {
318
388
  this.setState({ isPlaying: false, loadError: wrapped });
319
389
  this.options.onLoadError?.(wrapped);
390
+ } else if (kind === "playback") {
391
+ this.setState({ isPlaying: false, playbackError: wrapped });
392
+ this.options.onPlaybackError?.(wrapped);
320
393
  } else {
321
394
  this.setState({ micActive: false, micError: wrapped });
322
395
  this.options.onMicError?.(wrapped);
@@ -324,6 +397,36 @@ var AudioBands = class {
324
397
  this.options.onError?.(wrapped);
325
398
  return wrapped;
326
399
  }
400
+ bindMusicElementEvents(audio) {
401
+ this.musicEventCleanup?.();
402
+ const handlePlay = () => {
403
+ if (this.audioEl !== audio) return;
404
+ this.setState({ isPlaying: true, playbackError: null });
405
+ this.options.onPlay?.();
406
+ };
407
+ const handlePause = () => {
408
+ if (this.audioEl !== audio) return;
409
+ this.setState({ isPlaying: false });
410
+ this.options.onPause?.();
411
+ };
412
+ const handleEnded = () => {
413
+ if (this.audioEl !== audio) return;
414
+ this.setState({ isPlaying: false });
415
+ };
416
+ audio.addEventListener("play", handlePlay);
417
+ audio.addEventListener("pause", handlePause);
418
+ audio.addEventListener("ended", handleEnded);
419
+ this.musicEventCleanup = () => {
420
+ audio.removeEventListener("play", handlePlay);
421
+ audio.removeEventListener("pause", handlePause);
422
+ audio.removeEventListener("ended", handleEnded);
423
+ if (this.musicEventCleanup) this.musicEventCleanup = null;
424
+ };
425
+ }
426
+ handleLoadFailure(error) {
427
+ this.teardownMusic();
428
+ return this.handleError("load", error, "load_error");
429
+ }
327
430
  setState(patch) {
328
431
  let changed = false;
329
432
  for (const [key, value] of Object.entries(patch)) {
@@ -335,6 +438,23 @@ var AudioBands = class {
335
438
  if (changed) this.options.onStateChange?.(this.getState());
336
439
  }
337
440
  teardownMusic() {
441
+ this.musicEventCleanup?.();
442
+ if (this.pendingLoadReject) {
443
+ const reject = this.pendingLoadReject;
444
+ this.pendingLoadReject = null;
445
+ this.pendingLoadCleanup?.();
446
+ this.pendingLoadCleanup = null;
447
+ reject(
448
+ new AudioBandsError(
449
+ "load",
450
+ "load_error",
451
+ "Audio track loading was interrupted before completion"
452
+ )
453
+ );
454
+ } else if (this.pendingLoadCleanup) {
455
+ this.pendingLoadCleanup();
456
+ this.pendingLoadCleanup = null;
457
+ }
338
458
  this.audioEl?.pause();
339
459
  if (this.audioEl) {
340
460
  this.audioEl.src = "";
@@ -347,7 +467,11 @@ var AudioBands = class {
347
467
  }
348
468
  this.musicSource = null;
349
469
  this.musicWaveformData = this.musicAnalyser ? new Uint8Array(this.musicAnalyser.fftSize) : null;
350
- this.setState({ isPlaying: false, hasTrack: false });
470
+ this.setState({
471
+ isPlaying: false,
472
+ hasTrack: false,
473
+ playbackError: null
474
+ });
351
475
  }
352
476
  };
353
477
 
@@ -355,4 +479,4 @@ export {
355
479
  AudioBandsError,
356
480
  AudioBands
357
481
  };
358
- //# sourceMappingURL=chunk-33JHLQZJ.js.map
482
+ //# sourceMappingURL=chunk-EGVVFGLZ.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 musicEventCleanup: (() => void) | null = null;\n private pendingLoadCleanup: (() => void) | null = null;\n private pendingLoadReject: ((error: AudioBandsError) => void) | 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.preload = 'auto';\n audio.src = url;\n audio.loop = this.trackLoop;\n this.bindMusicElementEvents(audio);\n this.audioEl = audio;\n this.setState({ hasTrack: false, loadError: null, playbackError: null });\n\n const source = ctx.createMediaElementSource(audio);\n source.connect(this.musicAnalyser!);\n this.musicSource = source;\n\n await new Promise<void>((resolve, reject) => {\n const finalize = () => {\n cleanup();\n this.pendingLoadCleanup = null;\n this.pendingLoadReject = null;\n };\n\n const handleReady = () => {\n finalize();\n this.setState({ hasTrack: true, loadError: null });\n resolve();\n };\n\n const handleFailure = (event?: Event) => {\n finalize();\n const mediaError =\n (event?.target as HTMLMediaElement | null)?.error ??\n audio.error ??\n event;\n reject(this.handleLoadFailure(mediaError));\n };\n\n const cleanup = () => {\n audio.removeEventListener('loadedmetadata', handleReady);\n audio.removeEventListener('canplay', handleReady);\n audio.removeEventListener('error', handleFailure);\n };\n\n this.pendingLoadCleanup = cleanup;\n this.pendingLoadReject = reject;\n\n audio.addEventListener('loadedmetadata', handleReady, { once: true });\n audio.addEventListener('canplay', handleReady, { once: true });\n audio.addEventListener('error', handleFailure, { once: true });\n audio.load();\n });\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({ playbackError: null });\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 }\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 bindMusicElementEvents(audio: HTMLAudioElement): void {\n this.musicEventCleanup?.();\n\n const handlePlay = () => {\n if (this.audioEl !== audio) return;\n this.setState({ isPlaying: true, playbackError: null });\n this.options.onPlay?.();\n };\n\n const handlePause = () => {\n if (this.audioEl !== audio) return;\n this.setState({ isPlaying: false });\n this.options.onPause?.();\n };\n\n const handleEnded = () => {\n if (this.audioEl !== audio) return;\n this.setState({ isPlaying: false });\n };\n\n audio.addEventListener('play', handlePlay);\n audio.addEventListener('pause', handlePause);\n audio.addEventListener('ended', handleEnded);\n\n this.musicEventCleanup = () => {\n audio.removeEventListener('play', handlePlay);\n audio.removeEventListener('pause', handlePause);\n audio.removeEventListener('ended', handleEnded);\n if (this.musicEventCleanup) this.musicEventCleanup = null;\n };\n }\n\n private handleLoadFailure(error: unknown): AudioBandsError {\n this.teardownMusic();\n return this.handleError('load', error, 'load_error');\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.musicEventCleanup?.();\n if (this.pendingLoadReject) {\n const reject = this.pendingLoadReject;\n this.pendingLoadReject = null;\n this.pendingLoadCleanup?.();\n this.pendingLoadCleanup = null;\n reject(\n new AudioBandsError(\n 'load',\n 'load_error',\n 'Audio track loading was interrupted before completion',\n ),\n );\n } else if (this.pendingLoadCleanup) {\n this.pendingLoadCleanup();\n this.pendingLoadCleanup = null;\n }\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":[]}