@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 +96 -16
- package/dist/{chunk-33JHLQZJ.js → chunk-EGVVFGLZ.js} +144 -20
- package/dist/chunk-EGVVFGLZ.js.map +1 -0
- package/dist/core-entry.cjs +143 -19
- package/dist/core-entry.cjs.map +1 -1
- package/dist/core-entry.d.cts +17 -4
- package/dist/core-entry.d.ts +17 -4
- package/dist/core-entry.js +1 -1
- package/dist/index.cjs +143 -19
- 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 +271 -62
- 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 +129 -44
- 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 +15 -2
- package/dist/chunk-33JHLQZJ.js.map +0 -1
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.
|
|
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
|
|
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
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
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={
|
|
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({
|
|
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
|
-
|
|
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
|
|
151
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
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 =
|
|
162
|
+
audio.loop = this.trackLoop;
|
|
163
|
+
this.bindMusicElementEvents(audio);
|
|
157
164
|
this.audioEl = audio;
|
|
158
|
-
this.setState({ hasTrack:
|
|
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({
|
|
165
|
-
this.options.onPlay?.();
|
|
203
|
+
this.setState({ playbackError: null });
|
|
166
204
|
} catch (error) {
|
|
167
|
-
throw this.handleError("
|
|
205
|
+
throw this.handleError("playback", error, "playback_error");
|
|
168
206
|
}
|
|
169
207
|
}
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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({
|
|
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-
|
|
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":[]}
|