diff --git a/client/public/version.js b/client/public/version.js index c56f8ba..175da18 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.22 R158"; +window.CHGRID_WEB_VERSION = "2026.02.22 R159"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/audio/audioEngine.ts b/client/src/audio/audioEngine.ts index 9f8a68f..0c1be6d 100644 --- a/client/src/audio/audioEngine.ts +++ b/client/src/audio/audioEngine.ts @@ -204,6 +204,18 @@ export class AudioEngine { return this.loopbackEnabled; } + /** Returns current loopback monitor state. */ + isLoopbackEnabled(): boolean { + return this.loopbackEnabled; + } + + /** Sets loopback monitor state directly. */ + setLoopbackEnabled(enabled: boolean): boolean { + this.loopbackEnabled = enabled; + this.rebuildOutboundEffectGraph(); + return this.loopbackEnabled; + } + async attachRemoteStream( peer: SpatialPeerRuntime, stream: MediaStream, diff --git a/client/src/main.ts b/client/src/main.ts index eb1cfcf..b4b1aa6 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -191,6 +191,7 @@ const itemEmitRuntime = new ItemEmitRuntime(audio, resolveIncomingSoundUrl, getI let internalClipboardText = ''; let replaceTextOnNextType = false; let pendingEscapeDisconnect = false; +let micGainLoopbackRestoreState: boolean | null = null; let helpViewerLines: string[] = []; let helpViewerIndex = 0; let audioLayers: AudioLayerState = { @@ -1026,6 +1027,15 @@ function describeMediaError(error: unknown): string { return mediaSession.describeMediaError(error); } +/** Restores loopback state captured when entering microphone gain edit mode. */ +function restoreLoopbackAfterMicGainEdit(): void { + if (micGainLoopbackRestoreState === null) { + return; + } + audio.setLoopbackEnabled(micGainLoopbackRestoreState); + micGainLoopbackRestoreState = null; +} + /** Builds dependencies shared by connect/disconnect flow helpers. */ function getConnectionFlowDeps(): ConnectFlowDeps { return { @@ -1066,6 +1076,7 @@ async function connect(): Promise { function disconnect(): void { runDisconnectFlow(getConnectionFlowDeps()); pendingEscapeDisconnect = false; + restoreLoopbackAfterMicGainEdit(); } const onMessage = createOnMessageHandler({ @@ -1199,6 +1210,8 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void { state.nicknameInput = formatSteppedNumber(audio.getOutboundInputGain(), MIC_INPUT_GAIN_STEP); state.cursorPos = state.nicknameInput.length; replaceTextOnNextType = true; + micGainLoopbackRestoreState = audio.isLoopbackEnabled(); + audio.setLoopbackEnabled(true); updateStatus(`Set microphone gain: ${state.nicknameInput}`); audio.sfxUiBlip(); return; @@ -1522,6 +1535,7 @@ function handleMicGainEditModeInput(code: string, key: string, ctrlKey: boolean) state.nicknameInput = formatSteppedNumber(next, MIC_INPUT_GAIN_STEP); state.cursorPos = state.nicknameInput.length; replaceTextOnNextType = false; + audio.setOutboundInputGain(next); updateStatus(state.nicknameInput); if (Math.abs(next - base) < 1e-9 || Math.abs(next - attempted) > 1e-9) { audio.sfxUiCancel(); @@ -1549,6 +1563,7 @@ function handleMicGainEditModeInput(code: string, key: string, ctrlKey: boolean) persistMicInputGain(applied); state.mode = 'normal'; replaceTextOnNextType = false; + restoreLoopbackAfterMicGainEdit(); updateStatus(`Microphone gain set to ${formatSteppedNumber(applied, MIC_INPUT_GAIN_STEP)}.`); audio.sfxUiConfirm(); return; @@ -1557,6 +1572,7 @@ function handleMicGainEditModeInput(code: string, key: string, ctrlKey: boolean) if (editAction === 'cancel') { state.mode = 'normal'; replaceTextOnNextType = false; + restoreLoopbackAfterMicGainEdit(); updateStatus('Cancelled.'); audio.sfxUiCancel(); return;