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,641 @@
1
+ # OpenAudio.js API Reference
2
+
3
+ **Version:** 1.1.0
4
+ **Class:** `OpenAudio`
5
+ **Use Case:** Single-clip, one-shot audio playback with background tab awareness
6
+
7
+ ---
8
+
9
+ ## Quick Start
10
+
11
+ ```javascript
12
+ // Create a player
13
+ const player = new OpenAudio('audio/chime.mp3', {
14
+ volume: 0.8,
15
+ label: 'Chime',
16
+ pauseOnHidden: true, // Pause when tab loses focus
17
+ onPlay: () => console.log('Playing'),
18
+ onEnd: () => console.log('Done'),
19
+ onHidden: () => console.log('Tab hidden'),
20
+ onVisible: () => console.log('Tab visible')
21
+ });
22
+
23
+ // Play on user interaction
24
+ document.getElementById('btn').addEventListener('click', () => player.play());
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Constructor
30
+
31
+ ```javascript
32
+ new OpenAudio(src, options)
33
+ ```
34
+
35
+ ### Parameters
36
+
37
+ #### `src` (string, required)
38
+ Path or data URI to your audio file.
39
+
40
+ ```javascript
41
+ // File path
42
+ new OpenAudio('audio/sound.mp3');
43
+
44
+ // Absolute URL
45
+ new OpenAudio('https://cdn.example.com/audio/sound.mp3');
46
+
47
+ // Data URI (embedded, no external file)
48
+ new OpenAudio('data:audio/mp3;base64,SUQzBA...');
49
+ ```
50
+
51
+ #### `options` (object, optional)
52
+
53
+ ```javascript
54
+ {
55
+ volume: 0.8, // Number: 0.0–1.0 (default: 1.0)
56
+ label: 'My Sound', // String: display name for console messages
57
+ pauseOnHidden: false, // Boolean: pause when tab hides (default: false)
58
+ onPlay: () => {}, // Function: called when playback starts
59
+ onEnd: () => {}, // Function: called when playback ends
60
+ onHidden: () => {}, // Function: called when tab becomes hidden
61
+ onVisible: () => {} // Function: called when tab becomes visible
62
+ }
63
+ ```
64
+
65
+ **All options are optional.** You can pass an empty object `{}` or omit it entirely.
66
+
67
+ ### Constructor Examples
68
+
69
+ ```javascript
70
+ // Minimal
71
+ const player = new OpenAudio('audio/click.mp3');
72
+
73
+ // With volume
74
+ const player = new OpenAudio('audio/click.mp3', { volume: 0.5 });
75
+
76
+ // With callbacks
77
+ const player = new OpenAudio('audio/victory.mp3', {
78
+ volume: 0.9,
79
+ label: 'Victory Fanfare',
80
+ onPlay: () => console.log('Victory sound playing'),
81
+ onEnd: () => console.log('Victory sound finished')
82
+ });
83
+
84
+ // With background tab control
85
+ const player = new OpenAudio('audio/ambient.mp3', {
86
+ volume: 0.75,
87
+ label: 'Ambient Sound',
88
+ pauseOnHidden: true, // Pause when tab loses focus
89
+ onPlay: () => updateUI('sound_playing'),
90
+ onHidden: () => updateUI('background'),
91
+ onVisible: () => updateUI('foreground')
92
+ });
93
+
94
+ // Callbacks without pause-on-hide (just notifications)
95
+ const player = new OpenAudio('audio/notification.mp3', {
96
+ onHidden: () => console.log('Tab hidden, audio continues'),
97
+ onVisible: () => console.log('Tab visible again')
98
+ });
99
+ ```
100
+
101
+ ### Constructor Errors
102
+
103
+ ```javascript
104
+ // Throws TypeError if src is missing or not a string
105
+ new OpenAudio(); // ❌ TypeError
106
+ new OpenAudio(123); // ❌ TypeError
107
+ new OpenAudio(''); // ❌ TypeError
108
+ new OpenAudio(null); // ❌ TypeError
109
+
110
+ // Valid
111
+ new OpenAudio('audio.mp3'); // ✅
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Public Methods
117
+
118
+ ### `play()`
119
+
120
+ Unlock the audio element (if needed) and play the clip.
121
+
122
+ **Must be called inside a user gesture on first use.**
123
+
124
+ ```javascript
125
+ // First call (inside gesture)
126
+ document.addEventListener('click', () => {
127
+ player.play(); // ✅ Starts playback
128
+ });
129
+
130
+ // Subsequent calls
131
+ player.play(); // Safe to call anytime
132
+ ```
133
+
134
+ **Behavior:**
135
+ - If already playing: ignored (safe)
136
+ - If paused: resumes from current position
137
+ - If finished: restarts from beginning
138
+
139
+ ---
140
+
141
+ ### `stop()`
142
+
143
+ Stop playback and rewind to the start.
144
+
145
+ ```javascript
146
+ player.stop();
147
+ ```
148
+
149
+ **Behavior:**
150
+ - Pauses the audio
151
+ - Rewinds to 0:00
152
+ - Sets `isPlaying = false`
153
+ - Clears any pause-on-hidden state
154
+ - Calling `play()` after restarts from beginning
155
+
156
+ ```javascript
157
+ player.play(); // Playing at 2:30
158
+ player.stop(); // Paused at 0:00
159
+ player.play(); // Restart from 0:00
160
+ ```
161
+
162
+ ---
163
+
164
+ ### `destroy()`
165
+
166
+ Remove all listeners and clean up the audio element.
167
+
168
+ **Call this on SPA component unmount.**
169
+
170
+ ```javascript
171
+ player.destroy();
172
+ ```
173
+
174
+ **Behavior:**
175
+ - Removes the `visibilitychange` listener (prevents stale listeners in SPAs)
176
+ - Stops playback
177
+ - Releases the audio element
178
+ - After this, do not call any other methods
179
+
180
+ **Why this matters:** Without proper cleanup, listeners accumulate in SPAs:
181
+
182
+ ```javascript
183
+ // React example
184
+ import { useEffect } from 'react';
185
+ import OpenAudio from './OpenAudio.js';
186
+
187
+ function NotificationSound() {
188
+ useEffect(() => {
189
+ const player = new OpenAudio('notification.mp3');
190
+ return () => player.destroy(); // ✅ Clean up on unmount
191
+ }, []);
192
+
193
+ return <button onClick={() => player.play()}>Notify</button>;
194
+ }
195
+ ```
196
+
197
+ ---
198
+
199
+ ### `canPlay(type)` — Static Method
200
+
201
+ Check if the browser supports a specific audio format.
202
+
203
+ ```javascript
204
+ OpenAudio.canPlay(type) → boolean
205
+ ```
206
+
207
+ **Parameters:**
208
+ - **`type`** (string) — MIME type
209
+ - `'audio/mpeg'` or `'audio/mp3'` — MP3
210
+ - `'audio/ogg'` — OGG Vorbis
211
+ - `'audio/wav'` — WAV
212
+ - `'audio/webm'` — WebM
213
+ - `'audio/flac'` — FLAC
214
+
215
+ **Returns:** `true` if supported, `false` otherwise
216
+
217
+ ```javascript
218
+ // Check support before constructing
219
+ if (OpenAudio.canPlay('audio/ogg')) {
220
+ const player = new OpenAudio('audio/sound.ogg');
221
+ } else {
222
+ const player = new OpenAudio('audio/sound.mp3'); // Fallback
223
+ }
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Public Properties
229
+
230
+ ### `isPlaying`
231
+
232
+ **Type:** `boolean` (read-only)
233
+
234
+ `true` if a clip is actively playing, `false` if paused or stopped.
235
+
236
+ ```javascript
237
+ const player = new OpenAudio('audio/sound.mp3');
238
+
239
+ document.addEventListener('click', () => player.play());
240
+
241
+ setTimeout(() => {
242
+ if (player.isPlaying) {
243
+ console.log('Still playing');
244
+ } else {
245
+ console.log('Finished or paused');
246
+ }
247
+ }, 500);
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Callbacks
253
+
254
+ ### `onPlay()`
255
+
256
+ Called when playback **starts** (after the unlock MP3 finishes).
257
+
258
+ ```javascript
259
+ const player = new OpenAudio('audio/effect.mp3', {
260
+ onPlay: () => {
261
+ console.log('Playback started');
262
+ updateUI('sound_active');
263
+ }
264
+ });
265
+ ```
266
+
267
+ **Thrown errors are caught:** Errors in callbacks are logged but don't affect playback.
268
+
269
+ ```javascript
270
+ const player = new OpenAudio('audio/effect.mp3', {
271
+ onPlay: () => {
272
+ throw new Error('Oops!');
273
+ // Error logged to console, playback NOT affected
274
+ }
275
+ });
276
+ ```
277
+
278
+ ---
279
+
280
+ ### `onEnd()`
281
+
282
+ Called when playback **finishes naturally** (clip reaches end).
283
+
284
+ Does **not** fire if you call `stop()` before the clip ends.
285
+
286
+ ```javascript
287
+ const player = new OpenAudio('audio/narration.mp3', {
288
+ onEnd: () => {
289
+ console.log('Narration finished');
290
+ playNextScene();
291
+ }
292
+ });
293
+ ```
294
+
295
+ **Thrown errors are caught:** Same as `onPlay()`.
296
+
297
+ ---
298
+
299
+ ### `onHidden()` ← **NEW in v1.1.0**
300
+
301
+ Called when the tab/window **becomes hidden** (user switches tabs, minimizes window, etc.).
302
+
303
+ ```javascript
304
+ const player = new OpenAudio('audio/background-music.mp3', {
305
+ onHidden: () => {
306
+ console.log('Tab hidden');
307
+ updateUI('background_mode');
308
+ }
309
+ });
310
+ ```
311
+
312
+ **Behavior:**
313
+ - Fires whenever `document.visibilityState` changes to `'hidden'`
314
+ - Fires regardless of `pauseOnHidden` setting
315
+ - Useful for updating UI to reflect background state
316
+
317
+ **Thrown errors are caught:** Same error handling as other callbacks.
318
+
319
+ ---
320
+
321
+ ### `onVisible()` ← **NEW in v1.1.0**
322
+
323
+ Called when the tab/window **becomes visible** (user returns to the tab).
324
+
325
+ ```javascript
326
+ const player = new OpenAudio('audio/background-music.mp3', {
327
+ onVisible: () => {
328
+ console.log('Tab visible');
329
+ updateUI('foreground_mode');
330
+ }
331
+ });
332
+ ```
333
+
334
+ **Behavior:**
335
+ - Fires whenever `document.visibilityState` changes to `'visible'`
336
+ - Fires regardless of `pauseOnHidden` setting
337
+ - If `pauseOnHidden: true` and the clip was paused, it automatically resumes
338
+
339
+ **Interaction with `pauseOnHidden`:**
340
+ - If `pauseOnHidden: false` → `onVisible` fires but audio keeps playing
341
+ - If `pauseOnHidden: true` → `onVisible` fires AND audio automatically resumes
342
+
343
+ **Thrown errors are caught:** Same as other callbacks.
344
+
345
+ ---
346
+
347
+ ## Background Tab Detection ← **NEW in v1.1.0**
348
+
349
+ OpenAudio now uses the **Page Visibility API** to detect when the tab loses focus.
350
+
351
+ ### `pauseOnHidden: false` (Default)
352
+
353
+ Audio keeps playing in the background. Only callbacks fire:
354
+
355
+ ```javascript
356
+ const player = new OpenAudio('ambient.mp3', {
357
+ pauseOnHidden: false, // Default
358
+ onHidden: () => console.log('Tab hidden, audio continues'),
359
+ onVisible: () => console.log('Tab visible')
360
+ });
361
+
362
+ // User plays audio → switches tab → audio keeps playing
363
+ // User returns to tab → onVisible fires
364
+ ```
365
+
366
+ **Use case:** Background music, ambient sounds where uninterrupted playback is desired.
367
+
368
+ ---
369
+
370
+ ### `pauseOnHidden: true`
371
+
372
+ Audio automatically pauses when tab hides, resumes when tab returns.
373
+
374
+ ```javascript
375
+ const player = new OpenAudio('narration.mp3', {
376
+ pauseOnHidden: true, // Pause on hide, resume on show
377
+ onHidden: () => console.log('Paused'),
378
+ onVisible: () => console.log('Resumed')
379
+ });
380
+
381
+ // User plays → switches tab → audio pauses at 2:30
382
+ // User returns to tab → audio resumes at 2:30
383
+ ```
384
+
385
+ **Use case:** Game audio, UI sounds, narration where playback should be tied to app focus.
386
+
387
+ **Note:** After returning to the tab, the `resume()` call may be silently blocked if the user hasn't interacted with the page since the tab was hidden. This is due to stricter autoplay policies on some browsers.
388
+
389
+ ---
390
+
391
+ ## Usage Patterns
392
+
393
+ ### UI Sounds (Buttons)
394
+
395
+ ```javascript
396
+ const clickSound = new OpenAudio('ui/click.mp3', { volume: 0.7 });
397
+
398
+ document.querySelectorAll('button').forEach(btn => {
399
+ btn.addEventListener('click', (e) => {
400
+ clickSound.play();
401
+ // ... handle button press
402
+ });
403
+ });
404
+ ```
405
+
406
+ ---
407
+
408
+ ### Notifications / Alerts
409
+
410
+ ```javascript
411
+ const notificationSound = new OpenAudio('notifications/alert.mp3', {
412
+ volume: 0.9,
413
+ label: 'Alert',
414
+ onEnd: () => console.log('Alert sound finished')
415
+ });
416
+
417
+ function showNotification(message) {
418
+ notificationSound.play();
419
+ // ... show notification UI
420
+ }
421
+ ```
422
+
423
+ ---
424
+
425
+ ### Game Audio with Background Tab Control
426
+
427
+ ```javascript
428
+ const gameSound = new OpenAudio('game/explosion.mp3', {
429
+ volume: 0.95,
430
+ pauseOnHidden: true, // Pause when player switches tabs
431
+ onPlay: () => playExplosionAnimation(),
432
+ onHidden: () => updateGameUI('paused'),
433
+ onVisible: () => updateGameUI('resumed')
434
+ });
435
+
436
+ document.getElementById('fire-btn').addEventListener('click', () => {
437
+ gameSound.play();
438
+ });
439
+ ```
440
+
441
+ ---
442
+
443
+ ### Ambient Background Music
444
+
445
+ ```javascript
446
+ const bgMusic = new OpenAudio('music/ambient.mp3', {
447
+ volume: 0.5,
448
+ pauseOnHidden: false, // Keep playing in background
449
+ onHidden: () => hidePlaybackUI(),
450
+ onVisible: () => showPlaybackUI()
451
+ });
452
+
453
+ window.addEventListener('load', () => {
454
+ bgMusic.play(); // Won't work — must be in gesture
455
+ });
456
+
457
+ // Must be in gesture:
458
+ document.addEventListener('click', () => {
459
+ bgMusic.play();
460
+ });
461
+ ```
462
+
463
+ ---
464
+
465
+ ### Hover Feedback
466
+
467
+ ```javascript
468
+ const hoverSound = new OpenAudio('ui/hover.mp3', { volume: 0.4 });
469
+
470
+ document.querySelectorAll('a, button').forEach(el => {
471
+ el.addEventListener('mouseenter', () => {
472
+ if (!hoverSound.isPlaying) {
473
+ hoverSound.play();
474
+ }
475
+ });
476
+ });
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Troubleshooting
482
+
483
+ ### Audio Won't Play (Silent)
484
+
485
+ **Problem:** `play()` is called but nothing happens.
486
+
487
+ **Causes:**
488
+ 1. Called outside a user gesture on first use
489
+ 2. CORS or mixed-content issue (HTTP audio on HTTPS page)
490
+ 3. Browser doesn't support the audio format
491
+ 4. Audio file doesn't exist (404 error)
492
+
493
+ **Solutions:**
494
+
495
+ ```javascript
496
+ // ✅ Correct: gesture context
497
+ document.addEventListener('click', () => player.play());
498
+
499
+ // ❌ Wrong: no gesture
500
+ setTimeout(() => player.play(), 1000);
501
+
502
+ // ❌ Wrong: HTTPS page, HTTP audio
503
+ new OpenAudio('http://example.com/audio.mp3'); // Mixed content warning
504
+
505
+ // ✅ Check format support
506
+ if (!OpenAudio.canPlay('audio/ogg')) {
507
+ // Fall back to another format
508
+ }
509
+ ```
510
+
511
+ ---
512
+
513
+ ### "NotAllowedError" in Console
514
+
515
+ **Problem:** Console shows: `DOMException: play() failed because NotAllowedError`
516
+
517
+ **Cause:** Browser autoplay policy blocked the attempt (not in a user gesture).
518
+
519
+ **Solution:** Call `play()` only inside a user event handler.
520
+
521
+ ```javascript
522
+ // ✅ Correct
523
+ document.addEventListener('click', () => player.play());
524
+
525
+ // ❌ Wrong
526
+ window.addEventListener('load', () => player.play());
527
+ ```
528
+
529
+ ---
530
+
531
+ ### Audio Resumes Fail After Tab Return
532
+
533
+ **Problem:** Set `pauseOnHidden: true`, but audio doesn't resume when tab returns.
534
+
535
+ **Cause:** Browser autoplay policy requires a gesture after the tab was inactive.
536
+
537
+ **Solution:** This is browser behavior. The API will try to resume automatically, but stricter policies may block it silently. As a workaround, prompt the user to click a play button:
538
+
539
+ ```javascript
540
+ const player = new OpenAudio('audio.mp3', {
541
+ pauseOnHidden: true,
542
+ onVisible: () => {
543
+ // Try to resume (may be blocked by policy)
544
+ // Provide UI button as fallback
545
+ showPlayButton();
546
+ }
547
+ });
548
+ ```
549
+
550
+ ---
551
+
552
+ ### Can't Replay After Clip Ends
553
+
554
+ **Problem:** You want to play the same clip twice in a row.
555
+
556
+ **Solution:** Calling `play()` after `onEnd()` fires will automatically rewind and replay.
557
+
558
+ ```javascript
559
+ const player = new OpenAudio('audio/chime.mp3', {
560
+ onEnd: () => {
561
+ console.log('Clip finished');
562
+ // Can call play() immediately to loop
563
+ player.play();
564
+ }
565
+ });
566
+ ```
567
+
568
+ ---
569
+
570
+ ## Browser Compatibility
571
+
572
+ | Browser | Support | Notes |
573
+ |---------|---------|-------|
574
+ | Chrome 70+ | ✅ Full | Autoplay policy: gesture required first time; background tab throttling applies to timers, not audio |
575
+ | Firefox 65+ | ✅ Full | Same autoplay policy as Chrome; Page Visibility API fully supported |
576
+ | Safari 12+ | ✅ Full | iOS Safari: gesture required; Desktop: autoplay allowed |
577
+ | Edge 79+ | ✅ Full | Same as Chrome |
578
+ | iOS Safari 12+ | ✅ Full | Gesture required; Page Visibility API supported |
579
+ | Chrome Android | ✅ Full | Gesture required; touchstart counts as gesture |
580
+
581
+ ---
582
+
583
+ ## Performance
584
+
585
+ - **File Size:** ~5 KB (minified)
586
+ - **Gzipped:** ~2 KB
587
+ - **Runtime Memory:** < 150 KB per instance
588
+ - **CPU:** Negligible — just HTML5 Audio element + visibility listening
589
+
590
+ Creating multiple instances is fine:
591
+
592
+ ```javascript
593
+ const sounds = {
594
+ click: new OpenAudio('click.mp3'),
595
+ hover: new OpenAudio('hover.mp3'),
596
+ error: new OpenAudio('error.mp3'),
597
+ success: new OpenAudio('success.mp3')
598
+ };
599
+
600
+ // All four instances < 1 MB total memory
601
+ ```
602
+
603
+ ---
604
+
605
+ ## Changelog
606
+
607
+ ### v1.1.0 (March 2025)
608
+ - **NEW:** Background tab detection via Page Visibility API
609
+ - **NEW:** `pauseOnHidden` option — pause on tab hide, resume on show
610
+ - **NEW:** `onHidden` callback — fire when tab becomes hidden
611
+ - **NEW:** `onVisible` callback — fire when tab becomes visible
612
+ - **NEW:** `#boundVisibility` — stored bound reference for clean listener removal
613
+ - **IMPROVED:** `destroy()` now removes the visibilitychange listener
614
+ - **RENAMED:** Class `SingleAudio` → `OpenAudio` (matches filename)
615
+ - **IMPROVED:** Better documentation on background tab behavior
616
+
617
+ ### v1.0.0 (January 2025)
618
+ - Initial release
619
+ - Silent MP3 unlock for autoplay policy
620
+ - `onPlay`, `onEnd` callbacks
621
+ - `destroy()` for SPA cleanup
622
+ - `canPlay()` format checking
623
+
624
+ ---
625
+
626
+ ## License
627
+
628
+ GNU General Public License v3.0 or later. See [LICENSE](../LICENSE).
629
+
630
+ ---
631
+
632
+ ## See Also
633
+
634
+ - [OpenAudio_r.js API](./OPENAUDIO_R.md) — Randomized scheduler
635
+ - [OpenAudio_s.js API](./OPENAUDIO_S.md) — Sequential playlist
636
+ - [Feature Comparison](./COMPARISON.md) — Decide which library fits
637
+ - [Main README](../README.md) — OpenAudio suite overview
638
+
639
+ ---
640
+
641
+ *Last updated: March 2025*