diff --git a/Phaser/Game.ts b/Phaser/Game.ts index c996087f..9ec94962 100644 --- a/Phaser/Game.ts +++ b/Phaser/Game.ts @@ -14,6 +14,7 @@ /// /// /// +/// /// /// /// @@ -298,12 +299,12 @@ module Phaser { this.stage = new Stage(this, parent, width, height); this.world = new World(this, width, height); this.add = new GameObjectFactory(this); - this.sound = new SoundManager(this); this.cache = new Cache(this); this.load = new Loader(this, this.loadComplete); this.time = new Time(this); this.tweens = new TweenManager(this); this.input = new Input(this); + this.sound = new SoundManager(this); this.rnd = new RandomDataGenerator([(Date.now() * Math.random()).toString()]); this.physics = new Physics.Manager(this); @@ -391,6 +392,7 @@ module Phaser { this.tweens.update(); this.input.update(); this.stage.update(); + this.sound.update(); if (this.onPausedCallback !== null) { @@ -407,6 +409,7 @@ module Phaser { this.tweens.update(); this.input.update(); this.stage.update(); + this.sound.update(); this.physics.update(); this.world.update(); diff --git a/Phaser/Stage.ts b/Phaser/Stage.ts index d147a03a..1e8dd718 100644 --- a/Phaser/Stage.ts +++ b/Phaser/Stage.ts @@ -61,6 +61,8 @@ module Phaser { document.addEventListener('visibilitychange', (event) => this.visibilityChange(event), false); document.addEventListener('webkitvisibilitychange', (event) => this.visibilityChange(event), false); + document.addEventListener('pagehide', (event) => this.visibilityChange(event), false); + document.addEventListener('pageshow', (event) => this.visibilityChange(event), false); window.onblur = (event) => this.visibilityChange(event); window.onfocus = (event) => this.visibilityChange(event); @@ -216,7 +218,7 @@ module Phaser { return; } - if (event.type == 'blur' || document['hidden'] == true || document['webkitHidden'] == true) + if (event.type == 'pagehide' || event.type == 'blur' || document['hidden'] == true || document['webkitHidden'] == true) { if (this._game.paused == false) { diff --git a/Phaser/gameobjects/GameObjectFactory.ts b/Phaser/gameobjects/GameObjectFactory.ts index 30b8f781..78e981a3 100644 --- a/Phaser/gameobjects/GameObjectFactory.ts +++ b/Phaser/gameobjects/GameObjectFactory.ts @@ -94,6 +94,10 @@ module Phaser { return this._world.group.add(new Sprite(this._game, x, y, key, frame, bodyType)); } + public audio(key: string, volume?: number = 1, loop?: bool = false) { + return this._game.sound.add(key, volume, loop); + } + /** * Create a new Sprite with the physics automatically created and set to DYNAMIC. The Sprite position offset is set to its center. * diff --git a/Phaser/input/Input.ts b/Phaser/input/Input.ts index 50596489..b427341c 100644 --- a/Phaser/input/Input.ts +++ b/Phaser/input/Input.ts @@ -275,7 +275,7 @@ module Phaser { * @property recordPointerHistory * @type {Boolean} **/ - public recordPointerHistory: bool = true; + public recordPointerHistory: bool = false; /** * The rate in milliseconds at which the Pointer objects should update their tracking history @@ -286,7 +286,7 @@ module Phaser { /** * The total number of entries that can be recorded into the Pointer objects tracking history. - * The the Pointer is tracking one event every 100ms, then a trackLimit of 100 would store the last 10 seconds worth of history. + * If the Pointer is tracking one event every 100ms, then a trackLimit of 100 would store the last 10 seconds worth of history. * @property recordLimit * @type {Number} */ diff --git a/Phaser/input/Touch.ts b/Phaser/input/Touch.ts index 8d46b706..34d2e71c 100644 --- a/Phaser/input/Touch.ts +++ b/Phaser/input/Touch.ts @@ -79,9 +79,7 @@ module Phaser { * @param {Any} event **/ private consumeTouchMove(event) { - event.preventDefault(); - } /** diff --git a/Phaser/loader/Cache.ts b/Phaser/loader/Cache.ts index 7e82516f..c7af99c8 100644 --- a/Phaser/loader/Cache.ts +++ b/Phaser/loader/Cache.ts @@ -122,22 +122,40 @@ module Phaser { * @param url {string} URL of this sound file. * @param data {object} Extra sound data. */ - public addSound(key: string, url: string, data) { + public addSound(key: string, url: string, data, webAudio: bool = true, audioTag: bool = false) { - this._sounds[key] = { url: url, data: data, decoded: false }; + console.log('Cache addSound', key, url, webAudio, audioTag); + + var decoded: bool = false; + + if (audioTag) { + decoded = true; + } + + this._sounds[key] = { url: url, data: data, isDecoding: false, decoded: decoded, webAudio: webAudio, audioTag: audioTag }; + + } + + public updateSound(key: string, property: string, value) { + + if (this._sounds[key]) + { + this._sounds[key][property] = value; + } } /** * Add a new decoded sound. * @param key {string} Asset key for the sound. - * @param url {string} URL of this sound file. * @param data {object} Extra sound data. */ public decodedSound(key: string, data) { + console.log('decoded sound', key); this._sounds[key].data = data; this._sounds[key].decoded = true; + this._sounds[key].isDecoding = false; } @@ -201,12 +219,28 @@ module Phaser { } + /** + * Get sound by key. + * @param key Asset key of the sound you want. + * @return {object} The sound you want. + */ + public getSound(key: string) { + + if (this._sounds[key]) + { + return this._sounds[key]; + } + + return null; + + } + /** * Get sound data by key. * @param key Asset key of the sound you want. * @return {object} The sound data you want. */ - public getSound(key: string) { + public getSoundData(key: string) { if (this._sounds[key]) { diff --git a/Phaser/loader/Loader.ts b/Phaser/loader/Loader.ts index 0218c5a8..d37c5cdb 100644 --- a/Phaser/loader/Loader.ts +++ b/Phaser/loader/Loader.ts @@ -218,14 +218,15 @@ module Phaser { /** * Add a new audio file loading request. * @param key {string} Unique asset key of the audio file. - * @param url {string} URL of audio file. + * @param urls {Array} An array containing the URLs of the audio files, i.e.: [ 'jump.mp3', 'jump.ogg', 'jump.m4a' ] + * @param autoDecode {boolean} When using Web Audio the audio files can either be decoded at load time or run-time. They can't be played until they are decoded, but this let's you control when that happens. Decoding is a non-blocking async process. */ - public audio(key: string, url: string) { + public audio(key: string, urls: string[], autoDecode: bool = true) { if (this.checkKeyExists(key) === false) { this._queueSize++; - this._fileList[key] = { type: 'audio', key: key, url: url, data: null, buffer: null, error: false, loaded: false }; + this._fileList[key] = { type: 'audio', key: key, url: urls, data: null, buffer: null, error: false, loaded: false, autoDecode: autoDecode }; this._keys.push(key); } @@ -327,11 +328,32 @@ module Phaser { break; case 'audio': - this._xhr.open("GET", file.url, true); - this._xhr.responseType = "arraybuffer"; - this._xhr.onload = () => this.fileComplete(file.key); - this._xhr.onerror = () => this.fileError(file.key); - this._xhr.send(); + + file.url = this.getAudioURL(file.url); + + if (file.url !== null) + { + // WebAudio or Audio Tag? + if (this._game.sound.usingWebAudio) + { + this._xhr.open("GET", file.url, true); + this._xhr.responseType = "arraybuffer"; + this._xhr.onload = () => this.fileComplete(file.key); + this._xhr.onerror = () => this.fileError(file.key); + this._xhr.send(); + } + else if (this._game.sound.usingAudioTag) + { + file.data = new Audio(); + file.data.name = file.key; + file.data.onerror = () => this.fileError(file.key); + file.data.preload = 'auto'; + file.data.src = file.url; + file.data.addEventListener('canplaythrough', () => this.fileComplete(file.key), false); + file.data.load(); + } + } + break; case 'text': @@ -345,6 +367,27 @@ module Phaser { } + private getAudioURL(urls): string { + + var extension: string; + + for (var i = 0; i < urls.length; i++) + { + extension = urls[i].toLowerCase(); + extension = extension.substr((Math.max(0, extension.lastIndexOf(".")) || Infinity) + 1); + + if (this._game.device.canPlayAudio(extension)) + { + console.log('getAudioURL', urls[i]); + return urls[i]; + } + + } + + return null; + + } + /** * Error occured when load a file. * @param key {string} Key of the error loading file. @@ -406,8 +449,32 @@ module Phaser { break; case 'audio': - file.data = this._xhr.response; - this._game.cache.addSound(file.key, file.url, file.data); + + if (this._game.sound.usingWebAudio) + { + file.data = this._xhr.response; + + this._game.cache.addSound(file.key, file.url, file.data, true, false); + + if (file.autoDecode) + { + this._game.cache.updateSound(key, 'isDecoding', true); + + var that = this; + var key = file.key; + + this._game.sound.context.decodeAudioData(file.data, function (buffer) { + if (buffer) + { + that._game.cache.decodedSound(key, buffer); + } + }); + } + } + else + { + this._game.cache.addSound(file.key, file.url, file.data, false, true); + } break; case 'text': diff --git a/Phaser/sound/Sound.ts b/Phaser/sound/Sound.ts index 6f169aec..0fe2a212 100644 --- a/Phaser/sound/Sound.ts +++ b/Phaser/sound/Sound.ts @@ -13,56 +13,76 @@ module Phaser { /** * Sound constructor - * @param context {object} The AudioContext instance. - * @param gainNode {object} Gain node instance. - * @param data {object} Sound data. * @param [volume] {number} volume of this sound when playing. * @param [loop] {boolean} loop this sound when playing? (Default to false) */ - constructor(context, gainNode, data, volume?: number = 1, loop?: bool = false) { + constructor(game: Phaser.Game, key: string, volume?: number = 1, loop?: bool = false) { - this._context = context; - this._gainNode = gainNode; - this._buffer = data; - this._volume = volume; - this.loop = loop; + this.game = game; - // Local volume control - if (this._context !== null) + this.usingWebAudio = this.game.sound.usingWebAudio; + this.usingAudioTag = this.game.sound.usingAudioTag; + this.key = key; + + if (this.usingWebAudio) { - this._localGainNode = this._context.createGainNode(); - this._localGainNode.connect(this._gainNode); - this._localGainNode.gain.value = this._volume; - } + this.context = this.game.sound.context; + this.masterGainNode = this.game.sound.masterGain; - if (this._buffer === null) - { - this.isDecoding = true; + 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 { - this.play(); + this._sound = this.game.cache.getSoundData(key); + this.totalDuration = this._sound.duration; } + this._volume = volume; + this.loop = loop; + this.markers = {}; + + this.onDecoded = new Phaser.Signal; + this.onPlay = new Phaser.Signal; + this.onPause = new Phaser.Signal; + this.onResume = new Phaser.Signal; + this.onLoop = new Phaser.Signal; + this.onStop = new Phaser.Signal; + this.onMute = new Phaser.Signal; + } /** - * Local private reference to AudioContext. + * Local reference to the current Phaser.Game. */ - private _context; + public game: Game; + + /** + * Reference to AudioContext instance. + */ + public context = null; /** * Reference to gain node of SoundManager. */ - private _gainNode; + public masterGainNode; /** * GainNode of this sound. */ - private _localGainNode; + public gainNode; /** - * Decoded data buffer. + * Decoded data buffer / Audio tag. */ private _buffer; @@ -78,43 +98,247 @@ module Phaser { private _muteVolume: number; private _muted: bool = false; + private _tempPosition: number; + private _tempVolume: number; + private _tempLoop: bool; + private _tempMarker: string; + public usingWebAudio: bool = false; + public usingAudioTag: bool = false; + + public name: string = ''; + + autoplay: bool = false; + totalDuration: number = 0; + startTime: number = 0; + currentTime: number = 0; + duration: number = 0; + stopTime: number = 0; + position: number; + paused: bool = false; loop: bool = false; - duration: number; isPlaying: bool = false; - isDecoding: bool = false; + key: string; + markers; + currentMarker: string = ''; - public setDecodedBuffer(data) { + // events + public onDecoded: Phaser.Signal; + public onPlay: Phaser.Signal; + public onPause: Phaser.Signal; + public onResume: Phaser.Signal; + public onLoop: Phaser.Signal; + public onStop: Phaser.Signal; + public onMute: Phaser.Signal; - this._buffer = data; - this.isDecoding = false; - //this.play(); + public pendingPlayback: bool = false; + + public get isDecoding(): bool { + return this.game.cache.getSound(this.key).isDecoding; + } + + public get isDecoded(): bool { + return this.game.cache.isSoundDecoded(this.key); + } + + public addMarker(name: string, start: number, stop: number, volume: number = 1, loop: bool = false) { + this.markers[name] = { name: name, start: start, stop: stop, volume: volume, duration: stop - start, loop: loop }; + } + + public removeMarker(name: string) { + delete this.markers[name]; + } + + public update() { + + if (this.pendingPlayback && this.game.cache.isSoundDecoded(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.duration) + { + if (this.usingWebAudio) + { + if (this.loop) + { + this.onLoop.dispatch(this); + this.currentTime = 0; + this.startTime = this.game.time.now; + } + else + { + 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. + * Play this sound, or a marked section of it. + * @param marker {string} Assets key of the sound you want to play. + * @param [volume] {number} volume of the sound you want to play. + * @param [loop] {boolean} loop when it finished playing? (Default to false) + * @return {Sound} The playing sound object. */ - public play() { + public play(marker: string = '', position?: number = 0, volume?: number = 1, loop?: bool = false, forceRestart: bool = false) { - if (this._buffer === null || this.isDecoding === true) + if (this.isPlaying == true && forceRestart == false) { + // Use Restart instead return; } - this._sound = this._context.createBufferSource(); - this._sound.buffer = this._buffer; - this._sound.connect(this._localGainNode); + this.currentMarker = marker; - if (this.loop) + if (marker !== '' && this.markers[marker]) { - this._sound.loop = true; + this.loop = this.markers[marker].loop; + this.volume = this.markers[marker].volume; + this.position = this.markers[marker].start; + this.duration = this.markers[marker].duration * 1000; + } + else + { + this.loop = loop; + this.volume = volume; + this.position = position; + this.duration = 0; } - this._sound.noteOn(0); // the zero is vitally important, crashes iOS6 without it + 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? + 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; - this.duration = this._sound.buffer.duration; - this.isPlaying = true; + if (this.duration == 0) + { + this.duration = this.totalDuration * 1000; + } + + if (this.loop) + { + this._sound.loop = true; + } + + // Useful to cache this somewhere perhaps? + if (typeof this._sound.start === 'undefined') + { + this._sound.noteGrainOn(0, this.position, this.duration / 1000); + //this._sound.noteOn(0); // the zero is vitally important, crashes iOS6 without it + } + else + { + this._sound.start(0, this.position, this.duration / 1000); + } + + this.isPlaying = true; + this.startTime = this.game.time.now; + this.currentTime = 0; + this.stopTime = this.startTime + this.duration; + this.onPlay.dispatch(this); + } + else + { + this._tempVolume = volume; + this._tempLoop = loop; + this._tempPosition = position; + this._tempMarker = marker; + this.pendingPlayback = true; + + if (this.game.cache.getSound(this.key).isDecoding == false) + { + this.game.sound.decode(this.key, this); + } + } + } + else + { + if (this._sound.readyState == 4) + { + this._sound.currentTime = this.position; + this._sound.muted = this._muted; + this._sound.volume = this._volume; + this._sound.play(); + + this.isPlaying = true; + this.startTime = this.game.time.now; + this.currentTime = 0; + this.stopTime = this.startTime + this.duration; + this.onPlay.dispatch(this); + } + } + + } + + public restart(marker: string = '', position?: number = 0, volume?: number = 1, loop?: bool = false) { + this.play(marker, position, volume, loop, true); + } + + public pause() { + + if (this.isPlaying && this._sound) + { + this.stop(); + this.isPlaying = false; + this.paused = true; + this.onPause.dispatch(this); + } + + } + + public resume() { + + if (this.isPlaying == false && this._sound) + { + if (this.usingWebAudio) + { + if (typeof this._sound.start === 'undefined') + { + this._sound.noteGrainOn(0, this.position, this.duration); + //this._sound.noteOn(0); // the zero is vitally important, crashes iOS6 without it + } + else + { + this._sound.start(0, this.position, this.duration); + } + } + else + { + this._sound.play(); + } + + this.isPlaying = true; + this.paused = false; + this.onResume.dispatch(this); + } } @@ -123,11 +347,29 @@ module Phaser { */ public stop() { - if (this.isPlaying === true) + 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; + this.currentMarker = ''; + this.onStop.dispatch(this); - this._sound.noteOff(0); } } @@ -135,37 +377,56 @@ module Phaser { /** * Mute sounds. */ - public get mute():bool { + public get mute(): bool { return this._muted; } public set mute(value: bool) { - if (value && this._muted == false) + if (value) { - this._muteVolume = this._localGainNode.gain.value; - this._localGainNode.gain.value = 0; this._muted = true; + + if (this.usingWebAudio) + { + this._muteVolume = this.gainNode.gain.value; + this.gainNode.gain.value = 0; + } + else if (this.usingAudioTag) + { + this._muteVolume = this._sound.volume; + this._sound.volume = 0; + } } else { - this._localGainNode.gain.value = this._muteVolume; this._muted = false; + + if (this.usingWebAudio) + { + this.gainNode.gain.value = this._muteVolume; + } + else if (this.usingAudioTag) + { + this._sound.volume = this._muteVolume; + } } + this.onMute.dispatch(this); + } public set volume(value: number) { this._volume = value; - if (this._muted) + if (this.usingWebAudio) { - this._muteVolume = this._volume; + this.gainNode.gain.value = value; } - else + else if (this.usingAudioTag) { - this._localGainNode.gain.value = this._volume; + this._sound.volume = value; } } @@ -174,6 +435,30 @@ module Phaser { return this._volume; } + /** + * Renders the Pointer.circle object onto the stage in green if down or red if up. + * @method renderDebug + */ + public renderDebug(x: number, y: number) { + + this.game.stage.context.fillStyle = 'rgb(255,255,255)'; + this.game.stage.context.font = '16px Courier'; + this.game.stage.context.fillText('Sound: ' + this.key + ' Locked: ' + this.game.sound.touchLocked + ' Pending Playback: ' + this.pendingPlayback, x, y); + this.game.stage.context.fillText('Decoded: ' + this.isDecoded + ' Decoding: ' + this.isDecoding, x, y + 20); + this.game.stage.context.fillText('Total Duration: ' + this.totalDuration + ' Playing: ' + this.isPlaying, x, y + 40); + this.game.stage.context.fillText('Time: ' + this.currentTime, x, y + 60); + this.game.stage.context.fillText('Volume: ' + this.volume + ' Muted: ' + this.mute, x, y + 80); + this.game.stage.context.fillText('WebAudio: ' + this.usingWebAudio + ' Audio: ' + this.usingAudioTag, x, y + 100); + + if (this.currentMarker !== '') + { + this.game.stage.context.fillText('Marker: ' + this.currentMarker + ' Duration: ' + this.duration, x, y + 120); + this.game.stage.context.fillText('Start: ' + this.markers[this.currentMarker].start + ' Stop: ' + this.markers[this.currentMarker].stop, x, y + 140); + this.game.stage.context.fillText('Position: ' + this.position, x, y + 160); + } + + } + } } \ No newline at end of file diff --git a/Phaser/sound/SoundManager.ts b/Phaser/sound/SoundManager.ts index 7ced4fbd..364a3be3 100644 --- a/Phaser/sound/SoundManager.ts +++ b/Phaser/sound/SoundManager.ts @@ -17,48 +17,106 @@ module Phaser { */ constructor(game: Game) { - this._game = game; + this.game = game; - if (window['PhaserGlobal'] && window['PhaserGlobal'].disableAudio == true) + this._volume = 1; + this._muted = false; + this._sounds = []; + + if (this.game.device.iOS && this.game.device.webAudio == false) { - return; + this.channels = 1; } - if (game.device.webaudio == true) + if (window['PhaserGlobal']) { - if (!!window['AudioContext']) + // Check to see if all audio playback is disabled (i.e. handled by a 3rd party class) + if (window['PhaserGlobal'].disableAudio == true) { - this._context = new window['AudioContext'](); - } - else if (!!window['webkitAudioContext']) - { - this._context = new window['webkitAudioContext'](); + this.usingWebAudio = false; + this.noAudio = true; + return; } - if (this._context !== null) + // Check if the Web Audio API is disabled (for testing Audio Tag playback during development) + if (window['PhaserGlobal'].disableWebAudio == true) { - this._gainNode = this._context.createGainNode(); - this._gainNode.connect(this._context.destination); - this._volume = 1; + this.usingWebAudio = false; + this.usingAudioTag = true; + this.noAudio = false; + return; } } + if (this.game.device.iOS && this.game.device.webAudio) + { + this.game.input.touch.callbackContext = this; + this.game.input.touch.touchStartCallback = this.unlock; + this.touchLocked = true; + } + else + { + // What about iOS5? + this.touchLocked = false; + } + + this.usingWebAudio = true; + this.noAudio = false; + + if (!!window['AudioContext']) + { + this.context = new window['AudioContext'](); + } + else if (!!window['webkitAudioContext']) + { + this.context = new window['webkitAudioContext'](); + } + else if (!!window['Audio']) + { + this.usingWebAudio = false; + this.usingAudioTag = true; + } + else + { + this.usingWebAudio = false; + this.noAudio = true; + } + + if (this.context !== null) + { + if (typeof this.context.createGain === 'undefined') + { + this.masterGain = this.context.createGainNode(); + } + else + { + this.masterGain = this.context.createGain(); + } + + this.masterGain.gain.value = 1; + this.masterGain.connect(this.context.destination); + } + } + public usingWebAudio: bool = false; + public usingAudioTag: bool = false; + public noAudio: bool = false; + /** - * Local private reference to game. + * Local reference to the current Phaser.Game. */ - private _game: Game; + public game: Game; /** * Reference to AudioContext instance. */ - private _context = null; + public context = null; /** - * Gain node created from audio context. + * The Master Gain node through which all sounds */ - private _gainNode; + public masterGain; /** * Volume of sounds. @@ -66,11 +124,37 @@ module Phaser { */ private _volume: number; + private _sounds: Phaser.Sound[]; + private _muteVolume: number; private _muted: bool = false; + public channels: number; + + public touchLocked: bool = false; + + private _unlockSource = null; + + public unlock() { + + if (this.touchLocked == false || this.game.device.iOS == false) + { + return; + } + + // Create Audio tag? + + // Create empty buffer and play it + var buffer = this.context.createBuffer(1, 1, 22050); + this._unlockSource = this.context.createBufferSource(); + this._unlockSource.buffer = buffer; + this._unlockSource.connect(this.context.destination); + this._unlockSource.noteOn(0); + + } + /** - * Mute sounds. + * A global audio mute toggle. */ public get mute():bool { return this._muted; @@ -78,104 +162,155 @@ module Phaser { public set mute(value: bool) { - if (value && this._muted == false) + if (value) { - this._muteVolume = this._gainNode.gain.value; - this._gainNode.gain.value = 0; + if (this._muted) + { + return; + } + this._muted = true; + + if (this.usingWebAudio) + { + this._muteVolume = this.masterGain.gain.value; + this.masterGain.gain.value = 0; + } + + // Loop through sounds + for (var i = 0; i < this._sounds.length; i++) + { + if (this._sounds[i].usingAudioTag) + { + this._sounds[i].mute = true; + } + } } else { - this._gainNode.gain.value = this._muteVolume; + if (this._muted == false) + { + return; + } + this._muted = false; + + if (this.usingWebAudio) + { + this.masterGain.gain.value = this._muteVolume; + } + + // Loop through sounds + for (var i = 0; i < this._sounds.length; i++) + { + if (this._sounds[i].usingAudioTag) + { + this._sounds[i].mute = false; + } + } } } + /** + * The global audio volume. A value between 0 (silence) and 1 (full volume) + */ public set volume(value: number) { + value = this.game.math.clamp(value, 1, 0); + this._volume = value; - if (this._muted) + if (this.usingWebAudio) { - this._muteVolume = this._volume; + this.masterGain.gain.value = value; } - else + + // Loop through the sound cache and change the volume of all html audio tags + for (var i = 0; i < this._sounds.length; i++) { - this._gainNode.gain.value = this._volume; + if (this._sounds[i].usingAudioTag) + { + this._sounds[i].volume = this._sounds[i].volume * value; + } } } public get volume(): number { - return this._volume; + + if (this.usingWebAudio) + { + return this.masterGain.gain.value; + } + else + { + return this._volume; + } + } /** * Decode a sound with its assets key. * @param key {string} Assets key of the sound to be decoded. - * @param callback {function} This will be invoked when finished decoding. * @param [sound] {Sound} its bufer will be set to decoded data. */ - public decode(key: string, callback = null, sound?: Sound = null) { + public decode(key: string, sound?: Sound = null) { - var soundData = this._game.cache.getSound(key); + var soundData = this.game.cache.getSoundData(key); if (soundData) { - if (this._game.cache.isSoundDecoded(key) === false) + if (this.game.cache.isSoundDecoded(key) === false) { + this.game.cache.updateSound(key, 'isDecoding', true); + var that = this; - this._context.decodeAudioData(soundData, function (buffer) { - that._game.cache.decodedSound(key, buffer); + this.context.decodeAudioData(soundData, function (buffer) { + + that.game.cache.decodedSound(key, buffer); if (sound) { - sound.setDecodedBuffer(buffer); + that.onSoundDecode.dispatch(sound); } - - callback(); }); } } } - /** - * Play a sound with its assets key. - * @param key {string} Assets key of the sound you want to play. - * @param [volume] {number} volume of the sound you want to play. - * @param [loop] {boolean} loop when it finished playing? (Default to false) - * @return {Sound} The playing sound object. - */ - public play(key: string, volume?: number = 1, loop?: bool = false): Sound { + public onSoundDecode: Phaser.Signal = new Phaser.Signal; - if (this._context === null) + public update() { + + if (this.touchLocked && this._unlockSource !== null) { - return; - } - - var soundData = this._game.cache.getSound(key); - - if (soundData) - { - // Does the sound need decoding? - if (this._game.cache.isSoundDecoded(key) === true) + if ((this._unlockSource.playbackState === this._unlockSource.PLAYING_STATE || this._unlockSource.playbackState === this._unlockSource.FINISHED_STATE)) { - return new Sound(this._context, this._gainNode, soundData, volume, loop); - } - else - { - var tempSound: Sound = new Sound(this._context, this._gainNode, null, volume, loop); - - // this is an async process, so we can return the Sound object anyway, it just won't be playing yet - this.decode(key, () => tempSound.play(), tempSound); - - return tempSound; + this.touchLocked = false; + this._unlockSource = null; + this.game.input.touch.callbackContext = null; + this.game.input.touch.touchStartCallback = null; } } + for (var i = 0; i < this._sounds.length; i++) + { + this._sounds[i].update(); + } + + } + + public add(key: string, volume?: number = 1, loop?: bool = false): Sound { + + var sound: Phaser.Sound = new Sound(this.game, key, volume, loop); + + this._sounds.push(sound); + + return sound; + } } diff --git a/Phaser/system/Device.ts b/Phaser/system/Device.ts index 2132bea1..cb6d71b5 100644 --- a/Phaser/system/Device.ts +++ b/Phaser/system/Device.ts @@ -191,41 +191,53 @@ module Phaser { // Audio /** - * Is audioData available? + * Are Audio tags available? * @type {boolean} */ public audioData: bool = false; /** - * Is webaudio available? + * Is the WebAudio API available? * @type {boolean} */ - public webaudio: bool = false; + public webAudio: bool = false; /** - * Is ogg available? + * Can this device play ogg files? * @type {boolean} */ public ogg: bool = false; /** - * Is mp3 available? + * Can this device play opus files? + * @type {boolean} + */ + public opus: bool = false; + + /** + * Can this device play mp3 files? * @type {boolean} */ public mp3: bool = false; /** - * Is wav available? + * Can this device play wav files? * @type {boolean} */ public wav: bool = false; /** - * Is m4a available? + * Can this device play m4a files? * @type {boolean} */ public m4a: bool = false; + /** + * Can this device play webm files? + * @type {boolean} + */ + public webm: bool = false; + // Device /** @@ -355,7 +367,7 @@ module Phaser { this.mobileSafari = true; } else if (/MSIE (\d+\.\d+);/.test(ua)) - { + { this.ie = true; this.ieVersion = parseInt(RegExp.$1); } @@ -380,6 +392,33 @@ module Phaser { } + public canPlayAudio(type: string): bool { + + if (type == 'mp3' && this.mp3) + { + return true; + } + else if (type == 'ogg' && (this.ogg || this.opus)) + { + return true; + } + else if (type == 'm4a' && this.m4a) + { + return true; + } + else if (type == 'wav' && this.wav) + { + return true; + } + else if (type == 'webm' && this.webm) + { + return true; + } + + return false; + + } + /** * Check audio support. * @private @@ -387,7 +426,7 @@ module Phaser { private _checkAudio() { this.audioData = !!(window['Audio']); - this.webaudio = !!(window['webkitAudioContext'] || window['AudioContext']); + this.webAudio = !!(window['webkitAudioContext'] || window['AudioContext']); var audioElement: HTMLAudioElement = document.createElement('audio'); var result = false; @@ -401,6 +440,11 @@ module Phaser { this.ogg = true; } + if (audioElement.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, '')) + { + this.opus = true; + } + if (audioElement.canPlayType('audio/mpeg;').replace(/^no$/, '')) { this.mp3 = true; @@ -418,6 +462,11 @@ module Phaser { { this.m4a = true; } + + if (audioElement.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')) + { + this.webm = true; + } } } catch (e) { } diff --git a/README.md b/README.md index efd69115..705e77a8 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,11 @@ TODO: * When you toggle visible of a button that is over, it doesn't disable that 'over' state (should it? probably yes) * Stage.opaqueBackground = 'rgb()' or null, alpha, blendMode, filters, mask, rotation+XYZ,scaleXYZ,visible +* Need a way for Input event to redirect to audio to unlock playback +* AudioSprite object? +* How to get web audio to playback from an offset +* Stage lost to mute + V1.0.0 @@ -148,6 +153,12 @@ V1.0.0 * Fixed bug where the hand cursor would be reset on input up, even if the mouse was still over the sprite. * Fixed bug where pressing down then moving out of the sprite area wouldn't properly reset the input state next time you moved over the sprite. * Added the Sprite.tween property, really useful to avoid creating new tween vars in your local scope if you don't need them. +* Added support for pagehide and pageshow events to Stage, hooked in to the pause/resume game methods. +* Extended Device audio checks to include opus and webm. +* Updated Loader and Cache so they now support loading of Audio() tags as well as Web Audio. +* Set Input.recordPointerHistory to false, you now need to enable the pointer tracking if you wish to use it. +* SoundManager will now automatically handle iOS touch unlocking. + V0.9.6 diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index f57ac0db..bd659391 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -56,6 +56,14 @@ + + + + audio sprites 1.ts + + + play sound 1.ts + basic button 2.ts diff --git a/Tests/assets/mp3/oedipus_wizball_highscore.ogg b/Tests/assets/mp3/oedipus_wizball_highscore.ogg new file mode 100644 index 00000000..c9f7430f Binary files /dev/null and b/Tests/assets/mp3/oedipus_wizball_highscore.ogg differ diff --git a/Tests/assets/mp3/oedipus_wizball_highscore.xmp b/Tests/assets/mp3/oedipus_wizball_highscore.xmp new file mode 100644 index 00000000..cc00ef74 --- /dev/null +++ b/Tests/assets/mp3/oedipus_wizball_highscore.xmp @@ -0,0 +1,84 @@ + + + + + + + + CuePoint Markers + Cue + f44100 + + + Subclip Markers + InOut + f44100 + + + + + + 2013-07-16T02:39:59+01:00 + 2013-07-16T02:39:59+01:00 + + + xmp.iid:CF0377343CEDE211B46AA47009D9EF1D + xmp.did:CF0377343CEDE211B46AA47009D9EF1D + xmp.did:CE0377343CEDE211B46AA47009D9EF1D + + + + saved + xmp.iid:CE0377343CEDE211B46AA47009D9EF1D + 2013-07-16T02:39:59+01:00 + Adobe Audition CS6 (Windows) + /metadata + + + saved + xmp.iid:CF0377343CEDE211B46AA47009D9EF1D + 2013-07-16T02:39:59+01:00 + Adobe Audition CS6 (Windows) + / + + + + + xmp.iid:CE0377343CEDE211B46AA47009D9EF1D + xmp.did:CE0377343CEDE211B46AA47009D9EF1D + xmp.did:CE0377343CEDE211B46AA47009D9EF1D + + + + audio/ogg; codec="vorbis" + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/audio/audio sprites 1.js b/Tests/audio/audio sprites 1.js new file mode 100644 index 00000000..dd52c2ee --- /dev/null +++ b/Tests/audio/audio sprites 1.js @@ -0,0 +1,44 @@ +/// +/// +/// +//var PhaserGlobal = { disableWebAudio: true }; +(function () { + var game = new Phaser.Game(this, 'game', 800, 600, init, create, null, render); + function init() { + game.load.audio('rabbit', [ + 'assets/mp3/peter_rabbit.m4a', + 'assets/mp3/peter_rabbit.mp3', + 'assets/mp3/peter_rabbit.ogg' + ]); + game.load.spritesheet('button', 'assets/buttons/button_sprite_sheet.png', 193, 71); + game.load.start(); + } + audioSprite: +Phaser.Sound + button: +Phaser.Button + pause: +Phaser.Button + function create() { + this.audioSprite = game.add.audio('rabbit'); + this.audioSprite.addMarker('title', 3.00, 5.00, 1, true); + this.audioSprite.addMarker('help', 6.00, 12.00); + this.audioSprite.addMarker('intro', 14.00, 19.00); + this.audioSprite.addMarker('peter', 20.00, 21.50); + this.button = game.add.button(game.stage.centerX, 400, 'button', playMusic, this, 2, 1, 0); + //this.pause = game.add.button(200, 200, 'button', togglePause, this, 2, 1, 0); + } + function playMusic() { + this.audioSprite.play('help'); + } + function render() { + this.audioSprite.renderDebug(32, 32); + } + function togglePause() { + if(this.music.paused) { + this.music.resume(); + } else { + this.music.pause(); + } + } +})(); diff --git a/Tests/audio/audio sprites 1.ts b/Tests/audio/audio sprites 1.ts new file mode 100644 index 00000000..e24f22da --- /dev/null +++ b/Tests/audio/audio sprites 1.ts @@ -0,0 +1,58 @@ +/// +/// +/// + +//var PhaserGlobal = { disableWebAudio: true }; + +(function () { + + var game = new Phaser.Game(this, 'game', 800, 600, init, create, null, render); + + function init() { + + game.load.audio('rabbit', ['assets/mp3/peter_rabbit.m4a', 'assets/mp3/peter_rabbit.mp3', 'assets/mp3/peter_rabbit.ogg']); + game.load.spritesheet('button', 'assets/buttons/button_sprite_sheet.png', 193, 71); + game.load.start(); + + } + + audioSprite: Phaser.Sound; + button: Phaser.Button; + pause: Phaser.Button; + + function create() { + + this.audioSprite = game.add.audio('rabbit'); + this.audioSprite.addMarker('title', 3.00, 5.00, 1, true); + this.audioSprite.addMarker('help', 6.00, 12.00); + this.audioSprite.addMarker('intro', 14.00, 19.00); + this.audioSprite.addMarker('peter', 20.00, 21.50); + + this.button = game.add.button(game.stage.centerX, 400, 'button', playMusic, this, 2, 1, 0); + //this.pause = game.add.button(200, 200, 'button', togglePause, this, 2, 1, 0); + + } + + function playMusic() { + this.audioSprite.play('help'); + } + + function render() { + this.audioSprite.renderDebug(32, 32); + } + + function togglePause() { + + if (this.music.paused) + { + this.music.resume(); + } + else + { + this.music.pause(); + } + + } + + +})(); diff --git a/Tests/audio/play sound 1.js b/Tests/audio/play sound 1.js new file mode 100644 index 00000000..9e2c8499 --- /dev/null +++ b/Tests/audio/play sound 1.js @@ -0,0 +1,56 @@ +/// +/// +/// +//var PhaserGlobal = { disableWebAudio: true }; +(function () { + var game = new Phaser.Game(this, 'game', 800, 600, init, create, null, render); + function init() { + //game.load.audio('wizball', ['assets/mp3/oedipus_wizball_highscore.ogg', 'assets/mp3/oedipus_wizball_highscore.mp3']); + game.load.audio('boden', [ + 'assets/mp3/bodenstaendig_2000_in_rock_4bit.mp3' + ]); + //game.load.audio('boden', ['assets/mp3/puzzleover.mp3']); + game.load.spritesheet('button', 'assets/buttons/button_sprite_sheet.png', 193, 71); + game.load.start(); + } + button: +Phaser.Button + music: +Phaser.Sound + volumeUp: +Phaser.Button + volumeDown: +Phaser.Button + pause: +Phaser.Button + function create() { + this.music = game.add.audio('boden'); + this.button = game.add.button(game.stage.centerX, 400, 'button', playMusic, this, 2, 1, 0); + this.volumeUp = game.add.button(0, 0, 'button', volUp, this, 2, 1, 0); + this.volumeDown = game.add.button(700, 0, 'button', volDown, this, 2, 1, 0); + this.pause = game.add.button(200, 200, 'button', togglePause, this, 2, 1, 0); + } + function render() { + this.music.renderDebug(0, 300); + } + function togglePause() { + if(this.music.paused) { + this.music.resume(); + } else { + this.music.pause(); + } + } + function volUp() { + //game.sound.volume += 0.1; + this.music.volume += 0.1; + console.log('vol up', game.sound.volume); + } + function volDown() { + //game.sound.volume -= 0.1; + this.music.volume -= 0.1; + console.log('vol down', game.sound.volume); + } + function playMusic() { + this.music.play(); + } +})(); diff --git a/Tests/audio/play sound 1.ts b/Tests/audio/play sound 1.ts new file mode 100644 index 00000000..b77285ee --- /dev/null +++ b/Tests/audio/play sound 1.ts @@ -0,0 +1,73 @@ +/// +/// +/// + +//var PhaserGlobal = { disableWebAudio: true }; + +(function () { + + var game = new Phaser.Game(this, 'game', 800, 600, init, create, null, render); + + function init() { + + //game.load.audio('wizball', ['assets/mp3/oedipus_wizball_highscore.ogg', 'assets/mp3/oedipus_wizball_highscore.mp3']); + game.load.audio('boden', ['assets/mp3/bodenstaendig_2000_in_rock_4bit.mp3']); + //game.load.audio('boden', ['assets/mp3/puzzleover.mp3']); + game.load.spritesheet('button', 'assets/buttons/button_sprite_sheet.png', 193, 71); + game.load.start(); + + } + + button: Phaser.Button; + music: Phaser.Sound; + volumeUp: Phaser.Button; + volumeDown: Phaser.Button; + pause: Phaser.Button; + + function create() { + + this.music = game.add.audio('boden'); + + this.button = game.add.button(game.stage.centerX, 400, 'button', playMusic, this, 2, 1, 0); + this.volumeUp = game.add.button(0, 0, 'button', volUp, this, 2, 1, 0); + this.volumeDown = game.add.button(700, 0, 'button', volDown, this, 2, 1, 0); + this.pause = game.add.button(200, 200, 'button', togglePause, this, 2, 1, 0); + + } + + function render() { + + this.music.renderDebug(0, 300); + + } + + function togglePause() { + + if (this.music.paused) + { + this.music.resume(); + } + else + { + this.music.pause(); + } + + } + + function volUp() { + //game.sound.volume += 0.1; + this.music.volume += 0.1; + console.log('vol up', game.sound.volume); + } + + function volDown() { + //game.sound.volume -= 0.1; + this.music.volume -= 0.1; + console.log('vol down', game.sound.volume); + } + + function playMusic() { + this.music.play(); + } + +})(); diff --git a/Tests/phaser.js b/Tests/phaser.js index d4732c21..c616e6f0 100644 --- a/Tests/phaser.js +++ b/Tests/phaser.js @@ -7243,7 +7243,7 @@ var Phaser; this.cameraView = new Phaser.Rectangle(x, y, this.width, this.height); this.transform.setCache(); this.outOfBounds = false; - this.outOfBoundsAction = Phaser.Types.OUT_OF_BOUNDS_KILL; + this.outOfBoundsAction = Phaser.Types.OUT_OF_BOUNDS_PERSIST; // Handy proxies this.scale = this.transform.scale; this.alpha = this.texture.alpha; @@ -9134,19 +9134,22 @@ var Phaser; Loader.prototype.audio = /** * Add a new audio file loading request. * @param key {string} Unique asset key of the audio file. - * @param url {string} URL of audio file. + * @param urls {Array} An array containing the URLs of the audio files, i.e.: [ 'jump.mp3', 'jump.ogg', 'jump.m4a' ] + * @param autoDecode {boolean} When using Web Audio the audio files can either be decoded at load time or run-time. They can't be played until they are decoded, but this let's you control when that happens. Decoding is a non-blocking async process. */ - function (key, url) { + function (key, urls, autoDecode) { + if (typeof autoDecode === "undefined") { autoDecode = true; } if(this.checkKeyExists(key) === false) { this._queueSize++; this._fileList[key] = { type: 'audio', key: key, - url: url, + url: urls, data: null, buffer: null, error: false, - loaded: false + loaded: false, + autoDecode: autoDecode }; this._keys.push(key); } @@ -9234,15 +9237,33 @@ var Phaser; file.data.src = file.url; break; case 'audio': - this._xhr.open("GET", file.url, true); - this._xhr.responseType = "arraybuffer"; - this._xhr.onload = function () { - return _this.fileComplete(file.key); - }; - this._xhr.onerror = function () { - return _this.fileError(file.key); - }; - this._xhr.send(); + file.url = this.getAudioURL(file.url); + if(file.url !== null) { + // WebAudio or Audio Tag? + if(this._game.sound.usingWebAudio) { + this._xhr.open("GET", file.url, true); + this._xhr.responseType = "arraybuffer"; + this._xhr.onload = function () { + return _this.fileComplete(file.key); + }; + this._xhr.onerror = function () { + return _this.fileError(file.key); + }; + this._xhr.send(); + } else if(this._game.sound.usingAudioTag) { + file.data = new Audio(); + file.data.name = file.key; + file.data.onerror = function () { + return _this.fileError(file.key); + }; + file.data.preload = 'auto'; + file.data.src = file.url; + file.data.addEventListener('canplaythrough', function () { + return _this.fileComplete(file.key); + }, false); + file.data.load(); + } + } break; case 'text': this._xhr.open("GET", file.url, true); @@ -9257,6 +9278,18 @@ var Phaser; break; } }; + Loader.prototype.getAudioURL = function (urls) { + var extension; + for(var i = 0; i < urls.length; i++) { + extension = urls[i].toLowerCase(); + extension = extension.substr((Math.max(0, extension.lastIndexOf(".")) || Infinity) + 1); + if(this._game.device.canPlayAudio(extension)) { + console.log('getAudioURL', urls[i]); + return urls[i]; + } + } + return null; + }; Loader.prototype.fileError = /** * Error occured when load a file. * @param key {string} Key of the error loading file. @@ -9306,8 +9339,22 @@ var Phaser; } break; case 'audio': - file.data = this._xhr.response; - this._game.cache.addSound(file.key, file.url, file.data); + if(this._game.sound.usingWebAudio) { + file.data = this._xhr.response; + this._game.cache.addSound(file.key, file.url, file.data, true, false); + if(file.autoDecode) { + this._game.cache.updateSound(key, 'isDecoding', true); + var that = this; + var key = file.key; + this._game.sound.context.decodeAudioData(file.data, function (buffer) { + if(buffer) { + that._game.cache.decodedSound(key, buffer); + } + }); + } + } else { + this._game.cache.addSound(file.key, file.url, file.data, false, true); + } break; case 'text': file.data = this._xhr.response; @@ -9492,22 +9539,38 @@ var Phaser; * @param url {string} URL of this sound file. * @param data {object} Extra sound data. */ - function (key, url, data) { + function (key, url, data, webAudio, audioTag) { + if (typeof webAudio === "undefined") { webAudio = true; } + if (typeof audioTag === "undefined") { audioTag = false; } + console.log('Cache addSound', key, url, webAudio, audioTag); + var decoded = false; + if(audioTag) { + decoded = true; + } this._sounds[key] = { url: url, data: data, - decoded: false + isDecoding: false, + decoded: decoded, + webAudio: webAudio, + audioTag: audioTag }; }; + Cache.prototype.updateSound = function (key, property, value) { + if(this._sounds[key]) { + this._sounds[key][property] = value; + } + }; Cache.prototype.decodedSound = /** * Add a new decoded sound. * @param key {string} Asset key for the sound. - * @param url {string} URL of this sound file. * @param data {object} Extra sound data. */ function (key, data) { + console.log('decoded sound', key); this._sounds[key].data = data; this._sounds[key].decoded = true; + this._sounds[key].isDecoding = false; }; Cache.prototype.addText = /** * Add a new text data. @@ -9555,6 +9618,17 @@ var Phaser; return null; }; Cache.prototype.getSound = /** + * Get sound by key. + * @param key Asset key of the sound you want. + * @return {object} The sound you want. + */ + function (key) { + if(this._sounds[key]) { + return this._sounds[key]; + } + return null; + }; + Cache.prototype.getSoundData = /** * Get sound data by key. * @param key Asset key of the sound you want. * @return {object} The sound data you want. @@ -13794,6 +13868,11 @@ var Phaser; if (typeof bodyType === "undefined") { bodyType = Phaser.Types.BODY_DISABLED; } return this._world.group.add(new Phaser.Sprite(this._game, x, y, key, frame, bodyType)); }; + GameObjectFactory.prototype.audio = function (key, volume, loop) { + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + return this._game.sound.add(key, volume, loop); + }; GameObjectFactory.prototype.physicsSprite = /** * Create a new Sprite with the physics automatically created and set to DYNAMIC. The Sprite position offset is set to its center. * @@ -13889,13 +13968,15 @@ var Phaser; return this._world.group.add(new Phaser.Tilemap(this._game, key, mapData, format, resizeWorld, tileWidth, tileHeight)); }; GameObjectFactory.prototype.tween = /** - * Create a tween object for a specific object. + * Create a tween object for a specific object. The object can be any JavaScript object or Phaser object such as Sprite. * - * @param obj Object you wish the tween will affect. + * @param obj {object} Object the tween will be run on. + * @param [localReference] {bool} If true the tween will be stored in the object.tween property so long as it exists. If already set it'll be over-written. * @return {Phaser.Tween} The newly created tween object. */ - function (obj) { - return this._game.tweens.create(obj); + function (obj, localReference) { + if (typeof localReference === "undefined") { localReference = false; } + return this._game.tweens.create(obj, localReference); }; GameObjectFactory.prototype.existingSprite = /** * Add an existing Sprite to the current world. @@ -13983,90 +14064,313 @@ var Phaser; var Sound = (function () { /** * Sound constructor - * @param context {object} The AudioContext instance. - * @param gainNode {object} Gain node instance. - * @param data {object} Sound data. * @param [volume] {number} volume of this sound when playing. * @param [loop] {boolean} loop this sound when playing? (Default to false) */ - function Sound(context, gainNode, data, volume, loop) { + function Sound(game, key, volume, loop) { if (typeof volume === "undefined") { volume = 1; } if (typeof loop === "undefined") { loop = false; } + /** + * Reference to AudioContext instance. + */ + this.context = null; + this._muted = false; + this.usingWebAudio = false; + this.usingAudioTag = false; + this.name = ''; + this.autoplay = false; + this.totalDuration = 0; + this.startTime = 0; + this.currentTime = 0; + this.duration = 0; + this.stopTime = 0; + this.paused = false; this.loop = false; this.isPlaying = false; - this.isDecoding = false; - this._context = context; - this._gainNode = gainNode; - this._buffer = data; + this.currentMarker = ''; + this.pendingPlayback = false; + this.game = game; + this.usingWebAudio = this.game.sound.usingWebAudio; + this.usingAudioTag = this.game.sound.usingAudioTag; + this.key = key; + 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 { + this._sound = this.game.cache.getSoundData(key); + this.totalDuration = this._sound.duration; + } this._volume = volume; this.loop = loop; - // Local volume control - if(this._context !== null) { - this._localGainNode = this._context.createGainNode(); - this._localGainNode.connect(this._gainNode); - this._localGainNode.gain.value = this._volume; - } - if(this._buffer === null) { - this.isDecoding = true; - } else { - this.play(); - } + this.markers = { + }; + this.onDecoded = new Phaser.Signal(); + this.onPlay = new Phaser.Signal(); + this.onPause = new Phaser.Signal(); + this.onResume = new Phaser.Signal(); + this.onLoop = new Phaser.Signal(); + this.onStop = new Phaser.Signal(); + this.onMute = new Phaser.Signal(); } - Sound.prototype.setDecodedBuffer = function (data) { - this._buffer = data; - this.isDecoding = false; - //this.play(); - }; + Object.defineProperty(Sound.prototype, "isDecoding", { + get: function () { + return this.game.cache.getSound(this.key).isDecoding; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Sound.prototype, "isDecoded", { + get: function () { + return this.game.cache.isSoundDecoded(this.key); + }, + enumerable: true, + configurable: true + }); + Sound.prototype.addMarker = function (name, start, stop, volume, loop) { + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + this.markers[name] = { + name: name, + start: start, + stop: stop, + volume: volume, + duration: stop - start, + loop: loop + }; + }; + Sound.prototype.removeMarker = function (name) { + delete this.markers[name]; + }; + Sound.prototype.update = function () { + if(this.pendingPlayback && this.game.cache.isSoundDecoded(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.duration) { + if(this.usingWebAudio) { + if(this.loop) { + this.onLoop.dispatch(this); + this.currentTime = 0; + this.startTime = this.game.time.now; + } else { + this.stop(); + } + } else { + if(this.loop) { + this.onLoop.dispatch(this); + this.play(this.currentMarker, 0, this.volume, true, true); + } else { + this.stop(); + } + } + } + } + }; Sound.prototype.play = /** - * Play this sound. + * Play this sound, or a marked section of it. + * @param marker {string} Assets key of the sound you want to play. + * @param [volume] {number} volume of the sound you want to play. + * @param [loop] {boolean} loop when it finished playing? (Default to false) + * @return {Sound} The playing sound object. */ - function () { - if(this._buffer === null || this.isDecoding === true) { + function (marker, position, volume, loop, forceRestart) { + if (typeof marker === "undefined") { marker = ''; } + if (typeof position === "undefined") { position = 0; } + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + if (typeof forceRestart === "undefined") { forceRestart = false; } + if(this.isPlaying == true && forceRestart == false) { + // Use Restart instead return; } - this._sound = this._context.createBufferSource(); - this._sound.buffer = this._buffer; - this._sound.connect(this._localGainNode); - if(this.loop) { - this._sound.loop = true; + this.currentMarker = marker; + if(marker !== '' && this.markers[marker]) { + this.loop = this.markers[marker].loop; + this.volume = this.markers[marker].volume; + this.position = this.markers[marker].start; + this.duration = this.markers[marker].duration * 1000; + } else { + this.loop = loop; + this.volume = volume; + this.position = position; + this.duration = 0; + } + 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? + 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) { + this.duration = this.totalDuration * 1000; + } + if(this.loop) { + this._sound.loop = true; + } + // Useful to cache this somewhere perhaps? + if(typeof this._sound.start === 'undefined') { + this._sound.noteGrainOn(0, this.position, this.duration / 1000); + //this._sound.noteOn(0); // the zero is vitally important, crashes iOS6 without it + } else { + this._sound.start(0, this.position, this.duration / 1000); + } + this.isPlaying = true; + this.startTime = this.game.time.now; + this.currentTime = 0; + this.stopTime = this.startTime + this.duration; + this.onPlay.dispatch(this); + } else { + this._tempVolume = volume; + this._tempLoop = loop; + this._tempPosition = position; + this._tempMarker = marker; + this.pendingPlayback = true; + if(this.game.cache.getSound(this.key).isDecoding == false) { + this.game.sound.decode(this.key, this); + } + } + } else { + if(this._sound.readyState == 4) { + this._sound.currentTime = this.position; + this._sound.muted = this._muted; + this._sound.volume = this._volume; + this._sound.play(); + this.isPlaying = true; + this.startTime = this.game.time.now; + this.currentTime = 0; + this.stopTime = this.startTime + this.duration; + this.onPlay.dispatch(this); + } + } + }; + Sound.prototype.restart = function (marker, position, volume, loop) { + if (typeof marker === "undefined") { marker = ''; } + if (typeof position === "undefined") { position = 0; } + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + this.play(marker, position, volume, loop, true); + }; + Sound.prototype.pause = function () { + if(this.isPlaying && this._sound) { + this.stop(); + this.isPlaying = false; + this.paused = true; + this.onPause.dispatch(this); + } + }; + Sound.prototype.resume = function () { + if(this.isPlaying == false && this._sound) { + if(this.usingWebAudio) { + if(typeof this._sound.start === 'undefined') { + this._sound.noteGrainOn(0, this.position, this.duration); + //this._sound.noteOn(0); // the zero is vitally important, crashes iOS6 without it + } else { + this._sound.start(0, this.position, this.duration); + } + } else { + this._sound.play(); + } + this.isPlaying = true; + this.paused = false; + this.onResume.dispatch(this); } - this._sound.noteOn(0)// the zero is vitally important, crashes iOS6 without it - ; - this.duration = this._sound.buffer.duration; - this.isPlaying = true; }; Sound.prototype.stop = /** * Stop playing this sound. */ function () { - if(this.isPlaying === true) { + 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; - this._sound.noteOff(0); + this.currentMarker = ''; + this.onStop.dispatch(this); } }; - Sound.prototype.mute = /** - * Mute the sound. - */ - function () { - this._localGainNode.gain.value = 0; - }; - Sound.prototype.unmute = /** - * Enable the sound. - */ - function () { - this._localGainNode.gain.value = this._volume; - }; + Object.defineProperty(Sound.prototype, "mute", { + get: /** + * Mute sounds. + */ + function () { + return this._muted; + }, + set: function (value) { + if(value) { + this._muted = true; + if(this.usingWebAudio) { + this._muteVolume = this.gainNode.gain.value; + this.gainNode.gain.value = 0; + } else if(this.usingAudioTag) { + 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.volume = this._muteVolume; + } + } + this.onMute.dispatch(this); + }, + enumerable: true, + configurable: true + }); Object.defineProperty(Sound.prototype, "volume", { get: function () { return this._volume; }, set: function (value) { this._volume = value; - this._localGainNode.gain.value = this._volume; + if(this.usingWebAudio) { + this.gainNode.gain.value = value; + } else if(this.usingAudioTag) { + this._sound.volume = value; + } }, enumerable: true, configurable: true }); + Sound.prototype.renderDebug = /** + * Renders the Pointer.circle object onto the stage in green if down or red if up. + * @method renderDebug + */ + function (x, y) { + this.game.stage.context.fillStyle = 'rgb(255,255,255)'; + this.game.stage.context.font = '16px Courier'; + this.game.stage.context.fillText('Sound: ' + this.key + ' Locked: ' + this.game.sound.touchLocked + ' Pending Playback: ' + this.pendingPlayback, x, y); + this.game.stage.context.fillText('Decoded: ' + this.isDecoded + ' Decoding: ' + this.isDecoding, x, y + 20); + this.game.stage.context.fillText('Total Duration: ' + this.totalDuration + ' Playing: ' + this.isPlaying, x, y + 40); + this.game.stage.context.fillText('Time: ' + this.currentTime, x, y + 60); + this.game.stage.context.fillText('Volume: ' + this.volume + ' Muted: ' + this.mute, x, y + 80); + this.game.stage.context.fillText('WebAudio: ' + this.usingWebAudio + ' Audio: ' + this.usingAudioTag, x, y + 100); + if(this.currentMarker !== '') { + this.game.stage.context.fillText('Marker: ' + this.currentMarker + ' Duration: ' + this.duration, x, y + 120); + this.game.stage.context.fillText('Start: ' + this.markers[this.currentMarker].start + ' Stop: ' + this.markers[this.currentMarker].stop, x, y + 140); + this.game.stage.context.fillText('Position: ' + this.position, x, y + 160); + } + }; return Sound; })(); Phaser.Sound = Sound; @@ -14086,46 +14390,147 @@ var Phaser; * Create a new SoundManager. */ function SoundManager(game) { + this.usingWebAudio = false; + this.usingAudioTag = false; + this.noAudio = false; /** * Reference to AudioContext instance. */ - this._context = null; - this._game = game; - if(window['PhaserGlobal'] && window['PhaserGlobal'].disableAudio == true) { - return; + this.context = null; + this._muted = false; + this.touchLocked = false; + this._unlockSource = null; + this.onSoundDecode = new Phaser.Signal(); + this.game = game; + this._volume = 1; + this._muted = false; + this._sounds = []; + if(this.game.device.iOS && this.game.device.webAudio == false) { + this.channels = 1; } - if(game.device.webaudio == true) { - if(!!window['AudioContext']) { - this._context = new window['AudioContext'](); - } else if(!!window['webkitAudioContext']) { - this._context = new window['webkitAudioContext'](); + if(window['PhaserGlobal']) { + // Check to see if all audio playback is disabled (i.e. handled by a 3rd party class) + if(window['PhaserGlobal'].disableAudio == true) { + this.usingWebAudio = false; + this.noAudio = true; + return; } - if(this._context !== null) { - this._gainNode = this._context.createGainNode(); - this._gainNode.connect(this._context.destination); - this._volume = 1; + // Check if the Web Audio API is disabled (for testing Audio Tag playback during development) + if(window['PhaserGlobal'].disableWebAudio == true) { + this.usingWebAudio = false; + this.usingAudioTag = true; + this.noAudio = false; + return; } } + if(this.game.device.iOS && this.game.device.webAudio) { + this.game.input.touch.callbackContext = this; + this.game.input.touch.touchStartCallback = this.unlock; + this.touchLocked = true; + } else { + // What about iOS5? + this.touchLocked = false; + } + this.usingWebAudio = true; + this.noAudio = false; + if(!!window['AudioContext']) { + this.context = new window['AudioContext'](); + } else if(!!window['webkitAudioContext']) { + this.context = new window['webkitAudioContext'](); + } else if(!!window['Audio']) { + this.usingWebAudio = false; + this.usingAudioTag = true; + } else { + this.usingWebAudio = false; + this.noAudio = true; + } + if(this.context !== null) { + if(typeof this.context.createGain === 'undefined') { + this.masterGain = this.context.createGainNode(); + } else { + this.masterGain = this.context.createGain(); + } + this.masterGain.gain.value = 1; + this.masterGain.connect(this.context.destination); + } } - SoundManager.prototype.mute = /** - * Mute sounds. - */ - function () { - this._gainNode.gain.value = 0; + SoundManager.prototype.unlock = function () { + if(this.touchLocked == false || this.game.device.iOS == false) { + return; + } + // Create Audio tag? + // Create empty buffer and play it + var buffer = this.context.createBuffer(1, 1, 22050); + this._unlockSource = this.context.createBufferSource(); + this._unlockSource.buffer = buffer; + this._unlockSource.connect(this.context.destination); + this._unlockSource.noteOn(0); }; - SoundManager.prototype.unmute = /** - * Enable sounds. - */ - function () { - this._gainNode.gain.value = this._volume; - }; - Object.defineProperty(SoundManager.prototype, "volume", { - get: function () { - return this._volume; + Object.defineProperty(SoundManager.prototype, "mute", { + get: /** + * A global audio mute toggle. + */ + function () { + return this._muted; }, set: function (value) { + if(value) { + if(this._muted) { + return; + } + this._muted = true; + if(this.usingWebAudio) { + this._muteVolume = this.masterGain.gain.value; + this.masterGain.gain.value = 0; + } + // Loop through sounds + for(var i = 0; i < this._sounds.length; i++) { + if(this._sounds[i].usingAudioTag) { + this._sounds[i].mute = true; + } + } + } else { + if(this._muted == false) { + return; + } + this._muted = false; + if(this.usingWebAudio) { + this.masterGain.gain.value = this._muteVolume; + } + // Loop through sounds + for(var i = 0; i < this._sounds.length; i++) { + if(this._sounds[i].usingAudioTag) { + this._sounds[i].mute = false; + } + } + } + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(SoundManager.prototype, "volume", { + get: function () { + if(this.usingWebAudio) { + return this.masterGain.gain.value; + } else { + return this._volume; + } + }, + set: /** + * The global audio volume. A value between 0 (silence) and 1 (full volume) + */ + function (value) { + value = this.game.math.clamp(value, 1, 0); this._volume = value; - this._gainNode.gain.value = this._volume; + if(this.usingWebAudio) { + this.masterGain.gain.value = value; + } + // Loop through the sound cache and change the volume of all html audio tags + for(var i = 0; i < this._sounds.length; i++) { + if(this._sounds[i].usingAudioTag) { + this._sounds[i].volume = this._sounds[i].volume * value; + } + } }, enumerable: true, configurable: true @@ -14133,53 +14538,43 @@ var Phaser; SoundManager.prototype.decode = /** * Decode a sound with its assets key. * @param key {string} Assets key of the sound to be decoded. - * @param callback {function} This will be invoked when finished decoding. * @param [sound] {Sound} its bufer will be set to decoded data. */ - function (key, callback, sound) { - if (typeof callback === "undefined") { callback = null; } + function (key, sound) { if (typeof sound === "undefined") { sound = null; } - var soundData = this._game.cache.getSound(key); + var soundData = this.game.cache.getSoundData(key); if(soundData) { - if(this._game.cache.isSoundDecoded(key) === false) { + if(this.game.cache.isSoundDecoded(key) === false) { + this.game.cache.updateSound(key, 'isDecoding', true); var that = this; - this._context.decodeAudioData(soundData, function (buffer) { - that._game.cache.decodedSound(key, buffer); + this.context.decodeAudioData(soundData, function (buffer) { + that.game.cache.decodedSound(key, buffer); if(sound) { - sound.setDecodedBuffer(buffer); + that.onSoundDecode.dispatch(sound); } - callback(); }); } } }; - SoundManager.prototype.play = /** - * Play a sound with its assets key. - * @param key {string} Assets key of the sound you want to play. - * @param [volume] {number} volume of the sound you want to play. - * @param [loop] {boolean} loop when it finished playing? (Default to false) - * @return {Sound} The playing sound object. - */ - function (key, volume, loop) { - if (typeof volume === "undefined") { volume = 1; } - if (typeof loop === "undefined") { loop = false; } - if(this._context === null) { - return; - } - var soundData = this._game.cache.getSound(key); - if(soundData) { - // Does the sound need decoding? - if(this._game.cache.isSoundDecoded(key) === true) { - return new Phaser.Sound(this._context, this._gainNode, soundData, volume, loop); - } else { - var tempSound = new Phaser.Sound(this._context, this._gainNode, null, volume, loop); - // this is an async process, so we can return the Sound object anyway, it just won't be playing yet - this.decode(key, function () { - return tempSound.play(); - }, tempSound); - return tempSound; + SoundManager.prototype.update = function () { + if(this.touchLocked && this._unlockSource !== null) { + if((this._unlockSource.playbackState === this._unlockSource.PLAYING_STATE || this._unlockSource.playbackState === this._unlockSource.FINISHED_STATE)) { + this.touchLocked = false; + this._unlockSource = null; + this.game.input.touch.callbackContext = null; + this.game.input.touch.touchStartCallback = null; } } + for(var i = 0; i < this._sounds.length; i++) { + this._sounds[i].update(); + } + }; + SoundManager.prototype.add = function (key, volume, loop) { + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + var sound = new Phaser.Sound(this.game, key, volume, loop); + this._sounds.push(sound); + return sound; }; return SoundManager; })(); @@ -14868,6 +15263,12 @@ var Phaser; document.addEventListener('webkitvisibilitychange', function (event) { return _this.visibilityChange(event); }, false); + document.addEventListener('pagehide', function (event) { + return _this.visibilityChange(event); + }, false); + document.addEventListener('pageshow', function (event) { + return _this.visibilityChange(event); + }, false); window.onblur = function (event) { return _this.visibilityChange(event); }; @@ -14915,7 +15316,7 @@ var Phaser; if(this.disablePauseScreen) { return; } - if(event.type == 'blur' || document['hidden'] == true || document['webkitHidden'] == true) { + if(event.type == 'pagehide' || event.type == 'blur' || document['hidden'] == true || document['webkitHidden'] == true) { if(this._game.paused == false) { this.pauseGame(); } @@ -15251,13 +15652,20 @@ var Phaser; this._tweens.length = 0; }; TweenManager.prototype.create = /** - * Create a tween object for a specific object. + * Create a tween object for a specific object. The object can be any JavaScript object or Phaser object such as Sprite. * - * @param object {object} Object you wish the tween will affect. + * @param obj {object} Object the tween will be run on. + * @param [localReference] {bool} If true the tween will be stored in the object.tween property so long as it exists. If already set it'll be over-written. * @return {Phaser.Tween} The newly created tween object. */ - function (object) { - return new Phaser.Tween(object, this._game); + function (object, localReference) { + if (typeof localReference === "undefined") { localReference = false; } + if(localReference) { + object['tween'] = new Phaser.Tween(object, this._game); + return object['tween']; + } else { + return new Phaser.Tween(object, this._game); + } }; TweenManager.prototype.add = /** * Add a new tween into the TweenManager. @@ -15899,35 +16307,45 @@ var Phaser; this.webApp = false; // Audio /** - * Is audioData available? + * Are Audio tags available? * @type {boolean} */ this.audioData = false; /** - * Is webaudio available? + * Is the WebAudio API available? * @type {boolean} */ - this.webaudio = false; + this.webAudio = false; /** - * Is ogg available? + * Can this device play ogg files? * @type {boolean} */ this.ogg = false; /** - * Is mp3 available? + * Can this device play opus files? + * @type {boolean} + */ + this.opus = false; + /** + * Can this device play mp3 files? * @type {boolean} */ this.mp3 = false; /** - * Is wav available? + * Can this device play wav files? * @type {boolean} */ this.wav = false; /** - * Is m4a available? + * Can this device play m4a files? * @type {boolean} */ this.m4a = false; + /** + * Can this device play webm files? + * @type {boolean} + */ + this.webm = false; // Device /** * Is running on iPhone? @@ -16032,13 +16450,27 @@ var Phaser; this.webApp = true; } }; + Device.prototype.canPlayAudio = function (type) { + if(type == 'mp3' && this.mp3) { + return true; + } else if(type == 'ogg' && (this.ogg || this.opus)) { + return true; + } else if(type == 'm4a' && this.m4a) { + return true; + } else if(type == 'wav' && this.wav) { + return true; + } else if(type == 'webm' && this.webm) { + return true; + } + return false; + }; Device.prototype._checkAudio = /** * Check audio support. * @private */ function () { this.audioData = !!(window['Audio']); - this.webaudio = !!(window['webkitAudioContext'] || window['AudioContext']); + this.webAudio = !!(window['webkitAudioContext'] || window['AudioContext']); var audioElement = document.createElement('audio'); var result = false; try { @@ -16046,6 +16478,9 @@ var Phaser; if(audioElement.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '')) { this.ogg = true; } + if(audioElement.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, '')) { + this.opus = true; + } if(audioElement.canPlayType('audio/mpeg;').replace(/^no$/, '')) { this.mp3 = true; } @@ -16058,6 +16493,9 @@ var Phaser; if(audioElement.canPlayType('audio/x-m4a;') || audioElement.canPlayType('audio/aac;').replace(/^no$/, '')) { this.m4a = true; } + if(audioElement.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')) { + this.webm = true; + } } } catch (e) { } @@ -16644,6 +17082,12 @@ var Phaser; **/ this.totalTouches = 0; /** + * The number of miliseconds since the last click + * @property msSinceLastClick + * @type {Number} + **/ + this.msSinceLastClick = Number.MAX_VALUE; + /** * The Game Object this Pointer is currently over / touching / dragging. * @property targetObject * @type {Any} @@ -16711,6 +17155,8 @@ var Phaser; this.withinGame = true; this.isDown = true; this.isUp = false; + // Work out how long it has been since the last click + this.msSinceLastClick = this.game.time.now - this.timeDown; this.timeDown = this.game.time.now; this._holdSent = false; // This sets the x/y and other local values @@ -17768,7 +18214,7 @@ var Phaser; * @property recordPointerHistory * @type {Boolean} **/ - this.recordPointerHistory = true; + this.recordPointerHistory = false; /** * The rate in milliseconds at which the Pointer objects should update their tracking history * @property recordRate @@ -17777,7 +18223,7 @@ var Phaser; this.recordRate = 100; /** * The total number of entries that can be recorded into the Pointer objects tracking history. - * The the Pointer is tracking one event every 100ms, then a trackLimit of 100 would store the last 10 seconds worth of history. + * If the Pointer is tracking one event every 100ms, then a trackLimit of 100 would store the last 10 seconds worth of history. * @property recordLimit * @type {Number} */ @@ -19020,6 +19466,7 @@ var Phaser; /// /// /// +/// /// /// /// @@ -19190,12 +19637,12 @@ var Phaser; this.stage = new Phaser.Stage(this, parent, width, height); this.world = new Phaser.World(this, width, height); this.add = new Phaser.GameObjectFactory(this); - this.sound = new Phaser.SoundManager(this); this.cache = new Phaser.Cache(this); this.load = new Phaser.Loader(this, this.loadComplete); this.time = new Phaser.Time(this); this.tweens = new Phaser.TweenManager(this); this.input = new Phaser.Input(this); + this.sound = new Phaser.SoundManager(this); this.rnd = new Phaser.RandomDataGenerator([ (Date.now() * Math.random()).toString() ]); @@ -19257,6 +19704,7 @@ var Phaser; this.tweens.update(); this.input.update(); this.stage.update(); + this.sound.update(); if(this.onPausedCallback !== null) { this.onPausedCallback.call(this.callbackContext); } @@ -19268,6 +19716,7 @@ var Phaser; this.tweens.update(); this.input.update(); this.stage.update(); + this.sound.update(); this.physics.update(); this.world.update(); if(this._loadComplete && this.onUpdateCallback) { diff --git a/build/phaser.d.ts b/build/phaser.d.ts index b25b4535..959a53e9 100644 --- a/build/phaser.d.ts +++ b/build/phaser.d.ts @@ -4299,9 +4299,10 @@ module Phaser { /** * Add a new audio file loading request. * @param key {string} Unique asset key of the audio file. - * @param url {string} URL of audio file. + * @param urls {Array} An array containing the URLs of the audio files, i.e.: [ 'jump.mp3', 'jump.ogg', 'jump.m4a' ] + * @param autoDecode {boolean} When using Web Audio the audio files can either be decoded at load time or run-time. They can't be played until they are decoded, but this let's you control when that happens. Decoding is a non-blocking async process. */ - public audio(key: string, url: string): void; + public audio(key: string, urls: string[], autoDecode?: bool): void; /** * Add a new text file loading request. * @param key {string} Unique asset key of the text file. @@ -4327,6 +4328,7 @@ module Phaser { * Load files. Private method ONLY used by loader. */ private loadFile(); + private getAudioURL(urls); /** * Error occured when load a file. * @param key {string} Key of the error loading file. @@ -4436,11 +4438,11 @@ module Phaser { * @param url {string} URL of this sound file. * @param data {object} Extra sound data. */ - public addSound(key: string, url: string, data): void; + public addSound(key: string, url: string, data, webAudio?: bool, audioTag?: bool): void; + public updateSound(key: string, property: string, value): void; /** * Add a new decoded sound. * @param key {string} Asset key for the sound. - * @param url {string} URL of this sound file. * @param data {object} Extra sound data. */ public decodedSound(key: string, data): void; @@ -4470,11 +4472,17 @@ module Phaser { */ public getFrameData(key: string): FrameData; /** + * Get sound by key. + * @param key Asset key of the sound you want. + * @return {object} The sound you want. + */ + public getSound(key: string); + /** * Get sound data by key. * @param key Asset key of the sound you want. * @return {object} The sound data you want. */ - public getSound(key: string); + public getSoundData(key: string); /** * Check whether an asset is decoded sound. * @param key Asset key of the sound you want. @@ -6806,6 +6814,7 @@ module Phaser { * @returns {Sprite} The newly created sprite object. */ public sprite(x: number, y: number, key?: string, frame?, bodyType?: number): Sprite; + public audio(key: string, volume?: number, loop?: bool): Sound; /** * Create a new Sprite with the physics automatically created and set to DYNAMIC. The Sprite position offset is set to its center. * @@ -6938,27 +6947,28 @@ module Phaser { class Sound { /** * Sound constructor - * @param context {object} The AudioContext instance. - * @param gainNode {object} Gain node instance. - * @param data {object} Sound data. * @param [volume] {number} volume of this sound when playing. * @param [loop] {boolean} loop this sound when playing? (Default to false) */ - constructor(context, gainNode, data, volume?: number, loop?: bool); + constructor(game: Game, key: string, volume?: number, loop?: bool); /** - * Local private reference to AudioContext. + * Local reference to the current Phaser.Game. */ - private _context; + public game: Game; + /** + * Reference to AudioContext instance. + */ + public context; /** * Reference to gain node of SoundManager. */ - private _gainNode; + public masterGainNode; /** * GainNode of this sound. */ - private _localGainNode; + public gainNode; /** - * Decoded data buffer. + * Decoded data buffer / Audio tag. */ private _buffer; /** @@ -6969,28 +6979,66 @@ module Phaser { * The real sound object (buffer source). */ private _sound; - public loop: bool; + private _muteVolume; + private _muted; + private _tempPosition; + private _tempVolume; + private _tempLoop; + private _tempMarker; + public usingWebAudio: bool; + public usingAudioTag: bool; + public name: string; + public autoplay: bool; + public totalDuration: number; + public startTime: number; + public currentTime: number; public duration: number; + public stopTime: number; + public position: number; + public paused: bool; + public loop: bool; public isPlaying: bool; - public isDecoding: bool; - public setDecodedBuffer(data): void; + public key: string; + public markers; + public currentMarker: string; + public onDecoded: Signal; + public onPlay: Signal; + public onPause: Signal; + public onResume: Signal; + public onLoop: Signal; + public onStop: Signal; + public onMute: Signal; + public pendingPlayback: bool; + public isDecoding : bool; + public isDecoded : bool; + public addMarker(name: string, start: number, stop: number, volume?: number, loop?: bool): void; + public removeMarker(name: string): void; + public update(): void; /** - * Play this sound. + * Play this sound, or a marked section of it. + * @param marker {string} Assets key of the sound you want to play. + * @param [volume] {number} volume of the sound you want to play. + * @param [loop] {boolean} loop when it finished playing? (Default to false) + * @return {Sound} The playing sound object. */ - public play(): void; + public play(marker?: string, position?: number, volume?: number, loop?: bool, forceRestart?: bool): void; + public restart(marker?: string, position?: number, volume?: number, loop?: bool): void; + public pause(): void; + public resume(): void; /** * Stop playing this sound. */ public stop(): void; /** - * Mute the sound. + * Mute sounds. */ - public mute(): void; - /** - * Enable the sound. - */ - public unmute(): void; + public mute : bool; public volume : number; + /** + * Renders the Pointer.circle object onto the stage in green if down or red if up. + * @method renderDebug + */ + public renderDebug(x: number, y: number): void; } } /** @@ -7005,47 +7053,50 @@ module Phaser { * Create a new SoundManager. */ constructor(game: Game); + public usingWebAudio: bool; + public usingAudioTag: bool; + public noAudio: bool; /** - * Local private reference to game. + * Local reference to the current Phaser.Game. */ - private _game; + public game: Game; /** * Reference to AudioContext instance. */ - private _context; + public context; /** - * Gain node created from audio context. + * The Master Gain node through which all sounds */ - private _gainNode; + public masterGain; /** * Volume of sounds. * @type {number} */ private _volume; + private _sounds; + private _muteVolume; + private _muted; + public channels: number; + public touchLocked: bool; + private _unlockSource; + public unlock(): void; /** - * Mute sounds. + * A global audio mute toggle. */ - public mute(): void; + public mute : bool; /** - * Enable sounds. + * The global audio volume. A value between 0 (silence) and 1 (full volume) */ - public unmute(): void; public volume : number; /** * Decode a sound with its assets key. * @param key {string} Assets key of the sound to be decoded. - * @param callback {function} This will be invoked when finished decoding. * @param [sound] {Sound} its bufer will be set to decoded data. */ - public decode(key: string, callback?, sound?: Sound): void; - /** - * Play a sound with its assets key. - * @param key {string} Assets key of the sound you want to play. - * @param [volume] {number} volume of the sound you want to play. - * @param [loop] {boolean} loop when it finished playing? (Default to false) - * @return {Sound} The playing sound object. - */ - public play(key: string, volume?: number, loop?: bool): Sound; + public decode(key: string, sound?: Sound): void; + public onSoundDecode: Signal; + public update(): void; + public add(key: string, volume?: number, loop?: bool): Sound; } } /** @@ -8106,36 +8157,46 @@ module Phaser { public safari: bool; public webApp: bool; /** - * Is audioData available? + * Are Audio tags available? * @type {boolean} */ public audioData: bool; /** - * Is webaudio available? + * Is the WebAudio API available? * @type {boolean} */ - public webaudio: bool; + public webAudio: bool; /** - * Is ogg available? + * Can this device play ogg files? * @type {boolean} */ public ogg: bool; /** - * Is mp3 available? + * Can this device play opus files? + * @type {boolean} + */ + public opus: bool; + /** + * Can this device play mp3 files? * @type {boolean} */ public mp3: bool; /** - * Is wav available? + * Can this device play wav files? * @type {boolean} */ public wav: bool; /** - * Is m4a available? + * Can this device play m4a files? * @type {boolean} */ public m4a: bool; /** + * Can this device play webm files? + * @type {boolean} + */ + public webm: bool; + /** * Is running on iPhone? * @type {boolean} */ @@ -8170,6 +8231,7 @@ module Phaser { * @private */ private _checkBrowser(); + public canPlayAudio(type: string): bool; /** * Check audio support. * @private @@ -8405,6 +8467,9 @@ module Phaser { * @return {Phaser.Pointer} This object. */ constructor(game: Game, id: number); + private _highestRenderOrderID; + private _highestRenderObject; + private _highestInputPriorityID; /** * Local private reference to game. * @property game @@ -8582,6 +8647,12 @@ module Phaser { **/ public totalTouches: number; /** + * The number of miliseconds since the last click + * @property msSinceLastClick + * @type {Number} + **/ + public msSinceLastClick: number; + /** * How long the Pointer has been depressed on the touchscreen. If not currently down it returns -1. * @property duration * @type {Number} @@ -8610,9 +8681,6 @@ module Phaser { */ public start(event): Pointer; public update(): void; - private _highestRenderOrderID; - private _highestRenderObject; - private _highestInputPriorityID; /** * Called when the Pointer is moved on the touchscreen * @method move @@ -9265,7 +9333,7 @@ module Phaser { public recordRate: number; /** * The total number of entries that can be recorded into the Pointer objects tracking history. - * The the Pointer is tracking one event every 100ms, then a trackLimit of 100 would store the last 10 seconds worth of history. + * If the Pointer is tracking one event every 100ms, then a trackLimit of 100 would store the last 10 seconds worth of history. * @property recordLimit * @type {Number} */ diff --git a/build/phaser.js b/build/phaser.js index 3952482c..c616e6f0 100644 --- a/build/phaser.js +++ b/build/phaser.js @@ -7243,7 +7243,7 @@ var Phaser; this.cameraView = new Phaser.Rectangle(x, y, this.width, this.height); this.transform.setCache(); this.outOfBounds = false; - this.outOfBoundsAction = Phaser.Types.OUT_OF_BOUNDS_KILL; + this.outOfBoundsAction = Phaser.Types.OUT_OF_BOUNDS_PERSIST; // Handy proxies this.scale = this.transform.scale; this.alpha = this.texture.alpha; @@ -9134,19 +9134,22 @@ var Phaser; Loader.prototype.audio = /** * Add a new audio file loading request. * @param key {string} Unique asset key of the audio file. - * @param url {string} URL of audio file. + * @param urls {Array} An array containing the URLs of the audio files, i.e.: [ 'jump.mp3', 'jump.ogg', 'jump.m4a' ] + * @param autoDecode {boolean} When using Web Audio the audio files can either be decoded at load time or run-time. They can't be played until they are decoded, but this let's you control when that happens. Decoding is a non-blocking async process. */ - function (key, url) { + function (key, urls, autoDecode) { + if (typeof autoDecode === "undefined") { autoDecode = true; } if(this.checkKeyExists(key) === false) { this._queueSize++; this._fileList[key] = { type: 'audio', key: key, - url: url, + url: urls, data: null, buffer: null, error: false, - loaded: false + loaded: false, + autoDecode: autoDecode }; this._keys.push(key); } @@ -9234,15 +9237,33 @@ var Phaser; file.data.src = file.url; break; case 'audio': - this._xhr.open("GET", file.url, true); - this._xhr.responseType = "arraybuffer"; - this._xhr.onload = function () { - return _this.fileComplete(file.key); - }; - this._xhr.onerror = function () { - return _this.fileError(file.key); - }; - this._xhr.send(); + file.url = this.getAudioURL(file.url); + if(file.url !== null) { + // WebAudio or Audio Tag? + if(this._game.sound.usingWebAudio) { + this._xhr.open("GET", file.url, true); + this._xhr.responseType = "arraybuffer"; + this._xhr.onload = function () { + return _this.fileComplete(file.key); + }; + this._xhr.onerror = function () { + return _this.fileError(file.key); + }; + this._xhr.send(); + } else if(this._game.sound.usingAudioTag) { + file.data = new Audio(); + file.data.name = file.key; + file.data.onerror = function () { + return _this.fileError(file.key); + }; + file.data.preload = 'auto'; + file.data.src = file.url; + file.data.addEventListener('canplaythrough', function () { + return _this.fileComplete(file.key); + }, false); + file.data.load(); + } + } break; case 'text': this._xhr.open("GET", file.url, true); @@ -9257,6 +9278,18 @@ var Phaser; break; } }; + Loader.prototype.getAudioURL = function (urls) { + var extension; + for(var i = 0; i < urls.length; i++) { + extension = urls[i].toLowerCase(); + extension = extension.substr((Math.max(0, extension.lastIndexOf(".")) || Infinity) + 1); + if(this._game.device.canPlayAudio(extension)) { + console.log('getAudioURL', urls[i]); + return urls[i]; + } + } + return null; + }; Loader.prototype.fileError = /** * Error occured when load a file. * @param key {string} Key of the error loading file. @@ -9306,8 +9339,22 @@ var Phaser; } break; case 'audio': - file.data = this._xhr.response; - this._game.cache.addSound(file.key, file.url, file.data); + if(this._game.sound.usingWebAudio) { + file.data = this._xhr.response; + this._game.cache.addSound(file.key, file.url, file.data, true, false); + if(file.autoDecode) { + this._game.cache.updateSound(key, 'isDecoding', true); + var that = this; + var key = file.key; + this._game.sound.context.decodeAudioData(file.data, function (buffer) { + if(buffer) { + that._game.cache.decodedSound(key, buffer); + } + }); + } + } else { + this._game.cache.addSound(file.key, file.url, file.data, false, true); + } break; case 'text': file.data = this._xhr.response; @@ -9492,22 +9539,38 @@ var Phaser; * @param url {string} URL of this sound file. * @param data {object} Extra sound data. */ - function (key, url, data) { + function (key, url, data, webAudio, audioTag) { + if (typeof webAudio === "undefined") { webAudio = true; } + if (typeof audioTag === "undefined") { audioTag = false; } + console.log('Cache addSound', key, url, webAudio, audioTag); + var decoded = false; + if(audioTag) { + decoded = true; + } this._sounds[key] = { url: url, data: data, - decoded: false + isDecoding: false, + decoded: decoded, + webAudio: webAudio, + audioTag: audioTag }; }; + Cache.prototype.updateSound = function (key, property, value) { + if(this._sounds[key]) { + this._sounds[key][property] = value; + } + }; Cache.prototype.decodedSound = /** * Add a new decoded sound. * @param key {string} Asset key for the sound. - * @param url {string} URL of this sound file. * @param data {object} Extra sound data. */ function (key, data) { + console.log('decoded sound', key); this._sounds[key].data = data; this._sounds[key].decoded = true; + this._sounds[key].isDecoding = false; }; Cache.prototype.addText = /** * Add a new text data. @@ -9555,6 +9618,17 @@ var Phaser; return null; }; Cache.prototype.getSound = /** + * Get sound by key. + * @param key Asset key of the sound you want. + * @return {object} The sound you want. + */ + function (key) { + if(this._sounds[key]) { + return this._sounds[key]; + } + return null; + }; + Cache.prototype.getSoundData = /** * Get sound data by key. * @param key Asset key of the sound you want. * @return {object} The sound data you want. @@ -13794,6 +13868,11 @@ var Phaser; if (typeof bodyType === "undefined") { bodyType = Phaser.Types.BODY_DISABLED; } return this._world.group.add(new Phaser.Sprite(this._game, x, y, key, frame, bodyType)); }; + GameObjectFactory.prototype.audio = function (key, volume, loop) { + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + return this._game.sound.add(key, volume, loop); + }; GameObjectFactory.prototype.physicsSprite = /** * Create a new Sprite with the physics automatically created and set to DYNAMIC. The Sprite position offset is set to its center. * @@ -13985,90 +14064,313 @@ var Phaser; var Sound = (function () { /** * Sound constructor - * @param context {object} The AudioContext instance. - * @param gainNode {object} Gain node instance. - * @param data {object} Sound data. * @param [volume] {number} volume of this sound when playing. * @param [loop] {boolean} loop this sound when playing? (Default to false) */ - function Sound(context, gainNode, data, volume, loop) { + function Sound(game, key, volume, loop) { if (typeof volume === "undefined") { volume = 1; } if (typeof loop === "undefined") { loop = false; } + /** + * Reference to AudioContext instance. + */ + this.context = null; + this._muted = false; + this.usingWebAudio = false; + this.usingAudioTag = false; + this.name = ''; + this.autoplay = false; + this.totalDuration = 0; + this.startTime = 0; + this.currentTime = 0; + this.duration = 0; + this.stopTime = 0; + this.paused = false; this.loop = false; this.isPlaying = false; - this.isDecoding = false; - this._context = context; - this._gainNode = gainNode; - this._buffer = data; + this.currentMarker = ''; + this.pendingPlayback = false; + this.game = game; + this.usingWebAudio = this.game.sound.usingWebAudio; + this.usingAudioTag = this.game.sound.usingAudioTag; + this.key = key; + 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 { + this._sound = this.game.cache.getSoundData(key); + this.totalDuration = this._sound.duration; + } this._volume = volume; this.loop = loop; - // Local volume control - if(this._context !== null) { - this._localGainNode = this._context.createGainNode(); - this._localGainNode.connect(this._gainNode); - this._localGainNode.gain.value = this._volume; - } - if(this._buffer === null) { - this.isDecoding = true; - } else { - this.play(); - } + this.markers = { + }; + this.onDecoded = new Phaser.Signal(); + this.onPlay = new Phaser.Signal(); + this.onPause = new Phaser.Signal(); + this.onResume = new Phaser.Signal(); + this.onLoop = new Phaser.Signal(); + this.onStop = new Phaser.Signal(); + this.onMute = new Phaser.Signal(); } - Sound.prototype.setDecodedBuffer = function (data) { - this._buffer = data; - this.isDecoding = false; - //this.play(); - }; + Object.defineProperty(Sound.prototype, "isDecoding", { + get: function () { + return this.game.cache.getSound(this.key).isDecoding; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Sound.prototype, "isDecoded", { + get: function () { + return this.game.cache.isSoundDecoded(this.key); + }, + enumerable: true, + configurable: true + }); + Sound.prototype.addMarker = function (name, start, stop, volume, loop) { + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + this.markers[name] = { + name: name, + start: start, + stop: stop, + volume: volume, + duration: stop - start, + loop: loop + }; + }; + Sound.prototype.removeMarker = function (name) { + delete this.markers[name]; + }; + Sound.prototype.update = function () { + if(this.pendingPlayback && this.game.cache.isSoundDecoded(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.duration) { + if(this.usingWebAudio) { + if(this.loop) { + this.onLoop.dispatch(this); + this.currentTime = 0; + this.startTime = this.game.time.now; + } else { + this.stop(); + } + } else { + if(this.loop) { + this.onLoop.dispatch(this); + this.play(this.currentMarker, 0, this.volume, true, true); + } else { + this.stop(); + } + } + } + } + }; Sound.prototype.play = /** - * Play this sound. + * Play this sound, or a marked section of it. + * @param marker {string} Assets key of the sound you want to play. + * @param [volume] {number} volume of the sound you want to play. + * @param [loop] {boolean} loop when it finished playing? (Default to false) + * @return {Sound} The playing sound object. */ - function () { - if(this._buffer === null || this.isDecoding === true) { + function (marker, position, volume, loop, forceRestart) { + if (typeof marker === "undefined") { marker = ''; } + if (typeof position === "undefined") { position = 0; } + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + if (typeof forceRestart === "undefined") { forceRestart = false; } + if(this.isPlaying == true && forceRestart == false) { + // Use Restart instead return; } - this._sound = this._context.createBufferSource(); - this._sound.buffer = this._buffer; - this._sound.connect(this._localGainNode); - if(this.loop) { - this._sound.loop = true; + this.currentMarker = marker; + if(marker !== '' && this.markers[marker]) { + this.loop = this.markers[marker].loop; + this.volume = this.markers[marker].volume; + this.position = this.markers[marker].start; + this.duration = this.markers[marker].duration * 1000; + } else { + this.loop = loop; + this.volume = volume; + this.position = position; + this.duration = 0; + } + 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? + 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) { + this.duration = this.totalDuration * 1000; + } + if(this.loop) { + this._sound.loop = true; + } + // Useful to cache this somewhere perhaps? + if(typeof this._sound.start === 'undefined') { + this._sound.noteGrainOn(0, this.position, this.duration / 1000); + //this._sound.noteOn(0); // the zero is vitally important, crashes iOS6 without it + } else { + this._sound.start(0, this.position, this.duration / 1000); + } + this.isPlaying = true; + this.startTime = this.game.time.now; + this.currentTime = 0; + this.stopTime = this.startTime + this.duration; + this.onPlay.dispatch(this); + } else { + this._tempVolume = volume; + this._tempLoop = loop; + this._tempPosition = position; + this._tempMarker = marker; + this.pendingPlayback = true; + if(this.game.cache.getSound(this.key).isDecoding == false) { + this.game.sound.decode(this.key, this); + } + } + } else { + if(this._sound.readyState == 4) { + this._sound.currentTime = this.position; + this._sound.muted = this._muted; + this._sound.volume = this._volume; + this._sound.play(); + this.isPlaying = true; + this.startTime = this.game.time.now; + this.currentTime = 0; + this.stopTime = this.startTime + this.duration; + this.onPlay.dispatch(this); + } + } + }; + Sound.prototype.restart = function (marker, position, volume, loop) { + if (typeof marker === "undefined") { marker = ''; } + if (typeof position === "undefined") { position = 0; } + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + this.play(marker, position, volume, loop, true); + }; + Sound.prototype.pause = function () { + if(this.isPlaying && this._sound) { + this.stop(); + this.isPlaying = false; + this.paused = true; + this.onPause.dispatch(this); + } + }; + Sound.prototype.resume = function () { + if(this.isPlaying == false && this._sound) { + if(this.usingWebAudio) { + if(typeof this._sound.start === 'undefined') { + this._sound.noteGrainOn(0, this.position, this.duration); + //this._sound.noteOn(0); // the zero is vitally important, crashes iOS6 without it + } else { + this._sound.start(0, this.position, this.duration); + } + } else { + this._sound.play(); + } + this.isPlaying = true; + this.paused = false; + this.onResume.dispatch(this); } - this._sound.noteOn(0)// the zero is vitally important, crashes iOS6 without it - ; - this.duration = this._sound.buffer.duration; - this.isPlaying = true; }; Sound.prototype.stop = /** * Stop playing this sound. */ function () { - if(this.isPlaying === true) { + 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; - this._sound.noteOff(0); + this.currentMarker = ''; + this.onStop.dispatch(this); } }; - Sound.prototype.mute = /** - * Mute the sound. - */ - function () { - this._localGainNode.gain.value = 0; - }; - Sound.prototype.unmute = /** - * Enable the sound. - */ - function () { - this._localGainNode.gain.value = this._volume; - }; + Object.defineProperty(Sound.prototype, "mute", { + get: /** + * Mute sounds. + */ + function () { + return this._muted; + }, + set: function (value) { + if(value) { + this._muted = true; + if(this.usingWebAudio) { + this._muteVolume = this.gainNode.gain.value; + this.gainNode.gain.value = 0; + } else if(this.usingAudioTag) { + 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.volume = this._muteVolume; + } + } + this.onMute.dispatch(this); + }, + enumerable: true, + configurable: true + }); Object.defineProperty(Sound.prototype, "volume", { get: function () { return this._volume; }, set: function (value) { this._volume = value; - this._localGainNode.gain.value = this._volume; + if(this.usingWebAudio) { + this.gainNode.gain.value = value; + } else if(this.usingAudioTag) { + this._sound.volume = value; + } }, enumerable: true, configurable: true }); + Sound.prototype.renderDebug = /** + * Renders the Pointer.circle object onto the stage in green if down or red if up. + * @method renderDebug + */ + function (x, y) { + this.game.stage.context.fillStyle = 'rgb(255,255,255)'; + this.game.stage.context.font = '16px Courier'; + this.game.stage.context.fillText('Sound: ' + this.key + ' Locked: ' + this.game.sound.touchLocked + ' Pending Playback: ' + this.pendingPlayback, x, y); + this.game.stage.context.fillText('Decoded: ' + this.isDecoded + ' Decoding: ' + this.isDecoding, x, y + 20); + this.game.stage.context.fillText('Total Duration: ' + this.totalDuration + ' Playing: ' + this.isPlaying, x, y + 40); + this.game.stage.context.fillText('Time: ' + this.currentTime, x, y + 60); + this.game.stage.context.fillText('Volume: ' + this.volume + ' Muted: ' + this.mute, x, y + 80); + this.game.stage.context.fillText('WebAudio: ' + this.usingWebAudio + ' Audio: ' + this.usingAudioTag, x, y + 100); + if(this.currentMarker !== '') { + this.game.stage.context.fillText('Marker: ' + this.currentMarker + ' Duration: ' + this.duration, x, y + 120); + this.game.stage.context.fillText('Start: ' + this.markers[this.currentMarker].start + ' Stop: ' + this.markers[this.currentMarker].stop, x, y + 140); + this.game.stage.context.fillText('Position: ' + this.position, x, y + 160); + } + }; return Sound; })(); Phaser.Sound = Sound; @@ -14088,46 +14390,147 @@ var Phaser; * Create a new SoundManager. */ function SoundManager(game) { + this.usingWebAudio = false; + this.usingAudioTag = false; + this.noAudio = false; /** * Reference to AudioContext instance. */ - this._context = null; - this._game = game; - if(window['PhaserGlobal'] && window['PhaserGlobal'].disableAudio == true) { - return; + this.context = null; + this._muted = false; + this.touchLocked = false; + this._unlockSource = null; + this.onSoundDecode = new Phaser.Signal(); + this.game = game; + this._volume = 1; + this._muted = false; + this._sounds = []; + if(this.game.device.iOS && this.game.device.webAudio == false) { + this.channels = 1; } - if(game.device.webaudio == true) { - if(!!window['AudioContext']) { - this._context = new window['AudioContext'](); - } else if(!!window['webkitAudioContext']) { - this._context = new window['webkitAudioContext'](); + if(window['PhaserGlobal']) { + // Check to see if all audio playback is disabled (i.e. handled by a 3rd party class) + if(window['PhaserGlobal'].disableAudio == true) { + this.usingWebAudio = false; + this.noAudio = true; + return; } - if(this._context !== null) { - this._gainNode = this._context.createGainNode(); - this._gainNode.connect(this._context.destination); - this._volume = 1; + // Check if the Web Audio API is disabled (for testing Audio Tag playback during development) + if(window['PhaserGlobal'].disableWebAudio == true) { + this.usingWebAudio = false; + this.usingAudioTag = true; + this.noAudio = false; + return; } } + if(this.game.device.iOS && this.game.device.webAudio) { + this.game.input.touch.callbackContext = this; + this.game.input.touch.touchStartCallback = this.unlock; + this.touchLocked = true; + } else { + // What about iOS5? + this.touchLocked = false; + } + this.usingWebAudio = true; + this.noAudio = false; + if(!!window['AudioContext']) { + this.context = new window['AudioContext'](); + } else if(!!window['webkitAudioContext']) { + this.context = new window['webkitAudioContext'](); + } else if(!!window['Audio']) { + this.usingWebAudio = false; + this.usingAudioTag = true; + } else { + this.usingWebAudio = false; + this.noAudio = true; + } + if(this.context !== null) { + if(typeof this.context.createGain === 'undefined') { + this.masterGain = this.context.createGainNode(); + } else { + this.masterGain = this.context.createGain(); + } + this.masterGain.gain.value = 1; + this.masterGain.connect(this.context.destination); + } } - SoundManager.prototype.mute = /** - * Mute sounds. - */ - function () { - this._gainNode.gain.value = 0; + SoundManager.prototype.unlock = function () { + if(this.touchLocked == false || this.game.device.iOS == false) { + return; + } + // Create Audio tag? + // Create empty buffer and play it + var buffer = this.context.createBuffer(1, 1, 22050); + this._unlockSource = this.context.createBufferSource(); + this._unlockSource.buffer = buffer; + this._unlockSource.connect(this.context.destination); + this._unlockSource.noteOn(0); }; - SoundManager.prototype.unmute = /** - * Enable sounds. - */ - function () { - this._gainNode.gain.value = this._volume; - }; - Object.defineProperty(SoundManager.prototype, "volume", { - get: function () { - return this._volume; + Object.defineProperty(SoundManager.prototype, "mute", { + get: /** + * A global audio mute toggle. + */ + function () { + return this._muted; }, set: function (value) { + if(value) { + if(this._muted) { + return; + } + this._muted = true; + if(this.usingWebAudio) { + this._muteVolume = this.masterGain.gain.value; + this.masterGain.gain.value = 0; + } + // Loop through sounds + for(var i = 0; i < this._sounds.length; i++) { + if(this._sounds[i].usingAudioTag) { + this._sounds[i].mute = true; + } + } + } else { + if(this._muted == false) { + return; + } + this._muted = false; + if(this.usingWebAudio) { + this.masterGain.gain.value = this._muteVolume; + } + // Loop through sounds + for(var i = 0; i < this._sounds.length; i++) { + if(this._sounds[i].usingAudioTag) { + this._sounds[i].mute = false; + } + } + } + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(SoundManager.prototype, "volume", { + get: function () { + if(this.usingWebAudio) { + return this.masterGain.gain.value; + } else { + return this._volume; + } + }, + set: /** + * The global audio volume. A value between 0 (silence) and 1 (full volume) + */ + function (value) { + value = this.game.math.clamp(value, 1, 0); this._volume = value; - this._gainNode.gain.value = this._volume; + if(this.usingWebAudio) { + this.masterGain.gain.value = value; + } + // Loop through the sound cache and change the volume of all html audio tags + for(var i = 0; i < this._sounds.length; i++) { + if(this._sounds[i].usingAudioTag) { + this._sounds[i].volume = this._sounds[i].volume * value; + } + } }, enumerable: true, configurable: true @@ -14135,53 +14538,43 @@ var Phaser; SoundManager.prototype.decode = /** * Decode a sound with its assets key. * @param key {string} Assets key of the sound to be decoded. - * @param callback {function} This will be invoked when finished decoding. * @param [sound] {Sound} its bufer will be set to decoded data. */ - function (key, callback, sound) { - if (typeof callback === "undefined") { callback = null; } + function (key, sound) { if (typeof sound === "undefined") { sound = null; } - var soundData = this._game.cache.getSound(key); + var soundData = this.game.cache.getSoundData(key); if(soundData) { - if(this._game.cache.isSoundDecoded(key) === false) { + if(this.game.cache.isSoundDecoded(key) === false) { + this.game.cache.updateSound(key, 'isDecoding', true); var that = this; - this._context.decodeAudioData(soundData, function (buffer) { - that._game.cache.decodedSound(key, buffer); + this.context.decodeAudioData(soundData, function (buffer) { + that.game.cache.decodedSound(key, buffer); if(sound) { - sound.setDecodedBuffer(buffer); + that.onSoundDecode.dispatch(sound); } - callback(); }); } } }; - SoundManager.prototype.play = /** - * Play a sound with its assets key. - * @param key {string} Assets key of the sound you want to play. - * @param [volume] {number} volume of the sound you want to play. - * @param [loop] {boolean} loop when it finished playing? (Default to false) - * @return {Sound} The playing sound object. - */ - function (key, volume, loop) { - if (typeof volume === "undefined") { volume = 1; } - if (typeof loop === "undefined") { loop = false; } - if(this._context === null) { - return; - } - var soundData = this._game.cache.getSound(key); - if(soundData) { - // Does the sound need decoding? - if(this._game.cache.isSoundDecoded(key) === true) { - return new Phaser.Sound(this._context, this._gainNode, soundData, volume, loop); - } else { - var tempSound = new Phaser.Sound(this._context, this._gainNode, null, volume, loop); - // this is an async process, so we can return the Sound object anyway, it just won't be playing yet - this.decode(key, function () { - return tempSound.play(); - }, tempSound); - return tempSound; + SoundManager.prototype.update = function () { + if(this.touchLocked && this._unlockSource !== null) { + if((this._unlockSource.playbackState === this._unlockSource.PLAYING_STATE || this._unlockSource.playbackState === this._unlockSource.FINISHED_STATE)) { + this.touchLocked = false; + this._unlockSource = null; + this.game.input.touch.callbackContext = null; + this.game.input.touch.touchStartCallback = null; } } + for(var i = 0; i < this._sounds.length; i++) { + this._sounds[i].update(); + } + }; + SoundManager.prototype.add = function (key, volume, loop) { + if (typeof volume === "undefined") { volume = 1; } + if (typeof loop === "undefined") { loop = false; } + var sound = new Phaser.Sound(this.game, key, volume, loop); + this._sounds.push(sound); + return sound; }; return SoundManager; })(); @@ -14870,6 +15263,12 @@ var Phaser; document.addEventListener('webkitvisibilitychange', function (event) { return _this.visibilityChange(event); }, false); + document.addEventListener('pagehide', function (event) { + return _this.visibilityChange(event); + }, false); + document.addEventListener('pageshow', function (event) { + return _this.visibilityChange(event); + }, false); window.onblur = function (event) { return _this.visibilityChange(event); }; @@ -14917,7 +15316,7 @@ var Phaser; if(this.disablePauseScreen) { return; } - if(event.type == 'blur' || document['hidden'] == true || document['webkitHidden'] == true) { + if(event.type == 'pagehide' || event.type == 'blur' || document['hidden'] == true || document['webkitHidden'] == true) { if(this._game.paused == false) { this.pauseGame(); } @@ -15908,35 +16307,45 @@ var Phaser; this.webApp = false; // Audio /** - * Is audioData available? + * Are Audio tags available? * @type {boolean} */ this.audioData = false; /** - * Is webaudio available? + * Is the WebAudio API available? * @type {boolean} */ - this.webaudio = false; + this.webAudio = false; /** - * Is ogg available? + * Can this device play ogg files? * @type {boolean} */ this.ogg = false; /** - * Is mp3 available? + * Can this device play opus files? + * @type {boolean} + */ + this.opus = false; + /** + * Can this device play mp3 files? * @type {boolean} */ this.mp3 = false; /** - * Is wav available? + * Can this device play wav files? * @type {boolean} */ this.wav = false; /** - * Is m4a available? + * Can this device play m4a files? * @type {boolean} */ this.m4a = false; + /** + * Can this device play webm files? + * @type {boolean} + */ + this.webm = false; // Device /** * Is running on iPhone? @@ -16041,13 +16450,27 @@ var Phaser; this.webApp = true; } }; + Device.prototype.canPlayAudio = function (type) { + if(type == 'mp3' && this.mp3) { + return true; + } else if(type == 'ogg' && (this.ogg || this.opus)) { + return true; + } else if(type == 'm4a' && this.m4a) { + return true; + } else if(type == 'wav' && this.wav) { + return true; + } else if(type == 'webm' && this.webm) { + return true; + } + return false; + }; Device.prototype._checkAudio = /** * Check audio support. * @private */ function () { this.audioData = !!(window['Audio']); - this.webaudio = !!(window['webkitAudioContext'] || window['AudioContext']); + this.webAudio = !!(window['webkitAudioContext'] || window['AudioContext']); var audioElement = document.createElement('audio'); var result = false; try { @@ -16055,6 +16478,9 @@ var Phaser; if(audioElement.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '')) { this.ogg = true; } + if(audioElement.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, '')) { + this.opus = true; + } if(audioElement.canPlayType('audio/mpeg;').replace(/^no$/, '')) { this.mp3 = true; } @@ -16067,6 +16493,9 @@ var Phaser; if(audioElement.canPlayType('audio/x-m4a;') || audioElement.canPlayType('audio/aac;').replace(/^no$/, '')) { this.m4a = true; } + if(audioElement.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')) { + this.webm = true; + } } } catch (e) { } @@ -16653,6 +17082,12 @@ var Phaser; **/ this.totalTouches = 0; /** + * The number of miliseconds since the last click + * @property msSinceLastClick + * @type {Number} + **/ + this.msSinceLastClick = Number.MAX_VALUE; + /** * The Game Object this Pointer is currently over / touching / dragging. * @property targetObject * @type {Any} @@ -16720,6 +17155,8 @@ var Phaser; this.withinGame = true; this.isDown = true; this.isUp = false; + // Work out how long it has been since the last click + this.msSinceLastClick = this.game.time.now - this.timeDown; this.timeDown = this.game.time.now; this._holdSent = false; // This sets the x/y and other local values @@ -17777,7 +18214,7 @@ var Phaser; * @property recordPointerHistory * @type {Boolean} **/ - this.recordPointerHistory = true; + this.recordPointerHistory = false; /** * The rate in milliseconds at which the Pointer objects should update their tracking history * @property recordRate @@ -17786,7 +18223,7 @@ var Phaser; this.recordRate = 100; /** * The total number of entries that can be recorded into the Pointer objects tracking history. - * The the Pointer is tracking one event every 100ms, then a trackLimit of 100 would store the last 10 seconds worth of history. + * If the Pointer is tracking one event every 100ms, then a trackLimit of 100 would store the last 10 seconds worth of history. * @property recordLimit * @type {Number} */ @@ -19029,6 +19466,7 @@ var Phaser; /// /// /// +/// /// /// /// @@ -19199,12 +19637,12 @@ var Phaser; this.stage = new Phaser.Stage(this, parent, width, height); this.world = new Phaser.World(this, width, height); this.add = new Phaser.GameObjectFactory(this); - this.sound = new Phaser.SoundManager(this); this.cache = new Phaser.Cache(this); this.load = new Phaser.Loader(this, this.loadComplete); this.time = new Phaser.Time(this); this.tweens = new Phaser.TweenManager(this); this.input = new Phaser.Input(this); + this.sound = new Phaser.SoundManager(this); this.rnd = new Phaser.RandomDataGenerator([ (Date.now() * Math.random()).toString() ]); @@ -19266,6 +19704,7 @@ var Phaser; this.tweens.update(); this.input.update(); this.stage.update(); + this.sound.update(); if(this.onPausedCallback !== null) { this.onPausedCallback.call(this.callbackContext); } @@ -19277,6 +19716,7 @@ var Phaser; this.tweens.update(); this.input.update(); this.stage.update(); + this.sound.update(); this.physics.update(); this.world.update(); if(this._loadComplete && this.onUpdateCallback) {