Fixes, Improvements: Fixed stale audio subprocesses when disabling audio from Hardware settings and ensure more consistency between the way in which audio subprocesses are handled when USB Gadget settings are changed

This commit is contained in:
Alex P 2025-09-20 23:30:02 +00:00
parent a84f63c0c4
commit 7ce1520c2f
5 changed files with 188 additions and 41 deletions

View File

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

View File

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

View File

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

34
main.go
View File

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

View File

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