aiaudiojs Changelog
This content is not available in your language yet.
[Unreleased]
Section titled “[Unreleased]”[0.5.5] - 2026-06-08
Section titled “[0.5.5] - 2026-06-08”Changed
Section titled “Changed”- Project home migrated to the
isluminaGitHub org; the package is now published from there via npm trusted publisher (OIDC + SLSA provenance). Family-wide version alignment at0.5.5— no runtime or API changes.
[0.5.3] - 2026-06-05
Section titled “[0.5.3] - 2026-06-05”- Added a prominent “audio unlock must happen inside a user gesture” README callout and an app-side “ship audio before your assets exist” placeholder recipe (
Howler.ctx+ user-authoredOscillatorNode, explicitly not a library feature; no synth added). No runtime or API change;distbyte-identical to 0.5.2.
[0.5.2] - 2026-06-05
Section titled “[0.5.2] - 2026-06-05”- Review-driven documentation fixes (
README.md,README_ZHTW.md,llms.txt,llms-full.txt; plus repo-onlyCONTRIBUTING.md): clarity and accuracy from a cross-package code review. No runtime or API change;distbyte-identical to 0.5.1.
[0.5.1] - 2026-06-02
Section titled “[0.5.1] - 2026-06-02”- play() AbortSignal listener leak — The
onAbortlistener added tosignalinSound.play()is now removed on natural sound end and onstop(Howlerend/stopper-id events), not only when the signal fires.{ once: true }is also applied to theaddEventListenercall. Reusing a long-livedAbortControlleracross manyplay()calls no longer accumulates listeners on the signal.
- play() leak tests (D8, D9) — Two new unit tests verify that after a play() whose
sound ends or stops naturally (without the signal firing), no abort listener remains
on the signal and a subsequent
.abort()is a silent no-op. - visibilitychange-hidden test (A9) — New test confirms that
visibilitychangewhen the page ishiddendoes NOT callHowler.ctx.resume()(onlyvisibleshould). - Multi-voice from test (G1) — New equal-power test confirms that when
fromhas two concurrent active voices, both are ramped by the crossfade. - fast-check upgraded to ^4.8.0 — Updated from ^3.23.0; all 61 tests remain green.
No test-code changes required (fc API used —
fc.assert,fc.asyncProperty,fc.double— is unchanged between v3 and v4).
Changed (JSDoc / contract clarifications — no behavior change)
Section titled “Changed (JSDoc / contract clarifications — no behavior change)”Audio.load()— F3 limitation documented: abort after Howler’s decode completes leaves a briefly-registered Sound thatdisposeAll()will reclaim; documented in JSDoc with guidance.Audio.crossfade()— F2, F5, F9 contracts documented:- F2: concurrent crossfades on the same Sound — old
AbortControllermust not be fired once a new crossfade starts, or it overwrites the new schedule. - F5: equal-power crossfade assumes
fromis playing at masterVolume; a per-instance volume override causes a gain snap/click at the start. - F9:
rampSoundthrowing mid-crossfade leavestorunning silently — noted as a known defensive edge case.
- F2: concurrent crossfades on the same Sound — old
[0.4.0] - 2026-05-29
Section titled “[0.4.0] - 2026-05-29”Dependency-reduction cycle release. No runtime API change — src/index.ts is
byte-identical to 0.3.0, so every existing import from aiaudiojs behaves exactly
as before.
Changed (dependencies)
Section titled “Changed (dependencies)”howlerstays a requiredpeerDependency(^2.2.4). As the only ai*js package with a runtime peer dependency, aiaudiojs is the flagship of the family v0.4.0 dependency-reduction cycle. The A / B / C decision was evaluated against the live source: every public API —load()(new Howl()),unlock()(Howler.ctx.resume()),volume(Howler.volume()),play/pause/stop/fade, and both crossfade paths — is Howler-backed. There is no Howler-free subset to extract into a lightweight subpath, and a from-scratch Web Audio shim would have to re-own the iOS unlock / HTML5 fallback / sprite WebKit edge cases (and would not fit the 2 KB budget). So howler stays — see “Why aiaudiojs” for the full rationale. The honest dependency floor for this package is this one load-bearing peer.- devDependencies confirmed aligned to the ai*js family standard (biome, @types/node,
@vitest/coverage-v8, tsup, tsx, typescript, vite, vitest,
pnpm@9.12.3).@types/howlerandhappy-domremain aiaudiojs-specific (Howler types + DOM test environment). - Lockfile deduped (
pnpm dedupe);pnpm auditreports no known vulnerabilities. Maintainer-side supply-chain hygiene only — devDependency transitives are not shipped to consumers.
Stability
Section titled “Stability”- 0.3.x crossfade surface frozen on the 1.0 track.
crossfade('linear')andcrossfade('equal-power')are stable and will not change shape before 1.0; once 1.0 ships they are frozen for the 1.x line. See STABILITY.md. - Spatial audio (PannerNode / HRTF) remains experimental, deferred to v0.7; its API surface is still undefined and nothing is implemented this cycle.
Decisions
Section titled “Decisions”- No runtime API addition. This is a deliberate dependency-hygiene + stability-freeze release, not a feature release. The family lands on a unified 0.4.0 version line; the CHANGELOG is kept honest rather than padded with non-features.
- Direct 0.4.0 minor, no patch-step. Patch-stepping is reserved for de-risking changes that touch the public surface; this release touches none, so it lands straight on 0.4.0.
[0.3.0] - 2026-05-29
Section titled “[0.3.0] - 2026-05-29”CrossfadeCurvetype —'linear' | 'equal-power'exported fromaiaudiojs.CrossfadeOptions.curve?— optional fade-curve selector. Default'linear'preserves 0.1.1 behaviour byte-for-byte for every existing caller.- equal-power crossfade path —
crossfade({ curve: 'equal-power' })schedules perceptually-flat sin/cos ramps (scaled by master volume) directly on each sound’s Web Audio GainNode (_node.gain) viasetValueCurveAtTime: the outgoing sound followscos(mv → 0), the incoming followssin(0 → mv), sosin² + cos² = 1holds the perceived loudness flat through the transition.Howl.fade()is NOT invoked in this path. Terminal state matches the linear path (outgoing at 0, incoming at master volume). - 64-sample sin/cos curves — built lazily on first equal-power call, shared as module-scope
Float32Arraysingletons across allAudioinstances. STABILITY.md— new file documenting stability guarantees per feature.
Decisions
Section titled “Decisions”- 0.2.0 version skipped. This release is tagged
0.3.0to align with the cross-packagev0.3.xlimitation cycle. All sibling packages (aifsmjs,aiecsjs,aibridgejs,aieventjs,aipooljs,aiquadtreejs) are simultaneously shipping 0.3.x. Shipping0.2.0in isolation would break the ecosystem’s unified versioning signal. - Backward compatibility. Not passing
curve(or passingcurve: 'linear') routes to the original linear path — code is byte-identical to 0.1.1. No existing caller is affected. - Scheduled on Howler’s own per-sound GainNode, not an overlay. Howler routes each Web Audio sound as
bufferSource → sound._node (GainNode) → Howler.masterGain, so_node.gainalready is the per-sound volume param. The equal-power path schedules the sin/cos curve straight onto it — the same node Howler’s ownfade()uses — rather than inserting and re-routing additional GainNodes. This keeps the shell inside its original 2 KB gzip budget (no bump needed) and avoids any routing teardown. - Three pre-release defects caught and fixed (never shipped). An earlier overlay design (insert
gainA/gainB, re-route_nodethrough them) was found — verified against Howler’s source — to be broken three ways: (1) the incoming track was double-attenuated to permanent silence (to.play({volume:0})upstream of the overlay), (2) the outgoing track jumped back to full volume when routing was restored, and (3)fromwas re-played, layering a duplicate voice. Scheduling directly on_node.gain(above) is correct by construction and eliminates all three. Howl.fade()not called in equal-power path. The two curve paths are mutually exclusive; equal-power owns the gain schedule for the duration of the crossfade.
[0.1.1] - 2026-05-28
Section titled “[0.1.1] - 2026-05-28”Changed (CI)
Section titled “Changed (CI)”publish.ymlnow triggers onpush: tags: ["v*"](wasworkflow_dispatchonly). Aligns with the trigger used byaifsmjs/aiecsjs/aibridgejs. Tag push now automatically runs the OIDC trusted publish.npm publish --provenance --access public— the workflow now emits a sigstore provenance attestation so consumers can verify the tarball was built by this workflow on this commit.
No runtime / source / API changes from 0.1.0. 0.1.1 is also the first version to actually land on npm — 0.1.0 was tagged in git but never published to npm. Production bundles are byte-identical to the 0.1.0 git tag.
[0.1.0] - 2026-05-28
Section titled “[0.1.0] - 2026-05-28”createAudio({ autoUnlock?, volume?, resumeOnVisibility? })— fully implemented factory; closure-basedAudiohandle; all methods destructurable withoutthis.audio.unlock()— resumesHowler.ctx(best-effort); idempotent; swallows errors from already-running contexts.audio.load(url, signal?)— wrapsnew Howl({ src: [url], preload: true })in aPromise; resolves ononload, rejects withAudioErroronloaderror, rejects withDOMException("AbortError")ifsignalfires.audio.crossfade(from, to, { duration, signal? })— linear-power crossfade usingHowl.fade()on both ramps; resolves afterdurationMsviasetTimeout. Aborting viasignalclears the timer and resolves immediately; the in-progress Howler fade continues silently (Howler 2.x has no fade-cancel API). Equal-power curve is planned for 0.2.0.Sound.play(opts?),.pause(id?),.stop(id?)— delegate to Howler;playappliesvolume,rate,loopper-id and wiressignalabort tostop(id).Sound.fade(from, to, ms, id?)— delegates toHowl.fade; resolves aftermsviasetTimeout.Sound.dispose()/Audio.dispose()/Audio.disposeAll()— idempotent teardown; cascade-disposes Sounds; removes alldocumentlisteners.sound.nativeHowl— readonly escape hatch to the underlyingHowl.audio.volumegetter/setter — clamps to[0, 1]; propagates toHowler.volume().- Coverage thresholds tightened to 95/90/100/100 with happy-dom + Howler mock.
- Size budget tightened to 2 KB gzip (shell only; Howler stays external).
tsup.config.tsnow setsminify: true.vitest.config.tsnow setsenvironment: "happy-dom".happy-dom ^15.0.0added todevDependencies.
Decisions / deviations
Section titled “Decisions / deviations”- Linear-curve crossfade, not equal-power. The README and 0.0.1 spec
described equal-power crossfade via AudioContext
linearRampToValueAtTime. However, Howler 2.x’sHowl.fade()is a black-box ramp — it does not expose the curve or schedule it on the AudioContext timeline directly. WiringAudioContext.createGain()+linearRampToValueAtTimewould require bypassing Howler’s mixing layer and is architecturally out of scope for a 0.1.0 thin shell. Shipping linear for 0.1.0; equal-power planned for 0.2.0 (will require a GainNode path alongside Howler’s mix). - Abort-during-crossfade resolves (not rejects). Howler has no fade-cancel
API. Clearing the
setTimeoutand callingresolve()is the honest maximum here. Documented in JSDoc.
[0.0.1] - 2026-05-28
Section titled “[0.0.1] - 2026-05-28”Added (scaffold)
Section titled “Added (scaffold)”- Full package scaffold landed (
package.json,tsconfig.json,tsconfig.test.json,tsup.config.ts,vitest.config.ts,biome.json,scripts/{verify-exports,check-size,build-llms-full}.mjs,test/scaffold.test.ts,examples/.gitkeep,.github/workflows/{ci,publish}.yml,README.md,README_ZHTW.md,CHANGELOG.md,CONTRIBUTING.md,LICENSE,llms.txt,llms-full.txt). src/index.tsis athrowstub exposing the frozen 0.1.0 API surface (createAudio,Audio,Sound,AudioOptions,PlayOptions,CrossfadeOptions,AudioError,AudioDisposedError). The surface is deliberately narrower than Howler.js — only what the ai*js convention needs plus asound.nativeHowlescape hatch (readonly property).pnpm typecheck && pnpm lint && pnpm coverage && pnpm build && pnpm verify:exports && pnpm verify:llms && pnpm check:sizewalks clean against a single placeholder test.- Howler.js declared as a required
peerDependency(^2.2.4); also added todevDependenciesso typecheck / build resolves the types locally. Not optional — aiaudiojs has nothing to do without it. - Coverage thresholds temporarily set to
0/0/0/0; tightened to95/90/100/100in 0.1.0 with real tests (vitest-environment-happy-dom or similar for the AudioContext shim). - Size budget temporarily set to 3 KB gzip; tightened to the 2 KB shell target in 0.1.0 (this is the shell only — Howler.js is the user’s deps graph, not ours).
- Publish workflow exists but trigger is
workflow_dispatchonly — no accidental npm release on tag push until 0.1.0.
Planned for 0.1.0
Section titled “Planned for 0.1.0”createAudio({ autoUnlock?, ... })factory producing an idempotentAudiohandle.audio.unlock()— bind once to first user gesture (touchstart / mousedown / keydown), callHowler.ctx.resume(), detach listeners. Idempotent.audio.load(url, signal?)— wrapsnew Howl({ src: [url] })in a promise resolving whenonloadfires;signalaborts mid-load.Sound.play(opts?)returning Howler’s sound id so multiple concurrent plays of the same buffer are tracked.audio.crossfade(from, to, { duration, signal? })— first-class equal-power crossfade scheduled on the AudioContext timeline (notsetInterval), withAbortSignalcancellation.audio.disposeAll()+ per-Sounddispose(); both idempotent; post-dispose calls throwAudioDisposedError.sound.nativeHowlescape hatch (readonly property) for Howler advanced API.- iOS Safari unlock retry on
visibilitychange— best-effort, not a WebKit fix (those edge cases are upstream).
Decision log (carried over from LEARNINGS.md v0.3.0 cycle 預備區)
Section titled “Decision log (carried over from LEARNINGS.md v0.3.0 cycle 預備區)”- Not a Howler.js fork. Howler is MIT but its 9.7 KB gzip already delivers iOS unlock, HTML5 fallback, spatial, and sprite. Forking the 13-year-old iOS unlock pipeline would burn weeks for no real gain.
- Not from-scratch on raw Web Audio. Same reason: iOS edge cases are WebKit-bound; rewriting them would not fix them.
- Yes: peerDependency thin shell. The ~1.5–2 KB shell delivers the
ai*js conventions Howler doesn’t (
dispose()idempotency, AbortSignal, first-classcrossfade(), named errors) while keeping the proven Howler runtime underneath.