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.
@@ -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*