Restore useSound and add looping spatial emitSound
This commit is contained in:
@@ -169,6 +169,10 @@ export class AudioEngine {
|
||||
return this.outputMode;
|
||||
}
|
||||
|
||||
getOutputMode(): OutputMode {
|
||||
return this.outputMode;
|
||||
}
|
||||
|
||||
toggleLoopback(): boolean {
|
||||
this.loopbackEnabled = !this.loopbackEnabled;
|
||||
this.rebuildOutboundEffectGraph();
|
||||
|
||||
111
client/src/audio/itemEmitRuntime.ts
Normal file
111
client/src/audio/itemEmitRuntime.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { HEARING_RADIUS, type WorldItem } from '../state/gameState';
|
||||
import { AudioEngine } from './audioEngine';
|
||||
|
||||
type EmitOutput = {
|
||||
soundUrl: string;
|
||||
element: HTMLAudioElement;
|
||||
source: MediaElementAudioSourceNode;
|
||||
gain: GainNode;
|
||||
panner: StereoPannerNode | null;
|
||||
};
|
||||
|
||||
export class ItemEmitRuntime {
|
||||
private readonly outputs = new Map<string, EmitOutput>();
|
||||
|
||||
constructor(
|
||||
private readonly audio: AudioEngine,
|
||||
private readonly resolveSoundUrl: (soundPath: string) => string,
|
||||
) {}
|
||||
|
||||
cleanup(itemId: string): void {
|
||||
const output = this.outputs.get(itemId);
|
||||
if (!output) return;
|
||||
output.element.pause();
|
||||
output.element.src = '';
|
||||
output.source.disconnect();
|
||||
output.gain.disconnect();
|
||||
output.panner?.disconnect();
|
||||
this.outputs.delete(itemId);
|
||||
}
|
||||
|
||||
cleanupAll(): void {
|
||||
for (const itemId of Array.from(this.outputs.keys())) {
|
||||
this.cleanup(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
async sync(items: Iterable<WorldItem>): Promise<void> {
|
||||
const validIds = new Set<string>();
|
||||
await this.audio.ensureContext();
|
||||
const audioCtx = this.audio.context;
|
||||
if (!audioCtx) return;
|
||||
|
||||
for (const item of items) {
|
||||
const soundUrl = this.resolveSoundUrl(String(item.emitSound ?? '').trim());
|
||||
if (!soundUrl || item.carrierId) {
|
||||
this.cleanup(item.id);
|
||||
continue;
|
||||
}
|
||||
validIds.add(item.id);
|
||||
const existing = this.outputs.get(item.id);
|
||||
if (existing && existing.soundUrl === soundUrl) {
|
||||
continue;
|
||||
}
|
||||
if (existing) {
|
||||
this.cleanup(item.id);
|
||||
}
|
||||
const element = new Audio(soundUrl);
|
||||
element.loop = true;
|
||||
element.preload = 'none';
|
||||
element.crossOrigin = 'anonymous';
|
||||
const source = audioCtx.createMediaElementSource(element);
|
||||
const gain = audioCtx.createGain();
|
||||
gain.gain.value = 0;
|
||||
let panner: StereoPannerNode | null = null;
|
||||
source.connect(gain);
|
||||
if (this.audio.supportsStereoPanner()) {
|
||||
panner = audioCtx.createStereoPanner();
|
||||
gain.connect(panner).connect(audioCtx.destination);
|
||||
} else {
|
||||
gain.connect(audioCtx.destination);
|
||||
}
|
||||
this.outputs.set(item.id, { soundUrl, element, source, gain, panner });
|
||||
void element.play().catch(() => undefined);
|
||||
}
|
||||
|
||||
for (const itemId of Array.from(this.outputs.keys())) {
|
||||
if (!validIds.has(itemId)) {
|
||||
this.cleanup(itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSpatialAudio(items: Map<string, WorldItem>, playerPosition: { x: number; y: number }): void {
|
||||
const audioCtx = this.audio.context;
|
||||
if (!audioCtx) return;
|
||||
|
||||
for (const [itemId, output] of this.outputs.entries()) {
|
||||
const item = items.get(itemId);
|
||||
if (!item || item.carrierId) {
|
||||
output.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.05);
|
||||
continue;
|
||||
}
|
||||
const dist = Math.hypot(item.x - playerPosition.x, item.y - playerPosition.y);
|
||||
let gainValue = 0;
|
||||
let panValue = 0;
|
||||
if (dist < HEARING_RADIUS) {
|
||||
gainValue = Math.pow(1 - dist / HEARING_RADIUS, 2);
|
||||
panValue = Math.sin(((item.x - playerPosition.x) / HEARING_RADIUS) * (Math.PI / 2));
|
||||
}
|
||||
if (dist <= 1) {
|
||||
gainValue = 1;
|
||||
panValue = 0;
|
||||
}
|
||||
output.gain.gain.linearRampToValueAtTime(gainValue, audioCtx.currentTime + 0.1);
|
||||
if (output.panner) {
|
||||
const resolvedPan = this.audio.getOutputMode() === 'mono' ? 0 : Math.max(-1, Math.min(1, panValue));
|
||||
output.panner.pan.linearRampToValueAtTime(resolvedPan, audioCtx.currentTime + 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user