Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/loader/filetypes/AudioFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ var AudioFile = new Class({
var _this = this;

// interesting read https://github.com/WebAudio/web-audio-api/issues/1305
this.config.context.decodeAudioData(this.xhrLoader.response,
this.config.offlineContext.decodeAudioData(this.xhrLoader.response,
Copy link

Choose a reason for hiding this comment

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

Config property name mismatch causes audio decoding crash

High Severity

The onProcess method accesses this.config.offlineContext but the config object is constructed at line 61 with the property name context (i.e., { context: audioContext }). Since offlineContext was never set on the config, this will be undefined, and calling decodeAudioData on undefined will throw a runtime error. Every audio file load will crash.

Fix in Cursor Fix in Web

function (audioBuffer)
{
_this.data = audioBuffer;
Expand Down
2 changes: 1 addition & 1 deletion src/sound/webaudio/WebAudioSound.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var WebAudioSound = new Class({

if (!this.audioBuffer)
{
throw new Error('Audio key "' + key + '" not found in cache');
throw new Error('Audio key not found: ' + key);
}

/**
Expand Down
142 changes: 100 additions & 42 deletions src/sound/webaudio/WebAudioSoundManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,23 @@ var WebAudioSoundManager = new Class({

function WebAudioSoundManager (game)
{
/**
* The OfflineAudioContext used for audio decoding and state management.
*
* @name Phaser.Sound.WebAudioSoundManager#offlineContext
* @type {OfflineAudioContext}
* @since 3.85.0
*/
this.offlineContext = new OfflineAudioContext(2, 1, 8000);

/**
* The AudioContext being used for playback.
*
* @name Phaser.Sound.WebAudioSoundManager#context
* @type {AudioContext}
* @since 3.0.0
*/
this.context = this.createAudioContext(game);
this.context = null;

/**
* Gain node responsible for controlling global muting.
Expand All @@ -53,7 +62,7 @@ var WebAudioSoundManager = new Class({
* @type {GainNode}
* @since 3.0.0
*/
this.masterMuteNode = this.context.createGain();
this.masterMuteNode = null;

/**
* Gain node responsible for controlling global volume.
Expand All @@ -62,11 +71,7 @@ var WebAudioSoundManager = new Class({
* @type {GainNode}
* @since 3.0.0
*/
this.masterVolumeNode = this.context.createGain();

this.masterMuteNode.connect(this.masterVolumeNode);

this.masterVolumeNode.connect(this.context.destination);
this.masterVolumeNode = null;

/**
* Destination node for connecting individual sounds to.
Expand All @@ -75,19 +80,31 @@ var WebAudioSoundManager = new Class({
* @type {AudioNode}
* @since 3.0.0
*/
this.destination = this.masterMuteNode;
this.destination = null;

this.locked = this.context.state === 'suspended' && ('ontouchstart' in window || 'onclick' in window);
this._volume = 1;
this._mute = false;

BaseSoundManager.call(this, game);

if (this.locked && game.isBooted)
this.locked = this.offlineContext.state === 'suspended';
Copy link

Choose a reason for hiding this comment

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

OfflineAudioContext always suspended prevents AudioContext creation

High Severity

An OfflineAudioContext always starts in 'suspended' state per the Web Audio API spec, so this.locked at line 90 will always be true. This means createAudioContext() in the else branch (line 107) is never called. Meanwhile, the unlockHandler has the createAudioContext() call commented out (lines 347–350) and checks if (_this.context && body) — which is always false since context remains null. The AudioContext is never created, so audio can never play.

Additional Locations (1)

Fix in Cursor Fix in Web


console.log('SM locked', this.locked);
Copy link

Choose a reason for hiding this comment

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

Debug console.log statements left in production code

Medium Severity

Multiple console.log debug statements were left in the code: 'SM locked' (line 92), 'createAudioContext' (line 125), 'decodeAudio' (line 243), 'SM unlock' (line 337), and 'SM unlockHandler invoked' (line 345). These are clearly development/debugging aids that don't belong in production code for a framework library.

Additional Locations (2)

Fix in Cursor Fix in Web


if (this.locked)
{
this.unlock();
if (game.isBooted)
{
this.unlock();
}
else
{
game.events.once(GameEvents.BOOT, this.unlock, this);
}
}
else
{
game.events.once(GameEvents.BOOT, this.unlock, this);
this.createAudioContext();
}
},

Expand All @@ -101,29 +118,35 @@ var WebAudioSoundManager = new Class({
* @method Phaser.Sound.WebAudioSoundManager#createAudioContext
* @since 3.0.0
*
* @param {Phaser.Game} game - Reference to the current game instance.
*
* @return {AudioContext} The AudioContext instance to be used for playback.
*/
createAudioContext: function (game)
createAudioContext: function ()
{
var audioConfig = game.config.audio;
console.log('createAudioContext');

var audioConfig = this.game.config.audio;
var context;

if (audioConfig.context)
{
audioConfig.context.resume();

return audioConfig.context;
context = audioConfig.context;
}

if (window.hasOwnProperty('AudioContext'))
else if (window.hasOwnProperty('AudioContext'))
{
return new AudioContext();
context = new AudioContext();
}
else if (window.hasOwnProperty('webkitAudioContext'))
{
return new window.webkitAudioContext();
context = new window.webkitAudioContext();
}

this.setAudioContext(context);

this.locked = this.context.state === 'suspended';

return context;
},

/**
Expand Down Expand Up @@ -167,6 +190,9 @@ var WebAudioSoundManager = new Class({

this.destination = this.masterMuteNode;

this.setVolume(this._volume);
this.setMute(this._mute);

return this;
},

Expand Down Expand Up @@ -214,6 +240,8 @@ var WebAudioSoundManager = new Class({
*/
decodeAudio: function (audioKey, audioData)
{
console.log('decodeAudio', audioKey);

var audioFiles;

if (!Array.isArray(audioKey))
Expand Down Expand Up @@ -306,12 +334,21 @@ var WebAudioSoundManager = new Class({
*/
unlock: function ()
{
console.log('SM unlock');
Copy link

Choose a reason for hiding this comment

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

decodeAudio calls method on null context reference

High Severity

The decodeAudio method calls this.context.decodeAudioData(...) at line 298, but this.context can now be null since the AudioContext creation is deferred. If decodeAudio is called before the context is created (which is likely given the locked-state logic issues), this will throw a TypeError.

Fix in Cursor Fix in Web


var _this = this;

var body = document.body;

var unlockHandler = function unlockHandler ()
{
console.log('SM unlockHandler invoked');

// if (!_this.context)
// {
// _this.createAudioContext();
// }

if (_this.context && body)
{
var bodyRemove = body.removeEventListener.bind(body);
Expand Down Expand Up @@ -353,7 +390,7 @@ var WebAudioSoundManager = new Class({
*/
onBlur: function ()
{
if (!this.locked)
if (!this.locked && this.context)
{
this.context.suspend();
}
Expand Down Expand Up @@ -392,20 +429,23 @@ var WebAudioSoundManager = new Class({
*/
update: function (time, delta)
{
var listener = this.context.listener;

if (listener && listener.positionX !== undefined)
if (this.context)
{
var x = GetFastValue(this.listenerPosition, 'x', null);
var y = GetFastValue(this.listenerPosition, 'y', null);
var listener = this.context.listener;

if (x && x !== this._spatialx)
{
this._spatialx = listener.positionX.value = x;
}
if (y && y !== this._spatialy)
if (listener && listener.positionX !== undefined)
{
this._spatialy = listener.positionY.value = y;
var x = GetFastValue(this.listenerPosition, 'x', null);
var y = GetFastValue(this.listenerPosition, 'y', null);

if (x && x !== this._spatialx)
{
this._spatialx = listener.positionX.value = x;
}
if (y && y !== this._spatialy)
{
this._spatialy = listener.positionY.value = y;
}
}
}

Expand All @@ -428,16 +468,24 @@ var WebAudioSoundManager = new Class({
destroy: function ()
{
this.destination = null;
this.masterVolumeNode.disconnect();
this.masterVolumeNode = null;
this.masterMuteNode.disconnect();
this.masterMuteNode = null;

if (this.masterVolumeNode)
{
this.masterVolumeNode.disconnect();
this.masterVolumeNode = null;
}

if (this.masterMuteNode)
{
this.masterMuteNode.disconnect();
this.masterMuteNode = null;
}

if (this.game.config.audio.context)
{
this.context.suspend();
}
else
else if (this.context)
{
var _this = this;

Expand Down Expand Up @@ -478,12 +526,17 @@ var WebAudioSoundManager = new Class({

get: function ()
{
return (this.masterMuteNode.gain.value === 0);
return this._mute;
},

set: function (value)
{
this.masterMuteNode.gain.setValueAtTime(value ? 0 : 1, 0);
Copy link

Choose a reason for hiding this comment

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

Missing null check on context in destroy method

Medium Severity

The destroy method's first branch checks this.game.config.audio.context (the config value) and then calls this.context.suspend() without null-checking this.context. Since this.context now starts as null and may never be assigned (due to the deferred creation), calling suspend() on null will throw a TypeError. The else if branch correctly guards with this.context, but this branch does not.

Fix in Cursor Fix in Web

this._mute = value;

if (this.masterMuteNode)
{
this.masterMuteNode.gain.setValueAtTime(value ? 0 : 1, 0);
}

this.emit(Events.GLOBAL_MUTE, this, value);
}
Expand Down Expand Up @@ -518,12 +571,17 @@ var WebAudioSoundManager = new Class({

get: function ()
{
return this.masterVolumeNode.gain.value;
return this._volume;
},

set: function (value)
{
this.masterVolumeNode.gain.setValueAtTime(value, 0);
this._volume = value;

if (this.masterVolumeNode)
{
this.masterVolumeNode.gain.setValueAtTime(value, 0);
}

this.emit(Events.GLOBAL_VOLUME, this, value);
}
Expand Down