/**
* A FORGE.SoundManager is an object to manage all sounds.
*
* @constructor FORGE.SoundManager
* @param {FORGE.Viewer} viewer - The {@link FORGE.Viewer} reference.
* @extends {FORGE.BaseObject}
*
* @todo Start/Stop sound to avoid autoplay
*/
FORGE.SoundManager = function(viewer)
{
/**
* The viewer reference.
* @name FORGE.SoundManager#_viewer
* @type {FORGE.Viewer}
* @private
*/
this._viewer = viewer;
/**
* The global sound config backup.
* @name FORGE.SoundManager#_config
* @type {?AudioConfig}
* @private
*/
this._config = null;
/**
* The global volume for sounds.
* @name FORGE.SoundManager#_volume
* @type {number}
* @private
*/
this._volume = 1;
/**
* The volume has been changed?
* @name FORGE.SoundManager#_volumeChanged
* @type {boolean}
* @private
*/
this._volumeChanged = false;
/**
* The default volume for sounds.
* Can't be greater than the maximum volume.
* @name FORGE.SoundManager#_defaultVolume
* @type {number}
* @private
*/
this._defaultVolume = 1;
/**
* The maximum volume for sounds.
* @name FORGE.SoundManager#_maxVolume
* @type {number}
* @private
*/
this._maxVolume = 1;
/**
* The save of the global volume for sounds before a mute.
* @name FORGE.SoundManager#_mutedVolume
* @type {number}
* @private
*/
this._mutedVolume = 1;
/**
* Are all sounds muted?
* @name FORGE.SoundManager#_muted
* @type {boolean}
* @private
*/
this._muted = false;
/**
* Is the sound manager enabled?
* @name FORGE.SoundManager#_enabled
* @type {boolean}
* @private
*/
this._enabled = true;
/**
* Array of {@link FORGE.Sound}.
* @name FORGE.SoundManager#_sounds
* @type {?Array<FORGE.Sound>}
* @private
*/
this._sounds = null;
/**
* Is audio deactivated?
* @name FORGE.SoundManager#_noAudio
* @type {boolean}
* @private
*/
this._noAudio = false;
/**
* Is Audio tag activated?
* @name FORGE.SoundManager#_useAudioTag
* @type {boolean}
* @private
*/
this._useAudioTag = false;
/**
* Is WebAudio API activated?
* @name FORGE.SoundManager#_useWebAudio
* @type {boolean}
* @private
*/
this._useWebAudio = true;
/**
* Number of sound channels.
* @name FORGE.SoundManager#_channels
* @type {number}
* @private
*/
this._channels = 32;
/**
* The AudioContext interface.
* @name FORGE.SoundManager#_context
* @type {?(AudioContext|webkitAudioContext)}
* @private
*/
this._context = null;
/**
* The AudioContext state.
* @name FORGE.SoundManager#_contextState
* @type {string}
* @private
*/
this._contextState = "running";
/**
* AnalyserNode to expose audio time and frequency data and create data visualisations.
* @name FORGE.SoundManager#_analyser
* @type {AnalyserNode}
* @private
*/
this._analyser = null;
/**
* Master GainNode used to control the overall volume of the audio graph.
* @name FORGE.SoundManager#_masterGain
* @type {GainNode}
* @private
*/
this._masterGain = null;
/**
* On sounds muted event dispatcher.
* @name FORGE.SoundManager#_onMute
* @type {?FORGE.EventDispatcher}
* @private
*/
this._onMute = null;
/**
* On sounds unmuted event dispatcher.
* @name FORGE.SoundManager#_onUnmute
* @type {?FORGE.EventDispatcher}
* @private
*/
this._onUnmute = null;
/**
* On sounds volume change event dispatcher.
* @name FORGE.SoundManager#_onVolumeChange
* @type {?FORGE.EventDispatcher}
* @private
*/
this._onVolumeChange = null;
/**
* On sounds disabled event dispatcher.
* @name FORGE.SoundManager#_onDisable
* @type {?FORGE.EventDispatcher}
* @private
*/
this._onDisable = null;
/**
* On sounds enabled event dispatcher.
* @name FORGE.SoundManager#_onEnable
* @type {?FORGE.EventDispatcher}
* @private
*/
this._onEnable = null;
FORGE.BaseObject.call(this, "SoundManager");
};
FORGE.SoundManager.prototype = Object.create(FORGE.BaseObject.prototype);
FORGE.SoundManager.prototype.constructor = FORGE.SoundManager;
/**
* Boot sequence.
* @method FORGE.SoundManager#_boot
* @private
* @suppress {deprecated}
*/
FORGE.SoundManager.prototype.boot = function()
{
this._sounds = [];
if (FORGE.Device.iOS && FORGE.Device.webAudio === false)
{
this._channels = 1; //only 1 channel in iOS with AudioTag support
}
if (FORGE.Device.webAudio === true)
{
try
{
if (typeof window.AudioContext === "function")
{
this._context = new window.AudioContext();
}
else
{
this._context = new window.webkitAudioContext();
}
}
catch (error)
{
this._context = null;
this._useWebAudio = false;
this._noAudio = true;
}
}
if (FORGE.Device.audioTag === true && this._context === null)
{
this._useWebAudio = false;
this._useAudioTag = true;
this._noAudio = false;
}
if (this._context !== null)
{
if (typeof this._context.createGain === "undefined")
{
this._masterGain = this._context.createGainNode();
}
else
{
this._masterGain = this._context.createGain();
}
this._analyser = this._context.createAnalyser();
this._analyser.connect(this._masterGain);
this._masterGain.gain.value = this._volume;
this._masterGain.connect(this._context.destination);
// The common coordinate system used with WebGL.
// The listener is always facing down the negative Z axis, the
// positive Y axis points up, the positive X axis points right.
this._context.listener.setOrientation(0, 0, -1, 0, 1, 0);
}
this._viewer.story.onSceneLoadStart.add(this._sceneLoadStartHandler, this);
};
/**
* Event handler for scene start.
* @method FORGE.SoundManager#_sceneLoadStartHandler
* @private
*/
FORGE.SoundManager.prototype._sceneLoadStartHandler = function()
{
if(typeof this._viewer.story.scene.config.audio !== "undefined")
{
this._parseSceneConfig(this._viewer.story.scene.config.audio);
}
else
{
//restore global sounds config
this._applyConfig(this._config);
//Is the sound manager enabled?
if(this._enabled === false)
{
if(this._onDisable !== null)
{
this._onDisable.dispatch();
}
}
else
{
if(this._onEnable !== null)
{
this._onEnable.dispatch();
}
}
}
};
/**
* Parse the scene configuration part related to sounds.
* @method FORGE.SoundManager#_parseSceneConfig
* @private
* @param {AudioConfig} config - The scene configuration part related to sounds.
*/
FORGE.SoundManager.prototype._parseSceneConfig = function(config)
{
var extendedConfig = /** @type {AudioConfig} */ FORGE.Utils.extendMultipleObjects(this._config, config);
this._applyConfig(extendedConfig);
//Is the sound manager enabled?
if(this._enabled === false)
{
if(this._onDisable !== null)
{
this._onDisable.dispatch();
}
}
else
{
if(this._onEnable !== null)
{
this._onEnable.dispatch();
}
}
};
/**
* Set values from configuration file.
* @method FORGE.SoundManager#_applyConfig
* @param {?AudioConfig} config - The config file.
* @private
*/
FORGE.SoundManager.prototype._applyConfig = function(config)
{
if(config !== null)
{
this._enabled = typeof config.enabled !== "undefined" ? Boolean(config.enabled) : true;
this._maxVolume = (typeof config.volume !== "undefined" && typeof config.volume.max === "number") ? FORGE.Math.clamp(config.volume.max, 0, 1) : 1;
if(this._volumeChanged === false)
{
this._defaultVolume = (typeof config.volume !== "undefined" && typeof config.volume.default === "number") ? FORGE.Math.clamp(config.volume.default, 0, this._maxVolume) : 1;
}
else
{
this._defaultVolume = FORGE.Math.clamp(this._volume, 0, this._maxVolume);
}
}
};
/**
* Add a sound config to the manager.
* @method FORGE.SoundManager#addConfig
* @param {AudioConfig} config - The config you want to add.
*/
FORGE.SoundManager.prototype.addConfig = function(config)
{
this._parseConfig(config);
this._initSounds();
};
/**
* Parse a playlist config object.
* @method FORGE.SoundManager#_parseConfig
* @private
* @param {AudioConfig} config - The config you want to parse.
*/
FORGE.SoundManager.prototype._parseConfig = function(config)
{
this._config = config;
this._applyConfig(config);
};
/**
* Initialize the sounds manager.
* @method FORGE.PlaylistManager#_initSounds
* @private
*/
FORGE.SoundManager.prototype._initSounds = function()
{
if(typeof this._defaultVolume === "number" && this._volumeChanged === false)
{
this._volume = FORGE.Math.clamp(this._defaultVolume, 0, 1);
}
this._updateVolume();
};
/**
* Update method called by the viewer main loop.
* @method FORGE.SoundManager#update
*/
FORGE.SoundManager.prototype.update = function ()
{
for (var i = 0, ii = this._sounds.length; i < ii; i++)
{
this._sounds[i].update();
}
// update listener position
this._setContextListenerOrientation();
};
/**
* Add a {@link FORGE.Sound} into the _sounds Array.
* @method FORGE.SoundManager#add
* @param {FORGE.Sound} sound - The {@link FORGE.Sound} to add.
*/
FORGE.SoundManager.prototype.add = function (sound)
{
var index = this._indexOfSound(sound);
if(index === -1)
{
this._sounds.push(sound);
}
};
/**
* Remove a {@link FORGE.Sound} into the _sounds Array.
* @method FORGE.SoundManager#remove
* @param {FORGE.Sound} sound - The {@link FORGE.Sound} to remove.
*/
FORGE.SoundManager.prototype.remove = function (sound)
{
var index = this._indexOfSound(sound);
if(index > -1)
{
this._sounds.splice(index, 1);
}
};
/**
* Internal method to find a {@link FORGE.Sound} index in the _sounds Array.
* @method FORGE.SoundManager#_indexOfSound
* @private
* @param {FORGE.Sound} sound - The {@link FORGE.Sound} itself.
* @return {number} Returns the index of the searched {@link FORGE.Sound} if found, -1 if not.
*
* @todo Either the {@link FORGE.Sound} itself or its index or its uid. (FORGE.Sound|Number|String)
*/
FORGE.SoundManager.prototype._indexOfSound = function (sound)
{
if(this._sounds === null)
{
return -1;
}
var _sound;
for (var i = 0, ii = this._sounds.length; i < ii; i++)
{
_sound = this._sounds[i];
if(_sound === sound)
{
return i;
}
}
return -1;
};
/**
* Suspend audio context if no sound are playing.
* @method FORGE.SoundManager#suspend
*/
FORGE.SoundManager.prototype.suspend = function()
{
if(this._context !== null && typeof this._context.suspend !== "undefined" && this._useWebAudio === true && FORGE.Device.safari === false)
{
if(this._contextState === "running")
{
var allStopped = true;
for (var i = 0, ii = this._sounds.length; i < ii; i++)
{
if(this._sounds[i].playing === true || this._sounds[i].paused === true)
{
allStopped = false;
break;
}
}
if(allStopped === true)
{
this._contextState = "suspended";
this._context.suspend();
}
}
}
};
/**
* Resume the audio context if at least one sound is playing.
* @method FORGE.SoundManager#resume
*/
FORGE.SoundManager.prototype.resume = function()
{
if(this._context !== null && typeof this._context.resume !== "undefined" && this._useWebAudio === true && FORGE.Device.safari === false)
{
if(this._contextState === "suspended")
{
for (var i = 0, ii = this._sounds.length; i < ii; i++)
{
if(this._sounds[i].playing === true || this._sounds[i].paused === true)
{
this._contextState = "running";
this._context.resume();
break;
}
}
}
}
};
/**
* Pause all playing sounds.
* @method FORGE.SoundManager#pauseAll
*/
FORGE.SoundManager.prototype.pauseAll = function()
{
for (var i = 0, ii = this._sounds.length; i < ii; i++)
{
if (this._sounds[i].playing === true)
{
this._sounds[i].pause();
this._sounds[i].resumed = true;
}
}
};
/**
* Play all sounds that have been paused with the pauseAll method.
* @method FORGE.SoundManager#resumeAll
*/
FORGE.SoundManager.prototype.resumeAll = function()
{
for (var i = 0, ii = this._sounds.length; i < ii; i++)
{
if (this._sounds[i].resumed === true)
{
this._sounds[i].resume();
this._sounds[i].resumed = false;
}
}
};
/**
* Mute method of the sounds.
* @method FORGE.SoundManager#mute
*/
FORGE.SoundManager.prototype.mute = function()
{
if(this._muted === true)
{
return;
}
this._muted = true;
this._mutedVolume = this._volume;
this._volume = 0;
if (this._useWebAudio === true)
{
this._mutedVolume = this._masterGain.gain.value;
}
this._updateVolume();
if(this._onMute !== null)
{
this._onMute.dispatch();
}
};
/**
* Unmute method of the sounds.
* @method FORGE.SoundManager#unmute
*/
FORGE.SoundManager.prototype.unmute = function()
{
if(this._muted === false)
{
return;
}
this._muted = false;
this._volume = this._mutedVolume;
this._updateVolume();
if(this._onUnmute !== null)
{
this._onUnmute.dispatch();
}
};
/**
* Update volume method for the sounds.
* @method FORGE.SoundManager#unmute
* @private
*/
FORGE.SoundManager.prototype._updateVolume = function()
{
if (this._useWebAudio === true)
{
this._masterGain.gain.value = this._volume;
}
else if (this._useAudioTag === true)
{
// Loop through the sound cache and change the volume of all html audio tags
for (var i = 0; i < this._sounds.length; i++)
{
this._sounds[i]._sound.data.volume = FORGE.Math.clamp(this._sounds[i]._volume, 0, 1) * this._volume;
}
}
this._volumeChanged = true;
if(this._onVolumeChange !== null)
{
this._onVolumeChange.dispatch();
}
};
/**
* Change the sound manager orientation to follow the THREE perspective camera.
* @method FORGE.SoundManager#_setContextListenerOrientation
* @private
*/
FORGE.SoundManager.prototype._setContextListenerOrientation = function()
{
if (this._useWebAudio === true && this._viewer.renderer.camera.main !== null)
{
var cameraDirection = new THREE.Vector3();
var qCamera = this._viewer.renderer.camera.main.quaternion;
// front vector indicating where the listener is facing to
cameraDirection.set(0, 0, -1);
cameraDirection.applyQuaternion(qCamera);
var camera = cameraDirection.clone();
// up vector repesenting the direction of the top of the listener head
cameraDirection.set(0, 1, 0);
cameraDirection.applyQuaternion(qCamera);
var cameraUp = cameraDirection;
// apply orientation values
this._context.listener.setOrientation(camera.x, camera.y, camera.z, cameraUp.x, cameraUp.y, cameraUp.z);
}
};
/**
* Destroy sequence
* @method FORGE.SoundManager#destroy
*/
FORGE.SoundManager.prototype.destroy = function()
{
this._viewer.story.onSceneLoadStart.remove(this._sceneLoadStartHandler, this);
this._viewer = null;
this._config = null;
var i = this._sounds.length;
while(i--)
{
this._sounds[i].destroy();
}
this._sounds = null;
this._context = null;
this._analyser = null;
this._masterGain = null;
if(this._onMute !== null)
{
this._onMute.destroy();
this._onMute = null;
}
if(this._onUnmute !== null)
{
this._onUnmute.destroy();
this._onUnmute = null;
}
if(this._onVolumeChange !== null)
{
this._onVolumeChange.destroy();
this._onVolumeChange = null;
}
if(this._onDisable !== null)
{
this._onDisable.destroy();
this._onDisable = null;
}
if(this._onEnable !== null)
{
this._onEnable.destroy();
this._onEnable = null;
}
FORGE.BaseObject.prototype.destroy.call(this);
};
/**
* Get the audio context.
* @name FORGE.SoundManager#context
* @type {?AudioContext}
* @readonly
*/
Object.defineProperty(FORGE.SoundManager.prototype, "context",
{
/** @this {FORGE.SoundManager} */
get: function()
{
return this._context;
}
});
/**
* Get the analyser.
* @name FORGE.SoundManager#analyser
* @type {?AnalyserNode}
* @readonly
*/
Object.defineProperty(FORGE.SoundManager.prototype, "analyser",
{
/** @this {FORGE.SoundManager} */
get: function()
{
return this._analyser;
}
});
/**
* Get the analyser
* @name FORGE.SoundManager#inputNode
* @type {?AudioDestinationNode}
* @readonly
*/
Object.defineProperty(FORGE.SoundManager.prototype, "inputNode",
{
/** @this {FORGE.SoundManager} */
get: function()
{
return this._analyser;
}
});
/**
* Get the masterGain.
* @name FORGE.SoundManager#masterGain
* @type {?GainNode}
* @readonly
*/
Object.defineProperty(FORGE.SoundManager.prototype, "masterGain",
{
/** @this {FORGE.SoundManager} */
get: function()
{
return this._masterGain;
}
});
/**
* The WebAudio API tag must be used?
* @name FORGE.SoundManager#useWebAudio
* @type {boolean}
* @readonly
*/
Object.defineProperty(FORGE.SoundManager.prototype, "useWebAudio",
{
/** @this {FORGE.SoundManager} */
get: function()
{
return this._useWebAudio;
},
/** @this {FORGE.SoundManager} */
set: function(value)
{
if(typeof value === "boolean")
{
this._useWebAudio = value;
this._useAudioTag = !value;
}
}
});
/**
* The Audio tag must be used?
* @name FORGE.SoundManager#useAudioTag
* @type {boolean}
* @readonly
*/
Object.defineProperty(FORGE.SoundManager.prototype, "useAudioTag",
{
/** @this {FORGE.SoundManager} */
get: function()
{
return this._useAudioTag;
},
/** @this {FORGE.SoundManager} */
set: function(value)
{
if(typeof value === "boolean")
{
this._useAudioTag = value;
this._useWebAudio = !value;
}
}
});
/**
* Get the enabled state for sounds.
* @name FORGE.SoundManager#enabled
* @type {boolean}
*/
Object.defineProperty(FORGE.SoundManager.prototype, "enabled", {
/** @this {FORGE.SoundManager} */
get: function()
{
return this._enabled;
}
});
/**
* Get or set the muted state for sounds.
* @name FORGE.SoundManager#muted
* @type {boolean}
*/
Object.defineProperty(FORGE.SoundManager.prototype, "muted", {
/** @this {FORGE.SoundManager} */
get: function()
{
return this._muted;
},
/** @this {FORGE.SoundManager} */
set: function(value)
{
if(typeof value === "boolean")
{
if (value === true)
{
this.mute();
}
else
{
this.unmute();
}
}
}
});
/**
* Get or set the global volume for sounds.
* @name FORGE.SoundManager#volume
* @type {number}
*/
Object.defineProperty(FORGE.SoundManager.prototype, "volume", {
/** @this {FORGE.SoundManager} */
get: function()
{
if (this._useWebAudio === true)
{
return this._masterGain.gain.value;
}
else
{
return this._volume;
}
},
/** @this {FORGE.SoundManager} */
set: function(value)
{
if(typeof value !== "number")
{
return;
}
value = FORGE.Math.clamp(value, 0, 1);
if(this._maxVolume < value)
{
this._volume = this._maxVolume;
}
else
{
this._volume = value;
}
if (this._volume > 0)
{
this._muted = false;
}
this._updateVolume();
}
});
/**
* Get the sounds "onMute" event {@link FORGE.EventDispatcher}.
* The {@link FORGE.EventDispatcher} is created only if you ask for it.
* @name FORGE.SoundManager#onMute
* @type {FORGE.EventDispatcher}
*/
Object.defineProperty(FORGE.SoundManager.prototype, "onMute",
{
/** @this {FORGE.SoundManager} */
get: function()
{
if(this._onMute === null)
{
this._onMute = new FORGE.EventDispatcher(this);
}
return this._onMute;
}
});
/**
* Get the sounds "onUnmute" event {@link FORGE.EventDispatcher}.
* The {@link FORGE.EventDispatcher} is created only if you ask for it.
* @name FORGE.SoundManager#onUnmute
* @type {FORGE.EventDispatcher}
*/
Object.defineProperty(FORGE.SoundManager.prototype, "onUnmute",
{
/** @this {FORGE.SoundManager} */
get: function()
{
if(this._onUnmute === null)
{
this._onUnmute = new FORGE.EventDispatcher(this);
}
return this._onUnmute;
}
});
/**
* Get the sounds "onVolumeChange" event {@link FORGE.EventDispatcher}.
* The {@link FORGE.EventDispatcher} is created only if you ask for it.
* @name FORGE.SoundManager#onVolumeChange
* @type {FORGE.EventDispatcher}
*/
Object.defineProperty(FORGE.SoundManager.prototype, "onVolumeChange",
{
/** @this {FORGE.SoundManager} */
get: function()
{
if(this._onVolumeChange === null)
{
this._onVolumeChange = new FORGE.EventDispatcher(this);
}
return this._onVolumeChange;
}
});
/**
* Get the sounds "onDisable" event {@link FORGE.EventDispatcher}.
* The {@link FORGE.EventDispatcher} is created only if you ask for it.
* @name FORGE.SoundManager#onDisable
* @type {FORGE.EventDispatcher}
*/
Object.defineProperty(FORGE.SoundManager.prototype, "onDisable",
{
/** @this {FORGE.SoundManager} */
get: function()
{
if(this._onDisable === null)
{
this._onDisable = new FORGE.EventDispatcher(this);
}
return this._onDisable;
}
});
/**
* Get the sounds "onEnable" event {@link FORGE.EventDispatcher}.
* The {@link FORGE.EventDispatcher} is created only if you ask for it.
* @name FORGE.SoundManager#onDisable
* @type {FORGE.EventDispatcher}
*/
Object.defineProperty(FORGE.SoundManager.prototype, "onEnable",
{
/** @this {FORGE.SoundManager} */
get: function()
{
if(this._onEnable === null)
{
this._onEnable = new FORGE.EventDispatcher(this);
}
return this._onEnable;
}
});