openaudio-suite 2.5.6 → 2.6.3
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/CHANGELOG.md +169 -109
- package/CONTRIBUTING.md +39 -324
- package/OpenAudio.js +192 -284
- package/OpenAudio_r.js +211 -164
- package/OpenAudio_s.js +420 -204
- package/README.md +142 -122
- package/docs/OPENAUDIO_R.md +135 -143
- package/docs/OPENAUDIO_S.md +162 -377
- package/package.json +17 -27
package/docs/OPENAUDIO_R.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# OpenAudio_r.js API Reference
|
|
2
2
|
|
|
3
|
-
**Version:** 2.
|
|
4
|
-
**Class:** `AudioEngine`
|
|
3
|
+
**Version:** 2.6.0
|
|
4
|
+
**Class:** `AudioEngine`
|
|
5
5
|
**Use Case:** Randomized multi-clip scheduler with shuffle bag algorithm
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -14,11 +14,11 @@ const engine = new AudioEngine([
|
|
|
14
14
|
{ src: 'audio/rain.mp3', label: 'Rain' },
|
|
15
15
|
{ src: 'audio/birds.mp3', label: 'Birds' }
|
|
16
16
|
], {
|
|
17
|
-
lowTime:
|
|
18
|
-
maxTime:
|
|
17
|
+
lowTime: 3,
|
|
18
|
+
maxTime: 5,
|
|
19
19
|
volume: 0.8,
|
|
20
|
-
onPlay: () => console.log('Playing'),
|
|
21
|
-
onEnd: () => console.log('Finished'),
|
|
20
|
+
onPlay: (clip) => console.log('Playing:', clip.label),
|
|
21
|
+
onEnd: (clip) => console.log('Finished:', clip.label),
|
|
22
22
|
onCycleReset: () => console.log('New cycle')
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -42,7 +42,7 @@ Array of audio clip objects. Each clip must have:
|
|
|
42
42
|
```javascript
|
|
43
43
|
{
|
|
44
44
|
src: 'path/to/audio.mp3', // Required: URL or data URI
|
|
45
|
-
label: 'Clip Name' // Optional: name for logging
|
|
45
|
+
label: 'Clip Name' // Optional: name for logging/callbacks
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
@@ -57,7 +57,7 @@ const engine = new AudioEngine([
|
|
|
57
57
|
// With labels
|
|
58
58
|
const engine = new AudioEngine([
|
|
59
59
|
{ src: 'ambient/forest.mp3', label: 'Forest Ambience' },
|
|
60
|
-
{ src: 'ambient/rain.mp3',
|
|
60
|
+
{ src: 'ambient/rain.mp3', label: 'Rain Sounds' }
|
|
61
61
|
]);
|
|
62
62
|
|
|
63
63
|
// With data URIs
|
|
@@ -70,12 +70,12 @@ const engine = new AudioEngine([
|
|
|
70
70
|
|
|
71
71
|
```javascript
|
|
72
72
|
{
|
|
73
|
-
lowTime:
|
|
74
|
-
maxTime:
|
|
75
|
-
volume: 0.
|
|
73
|
+
lowTime: 1, // Min delay between clips (seconds), default: 1
|
|
74
|
+
maxTime: 10, // Max delay between clips (seconds), default: 10
|
|
75
|
+
volume: 0.85, // Volume 0.0–1.0, default: 0.85
|
|
76
76
|
onPlay: fn, // Callback when clip starts
|
|
77
77
|
onEnd: fn, // Callback when clip ends
|
|
78
|
-
onCycleReset: fn // Callback when cycle resets
|
|
78
|
+
onCycleReset: fn // Callback when all clips have played and cycle resets
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
@@ -87,20 +87,20 @@ const engine = new AudioEngine([
|
|
|
87
87
|
|
|
88
88
|
### `start()`
|
|
89
89
|
|
|
90
|
-
Begin playback. Must be called inside a user gesture on first use.
|
|
90
|
+
Begin playback. Must be called inside a user gesture on first use. No-op after `destroy()`.
|
|
91
91
|
|
|
92
92
|
```javascript
|
|
93
|
-
// First call (inside gesture
|
|
93
|
+
// First call (inside gesture — REQUIRED)
|
|
94
94
|
document.addEventListener('click', () => engine.start(), { once: true });
|
|
95
95
|
|
|
96
96
|
// Subsequent calls
|
|
97
|
-
engine.start(); // Safe to call anytime
|
|
97
|
+
engine.start(); // Safe to call anytime — ignored if already running
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
**Behavior:**
|
|
101
|
-
- If already
|
|
101
|
+
- If already started or unlocking: ignored (safe)
|
|
102
102
|
- If stopped: resumes cycle from next clip
|
|
103
|
-
- First call: plays silent unlock MP3, then
|
|
103
|
+
- First call: plays silent unlock MP3, then schedules first clip
|
|
104
104
|
|
|
105
105
|
**Browser Autoplay Policy:**
|
|
106
106
|
- ✅ Works: click, keydown, touchstart, mousedown
|
|
@@ -110,7 +110,7 @@ engine.start(); // Safe to call anytime
|
|
|
110
110
|
|
|
111
111
|
### `stop()`
|
|
112
112
|
|
|
113
|
-
Stop playback and
|
|
113
|
+
Stop playback and preserve cycle state. No-op after `destroy()`.
|
|
114
114
|
|
|
115
115
|
```javascript
|
|
116
116
|
engine.stop();
|
|
@@ -118,31 +118,41 @@ engine.stop();
|
|
|
118
118
|
|
|
119
119
|
**Behavior:**
|
|
120
120
|
- Pauses current clip
|
|
121
|
-
-
|
|
122
|
-
-
|
|
121
|
+
- Cancels pending timer
|
|
122
|
+
- Preserves played flags — next `start()` picks up from the same cycle position
|
|
123
123
|
|
|
124
124
|
```javascript
|
|
125
|
-
engine.start(); // Playing
|
|
125
|
+
engine.start(); // Playing
|
|
126
126
|
engine.stop(); // Stopped
|
|
127
|
-
engine.start(); //
|
|
127
|
+
engine.start(); // Resumes same cycle
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `reset()`
|
|
133
|
+
|
|
134
|
+
Stop and clear all played flags. No-op after `destroy()`.
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
engine.reset();
|
|
138
|
+
engine.start(); // Starts a completely fresh random cycle
|
|
128
139
|
```
|
|
129
140
|
|
|
130
141
|
---
|
|
131
142
|
|
|
132
143
|
### `setVolume(value)`
|
|
133
144
|
|
|
134
|
-
Change volume during playback (
|
|
145
|
+
Change volume during playback. No-op after `destroy()`.
|
|
135
146
|
|
|
136
147
|
```javascript
|
|
137
148
|
engine.setVolume(0.5); // 50% volume
|
|
138
149
|
engine.setVolume(1.0); // Full volume
|
|
150
|
+
engine.setVolume(0.0); // Mute (engine keeps running)
|
|
139
151
|
```
|
|
140
152
|
|
|
141
153
|
**Parameter:**
|
|
142
|
-
- `value` (number) — Volume 0.0–1.0
|
|
143
|
-
- Clamped to valid range automatically
|
|
154
|
+
- `value` (number) — Volume 0.0–1.0. Clamped automatically.
|
|
144
155
|
|
|
145
|
-
**Example:**
|
|
146
156
|
```javascript
|
|
147
157
|
// Volume slider
|
|
148
158
|
document.getElementById('volume-slider').addEventListener('input', (e) => {
|
|
@@ -152,39 +162,37 @@ document.getElementById('volume-slider').addEventListener('input', (e) => {
|
|
|
152
162
|
|
|
153
163
|
---
|
|
154
164
|
|
|
155
|
-
### `addClip(
|
|
165
|
+
### `addClip(clip)`
|
|
156
166
|
|
|
157
|
-
Add a clip to the engine at runtime.
|
|
167
|
+
Add a clip to the engine at runtime. No-op after `destroy()`.
|
|
158
168
|
|
|
159
169
|
```javascript
|
|
160
|
-
engine.addClip('audio/new-clip.mp3');
|
|
161
|
-
engine.addClip('audio/new-clip.mp3', 'New Sound');
|
|
170
|
+
engine.addClip({ src: 'audio/new-clip.mp3' });
|
|
171
|
+
engine.addClip({ src: 'audio/new-clip.mp3', label: 'New Sound' });
|
|
162
172
|
```
|
|
163
173
|
|
|
164
174
|
**Parameters:**
|
|
165
|
-
- `src` (string) — Audio URL or data URI
|
|
166
|
-
- `label` (string, optional) — Name for logging
|
|
175
|
+
- `clip.src` (string, required) — Audio URL or data URI. Throws if missing or empty.
|
|
176
|
+
- `clip.label` (string, optional) — Name for logging.
|
|
167
177
|
|
|
168
|
-
**Behavior:**
|
|
169
|
-
- New clip is added to the shuffle bag
|
|
170
|
-
- Takes effect on next cycle
|
|
171
|
-
- Doesn't interrupt current playback
|
|
178
|
+
**Behavior:** New clip enters the shuffle pool immediately with `played = false`. Takes effect on the current or next selection — does not interrupt current playback.
|
|
172
179
|
|
|
173
180
|
---
|
|
174
181
|
|
|
175
182
|
### `destroy()`
|
|
176
183
|
|
|
177
|
-
|
|
184
|
+
Stop the engine, release the Audio element, and remove all document-level listeners. No-op if called more than once.
|
|
178
185
|
|
|
179
186
|
```javascript
|
|
180
187
|
engine.destroy();
|
|
181
188
|
```
|
|
182
189
|
|
|
183
190
|
**Behavior:**
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
-
|
|
191
|
+
- Cancels pending timer
|
|
192
|
+
- Pauses current clip
|
|
193
|
+
- Removes `visibilitychange` listener
|
|
194
|
+
- Releases Audio element via `removeAttribute('src')` + `load()` (WHATWG spec)
|
|
195
|
+
- All subsequent method calls are safe no-ops
|
|
188
196
|
|
|
189
197
|
**React Example:**
|
|
190
198
|
```javascript
|
|
@@ -198,7 +206,7 @@ useEffect(() => {
|
|
|
198
206
|
|
|
199
207
|
### `canPlay(type)` — Static Method
|
|
200
208
|
|
|
201
|
-
Check if browser supports a format.
|
|
209
|
+
Check if browser supports a format before constructing.
|
|
202
210
|
|
|
203
211
|
```javascript
|
|
204
212
|
if (AudioEngine.canPlay('audio/ogg')) {
|
|
@@ -209,7 +217,7 @@ if (AudioEngine.canPlay('audio/ogg')) {
|
|
|
209
217
|
```
|
|
210
218
|
|
|
211
219
|
**Supported types:**
|
|
212
|
-
- `'audio/mpeg'`
|
|
220
|
+
- `'audio/mpeg'` — MP3
|
|
213
221
|
- `'audio/ogg'` — OGG Vorbis
|
|
214
222
|
- `'audio/wav'` — WAV
|
|
215
223
|
- `'audio/webm'` — WebM
|
|
@@ -221,9 +229,9 @@ if (AudioEngine.canPlay('audio/ogg')) {
|
|
|
221
229
|
|
|
222
230
|
### `isStarted`
|
|
223
231
|
|
|
224
|
-
**Type:** `boolean` (read-only)
|
|
232
|
+
**Type:** `boolean` (read-only getter)
|
|
225
233
|
|
|
226
|
-
`true` if engine
|
|
234
|
+
`true` if engine has been started, `false` if stopped or not yet started.
|
|
227
235
|
|
|
228
236
|
```javascript
|
|
229
237
|
const engine = new AudioEngine([...]);
|
|
@@ -237,79 +245,75 @@ console.log(engine.isStarted); // false
|
|
|
237
245
|
|
|
238
246
|
---
|
|
239
247
|
|
|
248
|
+
### `isPlaying`
|
|
249
|
+
|
|
250
|
+
**Type:** `boolean` (read-only getter)
|
|
251
|
+
|
|
252
|
+
`true` while a clip is actively playing, `false` between clips or when stopped.
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
engine.start();
|
|
256
|
+
// Shortly after...
|
|
257
|
+
console.log(engine.isPlaying); // true (clip playing)
|
|
258
|
+
// During inter-clip gap...
|
|
259
|
+
console.log(engine.isPlaying); // false (waiting)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
240
264
|
## Callbacks
|
|
241
265
|
|
|
242
|
-
### `onPlay()`
|
|
266
|
+
### `onPlay(clip)`
|
|
243
267
|
|
|
244
268
|
Called when a clip **starts** playback.
|
|
245
269
|
|
|
246
270
|
```javascript
|
|
247
271
|
const engine = new AudioEngine([...], {
|
|
248
|
-
onPlay: () => {
|
|
249
|
-
console.log('Clip started');
|
|
272
|
+
onPlay: (clip) => {
|
|
273
|
+
console.log('Clip started:', clip.label);
|
|
250
274
|
updateUI('playing');
|
|
251
275
|
}
|
|
252
276
|
});
|
|
253
277
|
```
|
|
254
278
|
|
|
255
|
-
**
|
|
279
|
+
**Parameter:** `clip` — the clip object `{ src, label }` currently playing.
|
|
256
280
|
|
|
257
|
-
**Error handling:** Errors are caught and logged. A throwing `onPlay`
|
|
258
|
-
|
|
259
|
-
```javascript
|
|
260
|
-
onPlay: () => {
|
|
261
|
-
throw new Error('Oops!'); // Caught, logged, won't break engine
|
|
262
|
-
}
|
|
263
|
-
```
|
|
281
|
+
**Error handling:** Errors are caught and logged. A throwing `onPlay` will not stall the engine.
|
|
264
282
|
|
|
265
283
|
---
|
|
266
284
|
|
|
267
|
-
### `onEnd()`
|
|
285
|
+
### `onEnd(clip)`
|
|
268
286
|
|
|
269
|
-
Called when a clip **finishes naturally** (reaches end).
|
|
270
|
-
|
|
271
|
-
Does **not** fire if you call `stop()` before clip ends.
|
|
287
|
+
Called when a clip **finishes naturally** (reaches end). Does not fire if `stop()` is called before the clip ends.
|
|
272
288
|
|
|
273
289
|
```javascript
|
|
274
290
|
const engine = new AudioEngine([...], {
|
|
275
|
-
onEnd: () => {
|
|
276
|
-
console.log('
|
|
291
|
+
onEnd: (clip) => {
|
|
292
|
+
console.log('Finished:', clip.label);
|
|
277
293
|
}
|
|
278
294
|
});
|
|
279
295
|
```
|
|
280
296
|
|
|
281
|
-
**Timing:** Fires after clip ends, before next clip
|
|
282
|
-
|
|
283
|
-
**Error handling:** Same as `onPlay()` — errors are caught.
|
|
297
|
+
**Timing:** Fires after the clip ends, before the next inter-clip delay starts.
|
|
284
298
|
|
|
285
299
|
---
|
|
286
300
|
|
|
287
301
|
### `onCycleReset()`
|
|
288
302
|
|
|
289
|
-
Called when all clips have played and the shuffle bag resets
|
|
303
|
+
Called when all clips have played once and the shuffle bag resets for a new cycle.
|
|
290
304
|
|
|
291
305
|
```javascript
|
|
292
|
-
const engine = new AudioEngine(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
{ src: 'clip2.mp3', label: 'Two' },
|
|
296
|
-
{ src: 'clip3.mp3', label: 'Three' }
|
|
297
|
-
],
|
|
298
|
-
{
|
|
299
|
-
onCycleReset: () => {
|
|
300
|
-
console.log('Completed full cycle, starting again');
|
|
301
|
-
}
|
|
306
|
+
const engine = new AudioEngine(clips, {
|
|
307
|
+
onCycleReset: () => {
|
|
308
|
+
console.log('Full cycle complete — starting again');
|
|
302
309
|
}
|
|
303
|
-
);
|
|
310
|
+
});
|
|
304
311
|
```
|
|
305
312
|
|
|
306
313
|
**Behavior:**
|
|
307
314
|
- Fires when all N clips have played once
|
|
308
|
-
-
|
|
309
|
-
-
|
|
310
|
-
- Clip that ended current cycle won't be first clip of next cycle (unless only 1 clip)
|
|
311
|
-
|
|
312
|
-
**Error handling:** Wrapped in try/catch (v2.4.0+).
|
|
315
|
+
- The clip that ended the previous cycle will not be the first clip of the next cycle (with 2+ clips)
|
|
316
|
+
- Error handling: wrapped in try/catch
|
|
313
317
|
|
|
314
318
|
---
|
|
315
319
|
|
|
@@ -326,7 +330,7 @@ Cycle 3: [Clip A, Clip C, Clip B]
|
|
|
326
330
|
|
|
327
331
|
**Not:**
|
|
328
332
|
```
|
|
329
|
-
❌
|
|
333
|
+
❌ Pure random: [A, A, B, C, A, A, C]
|
|
330
334
|
```
|
|
331
335
|
|
|
332
336
|
**Benefits:**
|
|
@@ -338,36 +342,34 @@ Cycle 3: [Clip A, Clip C, Clip B]
|
|
|
338
342
|
|
|
339
343
|
## Background Tab Throttling Mitigation
|
|
340
344
|
|
|
341
|
-
Browsers throttle
|
|
345
|
+
Browsers throttle `setTimeout` in background tabs (Chrome/Firefox: ~1Hz; some mobile power-saving modes may suspend entirely).
|
|
342
346
|
|
|
343
|
-
|
|
344
|
-
- You set `maxTime: 5000` (5-second max delay)
|
|
345
|
-
- Tab goes to background for 10 seconds
|
|
346
|
-
- When tab returns, engine recalculates
|
|
347
|
-
- If 5+ seconds have elapsed, next clip plays immediately
|
|
348
|
-
- Otherwise, remaining time is scheduled
|
|
347
|
+
This engine compensates using the Page Visibility API:
|
|
349
348
|
|
|
350
|
-
|
|
349
|
+
- When the tab returns to the foreground and a timer is pending, the elapsed wall-clock time is compared to the intended delay
|
|
350
|
+
- If the delay has already elapsed, `#playNext()` fires immediately
|
|
351
|
+
- Otherwise, a precise reschedule is set for the remaining duration
|
|
352
|
+
|
|
353
|
+
**Result:** Inter-clip timing recovers cleanly after backgrounding without bunching up missed clips.
|
|
351
354
|
|
|
352
355
|
---
|
|
353
356
|
|
|
354
357
|
## Usage Patterns
|
|
355
358
|
|
|
356
|
-
### Ambient Soundscape
|
|
359
|
+
### Ambient Soundscape
|
|
357
360
|
|
|
358
361
|
```javascript
|
|
359
362
|
const ambient = new AudioEngine([
|
|
360
|
-
{ src: 'ambient/wind.mp3',
|
|
361
|
-
{ src: 'ambient/birds.mp3',
|
|
363
|
+
{ src: 'ambient/wind.mp3', label: 'Wind' },
|
|
364
|
+
{ src: 'ambient/birds.mp3', label: 'Birds' },
|
|
362
365
|
{ src: 'ambient/rustling.mp3', label: 'Rustling' }
|
|
363
366
|
], {
|
|
364
|
-
lowTime:
|
|
365
|
-
maxTime:
|
|
367
|
+
lowTime: 2,
|
|
368
|
+
maxTime: 8,
|
|
366
369
|
volume: 0.6,
|
|
367
370
|
onCycleReset: () => console.log('Ambient cycle complete')
|
|
368
371
|
});
|
|
369
372
|
|
|
370
|
-
// Start on game begin
|
|
371
373
|
startButton.addEventListener('click', () => ambient.start());
|
|
372
374
|
```
|
|
373
375
|
|
|
@@ -378,12 +380,10 @@ startButton.addEventListener('click', () => ambient.start());
|
|
|
378
380
|
```javascript
|
|
379
381
|
const engine = new AudioEngine([...], { volume: 0.5 });
|
|
380
382
|
|
|
381
|
-
// Slider control
|
|
382
383
|
volumeSlider.addEventListener('input', (e) => {
|
|
383
384
|
engine.setVolume(e.target.value / 100);
|
|
384
385
|
});
|
|
385
386
|
|
|
386
|
-
// Fade out on game pause
|
|
387
387
|
pauseButton.addEventListener('click', () => {
|
|
388
388
|
engine.setVolume(0);
|
|
389
389
|
engine.stop();
|
|
@@ -402,7 +402,7 @@ const engine = new AudioEngine([
|
|
|
402
402
|
|
|
403
403
|
// User unlocks new sounds
|
|
404
404
|
unlockedSounds.forEach(sound => {
|
|
405
|
-
engine.addClip(sound.path, sound.name);
|
|
405
|
+
engine.addClip({ src: sound.path, label: sound.name });
|
|
406
406
|
});
|
|
407
407
|
```
|
|
408
408
|
|
|
@@ -412,7 +412,6 @@ unlockedSounds.forEach(sound => {
|
|
|
412
412
|
|
|
413
413
|
```javascript
|
|
414
414
|
import { useEffect } from 'react';
|
|
415
|
-
import AudioEngine from './OpenAudio_r.js';
|
|
416
415
|
|
|
417
416
|
export default function AmbientPlayer() {
|
|
418
417
|
useEffect(() => {
|
|
@@ -420,13 +419,12 @@ export default function AmbientPlayer() {
|
|
|
420
419
|
{ src: 'audio/clip1.mp3', label: 'One' },
|
|
421
420
|
{ src: 'audio/clip2.mp3', label: 'Two' }
|
|
422
421
|
], {
|
|
423
|
-
onPlay: () =>
|
|
424
|
-
onEnd: () => setIsPlaying(false)
|
|
422
|
+
onPlay: (clip) => console.log('Playing:', clip.label)
|
|
425
423
|
});
|
|
426
424
|
|
|
427
425
|
document.addEventListener('click', () => engine.start(), { once: true });
|
|
428
426
|
|
|
429
|
-
return () => engine.destroy(); //
|
|
427
|
+
return () => engine.destroy(); // Releases audio, removes listeners on unmount
|
|
430
428
|
}, []);
|
|
431
429
|
|
|
432
430
|
return <div>Audio Engine Ready</div>;
|
|
@@ -439,20 +437,18 @@ export default function AmbientPlayer() {
|
|
|
439
437
|
|
|
440
438
|
### Audio Won't Play (Silent)
|
|
441
439
|
|
|
442
|
-
**Problem:** `start()` is called but nothing happens.
|
|
443
|
-
|
|
444
440
|
**Causes:**
|
|
445
|
-
1.
|
|
441
|
+
1. `start()` called outside a user gesture
|
|
446
442
|
2. CORS or mixed-content issue
|
|
447
|
-
3. Browser doesn't support audio format
|
|
448
|
-
4. Audio file
|
|
443
|
+
3. Browser doesn't support the audio format
|
|
444
|
+
4. Audio file not found (404)
|
|
449
445
|
|
|
450
446
|
**Solutions:**
|
|
451
447
|
```javascript
|
|
452
448
|
// ✅ Correct: inside gesture
|
|
453
449
|
document.addEventListener('click', () => engine.start(), { once: true });
|
|
454
450
|
|
|
455
|
-
// ❌ Wrong: no gesture
|
|
451
|
+
// ❌ Wrong: no gesture context
|
|
456
452
|
setTimeout(() => engine.start(), 1000);
|
|
457
453
|
|
|
458
454
|
// ✅ Check format support
|
|
@@ -465,20 +461,14 @@ if (!AudioEngine.canPlay('audio/ogg')) {
|
|
|
465
461
|
|
|
466
462
|
### "NotAllowedError" in Console
|
|
467
463
|
|
|
468
|
-
**Problem:** `NotAllowedError: play() failed due to autoplay policy`
|
|
469
|
-
|
|
470
464
|
**Cause:** `start()` not called inside a user gesture.
|
|
471
|
-
|
|
472
|
-
**Solution:** Wrap `start()` in a click, keydown, or touch event.
|
|
465
|
+
**Solution:** Wrap `start()` in a click, keydown, or touchstart handler.
|
|
473
466
|
|
|
474
467
|
---
|
|
475
468
|
|
|
476
|
-
### Stale Listeners in React
|
|
477
|
-
|
|
478
|
-
**Problem:** Multiple engine instances leave listeners behind.
|
|
479
|
-
|
|
480
|
-
**Cause:** Not calling `destroy()` on unmount.
|
|
469
|
+
### Stale Listeners in React / SPA
|
|
481
470
|
|
|
471
|
+
**Cause:** Not calling `destroy()` on component unmount.
|
|
482
472
|
**Solution:**
|
|
483
473
|
```javascript
|
|
484
474
|
useEffect(() => {
|
|
@@ -507,55 +497,57 @@ useEffect(() => {
|
|
|
507
497
|
- **File Size:** ~9 KB (minified)
|
|
508
498
|
- **Gzipped:** ~3 KB
|
|
509
499
|
- **Runtime Memory:** < 200 KB
|
|
510
|
-
- **CPU:** Minimal (
|
|
500
|
+
- **CPU:** Minimal (HTML5 Audio API only)
|
|
511
501
|
|
|
512
|
-
|
|
513
|
-
```javascript
|
|
514
|
-
const forest = new AudioEngine([...]);
|
|
515
|
-
const city = new AudioEngine([...]);
|
|
516
|
-
const space = new AudioEngine([...]);
|
|
517
|
-
// All three use minimal resources
|
|
518
|
-
```
|
|
502
|
+
Multiple engines can run simultaneously without conflict.
|
|
519
503
|
|
|
520
504
|
---
|
|
521
505
|
|
|
522
506
|
## Changelog
|
|
523
507
|
|
|
524
|
-
### v2.
|
|
508
|
+
### v2.6.0 (March 2026)
|
|
509
|
+
- All constructor and `addClip()` validation throws changed from `Error` to `TypeError`, matching ECMAScript convention and the behaviour of `OpenAudio.js` and `OpenAudio_s.js`. Callers catching errors via `instanceof TypeError` now correctly catch `AudioEngine` validation failures.
|
|
510
|
+
- `#isStarted` is now set `true` inside the unlock `.then()` (after the Audio element is confirmed usable), not at the top of `start()` before the unlock. The duplicate-start guard during the unlock window is handled by `#isUnlocking`. This aligns state machine semantics with `OpenAudio_s.js`.
|
|
511
|
+
- Expanded three compact single-line `try/catch` blocks (in the `onended` handler, `#resetCycle()`, and `#playNext()` `.then()`) to multi-line style, matching the formatting convention used throughout the suite.
|
|
512
|
+
|
|
513
|
+
### v2.5.0 (March 2026)
|
|
514
|
+
- `isStarted` and `isPlaying` are now read-only getters backed by private fields
|
|
515
|
+
- `#isDestroyed` flag — all public methods are safe no-ops after `destroy()`
|
|
516
|
+
- `destroy()` releases Audio element via `removeAttribute('src')` + `load()` (WHATWG spec)
|
|
517
|
+
- `#isPlaying` set synchronously before `#audio.play()` in `#playNext()`, closing race window
|
|
518
|
+
|
|
519
|
+
### v2.4.1 (March 2026)
|
|
520
|
+
- Licence changed from GPL-3.0-or-later to Apache-2.0
|
|
521
|
+
|
|
522
|
+
### v2.4.0 (March 2026)
|
|
525
523
|
- `#isUnlocking` flag prevents spam-click race conditions
|
|
526
|
-
- `destroy()`
|
|
524
|
+
- `destroy()` removes `visibilitychange` listener properly
|
|
527
525
|
- `canPlay()` static format checking
|
|
528
526
|
- `onCycleReset` callback wrapped in try/catch
|
|
529
|
-
- Comprehensive documentation
|
|
530
527
|
|
|
531
|
-
### v2.3.0 (February
|
|
528
|
+
### v2.3.0 (February 2026)
|
|
532
529
|
- Clip src validation
|
|
533
530
|
- Next-clip prefetch (eliminates network gaps)
|
|
534
531
|
- Background tab throttling mitigation
|
|
535
|
-
- Wall-clock time recalculation
|
|
536
532
|
|
|
537
|
-
### v2.2.0 (January
|
|
538
|
-
- True private fields (
|
|
533
|
+
### v2.2.0 (January 2026)
|
|
534
|
+
- True private fields (`#`)
|
|
539
535
|
- NotAllowedError handling
|
|
540
536
|
- Silent base64 unlock
|
|
541
537
|
- `setVolume()` runtime control
|
|
542
538
|
|
|
543
|
-
### v2.1.0 (December
|
|
539
|
+
### v2.1.0 (December 2025)
|
|
544
540
|
- Single reusable Audio element
|
|
545
|
-
- Mobile autoplay fix (iOS)
|
|
546
541
|
- `stop()` race condition fix
|
|
547
542
|
|
|
548
|
-
### v2.0.0 (October
|
|
549
|
-
- Initial public release
|
|
550
|
-
- Shuffle bag algorithm
|
|
551
|
-
- Callbacks (onPlay, onEnd, onCycleReset)
|
|
552
|
-
- `addClip()` runtime insertion
|
|
543
|
+
### v2.0.0 (October 2025)
|
|
544
|
+
- Initial public release. Shuffle bag algorithm. Callbacks. `addClip()`.
|
|
553
545
|
|
|
554
546
|
---
|
|
555
547
|
|
|
556
548
|
## License
|
|
557
549
|
|
|
558
|
-
|
|
550
|
+
Apache License 2.0. See [LICENSE](../LICENSE).
|
|
559
551
|
|
|
560
552
|
---
|
|
561
553
|
|
|
@@ -568,4 +560,4 @@ GNU General Public License v3.0 or later. See [LICENSE](../LICENSE).
|
|
|
568
560
|
|
|
569
561
|
---
|
|
570
562
|
|
|
571
|
-
*Last updated: March
|
|
563
|
+
*Last updated: March 2026 — v2.6.0*
|