187 lines
6.9 KiB
JavaScript
187 lines
6.9 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { setupDom, teardownDom, FakeAudio } from "./framework/dom.js";
|
|
import { AudioManager, EFFECT_PRESETS } from "../client/js/audio.js";
|
|
|
|
// A minimal Web Audio stand-in that records every node it hands out and the
|
|
// connections made between them.
|
|
function fakeAudioContext() {
|
|
const nodes = [];
|
|
const node = (kind) => {
|
|
const created = {
|
|
kind,
|
|
connected: [],
|
|
connect(target) {
|
|
this.connected.push(target);
|
|
return target;
|
|
},
|
|
disconnect() {
|
|
this.disconnected = true;
|
|
},
|
|
};
|
|
nodes.push(created);
|
|
return created;
|
|
};
|
|
return {
|
|
nodes,
|
|
state: "running",
|
|
destination: node("destination"),
|
|
resume() {
|
|
return Promise.resolve();
|
|
},
|
|
createMediaElementSource: () => node("source"),
|
|
createBiquadFilter: () =>
|
|
Object.assign(node("biquad"), { type: "", frequency: { value: 0 }, Q: { value: 0 } }),
|
|
createGain: () => Object.assign(node("gain"), { gain: { value: 0 } }),
|
|
createDelay: () => Object.assign(node("delay"), { delayTime: { value: 0 } }),
|
|
};
|
|
}
|
|
|
|
export class AudioTest extends TestCase {
|
|
test_effect_sources_pick_the_chosen_library_files() {
|
|
const manager = new AudioManager();
|
|
this.assertEqual(manager.effects.click, "assets/sounds/button_click.mp3");
|
|
this.assertEqual(manager.effects.complete, "assets/sounds/build_complete.mp3");
|
|
this.assertEqual(manager.effects.notification, "assets/sounds/notification.mp3");
|
|
this.assertEqual(manager.effects.bomb, "assets/sounds/death_flash.flac");
|
|
this.assertEqual(manager.musicSources, [
|
|
"assets/sounds/crashed_ship.ogg",
|
|
"assets/sounds/dark_city.ogg",
|
|
"assets/sounds/constellation.ogg",
|
|
"assets/sounds/patrol.ogg",
|
|
]);
|
|
}
|
|
|
|
async test_plays_effects_and_mutes() {
|
|
const env = await setupDom();
|
|
try {
|
|
FakeAudio.reset();
|
|
const manager = new AudioManager();
|
|
manager.play("click");
|
|
this.assertEqual(FakeAudio.playedSources, ["assets/sounds/button_click.mp3"]);
|
|
|
|
// The bombardment passes an explicit, zoom-scaled volume.
|
|
manager.play("bomb", { volume: 0.25 });
|
|
const bomb = FakeAudio.instances[FakeAudio.instances.length - 1];
|
|
this.assertEqual(bomb.src, "assets/sounds/death_flash.flac");
|
|
this.assertApprox(bomb.volume, 0.25, 1e-9, "the volume option overrides the default");
|
|
|
|
manager.setEnabled(false);
|
|
const muted = FakeAudio.playedSources.length;
|
|
manager.play("click");
|
|
manager.play("bomb", { volume: 0.9 });
|
|
this.assertEqual(FakeAudio.playedSources.length, muted, "muted effects are silent");
|
|
|
|
let changed = null;
|
|
manager.onChange = (value) => {
|
|
changed = value;
|
|
};
|
|
manager.setEnabled(true);
|
|
this.assertEqual(changed, true, "the toggle reports its new state");
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_bombardment_uses_a_lowpass_and_echo() {
|
|
const env = await setupDom();
|
|
try {
|
|
FakeAudio.reset();
|
|
const context = fakeAudioContext();
|
|
const manager = new AudioManager({ contextFactory: () => context });
|
|
// Keep the graph-cleanup timer short so the test does not linger.
|
|
manager.presets.bomb.echoTail = 0.01;
|
|
manager.play("bomb", { volume: 0.4, duration: 0.01 });
|
|
|
|
const filter = context.nodes.find((node) => node.kind === "biquad");
|
|
this.assertNotNull(filter, "the clip runs through a biquad filter");
|
|
this.assertEqual(filter.type, "lowpass", "the blasts are muffled");
|
|
this.assertApprox(filter.frequency.value, EFFECT_PRESETS.bomb.lowpassHz);
|
|
|
|
const delay = context.nodes.find((node) => node.kind === "delay");
|
|
this.assertNotNull(delay, "an echo delay is built");
|
|
this.assertApprox(delay.delayTime.value, EFFECT_PRESETS.bomb.echoDelay);
|
|
const feedback = delay.connected.find(
|
|
(node) => node.kind === "gain" && node.connected.includes(delay)
|
|
);
|
|
this.assertNotNull(feedback, "the delay feeds back on itself");
|
|
this.assertApprox(feedback.gain.value, EFFECT_PRESETS.bomb.echoFeedback);
|
|
|
|
const played = FakeAudio.instances.find((audio) => audio.src === "assets/sounds/death_flash.flac");
|
|
this.assertNotNull(played, "the clip is played through the graph");
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_music_plays_a_shuffled_playlist() {
|
|
const env = await setupDom();
|
|
try {
|
|
FakeAudio.reset();
|
|
// random() near 1 leaves the shuffle order untouched: crashed_ship first.
|
|
const manager = new AudioManager({ random: () => 0.999 });
|
|
manager.requestMusic();
|
|
|
|
const first = FakeAudio.instances.find(
|
|
(audio) => audio.src === "assets/sounds/crashed_ship.ogg"
|
|
);
|
|
this.assertNotNull(first, "the first track was created");
|
|
this.assertFalse(first.loop, "a playlist track does not loop itself");
|
|
this.assertEqual(first.plays, 1, "the first track was started once");
|
|
this.assertTrue(typeof first.onended === "function", "the track knows what follows");
|
|
|
|
// A finished track hands over to the next one in the playlist.
|
|
first.onended();
|
|
const second = FakeAudio.instances.find(
|
|
(audio) => audio.src === "assets/sounds/dark_city.ogg"
|
|
);
|
|
this.assertNotNull(second, "the second track follows");
|
|
this.assertEqual(second.plays, 1, "the second track was started once");
|
|
|
|
second.onended();
|
|
const third = FakeAudio.instances.find(
|
|
(audio) => audio.src === "assets/sounds/constellation.ogg"
|
|
);
|
|
this.assertNotNull(third, "the third track follows");
|
|
this.assertEqual(third.plays, 1, "the third track was started once");
|
|
|
|
third.onended();
|
|
const fourth = FakeAudio.instances.find(
|
|
(audio) => audio.src === "assets/sounds/patrol.ogg"
|
|
);
|
|
this.assertNotNull(fourth, "the fourth track follows");
|
|
this.assertEqual(fourth.plays, 1, "the fourth track was started once");
|
|
|
|
// After the last track the playlist reshuffles instead of falling silent.
|
|
const before = FakeAudio.instances.length;
|
|
fourth.onended();
|
|
this.assertGreater(FakeAudio.instances.length, before, "playback wraps around");
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_music_stops_and_mutes() {
|
|
const env = await setupDom();
|
|
try {
|
|
FakeAudio.reset();
|
|
const manager = new AudioManager({ random: () => 0.999 });
|
|
manager.requestMusic();
|
|
const music = FakeAudio.instances.find(
|
|
(audio) => audio.src === "assets/sounds/crashed_ship.ogg"
|
|
);
|
|
this.assertNotNull(music, "the music element was created");
|
|
|
|
manager.stopMusic();
|
|
this.assertTrue(music.paused, "stopping pauses the music");
|
|
|
|
// Muting while music is wanted stops it; unmuting starts it again.
|
|
manager.requestMusic();
|
|
manager.setEnabled(false);
|
|
manager.setEnabled(true);
|
|
this.assertGreater(FakeAudio.instances.length, 1, "music restarts when unmuted");
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
}
|