Skip to content

[BREAKING] Remove HTMLAudioElement fallback from sound system#8636

Merged
willeastcott merged 8 commits into
mainfrom
refactor/remove-audio-element-fallback
Apr 22, 2026
Merged

[BREAKING] Remove HTMLAudioElement fallback from sound system#8636
willeastcott merged 8 commits into
mainfrom
refactor/remove-audio-element-fallback

Conversation

@willeastcott
Copy link
Copy Markdown
Contributor

@willeastcott willeastcott commented Apr 22, 2026

Summary

The engine used to ship a parallel HTMLAudioElement-based implementation of its sound system for browsers without Web Audio API support, gated by hasAudioContext(). Every browser the engine targets (Chrome 10+, Firefox 25+, Safari 6+, Edge 12+) has had Web Audio for years, so the fallback was dead code that only ever covered IE — which the engine no longer supports.

This PR removes the fallback and all the branching / duplicated prototype definitions that existed to support it.

Changes

  • Deleted src/platform/sound/capabilities.js (existed solely to gate the fallback).
  • SoundInstance: flattened the constructor to the Web Audio branch and removed the ~230-line Object.assign(SoundInstance.prototype, …) override block (duplicate play/pause/resume/stop/setExternalNodes/clearExternalNodes/getExternalNodes/_createSource/_onLoadedMetadata/_onTimeUpdate/_onManagerDestroy) and the redefined volume / pitch / sound / currentTime accessors.
  • SoundInstance3d: removed the fallOff JS distance-function fallback and the redefined position / maxDistance / refDistance / rollOffFactor / distanceModel accessors.
  • AudioHandler: removed the IE detection IIFE and the new Audio() + canplaythrough branch in _createSound; now uses Web Audio only.
  • Sound: removed the public audio property; constructor now accepts only an AudioBuffer.
  • SoundComponentSystem.context: no longer emits the "Audio context is not supported" warning — it just returns this.manager.context, which already returns null if no AudioContext/webkitAudioContext is available.

webkitAudioContext handling in SoundManager is intentionally left alone.

Net: +80 / -523 lines across 6 files.

Public API changes (breaking)

Sound.audio is removed.

Before

// A Sound could wrap an HTMLAudioElement if Web Audio wasn't available
const sound = new pc.Sound(audioElement);
sound.audio; // HTMLAudioElement

After

// Sound wraps an AudioBuffer only
const sound = new pc.Sound(audioBuffer);
sound.buffer; // AudioBuffer

SoundComponentSystem#context no longer logs a warning or returns null on the theoretical "no Web Audio" browser. It now returns the underlying SoundManager.context, which was already null in the edge case where neither AudioContext nor webkitAudioContext exist. Behaviour is effectively unchanged on every supported browser.

Test plan

  • npm run lint — clean
  • npm run build:types — succeeds; verified Sound.audio, HTMLAudioElement, and hasAudioContext no longer appear in build/playcanvas.d.ts
  • npm test — 1640 passing, 0 failing
  • Manual smoke: run the sound-* examples and confirm 2D / 3D playback still works

The engine used to provide a parallel HTMLAudioElement-based
implementation of SoundInstance / SoundInstance3d / AudioHandler /
Sound for browsers without a Web Audio API (gated via
hasAudioContext()). Every browser the engine targets has had Web
Audio support for many years, so the fallback was dead code that only
covered IE (already unsupported).

- Delete src/platform/sound/capabilities.js.
- Flatten SoundInstance / SoundInstance3d to the Web Audio path;
  remove the prototype override blocks and redefined accessors.
- Remove IE detection and the new Audio() branch from AudioHandler.
- Remove the public Sound.audio property; Sound now accepts only an
  AudioBuffer.
- Simplify SoundComponentSystem.context to return
  this.manager.context directly.

BREAKING: Sound.audio is removed and new Sound(resource) no longer
accepts an HTMLAudioElement.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes the legacy HTMLAudioElement-based sound fallback path and simplifies the engine’s sound stack to rely solely on Web Audio, aligning implementation with the project’s supported browser set.

Changes:

  • Removed hasAudioContext() capability gating (and deleted src/platform/sound/capabilities.js).
  • Flattened SoundInstance / SoundInstance3d to Web Audio-only implementations by deleting the fallback prototype overrides and distance falloff JS emulation.
  • Updated AudioHandler, Sound, and SoundComponentSystem to remove HTMLAudioElement support and associated branching.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/platform/sound/sound.js Sound now wraps an AudioBuffer only; removed audio property and simplified duration.
src/platform/sound/instance3d.js Removed non-WebAudio 3D falloff/path and capability checks; simplified imports.
src/platform/sound/instance.js Removed non-WebAudio fallback implementation and capability gating; constructor and lifecycle now assume Web Audio.
src/platform/sound/capabilities.js Deleted Web Audio capability helper (no longer used).
src/framework/handlers/audio.js Removed IE/HTMLAudioElement loading branch; always decodes into AudioBuffer.
src/framework/components/sound/system.js context accessor no longer warns/branches on capability; returns manager.context.
Comments suppressed due to low confidence (2)

src/platform/sound/sound.js:15

  • Sound is now constructed with a required AudioBuffer, but the buffer field is still documented as AudioBuffer|undefined. If buffer is no longer optional, consider updating the field JSDoc to AudioBuffer to keep generated typings and docs consistent with the new API.
    /**
     * Contains the decoded audio data.
     *
     * @type {AudioBuffer|undefined}
     */
    buffer;

src/platform/sound/instance.js:90

  • The source field is initialized to null, but the JSDoc annotates it as {AudioBufferSourceNode} (non-null). This likely flows into generated typings; update the type to include null (or make the field non-nullable by initializing it to an actual node).
    /**
     * Gets the source that plays the sound resource. Source is only available after calling play.
     *
     * @type {AudioBufferSourceNode}
     */
    source = null;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/platform/sound/instance3d.js
Previously, constructing a SoundInstance or SoundInstance3d when
manager.context was null (i.e. the browser exposes neither AudioContext
nor webkitAudioContext) would throw in _initializeNodes() when it
called createGain() on null.

Guard the construction and public surface so that instances become
inert no-ops in that case instead of crashing:

- SoundInstance: skip _initializeNodes() in the constructor; play()
  returns false; pitch setter skips the context-time snapshot;
  setExternalNodes / clearExternalNodes / _createSource short-circuit.
- SoundInstance3d: constructor skips the panner-backed property
  initialisation after super(); all panner-backed setters / getters
  guard this.panner and return sensible defaults.

AudioHandler already fails cleanly in this case, so the net effect is
that the engine degrades gracefully on Web Audio-less environments
instead of throwing.

Made-with: Cursor
Previously the constructor eagerly probed window for AudioContext /
webkitAudioContext, stashed the constructor on a public `AudioContext`
field, and emitted a warning. This is redundant work at instantiation
time (SoundManager is created on every AppBase, even when no audio is
used) and leaks an implementation detail onto the public surface.

Move the resolution into the lazy `get context()` accessor so the
capability lookup only happens on first access, remove the public
`AudioContext` field, and switch the "not supported" message to
`Debug.warnOnce` so repeated accesses don't spam the log.

Also updates the JSDoc type for `context` from `AudioContext` to
`AudioContext|null` to accurately reflect the returned value.

Made-with: Cursor
Aligns `SoundManager` with the class-fields convention in AGENTS.md
section 15. Pure defaults (`_context = null`, `_userSuspended = false`,
`_volume = 1`) are initialised at the declaration; `this`-referencing
assignments (`_unlockHandlerFunc`, `listener`) are kept in the
constructor, matching the pattern used by `Mouse` / `Keyboard` /
`TouchDevice` after the recent hoist refactor.

All fields now have a JSDoc `@type` block.

Made-with: Cursor
@willeastcott willeastcott changed the title refactor: remove HTMLAudioElement fallback from sound system [BREAKING] Remove HTMLAudioElement fallback from sound system Apr 22, 2026
Copy link
Copy Markdown
Contributor

@mvaligursky mvaligursky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@willeastcott willeastcott merged commit 7950cae into main Apr 22, 2026
8 checks passed
@willeastcott willeastcott deleted the refactor/remove-audio-element-fallback branch April 22, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: audio enhancement Request for a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants