diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc3ca1b5..da5fbc97 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
bounding box constructor.
- Added the `ex.vec(x, y)` shorthand for creating vectors.
([#1340](https://github.com/excaliburjs/Excalibur/issues/1340))
+- Added new event `processed` to `Sound` that passes processed `string | AudioBuffer` data. ([#1474](https://github.com/excaliburjs/Excalibur/pull/1474))
+- Added new property `duration` to `Sound` and `AudioInstance` that exposes the track's duration in seconds when Web Audio API is used. ([#1474](https://github.com/excaliburjs/Excalibur/pull/1474))
### Changed
@@ -24,6 +26,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Changed obsolete decorator to only log the same message 5 times. ([#1281](https://github.com/excaliburjs/Excalibur/issues/1281))
- Switched to core-js based polyfills instead of custom written ones ([#1214](https://github.com/excaliburjs/Excalibur/issues/1214))
- Updated to TypeScript@3.6.4 and node 10 LTS build
+- `Sound.stop()` now always rewinds the track, even when the sound is paused. ([#1474](https://github.com/excaliburjs/Excalibur/pull/1474))
### Deprecated
@@ -38,6 +41,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Fixed iframe event handling, excalibur will respond to keyboard events from the top window ([#1294](https://github.com/excaliburjs/Excalibur/issues/1294))
- Fixed camera to be vector backed so `ex.Camera.x = ?` and `ex.Camera.pos.setTo(...)` both work as expected([#1299](https://github.com/excaliburjs/Excalibur/issues/1299))
- Fixed missing on/once/off signatures on `ex.Pointer` ([#1345](https://github.com/excaliburjs/Excalibur/issues/1345))
+- Fixed sounds not being stopped when `Engine.stop()` is called. ([#1476](https://github.com/excaliburjs/Excalibur/pull/1476))
diff --git a/package-lock.json b/package-lock.json
index bb3a2964..59e324c8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3647,6 +3647,12 @@
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==",
"dev": true
},
+ "@fortawesome/fontawesome-free": {
+ "version": "5.13.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz",
+ "integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==",
+ "dev": true
+ },
"@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
diff --git a/package.json b/package.json
index 2daede6e..8a40d35d 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
},
"devDependencies": {
"@babel/core": "7.9.0",
+ "@fortawesome/fontawesome-free": "5.13.0",
"@storybook/addon-actions": "5.3.17",
"@storybook/addon-console": "1.2.1",
"@storybook/addon-docs": "5.3.17",
diff --git a/sandbox/index.html b/sandbox/index.html
index 1a6eb9c4..b559cbb9 100644
--- a/sandbox/index.html
+++ b/sandbox/index.html
@@ -28,9 +28,6 @@
Camera Zoom over time
Camera Strategies
Groups
- Audio
- Audio: Long-running sound muting
- Set volume with argument to play>
Label
Label - Fonts
Within distance
diff --git a/sandbox/tests/audio/index.html b/sandbox/tests/audio/index.html
deleted file mode 100644
index cf0139c9..00000000
--- a/sandbox/tests/audio/index.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- Audio test
-
-
-
-
-
-
-
diff --git a/sandbox/tests/audio/index.ts b/sandbox/tests/audio/index.ts
deleted file mode 100644
index 5ae12edf..00000000
--- a/sandbox/tests/audio/index.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-///
-
-// uncomment to hint fallback implementation
-//(window).AudioContext = null;
-ex.Logger.getInstance().defaultLevel = ex.LogLevel.Debug;
-
-var game = new ex.Engine();
-
-var loader = new ex.Loader();
-var testSound = new ex.Sound('loop.mp3');
-loader.addResource(testSound);
-
-const startBtn = new ex.Actor(game.currentScene.camera.x + 50, 50, 100, 100, ex.Color.White);
-const stopBtn = new ex.Actor(game.currentScene.camera.x + 50, 70, 175, 100, ex.Color.Blue);
-const pauseBtn = new ex.Actor(game.currentScene.camera.x + 50, 90, 250, 100, ex.Color.Green);
-const indicator = new ex.Actor(game.currentScene.camera.x, game.currentScene.camera.y, 150, 50, ex.Color.Red);
-
-startBtn.enableCapturePointer = true;
-startBtn.add(new ex.Label('start - indicator green', -50, 40));
-startBtn.on('pointerup', (evt) => {
- if (!testSound.isPlaying()) {
- indicator.color = ex.Color.Green;
-
- testSound.play().then(() => {
- indicator.color = ex.Color.Red;
- });
- }
- console.log('start called');
- evt.stopPropagation();
-});
-stopBtn.enableCapturePointer = true;
-stopBtn.add(new ex.Label('stop - indicator red', -50, 40));
-stopBtn.on('pointerup', (evt) => {
- if (testSound.isPlaying()) {
- testSound.stop();
- startBtn.color = ex.Color.Red;
- }
- console.log('stop called');
- evt.stopPropagation();
-});
-pauseBtn.enableCapturePointer = true;
-pauseBtn.add(new ex.Label('pause - indicator yellow', -50, 40));
-pauseBtn.on('pointerup', (evt) => {
- if (testSound.isPlaying()) {
- testSound.pause();
- indicator.color = ex.Color.Yellow;
- }
- console.log('pause called');
- evt.stopPropagation();
-});
-
-pauseBtn.on('pointerenter', () => {
- console.log('pointer enter - pause btn');
-});
-
-pauseBtn.on('pointerleave', () => {
- console.log('pointer leave - pause btn');
-});
-game.add(pauseBtn);
-game.add(stopBtn);
-game.add(startBtn);
-game.add(indicator);
-
-game.start(loader);
diff --git a/sandbox/tests/audio/longsound.html b/sandbox/tests/audio/longsound.html
deleted file mode 100644
index b246e24e..00000000
--- a/sandbox/tests/audio/longsound.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- Long sound muting test
-
-
- After 2 seconds the same loop will start. When you tab away from the browser (make game not visible), both tracks should mute themselves. Check console output for confirmation.
-
-
-
-
-
diff --git a/sandbox/tests/audio/longsound.ts b/sandbox/tests/audio/longsound.ts
deleted file mode 100644
index a593d176..00000000
--- a/sandbox/tests/audio/longsound.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-///
-
-// uncomment to hint fallback implementation
-// (window).AudioContext = null;
-
-var game = new ex.Engine();
-
-var loader = new ex.Loader();
-var testSound = new ex.Sound('loop.mp3');
-loader.addResource(testSound);
-
-game.on('visible', () => {
- ex.Logger.getInstance().info('Game was visible, sound should play');
-});
-
-game.on('hidden', () => {
- ex.Logger.getInstance().info('Game was hidden, sound should NOT play again');
-});
-
-game.start(loader).then(() => {
- testSound.loop = true;
- testSound.play();
-
- setTimeout(() => testSound.play(), 2000);
-});
diff --git a/sandbox/tests/audio/setvolume.html b/sandbox/tests/audio/setvolume.html
deleted file mode 100644
index 46c4b994..00000000
--- a/sandbox/tests/audio/setvolume.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- Audio test
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/sandbox/tests/audio/setvolume.ts b/sandbox/tests/audio/setvolume.ts
deleted file mode 100644
index 7e8f6cba..00000000
--- a/sandbox/tests/audio/setvolume.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-///
-
-ex.Logger.getInstance().defaultLevel = ex.LogLevel.Debug;
-//create game and load the sound file with custom loader
-var game = new ex.Engine();
-var loader = new ex.Loader();
-var testSound = new ex.Sound('loop.mp3');
-loader.addResource(testSound);
-
-//click a button to play the sound
-var button = new ex.Actor(100, 100, 100, 100, ex.Color.Red);
-button.enableCapturePointer = true;
-button.on('pointerup', function() {
- button.color = ex.Color.Green;
-
- //button will turn red again when song is done
- if (!testSound.isPlaying()) {
- testSound.play(0.2).then(function() {
- button.color = ex.Color.Red;
- });
-
- //change volume of the sound after 2000 ms to show that
- //initial setting worked
- setTimeout(function() {
- testSound.volume = 1;
- }, 2000);
- }
-});
-game.add(button);
-game.start(loader);
-//# sourceMappingURL=index.js.map
diff --git a/src/engine/Events/MediaEvents.ts b/src/engine/Events/MediaEvents.ts
index 9ddc46ce..52355fea 100644
--- a/src/engine/Events/MediaEvents.ts
+++ b/src/engine/Events/MediaEvents.ts
@@ -70,3 +70,13 @@ export class NativeSoundEvent extends MediaEvent {
super(target, 'NativeSoundEvent');
}
}
+
+export class NativeSoundProcessedEvent extends MediaEvent {
+ public data: string | AudioBuffer;
+
+ constructor(target: Sound, private processedData: string | AudioBuffer) {
+ super(target, 'NativeSoundProcessedEvent');
+
+ this.data = this.processedData;
+ }
+}
diff --git a/src/engine/Resources/Sound/AudioInstance.ts b/src/engine/Resources/Sound/AudioInstance.ts
index d914d492..31cef459 100644
--- a/src/engine/Resources/Sound/AudioInstance.ts
+++ b/src/engine/Resources/Sound/AudioInstance.ts
@@ -43,8 +43,19 @@ export class AudioInstance implements Audio {
public get volume(): number {
return this._volume;
}
+ public set duration(value: number | undefined) {
+ this._duration = value;
+ }
+
+ /**
+ * Duration of the sound, in seconds.
+ */
+ public get duration() {
+ return this._duration;
+ }
protected _volume = 1;
+ protected _duration: number | undefined = undefined;
protected _loop = false;
protected _playingPromise: Promise;
protected _isPlaying = false;
diff --git a/src/engine/Resources/Sound/Sound.ts b/src/engine/Resources/Sound/Sound.ts
index cb3d8846..39726437 100644
--- a/src/engine/Resources/Sound/Sound.ts
+++ b/src/engine/Resources/Sound/Sound.ts
@@ -4,7 +4,7 @@ import { Engine } from '../../Engine';
import { Resource } from '../Resource';
import { AudioInstance, AudioInstanceFactory } from './AudioInstance';
import { AudioContextFactory } from './AudioContext';
-import { NativeSoundEvent } from '../../Events/MediaEvents';
+import { NativeSoundEvent, NativeSoundProcessedEvent } from '../../Events/MediaEvents';
import { Promise } from '../../Promises';
import { canPlayFile } from '../../Util/Sound';
@@ -48,6 +48,10 @@ export class Sound extends Resource implements Audio {
return this._volume;
}
+ public get duration(): number | undefined {
+ return this._duration;
+ }
+
/**
* Return array of Current AudioInstances playing or being paused
*/
@@ -59,6 +63,8 @@ export class Sound extends Resource implements Audio {
private _loop = false;
private _volume = 1;
+ private _duration: number | undefined = undefined;
+ private _isStopped = false;
private _isPaused = false;
private _tracks: AudioInstance[] = [];
private _engine: Engine;
@@ -109,6 +115,15 @@ export class Sound extends Resource implements Audio {
this._wasPlayingOnHidden = false;
}
});
+
+ this._engine.on('start', () => {
+ this._isStopped = false;
+ });
+
+ this._engine.on('stop', () => {
+ this.stop();
+ this._isStopped = true;
+ });
}
}
@@ -137,6 +152,11 @@ export class Sound extends Resource implements Audio {
return Promise.resolve(true);
}
+ if (this._isStopped) {
+ this.logger.warn('Cannot start playing. Engine is in a stopped state.');
+ return Promise.resolve(false);
+ }
+
this.volume = volume || this.volume;
if (this._isPaused) {
@@ -166,13 +186,9 @@ export class Sound extends Resource implements Audio {
}
/**
- * Stop the sound and rewind
+ * Stop the sound if it is currently playing and rewind the track. If the sound is not playing, rewinds the track.
*/
public stop() {
- if (!this.isPlaying()) {
- return;
- }
-
for (const track of this._tracks) {
track.stop();
}
@@ -278,6 +294,8 @@ export class Sound extends Resource implements Audio {
private _setProcessedData(processedData: string | AudioBuffer): void {
this._processedData.resolve(processedData);
+ this._duration = typeof processedData === 'object' ? processedData.duration : undefined;
+ this.emit('processed', new NativeSoundProcessedEvent(this, processedData));
}
private _createNewTrack(): Promise {
@@ -306,6 +324,7 @@ export class Sound extends Resource implements Audio {
newTrack.loop = this.loop;
newTrack.volume = this.volume;
+ newTrack.duration = this.duration;
this._tracks.push(newTrack);
diff --git a/src/engine/files.d.ts b/src/engine/files.d.ts
index d968e5fc..5f02df92 100644
--- a/src/engine/files.d.ts
+++ b/src/engine/files.d.ts
@@ -3,6 +3,9 @@ declare module '*.png' {
export default value;
}
+declare module '*.mp3';
+declare module '*.svg';
+
declare module '*.css' {
const value: { toString: () => string };
export default value;
diff --git a/src/spec/SoundSpec.ts b/src/spec/SoundSpec.ts
index edbe090e..03d1b79c 100644
--- a/src/spec/SoundSpec.ts
+++ b/src/spec/SoundSpec.ts
@@ -1,6 +1,8 @@
import { ExcaliburMatchers, ensureImagesLoaded } from 'excalibur-jasmine';
import * as ex from '@excalibur';
import { Stubs } from './util/Stubs';
+import { Mocks } from './util/Mocks';
+import { TestUtils } from './util/TestUtils';
describe('Sound resource', () => {
let sut: ex.Sound;
@@ -29,6 +31,22 @@ describe('Sound resource', () => {
expect(sut.processData(sut.getData())).toBeDefined();
});
+ it('should fire processed event', (done) => {
+ sut.on('processed', (e: ex.NativeSoundProcessedEvent) => {
+ expect(e.data).toBeDefined();
+ done();
+ });
+ sut.processData(sut.getData());
+ });
+
+ it('should have duration', (done) => {
+ sut.processData(sut.getData()).then(() => {
+ expect(sut.duration).toBeDefined();
+ expect(sut.duration).toBeGreaterThan(0);
+ done();
+ });
+ });
+
it('should fire playbackstart event', (done) => {
let trigger = false;
@@ -242,10 +260,24 @@ describe('Sound resource', () => {
});
});
- it('should do nothing if already stopped', () => {
- expect(sut.isPlaying()).toBe(false, 'nothing should be playing');
+ it('should stop all tracks even when paused', (done) => {
+ sut.play();
+
+ sut.on('stop', () => {
+ done();
+ });
+
+ setTimeout(() => {
+ expect(sut.isPlaying()).toBe(true, 'should be playing');
+
+ // pause
+ sut.pause();
+
+ expect(sut.isPlaying()).toBe(false, 'should not be playing');
- sut.stop();
+ // stop and rewind
+ sut.stop();
+ }, 500);
});
it('should not have any tracks when stopped', (done) => {
@@ -313,6 +345,97 @@ describe('Sound resource', () => {
sut.play();
});
+
+ describe('wire engine', () => {
+ let engine: ex.Engine;
+
+ beforeEach(() => {
+ engine = TestUtils.engine();
+ engine.start();
+ });
+
+ afterEach(() => {
+ engine.stop();
+ engine = null;
+ });
+
+ it('should stop all tracks when engine is stopped', (done) => {
+ sut.wireEngine(engine);
+ sut.play();
+
+ sut.on('playbackstart', () => {
+ expect(sut.instanceCount()).toBe(1, 'should be one track');
+
+ engine.stop();
+
+ expect(sut.instanceCount()).toBe(0, 'should be no tracks');
+ expect(sut.isPlaying()).toBe(false, 'should not be playing');
+
+ done();
+ });
+ });
+
+ it('should not allow playing tracks when engine is stopped', (done) => {
+ sut.wireEngine(engine);
+ sut.play();
+
+ sut.on('playbackstart', () => {
+ expect(sut.isPlaying()).toBe(true, 'should be playing');
+
+ engine.stop();
+
+ sut.play();
+
+ expect(sut.isPlaying()).toBe(false, 'should not allow playing');
+
+ done();
+ });
+ });
+
+ it('should pause tracks when game is hidden and pauseAudioWhenHidden is true', (done) => {
+ engine.pauseAudioWhenHidden = true;
+ sut.wireEngine(engine);
+ sut.play();
+
+ sut.on('playbackstart', () => {
+ expect(sut.isPlaying()).toBe(true, 'should be playing');
+
+ setTimeout(() => {
+ engine.emit('hidden', new ex.HiddenEvent(engine));
+ }, 100);
+ });
+
+ engine.on('hidden', () => {
+ expect(sut.isPlaying()).toBe(false, 'should pause when hidden');
+ done();
+ });
+ });
+
+ it('should resume tracks when game is visible from hidden and pauseAudioWhenHidden is true', (done) => {
+ engine.pauseAudioWhenHidden = true;
+ sut.wireEngine(engine);
+ sut.play();
+
+ sut.on('playbackstart', () => {
+ expect(sut.isPlaying()).toBe(true, 'should be playing');
+
+ setTimeout(() => {
+ engine.emit('hidden', new ex.HiddenEvent(engine));
+ }, 100);
+ });
+
+ engine.on('hidden', () => {
+ setTimeout(() => {
+ engine.emit('visible', new ex.VisibleEvent(engine));
+ }, 100);
+ });
+
+ engine.on('visible', () => {
+ expect(sut.isPlaying()).toBe(true, 'should resume when visible');
+ done();
+ });
+ });
+ });
});
});
});
diff --git a/src/stories/Audio.stories.ts b/src/stories/Audio.stories.ts
new file mode 100644
index 00000000..21d5b9a9
--- /dev/null
+++ b/src/stories/Audio.stories.ts
@@ -0,0 +1,210 @@
+import { action } from '@storybook/addon-actions';
+import { number, withKnobs } from '@storybook/addon-knobs';
+import playIcon from '@fortawesome/fontawesome-free/svgs/solid/play.svg';
+import pauseIcon from '@fortawesome/fontawesome-free/svgs/solid/pause.svg';
+import stopIcon from '@fortawesome/fontawesome-free/svgs/solid/stop.svg';
+import { Actor, Sound, Loader, Color, Texture, Sprite, NativeSoundEvent, EasingFunctions, NativeSoundProcessedEvent } from '../engine';
+import { withEngine } from './utils';
+
+import guitarLoop from './assets/loop-guitar.mp3';
+import campfireLoop from './assets/loop-campfire.mp3';
+import forestLoop from './assets/loop-forest.mp3';
+
+export default {
+ title: 'Audio',
+ decorators: [withKnobs]
+};
+
+export const playingASound: Story = withEngine(async (game) => {
+ const loader = new Loader();
+ const guitarLoopSound = new Sound(guitarLoop);
+ const playIconTx = new Texture(playIcon);
+ const pauseIconTx = new Texture(pauseIcon);
+ const stopIconTx = new Texture(stopIcon);
+
+ loader.addResources([guitarLoopSound, playIconTx, pauseIconTx, stopIconTx]);
+
+ guitarLoopSound.on('processed', (e: NativeSoundProcessedEvent) => {
+ action('Guitar loop sound processed')(e);
+ });
+
+ await game.start(loader);
+
+ const startOrPauseBtn = new Actor(game.currentScene.camera.x - 42, 50, 32, 32, Color.White);
+ const stopBtn = new Actor(game.currentScene.camera.x, 50, 32, 32, Color.Blue);
+ const playHead = new Actor(game.currentScene.camera.x, 100, 2, 25, Color.White);
+ const playTimeline = new Actor(game.currentScene.camera.x, 100, 250, 3, Color.White);
+
+ const playSprite = new Sprite({ image: playIconTx, x: 0, y: 0, width: 32, height: 32 });
+ playSprite.fill(Color.White);
+ const pauseSprite = new Sprite({ image: pauseIconTx, x: 0, y: 0, width: 32, height: 32 });
+ pauseSprite.fill(Color.White);
+ const stopSprite = new Sprite({ image: stopIconTx, x: 0, y: 0, width: 32, height: 32 });
+ stopSprite.fill(Color.White);
+
+ startOrPauseBtn.addDrawing('play', playSprite);
+ startOrPauseBtn.addDrawing('pause', pauseSprite);
+ startOrPauseBtn.setDrawing('play');
+
+ startOrPauseBtn.enableCapturePointer = true;
+ startOrPauseBtn.on('pointerup', (evt) => {
+ if (!guitarLoopSound.isPlaying()) {
+ guitarLoopSound.play();
+ } else {
+ guitarLoopSound.pause();
+ }
+
+ evt.stopPropagation();
+ });
+ stopBtn.addDrawing(stopSprite);
+ stopBtn.enableCapturePointer = true;
+
+ const playheadStartPos = playTimeline.body.collider.bounds.left;
+ const playheadEndPos = playTimeline.body.collider.bounds.right;
+ let startTime = 0;
+ let elapsedTime = 0;
+
+ playHead.pos.setTo(playheadStartPos, playHead.pos.y);
+
+ guitarLoopSound.on('playbackstart', (e: NativeSoundEvent) => {
+ playHead.pos.setTo(playheadStartPos, playHead.pos.y);
+ if (guitarLoopSound.duration > 0) {
+ startTime = Date.now();
+ elapsedTime = 0;
+ playHead.actions.easeTo(playheadEndPos, playHead.pos.y, guitarLoopSound.duration * 1000, EasingFunctions.Linear);
+ }
+ startOrPauseBtn.setDrawing('pause');
+ action('playbackstart')(e);
+ });
+
+ guitarLoopSound.on('pause', (e) => {
+ elapsedTime = Date.now() - startTime + elapsedTime;
+ playHead.actions.clearActions();
+ startOrPauseBtn.setDrawing('play');
+ action('pause')(e, elapsedTime);
+ });
+
+ guitarLoopSound.on('resume', (e: NativeSoundEvent) => {
+ startTime = Date.now();
+ if (guitarLoopSound.duration > 0) {
+ playHead.actions.easeTo(playheadEndPos, playHead.pos.y, guitarLoopSound.duration * 1000 - elapsedTime, EasingFunctions.Linear);
+ }
+ startOrPauseBtn.setDrawing('pause');
+ action('resume')(e);
+ });
+
+ guitarLoopSound.on('playbackend', (e) => {
+ playHead.actions.clearActions();
+ playHead.pos.setTo(playheadStartPos, playHead.pos.y);
+ startOrPauseBtn.setDrawing('play');
+ action('playbackend')(e);
+ });
+
+ stopBtn.on('pointerup', (evt) => {
+ guitarLoopSound.stop();
+ playHead.pos.setTo(playheadStartPos, playHead.pos.y);
+
+ evt.stopPropagation();
+ });
+
+ game.add(stopBtn);
+ game.add(startOrPauseBtn);
+ game.add(playTimeline);
+ game.add(playHead);
+});
+
+export const multipleTracksAndLooping = withEngine(async (game) => {
+ const loader = new Loader();
+ const guitarLoopSound = new Sound(guitarLoop);
+ const campfireLoopSound = new Sound(campfireLoop);
+ const forestLoopSound = new Sound(forestLoop);
+ const playIconTx = new Texture(playIcon);
+ loader.addResources([guitarLoopSound, campfireLoopSound, forestLoopSound, playIconTx]);
+
+ game.on('visible', () => {
+ action('visible')('Game was visible, sound should continue playing');
+ });
+
+ game.on('hidden', () => {
+ action('hidden')('Game was hidden, sound should pause playing');
+ });
+
+ await game.start(loader);
+
+ const playAction = action('play');
+ const startGuitarAt = number('Begin playing guitar after (in milliseconds)', 2000);
+
+ const startBtn = new Actor(game.currentScene.camera.x - 42, 50, 32, 32, Color.White);
+ const playSprite = new Sprite({ image: playIconTx, x: 0, y: 0, width: 32, height: 32 });
+ playSprite.fill(Color.White);
+
+ startBtn.addDrawing(playSprite);
+ startBtn.enableCapturePointer = true;
+ startBtn.on('pointerup', (evt) => {
+ playAction('Playing campfire and forest sounds ');
+ playSprite.fill(Color.Green);
+
+ guitarLoopSound.loop = true;
+ campfireLoopSound.loop = true;
+ forestLoopSound.loop = true;
+ campfireLoopSound.play();
+ forestLoopSound.play();
+
+ setTimeout(() => {
+ playAction('Playing guitar loop');
+ guitarLoopSound.play();
+ }, startGuitarAt);
+
+ evt.stopPropagation();
+ });
+
+ game.add(startBtn);
+});
+
+
+export const volumeLevels = withEngine(async (game) => {
+ const loader = new Loader();
+ const testSound = new Sound(guitarLoop);
+ const playIconTx = new Texture(playIcon);
+
+ loader.addResources([testSound, playIconTx]);
+
+ //click a button to play the sound
+ const playAction = action('play');
+ const startBtn = new Actor(game.currentScene.camera.x - 42, 50, 32, 32, Color.White);
+ const playSprite = new Sprite({ image: playIconTx, x: 0, y: 0, width: 32, height: 32 });
+ playSprite.fill(Color.White);
+ startBtn.addDrawing(playSprite);
+ startBtn.enableCapturePointer = true;
+
+ const volumeOptions = {
+ range: true,
+ min: 0,
+ max: 1,
+ step: 0.1
+ };
+ const initialVolume = number('Initial volume', 0.2, volumeOptions);
+ const delayVolume = number('Adjusted volume', 1, volumeOptions);
+
+ startBtn.on('pointerup', function () {
+ playAction('Playing guitar sound, volume will adjust in 2 seconds');
+
+ playSprite.fill(Color.Green);
+
+ //button will turn red again when song is done
+ if (!testSound.isPlaying()) {
+ testSound.play(initialVolume).then(function () {
+ playSprite.fill(Color.White);
+ });
+
+ //change volume of the sound after 2000 ms to show that
+ //initial setting worked
+ setTimeout(function () {
+ testSound.volume = delayVolume;
+ }, 2000);
+ }
+ });
+ game.add(startBtn);
+
+ await game.start(loader);
+});
diff --git a/src/stories/assets/loop-campfire.mp3 b/src/stories/assets/loop-campfire.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..7d9514ec949f1e815abaaea77a0ddb1855c0c26e
GIT binary patch
literal 1829331
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 |