const sound = PIXI.sound.utils.sineTone(523.251);
-sound.play();
+ const sound = PIXI.sound.utils.sineTone(523.251, 3);
+sound.play({
+ fadeIn: 1,
+ fadeOut: 1
+});
diff --git a/src/Sound.ts b/src/Sound.ts
index 660c7a9..3408aed 100644
--- a/src/Sound.ts
+++ b/src/Sound.ts
@@ -30,6 +30,9 @@ export interface PlayOptions {
end?: number;
speed?: number;
loop?: boolean;
+ fadeIn?: number;
+ fadeOut?: number;
+ sprite?: string;
complete?: CompleteCallback;
loaded?: LoadedCallback;
}
@@ -39,8 +42,9 @@ export interface PlayOptions {
* @callback PIXI.sound.Sound~loadedCallback
* @param {Error} err The callback error.
* @param {PIXI.sound.Sound} sound The instance of new sound.
+ * @param {PIXI.sound.SoundInstance} instance The instance of auto-played sound.
*/
-export declare type LoadedCallback = (err: Error, sound?: Sound) => void;
+export declare type LoadedCallback = (err: Error, sound?: Sound, instance?: SoundInstance) => void;
/**
* Callback when sound is completed.
@@ -90,22 +94,6 @@ export default class Sound
*/
public autoPlay: boolean;
- /**
- * Callback when finished playing.
- * @name PIXI.sound.Sound#complete
- * @type {PIXI.sound.Sound~completeCallback}
- * @default false
- */
- public complete: CompleteCallback;
-
- /**
- * Callback when load is finished.
- * @type {PIXI.sound.Sound~loadedCallback}
- * @name PIXI.sound.Sound#loaded
- * @readOnly
- */
- public loaded: LoadedCallback;
-
/**
* `true` to disallow playing multiple layered instances at once.
* @name PIXI.sound.Sound#singleInstance
@@ -148,6 +136,22 @@ export default class Sound
*/
public useXHR: boolean;
+ /**
+ * The options when auto-playing.
+ * @name PIXI.sound.Sound#_autoPlayOptions
+ * @type {PlayOptions}
+ * @private
+ */
+ private _autoPlayOptions: PlayOptions;
+
+ /**
+ * The internal volume.
+ * @name PIXI.sound.Sound#_volume
+ * @type {Number}
+ * @private
+ */
+ private _volume: number;
+
/**
* Reference to the sound context.
* @name PIXI.sound.Sound#_context
@@ -253,13 +257,14 @@ export default class Sound
this._instances = [];
this._sprites = {};
+ const complete = options.complete;
+ this._autoPlayOptions = complete ? { complete } : null;
+
this.isLoaded = false;
this.isPlaying = false;
this.autoPlay = options.autoPlay;
this.singleInstance = options.singleInstance;
this.preload = options.preload || this.autoPlay;
- this.complete = options.complete;
- this.loaded = options.loaded;
this.src = options.src;
this.srcBuffer = options.srcBuffer;
this.useXHR = options.useXHR;
@@ -274,7 +279,7 @@ export default class Sound
if (this.preload)
{
- this._beginPreload();
+ this._beginPreload(options.loaded);
}
}
@@ -294,8 +299,6 @@ export default class Sound
this.removeSprites();
this._sprites = null;
- this.complete = null;
- this.loaded = null;
this.srcBuffer = null;
this._removeInstances();
@@ -331,11 +334,11 @@ export default class Sound
*/
public get volume(): number
{
- return this._nodes.gain.gain.value;
+ return this._volume;
}
public set volume(volume: number)
{
- this._nodes.gain.gain.value = volume;
+ this._volume = this._nodes.gain.gain.value = volume;
}
/**
@@ -528,9 +531,11 @@ export default class Sound
* @param {Number} data.end Time to end playing in seconds.
* @param {Number} [data.speed] Override default speed, default to the Sound's speed setting.
* @param {PIXI.sound.Sound~completeCallback} [callback] Callback when completed.
- * @return {PIXI.sound.SoundInstance} Current playing instance.
+ * @return {PIXI.sound.SoundInstance|Promise} The sound instance,
+ * this cannot be reused after it is done playing. Returns a Promise if the sound
+ * has not yet loaded.
*/
- public play(alias: string, callback?: CompleteCallback): SoundInstance;
+ public play(alias: string, callback?: CompleteCallback): SoundInstance|Promise;
/**
* Plays the sound.
@@ -538,33 +543,32 @@ export default class Sound
* @param {PIXI.sound.Sound~completeCallback|object} options Either completed function or play options.
* @param {Number} [options.start=0] Time when to play the sound in seconds.
* @param {Number} [options.end] Time to end playing in seconds.
+ * @param {String} [options.sprite] Play a named sprite. Will override end, start and speed options.
+ * @param {Number} [options.fadeIn] Amount of time to fade in volume. If less than 10,
+ * considered seconds or else milliseconds.
+ * @param {Number} [options.fadeOut] Amount of time to fade out volume. If less than 10,
+ * considered seconds or else milliseconds.
* @param {Number} [options.speed] Override default speed, default to the Sound's speed setting.
* @param {Boolean} [options.loop] Override default loop, default to the Sound's loop setting.
* @param {PIXI.sound.Sound~completeCallback} [options.complete] Callback when complete.
* @param {PIXI.sound.Sound~loadedCallback} [options.loaded] If the sound isn't already preloaded, callback when
* the audio has completely finished loading and decoded.
- * @return {PIXI.sound.SoundInstance} Current playing instance.
+ * @return {PIXI.sound.SoundInstance|Promise} The sound instance,
+ * this cannot be reused after it is done playing. Returns a Promise if the sound
+ * has not yet loaded.
*/
- public play(source?: PlayOptions|CompleteCallback, callback?: CompleteCallback): SoundInstance;
+ public play(source?: PlayOptions|CompleteCallback,
+ callback?: CompleteCallback): SoundInstance|Promise;
// Overloaded function
- public play(source?: any, callback?: CompleteCallback): SoundInstance
+ public play(source?: any, complete?: CompleteCallback): SoundInstance|Promise
{
let options: PlayOptions;
if (typeof source === "string")
{
- const alias: string = source as string;
- // @if DEBUG
- console.assert(!!this._sprites[alias], `Alias ${alias} is not available`);
- // @endif
- const sprite: SoundSprite = this._sprites[alias];
- options = {
- start: sprite.start,
- end: sprite.end,
- speed: sprite.speed,
- complete: callback,
- };
+ const sprite: string = source as string;
+ options = { sprite, complete };
}
else if (typeof source === "function")
{
@@ -579,9 +583,26 @@ export default class Sound
options = Object.assign({
complete: null,
loaded: null,
+ sprite: null,
start: 0,
+ fadeIn: 0,
+ fadeOut: 0,
}, options || {});
+ // A sprite is specified, add the options
+ if (options.sprite)
+ {
+ const alias: string = options.sprite;
+ // @if DEBUG
+ console.assert(!!this._sprites[alias], `Alias ${alias} is not available`);
+ // @endif
+ const sprite: SoundSprite = this._sprites[alias];
+ options.start = sprite.start;
+ options.end = sprite.end;
+ options.speed = sprite.speed;
+ delete options.sprite;
+ }
+
// @deprecated offset option
if ((options as any).offset) {
options.start = (options as any).offset as number;
@@ -589,19 +610,28 @@ export default class Sound
// if not yet playable, ignore
// - usefull when the sound download isnt yet completed
- if (!this.isPlayable)
+ if (!this.isLoaded)
{
- this.autoPlay = true;
- if (!this.isLoaded)
+ return new Promise((resolve, reject) =>
{
- const loaded = options.loaded;
- if (loaded)
+ this.autoPlay = true;
+ this._autoPlayOptions = options;
+ this._beginPreload((err: Error, sound: Sound, instance: SoundInstance) =>
{
- this.loaded = loaded;
- }
- this._beginPreload();
- }
- return;
+ if (err)
+ {
+ reject(err);
+ }
+ else
+ {
+ if (options.loaded)
+ {
+ options.loaded(err, sound, instance);
+ }
+ resolve(instance);
+ }
+ });
+ });
}
// Stop all sounds
@@ -624,11 +654,14 @@ export default class Sound
instance.once("stop", () => {
this._onComplete(instance);
});
+
instance.play(
options.start,
options.end,
options.speed,
options.loop,
+ options.fadeIn,
+ options.fadeOut,
);
return instance;
}
@@ -643,6 +676,7 @@ export default class Sound
if (!this.isPlayable)
{
this.autoPlay = false;
+ this._autoPlayOptions = null;
return this;
}
this.isPlaying = false;
@@ -690,21 +724,21 @@ export default class Sound
* @method PIXI.sound.Sound#_beginPreload
* @private
*/
- private _beginPreload(): void
+ private _beginPreload(callback?: LoadedCallback): void
{
// Load from the file path
if (this.src)
{
- this.useXHR ? this._loadUrl() : this._loadPath();
+ this.useXHR ? this._loadUrl(callback) : this._loadPath(callback);
}
// Load from the arraybuffer, incase it was loaded outside
else if (this.srcBuffer)
{
- this._decode(this.srcBuffer);
+ this._decode(this.srcBuffer, callback);
}
- else if (this.loaded)
+ else if (callback)
{
- this.loaded(new Error("sound.src or sound.srcBuffer must be set"));
+ callback(new Error("sound.src or sound.srcBuffer must be set"));
}
else
{
@@ -752,7 +786,7 @@ export default class Sound
* @method PIXI.sound.Sound#_loadUrl
* @private
*/
- private _loadUrl(): void
+ private _loadUrl(callback?: LoadedCallback): void
{
const request = new XMLHttpRequest();
const src: string = this.src;
@@ -761,9 +795,8 @@ export default class Sound
// Decode asynchronously
request.onload = () => {
- this.isLoaded = true;
this.srcBuffer = request.response as ArrayBuffer;
- this._decode(request.response);
+ this._decode(request.response, callback);
};
// actually start the request
@@ -775,7 +808,7 @@ export default class Sound
* @method PIXI.sound.Sound#_loadPath
* @private
*/
- private _loadPath()
+ private _loadPath(callback?: LoadedCallback)
{
const fs = require("fs");
const src: string = this.src;
@@ -785,9 +818,9 @@ export default class Sound
// @if DEBUG
console.error(err);
// @endif
- if (this.loaded)
+ if (callback)
{
- this.loaded(new Error(`File not found ${this.src}`));
+ callback(new Error(`File not found ${this.src}`));
}
return;
}
@@ -798,7 +831,7 @@ export default class Sound
view[i] = data[i];
}
this.srcBuffer = arrayBuffer;
- this._decode(arrayBuffer);
+ this._decode(arrayBuffer, callback);
});
}
@@ -808,25 +841,29 @@ export default class Sound
* @param {ArrayBuffer} arrayBuffer From load.
* @private
*/
- private _decode(arrayBuffer: ArrayBuffer): void
+ private _decode(arrayBuffer: ArrayBuffer, callback?: LoadedCallback): void
{
this._context.decode(arrayBuffer, (err: Error, buffer: AudioBuffer) =>
{
if (err)
{
- this.loaded(err);
+ if (callback)
+ {
+ callback(err);
+ }
}
else
{
this.isLoaded = true;
this.buffer = buffer;
- if (this.loaded)
+ let instance: SoundInstance;
+ if (this.autoPlay)
{
- this.loaded(null, this);
+ instance = this.play(this._autoPlayOptions) as SoundInstance;
}
- if (this.autoPlay)
+ if (callback)
{
- this.play(this.complete);
+ callback(null, this, instance);
}
}
},
diff --git a/src/SoundInstance.ts b/src/SoundInstance.ts
index bca0b4b..c679a79 100644
--- a/src/SoundInstance.ts
+++ b/src/SoundInstance.ts
@@ -58,6 +58,22 @@ export default class SoundInstance extends PIXI.utils.EventEmitter
*/
private _elapsed: number;
+ /**
+ * The number of time in seconds to fade in.
+ * @type {Number}
+ * @name PIXI.sound.SoundInstance#_fadeIn
+ * @private
+ */
+ private _fadeIn: number;
+
+ /**
+ * The number of time in seconds to fade out.
+ * @type {Number}
+ * @name PIXI.sound.SoundInstance#_fadeOut
+ * @private
+ */
+ private _fadeOut: number;
+
/**
* Playback rate, where 1 is 100%.
* @type {Number}
@@ -66,6 +82,22 @@ export default class SoundInstance extends PIXI.utils.EventEmitter
*/
private _speed: number;
+ /**
+ * Playback rate, where 1 is 100%.
+ * @type {Number}
+ * @name PIXI.sound.SoundInstance#_end
+ * @private
+ */
+ private _end: number;
+
+ /**
+ * `true` if should be looping.
+ * @type {Boolean}
+ * @name PIXI.sound.SoundInstance#_loop
+ * @private
+ */
+ private _loop: boolean;
+
/**
* Length of the sound in seconds.
* @type {Number}
@@ -149,8 +181,10 @@ export default class SoundInstance extends PIXI.utils.EventEmitter
* @param {Number} [end] The ending position in seconds.
* @param {Number} [speed] Override the default speed.
* @param {Boolean} [loop] Override the default loop.
+ * @param {Number} [fadeIn] Time to fadein volume.
+ * @param {Number} [fadeOut] Time to fadeout volume.
*/
- public play(start: number = 0, end?: number, speed?: number, loop?: boolean): void
+ public play(start: number, end: number, speed: number, loop: boolean, fadeIn: number, fadeOut: number): void
{
// @if DEBUG
if (end)
@@ -167,20 +201,46 @@ export default class SoundInstance extends PIXI.utils.EventEmitter
this._speed = this._source.playbackRate.value;
if (loop !== undefined)
{
- this._source.loop = loop;
+ this._loop = this._source.loop = !!loop;
}
// WebAudio doesn't support looping when a duration is set
// we'll set this just for the heck of it
- if (this._source.loop && end !== undefined)
+ if (this._loop && end !== undefined)
{
// @if DEBUG
console.warn('Looping not support when specifying an "end" time');
// @endif
- this._source.loop = false;
+ this._loop = this._source.loop = false;
}
+ this._end = end;
+
+ const duration: number = this._source.buffer.duration;
+
+ fadeIn = this._toSec(fadeIn);
+
+ // Clamp fadeIn to the duration
+ if (fadeIn > duration)
+ {
+ fadeIn = duration;
+ }
+
+ // Cannot fade out for looping sounds
+ if (!this._loop)
+ {
+ fadeOut = this._toSec(fadeOut);
+
+ // Clamp fadeOut to the duration + fadeIn
+ if (fadeOut > duration - fadeIn)
+ {
+ fadeOut = duration - fadeIn;
+ }
+ }
+
+ this._duration = duration;
+ this._fadeIn = fadeIn;
+ this._fadeOut = fadeOut;
this._lastUpdate = this._now();
this._elapsed = start;
- this._duration = this._source.buffer.duration;
this._source.onended = this._onComplete.bind(this);
this._source.start(0, start, (end ? end - start : undefined));
@@ -197,6 +257,22 @@ export default class SoundInstance extends PIXI.utils.EventEmitter
this._enabled = true;
}
+ /**
+ * Utility to convert time in millseconds or seconds
+ * @method PIXI.sound.SoundInstance#_toSec
+ * @private
+ * @param {Number} [time] Time in either ms or sec
+ * @return {Number} Time in seconds
+ */
+ private _toSec(time?: number): number
+ {
+ if (time > 10)
+ {
+ time /= 1000;
+ }
+ return time || 0;
+ }
+
/**
* Start the update progress.
* @name PIXI.sound.SoundInstance#_enabled
@@ -256,7 +332,14 @@ export default class SoundInstance extends PIXI.utils.EventEmitter
this.emit("resumed");
// resume the playing with offset
- this.play(this._elapsed % this._duration);
+ this.play(
+ this._elapsed % this._duration,
+ this._end,
+ this._speed,
+ this._loop,
+ this._fadeIn,
+ this._fadeOut,
+ );
}
/**
@@ -276,14 +359,15 @@ export default class SoundInstance extends PIXI.utils.EventEmitter
{
this.removeAllListeners();
this._internalStop();
- if (this._source)
- {
- this._source.onended = null;
- }
this._source = null;
+ this._speed = 0;
+ this._end = 0;
this._parent = null;
this._elapsed = 0;
this._duration = 0;
+ this._loop = false;
+ this._fadeIn = 0;
+ this._fadeOut = 0;
this._paused = false;
// Add it if it isn't already added
@@ -332,7 +416,38 @@ export default class SoundInstance extends PIXI.utils.EventEmitter
this._elapsed += delta;
this._lastUpdate = now;
const duration: number = this._duration;
- this._progress = ((this._elapsed * this._speed) % duration) / duration;
+ const progress: number = ((this._elapsed * this._speed) % duration) / duration;
+
+ if (this._fadeIn || this._fadeOut)
+ {
+ const position: number = progress * duration;
+ const gain = this._parent.nodes.gain.gain;
+ const maxVolume = this._parent.volume;
+
+ if (this._fadeIn)
+ {
+ if (position <= this._fadeIn && progress < 1)
+ {
+ // Manipulate the gain node directly
+ // so we can maintain the starting volume
+ gain.value = maxVolume * (position / this._fadeIn);
+ }
+ else
+ {
+ gain.value = maxVolume;
+ this._fadeIn = 0;
+ }
+ }
+
+ if (this._fadeOut && position >= duration - this._fadeOut)
+ {
+ const percent: number = (duration - position) / this._fadeOut;
+ gain.value = maxVolume * percent;
+ }
+ }
+
+ // Update the progress
+ this._progress = progress;
/**
* The sound progress is updated.
@@ -369,6 +484,8 @@ export default class SoundInstance extends PIXI.utils.EventEmitter
this._source.stop();
this._source = null;
+ // Reset the volume
+ this._parent.volume = this._parent.volume;
}
}
diff --git a/src/SoundLibrary.ts b/src/SoundLibrary.ts
index bf2f264..b1cc68a 100644
--- a/src/SoundLibrary.ts
+++ b/src/SoundLibrary.ts
@@ -358,10 +358,11 @@ export default class SoundLibrary
* @param {Number} [options.end] End time offset.
* @param {Number} [options.speed] Override default speed, default to the Sound's speed setting.
* @param {Boolean} [options.loop] Override default loop, default to the Sound's loop setting.
- * @return {PIXI.sound.SoundInstance|null} The sound instance, this cannot be reused
- * after it is done playing. Returns `null` if the sound has not yet loaded.
+ * @return {PIXI.sound.SoundInstance|Promise} The sound instance,
+ * this cannot be reused after it is done playing. Returns a Promise if the sound
+ * has not yet loaded.
*/
- public play(alias: string, options?: PlayOptions|Object|string): SoundInstance
+ public play(alias: string, options?: PlayOptions|Object|string): SoundInstance|Promise
{
return this.find(alias).play(options);
}
diff --git a/src/SoundSprite.ts b/src/SoundSprite.ts
index 43f2f56..456befb 100644
--- a/src/SoundSprite.ts
+++ b/src/SoundSprite.ts
@@ -79,9 +79,9 @@ export default class SoundSprite
* Play the sound sprite.
* @method PIXI.sound.SoundSprite#play
* @param {PIXI.sound.Sound~completeCallback} [complete] Function call when complete
- * @return {PIXI.sound.SoundInstance} Sound instance being played.
+ * @return {PIXI.sound.SoundInstance|Promise} Sound instance being played.
*/
- public play(complete?: CompleteCallback): SoundInstance
+ public play(complete?: CompleteCallback): SoundInstance|Promise
{
return this.parent.play(Object.assign({
complete,
diff --git a/src/deprecations.ts b/src/deprecations.ts
index 6fd3d67..086bcd2 100644
--- a/src/deprecations.ts
+++ b/src/deprecations.ts
@@ -72,3 +72,33 @@ Object.defineProperty(SoundPrototype, "block", {
this.singleInstance = value;
},
});
+
+/**
+ * Retired property on Sound for handing loaded event.
+ * @name PIXI.sound.Sound#loaded
+ * @deprecated since 1.4.0
+ */
+Object.defineProperty(SoundPrototype, "loaded", {
+ get() {
+ console.warn("PIXI.sound.Sound.prototype.loaded is deprecated, use constructor option instead");
+ return null;
+ },
+ set(value: boolean) {
+ console.warn("PIXI.sound.Sound.prototype.loaded is deprecated, use constructor option instead");
+ },
+});
+
+/**
+ * Retired property on Sound for handling autoPlay completed event.
+ * @name PIXI.sound.Sound#complete
+ * @deprecated since 1.4.0
+ */
+Object.defineProperty(SoundPrototype, "complete", {
+ get() {
+ console.warn("PIXI.sound.Sound.prototype.complete is deprecated, use constructor option instead");
+ return null;
+ },
+ set(value: boolean) {
+ console.warn("PIXI.sound.Sound.prototype.complete is deprecated, use constructor option instead");
+ },
+});
diff --git a/test/index.js b/test/index.js
index 1d549ec..f839c02 100644
--- a/test/index.js
+++ b/test/index.js
@@ -259,6 +259,43 @@ describe("PIXI.sound", function()
});
});
+describe("PIXI.sound.SoundInstance", function()
+{
+ afterEach(function()
+ {
+ library.default.removeAll();
+ });
+
+ it("should return Promise for playing unloaded sound", function(done)
+ {
+ const Sound = library.default.Sound;
+ const sound = Sound.from(manifest.silence);
+ expect(sound).to.be.instanceof(Sound);
+ const promise = sound.play();
+ promise.then((instance) => {
+ expect(instance).to.be.instanceof(library.default.SoundInstance);
+ done();
+ });
+ expect(promise).to.be.instanceof(Promise);
+ });
+
+ it("should return instance for playing loaded sound", function(done)
+ {
+ const sound = library.default.Sound.from({
+ src: manifest.silence,
+ preload: true,
+ loaded: (err) => {
+ expect(err).to.be.null;
+ expect(sound.isLoaded).to.be.true;
+ expect(sound.isPlayable).to.be.true;
+ const instance = sound.play();
+ expect(instance).to.be.instanceof(library.default.SoundInstance);
+ done();
+ },
+ });
+ });
+});
+
describe("PIXI.loader", function()
{
afterEach(function()