UNPKG

phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.

663 lines (546 loc) 20.1 kB
var WebAudioSound = require('../../../src/sound/webaudio/WebAudioSound'); // --------------------------------------------------------------------------- // Mock factory helpers // --------------------------------------------------------------------------- function createMockGainNode () { var gainObj = { value: 1 }; gainObj.setValueAtTime = function (value) { gainObj.value = value; }; return { gain: gainObj, connect: vi.fn(), disconnect: vi.fn() }; } function createMockPannerNode () { return { connect: vi.fn(), disconnect: vi.fn(), positionX: { value: 0 }, positionY: { value: 0 }, positionZ: { value: 0 }, orientationX: { value: 0 }, orientationY: { value: 0 }, orientationZ: { value: -1 }, panningModel: 'equalpower', distanceModel: 'inverse', refDistance: 1, maxDistance: 10000, rolloffFactor: 1, coneInnerAngle: 360, coneOuterAngle: 0, coneOuterGain: 0 }; } function createMockStereoPannerNode () { var panObj = { value: 0 }; panObj.setValueAtTime = function (value) { panObj.value = value; }; return { pan: panObj, connect: vi.fn(), disconnect: vi.fn() }; } function createMockBufferSourceNode () { return { buffer: null, connect: vi.fn(), disconnect: vi.fn(), start: vi.fn(), stop: vi.fn(), playbackRate: { value: 1, setValueAtTime: vi.fn() }, onended: null }; } function createMockManager (duration) { var audioDuration = (duration !== undefined) ? duration : 2.0; var audioBuffer = { duration: audioDuration }; return { game: { cache: { audio: { get: function (key) { return (key === 'test-sound') ? audioBuffer : null; } } } }, context: { currentTime: 0, createGain: function () { return createMockGainNode(); }, createPanner: function () { return createMockPannerNode(); }, createStereoPanner: function () { return createMockStereoPannerNode(); }, createBufferSource: function () { return createMockBufferSourceNode(); } }, destination: {}, sounds: [], rate: 1, detune: 0 }; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('WebAudioSound', function () { var manager; var sound; beforeEach(function () { manager = createMockManager(); sound = new WebAudioSound(manager, 'test-sound'); }); // ----------------------------------------------------------------------- // Constructor // ----------------------------------------------------------------------- describe('constructor', function () { it('should throw when audio key is not found in cache', function () { expect(function () { new WebAudioSound(manager, 'missing-key'); }).toThrow('Audio key "missing-key" not found in cache'); }); it('should set audioBuffer from cache', function () { expect(sound.audioBuffer).not.toBeNull(); expect(sound.audioBuffer.duration).toBe(2.0); }); it('should initialise source and loopSource to null', function () { expect(sound.source).toBeNull(); expect(sound.loopSource).toBeNull(); }); it('should initialise playTime, startTime and loopTime to zero', function () { expect(sound.playTime).toBe(0); expect(sound.startTime).toBe(0); expect(sound.loopTime).toBe(0); }); it('should initialise rateUpdates as an empty array', function () { expect(Array.isArray(sound.rateUpdates)).toBe(true); expect(sound.rateUpdates.length).toBe(0); }); it('should initialise hasEnded and hasLooped to false', function () { expect(sound.hasEnded).toBe(false); expect(sound.hasLooped).toBe(false); }); it('should set duration and totalDuration from the audio buffer', function () { expect(sound.duration).toBe(2.0); expect(sound.totalDuration).toBe(2.0); }); it('should create muteNode and volumeNode gain nodes', function () { expect(sound.muteNode).not.toBeNull(); expect(sound.volumeNode).not.toBeNull(); }); it('should create spatialNode when context supports createPanner', function () { expect(sound.spatialNode).not.toBeNull(); }); it('should create pannerNode when context supports createStereoPanner', function () { expect(sound.pannerNode).not.toBeNull(); }); it('should initialise with isPlaying false and isPaused false', function () { expect(sound.isPlaying).toBe(false); expect(sound.isPaused).toBe(false); }); }); // ----------------------------------------------------------------------- // stopAndRemoveBufferSource // ----------------------------------------------------------------------- describe('stopAndRemoveBufferSource', function () { it('should reset playTime and startTime to zero', function () { sound.playTime = 5; sound.startTime = 5; sound.stopAndRemoveBufferSource(); expect(sound.playTime).toBe(0); expect(sound.startTime).toBe(0); }); it('should set hasEnded to false', function () { sound.hasEnded = true; sound.stopAndRemoveBufferSource(); expect(sound.hasEnded).toBe(false); }); it('should stop and disconnect an existing source node', function () { var mockSource = createMockBufferSourceNode(); sound.source = mockSource; sound.stopAndRemoveBufferSource(); expect(mockSource.stop).toHaveBeenCalled(); expect(mockSource.disconnect).toHaveBeenCalled(); expect(sound.source).toBeNull(); }); it('should also reset loopTime via stopAndRemoveLoopBufferSource', function () { sound.loopTime = 3; sound.stopAndRemoveBufferSource(); expect(sound.loopTime).toBe(0); }); }); // ----------------------------------------------------------------------- // stopAndRemoveLoopBufferSource // ----------------------------------------------------------------------- describe('stopAndRemoveLoopBufferSource', function () { it('should reset loopTime to zero', function () { sound.loopTime = 4; sound.stopAndRemoveLoopBufferSource(); expect(sound.loopTime).toBe(0); }); it('should stop, disconnect and null out an existing loopSource', function () { var mockLoopSource = createMockBufferSourceNode(); sound.loopSource = mockLoopSource; sound.stopAndRemoveLoopBufferSource(); expect(mockLoopSource.stop).toHaveBeenCalled(); expect(mockLoopSource.disconnect).toHaveBeenCalled(); expect(sound.loopSource).toBeNull(); }); }); // ----------------------------------------------------------------------- // getCurrentTime // ----------------------------------------------------------------------- describe('getCurrentTime', function () { it('should return zero when rateUpdates is empty', function () { sound.rateUpdates = []; sound.playTime = 0; manager.context.currentTime = 0; expect(sound.getCurrentTime()).toBe(0); }); it('should calculate elapsed time with a single rate update at rate 1', function () { sound.rateUpdates = [ { time: 0, rate: 1 } ]; sound.playTime = 0; manager.context.currentTime = 3; // elapsed = (3 - 0) * 1 = 3 expect(sound.getCurrentTime()).toBeCloseTo(3, 5); }); it('should calculate elapsed time with a single rate update at rate 2', function () { sound.rateUpdates = [ { time: 0, rate: 2 } ]; sound.playTime = 0; manager.context.currentTime = 2; // elapsed = (2 - 0) * 2 = 4 expect(sound.getCurrentTime()).toBeCloseTo(4, 5); }); it('should accumulate across multiple rate update segments', function () { // Segment 0 → 1 at rate 1 (contributes 1) // Segment 1 → now(3) at rate 2 (contributes (3-1)*2 = 4) sound.rateUpdates = [ { time: 0, rate: 1 }, { time: 1, rate: 2 } ]; sound.playTime = 0; manager.context.currentTime = 3; expect(sound.getCurrentTime()).toBeCloseTo(5, 5); }); }); // ----------------------------------------------------------------------- // getLoopTime // ----------------------------------------------------------------------- describe('getLoopTime', function () { it('should return playTime + duration when a single rate update at rate 1', function () { sound.rateUpdates = [ { time: 0, rate: 1 } ]; sound.playTime = 0; // duration = 2.0 (from audioBuffer mock) // loopTime = playTime + lastUpdate.time + (duration - 0) / rate // = 0 + 0 + 2 / 1 = 2 expect(sound.getLoopTime()).toBeCloseTo(2, 5); }); it('should account for playback rate when calculating loop time', function () { sound.rateUpdates = [ { time: 0, rate: 2 } ]; sound.playTime = 0; // loopTime = 0 + 0 + (2 / 2) = 1 expect(sound.getLoopTime()).toBeCloseTo(1, 5); }); it('should account for a non-zero playTime', function () { sound.rateUpdates = [ { time: 0, rate: 1 } ]; sound.playTime = 5; // loopTime = 5 + 0 + (2 / 1) = 7 expect(sound.getLoopTime()).toBeCloseTo(7, 5); }); }); // ----------------------------------------------------------------------- // stop / pause / resume return values // ----------------------------------------------------------------------- describe('stop', function () { it('should return false when sound is not playing or paused', function () { expect(sound.stop()).toBe(false); }); }); describe('pause', function () { it('should return false when sound is not playing', function () { expect(sound.pause()).toBe(false); }); }); describe('resume', function () { it('should return false when sound is not paused', function () { expect(sound.resume()).toBe(false); }); }); // ----------------------------------------------------------------------- // mute property / setMute // ----------------------------------------------------------------------- describe('mute / setMute', function () { it('should return false (unmuted) after construction', function () { expect(sound.mute).toBe(false); }); it('should update the muteNode gain when set to true', function () { sound.mute = true; expect(sound.muteNode.gain.value).toBe(0); expect(sound.mute).toBe(true); }); it('should update the muteNode gain back to 1 when set to false', function () { sound.mute = true; sound.mute = false; expect(sound.muteNode.gain.value).toBe(1); expect(sound.mute).toBe(false); }); it('setMute should return the sound instance', function () { expect(sound.setMute(true)).toBe(sound); }); }); // ----------------------------------------------------------------------- // volume property / setVolume // ----------------------------------------------------------------------- describe('volume / setVolume', function () { it('should return 1 after construction', function () { expect(sound.volume).toBe(1); }); it('should update volumeNode gain value', function () { sound.volume = 0.5; expect(sound.volumeNode.gain.value).toBeCloseTo(0.5, 5); expect(sound.volume).toBeCloseTo(0.5, 5); }); it('setVolume should return the sound instance', function () { expect(sound.setVolume(0.5)).toBe(sound); }); }); // ----------------------------------------------------------------------- // pan property / setPan // ----------------------------------------------------------------------- describe('pan / setPan', function () { it('should return 0 after construction', function () { expect(sound.pan).toBe(0); }); it('should return 0 when no pannerNode is present', function () { sound.pannerNode = null; expect(sound.pan).toBe(0); }); it('setPan should return the sound instance', function () { expect(sound.setPan(0.5)).toBe(sound); }); }); // ----------------------------------------------------------------------- // rate / setRate // ----------------------------------------------------------------------- describe('rate / setRate', function () { it('should return 1 after construction', function () { expect(sound.rate).toBe(1); }); it('should update currentConfig.rate', function () { sound.rate = 2; expect(sound.currentConfig.rate).toBe(2); expect(sound.rate).toBe(2); }); it('setRate should return the sound instance', function () { expect(sound.setRate(0.5)).toBe(sound); }); }); // ----------------------------------------------------------------------- // detune / setDetune // ----------------------------------------------------------------------- describe('detune / setDetune', function () { it('should return 0 after construction', function () { expect(sound.detune).toBe(0); }); it('should update currentConfig.detune', function () { sound.detune = 100; expect(sound.currentConfig.detune).toBe(100); expect(sound.detune).toBe(100); }); it('setDetune should return the sound instance', function () { expect(sound.setDetune(50)).toBe(sound); }); }); // ----------------------------------------------------------------------- // loop / setLoop // ----------------------------------------------------------------------- describe('loop / setLoop', function () { it('should return false after construction', function () { expect(sound.loop).toBe(false); }); it('should update currentConfig.loop', function () { sound.loop = true; expect(sound.currentConfig.loop).toBe(true); expect(sound.loop).toBe(true); }); it('setLoop should return the sound instance', function () { expect(sound.setLoop(true)).toBe(sound); }); }); // ----------------------------------------------------------------------- // setSeek // ----------------------------------------------------------------------- describe('setSeek', function () { it('should return the sound instance', function () { expect(sound.setSeek(0)).toBe(sound); }); it('should have no effect when sound is stopped', function () { // Neither isPlaying nor isPaused — setter exits early sound.setSeek(1.0); expect(sound.currentConfig.seek).toBe(0); }); }); // ----------------------------------------------------------------------- // applyConfig // ----------------------------------------------------------------------- describe('applyConfig', function () { it('should reset rateUpdates to a single entry with rate 1', function () { sound.rateUpdates.push({ time: 1, rate: 2 }); sound.rateUpdates.push({ time: 2, rate: 3 }); sound.applyConfig(); expect(sound.rateUpdates.length).toBe(1); expect(sound.rateUpdates[0].time).toBe(0); expect(sound.rateUpdates[0].rate).toBe(1); }); }); // ----------------------------------------------------------------------- // update // ----------------------------------------------------------------------- describe('update', function () { it('should emit COMPLETE and reset state when hasEnded is true', function () { var completeFired = false; sound.on('complete', function () { completeFired = true; }); // Simulate a sound that was playing and has now ended naturally sound.isPlaying = true; sound.isPaused = false; sound.hasEnded = true; sound.update(); expect(completeFired).toBe(true); expect(sound.isPlaying).toBe(false); }); it('should emit LOOPED and swap sources when hasLooped is true', function () { var loopedFired = false; sound.on('looped', function () { loopedFired = true; }); var mockLoopSource = createMockBufferSourceNode(); sound.isPlaying = true; sound.hasLooped = true; sound.loopSource = mockLoopSource; sound.loopTime = 5; sound.totalRate = 1; sound.rateUpdates = [ { time: 0, rate: 1 } ]; sound.update(); expect(loopedFired).toBe(true); expect(sound.hasLooped).toBe(false); expect(sound.source).toBe(mockLoopSource); }); }); // ----------------------------------------------------------------------- // destroy // ----------------------------------------------------------------------- describe('destroy', function () { it('should null out audioBuffer, muteNode, volumeNode after destroy', function () { sound.destroy(); expect(sound.audioBuffer).toBeNull(); expect(sound.muteNode).toBeNull(); expect(sound.volumeNode).toBeNull(); }); it('should null out pannerNode and spatialNode after destroy', function () { sound.destroy(); expect(sound.pannerNode).toBeNull(); expect(sound.spatialNode).toBeNull(); }); it('should set pendingRemove to true', function () { sound.destroy(); expect(sound.pendingRemove).toBe(true); }); it('should be a no-op when called a second time', function () { sound.destroy(); // Second call should not throw expect(function () { sound.destroy(); }).not.toThrow(); }); }); });