Fix extra sound fetching
All checks were successful
Build & deploy / Build & publish server image (pull_request) Has been skipped
Build & deploy / Build & deploy website (pull_request) Successful in 29s

This commit is contained in:
Andras Schmelczer 2026-09-15 21:03:14 +01:00
commit 38e3f8cf7a
3 changed files with 266 additions and 35 deletions

View file

@ -73,7 +73,6 @@ function killConfirmed(victimName?: string, streak = 1, charge = 0) {
}
}
function resetFeedbackHud(): void {
root?.remove();
root = undefined;

View file

@ -13,7 +13,6 @@ import {
RemoteCallsForObjects,
UpdatePropertyCommand,
} from 'shared';
import { FeedbackHud } from '../feedback-hud';
import type { Game } from '../game';
import { serverTimeline } from '../helper/server-timeline';
import { localCharacterPredictor } from '../helper/prediction/local-character-predictor';

View file

@ -8,37 +8,51 @@ import { options } from './options-handler';
export const Sounds = { hit: 'hit', shoot: 'shoot', click: 'click' } as const;
export type Sound = (typeof Sounds)[keyof typeof Sounds];
let sounds: Record<Sound, HTMLAudioElement>;
const sounds: Partial<Record<Sound, AudioBuffer>> = {};
const maxVoices = 16;
const voices = new Set<{ source: AudioBufferSourceNode; gain: GainNode }>();
let context: AudioContext | undefined;
let initialization: Promise<void> | undefined;
let isAmbientPlaying = false;
const ambient = new Audio(ambientAudio);
let initialized = false;
let ambientInitialized = false;
async function initializeSound(src: string): Promise<HTMLAudioElement> {
const snd = new Audio(src);
snd.muted = true;
await snd.play().catch(() => undefined);
snd.pause();
snd.muted = false;
snd.currentTime = 0;
return snd;
async function initializeEffects() {
try {
context = new AudioContext({ latencyHint: 'interactive' });
} catch {
return;
}
const audioContext = context;
// Unlock during the initiating gesture, before any network or decode work.
void audioContext.resume().catch(() => undefined);
await Promise.all(
Object.entries({ hit: hitSound, shoot: shootSound, click: clickSound }).map(
async ([name, src]) => {
try {
const response = await fetch(src);
if (response.ok) {
sounds[name as Sound] = await audioContext.decodeAudioData(
await response.arrayBuffer(),
);
}
} catch {
// Missing audio must not interrupt gameplay or prevent other effects loading.
}
},
),
);
}
async function initialize(
onPlayKeypress: () => unknown = () => null,
onPauseKeypress: () => unknown = () => null,
async function initializeAmbient(
onPlayKeypress: () => unknown,
onPauseKeypress: () => unknown,
) {
ambient.muted = true;
ambient.volume = 0.5;
ambient.loop = true;
// Unlock every audio element during the initiating click, before awaiting any one.
const ambientReady = ambient.play().catch(() => undefined);
const [hit, shoot, click] = await Promise.all(
[hitSound, shootSound, clickSound].map(initializeSound),
);
sounds = { hit, shoot, click };
await ambientReady;
initialized = true;
await ambient.play().catch(() => undefined);
ambientInitialized = true;
ambient.onpause = onPauseKeypress;
ambient.onplay = onPlayKeypress;
@ -49,32 +63,65 @@ async function initialize(
}
}
function initialize(
onPlayKeypress: () => unknown = () => null,
onPauseKeypress: () => unknown = () => null,
): Promise<void> {
if (!initialization) {
initialization = initializeEffects();
// Streaming music can take much longer to load than the short effects.
void initializeAmbient(onPlayKeypress, onPauseKeypress);
}
return initialization;
}
function play(snd: Sound, volume = 1, playbackRate = 1) {
if (!initialized || !options.soundsEnabled) {
const buffer = sounds[snd];
if (!context || !buffer || !options.soundsEnabled) {
return;
}
if (context.state !== 'running') {
void context.resume().catch(() => undefined);
return;
}
const pooled = sounds[snd];
const isBusy = !pooled.paused && !pooled.ended;
const audio = isBusy ? (pooled.cloneNode(true) as HTMLAudioElement) : pooled;
if (!isBusy) {
audio.currentTime = 0;
// Delayed network packets can deliver many effects together. Keep their audio work bounded.
if (voices.size >= maxVoices) {
const oldest = voices.values().next().value!;
oldest.source.stop();
oldest.source.disconnect();
oldest.gain.disconnect();
voices.delete(oldest);
}
audio.volume = clamp01(volume);
audio.playbackRate = playbackRate;
void audio.play().catch(() => undefined);
const source = context.createBufferSource();
const gain = context.createGain();
source.buffer = buffer;
source.playbackRate.value = playbackRate;
gain.gain.value = clamp01(volume);
source.connect(gain);
gain.connect(context.destination);
const voice = { source, gain };
voices.add(voice);
source.onended = () => {
if (voices.delete(voice)) {
source.disconnect();
gain.disconnect();
}
};
source.start();
}
function playAmbient() {
isAmbientPlaying = true;
if (initialized) {
if (ambientInitialized) {
void ambient.play().catch(() => undefined);
}
}
function stopAmbient() {
isAmbientPlaying = false;
if (initialized) {
if (ambientInitialized) {
ambient.pause();
}
}