From 63083c54ec22b9258140b8522a55e8e6864867ae Mon Sep 17 00:00:00 2001 From: Fisto Date: Mon, 21 Sep 2026 18:23:40 +0200 Subject: [PATCH] Add voice processing settings and a speak-time bypass --- CURRENT.md | 8 +++++ docs/voice.md | 9 +++-- poc/public/app.js | 36 +++++++++++++++++++ poc/public/index.html | 9 +++++ poc/src/audio-service.ts | 20 +++++++++++ poc/src/control-bridge.ts | 65 +++++++++++++++++++++++++++++++--- poc/src/preferences.ts | 36 +++++++++++++++++++ poc/src/webui.ts | 39 +++++++++++++++++++- poc/test/media-ui-smoke.mjs | 13 ++++++- src/renderer/puppet/actions.ts | 4 +++ 10 files changed, 230 insertions(+), 9 deletions(-) diff --git a/CURRENT.md b/CURRENT.md index 08be186..77977e3 100644 --- a/CURRENT.md +++ b/CURRENT.md @@ -40,6 +40,14 @@ mobile draft at 390px. - [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`: `previewed: 2`; thumbs served over `/api/media///thumb` (200). + - [x] C72 Voice processing settings and the speak bypass. The TTS panel gained a **voice + processing** block (echo cancellation, auto gain, noise cancellation, and **off while + speaking**), stored in `preferences.voice` and applied live on save. While a TTS burst plays, + the daemon turns every supported gate off and restores the resting state when the queue drains; + `voice.join` applies the resting state after connecting. `voice.processing` gained a + `readOnly` probe (MCP `set_voice_processing` too). Verified live: gates ON at rest → all off + during speech → restored after. Krisp noise suppression is not a checkbox because its getter + ignores the setter in this client build. - [x] C71 Voice processing gates off. The TTS line was ramping in because Discord's automatic gain control (and echo/noise cancellation) were on. New `voice.processing` core action reads and sets the gates (defaults off) plus an optional input volume; MCP `set_voice_processing` exposes diff --git a/docs/voice.md b/docs/voice.md index cde0e71..e7c654f 100644 --- a/docs/voice.md +++ b/docs/voice.md @@ -58,8 +58,13 @@ Voice audio uses the system audio stack, not Discord internals. The daemon owns Discord's own microphone processing fights a TTS line: **automatic gain control** ramps the volume up over the first words (so the start is quiet or clipped), and echo/noise cancellation gates the first syllable. The core reads and turns those gates off (`voice.processing`; MCP -`set_voice_processing`), and every `voice.join` applies it automatically before connecting. Discord -stores these per client, not per account, so a user's own app is unaffected. +`set_voice_processing`, `readOnly: true` to just report). The TTS panel has **voice processing** +checkboxes for the resting state — echo cancellation, auto gain, noise cancellation — plus **off +while speaking**, which turns every gate off for a TTS burst and restores the resting state when +the queue drains. Every `voice.join` applies the resting state after connecting (the core joins +with processing off). Discord stores these per client, not per account, so a user's own app is +unaffected. Krisp noise suppression is not exposed as a checkbox: its getter does not follow the +setter in the current client build, though the preference and API field remain. The host needs a sound server. On Debian and Ubuntu: diff --git a/poc/public/app.js b/poc/public/app.js index 3f751f6..19aa924 100644 --- a/poc/public/app.js +++ b/poc/public/app.js @@ -1788,6 +1788,37 @@ async function refreshAudio() { await refreshSpeech(); } +/* Discord voice processing gates (Settings -> TTS panel). */ +function fillVoiceGates(voice) { + const gates = voice ?? {}; + for (const [id, key] of [ + ["#vp-echo", "echoCancellation"], + ["#vp-agc", "automaticGainControl"], + ["#vp-nc", "noiseCancellation"], + ["#vp-bypass", "bypassWhileSpeaking"] + ]) { + const box = $(id); + if (box) box.checked = gates[key] === true; + } +} + +async function saveVoiceGates() { + const voice = { + noiseSuppression: false, + echoCancellation: $("#vp-echo").checked, + automaticGainControl: $("#vp-agc").checked, + noiseCancellation: $("#vp-nc").checked, + bypassWhileSpeaking: $("#vp-bypass").checked + }; + + try { + const prefs = await VK.api.post("/api/preferences", { voice }); + fillVoiceGates(prefs.voice ?? voice); + } catch (error) { + toast(`voice processing save failed: ${error.message}`, "error"); + } +} + /* Speech settings (server-side preferences). */ async function refreshSpeech() { @@ -1815,6 +1846,7 @@ async function refreshSpeech() { try { const prefs = await VK.api.get("/api/preferences"); state.speech = prefs.tts ?? { provider: null, language: null, voice: null, speed: null, pitch: null }; + fillVoiceGates(prefs.voice); } catch { // preferences are optional } @@ -2728,6 +2760,10 @@ function init() { $("#transcript-refresh").addEventListener("click", () => void refreshTranscript()); $("#transcript-start").addEventListener("click", () => void setCapture(true)); $("#transcript-stop").addEventListener("click", () => void setCapture(false)); + for (const id of ["#vp-echo", "#vp-agc", "#vp-nc", "#vp-bypass"]) { + $(id)?.addEventListener("change", () => void saveVoiceGates()); + } + $("#tts-duck").addEventListener("click", async () => { const on = !$("#tts-duck").classList.contains("vk-chip-on"); try { diff --git a/poc/public/index.html b/poc/public/index.html index 6d0111b..1d18e8b 100644 --- a/poc/public/index.html +++ b/poc/public/index.html @@ -139,6 +139,15 @@ duck on voice queue lines +
+ voice processing + + + + +