audio: add master volume on -/= and move effect value to shift -/=; route connect flow notices to chat buffer

This commit is contained in:
Jage9
2026-02-22 18:33:55 -05:00
parent 12d3c62916
commit f2734659d2
9 changed files with 93 additions and 13 deletions

View File

@@ -31,6 +31,7 @@ type OutputMode = 'stereo' | 'mono';
export class AudioEngine {
private audioCtx: AudioContext | null = null;
private masterGainNode: GainNode | null = null;
private sfxGainNode: GainNode | null = null;
private readonly sampleCache = new Map<string, AudioBuffer>();
private readonly sampleLoaders = new Map<string, Promise<AudioBuffer>>();
@@ -43,6 +44,7 @@ export class AudioEngine {
private loopbackEnabled = false;
private loopbackRuntime: EffectRuntime | null = null;
private outputMode: OutputMode = 'stereo';
private masterVolume = 50;
private voiceLayerEnabled = true;
private effectIndex = EFFECT_SEQUENCE.findIndex((effect) => effect.id === 'off');
private readonly effectValues: Record<EffectId, number> = {
@@ -61,8 +63,11 @@ export class AudioEngine {
(window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!Ctor) return;
this.audioCtx = new Ctor();
this.masterGainNode = this.audioCtx.createGain();
this.masterGainNode.gain.value = this.masterVolume / 100;
this.masterGainNode.connect(this.audioCtx.destination);
this.sfxGainNode = this.audioCtx.createGain();
this.sfxGainNode.connect(this.audioCtx.destination);
this.sfxGainNode.connect(this.masterGainNode);
}
if (this.audioCtx.state === 'suspended') {
await this.audioCtx.resume();
@@ -73,6 +78,10 @@ export class AudioEngine {
return this.audioCtx;
}
getOutputDestinationNode(): AudioNode | null {
return this.masterGainNode ?? this.audioCtx?.destination ?? null;
}
supportsStereoPanner(): boolean {
return !!this.audioCtx && typeof this.audioCtx.createStereoPanner === 'function';
}
@@ -168,6 +177,23 @@ export class AudioEngine {
this.outputMode = mode;
}
setMasterVolume(value: number): number {
const next = Math.max(0, Math.min(100, Number.isFinite(value) ? Math.round(value) : 50));
this.masterVolume = next;
if (this.masterGainNode && this.audioCtx) {
this.masterGainNode.gain.setValueAtTime(next / 100, this.audioCtx.currentTime);
}
return this.masterVolume;
}
adjustMasterVolume(step: number): number {
return this.setMasterVolume(this.masterVolume + step);
}
getMasterVolume(): number {
return this.masterVolume;
}
toggleOutputMode(): OutputMode {
this.outputMode = this.outputMode === 'stereo' ? 'mono' : 'stereo';
return this.outputMode;
@@ -245,11 +271,11 @@ export class AudioEngine {
if (this.supportsStereoPanner()) {
pannerNode = this.audioCtx.createStereoPanner();
if (this.voiceLayerEnabled) {
gainNode.connect(pannerNode).connect(this.audioCtx.destination);
gainNode.connect(pannerNode).connect(this.masterGainNode ?? this.audioCtx.destination);
}
} else {
if (this.voiceLayerEnabled) {
gainNode.connect(this.audioCtx.destination);
gainNode.connect(this.masterGainNode ?? this.audioCtx.destination);
}
}
@@ -402,7 +428,13 @@ export class AudioEngine {
if (!this.loopbackEnabled) {
return;
}
this.loopbackRuntime = connectEffectChain(this.audioCtx, this.outboundInputGain, this.audioCtx.destination, effect, effectValue);
this.loopbackRuntime = connectEffectChain(
this.audioCtx,
this.outboundInputGain,
this.masterGainNode ?? this.audioCtx.destination,
effect,
effectValue,
);
}
private clampLevel(value: number): number {