Skip to content

Commit

Permalink
fix: bug where incorrect track was removed when stop/start in rapid s…
Browse files Browse the repository at this point in the history
…uccession (#2805)

This PR fixes a bug that caused looping Sound to be unstoppable when stopped then started in rapid succession. There were two issues, an errant await and not checking the result of an `indexOf` for -1
  • Loading branch information
eonarheim authored Nov 26, 2023
1 parent 44ddc31 commit 5be00b5
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

- Fixed async issue where sound could not be stopped if stop()/start() were called in rapid succession
- Fixed issue with input mapper where `keyboard.wasPressed(...)` did not fire
- Fixed issue issue where TileMaps would not properly draw Tiles when setup in screen space coordinates
- Fixed issue where the ex.Line graphics bounds were incorrect causing erroneous offscreen culling
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"core:bundle:esm": "webpack --progress --config webpack.config.js --mode production --env output=esm",
"core:watch": "npm run core:bundle -- --watch",
"lint": "npm run eslint && npm run eslint:spec",
"lint:fix": "npm run eslint --fix && npm run eslint:spec --fix",
"eslint": "eslint \"src/engine/**/*.ts\"",
"eslint:sandbox": "eslint \"sandbox/**/*.ts\"",
"eslint:spec": "eslint \"src/spec/**/*.ts\"",
Expand Down
Binary file added sandbox/tests/sound-loop/gba1complete.mp3
Binary file not shown.
13 changes: 13 additions & 0 deletions sandbox/tests/sound-loop/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sound Loop Test</title>
</head>
<body>
Stopping a looping sounds should stop all running tracks
<script src="../../lib/excalibur.js"></script>
<script src="index.js"></script>
</body>
</html>
48 changes: 48 additions & 0 deletions sandbox/tests/sound-loop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@


var game = new ex.Engine({
width: 400,
height: 400
});

var sound = new ex.Sound('./gba1complete.mp3');
sound.volume = .05;
sound.loop = true;


var loader = new ex.Loader();
loader.addResource(sound)

class BaseScene extends ex.Scene {
constructor(public name) {
super();
}
override onActivate(): void {
console.log('activate', this.name);
sound.play();
}
override onDeactivate(): void {
console.log('deactivate', this.name);
sound.stop();
}
}

var scene1 = new BaseScene("scene1");
var scene22 = new BaseScene("scene2");
var scene3 = new BaseScene("scene3");

game.add('scene1', scene1);
game.add('scene2', scene22);
game.add('scene3', scene3);

game.start(loader).then(() => {
game.goToScene('scene1');

setTimeout(() => {
game.goToScene('scene1');
sound.stop();
}, 1000)
});

// going to the same scene again causes the sound to become unstoppable?!?!

5 changes: 5 additions & 0 deletions src/engine/Interfaces/Audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export interface Audio {
*/
isPaused(): boolean;

/**
* Returns if the audio is stopped
*/
isStopped(): boolean;

/**
* Will play the sound or resume if paused
*/
Expand Down
14 changes: 11 additions & 3 deletions src/engine/Resources/Sound/Sound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ export class Sound implements Audio, Loadable<AudioBuffer> {
return this._tracks.some(t => t.isPaused());
}

public isStopped(): boolean {
return this._tracks.some(t => t.isStopped());
}

/**
* Play the sound, returns a promise that resolves when the sound is done playing
* An optional volume argument can be passed in to play the sound. Max volume is 1.0
Expand Down Expand Up @@ -355,16 +359,20 @@ export class Sound implements Audio, Loadable<AudioBuffer> {
* Starts playback, returns a promise that resolves when playback is complete
*/
private async _startPlayback(): Promise<boolean> {
const track = await this._getTrackInstance(this.data);
const track = this._getTrackInstance(this.data);

const complete = await track.play(() => {
this.events.emit('playbackstart', new NativeSoundEvent(this, track));
this.logger.debug('Playing new instance for sound', this.path);
});

// when done, remove track
this.events.emit('playbackend', new NativeSoundEvent(this, track));
this._tracks.splice(this.getTrackId(track), 1);

// cleanup any done tracks
const trackId = this.getTrackId(track);
if (trackId !== -1) {
this._tracks.splice(trackId, 1);
}

return complete;
}
Expand Down
21 changes: 12 additions & 9 deletions src/engine/Resources/Sound/WebAudioInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { StateMachine } from '../../Util/StateMachine';
import { Audio } from '../../Interfaces/Audio';
import { clamp } from '../../Math/util';
import { AudioContextFactory } from './AudioContext';
import { Future } from '../../Util/Future';

interface SoundState {
startedAt: number;
Expand All @@ -16,10 +17,8 @@ export class WebAudioInstance implements Audio {
private _instance: AudioBufferSourceNode;
private _audioContext: AudioContext = AudioContextFactory.create();
private _volumeNode = this._audioContext.createGain();
private _playingResolve: (value: boolean) => void;
private _playingPromise = new Promise<boolean>((resolve) => {
this._playingResolve = resolve;
});

private _playingFuture = new Future<boolean>();
private _stateMachine = StateMachine.create({
start: 'STOPPED',
states: {
Expand All @@ -42,7 +41,7 @@ export class WebAudioInstance implements Audio {
onExit: ({to}) => {
// If you've exited early only resolve if explicitly STOPPED
if (to === 'STOPPED') {
this._playingResolve(true);
this._playingFuture.resolve(true);
}
// Whenever you're not playing... you stop!
this._instance.onended = null; // disconnect the wired on-end handler
Expand All @@ -63,7 +62,7 @@ export class WebAudioInstance implements Audio {
onEnter: ({data}: {from: string, data: SoundState}) => {
data.pausedAt = 0;
data.startedAt = 0;
this._playingResolve(true);
this._playingFuture.resolve(true);
},
transitions: ['PLAYING', 'PAUSED', 'SEEK']
},
Expand Down Expand Up @@ -94,7 +93,7 @@ export class WebAudioInstance implements Audio {
private _handleEnd() {
if (!this.loop) {
this._instance.onended = () => {
this._playingResolve(true);
this._playingFuture.resolve(true);
};
}
}
Expand All @@ -110,7 +109,7 @@ export class WebAudioInstance implements Audio {
this._instance.loop = value;
if (!this.loop) {
this._instance.onended = () => {
this._playingResolve(true);
this._playingFuture.resolve(true);
};
}
}
Expand Down Expand Up @@ -168,11 +167,15 @@ export class WebAudioInstance implements Audio {
return this._stateMachine.in('PAUSED') || this._stateMachine.in('SEEK');
}

public isStopped() {
return this._stateMachine.in('STOPPED');
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
public play(playStarted: () => any = () => {}) {
this._playStarted = playStarted;
this._stateMachine.go('PLAYING');
return this._playingPromise;
return this._playingFuture.promise;
}

public pause() {
Expand Down
20 changes: 9 additions & 11 deletions src/spec/SoundSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,6 @@ describe('Sound resource', () => {

it('should not have any tracks when stopped', (done) => {
sut.load().then(() => {
sut.play();

sut.once('playbackstart', () => {
expect(sut.instanceCount()).toBe(1, 'should be one track');

Expand All @@ -306,13 +304,12 @@ describe('Sound resource', () => {

done();
});
sut.play();
});
});

it('should not remove instance if paused', (done) => {
sut.load().then(() => {
sut.play();

sut.once('playbackstart', () => {
expect(sut.instanceCount()).toBe(1, 'should be one track');

Expand All @@ -322,6 +319,7 @@ describe('Sound resource', () => {

done();
});
sut.play();
});
});

Expand All @@ -330,17 +328,17 @@ describe('Sound resource', () => {
sut.loop = false;
// start playing first track
sut.play().then(() => {
expect(sut.instanceCount()).toBe(1, 'should be one track');
expect(sut.instanceCount()).withContext('should be on track').toBe(1);
});

// wait 250ms then play 2nd track
setTimeout(() => {
sut.on('playbackstart', () => {
expect(sut.instanceCount()).toBe(2, 'should be two simultaneous tracks');
expect(sut.instanceCount()).withContext('should be two simultaneous tracks').toBe(2);
});

sut.play().then(() => {
expect(sut.instanceCount()).toBe(0, 'should be no tracks');
expect(sut.instanceCount()).withContext('should be no tracks').toBe(0);
done();
});
}, 250);
Expand Down Expand Up @@ -415,7 +413,6 @@ describe('Sound resource', () => {
it('should stop all tracks when engine is stopped', (done) => {
sut.load().then(() => {
sut.wireEngine(engine);
sut.play();

sut.once('playbackstart', () => {
expect(sut.instanceCount()).toBe(1, 'should be one track');
Expand All @@ -427,14 +424,13 @@ describe('Sound resource', () => {

done();
});
sut.play();
});
});

it('should not allow playing tracks when engine is stopped', (done) => {
sut.load().then(() => {
sut.wireEngine(engine);
sut.play();

sut.once('playbackstart', () => {
expect(sut.isPlaying()).toBe(true, 'should be playing');

Expand All @@ -446,6 +442,7 @@ describe('Sound resource', () => {

done();
});
sut.play();
});
});

Expand Down Expand Up @@ -476,7 +473,6 @@ describe('Sound resource', () => {
sut.load().then(() => {
engine.pauseAudioWhenHidden = true;
sut.wireEngine(engine);
sut.play();

sut.once('playbackstart', () => {
expect(sut.isPlaying()).toBe(true, 'should be playing');
Expand All @@ -486,6 +482,8 @@ describe('Sound resource', () => {
}, 100);
});

sut.play();

engine.once('hidden', () => {
setTimeout(() => {
engine.emit('visible', new ex.VisibleEvent(engine));
Expand Down

0 comments on commit 5be00b5

Please sign in to comment.