From a12bf4837b8e237444f0fceafee04767378455c1 Mon Sep 17 00:00:00 2001 From: gagan Date: Mon, 22 Jun 2026 14:10:56 -0700 Subject: [PATCH] feat(voice): audio-reactive waveform while recording (no live transcript) (#634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(voice): show audio-reactive waveform instead of live transcript When recording, the chat input now displays only a live waveform that accumulates from the left and grows to full width, with bar heights driven by real mic amplitude. The transcribed words are still captured and submitted, just not shown while recording. New bars animate in and flow smoothly at ~16 updates/sec. * feat(voice): auto-gain waveform bar heights to track voice dynamics Normalize each frame's amplitude against a running peak (instant attack, slow release) at capture time and map it with a near-linear curve, so bar heights accurately reflect how loud/soft the voice is regardless of mic gain — replacing the old fixed-gain sqrt curve that saturated near max. --- apps/x/apps/renderer/src/App.tsx | 2 + .../components/chat-input-with-mentions.tsx | 99 ++++++++++++++++--- .../renderer/src/components/chat-sidebar.tsx | 3 + .../x/apps/renderer/src/hooks/useVoiceMode.ts | 42 +++++++- 4 files changed, 130 insertions(+), 16 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index f9019ede..03e8e998 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -6193,6 +6193,7 @@ function App() { isRecording={isActive && isRecording} recordingText={isActive ? voice.interimText : undefined} recordingState={isActive ? (voice.state === 'connecting' ? 'connecting' : 'listening') : undefined} + audioLevelsRef={voice.audioLevelsRef} onStartRecording={isActive ? handleStartRecording : undefined} onSubmitRecording={isActive ? handleSubmitRecording : undefined} onCancelRecording={isActive ? handleCancelRecording : undefined} @@ -6301,6 +6302,7 @@ function App() { isRecording={isRecording} recordingText={voice.interimText} recordingState={voice.state === 'connecting' ? 'connecting' : 'listening'} + audioLevelsRef={voice.audioLevelsRef} onStartRecording={handleStartRecording} onSubmitRecording={handleSubmitRecording} onCancelRecording={handleCancelRecording} diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index 00bcaa8e..412b4bbf 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -224,6 +224,8 @@ interface ChatInputInnerProps { isRecording?: boolean recordingText?: string recordingState?: 'connecting' | 'listening' + /** Live mic amplitude history (RMS per frame) driving the recording waveform. */ + audioLevelsRef?: React.MutableRefObject onStartRecording?: () => void onSubmitRecording?: () => void onCancelRecording?: () => void @@ -260,7 +262,7 @@ function ChatInputInner({ onDraftChange, isRecording, recordingText, - recordingState, + audioLevelsRef, onStartRecording, onSubmitRecording, onCancelRecording, @@ -795,11 +797,10 @@ function ChatInputInner({ > -
- - - {recordingState === 'connecting' ? 'Connecting...' : recordingText || 'Listening...'} - + {/* Audio-reactive waveform only — the transcribed words are intentionally + not shown while recording; they're still captured and submitted. */} +
+