From 2868b8b5888cc5cb9bd558dabc86372e1160b64c Mon Sep 17 00:00:00 2001 From: Richard Davey Date: Wed, 28 Mar 2018 14:14:07 +0100 Subject: [PATCH] Added new chainable methods: setRate, setMute, setVolume, setSeek, setDune --- CHANGELOG.md | 14 +- src/sound/html5/HTML5AudioSound.js | 600 ++++++++++++++++++++-------- src/sound/webaudio/WebAudioSound.js | 547 +++++++++++++++++++------ 3 files changed, 871 insertions(+), 290 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d3798bc5..3291a1bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,19 +11,27 @@ being passed to the simulation. The default value is 1 to remain consistent with * Matter Physics has two new methods: `set60Hz` and `set30Hz` which will set an Engine update rate of 60Hz and 30Hz respectively. 60Hz being the default. * Matter Physics has a new config and run-time property `autoUpdate`, which defaults to `true`. When enabled the Matter Engine will update in sync with the game step (set by Request Animation Frame). The delta value given to Matter is now controlled by the `getDelta` function. * Matter Physics has a new method `step` which manually advances the physics simulation by one iteration, using whatever delta and correction values you pass in to it. When used in combination with `autoUpdate=false` you can now explicitly control the update frequency of the physics simulation and unbind it from the game step. +* WebAudioSound.setMute is a chainable way to mute a single Sound instance. +* WebAudioSound.setVolume is a chainable way to set the volume of a single Sound instance. +* WebAudioSound.setSeek is a chainable way to set seek to a point of a single Sound instance. +* WebAudioSound.setLoop is a chainable way to set the loop state of a single Sound instance. +* HTML5AudioSound.setMute is a chainable way to mute a single Sound instance. +* HTML5AudioSound.setVolume is a chainable way to set the volume of a single Sound instance. +* HTML5AudioSound.setSeek is a chainable way to set seek to a point of a single Sound instance. +* HTML5AudioSound.setLoop is a chainable way to set the loop state of a single Sound instance. ### Bug Fixes * In the WebGL Render Texture the tint of the texture was always set to 0xffffff and therefore the alpha values were ignored. The tint is now calculated using the alpha value. Fix #3385 (thanks @ger1995) * The RenderTexture now uses the ComputedSize component instead of Size (which requires a frame), allowing calls to getBounds to work. Fix #3451 (thanks @kuoruan) * PathFollower.start has been renamed to `startFollow`, but PathFollower.setPath was still using `PathFollower.start` (thanks @samid737) +* BaseSoundManager.rate and BaseSoundManager.detune would incorrectly called `setRate` on its sounds, instead of `calculateRate`. ### Updates * The RTree library (rbush) used by Phaser 3 suffered from violating CSP policies by dynamically creating Functions at run-time in an eval-like manner. These are now defined via generators. Fix #3441 (thanks @jamierocks @Colbydude) - - - +* BaseSound has had its `rate` and `detune` properties removed as they are always set in the overriding class. +* BaseSound `setRate` and `setDetune` from the 3.3.0 release have moved to the WebAudioSound and HTML5AudioSound classes respectively, as they each handle the values differently. diff --git a/src/sound/html5/HTML5AudioSound.js b/src/sound/html5/HTML5AudioSound.js index 137910b24..607d4cee4 100644 --- a/src/sound/html5/HTML5AudioSound.js +++ b/src/sound/html5/HTML5AudioSound.js @@ -1,10 +1,12 @@ /** * @author Richard Davey + * @author Pavle Goloskokovic (http://prunegames.com) * @copyright 2018 Photon Storm Ltd. * @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} */ -var Class = require('../../utils/Class'); + var BaseSound = require('../BaseSound'); +var Class = require('../../utils/Class'); /** * @classdesc @@ -14,7 +16,6 @@ var BaseSound = require('../BaseSound'); * @extends Phaser.Sound.BaseSound * @memberOf Phaser.Sound * @constructor - * @author Pavle Goloskokovic (http://prunegames.com) * @since 3.0.0 * * @param {Phaser.Sound.HTML5AudioSoundManager} manager - Reference to the current sound manager instance. @@ -22,10 +23,14 @@ var BaseSound = require('../BaseSound'); * @param {SoundConfig} [config={}] - An optional config object containing default sound settings. */ var HTML5AudioSound = new Class({ + Extends: BaseSound, - initialize: function HTML5AudioSound (manager, key, config) + + initialize: + + function HTML5AudioSound (manager, key, config) { - if (config === void 0) { config = {}; } + if (config === undefined) { config = {}; } /** * An array containing all HTML5 Audio tags that could be used for individual @@ -38,6 +43,7 @@ var HTML5AudioSound = new Class({ * @since 3.0.0 */ this.tags = manager.game.cache.audio.get(key); + if (!this.tags) { // eslint-disable-next-line no-console @@ -80,17 +86,26 @@ var HTML5AudioSound = new Class({ * @since 3.0.0 */ this.previousTime = 0; + this.duration = this.tags[0].duration; + this.totalDuration = this.tags[0].duration; + BaseSound.call(this, manager, key, config); }, + /** + * @event Phaser.Sound.HTML5AudioSound#playEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + */ + /** * Play this sound, or a marked section of it. * It always plays the sound from the start. If you want to start playback from a specific time * you can set 'seek' setting of the config object, provided to this call, to that value. * * @method Phaser.Sound.HTML5AudioSound#play + * @fires Phaser.Sound.HTML5AudioSound#playEvent * @since 3.0.0 * * @param {string} [markerName=''] - If you want to play a marker then provide the marker name here, otherwise omit it to play the full sound. @@ -115,18 +130,21 @@ var HTML5AudioSound = new Class({ return false; } - /** - * @event Phaser.Sound.HTML5AudioSound#play - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - */ this.emit('play', this); + return true; }, + /** + * @event Phaser.Sound.HTML5AudioSound#pauseEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + */ + /** * Pauses the sound. * * @method Phaser.Sound.HTML5AudioSound#pause + * @fires Phaser.Sound.HTML5AudioSound#pauseEvent * @since 3.0.0 * * @return {boolean} Whether the sound was paused successfully. @@ -137,32 +155,37 @@ var HTML5AudioSound = new Class({ { return false; } + if (this.startTime > 0) { return false; } + if (!BaseSound.prototype.pause.call(this)) { return false; } // \/\/\/ isPlaying = false, isPaused = true \/\/\/ - this.currentConfig.seek = this.audio.currentTime - - (this.currentMarker ? this.currentMarker.start : 0); + this.currentConfig.seek = this.audio.currentTime - (this.currentMarker ? this.currentMarker.start : 0); + this.stopAndReleaseAudioTag(); - /** - * @event Phaser.Sound.HTML5AudioSound#pause - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - */ this.emit('pause', this); + return true; }, + /** + * @event Phaser.Sound.HTML5AudioSound#resumeEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + */ + /** * Resumes the sound. * * @method Phaser.Sound.HTML5AudioSound#resume + * @fires Phaser.Sound.HTML5AudioSound#resumeEvent * @since 3.0.0 * * @return {boolean} Whether the sound was resumed successfully. @@ -173,10 +196,12 @@ var HTML5AudioSound = new Class({ { return false; } + if (this.startTime > 0) { return false; } + if (!BaseSound.prototype.resume.call(this)) { return false; @@ -188,18 +213,21 @@ var HTML5AudioSound = new Class({ return false; } - /** - * @event Phaser.Sound.HTML5AudioSound#resume - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - */ this.emit('resume', this); + return true; }, + /** + * @event Phaser.Sound.HTML5AudioSound#stopEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + */ + /** * Stop playing this sound. * * @method Phaser.Sound.HTML5AudioSound#stop + * @fires Phaser.Sound.HTML5AudioSound#stopEvent * @since 3.0.0 * * @return {boolean} Whether the sound was stopped successfully. @@ -210,6 +238,7 @@ var HTML5AudioSound = new Class({ { return false; } + if (!BaseSound.prototype.stop.call(this)) { return false; @@ -218,11 +247,8 @@ var HTML5AudioSound = new Class({ // \/\/\/ isPlaying = false, isPaused = false \/\/\/ this.stopAndReleaseAudioTag(); - /** - * @event Phaser.Sound.HTML5AudioSound#stop - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - */ this.emit('stop', this); + return true; }, @@ -242,15 +268,19 @@ var HTML5AudioSound = new Class({ this.reset(); return false; } + var seek = this.currentConfig.seek; var delay = this.currentConfig.delay; var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek; + this.previousTime = offset; this.audio.currentTime = offset; this.applyConfig(); + if (delay === 0) { this.startTime = 0; + if (this.audio.paused) { this.playCatchPromise(); @@ -259,12 +289,15 @@ var HTML5AudioSound = new Class({ else { this.startTime = window.performance.now() + delay * 1000; + if (!this.audio.paused) { this.audio.pause(); } } + this.resetConfig(); + return true; }, @@ -287,9 +320,11 @@ var HTML5AudioSound = new Class({ { return true; } + for (var i = 0; i < this.tags.length; i++) { var audio = this.tags[i]; + if (audio.dataset.used === 'false') { audio.dataset.used = 'true'; @@ -297,11 +332,14 @@ var HTML5AudioSound = new Class({ return true; } } + if (!this.manager.override) { return false; } + var otherSounds = []; + this.manager.forEachActiveSound(function (sound) { if (sound.key === this.key && sound.audio) @@ -309,6 +347,7 @@ var HTML5AudioSound = new Class({ otherSounds.push(sound); } }, this); + otherSounds.sort(function (a1, a2) { if (a1.loop === a2.loop) @@ -318,12 +357,16 @@ var HTML5AudioSound = new Class({ } return a1.loop ? 1 : -1; }); + var selectedSound = otherSounds[0]; + this.audio = selectedSound.audio; + selectedSound.reset(); selectedSound.audio = null; selectedSound.startTime = 0; selectedSound.previousTime = 0; + return true; }, @@ -338,6 +381,7 @@ var HTML5AudioSound = new Class({ playCatchPromise: function () { var playPromise = this.audio.play(); + if (playPromise) { // eslint-disable-next-line no-unused-vars @@ -389,9 +433,11 @@ var HTML5AudioSound = new Class({ { this.isPlaying = false; this.isPaused = true; - this.currentConfig.seek = this.audio.currentTime - - (this.currentMarker ? this.currentMarker.start : 0); + + this.currentConfig.seek = this.audio.currentTime - (this.currentMarker ? this.currentMarker.start : 0); + this.currentConfig.delay = Math.max(0, (this.startTime - window.performance.now()) / 1000); + this.stopAndReleaseAudioTag(); }, @@ -410,10 +456,22 @@ var HTML5AudioSound = new Class({ this.pickAndPlayAudioTag(); }, + /** + * @event Phaser.Sound.HTML5AudioSound#loopedEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + */ + + /** + * @event Phaser.Sound.HTML5AudioSound#endedEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + */ + /** * Update method called automatically by sound manager on every game step. * * @method Phaser.Sound.HTML5AudioSound#update + * @fires Phaser.Sound.HTML5AudioSound#loopedEvent + * @fires Phaser.Sound.HTML5AudioSound#endedEvent * @protected * @since 3.0.0 * @@ -438,6 +496,7 @@ var HTML5AudioSound = new Class({ this.previousTime = this.audio.currentTime; this.playCatchPromise(); } + return; } @@ -445,6 +504,7 @@ var HTML5AudioSound = new Class({ var startTime = this.currentMarker ? this.currentMarker.start : 0; var endTime = startTime + this.duration; var currentTime = this.audio.currentTime; + if (this.currentConfig.loop) { if (currentTime >= endTime - this.manager.loopEndOffset) @@ -457,27 +517,23 @@ var HTML5AudioSound = new Class({ this.audio.currentTime += startTime; currentTime = this.audio.currentTime; } + if (currentTime < this.previousTime) { - /** - * @event Phaser.Sound.HTML5AudioSound#looped - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - */ this.emit('looped', this); } } else if (currentTime >= endTime) { this.reset(); + this.stopAndReleaseAudioTag(); - /** - * @event Phaser.Sound.HTML5AudioSound#ended - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - */ this.emit('ended', this); + return; } + this.previousTime = currentTime; }, @@ -491,7 +547,9 @@ var HTML5AudioSound = new Class({ destroy: function () { BaseSound.prototype.destroy.call(this); + this.tags = null; + if (this.audio) { this.stopAndReleaseAudioTag(); @@ -501,11 +559,11 @@ var HTML5AudioSound = new Class({ /** * Method used internally to determine mute setting of the sound. * - * @method Phaser.Sound.HTML5AudioSound#setMute + * @method Phaser.Sound.HTML5AudioSound#updateMute * @private * @since 3.0.0 */ - setMute: function () + updateMute: function () { if (this.audio) { @@ -516,11 +574,11 @@ var HTML5AudioSound = new Class({ /** * Method used internally to calculate total volume of the sound. * - * @method Phaser.Sound.HTML5AudioSound#setVolume + * @method Phaser.Sound.HTML5AudioSound#updateVolume * @private * @since 3.0.0 */ - setVolume: function () + updateVolume: function () { if (this.audio) { @@ -531,165 +589,387 @@ var HTML5AudioSound = new Class({ /** * Method used internally to calculate total playback rate of the sound. * - * @method Phaser.Sound.HTML5AudioSound#setRate + * @method Phaser.Sound.HTML5AudioSound#calculateRate * @protected * @since 3.0.0 */ - setRate: function () + calculateRate: function () { - BaseSound.prototype.setRate.call(this); + BaseSound.prototype.calculateRate.call(this); + if (this.audio) { this.audio.playbackRate = this.totalRate; } - } -}); -Object.defineProperty(HTML5AudioSound.prototype, 'mute', { - get: function () - { - return this.currentConfig.mute; }, - set: function (value) - { - this.currentConfig.mute = value; - if (this.manager.isLocked(this, 'mute', value)) - { - return; - } - this.setMute(); - /** - * @event Phaser.Sound.HTML5AudioSound#mute - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - * @param {boolean} value - An updated value of Phaser.Sound.HTML5AudioSound#mute property. - */ - this.emit('mute', this, value); - } -}); -Object.defineProperty(HTML5AudioSound.prototype, 'volume', { - get: function () - { - return this.currentConfig.volume; - }, - set: function (value) - { - this.currentConfig.volume = value; - if (this.manager.isLocked(this, 'volume', value)) - { - return; - } - this.setVolume(); + /** + * @event Phaser.Sound.HTML5AudioSound#muteEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + * @param {boolean} value - An updated value of Phaser.Sound.HTML5AudioSound#mute property. + */ - /** - * @event Phaser.Sound.HTML5AudioSound#volume - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - * @param {number} value - An updated value of Phaser.Sound.HTML5AudioSound#volume property. - */ - this.emit('volume', this, value); - } -}); -Object.defineProperty(HTML5AudioSound.prototype, 'rate', { - get: function () - { - return Object.getOwnPropertyDescriptor(BaseSound.prototype, 'rate').get.call(this); - }, - set: function (value) - { - this.currentConfig.rate = value; - if (this.manager.isLocked(this, 'rate', value)) + /** + * [description] + * + * @name Phaser.Sound.HTML5AudioSound#mute + * @type {number} + * @default 1 + * @since 3.0.0 + */ + mute: { + + get: function () { - return; - } - Object.getOwnPropertyDescriptor(BaseSound.prototype, 'rate').set.call(this, value); - } -}); -Object.defineProperty(HTML5AudioSound.prototype, 'detune', { - get: function () - { - return Object.getOwnPropertyDescriptor(BaseSound.prototype, 'detune').get.call(this); - }, - set: function (value) - { - this.currentConfig.detune = value; - if (this.manager.isLocked(this, 'detune', value)) + return this.currentConfig.mute; + }, + + set: function (value) { - return; - } - Object.getOwnPropertyDescriptor(BaseSound.prototype, 'detune').set.call(this, value); - } -}); -Object.defineProperty(HTML5AudioSound.prototype, 'seek', { - get: function () - { - if (this.isPlaying) - { - return this.audio.currentTime - - (this.currentMarker ? this.currentMarker.start : 0); - } - else if (this.isPaused) - { - return this.currentConfig.seek; - } - else - { - return 0; + this.currentConfig.mute = value; + + if (this.manager.isLocked(this, 'mute', value)) + { + return; + } + + this.setMute(); + + this.emit('mute', this, value); } }, - set: function (value) + + /** + * Sets the muted state of this Sound. + * + * @method Phaser.Sound.HTML5AudioSound#setMute + * @fires Phaser.Sound.HTML5AudioSound#muteEvent + * @since 3.4.0 + * + * @param {boolean} value - `true` to mute this sound, `false` to unmute it. + * + * @return {Phaser.Sound.HTML5AudioSound} This Sound instance. + */ + setMute: function (value) { - if (this.manager.isLocked(this, 'seek', value)) + this.mute = value; + + return this; + }, + + /** + * @event Phaser.Sound.HTML5AudioSound#volumeEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + * @param {number} value - An updated value of Phaser.Sound.HTML5AudioSound#volume property. + */ + + /** + * [description] + * + * @name Phaser.Sound.HTML5AudioSound#volume + * @type {number} + * @default 1 + * @since 3.0.0 + */ + volume: { + + get: function () { - return; + return this.currentConfig.volume; + }, + + set: function (value) + { + this.currentConfig.volume = value; + + if (this.manager.isLocked(this, 'volume', value)) + { + return; + } + + this.setVolume(); + + this.emit('volume', this, value); } - if (this.startTime > 0) + }, + + /** + * Sets the volume of this Sound. + * + * @method Phaser.Sound.HTML5AudioSound#setVolume + * @fires Phaser.Sound.HTML5AudioSound#volumeEvent + * @since 3.4.0 + * + * @param {number} value - The volume of the sound. + * + * @return {Phaser.Sound.HTML5AudioSound} This Sound instance. + */ + setVolume: function (value) + { + this.volume = value; + + return this; + }, + + /** + * @event Phaser.Sound.HTML5AudioSound#rateEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted the event. + * @param {number} value - An updated value of Phaser.Sound.HTML5AudioSound#rate property. + */ + + /** + * Rate at which this Sound will be played. + * Value of 1.0 plays the audio at full speed, 0.5 plays the audio at half speed + * and 2.0 doubles the audios playback speed. + * + * @name Phaser.Sound.HTML5AudioSound#rate + * @type {number} + * @default 1 + * @since 3.0.0 + */ + rate: { + + get: function () { - return; + return this.currentConfig.rate; + }, + + set: function (value) + { + this.currentConfig.rate = value; + + if (this.manager.isLocked(this, 'rate', value)) + { + return; + } + else + { + this.calculateRate(); + + this.emit('rate', this, value); + } } - if (this.isPlaying || this.isPaused) + + }, + + /** + * Sets the playback rate of this Sound. + * + * For example, a value of 1.0 plays the audio at full speed, 0.5 plays the audio at half speed + * and 2.0 doubles the audios playback speed. + * + * @method Phaser.Sound.HTML5AudioSound#setRate + * @fires Phaser.Sound.HTML5AudioSound#rateEvent + * @since 3.3.0 + * + * @param {number} value - The playback rate at of this Sound. + * + * @return {Phaser.Sound.HTML5AudioSound} This Sound. + */ + setRate: function (value) + { + this.rate = value; + + return this; + }, + + /** + * @event Phaser.Sound.HTML5AudioSound#detuneEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the Sound that emitted event. + * @param {number} value - An updated value of Phaser.Sound.HTML5AudioSound#detune property. + */ + + /** + * The detune value of this Sound, given in [cents](https://en.wikipedia.org/wiki/Cent_%28music%29). + * The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent). + * + * @name Phaser.Sound.HTML5AudioSound#detune + * @type {number} + * @default 0 + * @since 3.0.0 + */ + detune: { + + get: function () + { + return this.currentConfig.detune; + }, + + set: function (value) + { + this.currentConfig.detune = value; + + if (this.manager.isLocked(this, 'detune', value)) + { + return; + } + else + { + this.calculateRate(); + + this.emit('detune', this, value); + } + } + + }, + + /** + * Sets the detune value of this Sound, given in [cents](https://en.wikipedia.org/wiki/Cent_%28music%29). + * The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent). + * + * @method Phaser.Sound.HTML5AudioSound#setDetune + * @fires Phaser.Sound.HTML5AudioSound#detuneEvent + * @since 3.3.0 + * + * @param {number} value - The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent). + * + * @return {Phaser.Sound.HTML5AudioSound} This Sound. + */ + setDetune: function (value) + { + this.detune = value; + + return this; + }, + + /** + * @event Phaser.Sound.HTML5AudioSound#seekEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + * @param {number} value - An updated value of Phaser.Sound.HTML5AudioSound#seek property. + */ + + /** + * [description] + * + * @name Phaser.Sound.HTML5AudioSound#seek + * @type {number} + * @since 3.0.0 + */ + seek: { + + get: function () { - value = Math.min(Math.max(0, value), this.duration); if (this.isPlaying) { - this.previousTime = value; - this.audio.currentTime = value; + return this.audio.currentTime - (this.currentMarker ? this.currentMarker.start : 0); } else if (this.isPaused) { - this.currentConfig.seek = value; + return this.currentConfig.seek; + } + else + { + return 0; + } + }, + + set: function (value) + { + if (this.manager.isLocked(this, 'seek', value)) + { + return; } - /** - * @event Phaser.Sound.HTML5AudioSound#seek - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - * @param {number} value - An updated value of Phaser.Sound.HTML5AudioSound#seek property. - */ - this.emit('seek', this, value); + if (this.startTime > 0) + { + return; + } + + if (this.isPlaying || this.isPaused) + { + value = Math.min(Math.max(0, value), this.duration); + + if (this.isPlaying) + { + this.previousTime = value; + this.audio.currentTime = value; + } + else if (this.isPaused) + { + this.currentConfig.seek = value; + } + + this.emit('seek', this, value); + } } - } -}); -Object.defineProperty(HTML5AudioSound.prototype, 'loop', { - get: function () - { - return this.currentConfig.loop; }, - set: function (value) + + /** + * Seeks to a specific point in this sound. + * + * @method Phaser.Sound.HTML5AudioSound#setSeek + * @fires Phaser.Sound.HTML5AudioSound#seekEvent + * @since 3.4.0 + * + * @param {number} value - The point in the sound to seek to. + * + * @return {Phaser.Sound.HTML5AudioSound} This Sound instance. + */ + setSeek: function (value) { - this.currentConfig.loop = value; - if (this.manager.isLocked(this, 'loop', value)) + this.seek = value; + + return this; + }, + + /** + * @event Phaser.Sound.HTML5AudioSound#loopEvent + * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. + * @param {boolean} value - An updated value of Phaser.Sound.HTML5AudioSound#loop property. + */ + + /** + * [description] + * + * @name Phaser.Sound.HTML5AudioSound#loop + * @type {boolean} + * @default false + * @since 3.0.0 + */ + loop: { + + get: function () { - return; - } - if (this.audio) + return this.currentConfig.loop; + }, + + set: function (value) { - this.audio.loop = value; + this.currentConfig.loop = value; + + if (this.manager.isLocked(this, 'loop', value)) + { + return; + } + + if (this.audio) + { + this.audio.loop = value; + } + + this.emit('loop', this, value); } - /** - * @event Phaser.Sound.HTML5AudioSound#loop - * @param {Phaser.Sound.HTML5AudioSound} sound - Reference to the sound that emitted event. - * @param {boolean} value - An updated value of Phaser.Sound.HTML5AudioSound#loop property. - */ - this.emit('loop', this, value); + }, + + /** + * Sets the loop state of this Sound. + * + * @method Phaser.Sound.HTML5AudioSound#setLoop + * @fires Phaser.Sound.HTML5AudioSound#loopEvent + * @since 3.4.0 + * + * @param {boolean} value - `true` to loop this sound, `false` to not loop it. + * + * @return {Phaser.Sound.HTML5AudioSound} This Sound instance. + */ + setLoop: function (value) + { + this.loop = value; + + return this; } + }); + module.exports = HTML5AudioSound; diff --git a/src/sound/webaudio/WebAudioSound.js b/src/sound/webaudio/WebAudioSound.js index 964ae6681..dfc4038af 100644 --- a/src/sound/webaudio/WebAudioSound.js +++ b/src/sound/webaudio/WebAudioSound.js @@ -1,10 +1,12 @@ /** * @author Richard Davey + * @author Pavle Goloskokovic (http://prunegames.com) * @copyright 2018 Photon Storm Ltd. * @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} */ -var Class = require('../../utils/Class'); + var BaseSound = require('../BaseSound'); +var Class = require('../../utils/Class'); /** * @classdesc @@ -14,7 +16,6 @@ var BaseSound = require('../BaseSound'); * @extends Phaser.Sound.BaseSound * @memberOf Phaser.Sound * @constructor - * @author Pavle Goloskokovic (http://prunegames.com) * @since 3.0.0 * * @param {Phaser.Sound.WebAudioSoundManager} manager - Reference to the current sound manager instance. @@ -22,10 +23,14 @@ var BaseSound = require('../BaseSound'); * @param {SoundConfig} [config={}] - An optional config object containing default sound settings. */ var WebAudioSound = new Class({ + Extends: BaseSound, - initialize: function WebAudioSound (manager, key, config) + + initialize: + + function WebAudioSound (manager, key, config) { - if (config === void 0) { config = {}; } + if (config === undefined) { config = {}; } /** * Audio buffer containing decoded data of the audio asset to be played. @@ -36,6 +41,7 @@ var WebAudioSound = new Class({ * @since 3.0.0 */ this.audioBuffer = manager.game.cache.audio.get(key); + if (!this.audioBuffer) { // eslint-disable-next-line no-console @@ -157,19 +163,31 @@ var WebAudioSound = new Class({ * @since 3.0.0 */ this.hasLooped = false; + this.muteNode.connect(this.volumeNode); + this.volumeNode.connect(manager.destination); + this.duration = this.audioBuffer.duration; + this.totalDuration = this.audioBuffer.duration; + BaseSound.call(this, manager, key, config); }, + /** + * @event Phaser.Sound.WebAudioSound#playEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the Sound that emitted event. + */ + /** * Play this sound, or a marked section of it. + * * It always plays the sound from the start. If you want to start playback from a specific time * you can set 'seek' setting of the config object, provided to this call, to that value. * * @method Phaser.Sound.WebAudioSound#play + * @fires Phaser.Sound.WebAudioSound#playEvent * @since 3.0.0 * * @param {string} [markerName=''] - If you want to play a marker then provide the marker name here, otherwise omit it to play the full sound. @@ -188,18 +206,21 @@ var WebAudioSound = new Class({ this.stopAndRemoveBufferSource(); this.createAndStartBufferSource(); - /** - * @event Phaser.Sound.WebAudioSound#play - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - */ this.emit('play', this); + return true; }, + /** + * @event Phaser.Sound.WebAudioSound#pauseEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the Sound that emitted event. + */ + /** * Pauses the sound. * * @method Phaser.Sound.WebAudioSound#pause + * @fires Phaser.Sound.WebAudioSound#pauseEvent * @since 3.0.0 * * @return {boolean} Whether the sound was paused successfully. @@ -210,6 +231,7 @@ var WebAudioSound = new Class({ { return false; } + if (!BaseSound.prototype.pause.call(this)) { return false; @@ -219,18 +241,21 @@ var WebAudioSound = new Class({ this.currentConfig.seek = this.getCurrentTime(); // Equivalent to setting paused time this.stopAndRemoveBufferSource(); - /** - * @event Phaser.Sound.WebAudioSound#pause - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - */ this.emit('pause', this); + return true; }, + /** + * @event Phaser.Sound.WebAudioSound#resumeEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the Sound that emitted event. + */ + /** * Resumes the sound. * * @method Phaser.Sound.WebAudioSound#resume + * @fires Phaser.Sound.WebAudioSound#resumeEvent * @since 3.0.0 * * @return {boolean} Whether the sound was resumed successfully. @@ -241,6 +266,7 @@ var WebAudioSound = new Class({ { return false; } + if (!BaseSound.prototype.resume.call(this)) { return false; @@ -249,18 +275,21 @@ var WebAudioSound = new Class({ // \/\/\/ isPlaying = true, isPaused = false \/\/\/ this.createAndStartBufferSource(); - /** - * @event Phaser.Sound.WebAudioSound#resume - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - */ this.emit('resume', this); + return true; }, + /** + * @event Phaser.Sound.WebAudioSound#stopEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the Sound that emitted event. + */ + /** * Stop playing this sound. * * @method Phaser.Sound.WebAudioSound#stop + * @fires Phaser.Sound.WebAudioSound#stopEvent * @since 3.0.0 * * @return {boolean} Whether the sound was stopped successfully. @@ -275,16 +304,13 @@ var WebAudioSound = new Class({ // \/\/\/ isPlaying = false, isPaused = false \/\/\/ this.stopAndRemoveBufferSource(); - /** - * @event Phaser.Sound.WebAudioSound#stop - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - */ this.emit('stop', this); + return true; }, /** - * Used internally to do what the name says. + * Used internally. * * @method Phaser.Sound.WebAudioSound#createAndStartBufferSource * @private @@ -297,16 +323,20 @@ var WebAudioSound = new Class({ var when = this.manager.context.currentTime + delay; var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek; var duration = this.duration - seek; + this.playTime = when - seek; this.startTime = when; this.source = this.createBufferSource(); + this.applyConfig(); + this.source.start(Math.max(0, when), Math.max(0, offset), Math.max(0, duration)); + this.resetConfig(); }, /** - * Used internally to do what the name says. + * Used internally. * * @method Phaser.Sound.WebAudioSound#createAndStartLoopBufferSource * @private @@ -317,6 +347,7 @@ var WebAudioSound = new Class({ var when = this.getLoopTime(); var offset = this.currentMarker ? this.currentMarker.start : 0; var duration = this.duration; + this.loopTime = when; this.loopSource = this.createBufferSource(); this.loopSource.playbackRate.setValueAtTime(this.totalRate, 0); @@ -324,7 +355,7 @@ var WebAudioSound = new Class({ }, /** - * Used internally to do what the name says. + * Used internally. * * @method Phaser.Sound.WebAudioSound#createBufferSource * @private @@ -336,8 +367,11 @@ var WebAudioSound = new Class({ { var _this = this; var source = this.manager.context.createBufferSource(); + source.buffer = this.audioBuffer; + source.connect(this.muteNode); + source.onended = function (ev) { if (ev.target === _this.source) @@ -355,11 +389,12 @@ var WebAudioSound = new Class({ // else was stopped }; + return source; }, /** - * Used internally to do what the name says. + * Used internally. * * @method Phaser.Sound.WebAudioSound#stopAndRemoveBufferSource * @private @@ -373,13 +408,15 @@ var WebAudioSound = new Class({ this.source.disconnect(); this.source = null; } + this.playTime = 0; this.startTime = 0; + this.stopAndRemoveLoopBufferSource(); }, /** - * Used internally to do what the name says. + * Used internally. * * @method Phaser.Sound.WebAudioSound#stopAndRemoveLoopBufferSource * @private @@ -393,6 +430,7 @@ var WebAudioSound = new Class({ this.loopSource.disconnect(); this.loopSource = null; } + this.loopTime = 0; }, @@ -406,17 +444,31 @@ var WebAudioSound = new Class({ applyConfig: function () { this.rateUpdates.length = 0; + this.rateUpdates.push({ time: 0, rate: 1 }); + BaseSound.prototype.applyConfig.call(this); }, + /** + * @event Phaser.Sound.WebAudioSound#endedEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. + */ + + /** + * @event Phaser.Sound.WebAudioSound#loopedEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. + */ + /** * Update method called automatically by sound manager on every game step. * * @method Phaser.Sound.WebAudioSound#update + * @fires Phaser.Sound.WebAudioSound#endedEvent + * @fires Phaser.Sound.WebAudioSound#loopedEvent * @protected * @since 3.0.0 * @@ -429,13 +481,11 @@ var WebAudioSound = new Class({ if (this.hasEnded) { this.hasEnded = false; + BaseSound.prototype.stop.call(this); + this.stopAndRemoveBufferSource(); - /** - * @event Phaser.Sound.WebAudioSound#ended - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - */ this.emit('ended', this); } else if (this.hasLooped) @@ -445,16 +495,14 @@ var WebAudioSound = new Class({ this.loopSource = null; this.playTime = this.startTime = this.loopTime; this.rateUpdates.length = 0; + this.rateUpdates.push({ time: 0, rate: this.totalRate }); + this.createAndStartLoopBufferSource(); - /** - * @event Phaser.Sound.WebAudioSound#looped - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - */ this.emit('looped', this); } }, @@ -469,6 +517,7 @@ var WebAudioSound = new Class({ destroy: function () { BaseSound.prototype.destroy.call(this); + this.audioBuffer = null; this.stopAndRemoveBufferSource(); this.muteNode.disconnect(); @@ -482,24 +531,28 @@ var WebAudioSound = new Class({ /** * Method used internally to calculate total playback rate of the sound. * - * @method Phaser.Sound.WebAudioSound#setRate + * @method Phaser.Sound.WebAudioSound#calculateRate * @protected * @since 3.0.0 */ - setRate: function () + calculateRate: function () { - BaseSound.prototype.setRate.call(this); + BaseSound.prototype.calculateRate.call(this); + var now = this.manager.context.currentTime; + if (this.source) { this.source.playbackRate.setValueAtTime(this.totalRate, now); } + if (this.isPlaying) { this.rateUpdates.push({ time: Math.max(this.startTime, now) - this.playTime, rate: this.totalRate }); + if (this.loopSource) { this.stopAndRemoveLoopBufferSource(); @@ -518,9 +571,11 @@ var WebAudioSound = new Class({ getCurrentTime: function () { var currentTime = 0; + for (var i = 0; i < this.rateUpdates.length; i++) { - var nextTime = void 0; + var nextTime = 0; + if (i < this.rateUpdates.length - 1) { nextTime = this.rateUpdates[i + 1].time; @@ -529,8 +584,10 @@ var WebAudioSound = new Class({ { nextTime = this.manager.context.currentTime - this.playTime; } + currentTime += (nextTime - this.rateUpdates[i].time) * this.rateUpdates[i].rate; } + return currentTime; }, @@ -545,120 +602,356 @@ var WebAudioSound = new Class({ getLoopTime: function () { var lastRateUpdateCurrentTime = 0; + for (var i = 0; i < this.rateUpdates.length - 1; i++) { - lastRateUpdateCurrentTime += - (this.rateUpdates[i + 1].time - this.rateUpdates[i].time) * this.rateUpdates[i].rate; + lastRateUpdateCurrentTime += (this.rateUpdates[i + 1].time - this.rateUpdates[i].time) * this.rateUpdates[i].rate; } + var lastRateUpdate = this.rateUpdates[this.rateUpdates.length - 1]; - return this.playTime + lastRateUpdate.time - + (this.duration - lastRateUpdateCurrentTime) / lastRateUpdate.rate; - } -}); -Object.defineProperty(WebAudioSound.prototype, 'mute', { - get: function () - { - return this.muteNode.gain.value === 0; - }, - set: function (value) - { - this.currentConfig.mute = value; - this.muteNode.gain.setValueAtTime(value ? 0 : 1, 0); - /** - * @event Phaser.Sound.WebAudioSound#mute - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - * @param {boolean} value - An updated value of Phaser.Sound.WebAudioSound#mute property. - */ - this.emit('mute', this, value); - } -}); -Object.defineProperty(WebAudioSound.prototype, 'volume', { - get: function () - { - return this.volumeNode.gain.value; + return this.playTime + lastRateUpdate.time + (this.duration - lastRateUpdateCurrentTime) / lastRateUpdate.rate; }, - set: function (value) - { - this.currentConfig.volume = value; - this.volumeNode.gain.setValueAtTime(value, 0); - /** - * @event Phaser.Sound.WebAudioSound#volume - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - * @param {number} value - An updated value of Phaser.Sound.WebAudioSound#volume property. - */ - this.emit('volume', this, value); - } -}); -Object.defineProperty(WebAudioSound.prototype, 'seek', { - get: function () + /** + * @event Phaser.Sound.WebAudioSound#rateEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted the event. + * @param {number} value - An updated value of Phaser.Sound.WebAudioSound#rate property. + */ + + /** + * Rate at which this Sound will be played. + * Value of 1.0 plays the audio at full speed, 0.5 plays the audio at half speed + * and 2.0 doubles the audios playback speed. + * + * @name Phaser.Sound.WebAudioSound#rate + * @type {number} + * @default 1 + * @since 3.0.0 + */ + rate: { + + get: function () + { + return this.currentConfig.rate; + }, + + set: function (value) + { + this.currentConfig.rate = value; + + this.calculateRate(); + + this.emit('rate', this, value); + } + + }, + + /** + * Sets the playback rate of this Sound. + * + * For example, a value of 1.0 plays the audio at full speed, 0.5 plays the audio at half speed + * and 2.0 doubles the audios playback speed. + * + * @method Phaser.Sound.WebAudioSound#setRate + * @fires Phaser.Sound.WebAudioSound#rateEvent + * @since 3.3.0 + * + * @param {number} value - The playback rate at of this Sound. + * + * @return {Phaser.Sound.WebAudioSound} This Sound. + */ + setRate: function (value) { - if (this.isPlaying) + this.rate = value; + + return this; + }, + + /** + * @event Phaser.Sound.WebAudioSound#detuneEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the Sound that emitted event. + * @param {number} value - An updated value of Phaser.Sound.WebAudioSound#detune property. + */ + + /** + * The detune value of this Sound, given in [cents](https://en.wikipedia.org/wiki/Cent_%28music%29). + * The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent). + * + * @name Phaser.Sound.WebAudioSound#detune + * @type {number} + * @default 0 + * @since 3.0.0 + */ + detune: { + + get: function () + { + return this.currentConfig.detune; + }, + + set: function (value) + { + this.currentConfig.detune = value; + + this.calculateRate(); + + this.emit('detune', this, value); + } + + }, + + /** + * Sets the detune value of this Sound, given in [cents](https://en.wikipedia.org/wiki/Cent_%28music%29). + * The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent). + * + * @method Phaser.Sound.WebAudioSound#setDetune + * @fires Phaser.Sound.WebAudioSound#detuneEvent + * @since 3.3.0 + * + * @param {number} value - The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent). + * + * @return {Phaser.Sound.WebAudioSound} This Sound. + */ + setDetune: function (value) + { + this.detune = value; + + return this; + }, + + /** + * @event Phaser.Sound.WebAudioSound#muteEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. + * @param {boolean} value - An updated value of Phaser.Sound.WebAudioSound#mute property. + */ + + /** + * [description] + * + * @name Phaser.Sound.WebAudioSound#mute + * @type {number} + * @default 1 + * @since 3.0.0 + */ + mute: { + + get: function () + { + return (this.muteNode.gain.value === 0); + }, + + set: function (value) + { + this.currentConfig.mute = value; + this.muteNode.gain.setValueAtTime(value ? 0 : 1, 0); + + this.emit('mute', this, value); + } + + }, + + /** + * Sets the muted state of this Sound. + * + * @method Phaser.Sound.WebAudioSound#setMute + * @fires Phaser.Sound.WebAudioSound#muteEvent + * @since 3.4.0 + * + * @param {boolean} value - `true` to mute this sound, `false` to unmute it. + * + * @return {Phaser.Sound.WebAudioSound} This Sound instance. + */ + setMute: function (value) + { + this.mute = value; + + return this; + }, + + /** + * @event Phaser.Sound.WebAudioSound#volumeEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. + * @param {number} value - An updated value of Phaser.Sound.WebAudioSound#volume property. + */ + + /** + * [description] + * + * @name Phaser.Sound.WebAudioSound#volume + * @type {number} + * @default 1 + * @since 3.0.0 + */ + volume: { + + get: function () + { + return this.volumeNode.gain.value; + }, + + set: function (value) + { + this.currentConfig.volume = value; + this.volumeNode.gain.setValueAtTime(value, 0); + + this.emit('volume', this, value); + } + }, + + /** + * Sets the volume of this Sound. + * + * @method Phaser.Sound.WebAudioSound#setVolume + * @fires Phaser.Sound.WebAudioSound#volumeEvent + * @since 3.4.0 + * + * @param {number} value - The volume of the sound. + * + * @return {Phaser.Sound.WebAudioSound} This Sound instance. + */ + setVolume: function (value) + { + this.volume = value; + + return this; + }, + + /** + * @event Phaser.Sound.WebAudioSound#seekEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. + * @param {number} value - An updated value of Phaser.Sound.WebAudioSound#seek property. + */ + + /** + * [description] + * + * @name Phaser.Sound.WebAudioSound#seek + * @type {number} + * @since 3.0.0 + */ + seek: { + + get: function () + { + if (this.isPlaying) + { + if (this.manager.context.currentTime < this.startTime) + { + return this.startTime - this.playTime; + } + + return this.getCurrentTime(); + } + else if (this.isPaused) + { + return this.currentConfig.seek; + } + else + { + return 0; + } + }, + + set: function (value) { if (this.manager.context.currentTime < this.startTime) { - return this.startTime - this.playTime; + return; + } + + if (this.isPlaying || this.isPaused) + { + value = Math.min(Math.max(0, value), this.duration); + + this.currentConfig.seek = value; + + if (this.isPlaying) + { + this.stopAndRemoveBufferSource(); + this.createAndStartBufferSource(); + } + + this.emit('seek', this, value); } - return this.getCurrentTime(); - } - else if (this.isPaused) - { - return this.currentConfig.seek; - } - else - { - return 0; } }, - set: function (value) + + /** + * Seeks to a specific point in this sound. + * + * @method Phaser.Sound.WebAudioSound#setSeek + * @fires Phaser.Sound.WebAudioSound#seekEvent + * @since 3.4.0 + * + * @param {number} value - The point in the sound to seek to. + * + * @return {Phaser.Sound.WebAudioSound} This Sound instance. + */ + setSeek: function (value) { - if (this.manager.context.currentTime < this.startTime) + this.seek = value; + + return this; + }, + + /** + * @event Phaser.Sound.WebAudioSound#loopEvent + * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. + * @param {boolean} value - An updated value of Phaser.Sound.WebAudioSound#loop property. + */ + + /** + * [description] + * + * @name Phaser.Sound.WebAudioSound#loop + * @type {boolean} + * @default false + * @since 3.0.0 + */ + loop: { + + get: function () { - return; - } - if (this.isPlaying || this.isPaused) + return this.currentConfig.loop; + }, + + set: function (value) { - value = Math.min(Math.max(0, value), this.duration); - this.currentConfig.seek = value; + this.currentConfig.loop = value; + if (this.isPlaying) { - this.stopAndRemoveBufferSource(); - this.createAndStartBufferSource(); + this.stopAndRemoveLoopBufferSource(); + + if (value) + { + this.createAndStartLoopBufferSource(); + } } - /** - * @event Phaser.Sound.WebAudioSound#seek - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - * @param {number} value - An updated value of Phaser.Sound.WebAudioSound#seek property. - */ - this.emit('seek', this, value); + this.emit('loop', this, value); } - } -}); -Object.defineProperty(WebAudioSound.prototype, 'loop', { - get: function () - { - return this.currentConfig.loop; }, - set: function (value) - { - this.currentConfig.loop = value; - if (this.isPlaying) - { - this.stopAndRemoveLoopBufferSource(); - if (value) - { - this.createAndStartLoopBufferSource(); - } - } - /** - * @event Phaser.Sound.WebAudioSound#loop - * @param {Phaser.Sound.WebAudioSound} sound - Reference to the sound that emitted event. - * @param {boolean} value - An updated value of Phaser.Sound.WebAudioSound#loop property. - */ - this.emit('loop', this, value); + /** + * Sets the loop state of this Sound. + * + * @method Phaser.Sound.WebAudioSound#setLoop + * @fires Phaser.Sound.WebAudioSound#loopEvent + * @since 3.4.0 + * + * @param {boolean} value - `true` to loop this sound, `false` to not loop it. + * + * @return {Phaser.Sound.WebAudioSound} This Sound instance. + */ + setLoop: function (value) + { + this.loop = value; + + return this; } + }); + module.exports = WebAudioSound;