fix(ui): gate voice orb on a true ready signal, not start() returning (#38)#47
Open
nithiink wants to merge 2 commits into
Open
fix(ui): gate voice orb on a true ready signal, not start() returning (#38)#47nithiink wants to merge 2 commits into
nithiink wants to merge 2 commits into
Conversation
…#38) The orb's --amp mic-volume animation scaled from live mic input before the voice connection was actually ready, for all three providers. The gate was keyed on `connected`, which flips true the instant `s.start()` resolves — but `start()` returns before each provider's post-start handshake completes, while the mic analyser is already attached. So the orb twitched from mic input during the still-in-progress handshake. Fix: introduce an explicit `ready` transport event emitted at each provider's genuine ready point, and gate the orb on it. - voice.ts: add `{ type: "ready" }` to RealtimeEvent. - gemini.ts: emit `ready` on `setupComplete` (not when openSocket() returns). - realtime.ts: emit `ready`/`listening` from the data-channel `open` handler after configureSession(), and drop the premature signals at the end of start() (which fired before the channel was even open). - VoiceAgent.tsx: gate orbLoop on a `readyRef` driven by the `ready` event instead of `connectedRef`; reset it in connect()/disconnect(). Extract the orb gate/amplitude math into pure helpers (lib/orb.ts) so the gate timing is unit-testable, and add lib/orb.test.ts — the core case asserts --amp stays 0 while the gate is closed even under loud mic input. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code review surfaced that readyRef was only reset on connect()/disconnect(), so during a Gemini reconnect (setupComplete → socket drop → reconnect) the orb kept scaling from live mic input across the outage — the same class of bug this PR fixes. The transport already emits `state: "connecting"` from its reconnect path, so close the gate on that and let the next `ready` (on the fresh setupComplete) re-open it. The orb now rests through the reconnect window. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #38.
Problem
The voice orb's
--ampmic-volume animation began scaling from live mic input before the voice connection was actually ready, across all three providers (Gemini, OpenAI, Azure). The orb gate was keyed onconnectedRef, which mirrorsconnected— andconnectedflipstruethe instants.start()resolves. Butstart()returns before each provider's post-start application handshake completes, while the mic analyser is already attached. So the momentconnectedflipped, the orb twitched from mic input during the still-in-progress handshake.start()/openSocket()return beforesetupCompletearrives.start()resolves right aftersetRemoteDescription, but the data channelopen→configureSession()fires asynchronously after that. The old code also emittedstate: "listening"prematurely at the end ofstart().Fix
Introduce an explicit
readytransport event emitted at each provider's genuine ready point, and gate the orb on it instead of onstart()returning.lib/voice.ts— add{ type: "ready" }toRealtimeEvent.lib/gemini.ts— emitreadyonsetupComplete.lib/realtime.ts— emitready/listeningfrom the data-channelopenhandler (afterconfigureSession()), and remove the premature signals at the end ofstart().components/VoiceAgent.tsx— gateorbLoopon areadyRefdriven by thereadyevent instead ofconnectedRef; reset it inconnect()/disconnect().Comment cleanup
Per review, trimmed the inline comments to remove redundancy: the orb-gate rationale now lives once in
lib/orb.ts(its home) and on thereadyevent invoice.ts; the duplicate restatements inorbLoop,onEvent, and thereadyRefdeclaration were removed or shortened to one-liners.Tests
Extracted the orb gate/amplitude math into pure helpers (
lib/orb.ts:rmsAmp,orbTarget,envelopeStep) so the gate timing is unit-testable, and addedlib/orb.test.ts. The core case asserts--ampstays0while the gate is closed even under loud mic input, plus envelope decay/rise behavior.Checks run before opening:
node --test(12/12 pass) andtsc --noEmit(clean).🤖 Generated with Claude Code