phaser/src/sound/Sound.js

829 lines
23 KiB
JavaScript
Raw Normal View History

2013-10-01 12:54:29 +00:00
/**
* @author Richard Davey <rich@photonstorm.com>
* @copyright 2013 Photon Storm Ltd.
* @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
*/
/**
2013-10-01 12:54:29 +00:00
* The Sound class constructor.
*
2013-10-01 12:54:29 +00:00
* @class Phaser.Sound
* @classdesc The Sound class
* @constructor
2013-10-01 12:54:29 +00:00
* @param {Phaser.Game} game - Reference to the current game instance.
* @param {string} key - Asset key for the sound.
2013-10-02 19:18:24 +00:00
* @param {number} [volume=1] - Default value for the volume, between 0 and 1.
* @param {boolean} [loop=false] - Whether or not the sound will loop.
*/
Phaser.Sound = function (game, key, volume, loop) {
if (typeof volume == 'undefined') { volume = 1; }
if (typeof loop == 'undefined') { loop = false; }
2013-09-19 14:34:48 +00:00
/**
* A reference to the currently running Game.
2013-10-01 12:54:29 +00:00
* @property {Phaser.Game} game
2013-09-19 14:34:48 +00:00
*/
this.game = game;
2013-09-19 14:34:48 +00:00
/**
2013-10-01 12:54:29 +00:00
* Name of the sound.
* @property {string} name
* @default
2013-09-19 14:34:48 +00:00
*/
2013-09-20 22:21:12 +00:00
this.name = key;
2013-09-19 14:34:48 +00:00
/**
* Asset key for the sound.
2013-10-01 12:54:29 +00:00
* @property {string} key
2013-09-19 14:34:48 +00:00
*/
this.key = key;
2013-09-19 14:34:48 +00:00
/**
* Whether or not the sound will loop.
2013-10-01 15:39:39 +00:00
* @property {boolean} loop
2013-09-19 14:34:48 +00:00
*/
this.loop = loop;
2013-09-19 14:34:48 +00:00
/**
2013-10-01 12:54:29 +00:00
* The global audio volume. A value between 0 (silence) and 1 (full volume).
* @property {number} _volume
2013-09-19 14:34:48 +00:00
* @private
*/
this._volume = volume;
2013-09-19 14:34:48 +00:00
/**
2013-10-01 12:54:29 +00:00
* The sound markers, empty by default.
* @property {object} markers
2013-09-19 14:34:48 +00:00
*/
this.markers = {};
/**
* Reference to AudioContext instance.
2013-10-01 12:54:29 +00:00
* @property {AudioContext} context
* @default
*/
this.context = null;
/**
* Decoded data buffer / Audio tag.
2013-10-01 12:54:29 +00:00
* @property {Description} _buffer
* @private
*/
this._buffer = null;
2013-09-19 14:34:48 +00:00
/**
2013-10-01 12:54:29 +00:00
* Boolean indicating whether the game is on "mute".
2013-10-01 15:39:39 +00:00
* @property {boolean} _muted
2013-09-19 14:34:48 +00:00
* @private
2013-10-01 12:54:29 +00:00
* @default
2013-09-19 14:34:48 +00:00
*/
this._muted = false;
2013-09-19 14:34:48 +00:00
/**
2013-10-01 12:54:29 +00:00
* Boolean indicating whether the sound should start automatically.
2013-10-01 15:39:39 +00:00
* @property {boolean} autoplay
2013-10-01 12:54:29 +00:00
* @private
2013-09-19 14:34:48 +00:00
*/
this.autoplay = false;
2013-09-19 14:34:48 +00:00
/**
* The total duration of the sound, in milliseconds
2013-10-01 12:54:29 +00:00
* @property {number} totalDuration
* @default
2013-09-19 14:34:48 +00:00
*/
this.totalDuration = 0;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {number} startTime
* @default
*/
this.startTime = 0;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {number} currentTime
* @default
*/
this.currentTime = 0;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {number} duration
* @default
*/
this.duration = 0;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {number} stopTime
2013-10-01 12:54:29 +00:00
*/
this.stopTime = 0;
2013-10-01 12:54:29 +00:00
/**
* Description.
2013-10-01 15:39:39 +00:00
* @property {boolean} paused
2013-10-01 12:54:29 +00:00
* @default
*/
this.paused = false;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {number} pausedPosition
*/
this.pausedPosition = 0;
/**
* Description.
* @property {number} pausedTime
*/
this.pausedTime = 0;
2013-10-01 12:54:29 +00:00
/**
* Description.
2013-10-01 15:39:39 +00:00
* @property {boolean} isPlaying
2013-10-01 12:54:29 +00:00
* @default
*/
this.isPlaying = false;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {string} currentMarker
* @default
*/
this.currentMarker = '';
2013-10-01 12:54:29 +00:00
/**
* Description.
2013-10-01 15:39:39 +00:00
* @property {boolean} pendingPlayback
2013-10-01 12:54:29 +00:00
* @default
*/
this.pendingPlayback = false;
2013-10-01 12:54:29 +00:00
/**
* Description.
2013-10-01 15:39:39 +00:00
* @property {boolean} override
2013-10-01 12:54:29 +00:00
* @default
*/
this.override = false;
2013-10-01 12:54:29 +00:00
/**
* Description.
2013-10-01 15:39:39 +00:00
* @property {boolean} usingWebAudio
2013-10-01 12:54:29 +00:00
*/
this.usingWebAudio = this.game.sound.usingWebAudio;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {Description} usingAudioTag
*/
this.usingAudioTag = this.game.sound.usingAudioTag;
if (this.usingWebAudio)
{
this.context = this.game.sound.context;
this.masterGainNode = this.game.sound.masterGain;
if (typeof this.context.createGain === 'undefined')
{
this.gainNode = this.context.createGainNode();
}
else
{
this.gainNode = this.context.createGain();
}
this.gainNode.gain.value = volume * this.game.sound.volume;
this.gainNode.connect(this.masterGainNode);
}
else
{
if (this.game.cache.getSound(key) && this.game.cache.isSoundReady(key))
{
this._sound = this.game.cache.getSoundData(key);
this.totalDuration = 0;
if (this._sound.duration)
{
this.totalDuration = this._sound.duration;
}
}
else
{
this.game.cache.onSoundUnlock.add(this.soundHasUnlocked, this);
}
}
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {Phaser.Signal} onDecoded
*/
this.onDecoded = new Phaser.Signal;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {Phaser.Signal} onPlay
*/
this.onPlay = new Phaser.Signal;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {Phaser.Signal} onPause
*/
this.onPause = new Phaser.Signal;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {Phaser.Signal} onResume
*/
this.onResume = new Phaser.Signal;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {Phaser.Signal} onLoop
*/
this.onLoop = new Phaser.Signal;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {Phaser.Signal} onStop
*/
this.onStop = new Phaser.Signal;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {Phaser.Signal} onMute
*/
this.onMute = new Phaser.Signal;
2013-10-01 12:54:29 +00:00
/**
* Description.
* @property {Phaser.Signal} onMarkerComplete
*/
this.onMarkerComplete = new Phaser.Signal;
};
Phaser.Sound.prototype = {
2013-10-01 12:54:29 +00:00
/**
2013-10-02 19:18:24 +00:00
* Called automatically when this sound is unlocked.
* @method Phaser.Sound#soundHasUnlocked
* @param {string} key - Description.
* @protected
*/
soundHasUnlocked: function (key) {
if (key == this.key)
{
this._sound = this.game.cache.getSoundData(this.key);
this.totalDuration = this._sound.duration;
2013-10-02 19:18:24 +00:00
// console.log('sound has unlocked' + this._sound);
}
},
2013-10-01 12:54:29 +00:00
/**
* Description.
2013-10-02 19:18:24 +00:00
* @method Phaser.Sound#addMarker
2013-10-01 12:54:29 +00:00
* @param {string} name - Description.
* @param {Description} start - Description.
* @param {Description} stop - Description.
* @param {Description} volume - Description.
* @param {Description} loop - Description.
addMarker: function (name, start, stop, volume, loop) {
volume = volume || 1;
if (typeof loop == 'undefined') { loop = false; }
this.markers[name] = {
name: name,
start: start,
stop: stop,
volume: volume,
duration: stop - start,
loop: loop
};
},
*/
2013-10-02 19:18:24 +00:00
/**
* Adds a marker into the current Sound. A marker is represented by a unique key and a start time and duration.
* This allows you to bundle multiple sounds together into a single audio file and use markers to jump between them for playback.
*
* @method Phaser.Sound#addMarker
* @param {string} name - A unique name for this marker, i.e. 'explosion', 'gunshot', etc.
* @param {number} start - The start point of this marker in the audio file, given in seconds. 2.5 = 2500ms, 0.5 = 500ms, etc.
* @param {number} duration - The duration of the marker in seconds. 2.5 = 2500ms, 0.5 = 500ms, etc.
* @param {number} [volume=1] - The volume the sound will play back at, between 0 (silent) and 1 (full volume).
* @param {boolean} [loop=false] - Sets if the sound will loop or not.
*/
addMarker: function (name, start, duration, volume, loop) {
volume = volume || 1;
if (typeof loop == 'undefined') { loop = false; }
this.markers[name] = {
name: name,
start: start,
stop: start + duration,
volume: volume,
duration: duration,
2013-09-30 11:17:07 +00:00
durationMS: duration * 1000,
loop: loop
};
},
2013-10-01 12:54:29 +00:00
/**
2013-10-02 19:18:24 +00:00
* Removes a marker from the sound.
* @method Phaser.Sound#removeMarker
* @param {string} name - The key of the marker to remove.
*/
removeMarker: function (name) {
delete this.markers[name];
},
2013-10-01 12:54:29 +00:00
/**
2013-10-02 19:18:24 +00:00
* Called automatically by Phaser.SoundManager.
* @method Phaser.Sound#update
* @protected
*/
update: function () {
if (this.pendingPlayback && this.game.cache.isSoundReady(this.key))
{
this.pendingPlayback = false;
this.play(this._tempMarker, this._tempPosition, this._tempVolume, this._tempLoop);
}
if (this.isPlaying)
{
this.currentTime = this.game.time.now - this.startTime;
if (this.currentTime >= this.durationMS)
{
//console.log(this.currentMarker, 'has hit duration');
if (this.usingWebAudio)
{
if (this.loop)
{
//console.log('loop1');
// won't work with markers, needs to reset the position
this.onLoop.dispatch(this);
if (this.currentMarker == '')
{
//console.log('loop2');
this.currentTime = 0;
this.startTime = this.game.time.now;
}
else
{
//console.log('loop3');
this.play(this.currentMarker, 0, this.volume, true, true);
}
}
else
{
//console.log('stopping, no loop for marker');
this.stop();
}
}
else
{
if (this.loop)
{
this.onLoop.dispatch(this);
this.play(this.currentMarker, 0, this.volume, true, true);
}
else
{
this.stop();
}
}
}
}
},
/**
* Play this sound, or a marked section of it.
2013-10-02 19:18:24 +00:00
* @method Phaser.Sound#play
* @param {string} [marker=''] - If you want to play a marker then give the key here, otherwise leave blank to play the full sound.
* @param {number} [position=0] - The starting position to play the sound from - this is ignored if you provide a marker.
* @param {number} [volume=1] - Volume of the sound you want to play. If none is given it will use the volume given to the Sound when it was created (which defaults to 1 if none was specified).
2013-10-02 19:18:24 +00:00
* @param {boolean} [loop=false] - Loop when it finished playing?
* @param {boolean} [forceRestart=true] - If the sound is already playing you can set forceRestart to restart it from the beginning.
* @return {Phaser.Sound} This sound instance.
*/
play: function (marker, position, volume, loop, forceRestart) {
marker = marker || '';
position = position || 0;
if (typeof volume === 'undefined') { volume = this._volume; }
if (typeof loop === 'undefined') { loop = false; }
if (typeof forceRestart === 'undefined') { forceRestart = true; }
// console.log(this.name + ' play ' + marker + ' position ' + position + ' volume ' + volume + ' loop ' + loop, 'force', forceRestart);
if (this.isPlaying === true && forceRestart === false && this.override === false)
{
// Use Restart instead
return;
}
if (this.isPlaying && this.override)
{
// console.log('asked to play ' + marker + ' but already playing ' + this.currentMarker);
if (this.usingWebAudio)
{
if (typeof this._sound.stop === 'undefined')
{
this._sound.noteOff(0);
}
else
{
this._sound.stop(0);
}
}
else if (this.usingAudioTag)
{
this._sound.pause();
this._sound.currentTime = 0;
}
}
this.currentMarker = marker;
if (marker !== '')
{
if (this.markers[marker])
{
this.position = this.markers[marker].start;
this.volume = this.markers[marker].volume;
this.loop = this.markers[marker].loop;
this.duration = this.markers[marker].duration;
this.durationMS = this.markers[marker].durationMS;
// console.log('Marker Loaded: ', marker, 'start:', this.position, 'end: ', this.duration, 'loop', this.loop);
this._tempMarker = marker;
this._tempPosition = this.position;
this._tempVolume = this.volume;
this._tempLoop = this.loop;
}
else
{
console.warn("Phaser.Sound.play: audio marker " + marker + " doesn't exist");
return;
}
}
else
{
// console.log('no marker info loaded', marker);
this.position = position;
this.volume = volume;
this.loop = loop;
this.duration = 0;
this.durationMS = 0;
this._tempMarker = marker;
this._tempPosition = position;
this._tempVolume = volume;
this._tempLoop = loop;
}
if (this.usingWebAudio)
{
// Does the sound need decoding?
if (this.game.cache.isSoundDecoded(this.key))
{
// Do we need to do this every time we play? How about just if the buffer is empty?
if (this._buffer == null)
{
this._buffer = this.game.cache.getSoundData(this.key);
}
this._sound = this.context.createBufferSource();
this._sound.buffer = this._buffer;
this._sound.connect(this.gainNode);
this.totalDuration = this._sound.buffer.duration;
if (this.duration === 0)
{
// console.log('duration reset');
this.duration = this.totalDuration;
this.durationMS = this.totalDuration * 1000;
}
if (this.loop && marker == '')
{
this._sound.loop = true;
}
// Useful to cache this somewhere perhaps?
if (typeof this._sound.start === 'undefined')
{
2013-09-30 11:17:07 +00:00
this._sound.noteGrainOn(0, this.position, this.duration);
// this._sound.noteGrainOn(0, this.position, this.duration / 1000);
//this._sound.noteOn(0); // the zero is vitally important, crashes iOS6 without it
}
else
{
2013-09-30 11:17:07 +00:00
// this._sound.start(0, this.position, this.duration / 1000);
this._sound.start(0, this.position, this.duration);
}
this.isPlaying = true;
this.startTime = this.game.time.now;
this.currentTime = 0;
this.stopTime = this.startTime + this.durationMS;
this.onPlay.dispatch(this);
}
else
{
this.pendingPlayback = true;
if (this.game.cache.getSound(this.key) && this.game.cache.getSound(this.key).isDecoding === false)
{
this.game.sound.decode(this.key, this);
}
}
}
else
{
// console.log('Sound play Audio');
if (this.game.cache.getSound(this.key) && this.game.cache.getSound(this.key).locked)
{
// console.log('tried playing locked sound, pending set, reload started');
this.game.cache.reloadSound(this.key);
this.pendingPlayback = true;
}
else
{
// console.log('sound not locked, state?', this._sound.readyState);
if (this._sound && this._sound.readyState == 4)
{
this._sound.play();
// This doesn't become available until you call play(), wonderful ...
this.totalDuration = this._sound.duration;
if (this.duration === 0)
{
this.duration = this.totalDuration;
this.durationMS = this.totalDuration * 1000;
}
// console.log('playing', this._sound);
this._sound.currentTime = this.position;
this._sound.muted = this._muted;
if (this._muted)
{
this._sound.volume = 0;
}
else
{
this._sound.volume = this._volume;
}
this.isPlaying = true;
this.startTime = this.game.time.now;
this.currentTime = 0;
this.stopTime = this.startTime + this.durationMS;
this.onPlay.dispatch(this);
}
else
{
this.pendingPlayback = true;
}
}
}
},
2013-09-19 14:34:48 +00:00
/**
* Restart the sound, or a marked section of it.
2013-10-02 19:18:24 +00:00
* @method Phaser.Sound#restart
2013-10-03 00:21:08 +00:00
* @param {string} [marker=''] - If you want to play a marker then give the key here, otherwise leave blank to play the full sound.
* @param {number} [position=0] - The starting position to play the sound from - this is ignored if you provide a marker.
* @param {number} [volume=1] - Volume of the sound you want to play.
* @param {boolean} [loop=false] - Loop when it finished playing?
2013-09-19 14:34:48 +00:00
*/
restart: function (marker, position, volume, loop) {
marker = marker || '';
position = position || 0;
volume = volume || 1;
if (typeof loop == 'undefined') { loop = false; }
this.play(marker, position, volume, loop, true);
},
2013-09-19 14:34:48 +00:00
/**
* Pauses the sound
2013-10-02 19:18:24 +00:00
* @method Phaser.Sound#pause
2013-09-19 14:34:48 +00:00
*/
pause: function () {
if (this.isPlaying && this._sound)
{
this.stop();
this.isPlaying = false;
this.paused = true;
this.pausedPosition = this.currentTime;
this.pausedTime = this.game.time.now;
this.onPause.dispatch(this);
}
},
2013-09-19 14:34:48 +00:00
/**
* Resumes the sound
2013-10-02 19:18:24 +00:00
* @method Phaser.Sound#resume
2013-09-19 14:34:48 +00:00
*/
resume: function () {
if (this.paused && this._sound)
{
if (this.usingWebAudio)
{
var p = this.position + (this.pausedPosition / 1000);
this._sound = this.context.createBufferSource();
this._sound.buffer = this._buffer;
this._sound.connect(this.gainNode);
if (typeof this._sound.start === 'undefined')
{
this._sound.noteGrainOn(0, p, this.duration);
//this._sound.noteOn(0); // the zero is vitally important, crashes iOS6 without it
}
else
{
this._sound.start(0, p, this.duration);
}
}
else
{
this._sound.play();
}
this.isPlaying = true;
this.paused = false;
this.startTime += (this.game.time.now - this.pausedTime);
this.onResume.dispatch(this);
}
},
/**
* Stop playing this sound.
2013-10-02 19:18:24 +00:00
* @method Phaser.Sound#stop
*/
stop: function () {
if (this.isPlaying && this._sound)
{
if (this.usingWebAudio)
{
if (typeof this._sound.stop === 'undefined')
{
this._sound.noteOff(0);
}
else
{
this._sound.stop(0);
}
}
else if (this.usingAudioTag)
{
this._sound.pause();
this._sound.currentTime = 0;
}
}
this.isPlaying = false;
var prevMarker = this.currentMarker;
this.currentMarker = '';
this.onStop.dispatch(this, prevMarker);
}
};
2013-10-01 12:54:29 +00:00
/**
2013-10-03 00:21:08 +00:00
* @name Phaser.Sound#isDecoding
* @property {boolean} isDecoding - Returns true if the sound file is still decoding.
* @readonly
2013-10-01 12:54:29 +00:00
*/
Object.defineProperty(Phaser.Sound.prototype, "isDecoding", {
get: function () {
return this.game.cache.getSound(this.key).isDecoding;
}
});
2013-10-01 12:54:29 +00:00
/**
2013-10-03 00:21:08 +00:00
* @name Phaser.Sound#isDecoded
* @property {boolean} isDecoded - Returns true if the sound file has decoded.
* @readonly
2013-10-01 12:54:29 +00:00
*/
Object.defineProperty(Phaser.Sound.prototype, "isDecoded", {
get: function () {
return this.game.cache.isSoundDecoded(this.key);
}
});
2013-10-01 12:54:29 +00:00
/**
2013-10-03 00:21:08 +00:00
* @name Phaser.Sound#mute
* @property {boolean} mute - Gets or sets the muted state of this sound.
2013-10-01 12:54:29 +00:00
*/
Object.defineProperty(Phaser.Sound.prototype, "mute", {
2013-09-19 14:34:48 +00:00
get: function () {
return this._muted;
},
2013-10-01 12:54:29 +00:00
set: function (value) {
value = value || null;
if (value)
{
this._muted = true;
if (this.usingWebAudio)
{
this._muteVolume = this.gainNode.gain.value;
this.gainNode.gain.value = 0;
}
else if (this.usingAudioTag && this._sound)
{
this._muteVolume = this._sound.volume;
this._sound.volume = 0;
}
}
else
{
this._muted = false;
if (this.usingWebAudio)
{
this.gainNode.gain.value = this._muteVolume;
}
else if (this.usingAudioTag && this._sound)
{
this._sound.volume = this._muteVolume;
}
}
this.onMute.dispatch(this);
}
});
2013-10-01 12:54:29 +00:00
/**
2013-10-03 00:21:08 +00:00
* @name Phaser.Sound#volume
* @property {number} volume - Gets or sets the volume of this sound, a value between 0 and 1.
* @readonly
2013-10-01 12:54:29 +00:00
*/
Object.defineProperty(Phaser.Sound.prototype, "volume", {
get: function () {
return this._volume;
},
set: function (value) {
if (this.usingWebAudio)
{
this._volume = value;
this.gainNode.gain.value = value;
}
else if (this.usingAudioTag && this._sound)
{
// Causes an Index size error in Firefox if you don't clamp the value
if (value >= 0 && value <= 1)
{
this._volume = value;
this._sound.volume = value;
}
}
}
});