Compare commits

..

12 Commits

12 changed files with 393 additions and 1053 deletions

View File

@ -15,14 +15,9 @@ func ensureAudioControlService() *audio.AudioControlService {
sessionProvider := &SessionProviderImpl{}
audioControlService = audio.NewAudioControlService(sessionProvider, logger)
// Set up RPC callback functions for the audio package
// Set up RPC callback function for the audio package
audio.SetRPCCallbacks(
func() *audio.AudioControlService { return audioControlService },
func() audio.AudioConfig { return audioControlService.GetCurrentAudioQuality() },
func(quality audio.AudioQuality) error {
audioControlService.SetAudioQuality(quality)
return nil
},
)
}
return audioControlService

File diff suppressed because it is too large Load Diff

View File

@ -26,58 +26,17 @@ type AudioConfigConstants struct {
FrameSize int // Samples per audio frame (default: 960 for 20ms at 48kHz)
MaxPacketSize int // Maximum encoded packet size in bytes (default: 4000)
// Audio Quality Bitrates (kbps)
AudioQualityLowOutputBitrate int // Low-quality output bitrate (default: 32)
AudioQualityLowInputBitrate int // Low-quality input bitrate (default: 16)
AudioQualityMediumOutputBitrate int // Medium-quality output bitrate (default: 64)
AudioQualityMediumInputBitrate int // Medium-quality input bitrate (default: 32)
AudioQualityHighOutputBitrate int // High-quality output bitrate (default: 128)
AudioQualityHighInputBitrate int // High-quality input bitrate (default: 64)
AudioQualityUltraOutputBitrate int // Ultra-quality output bitrate (default: 192)
AudioQualityUltraInputBitrate int // Ultra-quality input bitrate (default: 96)
// Optimal Audio Configuration (S16_LE @ 48kHz stereo from HDMI)
// Single optimized setting - no quality presets needed
OptimalOutputBitrate int // Output bitrate: 96 kbps (optimal for stereo @ 48kHz)
OptimalInputBitrate int // Input bitrate: 48 kbps (optimal for mono mic @ 48kHz)
// Audio Quality Sample Rates (Hz)
AudioQualityLowSampleRate int // Low-quality sample rate (default: 22050)
AudioQualityMediumSampleRate int // Medium-quality sample rate (default: 44100)
AudioQualityMicLowSampleRate int // Low-quality microphone sample rate (default: 16000)
// Audio Quality Frame Sizes
AudioQualityLowFrameSize time.Duration // Low-quality frame duration (default: 40ms)
AudioQualityMediumFrameSize time.Duration // Medium-quality frame duration (default: 20ms)
AudioQualityHighFrameSize time.Duration // High-quality frame duration (default: 20ms)
AudioQualityUltraFrameSize time.Duration // Ultra-quality frame duration (default: 10ms)
// Audio Quality Channels
AudioQualityLowChannels int // Low-quality channel count (default: 1)
AudioQualityMediumChannels int // Medium-quality channel count (default: 2)
AudioQualityHighChannels int // High-quality channel count (default: 2)
AudioQualityUltraChannels int // Ultra-quality channel count (default: 2)
// Audio Quality OPUS Encoder Parameters
AudioQualityLowOpusComplexity int // Low-quality OPUS complexity (default: 1)
AudioQualityLowOpusVBR int // Low-quality OPUS VBR setting (default: 0)
AudioQualityLowOpusSignalType int // Low-quality OPUS signal type (default: 3001)
AudioQualityLowOpusBandwidth int // Low-quality OPUS bandwidth (default: 1101)
AudioQualityLowOpusDTX int // Low-quality OPUS DTX setting (default: 1)
AudioQualityMediumOpusComplexity int // Medium-quality OPUS complexity (default: 5)
AudioQualityMediumOpusVBR int // Medium-quality OPUS VBR setting (default: 1)
AudioQualityMediumOpusSignalType int // Medium-quality OPUS signal type (default: 3002)
AudioQualityMediumOpusBandwidth int // Medium-quality OPUS bandwidth (default: 1103)
AudioQualityMediumOpusDTX int // Medium-quality OPUS DTX setting (default: 0)
AudioQualityHighOpusComplexity int // High-quality OPUS complexity (default: 8)
AudioQualityHighOpusVBR int // High-quality OPUS VBR setting (default: 1)
AudioQualityHighOpusSignalType int // High-quality OPUS signal type (default: 3002)
AudioQualityHighOpusBandwidth int // High-quality OPUS bandwidth (default: 1104)
AudioQualityHighOpusDTX int // High-quality OPUS DTX setting (default: 0)
AudioQualityUltraOpusComplexity int // Ultra-quality OPUS complexity (default: 10)
AudioQualityUltraOpusVBR int // Ultra-quality OPUS VBR setting (default: 1)
AudioQualityUltraOpusSignalType int // Ultra-quality OPUS signal type (default: 3002)
AudioQualityUltraOpusBandwidth int // Ultra-quality OPUS bandwidth (default: 1105)
AudioQualityUltraOpusDTX int // Ultra-quality OPUS DTX setting (default: 0)
// Optimal OPUS Encoder Parameters (minimal CPU usage)
OptimalOpusComplexity int // Complexity: 1 (minimal CPU ~0.5%)
OptimalOpusVBR int // VBR: enabled for efficiency
OptimalOpusSignalType int // Signal: OPUS_SIGNAL_MUSIC (3002)
OptimalOpusBandwidth int // Bandwidth: WIDEBAND (1103 = native 48kHz)
OptimalOpusDTX int // DTX: disabled for continuous audio
// CGO Audio Constants
CGOOpusBitrate int // Native Opus encoder bitrate in bps (default: 96000)
@ -315,66 +274,27 @@ func DefaultAudioConfig() *AudioConfigConstants {
FrameSize: 960,
MaxPacketSize: 4000,
AudioQualityLowOutputBitrate: 32,
AudioQualityLowInputBitrate: 16,
AudioQualityMediumOutputBitrate: 48,
AudioQualityMediumInputBitrate: 24,
AudioQualityHighOutputBitrate: 64,
AudioQualityHighInputBitrate: 32,
AudioQualityUltraOutputBitrate: 96,
AudioQualityUltraInputBitrate: 48,
AudioQualityLowSampleRate: 48000,
AudioQualityMediumSampleRate: 48000,
AudioQualityMicLowSampleRate: 16000,
AudioQualityLowFrameSize: 20 * time.Millisecond,
AudioQualityMediumFrameSize: 20 * time.Millisecond,
AudioQualityHighFrameSize: 20 * time.Millisecond,
// Optimal Audio Configuration (single setting for all use cases)
OptimalOutputBitrate: 96, // 96 kbps for stereo @ 48kHz
OptimalInputBitrate: 48, // 48 kbps for mono mic @ 48kHz
OptimalOpusComplexity: 1, // Complexity 1: minimal CPU (~0.5%)
OptimalOpusVBR: 1, // VBR enabled for efficiency
OptimalOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
OptimalOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND (native 48kHz)
OptimalOpusDTX: 0, // DTX disabled for continuous audio
AudioQualityUltraFrameSize: 20 * time.Millisecond, // Ultra-quality frame duration
// Audio Quality Channels
AudioQualityLowChannels: 1, // Mono for low quality
AudioQualityMediumChannels: 2, // Stereo for medium quality
AudioQualityHighChannels: 2, // Stereo for high quality
AudioQualityUltraChannels: 2, // Stereo for ultra quality
// Audio Quality OPUS Parameters
AudioQualityLowOpusComplexity: 0, // Low complexity
AudioQualityLowOpusVBR: 1, // VBR enabled
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND
AudioQualityLowOpusDTX: 1, // DTX enabled
AudioQualityMediumOpusComplexity: 1, // Low complexity
AudioQualityMediumOpusVBR: 1, // VBR enabled
AudioQualityMediumOpusSignalType: 3001, // OPUS_SIGNAL_VOICE
AudioQualityMediumOpusBandwidth: 1102, // OPUS_BANDWIDTH_MEDIUMBAND
AudioQualityMediumOpusDTX: 1, // DTX enabled
AudioQualityHighOpusComplexity: 2, // Medium complexity
AudioQualityHighOpusVBR: 1, // VBR enabled
AudioQualityHighOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityHighOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND
AudioQualityHighOpusDTX: 0, // DTX disabled
AudioQualityUltraOpusComplexity: 3, // Higher complexity
AudioQualityUltraOpusVBR: 1, // VBR enabled
AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityUltraOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND
AudioQualityUltraOpusDTX: 0, // DTX disabled
// CGO Audio Constants - Optimized for RV1106 native audio processing
CGOOpusBitrate: 64000, // Reduced for RV1106 efficiency
CGOOpusComplexity: 2, // Minimal complexity for RV1106
CGOOpusVBR: 1,
CGOOpusVBRConstraint: 1,
CGOOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for RV1106
CGOOpusDTX: 0,
CGOSampleRate: 48000,
CGOChannels: 2,
CGOFrameSize: 960,
CGOMaxPacketSize: 1200, // Reduced for RV1106 memory efficiency
// CGO Audio Constants - Optimized for S16_LE @ 48kHz with minimal CPU
CGOOpusBitrate: 96000, // 96 kbps optimal for stereo @ 48kHz
CGOOpusComplexity: 1, // Complexity 1: minimal CPU (~0.5% on RV1106)
CGOOpusVBR: 1, // VBR enabled for efficiency
CGOOpusVBRConstraint: 1, // Constrained VBR for predictable bitrate
CGOOpusSignalType: -1000, // OPUS_AUTO (automatic voice/music detection)
CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND (native 48kHz, no resampling)
CGOOpusDTX: 0, // DTX disabled for continuous audio
CGOSampleRate: 48000, // 48 kHz native HDMI sample rate
CGOChannels: 2, // Stereo
CGOFrameSize: 960, // 20ms frames at 48kHz
CGOMaxPacketSize: 1500, // Standard Ethernet MTU
// Input IPC Constants
InputIPCSampleRate: 48000, // Input IPC sample rate (48kHz)

View File

@ -236,19 +236,19 @@ func (s *AudioControlService) GetMicrophoneStatus() map[string]interface{} {
}
}
// SetAudioQuality sets the audio output quality
func (s *AudioControlService) SetAudioQuality(quality AudioQuality) {
SetAudioQuality(quality)
// SetAudioQuality is deprecated - audio quality is now fixed at optimal settings
func (s *AudioControlService) SetAudioQuality(quality int) {
// No-op: quality is fixed at optimal configuration
}
// GetAudioQualityPresets returns available audio quality presets
func (s *AudioControlService) GetAudioQualityPresets() map[AudioQuality]AudioConfig {
return GetAudioQualityPresets()
// GetAudioQualityPresets is deprecated - returns empty map
func (s *AudioControlService) GetAudioQualityPresets() map[int]AudioConfig {
return map[int]AudioConfig{}
}
// GetMicrophoneQualityPresets returns available microphone quality presets
func (s *AudioControlService) GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
return GetMicrophoneQualityPresets()
// GetMicrophoneQualityPresets is deprecated - returns empty map
func (s *AudioControlService) GetMicrophoneQualityPresets() map[int]AudioConfig {
return map[int]AudioConfig{}
}
// GetCurrentAudioQuality returns the current audio quality configuration

View File

@ -11,7 +11,6 @@ import (
// Validation errors
var (
ErrInvalidAudioQuality = errors.New("invalid audio quality level")
ErrInvalidFrameSize = errors.New("invalid frame size")
ErrInvalidFrameData = errors.New("invalid frame data")
ErrFrameDataEmpty = errors.New("invalid frame data: frame data is empty")
@ -30,13 +29,9 @@ var (
ErrInvalidLength = errors.New("invalid length")
)
// ValidateAudioQuality validates audio quality enum values with enhanced checks
func ValidateAudioQuality(quality AudioQuality) error {
// Validate enum range
if quality < AudioQualityLow || quality > AudioQualityUltra {
return fmt.Errorf("%w: quality value %d outside valid range [%d, %d]",
ErrInvalidAudioQuality, int(quality), int(AudioQualityLow), int(AudioQualityUltra))
}
// ValidateAudioQuality is deprecated - quality is now fixed at optimal settings
func ValidateAudioQuality(quality int) error {
// Quality validation removed - using fixed optimal configuration
return nil
}
@ -316,9 +311,6 @@ func ValidateAudioConfigComplete(config AudioConfig) error {
}
// Slower path: validate each parameter individually
if err := ValidateAudioQuality(config.Quality); err != nil {
return fmt.Errorf("quality validation failed: %w", err)
}
if err := ValidateBitrate(config.Bitrate); err != nil {
return fmt.Errorf("bitrate validation failed: %w", err)
}
@ -336,12 +328,7 @@ func ValidateAudioConfigComplete(config AudioConfig) error {
// ValidateAudioConfigConstants validates audio configuration constants
func ValidateAudioConfigConstants(config *AudioConfigConstants) error {
// Validate that audio quality constants are within valid ranges
for _, quality := range []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} {
if err := ValidateAudioQuality(quality); err != nil {
return fmt.Errorf("invalid audio quality constant %v: %w", quality, err)
}
}
// Quality validation removed - using fixed optimal configuration
// Validate configuration values if config is provided
if config != nil {
if Config.MaxFrameSize <= 0 {

View File

@ -59,7 +59,7 @@ func (aom *AudioOutputIPCManager) Start() error {
config := UnifiedIPCConfig{
SampleRate: Config.SampleRate,
Channels: Config.Channels,
FrameSize: int(Config.AudioQualityMediumFrameSize.Milliseconds()),
FrameSize: 20, // Fixed 20ms frame size for optimal audio
}
if err := aom.SendConfig(config); err != nil {

View File

@ -6,7 +6,7 @@
// Key components: output/input pipelines with Opus codec, buffer management,
// zero-copy frame pools, IPC communication, and process supervision.
//
// Supports four quality presets (Low/Medium/High/Ultra) with configurable bitrates.
// Optimized for S16_LE @ 48kHz stereo HDMI audio with minimal CPU usage.
// All APIs are thread-safe with comprehensive error handling and metrics collection.
//
// # Performance Characteristics
@ -14,13 +14,12 @@
// Designed for embedded ARM systems with limited resources:
// - Sub-50ms end-to-end latency under normal conditions
// - Memory usage scales with buffer configuration
// - CPU usage optimized through zero-copy operations
// - Network bandwidth adapts to quality settings
// - CPU usage optimized through zero-copy operations and complexity=1 Opus
// - Fixed optimal configuration (96 kbps output, 48 kbps input)
//
// # Usage Example
//
// config := GetAudioConfig()
// SetAudioQuality(AudioQualityHigh)
//
// // Audio output will automatically start when frames are received
package audio
@ -29,8 +28,6 @@ import (
"errors"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
)
var (
@ -42,23 +39,13 @@ func GetMaxAudioFrameSize() int {
return Config.MaxAudioFrameSize
}
// AudioQuality represents different audio quality presets
type AudioQuality int
const (
AudioQualityLow AudioQuality = iota
AudioQualityMedium
AudioQualityHigh
AudioQualityUltra
)
// AudioConfig holds configuration for audio processing
// AudioConfig holds the optimal audio configuration
// All settings are fixed for S16_LE @ 48kHz HDMI audio
type AudioConfig struct {
Quality AudioQuality
Bitrate int // kbps
SampleRate int // Hz
Channels int
FrameSize time.Duration // ms
Bitrate int // kbps (96 for output, 48 for input)
SampleRate int // Hz (always 48000)
Channels int // 2 for output (stereo), 1 for input (mono)
FrameSize time.Duration // ms (always 20ms)
}
// AudioMetrics tracks audio performance metrics
@ -72,195 +59,29 @@ type AudioMetrics struct {
}
var (
// Optimal configuration for audio output (HDMI → client)
currentConfig = AudioConfig{
Quality: AudioQualityMedium,
Bitrate: Config.AudioQualityMediumOutputBitrate,
Bitrate: Config.OptimalOutputBitrate,
SampleRate: Config.SampleRate,
Channels: Config.Channels,
FrameSize: Config.AudioQualityMediumFrameSize,
FrameSize: 20 * time.Millisecond,
}
// Optimal configuration for microphone input (client → target)
currentMicrophoneConfig = AudioConfig{
Quality: AudioQualityMedium,
Bitrate: Config.AudioQualityMediumInputBitrate,
Bitrate: Config.OptimalInputBitrate,
SampleRate: Config.SampleRate,
Channels: 1,
FrameSize: Config.AudioQualityMediumFrameSize,
FrameSize: 20 * time.Millisecond,
}
metrics AudioMetrics
)
// qualityPresets defines the base quality configurations
var qualityPresets = map[AudioQuality]struct {
outputBitrate, inputBitrate int
sampleRate, channels int
frameSize time.Duration
}{
AudioQualityLow: {
outputBitrate: Config.AudioQualityLowOutputBitrate, inputBitrate: Config.AudioQualityLowInputBitrate,
sampleRate: Config.AudioQualityLowSampleRate, channels: Config.AudioQualityLowChannels,
frameSize: Config.AudioQualityLowFrameSize,
},
AudioQualityMedium: {
outputBitrate: Config.AudioQualityMediumOutputBitrate, inputBitrate: Config.AudioQualityMediumInputBitrate,
sampleRate: Config.AudioQualityMediumSampleRate, channels: Config.AudioQualityMediumChannels,
frameSize: Config.AudioQualityMediumFrameSize,
},
AudioQualityHigh: {
outputBitrate: Config.AudioQualityHighOutputBitrate, inputBitrate: Config.AudioQualityHighInputBitrate,
sampleRate: Config.SampleRate, channels: Config.AudioQualityHighChannels,
frameSize: Config.AudioQualityHighFrameSize,
},
AudioQualityUltra: {
outputBitrate: Config.AudioQualityUltraOutputBitrate, inputBitrate: Config.AudioQualityUltraInputBitrate,
sampleRate: Config.SampleRate, channels: Config.AudioQualityUltraChannels,
frameSize: Config.AudioQualityUltraFrameSize,
},
}
// GetAudioQualityPresets returns predefined quality configurations for audio output
func GetAudioQualityPresets() map[AudioQuality]AudioConfig {
result := make(map[AudioQuality]AudioConfig)
for quality, preset := range qualityPresets {
config := AudioConfig{
Quality: quality,
Bitrate: preset.outputBitrate,
SampleRate: preset.sampleRate,
Channels: preset.channels,
FrameSize: preset.frameSize,
}
result[quality] = config
}
return result
}
// GetMicrophoneQualityPresets returns predefined quality configurations for microphone input
func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
result := make(map[AudioQuality]AudioConfig)
for quality, preset := range qualityPresets {
config := AudioConfig{
Quality: quality,
Bitrate: preset.inputBitrate,
SampleRate: func() int {
if quality == AudioQualityLow {
return Config.AudioQualityMicLowSampleRate
}
return preset.sampleRate
}(),
Channels: 1, // Microphone is always mono
FrameSize: preset.frameSize,
}
result[quality] = config
}
return result
}
// SetAudioQuality updates the current audio quality configuration
func SetAudioQuality(quality AudioQuality) {
// Validate audio quality parameter
if err := ValidateAudioQuality(quality); err != nil {
// Log validation error but don't fail - maintain backward compatibility
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Warn().Err(err).Int("quality", int(quality)).Msg("invalid audio quality, using current config")
return
}
presets := GetAudioQualityPresets()
if config, exists := presets[quality]; exists {
currentConfig = config
// Get OPUS encoder parameters based on quality
var complexity, vbr, signalType, bandwidth, dtx int
switch quality {
case AudioQualityLow:
complexity = Config.AudioQualityLowOpusComplexity
vbr = Config.AudioQualityLowOpusVBR
signalType = Config.AudioQualityLowOpusSignalType
bandwidth = Config.AudioQualityLowOpusBandwidth
dtx = Config.AudioQualityLowOpusDTX
case AudioQualityMedium:
complexity = Config.AudioQualityMediumOpusComplexity
vbr = Config.AudioQualityMediumOpusVBR
signalType = Config.AudioQualityMediumOpusSignalType
bandwidth = Config.AudioQualityMediumOpusBandwidth
dtx = Config.AudioQualityMediumOpusDTX
case AudioQualityHigh:
complexity = Config.AudioQualityHighOpusComplexity
vbr = Config.AudioQualityHighOpusVBR
signalType = Config.AudioQualityHighOpusSignalType
bandwidth = Config.AudioQualityHighOpusBandwidth
dtx = Config.AudioQualityHighOpusDTX
case AudioQualityUltra:
complexity = Config.AudioQualityUltraOpusComplexity
vbr = Config.AudioQualityUltraOpusVBR
signalType = Config.AudioQualityUltraOpusSignalType
bandwidth = Config.AudioQualityUltraOpusBandwidth
dtx = Config.AudioQualityUltraOpusDTX
default:
// Use medium quality as fallback
complexity = Config.AudioQualityMediumOpusComplexity
vbr = Config.AudioQualityMediumOpusVBR
signalType = Config.AudioQualityMediumOpusSignalType
bandwidth = Config.AudioQualityMediumOpusBandwidth
dtx = Config.AudioQualityMediumOpusDTX
}
// Update audio output subprocess configuration dynamically without restart
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Info().Int("quality", int(quality)).Msg("updating audio output quality settings dynamically")
// Set new OPUS configuration for future restarts
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
// Send dynamic configuration update to running subprocess via IPC
if supervisor.IsConnected() {
// Convert AudioConfig to UnifiedIPCOpusConfig with complete Opus parameters
opusConfig := UnifiedIPCOpusConfig{
SampleRate: config.SampleRate,
Channels: config.Channels,
FrameSize: int(config.FrameSize.Milliseconds() * int64(config.SampleRate) / 1000), // Convert ms to samples
Bitrate: config.Bitrate * 1000, // Convert kbps to bps
Complexity: complexity,
VBR: vbr,
SignalType: signalType,
Bandwidth: bandwidth,
DTX: dtx,
}
logger.Info().Interface("opusConfig", opusConfig).Msg("sending Opus configuration to audio output subprocess")
if err := supervisor.SendOpusConfig(opusConfig); err != nil {
logger.Warn().Err(err).Msg("failed to send dynamic Opus config update via IPC, falling back to subprocess restart")
// Fallback to subprocess restart if IPC update fails
supervisor.Stop()
if err := supervisor.Start(); err != nil {
logger.Error().Err(err).Msg("failed to restart audio output subprocess after IPC update failure")
}
} else {
logger.Info().Msg("audio output quality updated dynamically via IPC")
// Reset audio output stats after config update
go func() {
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
// Reset audio input server stats to clear persistent warnings
ResetGlobalAudioInputServerStats()
// Attempt recovery if there are still issues
time.Sleep(1 * time.Second)
RecoverGlobalAudioInputServer()
}()
}
} else {
logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio output subprocess not connected, configuration will apply on next start")
}
}
}
}
// GetAudioConfig returns the current audio configuration
// GetAudioConfig returns the current optimal audio configuration
func GetAudioConfig() AudioConfig {
return currentConfig
}
// GetMicrophoneConfig returns the current microphone configuration
// GetMicrophoneConfig returns the current optimal microphone configuration
func GetMicrophoneConfig() AudioConfig {
return currentMicrophoneConfig
}

View File

@ -7,22 +7,14 @@ import (
// RPC wrapper functions for audio control
// These functions bridge the RPC layer to the AudioControlService
// These variables will be set by the main package to provide access to the global service
// This variable will be set by the main package to provide access to the global service
var (
getAudioControlServiceFunc func() *AudioControlService
getAudioQualityFunc func() AudioConfig
setAudioQualityFunc func(AudioQuality) error
)
// SetRPCCallbacks sets the callback functions for RPC operations
func SetRPCCallbacks(
getService func() *AudioControlService,
getQuality func() AudioConfig,
setQuality func(AudioQuality) error,
) {
// SetRPCCallbacks sets the callback function for RPC operations
func SetRPCCallbacks(getService func() *AudioControlService) {
getAudioControlServiceFunc = getService
getAudioQualityFunc = getQuality
setAudioQualityFunc = setQuality
}
// RPCAudioMute handles audio mute/unmute RPC requests
@ -37,30 +29,11 @@ func RPCAudioMute(muted bool) error {
return service.MuteAudio(muted)
}
// RPCAudioQuality handles audio quality change RPC requests
// RPCAudioQuality is deprecated - quality is now fixed at optimal settings
// Returns current config for backward compatibility
func RPCAudioQuality(quality int) (map[string]any, error) {
if getAudioQualityFunc == nil || setAudioQualityFunc == nil {
return nil, fmt.Errorf("audio quality functions not available")
}
// Convert int to AudioQuality type
audioQuality := AudioQuality(quality)
// Get current audio quality configuration
currentConfig := getAudioQualityFunc()
// Set new quality if different
if currentConfig.Quality != audioQuality {
err := setAudioQualityFunc(audioQuality)
if err != nil {
return nil, fmt.Errorf("failed to set audio quality: %w", err)
}
// Get updated config after setting
newConfig := getAudioQualityFunc()
return map[string]any{"config": newConfig}, nil
}
// Return current config if no change needed
// Quality is now fixed - return current optimal configuration
currentConfig := GetAudioConfig()
return map[string]any{"config": currentConfig}, nil
}
@ -100,21 +73,15 @@ func RPCAudioStatus() (map[string]interface{}, error) {
return service.GetAudioStatus(), nil
}
// RPCAudioQualityPresets handles audio quality presets RPC requests (read-only)
// RPCAudioQualityPresets is deprecated - returns single optimal configuration
// Kept for backward compatibility with UI
func RPCAudioQualityPresets() (map[string]any, error) {
if getAudioControlServiceFunc == nil || getAudioQualityFunc == nil {
return nil, fmt.Errorf("audio control service not available")
}
service := getAudioControlServiceFunc()
if service == nil {
return nil, fmt.Errorf("audio control service not initialized")
}
presets := service.GetAudioQualityPresets()
current := getAudioQualityFunc()
// Return single optimal configuration as both preset and current
current := GetAudioConfig()
// Return empty presets map (UI will handle this gracefully)
return map[string]any{
"presets": presets,
"presets": map[string]any{},
"current": current,
}, nil
}

14
main.go
View File

@ -36,15 +36,15 @@ func startAudioSubprocess() error {
audioInputSupervisor := audio.NewAudioInputSupervisor()
audio.SetAudioInputSupervisor(audioInputSupervisor)
// Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106)
// Set optimal OPUS configuration for audio input supervisor (48 kbps mono mic)
audioConfig := audio.Config
audioInputSupervisor.SetOpusConfig(
audioConfig.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
audioConfig.AudioQualityLowOpusComplexity,
audioConfig.AudioQualityLowOpusVBR,
audioConfig.AudioQualityLowOpusSignalType,
audioConfig.AudioQualityLowOpusBandwidth,
audioConfig.AudioQualityLowOpusDTX,
audioConfig.OptimalInputBitrate*1000, // Convert kbps to bps (48 kbps)
audioConfig.OptimalOpusComplexity, // Complexity 1 for minimal CPU
audioConfig.OptimalOpusVBR, // VBR enabled
audioConfig.OptimalOpusSignalType, // MUSIC signal type
audioConfig.OptimalOpusBandwidth, // WIDEBAND for 48kHz
audioConfig.OptimalOpusDTX, // DTX disabled
)
// Note: Audio input supervisor is NOT started here - it will be started on-demand

View File

@ -8,7 +8,6 @@ import { useAudioEvents } from "@/hooks/useAudioEvents";
import { useJsonRpc, JsonRpcResponse } from "@/hooks/useJsonRpc";
import { useRTCStore } from "@/hooks/stores";
import notifications from "@/notifications";
import audioQualityService from "@/services/audioQualityService";
// Type for microphone error
interface MicrophoneError {
@ -69,11 +68,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
const { send } = useJsonRpc();
// Initialize audio quality service with RPC for cloud compatibility
useEffect(() => {
if (send) {
audioQualityService.setRpcSend(send);
}
}, [send]);
// Audio quality service removed - using fixed optimal configuration
// WebSocket-only implementation - no fallback polling
@ -131,12 +126,24 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
const loadAudioConfigurations = async () => {
try {
// Use centralized audio quality service
const { audio } = await audioQualityService.loadAllConfigurations();
// Load audio configuration directly via RPC
if (!send) return;
if (audio) {
setCurrentConfig(audio.current);
await new Promise<void>((resolve, reject) => {
send("audioStatus", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
reject(new Error(resp.error.message));
} else if ("result" in resp && resp.result) {
const result = resp.result as any;
if (result.config) {
setCurrentConfig(result.config);
}
resolve();
} else {
resolve();
}
});
});
setConfigsLoaded(true);
} catch {
@ -180,34 +187,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
}
};
const handleQualityChange = async (quality: number) => {
setIsLoading(true);
try {
// Use RPC for device communication - works for both local and cloud
if (rpcDataChannel?.readyState !== "open") {
throw new Error("Device connection not available");
}
await new Promise<void>((resolve, reject) => {
send("audioQuality", { quality }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
reject(new Error(resp.error.message));
} else {
// Update local state with response
if ("result" in resp && resp.result && typeof resp.result === 'object' && 'config' in resp.result) {
setCurrentConfig(resp.result.config as AudioConfig);
}
resolve();
}
});
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Failed to change audio quality";
notifications.error(errorMessage);
} finally {
setIsLoading(false);
}
};
// Quality change handler removed - quality is now fixed at optimal settings
const handleToggleMicrophoneEnable = async () => {
const now = Date.now();
@ -447,45 +427,24 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
</button>
</div>
{/* Quality Settings */}
<div className="space-y-3">
{/* Audio Quality Info (fixed optimal configuration) */}
{currentConfig && (
<div className="space-y-2 rounded-md bg-slate-50 p-3 dark:bg-slate-800">
<div className="flex items-center gap-2">
<MdGraphicEq className="h-4 w-4 text-slate-600 dark:text-slate-400" />
<span className="font-medium text-slate-900 dark:text-slate-100">
Audio Output Quality
Audio Configuration
</span>
</div>
<div className="grid grid-cols-2 gap-2">
{Object.entries(audioQualityService.getQualityLabels()).map(([quality, label]) => (
<button
key={quality}
onClick={() => handleQualityChange(parseInt(quality))}
disabled={isLoading}
className={cx(
"rounded-md border px-3 py-2 text-sm font-medium transition-colors",
currentConfig?.Quality === parseInt(quality)
? "border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
: "border-slate-200 bg-white text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600",
isLoading && "opacity-50 cursor-not-allowed"
)}
>
{label}
</button>
))}
<div className="text-sm text-slate-600 dark:text-slate-400">
Optimized for S16_LE @ 48kHz stereo HDMI audio
</div>
<div className="text-xs text-slate-500 dark:text-slate-500">
Bitrate: {currentConfig.Bitrate} kbps | Sample Rate: {currentConfig.SampleRate} Hz | Channels: {currentConfig.Channels}
</div>
{currentConfig && (
<div className="text-xs text-slate-600 dark:text-slate-400 mt-2">
Bitrate: {currentConfig.Bitrate}kbps |
Sample Rate: {currentConfig.SampleRate}Hz
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -89,17 +89,8 @@ export const AUDIO_CONFIG = {
SYNC_DEBOUNCE_MS: 1000, // debounce state synchronization
AUDIO_TEST_TIMEOUT: 100, // ms - timeout for audio testing
// NOTE: Audio quality presets (bitrates, sample rates, channels, frame sizes)
// are now fetched dynamically from the backend API via audioQualityService
// to eliminate duplication with backend config_constants.go
// Default Quality Labels - will be updated dynamically by audioQualityService
DEFAULT_QUALITY_LABELS: {
0: "Low",
1: "Medium",
2: "High",
3: "Ultra",
} as const,
// Audio quality is fixed at optimal settings (96 kbps @ 48kHz stereo)
// No quality presets needed - single optimal configuration for all use cases
// Audio Analysis
ANALYSIS_FFT_SIZE: 256, // for detailed audio analysis

View File

@ -1,146 +0,0 @@
import { JsonRpcResponse } from '@/hooks/useJsonRpc';
interface AudioConfig {
Quality: number;
Bitrate: number;
SampleRate: number;
Channels: number;
FrameSize: string;
}
type QualityPresets = Record<number, AudioConfig>;
interface AudioQualityResponse {
current: AudioConfig;
presets: QualityPresets;
}
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (resp: JsonRpcResponse) => void) => void;
class AudioQualityService {
private audioPresets: QualityPresets | null = null;
private microphonePresets: QualityPresets | null = null;
private qualityLabels: Record<number, string> = {
0: 'Low',
1: 'Medium',
2: 'High',
3: 'Ultra'
};
private rpcSend: RpcSendFunction | null = null;
/**
* Set RPC send function for cloud compatibility
*/
setRpcSend(rpcSend: RpcSendFunction): void {
this.rpcSend = rpcSend;
}
/**
* Fetch audio quality presets using RPC (cloud-compatible)
*/
async fetchAudioQualityPresets(): Promise<AudioQualityResponse | null> {
if (!this.rpcSend) {
console.error('RPC not available for audio quality presets');
return null;
}
try {
return await new Promise<AudioQualityResponse | null>((resolve) => {
this.rpcSend!("audioQualityPresets", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.error('RPC audio quality presets failed:', resp.error);
resolve(null);
} else if ("result" in resp) {
const data = resp.result as AudioQualityResponse;
this.audioPresets = data.presets;
this.updateQualityLabels(data.presets);
resolve(data);
} else {
resolve(null);
}
});
});
} catch (error) {
console.error('Failed to fetch audio quality presets:', error);
return null;
}
}
/**
* Update quality labels with actual bitrates from presets
*/
private updateQualityLabels(presets: QualityPresets): void {
const newQualityLabels: Record<number, string> = {};
Object.entries(presets).forEach(([qualityNum, preset]) => {
const quality = parseInt(qualityNum);
const qualityNames = ['Low', 'Medium', 'High', 'Ultra'];
const name = qualityNames[quality] || `Quality ${quality}`;
newQualityLabels[quality] = `${name} (${preset.Bitrate}kbps)`;
});
this.qualityLabels = newQualityLabels;
}
/**
* Get quality labels with bitrates
*/
getQualityLabels(): Record<number, string> {
return this.qualityLabels;
}
/**
* Get cached audio presets
*/
getAudioPresets(): QualityPresets | null {
return this.audioPresets;
}
/**
* Get cached microphone presets
*/
getMicrophonePresets(): QualityPresets | null {
return this.microphonePresets;
}
/**
* Set audio quality using RPC (cloud-compatible)
*/
async setAudioQuality(quality: number): Promise<boolean> {
if (!this.rpcSend) {
console.error('RPC not available for audio quality change');
return false;
}
try {
return await new Promise<boolean>((resolve) => {
this.rpcSend!("audioQuality", { quality }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.error('RPC audio quality change failed:', resp.error);
resolve(false);
} else {
resolve(true);
}
});
});
} catch (error) {
console.error('Failed to set audio quality:', error);
return false;
}
}
/**
* Load both audio and microphone configurations
*/
async loadAllConfigurations(): Promise<{
audio: AudioQualityResponse | null;
}> {
const [audio ] = await Promise.all([
this.fetchAudioQualityPresets(),
]);
return { audio };
}
}
// Export a singleton instance
export const audioQualityService = new AudioQualityService();
export default audioQualityService;