refactor(audio): replace mute functionality with start/stop for microphone

- Replace MuteMicrophone calls with StartMicrophone/StopMicrophone for clearer behavior
- Update microphone state broadcasting to reflect actual subprocess status
- Modify UI to use enable/disable terminology instead of mute/unmute
- Ensure microphone device changes properly restart the active microphone
This commit is contained in:
Alex P 2025-09-07 18:32:42 +00:00
parent 7d39a2741e
commit e3b4bb2002
3 changed files with 56 additions and 32 deletions

View File

@ -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

View File

@ -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()
}

View File

@ -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
<div className="flex items-center justify-between rounded-lg bg-slate-50 p-3 dark:bg-slate-700">
<div className="flex items-center gap-3">
{isMicrophoneActiveFromWS ? (
{isMicrophoneActiveFromHook ? (
<MdMic className="h-5 w-5 text-green-500" />
) : (
<MdMicOff className="h-5 w-5 text-red-500" />
)}
<span className="font-medium text-slate-900 dark:text-slate-100">
{isMicrophoneActiveFromWS ? "Unmuted" : "Muted"}
{isMicrophoneActiveFromHook ? "Enabled" : "Disabled"}
</span>
</div>
<Button
size="SM"
theme={isMicrophoneActiveFromWS ? "danger" : "primary"}
text={isMicrophoneActiveFromWS ? "Disable" : "Enable"}
onClick={handleToggleMicrophoneMute}
theme={isMicrophoneActiveFromHook ? "danger" : "primary"}
text={isMicrophoneActiveFromHook ? "Disable" : "Enable"}
onClick={handleToggleMicrophoneEnable}
disabled={isLoading}
/>
</div>
@ -381,7 +380,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
</option>
))}
</select>
{isMicrophoneActiveFromWS && (
{isMicrophoneActiveFromHook && (
<p className="text-xs text-slate-500 dark:text-slate-400">
Changing device will restart the microphone
</p>
@ -418,7 +417,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
</div>
{/* Microphone Quality Settings */}
{isMicrophoneActiveFromWS && (
{isMicrophoneActiveFromHook && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<MdMic className="h-4 w-4 text-slate-600 dark:text-slate-400" />