diff --git a/audio_handlers.go b/audio_handlers.go index 9833fa2b..42af2428 100644 --- a/audio_handlers.go +++ b/audio_handlers.go @@ -42,13 +42,13 @@ func StartAudioOutputAndAddTracks() error { // StopMicrophoneAndRemoveTracks is a global helper to stop microphone subprocess and remove WebRTC tracks func StopMicrophoneAndRemoveTracks() error { initAudioControlService() - return audioControlService.MuteMicrophone(true) + return audioControlService.StopMicrophone() } // StartMicrophoneAndAddTracks is a global helper to start microphone subprocess and add WebRTC tracks func StartMicrophoneAndAddTracks() error { initAudioControlService() - return audioControlService.MuteMicrophone(false) + return audioControlService.StartMicrophone() } // IsAudioOutputActive is a global helper to check if audio output subprocess is running diff --git a/internal/audio/core_handlers.go b/internal/audio/core_handlers.go index 943a20fb..5bc3137e 100644 --- a/internal/audio/core_handlers.go +++ b/internal/audio/core_handlers.go @@ -95,6 +95,12 @@ func (s *AudioControlService) StartMicrophone() error { } s.logger.Info().Msg("microphone started successfully") + + // Broadcast microphone state change via WebSocket + broadcaster := GetAudioEventBroadcaster() + sessionActive := s.sessionProvider.IsSessionActive() + broadcaster.BroadcastMicrophoneStateChanged(true, sessionActive) + return nil } @@ -116,6 +122,12 @@ func (s *AudioControlService) StopMicrophone() error { audioInputManager.Stop() s.logger.Info().Msg("microphone stopped successfully") + + // Broadcast microphone state change via WebSocket + broadcaster := GetAudioEventBroadcaster() + sessionActive := s.sessionProvider.IsSessionActive() + broadcaster.BroadcastMicrophoneStateChanged(false, sessionActive) + return nil } @@ -153,8 +165,17 @@ func (s *AudioControlService) MuteMicrophone(muted bool) error { // Broadcast microphone state change via WebSocket broadcaster := GetAudioEventBroadcaster() sessionActive := s.sessionProvider.IsSessionActive() - // With the new approach, "running" means "not muted" - broadcaster.BroadcastMicrophoneStateChanged(!muted, sessionActive) + + // Get actual subprocess running status (not mute status) + var subprocessRunning bool + if sessionActive { + audioInputManager := s.sessionProvider.GetAudioInputManager() + if audioInputManager != nil { + subprocessRunning = audioInputManager.IsRunning() + } + } + + broadcaster.BroadcastMicrophoneStateChanged(subprocessRunning, sessionActive) return nil } @@ -267,13 +288,17 @@ func (s *AudioControlService) IsAudioOutputActive() bool { return !IsAudioMuted() && IsAudioRelayRunning() } -// IsMicrophoneActive returns whether the microphone is active (not muted) +// IsMicrophoneActive returns whether the microphone subprocess is running func (s *AudioControlService) IsMicrophoneActive() bool { if !s.sessionProvider.IsSessionActive() { return false } - // With the new unified approach, microphone "active" means "not muted" - // This matches how audio output works - active means not muted - return !IsMicrophoneMuted() + audioInputManager := s.sessionProvider.GetAudioInputManager() + if audioInputManager == nil { + return false + } + + // For Enable/Disable buttons, we check subprocess status + return audioInputManager.IsRunning() } diff --git a/ui/src/components/popovers/AudioControlPopover.tsx b/ui/src/components/popovers/AudioControlPopover.tsx index 8187257f..6763ff2f 100644 --- a/ui/src/components/popovers/AudioControlPopover.tsx +++ b/ui/src/components/popovers/AudioControlPopover.tsx @@ -61,7 +61,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP // Use WebSocket-based audio events for real-time updates const { audioMuted, - microphoneState, + // microphoneState - now using hook state instead isConnected: wsConnected } = useAudioEvents(); @@ -69,6 +69,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP // Microphone state from props (keeping hook for legacy device operations) const { + isMicrophoneActive: isMicrophoneActiveFromHook, startMicrophone, stopMicrophone, syncMicrophoneState, @@ -82,8 +83,8 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP const isMuted = audioMuted ?? false; const isConnected = wsConnected; - // Use WebSocket microphone state instead of hook state for real-time updates - const isMicrophoneActiveFromWS = microphoneState?.running ?? false; + // Note: We now use hook state instead of WebSocket state for microphone Enable/Disable + // const isMicrophoneActiveFromWS = microphoneState?.running ?? false; @@ -200,7 +201,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP } }; - const handleToggleMicrophoneMute = async () => { + const handleToggleMicrophoneEnable = async () => { const now = Date.now(); // Prevent rapid clicking - if any operation is in progress or within cooldown, ignore the click @@ -212,20 +213,18 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP setIsLoading(true); try { - if (isMicrophoneActiveFromWS) { - // Mute: Use unified microphone mute API (like audio output) - const resp = await api.POST("/microphone/mute", { muted: true }); - if (!resp.ok) { - throw new Error(`Failed to mute microphone: ${resp.status}`); + if (isMicrophoneActiveFromHook) { + // Disable: Stop microphone subprocess AND remove WebRTC tracks + const result = await stopMicrophone(); + if (!result.success) { + throw new Error(result.error?.message || "Failed to stop microphone"); } - // WebSocket will handle the state update automatically } else { - // Unmute: Use unified microphone mute API (like audio output) - const resp = await api.POST("/microphone/mute", { muted: false }); - if (!resp.ok) { - throw new Error(`Failed to unmute microphone: ${resp.status}`); + // Enable: Start microphone subprocess AND add WebRTC tracks + const result = await startMicrophone(); + if (!result.success) { + throw new Error(result.error?.message || "Failed to start microphone"); } - // WebSocket will handle the state update automatically } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to toggle microphone"; @@ -239,8 +238,8 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP const handleMicrophoneDeviceChange = async (deviceId: string) => { setSelectedInputDevice(deviceId); - // If microphone is currently active (unmuted), restart it with the new device - if (isMicrophoneActiveFromWS) { + // If microphone is currently active, restart it with the new device + if (isMicrophoneActiveFromHook) { try { // Stop current microphone await stopMicrophone(); @@ -325,20 +324,20 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
- {isMicrophoneActiveFromWS ? ( + {isMicrophoneActiveFromHook ? ( ) : ( )} - {isMicrophoneActiveFromWS ? "Unmuted" : "Muted"} + {isMicrophoneActiveFromHook ? "Enabled" : "Disabled"}
@@ -381,7 +380,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP ))} - {isMicrophoneActiveFromWS && ( + {isMicrophoneActiveFromHook && (

Changing device will restart the microphone

@@ -418,7 +417,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP {/* Microphone Quality Settings */} - {isMicrophoneActiveFromWS && ( + {isMicrophoneActiveFromHook && (