openaudio-suite 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +196 -0
- package/CONTRIBUTING.md +349 -0
- package/LICENSE +674 -0
- package/OpenAudio.js +386 -0
- package/OpenAudio_r.js +863 -0
- package/OpenAudio_s.js +445 -0
- package/README.md +433 -0
- package/docs/COMPARISON.md +449 -0
- package/docs/OPENAUDIO-v1.1.0.md +641 -0
- package/docs/OPENAUDIO_R.md +571 -0
- package/docs/OPENAUDIO_S.md +760 -0
- package/package.json +79 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
# OpenAudio_r.js API Reference
|
|
2
|
+
|
|
3
|
+
**Version:** 2.4.0
|
|
4
|
+
**Class:** `AudioEngine`
|
|
5
|
+
**Use Case:** Randomized multi-clip scheduler with shuffle bag algorithm
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
const engine = new AudioEngine([
|
|
13
|
+
{ src: 'audio/forest.mp3', label: 'Forest' },
|
|
14
|
+
{ src: 'audio/rain.mp3', label: 'Rain' },
|
|
15
|
+
{ src: 'audio/birds.mp3', label: 'Birds' }
|
|
16
|
+
], {
|
|
17
|
+
lowTime: 3000, // Min delay between clips (ms)
|
|
18
|
+
maxTime: 5000, // Max delay between clips (ms)
|
|
19
|
+
volume: 0.8,
|
|
20
|
+
onPlay: () => console.log('Playing'),
|
|
21
|
+
onEnd: () => console.log('Finished'),
|
|
22
|
+
onCycleReset: () => console.log('New cycle')
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Start on first user gesture
|
|
26
|
+
document.addEventListener('click', () => engine.start(), { once: true });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Constructor
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
new AudioEngine(clips, options)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Parameters
|
|
38
|
+
|
|
39
|
+
#### `clips` (array, required)
|
|
40
|
+
Array of audio clip objects. Each clip must have:
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
{
|
|
44
|
+
src: 'path/to/audio.mp3', // Required: URL or data URI
|
|
45
|
+
label: 'Clip Name' // Optional: name for logging
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Examples:**
|
|
50
|
+
```javascript
|
|
51
|
+
// Minimal
|
|
52
|
+
const engine = new AudioEngine([
|
|
53
|
+
{ src: 'audio/clip1.mp3' },
|
|
54
|
+
{ src: 'audio/clip2.mp3' }
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
// With labels
|
|
58
|
+
const engine = new AudioEngine([
|
|
59
|
+
{ src: 'ambient/forest.mp3', label: 'Forest Ambience' },
|
|
60
|
+
{ src: 'ambient/rain.mp3', label: 'Rain Sounds' }
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
// With data URIs
|
|
64
|
+
const engine = new AudioEngine([
|
|
65
|
+
{ src: 'data:audio/mp3;base64,SUQzBA...', label: 'Embedded' }
|
|
66
|
+
]);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### `options` (object, optional)
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
{
|
|
73
|
+
lowTime: 3000, // Min delay (ms), default: 3000
|
|
74
|
+
maxTime: 5000, // Max delay (ms), default: 5000
|
|
75
|
+
volume: 0.8, // Volume 0.0–1.0, default: 1.0
|
|
76
|
+
onPlay: fn, // Callback when clip starts
|
|
77
|
+
onEnd: fn, // Callback when clip ends
|
|
78
|
+
onCycleReset: fn // Callback when cycle resets
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**All options are optional.**
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Public Methods
|
|
87
|
+
|
|
88
|
+
### `start()`
|
|
89
|
+
|
|
90
|
+
Begin playback. Must be called inside a user gesture on first use.
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
// First call (inside gesture - REQUIRED)
|
|
94
|
+
document.addEventListener('click', () => engine.start(), { once: true });
|
|
95
|
+
|
|
96
|
+
// Subsequent calls
|
|
97
|
+
engine.start(); // Safe to call anytime
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Behavior:**
|
|
101
|
+
- If already playing: ignored (safe)
|
|
102
|
+
- If stopped: resumes cycle from next clip
|
|
103
|
+
- First call: plays silent unlock MP3, then starts scheduling
|
|
104
|
+
|
|
105
|
+
**Browser Autoplay Policy:**
|
|
106
|
+
- ✅ Works: click, keydown, touchstart, mousedown
|
|
107
|
+
- ❌ Doesn't work: scroll, page load, setTimeout
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `stop()`
|
|
112
|
+
|
|
113
|
+
Stop playback and reset the engine.
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
engine.stop();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Behavior:**
|
|
120
|
+
- Pauses current clip
|
|
121
|
+
- Resets timer
|
|
122
|
+
- Next `start()` begins a fresh cycle
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
engine.start(); // Playing clip
|
|
126
|
+
engine.stop(); // Stopped
|
|
127
|
+
engine.start(); // Starts new cycle
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `setVolume(value)`
|
|
133
|
+
|
|
134
|
+
Change volume during playback (runtime control).
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
engine.setVolume(0.5); // 50% volume
|
|
138
|
+
engine.setVolume(1.0); // Full volume
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Parameter:**
|
|
142
|
+
- `value` (number) — Volume 0.0–1.0
|
|
143
|
+
- Clamped to valid range automatically
|
|
144
|
+
|
|
145
|
+
**Example:**
|
|
146
|
+
```javascript
|
|
147
|
+
// Volume slider
|
|
148
|
+
document.getElementById('volume-slider').addEventListener('input', (e) => {
|
|
149
|
+
engine.setVolume(e.target.value / 100);
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### `addClip(src, label)`
|
|
156
|
+
|
|
157
|
+
Add a clip to the engine at runtime.
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
engine.addClip('audio/new-clip.mp3');
|
|
161
|
+
engine.addClip('audio/new-clip.mp3', 'New Sound');
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Parameters:**
|
|
165
|
+
- `src` (string) — Audio URL or data URI
|
|
166
|
+
- `label` (string, optional) — Name for logging
|
|
167
|
+
|
|
168
|
+
**Behavior:**
|
|
169
|
+
- New clip is added to the shuffle bag
|
|
170
|
+
- Takes effect on next cycle
|
|
171
|
+
- Doesn't interrupt current playback
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### `destroy()`
|
|
176
|
+
|
|
177
|
+
Clean up and remove listeners (essential for SPAs).
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
engine.destroy();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Behavior:**
|
|
184
|
+
- Removes visibilitychange listener
|
|
185
|
+
- Stops playback
|
|
186
|
+
- Releases audio element
|
|
187
|
+
- After this, don't call other methods
|
|
188
|
+
|
|
189
|
+
**React Example:**
|
|
190
|
+
```javascript
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const engine = new AudioEngine([...]);
|
|
193
|
+
return () => engine.destroy(); // Clean up on unmount
|
|
194
|
+
}, []);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### `canPlay(type)` — Static Method
|
|
200
|
+
|
|
201
|
+
Check if browser supports a format.
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
if (AudioEngine.canPlay('audio/ogg')) {
|
|
205
|
+
// Use .ogg files
|
|
206
|
+
} else {
|
|
207
|
+
// Use .mp3 fallback
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Supported types:**
|
|
212
|
+
- `'audio/mpeg'` or `'audio/mp3'` — MP3
|
|
213
|
+
- `'audio/ogg'` — OGG Vorbis
|
|
214
|
+
- `'audio/wav'` — WAV
|
|
215
|
+
- `'audio/webm'` — WebM
|
|
216
|
+
- `'audio/flac'` — FLAC
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Public Properties
|
|
221
|
+
|
|
222
|
+
### `isStarted`
|
|
223
|
+
|
|
224
|
+
**Type:** `boolean` (read-only)
|
|
225
|
+
|
|
226
|
+
`true` if engine is currently running, `false` if stopped.
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
const engine = new AudioEngine([...]);
|
|
230
|
+
|
|
231
|
+
console.log(engine.isStarted); // false
|
|
232
|
+
engine.start();
|
|
233
|
+
console.log(engine.isStarted); // true
|
|
234
|
+
engine.stop();
|
|
235
|
+
console.log(engine.isStarted); // false
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Callbacks
|
|
241
|
+
|
|
242
|
+
### `onPlay()`
|
|
243
|
+
|
|
244
|
+
Called when a clip **starts** playback.
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
const engine = new AudioEngine([...], {
|
|
248
|
+
onPlay: () => {
|
|
249
|
+
console.log('Clip started');
|
|
250
|
+
updateUI('playing');
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Timing:** Fires after silent unlock completes.
|
|
256
|
+
|
|
257
|
+
**Error handling:** Errors are caught and logged. A throwing `onPlay` won't stall the engine.
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
onPlay: () => {
|
|
261
|
+
throw new Error('Oops!'); // Caught, logged, won't break engine
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### `onEnd()`
|
|
268
|
+
|
|
269
|
+
Called when a clip **finishes naturally** (reaches end).
|
|
270
|
+
|
|
271
|
+
Does **not** fire if you call `stop()` before clip ends.
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
const engine = new AudioEngine([...], {
|
|
275
|
+
onEnd: () => {
|
|
276
|
+
console.log('Clip finished, scheduling next');
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Timing:** Fires after clip ends, before next clip's delay starts.
|
|
282
|
+
|
|
283
|
+
**Error handling:** Same as `onPlay()` — errors are caught.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### `onCycleReset()`
|
|
288
|
+
|
|
289
|
+
Called when all clips have played and the shuffle bag resets (new cycle begins).
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
const engine = new AudioEngine(
|
|
293
|
+
[
|
|
294
|
+
{ src: 'clip1.mp3', label: 'One' },
|
|
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
|
+
}
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Behavior:**
|
|
307
|
+
- Fires when all N clips have played once
|
|
308
|
+
- Shuffle bag is reset with same N clips
|
|
309
|
+
- Cycle can start with any clip (no repeat rule)
|
|
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+).
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Shuffle Bag Algorithm
|
|
317
|
+
|
|
318
|
+
Each cycle, every clip plays exactly once before repeating.
|
|
319
|
+
|
|
320
|
+
**Example (3 clips):**
|
|
321
|
+
```
|
|
322
|
+
Cycle 1: [Clip B, Clip A, Clip C]
|
|
323
|
+
Cycle 2: [Clip C, Clip B, Clip A]
|
|
324
|
+
Cycle 3: [Clip A, Clip C, Clip B]
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Not:**
|
|
328
|
+
```
|
|
329
|
+
❌ Random repetition: [A, A, B, C, A]
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Benefits:**
|
|
333
|
+
- Prevents same clip twice in a row
|
|
334
|
+
- Guarantees variety within a cycle
|
|
335
|
+
- Feels more natural than pure random
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Background Tab Throttling Mitigation
|
|
340
|
+
|
|
341
|
+
Browsers throttle timers in background tabs. This engine detects and compensates.
|
|
342
|
+
|
|
343
|
+
**Example:**
|
|
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
|
|
349
|
+
|
|
350
|
+
**Result:** Audio doesn't bunch up when tab returns.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Usage Patterns
|
|
355
|
+
|
|
356
|
+
### Ambient Soundscape (Game)
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
const ambient = new AudioEngine([
|
|
360
|
+
{ src: 'ambient/wind.mp3', label: 'Wind' },
|
|
361
|
+
{ src: 'ambient/birds.mp3', label: 'Birds' },
|
|
362
|
+
{ src: 'ambient/rustling.mp3', label: 'Rustling' }
|
|
363
|
+
], {
|
|
364
|
+
lowTime: 2000,
|
|
365
|
+
maxTime: 8000,
|
|
366
|
+
volume: 0.6,
|
|
367
|
+
onCycleReset: () => console.log('Ambient cycle complete')
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Start on game begin
|
|
371
|
+
startButton.addEventListener('click', () => ambient.start());
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### Dynamic Volume Control
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
const engine = new AudioEngine([...], { volume: 0.5 });
|
|
380
|
+
|
|
381
|
+
// Slider control
|
|
382
|
+
volumeSlider.addEventListener('input', (e) => {
|
|
383
|
+
engine.setVolume(e.target.value / 100);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Fade out on game pause
|
|
387
|
+
pauseButton.addEventListener('click', () => {
|
|
388
|
+
engine.setVolume(0);
|
|
389
|
+
engine.stop();
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
### Add Clips at Runtime
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
const engine = new AudioEngine([
|
|
399
|
+
{ src: 'ambient/base1.mp3' },
|
|
400
|
+
{ src: 'ambient/base2.mp3' }
|
|
401
|
+
]);
|
|
402
|
+
|
|
403
|
+
// User unlocks new sounds
|
|
404
|
+
unlockedSounds.forEach(sound => {
|
|
405
|
+
engine.addClip(sound.path, sound.name);
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### React Component
|
|
412
|
+
|
|
413
|
+
```javascript
|
|
414
|
+
import { useEffect } from 'react';
|
|
415
|
+
import AudioEngine from './OpenAudio_r.js';
|
|
416
|
+
|
|
417
|
+
export default function AmbientPlayer() {
|
|
418
|
+
useEffect(() => {
|
|
419
|
+
const engine = new AudioEngine([
|
|
420
|
+
{ src: 'audio/clip1.mp3', label: 'One' },
|
|
421
|
+
{ src: 'audio/clip2.mp3', label: 'Two' }
|
|
422
|
+
], {
|
|
423
|
+
onPlay: () => setIsPlaying(true),
|
|
424
|
+
onEnd: () => setIsPlaying(false)
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
document.addEventListener('click', () => engine.start(), { once: true });
|
|
428
|
+
|
|
429
|
+
return () => engine.destroy(); // Clean up on unmount
|
|
430
|
+
}, []);
|
|
431
|
+
|
|
432
|
+
return <div>Audio Engine Ready</div>;
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Troubleshooting
|
|
439
|
+
|
|
440
|
+
### Audio Won't Play (Silent)
|
|
441
|
+
|
|
442
|
+
**Problem:** `start()` is called but nothing happens.
|
|
443
|
+
|
|
444
|
+
**Causes:**
|
|
445
|
+
1. Called outside a user gesture
|
|
446
|
+
2. CORS or mixed-content issue
|
|
447
|
+
3. Browser doesn't support audio format
|
|
448
|
+
4. Audio file doesn't exist (404)
|
|
449
|
+
|
|
450
|
+
**Solutions:**
|
|
451
|
+
```javascript
|
|
452
|
+
// ✅ Correct: inside gesture
|
|
453
|
+
document.addEventListener('click', () => engine.start(), { once: true });
|
|
454
|
+
|
|
455
|
+
// ❌ Wrong: no gesture
|
|
456
|
+
setTimeout(() => engine.start(), 1000);
|
|
457
|
+
|
|
458
|
+
// ✅ Check format support
|
|
459
|
+
if (!AudioEngine.canPlay('audio/ogg')) {
|
|
460
|
+
// Use .mp3 instead
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
### "NotAllowedError" in Console
|
|
467
|
+
|
|
468
|
+
**Problem:** `NotAllowedError: play() failed due to autoplay policy`
|
|
469
|
+
|
|
470
|
+
**Cause:** `start()` not called inside a user gesture.
|
|
471
|
+
|
|
472
|
+
**Solution:** Wrap `start()` in a click, keydown, or touch event.
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
### Stale Listeners in React
|
|
477
|
+
|
|
478
|
+
**Problem:** Multiple engine instances leave listeners behind.
|
|
479
|
+
|
|
480
|
+
**Cause:** Not calling `destroy()` on unmount.
|
|
481
|
+
|
|
482
|
+
**Solution:**
|
|
483
|
+
```javascript
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
const engine = new AudioEngine([...]);
|
|
486
|
+
return () => engine.destroy(); // Always clean up
|
|
487
|
+
}, []);
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Browser Compatibility
|
|
493
|
+
|
|
494
|
+
| Browser | Support | Notes |
|
|
495
|
+
|---------|---------|-------|
|
|
496
|
+
| Chrome 70+ | ✅ Full | Autoplay: gesture required |
|
|
497
|
+
| Firefox 65+ | ✅ Full | Same as Chrome |
|
|
498
|
+
| Safari 12+ | ✅ Full | iOS Safari: gesture required |
|
|
499
|
+
| Edge 79+ | ✅ Full | Chromium-based |
|
|
500
|
+
| iOS Safari 12+ | ✅ Full | Gesture required |
|
|
501
|
+
| Chrome Android | ✅ Full | Touchstart counts as gesture |
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Performance
|
|
506
|
+
|
|
507
|
+
- **File Size:** ~9 KB (minified)
|
|
508
|
+
- **Gzipped:** ~3 KB
|
|
509
|
+
- **Runtime Memory:** < 200 KB
|
|
510
|
+
- **CPU:** Minimal (just HTML5 Audio API)
|
|
511
|
+
|
|
512
|
+
Creating multiple engines is fine:
|
|
513
|
+
```javascript
|
|
514
|
+
const forest = new AudioEngine([...]);
|
|
515
|
+
const city = new AudioEngine([...]);
|
|
516
|
+
const space = new AudioEngine([...]);
|
|
517
|
+
// All three use minimal resources
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Changelog
|
|
523
|
+
|
|
524
|
+
### v2.4.0 (March 2025)
|
|
525
|
+
- `#isUnlocking` flag prevents spam-click race conditions
|
|
526
|
+
- `destroy()` method removes listeners properly (SPA fix)
|
|
527
|
+
- `canPlay()` static format checking
|
|
528
|
+
- `onCycleReset` callback wrapped in try/catch
|
|
529
|
+
- Comprehensive documentation
|
|
530
|
+
|
|
531
|
+
### v2.3.0 (February 2025)
|
|
532
|
+
- Clip src validation
|
|
533
|
+
- Next-clip prefetch (eliminates network gaps)
|
|
534
|
+
- Background tab throttling mitigation
|
|
535
|
+
- Wall-clock time recalculation
|
|
536
|
+
|
|
537
|
+
### v2.2.0 (January 2025)
|
|
538
|
+
- True private fields (#)
|
|
539
|
+
- NotAllowedError handling
|
|
540
|
+
- Silent base64 unlock
|
|
541
|
+
- `setVolume()` runtime control
|
|
542
|
+
|
|
543
|
+
### v2.1.0 (December 2024)
|
|
544
|
+
- Single reusable Audio element
|
|
545
|
+
- Mobile autoplay fix (iOS)
|
|
546
|
+
- `stop()` race condition fix
|
|
547
|
+
|
|
548
|
+
### v2.0.0 (October 2024)
|
|
549
|
+
- Initial public release
|
|
550
|
+
- Shuffle bag algorithm
|
|
551
|
+
- Callbacks (onPlay, onEnd, onCycleReset)
|
|
552
|
+
- `addClip()` runtime insertion
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## License
|
|
557
|
+
|
|
558
|
+
GNU General Public License v3.0 or later. See [LICENSE](../LICENSE).
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## See Also
|
|
563
|
+
|
|
564
|
+
- [OpenAudio_s.js API](./OPENAUDIO_S.md) — Sequential player
|
|
565
|
+
- [OpenAudio.js API](./OPENAUDIO.md) — Single-clip player
|
|
566
|
+
- [Feature Comparison](./COMPARISON.md) — Which library to use
|
|
567
|
+
- [Main README](../README.md) — OpenAudio suite overview
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
*Last updated: March 2025*
|