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.
@@ -1,7 +1,7 @@
1
1
  # OpenAudio_r.js API Reference
2
2
 
3
- **Version:** 2.4.0
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: 3000, // Min delay between clips (ms)
18
- maxTime: 5000, // Max delay between clips (ms)
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', label: 'Rain Sounds' }
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: 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
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 - REQUIRED)
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 playing: ignored (safe)
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 starts scheduling
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 reset the engine.
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
- - Resets timer
122
- - Next `start()` begins a fresh cycle
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 clip
125
+ engine.start(); // Playing
126
126
  engine.stop(); // Stopped
127
- engine.start(); // Starts new cycle
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 (runtime control).
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(src, label)`
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
- Clean up and remove listeners (essential for SPAs).
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
- - Removes visibilitychange listener
185
- - Stops playback
186
- - Releases audio element
187
- - After this, don't call other methods
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'` or `'audio/mp3'` — MP3
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 is currently running, `false` if stopped.
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
- **Timing:** Fires after silent unlock completes.
279
+ **Parameter:** `clip` the clip object `{ src, label }` currently playing.
256
280
 
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
- ```
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('Clip finished, scheduling next');
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's delay starts.
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 (new cycle begins).
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
- { 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
- }
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
- - 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+).
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
- Random repetition: [A, A, B, C, A]
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 timers in background tabs. This engine detects and compensates.
345
+ Browsers throttle `setTimeout` in background tabs (Chrome/Firefox: ~1Hz; some mobile power-saving modes may suspend entirely).
342
346
 
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
347
+ This engine compensates using the Page Visibility API:
349
348
 
350
- **Result:** Audio doesn't bunch up when tab returns.
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 (Game)
359
+ ### Ambient Soundscape
357
360
 
358
361
  ```javascript
359
362
  const ambient = new AudioEngine([
360
- { src: 'ambient/wind.mp3', label: 'Wind' },
361
- { src: 'ambient/birds.mp3', label: 'Birds' },
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: 2000,
365
- maxTime: 8000,
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: () => setIsPlaying(true),
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(); // Clean up on unmount
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. Called outside a user gesture
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 doesn't exist (404)
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 (just HTML5 Audio API)
500
+ - **CPU:** Minimal (HTML5 Audio API only)
511
501
 
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
- ```
502
+ Multiple engines can run simultaneously without conflict.
519
503
 
520
504
  ---
521
505
 
522
506
  ## Changelog
523
507
 
524
- ### v2.4.0 (March 2025)
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()` method removes listeners properly (SPA fix)
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 2025)
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 2025)
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 2024)
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 2024)
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
- GNU General Public License v3.0 or later. See [LICENSE](../LICENSE).
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 2025*
563
+ *Last updated: March 2026 — v2.6.0*