diff --git a/internal/audio/input_supervisor.go b/internal/audio/input_supervisor.go index e39e6a16..258b216a 100644 --- a/internal/audio/input_supervisor.go +++ b/internal/audio/input_supervisor.go @@ -51,6 +51,11 @@ func (ais *AudioInputSupervisor) SetOpusConfig(bitrate, complexity, vbr, signalT // Start begins supervising the audio input server process func (ais *AudioInputSupervisor) Start() error { + // Check if USB audio is enabled before starting + if !IsUsbAudioEnabled() { + return fmt.Errorf("USB audio device is disabled - cannot start audio input supervisor") + } + if !atomic.CompareAndSwapInt32(&ais.running, 0, 1) { return fmt.Errorf("audio input supervisor is already running") } diff --git a/internal/audio/supervisor_api.go b/internal/audio/supervisor_api.go index 5d9fe5fa..260e7536 100644 --- a/internal/audio/supervisor_api.go +++ b/internal/audio/supervisor_api.go @@ -10,6 +10,7 @@ import ( var ( globalOutputSupervisor unsafe.Pointer // *AudioOutputSupervisor globalInputSupervisor unsafe.Pointer // *AudioInputSupervisor + usbAudioEnabledFunc func() bool // Callback to check USB audio status ) // isAudioServerProcess detects if we're running as the audio server subprocess @@ -84,3 +85,16 @@ func GetAudioInputSupervisor() *AudioInputSupervisor { } return (*AudioInputSupervisor)(ptr) } + +// SetUsbAudioEnabledCallback sets the callback function to check USB audio status +func SetUsbAudioEnabledCallback(callback func() bool) { + usbAudioEnabledFunc = callback +} + +// IsUsbAudioEnabled checks if USB audio device is enabled via the callback +func IsUsbAudioEnabled() bool { + if usbAudioEnabledFunc == nil { + return false // Default to disabled if callback not set + } + return usbAudioEnabledFunc() +} diff --git a/jsonrpc.go b/jsonrpc.go index cfc777ad..584b17b5 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -914,11 +914,6 @@ func validateAudioConfiguration(enabled bool) error { return nil // Disabling audio is always allowed } - // Check if audio supervisor is available - if audioSupervisor == nil { - return fmt.Errorf("audio supervisor not initialized - audio functionality not available") - } - // Check if ALSA devices are available by attempting to list them // This is a basic check to ensure the system has audio capabilities if _, err := os.Stat("/proc/asound/cards"); os.IsNotExist(err) { @@ -964,13 +959,27 @@ func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { logger.Info().Msg("audio input manager stopped") } + // Stop global audio input supervisor if active + if AudioInputSupervisor != nil && AudioInputSupervisor.IsRunning() { + logger.Info().Msg("stopping global audio input supervisor") + AudioInputSupervisor.Stop() + // Wait for audio input supervisor to fully stop + for i := 0; i < 50; i++ { // Wait up to 5 seconds + if !AudioInputSupervisor.IsRunning() { + break + } + time.Sleep(100 * time.Millisecond) + } + logger.Info().Msg("global audio input supervisor stopped") + } + // Stop audio output supervisor - if audioSupervisor != nil && audioSupervisor.IsRunning() { + if AudioOutputSupervisor != nil && AudioOutputSupervisor.IsRunning() { logger.Info().Msg("stopping audio output supervisor") - audioSupervisor.Stop() + AudioOutputSupervisor.Stop() // Wait for audio processes to fully stop before proceeding for i := 0; i < 50; i++ { // Wait up to 5 seconds - if !audioSupervisor.IsRunning() { + if !AudioOutputSupervisor.IsRunning() { break } time.Sleep(100 * time.Millisecond) @@ -979,8 +988,8 @@ func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { } logger.Info().Msg("audio processes stopped, proceeding with USB gadget reconfiguration") - } else if newAudioEnabled && audioSupervisor != nil && !audioSupervisor.IsRunning() { - // Start audio processes when audio is enabled (after USB reconfiguration) + } else if newAudioEnabled { + // Audio being enabled - supervisors will be created/started after USB reconfiguration logger.Info().Msg("audio will be started after USB gadget reconfiguration") } } @@ -995,17 +1004,42 @@ func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { } // Start audio processes after successful USB reconfiguration if needed - if previousAudioEnabled != newAudioEnabled && newAudioEnabled && audioSupervisor != nil { - // Ensure supervisor is fully stopped before starting + if previousAudioEnabled != newAudioEnabled && newAudioEnabled { + // Create supervisors if they don't exist (happens when audio was previously disabled) + if AudioOutputSupervisor == nil { + logger.Info().Msg("creating audio output supervisor for USB audio enablement") + AudioOutputSupervisor = audio.NewAudioOutputSupervisor() + audio.SetAudioOutputSupervisor(AudioOutputSupervisor) + } + + // Create audio input supervisor if it doesn't exist + if AudioInputSupervisor == nil { + logger.Info().Msg("creating audio input supervisor for USB audio enablement") + AudioInputSupervisor = audio.NewAudioInputSupervisor() + audio.SetAudioInputSupervisor(AudioInputSupervisor) + + // Set default OPUS configuration for audio input supervisor + audioConfig := audio.Config + AudioInputSupervisor.SetOpusConfig( + audioConfig.AudioQualityLowInputBitrate*1000, // Convert kbps to bps + audioConfig.AudioQualityLowOpusComplexity, + audioConfig.AudioQualityLowOpusVBR, + audioConfig.AudioQualityLowOpusSignalType, + audioConfig.AudioQualityLowOpusBandwidth, + audioConfig.AudioQualityLowOpusDTX, + ) + } + + // Ensure audio output supervisor is fully stopped before starting for i := 0; i < 50; i++ { // Wait up to 5 seconds - if !audioSupervisor.IsRunning() { + if !AudioOutputSupervisor.IsRunning() { break } time.Sleep(100 * time.Millisecond) } logger.Info().Msg("starting audio processes after USB gadget reconfiguration") - if err := audioSupervisor.Start(); err != nil { - logger.Error().Err(err).Msg("failed to start audio supervisor") + if err := AudioOutputSupervisor.Start(); err != nil { + logger.Error().Err(err).Msg("failed to start audio output supervisor") // Don't return error here as USB reconfiguration was successful } else { // Broadcast audio device change event to notify WebRTC session @@ -1013,6 +1047,18 @@ func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { broadcaster.BroadcastAudioDeviceChanged(true, "usb_reconfiguration") logger.Info().Msg("broadcasted audio device change event after USB reconfiguration") } + + // Start audio input supervisor if microphone restoration is enabled + // This makes audio input consistent with audio output - both auto-start when USB audio enabled + if AudioInputSupervisor != nil { + logger.Info().Msg("starting audio input supervisor after USB gadget reconfiguration") + if err := AudioInputSupervisor.Start(); err != nil { + logger.Warn().Err(err).Msg("failed to start audio input supervisor - will be available on-demand") + // This is not a critical failure - audio input can be started on-demand later + } else { + logger.Info().Msg("audio input supervisor started successfully after USB reconfiguration") + } + } } else if previousAudioEnabled != newAudioEnabled { // Broadcast audio device change event for disabling audio broadcaster := audio.GetAudioEventBroadcaster() @@ -1058,42 +1104,92 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { logger.Info().Msg("audio input manager stopped") } + // Stop global audio input supervisor if active + if AudioInputSupervisor != nil && AudioInputSupervisor.IsRunning() { + logger.Info().Msg("stopping global audio input supervisor") + AudioInputSupervisor.Stop() + // Wait for audio input supervisor to fully stop + for i := 0; i < 50; i++ { // Wait up to 5 seconds + if !AudioInputSupervisor.IsRunning() { + break + } + time.Sleep(100 * time.Millisecond) + } + logger.Info().Msg("global audio input supervisor stopped") + } + // Stop audio output supervisor - if audioSupervisor != nil && audioSupervisor.IsRunning() { + if AudioOutputSupervisor != nil && AudioOutputSupervisor.IsRunning() { logger.Info().Msg("stopping audio output supervisor") - audioSupervisor.Stop() + AudioOutputSupervisor.Stop() // Wait for audio processes to fully stop for i := 0; i < 50; i++ { // Wait up to 5 seconds - if !audioSupervisor.IsRunning() { + if !AudioOutputSupervisor.IsRunning() { break } time.Sleep(100 * time.Millisecond) } logger.Info().Msg("audio output supervisor stopped") } - } else if enabled && audioSupervisor != nil { - // Ensure supervisor is fully stopped before starting + } else if enabled { + // Create supervisors if they don't exist (happens when audio was previously disabled) + if AudioOutputSupervisor == nil { + logger.Info().Msg("creating audio output supervisor for audio device enablement") + AudioOutputSupervisor = audio.NewAudioOutputSupervisor() + audio.SetAudioOutputSupervisor(AudioOutputSupervisor) + } + + // Create audio input supervisor if it doesn't exist + if AudioInputSupervisor == nil { + logger.Info().Msg("creating audio input supervisor for audio device enablement") + AudioInputSupervisor = audio.NewAudioInputSupervisor() + audio.SetAudioInputSupervisor(AudioInputSupervisor) + + // Set default OPUS configuration for audio input supervisor + audioConfig := audio.Config + AudioInputSupervisor.SetOpusConfig( + audioConfig.AudioQualityLowInputBitrate*1000, // Convert kbps to bps + audioConfig.AudioQualityLowOpusComplexity, + audioConfig.AudioQualityLowOpusVBR, + audioConfig.AudioQualityLowOpusSignalType, + audioConfig.AudioQualityLowOpusBandwidth, + audioConfig.AudioQualityLowOpusDTX, + ) + } + + // Ensure audio output supervisor is fully stopped before starting for i := 0; i < 50; i++ { // Wait up to 5 seconds - if !audioSupervisor.IsRunning() { + if !AudioOutputSupervisor.IsRunning() { break } time.Sleep(100 * time.Millisecond) } // Start audio processes when audio is enabled logger.Info().Msg("starting audio processes due to audio device being enabled") - if err := audioSupervisor.Start(); err != nil { - logger.Error().Err(err).Msg("failed to start audio supervisor") + if err := AudioOutputSupervisor.Start(); err != nil { + logger.Error().Err(err).Msg("failed to start audio output supervisor") } else { // Broadcast audio device change event to notify WebRTC session broadcaster := audio.GetAudioEventBroadcaster() broadcaster.BroadcastAudioDeviceChanged(true, "device_enabled") logger.Info().Msg("broadcasted audio device change event after enabling audio device") } - // Always broadcast the audio device change event regardless of enable/disable - broadcaster := audio.GetAudioEventBroadcaster() - broadcaster.BroadcastAudioDeviceChanged(enabled, "device_state_changed") - logger.Info().Bool("enabled", enabled).Msg("broadcasted audio device state change event") + + // Start audio input supervisor consistently with output + if AudioInputSupervisor != nil { + logger.Info().Msg("starting audio input supervisor due to audio device being enabled") + if err := AudioInputSupervisor.Start(); err != nil { + logger.Warn().Err(err).Msg("failed to start audio input supervisor - will be available on-demand") + } else { + logger.Info().Msg("audio input supervisor started successfully after enabling audio device") + } + } } + + // Always broadcast the audio device change event regardless of enable/disable + broadcaster := audio.GetAudioEventBroadcaster() + broadcaster.BroadcastAudioDeviceChanged(enabled, "device_state_changed") + logger.Info().Bool("enabled", enabled).Msg("broadcasted audio device state change event") config.UsbDevices.Audio = enabled default: return fmt.Errorf("invalid device: %s", device) diff --git a/main.go b/main.go index 603e8bb9..9286ac58 100644 --- a/main.go +++ b/main.go @@ -15,30 +15,36 @@ import ( ) var ( - appCtx context.Context - isAudioServer bool - audioProcessDone chan struct{} - audioSupervisor *audio.AudioOutputSupervisor + appCtx context.Context + isAudioServer bool + audioProcessDone chan struct{} + AudioOutputSupervisor *audio.AudioOutputSupervisor // Exported for jsonrpc access + AudioInputSupervisor *audio.AudioInputSupervisor // Exported for jsonrpc access ) func startAudioSubprocess() error { // Initialize validation cache for optimal performance audio.InitValidationCache() + // Set up USB audio status callback for the audio package + audio.SetUsbAudioEnabledCallback(func() bool { + return config.UsbDevices != nil && config.UsbDevices.Audio + }) + // Create audio server supervisor - audioSupervisor = audio.NewAudioOutputSupervisor() + AudioOutputSupervisor = audio.NewAudioOutputSupervisor() // Set the global supervisor for access from audio package - audio.SetAudioOutputSupervisor(audioSupervisor) + audio.SetAudioOutputSupervisor(AudioOutputSupervisor) // Create and register audio input supervisor (but don't start it) // Audio input will be started on-demand through the UI - audioInputSupervisor := audio.NewAudioInputSupervisor() - audio.SetAudioInputSupervisor(audioInputSupervisor) + AudioInputSupervisor = audio.NewAudioInputSupervisor() + audio.SetAudioInputSupervisor(AudioInputSupervisor) // Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106) audioConfig := audio.Config - audioInputSupervisor.SetOpusConfig( + AudioInputSupervisor.SetOpusConfig( audioConfig.AudioQualityLowInputBitrate*1000, // Convert kbps to bps audioConfig.AudioQualityLowOpusComplexity, audioConfig.AudioQualityLowOpusVBR, @@ -51,7 +57,7 @@ func startAudioSubprocess() error { // when the user activates microphone input through the UI // Set up callbacks for process lifecycle events - audioSupervisor.SetCallbacks( + AudioOutputSupervisor.SetCallbacks( // onProcessStart func(pid int) { logger.Info().Int("pid", pid).Msg("audio server process started") @@ -107,7 +113,7 @@ func startAudioSubprocess() error { } // Start the supervisor - if err := audioSupervisor.Start(); err != nil { + if err := AudioOutputSupervisor.Start(); err != nil { return fmt.Errorf("failed to start audio supervisor: %w", err) } @@ -116,7 +122,7 @@ func startAudioSubprocess() error { defer close(audioProcessDone) // Wait for supervisor to stop - for audioSupervisor.IsRunning() { + for AudioOutputSupervisor.IsRunning() { time.Sleep(100 * time.Millisecond) } @@ -288,9 +294,9 @@ func Main(audioServer bool, audioInputServer bool) { // Stop audio subprocess and wait for cleanup if !isAudioServer { - if audioSupervisor != nil { + if AudioOutputSupervisor != nil { logger.Info().Msg("stopping audio supervisor") - audioSupervisor.Stop() + AudioOutputSupervisor.Stop() } <-audioProcessDone } else { diff --git a/ui/src/hooks/useMicrophone.ts b/ui/src/hooks/useMicrophone.ts index 25124743..eba83ae3 100644 --- a/ui/src/hooks/useMicrophone.ts +++ b/ui/src/hooks/useMicrophone.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useRTCStore, useSettingsStore } from "@/hooks/stores"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; +import { useAudioEvents, AudioDeviceChangedData } from "@/hooks/useAudioEvents"; import { devLog, devInfo, devWarn, devError, devOnly } from "@/utils/debug"; import { AUDIO_CONFIG } from "@/config/constants"; @@ -589,6 +590,31 @@ export function useMicrophone() { }; }, []); // No dependencies to prevent re-running + // Handle audio device changes (USB audio enable/disable) + const handleAudioDeviceChanged = useCallback((data: AudioDeviceChangedData) => { + // When USB audio is re-enabled and user previously had microphone enabled, restore it + if (data.enabled && data.reason === "usb_reconfiguration" && microphoneWasEnabled && !isMicrophoneActive && peerConnection) { + devInfo("USB audio re-enabled and microphone was previously enabled - attempting to restore"); + startMicrophone().then((result) => { + if (result.success) { + devInfo("Microphone successfully restored after USB audio re-enable"); + } else { + devWarn("Failed to restore microphone after USB audio re-enable:", result.error); + } + }).catch((error) => { + devWarn("Error restoring microphone after USB audio re-enable:", error); + }); + } + // When USB audio is disabled, clear the microphone enabled flag if it was set + else if (!data.enabled && data.reason === "usb_reconfiguration" && microphoneWasEnabled) { + devInfo("USB audio disabled - clearing microphone enabled flag"); + setMicrophoneWasEnabled(false); + } + }, [microphoneWasEnabled, isMicrophoneActive, peerConnection, startMicrophone, setMicrophoneWasEnabled]); + + // Subscribe to audio device change events + useAudioEvents(handleAudioDeviceChanged); + return { isMicrophoneActive, isMicrophoneMuted,