Compare commits

...

2 Commits

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,