openaudio-suite 2.5.6 → 2.6.1

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 CHANGED
@@ -2,201 +2,255 @@
2
2
 
3
3
  All notable changes to the OpenAudio suite are documented here.
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
-
7
- ## v2.5.0 – 2026-03-15
8
- ### Improvements
9
- - Added consistent **semantic versioning** strategy across releases.
10
- - Added **package-lock.json** to support reproducible installs and GitHub Actions CI.
11
- - No changes to OpenAudio JS files; all improvements are tooling and release management.
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/).
12
6
 
13
7
  ---
14
8
 
15
9
  ## OpenAudio.js
16
10
 
17
- ### [1.1.0] — 2025-03-15
11
+ ### [1.3.0] — 2026-03-16
12
+
13
+ #### Fixed
14
+ - **`#isUnlocked` flag** — The silent-MP3 unlock is now performed only once per instance. Previously, every `play()` call (including replays) re-ran the full unlock dance: discarding all buffered/preloaded audio, reassigning `src` to the silent MP3, awaiting the Promise, then restoring the real `src`. This meant the constructor's `preload = 'auto'` was silently wasted on every play. From 1.3.0, subsequent `play()` calls skip directly to `#playClip()` — preloaded data is preserved and replay latency is eliminated.
15
+ - **`#playCancelled` flag** — `stop()` now plants a cancellation token that the in-flight unlock `.then()` checks before calling `#playClip()`. Previously, calling `stop()` during an async unlock was a race: the `.then()` would resolve ~50–200 ms later and start the clip anyway, overriding the explicit stop.
16
+ - **`isPlaying` encapsulation** — `isPlaying` is now a private field (`#isPlaying`) exposed via a read-only getter. Previously a plain public property, it could be written by callers to silently corrupt the state machine.
17
+ - **`#isPlaying` race in `#playClip()`** — `#isPlaying` is now set `true` synchronously before `#audio.play()`, closing the double-play race window where a rapid second `play()` call between the `.play()` call and its `.then()` resolution could bypass the guard and attempt concurrent playback. `#isPlaying` is reverted in `.catch()` on failure.
18
+ - **`resume()` race in `#onVisibilityChange()`** — The visibility-restore `.play()` path now uses the same pre-set/revert pattern. Previously, a concurrent `stop()` in the `.then()` window could leave `isPlaying = true` after `stop()` had set it to `false`.
19
+ - **`destroy()` resource release** — Uses `removeAttribute('src')` + `load()` per the WHATWG HTMLMediaElement resource-release spec, replacing `src = ''` which can fire spurious `error` events on some browsers.
20
+ - **`#pausedByVisibility` in `destroy()`** — Now reset to `false` during teardown for consistent state.
21
+ - **Removed unreachable optional chain** — `this.#audio?.pause()` in `#playClip()` `.then()` was unreachable since `#isDestroyed` implies `destroy()` already paused; removed to avoid misleading readers.
22
+
23
+ ---
24
+
25
+ ### [1.2.0] — 2026-03-16
26
+
27
+ #### Fixed
28
+ - **Unlock now plays on the shared `#audio` element** — Previously the silent MP3 was played on a throwaway `new Audio()`, blessing the wrong element. On iOS Safari this caused `NotAllowedError` for every clip after the first. The shared element is now unlocked directly.
29
+ - **`#isDestroyed` flag** — All public methods (`play`, `stop`, `destroy`) are safe no-ops after `destroy()` has been called, preventing throws on the nulled `#audio` element.
30
+ - **Double-`destroy()` safe** — Calling `destroy()` more than once no longer throws.
31
+ - **`ended` listener stored and removed** — `#endedHandler` is now stored as a named reference and removed in `destroy()`, preventing a callback-into-null race on teardown.
32
+ - **`canPlay()` input guard** — Returns `false` for non-string or empty input rather than passing `undefined` to `canPlayType()`.
33
+
34
+ ---
18
35
 
36
+ ### [1.1.0] — 2026-03-16
19
37
  #### Added
20
- - **Background tab detection** Listens to Page Visibility API (`document.visibilitychange`)
21
- - **`pauseOnHidden` option**Pause audio when tab hides, resume when tab returns
22
- - **`onHidden` callback**Fires when tab becomes hidden
23
- - **`onVisible` callback**Fires when tab becomes visible
24
- - **`#boundVisibility` bound listener**Proper cleanup prevents stale listeners in SPAs
38
+ - Background tab detection via Page Visibility API (`document.visibilitychange`)
39
+ - `pauseOnHidden` option — pause audio when tab hides, resume when tab returns
40
+ - `onHidden` callback — fires when tab becomes hidden
41
+ - `onVisible` callback — fires when tab becomes visible
42
+ - `#boundVisibility` bound listener — proper cleanup prevents stale listeners in SPAs
25
43
 
26
44
  #### Fixed
27
- - Stale visibilitychange listeners in SPA environments (React, Vue, Svelte, etc.)
45
+ - Stale `visibilitychange` listeners in SPA environments
28
46
  - Listener cleanup in `destroy()` now removes the exact function reference
29
47
 
30
48
  #### Changed
31
49
  - Class renamed: `SingleAudio` → `OpenAudio` (matches filename)
32
- - Improved documentation with background tab behavior examples
33
50
 
34
51
  ---
35
52
 
36
- ### [1.0.0] — 2025-01-15
53
+ ### [1.0.0] — 2026-03-16
37
54
 
38
55
  #### Added
39
- - Initial release: Single-clip, one-shot player
56
+ - Initial release: single-clip, one-shot player
40
57
  - Silent MP3 unlock for autoplay policy compliance
41
58
  - `onPlay` and `onEnd` callbacks
42
59
  - `destroy()` method for SPA cleanup
43
60
  - `canPlay()` static format checking
44
- - Complete API documentation
45
61
 
46
62
  ---
47
63
 
48
64
  ## OpenAudio_s.js
49
65
 
50
- ### [1.0.0] — 2025-01-15
66
+ ### [1.3.0] — 2026-03-16
67
+
68
+ #### Fixed
69
+ - **`#playCancelled` flag** — `stop()` and `reset()` now plant a cancellation token that the in-flight silent-MP3 unlock `.then()` checks before calling `#playClip()`. Previously, calling `reset()` or `stop()` during the ~50–200 ms unlock window was silently ignored: the unlock `.then()` would fire unconditionally and start the first clip regardless. The fix mirrors the pattern introduced in `OpenAudio.js` 1.3.0.
70
+
71
+ #### Changed
72
+ - **Usage example** — `goto(getCurrentIndex() - 1)` in the guided-tour example now uses `Math.max(0, …)`, preventing a spurious out-of-range `console.warn` when the user is already on clip 0.
73
+
74
+ #### Documentation
75
+ - `stop()` method note updated to describe both the auto-advance timer cancellation (v1.2.0) and the new unlock cancellation (v1.3.0).
76
+ - `play()` JSDoc updated with a design note explaining that `#isStarted` becomes `true` after the unlock resolves (inside `#playClip()`), and why this intentionally differs from `OpenAudio_r.js`.
77
+
78
+ ---
79
+
80
+ ### [1.2.0] — 2026-03-16
81
+
82
+ #### Fixed
83
+ - **`isPlaying` + `isStarted` encapsulation** — Both are now private fields (`#isPlaying`, `#isStarted`) exposed via read-only getters. Previously plain public properties, they could be written by callers to silently corrupt the state machine and all navigation guards.
84
+ - **`#isPlaying` / `#isStarted` race in `#playClip()`** — Both flags are now set `true` synchronously before `#audio.play()`, closing the double-play race window where a rapid `next()` or `goto()` between the `.play()` call and its `.then()` resolution could bypass guards and abort a starting clip. `#isPlaying` is reverted in `.catch()` on failure.
85
+ - **`#advanceTimer`** — The auto-advance `setTimeout` handle is now stored and cleared by `stop()`, `reset()`, and `destroy()`. Previously, calling `stop()` or `reset()` during the advance delay window could not cancel the pending `next()`, causing the next clip to start after an explicit stop.
86
+ - **`resume()` race** — `#isPlaying` is now set `true` synchronously before `#audio.play()` in `resume()`, then reverted in `.catch()`. Previously the `.then()` could set `isPlaying = true` after a concurrent `stop()` had already set it to `false`.
87
+ - **`destroy()` resource release** — Uses `removeAttribute('src')` + `load()` per the WHATWG HTMLMediaElement resource-release spec, replacing `src = ''`.
88
+ - **`getCurrentClip()` live reference** — Now returns a shallow copy `({ ...clip })` instead of a direct reference to the internal clip object. Callers can no longer mutate the internal playlist via the returned object.
89
+ - **`getClips()` live references** — Now deep-copies inner objects via `map(c => ({ ...c }))`. Previously the shallow spread copied the array but left inner object references live.
90
+ - **Removed unreachable optional chain** — `this.#audio?.pause()` in `#playClip()` `.then()` removed.
91
+
92
+ ---
93
+
94
+ ### [1.1.0] — 2026-03-16
95
+
96
+ #### Fixed
97
+ - **Unlock now plays on the shared `#audio` element** — Same root cause as `OpenAudio.js` 1.2.0.
98
+ - **`play()` guard extended** — Now checks `isStarted` in addition to `isPlaying` and `#isUnlocking`.
99
+ - **`next()` / `goto()` / `gotoLabel()` guard** — All return immediately if `!isStarted`.
100
+ - **`pause()` / `resume()` guard** — Both check `isStarted` before touching the Audio element.
101
+ - **`destroy()` teardown** — `ended` listener removed via stored `#endedHandler` reference before `#audio` is nulled.
102
+ - **`#isDestroyed` flag** — All public methods are safe no-ops after `destroy()`.
103
+
104
+ #### Added
105
+ - `advanceDelay` option (default `0.5s`) — replaces the hardcoded 500ms gap between auto-advance clips.
106
+
107
+ ---
108
+
109
+ ### [1.0.0] — 2026-03-16
51
110
 
52
111
  #### Added
53
- - Initial release: Sequential playlist player
112
+ - Initial release: sequential playlist player
54
113
  - Click-to-advance playback control
55
- - Jump to clip by index or label
114
+ - Jump to clip by index (`goto()`) or label (`gotoLabel()`)
56
115
  - Pause/resume support
57
- - Progress tracking
58
- - Complete API documentation with usage patterns
116
+ - Progress tracking (`getCurrentClip()`, `getCurrentIndex()`, `getClipCount()`)
59
117
 
60
118
  ---
61
119
 
62
120
  ## OpenAudio_r.js
63
121
 
64
- ### [2.4.0] — 2025-03-15
65
-
66
- ### Added
67
- - **`#isUnlocking` flag** — Prevents duplicate unlock attempts when `start()` is called rapidly (spam-clicks). The unlock play() is async; without this flag, a race condition allowed multiple overlapping unlock sequences.
68
- - **`destroy()` method** — Removes the `visibilitychange` listener. Essential for SPAs (React, Vue, Svelte, etc.) where engine instances are created and destroyed across component lifecycles. Without it, stale listeners accumulate on `document` and defunct engines wake up on every tab-focus event.
69
- - **`AudioEngine.canPlay(type)` static method** — Wraps `HTMLAudioElement.canPlayType()` with a clean boolean return. Use to check browser support for `.ogg`, `.wav`, or `.flac` before constructing an engine, rather than discovering a silent failure at play() time.
70
- - **Callback resilience for `onCycleReset`** — Wrapped in try/catch, matching the existing resilience applied to `onPlay` and `onEnd`. A throwing `onCycleReset` can no longer stall the engine loop.
71
- - **Comprehensive documentation** — Added HTML5 AUDIO vs. WEB AUDIO API comparison, browser autoplay policy deep-dive, background tab throttling mitigation, and callback resilience guarantees.
122
+ ### [2.6.0] — 2026-03-16
72
123
 
73
- ### Fixed
74
- - Race condition where spam-clicking `start()` could trigger multiple `#scheduleNext()` calls before the first unlock completed.
75
- - Potential memory leak: `visibilitychange` listener now properly removed on `destroy()`.
76
-
77
- ### Changed
78
- - Better error messages in console (include clip label and context).
124
+ #### Fixed
125
+ - **`TypeError` for invalid arguments** — All constructor and `addClip()` validation throws changed from `Error` to `TypeError`. This matches the ECMAScript convention that type-mismatch errors should be `TypeError`, and aligns with `OpenAudio.js` and `OpenAudio_s.js`. Callers catching errors via `instanceof TypeError` now correctly catch `AudioEngine` validation failures.
79
126
 
80
- ### Documentation
81
- - Added CHANGELOG.md (this file)
82
- - Expanded README with better examples and API reference
83
- - Created CONTRIBUTING.md with PR guidelines and testing checklist
127
+ #### Changed
128
+ - **`#isStarted` timing** — `#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 continues to be handled by `#isUnlocking`. This aligns the state machine semantics with `OpenAudio_s.js`.
129
+ - **`try/catch` formatting** Three compact single-line `try/catch` blocks (in the `onended` handler, `#resetCycle()`, and `#playNext()` `.then()`) expanded to multi-line style, matching the formatting convention used throughout the suite.
84
130
 
85
131
  ---
86
132
 
87
- ## [2.3.0] — 2025-02-20
133
+ ### [2.5.0] — 2026-03-16
88
134
 
89
- ### Added
90
- - **Clip src validation** — Constructor and `addClip()` now verify that every clip has a non-empty string `src`. Previously a clip with missing or non-string `src` would silently map `undefined` into the engine, producing a confusing audio error rather than a clear failure at the point of entry.
91
- - **Next-clip prefetch** — `#scheduleNext()` now sets `#audio.src` to the next selected clip immediately after the current clip ends, before the inter-clip delay fires. The browser begins buffering the file during the gap, eliminating the network fetch delay at `#playNext()` time. The clip is still marked played and selected at schedule time, preserving shuffle-bag correctness.
92
- - **Background tab throttling mitigation** — `#scheduleNext()` records the wall-clock time (`#timerSetAt`) and delay duration (`#timerDelay`). A `visibilitychange` listener fires when the tab returns to foreground. If elapsed time meets or exceeds the intended delay, the pending timer is cancelled and `#playNext()` is called immediately. Otherwise, the remaining time is rescheduled precisely.
135
+ #### Fixed
136
+ - **`isStarted` + `isPlaying` encapsulation** — Both are now private fields (`#isStarted`, `#isPlaying`) exposed via read-only getters. Previously plain public properties, they could be written by callers to silently corrupt the scheduling state machine and all internal guards.
137
+ - **`#isDestroyed` flag** — All public methods (`start`, `stop`, `reset`, `setVolume`, `addClip`, `destroy`) now return immediately after `destroy()` has been called, making post-destroy calls safe no-ops. Previously `destroy()` documented post-destroy behaviour as "undefined"; in practice, calling `start()` after `destroy()` would silently re-run the engine without a `visibilitychange` listener, leaving it in a partially torn-down state.
138
+ - **`destroy()` resource release** — Now releases the Audio element via `removeAttribute('src')` + `load()` per the WHATWG HTMLMediaElement resource-release spec, then nulls `#audio`. Previously the element was kept live with its src intact after `destroy()`.
139
+ - **`#isPlaying` race in `#playNext()`** — `#isPlaying` is now set `true` synchronously before `#audio.play()`, closing the narrow race window where `isPlaying` reported `false` while audio was starting. Reverted in `.catch()` on failure.
140
+
141
+ ---
93
142
 
94
- ### Fixed
95
- - Network latency no longer visible as gaps between clips (prefetch eliminates fetch delay).
96
- - Background tab throttling no longer causes clips to bunch together on tab return (recalculation uses wall-clock time, not timer promises).
143
+ ### [2.4.1] — 2026-03-16
97
144
 
98
- ### Changed
99
- - Improved console logging for debugging background tab behavior.
145
+ #### Changed
146
+ - Licence changed from GPL-3.0-or-later to Apache-2.0 across the entire suite.
100
147
 
101
148
  ---
102
149
 
103
- ## [2.2.0] — 2025-01-10
150
+ ### [2.4.0] — 2026-03-16
104
151
 
105
- ### Added
106
- - **`reset()` method**Stops playback, clears all `played` flags, and optionally resets the current clip. Next `start()` begins a completely fresh random cycle.
107
- - **`setVolume()` method**Updates volume immediately on live playback and all future clips. Clamps to [0, 1].
152
+ #### Added
153
+ - `#isUnlocking` flagprevents duplicate unlock attempts on rapid `start()` calls.
154
+ - `destroy()` method — removes the `visibilitychange` listener.
155
+ - `AudioEngine.canPlay(type)` static method.
156
+ - `onCycleReset` callback wrapped in try/catch.
108
157
 
109
- ### Fixed
110
- - Volume no longer resets to default on clip transition.
158
+ #### Fixed
159
+ - Race condition on `start()` spam.
160
+ - Potential memory leak: `visibilitychange` listener now properly removed.
111
161
 
112
162
  ---
113
163
 
114
- ## [2.1.0] — 2024-12-05
164
+ ### [2.3.0] — 2026-03-16
115
165
 
116
- ### Added
117
- - **`addClip(clip)` method** Add new clips to the engine at runtime without reconstructing.
118
- - **Cycle-boundary repeat prevention** — When the unplayed pool is empty (cycle reset), the next clip is selected from all clips *except* the current clip, preventing the same clip from playing twice in a row across a cycle boundary.
166
+ #### Added
167
+ - Clip `src` validation in constructor and `addClip()`.
168
+ - Next-clip prefetch to eliminate network fetch delay.
169
+ - Background tab throttling mitigation via wall-clock correction.
119
170
 
120
- ### Fixed
121
- - Shuffle bag now correctly prevents the same clip from playing at the start and end of successive cycles.
171
+ ---
172
+
173
+ ### [2.2.0] — 2026-03-16
174
+
175
+ #### Added
176
+ - `reset()` method.
177
+ - `setVolume()` method with clamping.
178
+ - True private fields (`#`).
179
+ - NotAllowedError handling — engine halts rather than silent-loops.
180
+ - Silent base64 unlock in `start()`.
122
181
 
123
182
  ---
124
183
 
125
- ## [2.0.0] — 2024-10-01
184
+ ### [2.1.1] — 2026-03-16
126
185
 
127
- ### Added
128
- - **Public API methods** — `start()`, `stop()`, `start()` for public control.
129
- - **Lifecycle callbacks** — `onPlay(clip)`, `onEnd(clip)`, `onCycleReset()` for integration with game/app logic.
130
- - **Options object** — `lowTime`, `maxTime`, `volume`, and callback configuration.
131
- - **Shuffle bag algorithm** Guarantees each clip plays exactly once per cycle before repeats.
132
- - **Browser autoplay policy handling** — Silent base64 MP3 unlock on first `start()` call, subsequent clips scheduled via `setTimeout`.
133
- - **Background tab throttling mitigation** — Page Visibility API monitoring and wall-clock delay recalculation.
186
+ #### Fixed
187
+ - Constructor validates `lowTime` and `maxTime`.
188
+ - `#scheduleNext()` clears existing timer before setting a new one.
189
+ - Cycle-boundary repeat fix.
190
+ - `stop()` no longer resets `currentTime`.
191
+
192
+ ---
134
193
 
135
- ### Documentation
136
- - Comprehensive JSDoc comments throughout
137
- - Browser compatibility notes
138
- - Web Audio API vs. HTML5 Audio comparison
139
- - Usage examples in code comments
194
+ ### [2.1.0] — 2026-03-16
195
+
196
+ #### Added
197
+ - `addClip()` method for runtime updates.
198
+ - Cycle-boundary repeat prevention.
199
+ - Single reusable Audio element created once in constructor.
140
200
 
141
201
  ---
142
202
 
143
- ## [1.0.0] — 2024-09-01
203
+ ### [2.0.0] — 2026-03-16
144
204
 
145
- ### Initial Release
146
- - Self-contained randomized audio scheduling engine
147
- - No dependencies, no external libraries
148
- - HTML5 `<audio>` element-based scheduling
149
- - Basic shuffle bag implementation
150
- - Mobile-compatible (respects autoplay policies)
205
+ #### Added
206
+ - Initial public release. Shuffle bag algorithm. `onPlay` / `onEnd` / `onCycleReset` callbacks. `addClip()` runtime insertion.
151
207
 
152
208
  ---
153
209
 
154
210
  ## Upgrade Guide
155
211
 
156
- ### 2.32.4
157
- - **No breaking changes.** New features are additive.
158
- - **Recommended:** Call `engine.destroy()` when tearing down in SPAs to prevent listener accumulation.
159
- - **New:** Use `AudioEngine.canPlay('audio/ogg')` to check format support before constructing.
212
+ ### OpenAudio_s.js 1.2 1.3
213
+ No breaking changes. `stop()` and `reset()` now cancel in-flight unlocks via `#playCancelled`; callers that relied on the previous (unintended) behaviour of the clip starting despite a `stop()` during the unlock window should review their flow, but this was never a documented or safe pattern.
160
214
 
161
- ### 2.2 → 2.3
162
- - **No breaking changes.** Prefetch is transparent.
163
- - **Benefit:** Faster clip transitions due to network prefetch during inter-clip gap.
215
+ ### OpenAudio_r.js 2.5 → 2.6
216
+ No breaking changes. `TypeError` is now thrown instead of `Error` for constructor and `addClip()` validation failures. If you catch these via `catch (e)` and check `e.message`, nothing changes. If you check `e instanceof Error` it still holds. Only `instanceof TypeError` (which previously returned `false`) now returns `true`.
164
217
 
165
- ### 2.02.1
166
- - **No breaking changes.** New methods are additive.
167
- - **New:** Cycle-boundary repeat prevention automatically enabled.
218
+ ### OpenAudio.js 1.2 1.3
219
+ No breaking changes. `player.isPlaying` is now read-only (getter); existing read access is unaffected. Write access (`player.isPlaying = ...`) was never a supported pattern.
168
220
 
169
- ### 1.0 → 2.0
170
- - **Breaking:** Old version had no public API. Complete rewrite with methods and callbacks.
171
- - **Migration:** Wrap old code in a new `AudioEngine` constructor.
221
+ ### OpenAudio_s.js 1.11.2
222
+ No breaking changes. `player.isPlaying` and `player.isStarted` are now read-only getters; existing read access is unaffected.
172
223
 
173
- ---
224
+ ### OpenAudio_r.js 2.4 → 2.5
225
+ No breaking changes. `engine.isStarted` and `engine.isPlaying` are now read-only getters; existing read access is unaffected.
226
+
227
+ ### OpenAudio.js 1.1 → 1.2
228
+ No breaking changes. Fixes are internal.
174
229
 
175
- ## Future Considerations
230
+ ### OpenAudio_s.js 1.0 → 1.1
231
+ No breaking changes. `advanceDelay` is now configurable.
176
232
 
177
- These features are **out of scope** for OpenAudio_r.js (it would violate the "self-contained, no dependencies" principle):
233
+ ### OpenAudio_r.js 2.4 2.4.1
234
+ Licence header update only.
178
235
 
179
- - Crossfading between clips
180
- - Sub-second precision scheduling
181
- - Real-time DSP effects (reverb, EQ, compression)
182
- - Frequency analysis or visualization
236
+ ### OpenAudio_r.js 2.3 → 2.4
237
+ Additive features only.
183
238
 
184
- For these, consider graduating to the **Web Audio API**. See README.md for comparison.
239
+ ### OpenAudio_r.js 1.0 2.0
240
+ **Breaking:** Complete rewrite with methods and callbacks.
185
241
 
186
242
  ---
187
243
 
188
244
  ## Contributing
189
245
 
190
- Have a feature request or bug to report? See [CONTRIBUTING.md](./CONTRIBUTING.md).
191
-
192
- All contributions will be credited in the changelog.
246
+ See [CONTRIBUTING.md](https://github.com/Rexore/OpenAudio/blob/main/CONTRIBUTING.md).
193
247
 
194
248
  ---
195
249
 
196
250
  ## License
197
251
 
198
- OpenAudio_r.js is licensed under the GNU General Public License v3.0 or later. See [LICENSE](./LICENSE) for details.
252
+ Licensed under the **Apache License 2.0**.
199
253
 
200
254
  ---
201
255
 
202
- *Last updated: March 15, 2025*
256
+ *Last updated: March 16, 2026*