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.
package/CHANGELOG.md CHANGED
@@ -2,201 +2,261 @@
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
  ---
8
+ ## OpenAudio.js
14
9
 
10
+ ### [1.4.0] — 2026-03-17
15
11
  ## OpenAudio.js
12
+ * <Strong>Lazy Loading</strong>: The audio source is set only when play() is called for the first time, <em>optimizing performance</em> by avoiding unnecessary network requests.
13
+ * <strong>Enhanced Error Handling</strong>: <em>Additional</em> error checks and informative error messages are added to ensure robust error handling.
14
+ * <strong>Improved Readability</strong>: Comments are <em>added</em> to explain complex parts of the code, making it easier to understand.<br><br>
15
+ <em>Patched version should be more robust, performant, and easier to maintain.</em>
16
+
17
+ ### [1.3.0] — 2026-03-16
18
+
19
+ #### Fixed
20
+ - **`#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.
21
+ - **`#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.
22
+ - **`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.
23
+ - **`#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.
24
+ - **`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`.
25
+ - **`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.
26
+ - **`#pausedByVisibility` in `destroy()`** — Now reset to `false` during teardown for consistent state.
27
+ - **Removed unreachable optional chain** — `this.#audio?.pause()` in `#playClip()` `.then()` was unreachable since `#isDestroyed` implies `destroy()` already paused; removed to avoid misleading readers.
28
+
29
+ ---
16
30
 
17
- ### [1.1.0] — 2025-03-15
31
+ ### [1.2.0] — 2026-03-16
18
32
 
33
+ #### Fixed
34
+ - **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.
35
+ - **`#isDestroyed` flag** — All public methods (`play`, `stop`, `destroy`) are safe no-ops after `destroy()` has been called, preventing throws on the nulled `#audio` element.
36
+ - **Double-`destroy()` safe** — Calling `destroy()` more than once no longer throws.
37
+ - **`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.
38
+ - **`canPlay()` input guard** — Returns `false` for non-string or empty input rather than passing `undefined` to `canPlayType()`.
39
+
40
+ ---
41
+
42
+ ### [1.1.0] — 2026-03-16
19
43
  #### 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
44
+ - Background tab detection via Page Visibility API (`document.visibilitychange`)
45
+ - `pauseOnHidden` option — pause audio when tab hides, resume when tab returns
46
+ - `onHidden` callback — fires when tab becomes hidden
47
+ - `onVisible` callback — fires when tab becomes visible
48
+ - `#boundVisibility` bound listener — proper cleanup prevents stale listeners in SPAs
25
49
 
26
50
  #### Fixed
27
- - Stale visibilitychange listeners in SPA environments (React, Vue, Svelte, etc.)
51
+ - Stale `visibilitychange` listeners in SPA environments
28
52
  - Listener cleanup in `destroy()` now removes the exact function reference
29
53
 
30
54
  #### Changed
31
55
  - Class renamed: `SingleAudio` → `OpenAudio` (matches filename)
32
- - Improved documentation with background tab behavior examples
33
56
 
34
57
  ---
35
58
 
36
- ### [1.0.0] — 2025-01-15
59
+ ### [1.0.0] — 2026-03-16
37
60
 
38
61
  #### Added
39
- - Initial release: Single-clip, one-shot player
62
+ - Initial release: single-clip, one-shot player
40
63
  - Silent MP3 unlock for autoplay policy compliance
41
64
  - `onPlay` and `onEnd` callbacks
42
65
  - `destroy()` method for SPA cleanup
43
66
  - `canPlay()` static format checking
44
- - Complete API documentation
45
67
 
46
68
  ---
47
69
 
48
70
  ## OpenAudio_s.js
49
71
 
50
- ### [1.0.0] — 2025-01-15
72
+ ### [1.3.0] — 2026-03-16
73
+
74
+ #### Fixed
75
+ - **`#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.
76
+
77
+ #### Changed
78
+ - **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.
79
+
80
+ #### Documentation
81
+ - `stop()` method note updated to describe both the auto-advance timer cancellation (v1.2.0) and the new unlock cancellation (v1.3.0).
82
+ - `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`.
83
+
84
+ ---
85
+
86
+ ### [1.2.0] — 2026-03-16
87
+
88
+ #### Fixed
89
+ - **`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.
90
+ - **`#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.
91
+ - **`#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.
92
+ - **`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`.
93
+ - **`destroy()` resource release** — Uses `removeAttribute('src')` + `load()` per the WHATWG HTMLMediaElement resource-release spec, replacing `src = ''`.
94
+ - **`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.
95
+ - **`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.
96
+ - **Removed unreachable optional chain** — `this.#audio?.pause()` in `#playClip()` `.then()` removed.
97
+
98
+ ---
99
+
100
+ ### [1.1.0] — 2026-03-16
101
+
102
+ #### Fixed
103
+ - **Unlock now plays on the shared `#audio` element** — Same root cause as `OpenAudio.js` 1.2.0.
104
+ - **`play()` guard extended** — Now checks `isStarted` in addition to `isPlaying` and `#isUnlocking`.
105
+ - **`next()` / `goto()` / `gotoLabel()` guard** — All return immediately if `!isStarted`.
106
+ - **`pause()` / `resume()` guard** — Both check `isStarted` before touching the Audio element.
107
+ - **`destroy()` teardown** — `ended` listener removed via stored `#endedHandler` reference before `#audio` is nulled.
108
+ - **`#isDestroyed` flag** — All public methods are safe no-ops after `destroy()`.
51
109
 
52
110
  #### Added
53
- - Initial release: Sequential playlist player
111
+ - `advanceDelay` option (default `0.5s`) — replaces the hardcoded 500ms gap between auto-advance clips.
112
+
113
+ ---
114
+
115
+ ### [1.0.0] — 2026-03-16
116
+
117
+ #### Added
118
+ - Initial release: sequential playlist player
54
119
  - Click-to-advance playback control
55
- - Jump to clip by index or label
120
+ - Jump to clip by index (`goto()`) or label (`gotoLabel()`)
56
121
  - Pause/resume support
57
- - Progress tracking
58
- - Complete API documentation with usage patterns
122
+ - Progress tracking (`getCurrentClip()`, `getCurrentIndex()`, `getClipCount()`)
59
123
 
60
124
  ---
61
125
 
62
126
  ## OpenAudio_r.js
63
127
 
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.
128
+ ### [2.6.0] — 2026-03-16
72
129
 
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).
130
+ #### Fixed
131
+ - **`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
132
 
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
133
+ #### Changed
134
+ - **`#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`.
135
+ - **`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
136
 
85
137
  ---
86
138
 
87
- ## [2.3.0] — 2025-02-20
139
+ ### [2.5.0] — 2026-03-16
88
140
 
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.
141
+ #### Fixed
142
+ - **`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.
143
+ - **`#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.
144
+ - **`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()`.
145
+ - **`#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.
146
+
147
+ ---
93
148
 
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).
149
+ ### [2.4.1] — 2026-03-16
97
150
 
98
- ### Changed
99
- - Improved console logging for debugging background tab behavior.
151
+ #### Changed
152
+ - Licence changed from GPL-3.0-or-later to Apache-2.0 across the entire suite.
100
153
 
101
154
  ---
102
155
 
103
- ## [2.2.0] — 2025-01-10
156
+ ### [2.4.0] — 2026-03-16
104
157
 
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].
158
+ #### Added
159
+ - `#isUnlocking` flagprevents duplicate unlock attempts on rapid `start()` calls.
160
+ - `destroy()` method — removes the `visibilitychange` listener.
161
+ - `AudioEngine.canPlay(type)` static method.
162
+ - `onCycleReset` callback wrapped in try/catch.
108
163
 
109
- ### Fixed
110
- - Volume no longer resets to default on clip transition.
164
+ #### Fixed
165
+ - Race condition on `start()` spam.
166
+ - Potential memory leak: `visibilitychange` listener now properly removed.
111
167
 
112
168
  ---
113
169
 
114
- ## [2.1.0] — 2024-12-05
170
+ ### [2.3.0] — 2026-03-16
115
171
 
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.
172
+ #### Added
173
+ - Clip `src` validation in constructor and `addClip()`.
174
+ - Next-clip prefetch to eliminate network fetch delay.
175
+ - Background tab throttling mitigation via wall-clock correction.
119
176
 
120
- ### Fixed
121
- - Shuffle bag now correctly prevents the same clip from playing at the start and end of successive cycles.
177
+ ---
178
+
179
+ ### [2.2.0] — 2026-03-16
180
+
181
+ #### Added
182
+ - `reset()` method.
183
+ - `setVolume()` method with clamping.
184
+ - True private fields (`#`).
185
+ - NotAllowedError handling — engine halts rather than silent-loops.
186
+ - Silent base64 unlock in `start()`.
122
187
 
123
188
  ---
124
189
 
125
- ## [2.0.0] — 2024-10-01
190
+ ### [2.1.1] — 2026-03-16
126
191
 
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.
192
+ #### Fixed
193
+ - Constructor validates `lowTime` and `maxTime`.
194
+ - `#scheduleNext()` clears existing timer before setting a new one.
195
+ - Cycle-boundary repeat fix.
196
+ - `stop()` no longer resets `currentTime`.
197
+
198
+ ---
134
199
 
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
200
+ ### [2.1.0] — 2026-03-16
201
+
202
+ #### Added
203
+ - `addClip()` method for runtime updates.
204
+ - Cycle-boundary repeat prevention.
205
+ - Single reusable Audio element created once in constructor.
140
206
 
141
207
  ---
142
208
 
143
- ## [1.0.0] — 2024-09-01
209
+ ### [2.0.0] — 2026-03-16
144
210
 
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)
211
+ #### Added
212
+ - Initial public release. Shuffle bag algorithm. `onPlay` / `onEnd` / `onCycleReset` callbacks. `addClip()` runtime insertion.
151
213
 
152
214
  ---
153
215
 
154
216
  ## Upgrade Guide
155
217
 
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.
218
+ ### OpenAudio_s.js 1.2 1.3
219
+ 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
220
 
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.
221
+ ### OpenAudio_r.js 2.5 → 2.6
222
+ 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
223
 
165
- ### 2.02.1
166
- - **No breaking changes.** New methods are additive.
167
- - **New:** Cycle-boundary repeat prevention automatically enabled.
224
+ ### OpenAudio.js 1.2 1.3
225
+ 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
226
 
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.
227
+ ### OpenAudio_s.js 1.11.2
228
+ No breaking changes. `player.isPlaying` and `player.isStarted` are now read-only getters; existing read access is unaffected.
172
229
 
173
- ---
230
+ ### OpenAudio_r.js 2.4 → 2.5
231
+ No breaking changes. `engine.isStarted` and `engine.isPlaying` are now read-only getters; existing read access is unaffected.
232
+
233
+ ### OpenAudio.js 1.1 → 1.2
234
+ No breaking changes. Fixes are internal.
174
235
 
175
- ## Future Considerations
236
+ ### OpenAudio_s.js 1.0 → 1.1
237
+ No breaking changes. `advanceDelay` is now configurable.
176
238
 
177
- These features are **out of scope** for OpenAudio_r.js (it would violate the "self-contained, no dependencies" principle):
239
+ ### OpenAudio_r.js 2.4 2.4.1
240
+ Licence header update only.
178
241
 
179
- - Crossfading between clips
180
- - Sub-second precision scheduling
181
- - Real-time DSP effects (reverb, EQ, compression)
182
- - Frequency analysis or visualization
242
+ ### OpenAudio_r.js 2.3 → 2.4
243
+ Additive features only.
183
244
 
184
- For these, consider graduating to the **Web Audio API**. See README.md for comparison.
245
+ ### OpenAudio_r.js 1.0 2.0
246
+ **Breaking:** Complete rewrite with methods and callbacks.
185
247
 
186
248
  ---
187
249
 
188
250
  ## Contributing
189
251
 
190
- Have a feature request or bug to report? See [CONTRIBUTING.md](./CONTRIBUTING.md).
191
-
192
- All contributions will be credited in the changelog.
252
+ See [CONTRIBUTING.md](https://github.com/Rexore/OpenAudio/blob/main/CONTRIBUTING.md).
193
253
 
194
254
  ---
195
255
 
196
256
  ## License
197
257
 
198
- OpenAudio_r.js is licensed under the GNU General Public License v3.0 or later. See [LICENSE](./LICENSE) for details.
258
+ Licensed under the **Apache License 2.0**.
199
259
 
200
260
  ---
201
261
 
202
- *Last updated: March 15, 2025*
262
+ *Last updated: March 16, 2026*