Compare commits

..

No commits in common. "950ca2bd9966ae69ebe0463b1c4ba2d22f73b5ec" and "0d4176cf98748a18481b5893c282980d30864836" have entirely different histories.

14 changed files with 106 additions and 689 deletions

View File

@ -13,7 +13,6 @@ func main() {
versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit") versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit")
audioServerPtr := flag.Bool("audio-output-server", false, "Run as audio server subprocess") audioServerPtr := flag.Bool("audio-output-server", false, "Run as audio server subprocess")
audioInputServerPtr := flag.Bool("audio-input-server", false, "Run as audio input server subprocess") audioInputServerPtr := flag.Bool("audio-input-server", false, "Run as audio input server subprocess")
flag.Parse() flag.Parse()
if *versionPtr || *versionJsonPtr { if *versionPtr || *versionJsonPtr {

View File

@ -8,8 +8,8 @@ import (
) )
var ( var (
// Global audio output supervisor instance
globalOutputSupervisor unsafe.Pointer // *AudioOutputSupervisor globalOutputSupervisor unsafe.Pointer // *AudioOutputSupervisor
globalInputSupervisor unsafe.Pointer // *AudioInputSupervisor
) )
// isAudioServerProcess detects if we're running as the audio server subprocess // isAudioServerProcess detects if we're running as the audio server subprocess
@ -70,17 +70,3 @@ func GetAudioOutputSupervisor() *AudioOutputSupervisor {
} }
return (*AudioOutputSupervisor)(ptr) return (*AudioOutputSupervisor)(ptr)
} }
// SetAudioInputSupervisor sets the global audio input supervisor
func SetAudioInputSupervisor(supervisor *AudioInputSupervisor) {
atomic.StorePointer(&globalInputSupervisor, unsafe.Pointer(supervisor))
}
// GetAudioInputSupervisor returns the global audio input supervisor
func GetAudioInputSupervisor() *AudioInputSupervisor {
ptr := atomic.LoadPointer(&globalInputSupervisor)
if ptr == nil {
return nil
}
return (*AudioInputSupervisor)(ptr)
}

View File

@ -1,6 +1,3 @@
//go:build cgo
// +build cgo
// Package audio provides real-time audio processing for JetKVM with low-latency streaming. // Package audio provides real-time audio processing for JetKVM with low-latency streaming.
// //
// Key components: output/input pipelines with Opus codec, adaptive buffer management, // Key components: output/input pipelines with Opus codec, adaptive buffer management,
@ -170,7 +167,7 @@ func SetAudioQuality(quality AudioQuality) {
if config, exists := presets[quality]; exists { if config, exists := presets[quality]; exists {
currentConfig = config currentConfig = config
// Get OPUS encoder parameters based on quality // Update CGO OPUS encoder parameters based on quality
var complexity, vbr, signalType, bandwidth, dtx int var complexity, vbr, signalType, bandwidth, dtx int
switch quality { switch quality {
case AudioQualityLow: case AudioQualityLow:
@ -206,30 +203,14 @@ func SetAudioQuality(quality AudioQuality) {
dtx = GetConfig().AudioQualityMediumOpusDTX dtx = GetConfig().AudioQualityMediumOpusDTX
} }
// Restart audio output subprocess with new OPUS configuration // Dynamically update CGO OPUS encoder parameters
if supervisor := GetAudioOutputSupervisor(); supervisor != nil { // Use current VBR constraint setting from config
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Info().Int("quality", int(quality)).Msg("restarting audio output subprocess with new quality settings")
// Set new OPUS configuration
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
// Stop current subprocess
supervisor.Stop()
// Start subprocess with new configuration
if err := supervisor.Start(); err != nil {
logger.Error().Err(err).Msg("failed to restart audio output subprocess")
}
} else {
// Fallback to dynamic update if supervisor is not available
vbrConstraint := GetConfig().CGOOpusVBRConstraint vbrConstraint := GetConfig().CGOOpusVBRConstraint
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil { if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters") logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
} }
} }
} }
}
// GetAudioConfig returns the current audio configuration // GetAudioConfig returns the current audio configuration
func GetAudioConfig() AudioConfig { func GetAudioConfig() AudioConfig {
@ -249,81 +230,6 @@ func SetMicrophoneQuality(quality AudioQuality) {
presets := GetMicrophoneQualityPresets() presets := GetMicrophoneQualityPresets()
if config, exists := presets[quality]; exists { if config, exists := presets[quality]; exists {
currentMicrophoneConfig = config currentMicrophoneConfig = config
// Get OPUS parameters for the selected quality
var complexity, vbr, signalType, bandwidth, dtx int
switch quality {
case AudioQualityLow:
complexity = GetConfig().AudioQualityLowOpusComplexity
vbr = GetConfig().AudioQualityLowOpusVBR
signalType = GetConfig().AudioQualityLowOpusSignalType
bandwidth = GetConfig().AudioQualityLowOpusBandwidth
dtx = GetConfig().AudioQualityLowOpusDTX
case AudioQualityMedium:
complexity = GetConfig().AudioQualityMediumOpusComplexity
vbr = GetConfig().AudioQualityMediumOpusVBR
signalType = GetConfig().AudioQualityMediumOpusSignalType
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
dtx = GetConfig().AudioQualityMediumOpusDTX
case AudioQualityHigh:
complexity = GetConfig().AudioQualityHighOpusComplexity
vbr = GetConfig().AudioQualityHighOpusVBR
signalType = GetConfig().AudioQualityHighOpusSignalType
bandwidth = GetConfig().AudioQualityHighOpusBandwidth
dtx = GetConfig().AudioQualityHighOpusDTX
case AudioQualityUltra:
complexity = GetConfig().AudioQualityUltraOpusComplexity
vbr = GetConfig().AudioQualityUltraOpusVBR
signalType = GetConfig().AudioQualityUltraOpusSignalType
bandwidth = GetConfig().AudioQualityUltraOpusBandwidth
dtx = GetConfig().AudioQualityUltraOpusDTX
default:
// Use medium quality as fallback
complexity = GetConfig().AudioQualityMediumOpusComplexity
vbr = GetConfig().AudioQualityMediumOpusVBR
signalType = GetConfig().AudioQualityMediumOpusSignalType
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
dtx = GetConfig().AudioQualityMediumOpusDTX
}
// Update audio input subprocess configuration dynamically without restart
if supervisor := GetAudioInputSupervisor(); supervisor != nil {
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Info().Int("quality", int(quality)).Msg("updating audio input subprocess quality settings dynamically")
// Set new OPUS configuration for future restarts
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
// Send dynamic configuration update to running subprocess
if supervisor.IsConnected() {
// Convert AudioConfig to InputIPCOpusConfig with complete Opus parameters
opusConfig := InputIPCOpusConfig{
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 input subprocess")
if err := supervisor.SendOpusConfig(opusConfig); err != nil {
logger.Warn().Err(err).Msg("failed to send dynamic Opus config update, subprocess may need restart")
// Fallback to restart if dynamic update fails
supervisor.Stop()
if err := supervisor.Start(); err != nil {
logger.Error().Err(err).Msg("failed to restart audio input subprocess after config update failure")
}
} else {
logger.Info().Msg("audio input quality updated dynamically with complete Opus configuration")
}
} else {
logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio input subprocess not connected, configuration will apply on next start")
}
}
} }
} }

View File

@ -6,6 +6,8 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"unsafe" "unsafe"
"github.com/jetkvm/kvm/internal/logging"
) )
// Lock-free buffer cache for per-goroutine optimization // Lock-free buffer cache for per-goroutine optimization
@ -16,9 +18,6 @@ type lockFreeBufferCache struct {
// Per-goroutine buffer cache using goroutine-local storage // Per-goroutine buffer cache using goroutine-local storage
var goroutineBufferCache = make(map[int64]*lockFreeBufferCache) var goroutineBufferCache = make(map[int64]*lockFreeBufferCache)
var goroutineCacheMutex sync.RWMutex var goroutineCacheMutex sync.RWMutex
var lastCleanupTime int64 // Unix timestamp of last cleanup
const maxCacheSize = 1000 // Maximum number of goroutine caches
const cleanupInterval = 300 // Cleanup interval in seconds (5 minutes)
// getGoroutineID extracts goroutine ID from runtime stack for cache key // getGoroutineID extracts goroutine ID from runtime stack for cache key
func getGoroutineID() int64 { func getGoroutineID() int64 {
@ -39,41 +38,6 @@ func getGoroutineID() int64 {
return 0 return 0
} }
// cleanupGoroutineCache removes stale entries from the goroutine cache
func cleanupGoroutineCache() {
now := time.Now().Unix()
lastCleanup := atomic.LoadInt64(&lastCleanupTime)
// Only cleanup if enough time has passed
if now-lastCleanup < cleanupInterval {
return
}
// Try to acquire cleanup lock atomically
if !atomic.CompareAndSwapInt64(&lastCleanupTime, lastCleanup, now) {
return // Another goroutine is already cleaning up
}
goroutineCacheMutex.Lock()
defer goroutineCacheMutex.Unlock()
// If cache is too large, remove oldest entries (simple FIFO)
if len(goroutineBufferCache) > maxCacheSize {
// Remove half of the entries to avoid frequent cleanups
toRemove := len(goroutineBufferCache) - maxCacheSize/2
count := 0
for gid := range goroutineBufferCache {
delete(goroutineBufferCache, gid)
count++
if count >= toRemove {
break
}
}
// Log cleanup for debugging (removed logging dependency)
_ = count // Avoid unused variable warning
}
}
type AudioBufferPool struct { type AudioBufferPool struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment) // Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
currentSize int64 // Current pool size (atomic) currentSize int64 // Current pool size (atomic)
@ -93,7 +57,9 @@ type AudioBufferPool struct {
func NewAudioBufferPool(bufferSize int) *AudioBufferPool { func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
// Validate buffer size parameter // Validate buffer size parameter
if err := ValidateBufferSize(bufferSize); err != nil { if err := ValidateBufferSize(bufferSize); err != nil {
// Use default value on validation error // Log validation error and use default value
logger := logging.GetDefaultLogger().With().Str("component", "AudioBufferPool").Logger()
logger.Warn().Err(err).Int("bufferSize", bufferSize).Msg("invalid buffer size, using default")
bufferSize = GetConfig().AudioFramePoolSize bufferSize = GetConfig().AudioFramePoolSize
} }
@ -133,9 +99,6 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
} }
func (p *AudioBufferPool) Get() []byte { func (p *AudioBufferPool) Get() []byte {
// Trigger periodic cleanup of goroutine cache
cleanupGoroutineCache()
start := time.Now() start := time.Now()
wasHit := false wasHit := false
defer func() { defer func() {

View File

@ -709,12 +709,6 @@ func cgoAudioReadEncode(buf []byte) (int, error) {
return 0, newBufferTooSmallError(len(buf), minRequired) return 0, newBufferTooSmallError(len(buf), minRequired)
} }
// Skip initialization check for now to avoid CGO compilation issues
// TODO: Add proper initialization state checking
// Note: The C code already has comprehensive state tracking with capture_initialized,
// capture_initializing, playback_initialized, and playback_initializing flags.
// When CGO environment is properly configured, this should check C.capture_initialized.
n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0])) n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0]))
if n < 0 { if n < 0 {
return 0, newAudioReadEncodeError(int(n)) return 0, newAudioReadEncodeError(int(n))

View File

@ -1554,36 +1554,35 @@ func DefaultAudioConfig() *AudioConfigConstants {
FrameSize: 960, FrameSize: 960,
MaxPacketSize: 4000, MaxPacketSize: 4000,
// Audio Quality Bitrates - Optimized for RV1106 SoC and KVM layer compatibility // Audio Quality Bitrates
// Reduced bitrates to minimize CPU load and prevent mouse lag
AudioQualityLowOutputBitrate: 32, AudioQualityLowOutputBitrate: 32,
AudioQualityLowInputBitrate: 16, AudioQualityLowInputBitrate: 16,
AudioQualityMediumOutputBitrate: 48, AudioQualityMediumOutputBitrate: 64,
AudioQualityMediumInputBitrate: 24, AudioQualityMediumInputBitrate: 32,
// AudioQualityHighOutputBitrate defines bitrate for high-quality output. // AudioQualityHighOutputBitrate defines bitrate for high-quality output.
// Used in: Professional applications requiring good audio fidelity on RV1106 // Used in: Professional applications requiring excellent audio fidelity
// Impact: Balanced quality optimized for single-core ARM performance. // Impact: Provides excellent quality but increases bandwidth usage.
// Reduced to 64kbps for RV1106 compatibility and minimal CPU overhead. // Default 128kbps matches professional Opus encoding standards.
AudioQualityHighOutputBitrate: 64, AudioQualityHighOutputBitrate: 128,
// AudioQualityHighInputBitrate defines bitrate for high-quality input. // AudioQualityHighInputBitrate defines bitrate for high-quality input.
// Used in: High-quality microphone input optimized for RV1106 // Used in: High-quality microphone input for professional use
// Impact: Clear voice reproduction without overwhelming single-core CPU. // Impact: Ensures clear voice reproduction for professional scenarios.
// Reduced to 32kbps for optimal RV1106 performance without lag. // Default 64kbps provides excellent voice quality.
AudioQualityHighInputBitrate: 32, AudioQualityHighInputBitrate: 64,
// AudioQualityUltraOutputBitrate defines bitrate for ultra-quality output. // AudioQualityUltraOutputBitrate defines bitrate for ultra-quality output.
// Used in: Maximum quality while ensuring RV1106 stability // Used in: Audiophile-grade reproduction and high-bandwidth connections
// Impact: Best possible quality without interfering with KVM operations. // Impact: Maximum quality but requires significant bandwidth.
// Reduced to 96kbps for RV1106 maximum performance without mouse lag. // Default 192kbps suitable for high-bandwidth, quality-critical scenarios.
AudioQualityUltraOutputBitrate: 96, AudioQualityUltraOutputBitrate: 192,
// AudioQualityUltraInputBitrate defines bitrate for ultra-quality input. // AudioQualityUltraInputBitrate defines bitrate for ultra-quality input.
// Used in: Premium microphone input optimized for RV1106 constraints // Used in: Professional microphone input requiring maximum quality
// Impact: Excellent voice quality within RV1106 processing limits. // Impact: Provides audiophile-grade voice quality with high bandwidth.
// Reduced to 48kbps for stable RV1106 operation without lag. // Default 96kbps ensures maximum voice reproduction quality.
AudioQualityUltraInputBitrate: 48, AudioQualityUltraInputBitrate: 96,
// Audio Quality Sample Rates - Sampling frequencies for different quality levels // Audio Quality Sample Rates - Sampling frequencies for different quality levels
// Used in: Audio capture, processing, and format negotiation // Used in: Audio capture, processing, and format negotiation
@ -1591,15 +1590,15 @@ func DefaultAudioConfig() *AudioConfigConstants {
// AudioQualityLowSampleRate defines sampling frequency for low-quality audio. // AudioQualityLowSampleRate defines sampling frequency for low-quality audio.
// Used in: Bandwidth-constrained scenarios and basic audio requirements // Used in: Bandwidth-constrained scenarios and basic audio requirements
// Impact: Captures frequencies up to 24kHz while maintaining efficiency. // Impact: Captures frequencies up to 11kHz while minimizing processing load.
// Default 48kHz provides better quality while maintaining compatibility. // Default 22.05kHz sufficient for speech and basic audio.
AudioQualityLowSampleRate: 48000, AudioQualityLowSampleRate: 22050,
// AudioQualityMediumSampleRate defines sampling frequency for medium-quality audio. // AudioQualityMediumSampleRate defines sampling frequency for medium-quality audio.
// Used in: Standard audio scenarios requiring high-quality reproduction // Used in: Standard audio scenarios requiring CD-quality reproduction
// Impact: Captures full audible range up to 24kHz with excellent processing. // Impact: Captures full audible range up to 22kHz with balanced processing.
// Default 48kHz provides professional standard with optimal balance. // Default 44.1kHz provides CD-quality standard with excellent balance.
AudioQualityMediumSampleRate: 48000, AudioQualityMediumSampleRate: 44100,
// AudioQualityMicLowSampleRate defines sampling frequency for low-quality microphone. // AudioQualityMicLowSampleRate defines sampling frequency for low-quality microphone.
// Used in: Voice/microphone input in bandwidth-constrained scenarios // Used in: Voice/microphone input in bandwidth-constrained scenarios
@ -1612,80 +1611,80 @@ func DefaultAudioConfig() *AudioConfigConstants {
// Impact: Controls latency vs processing efficiency trade-offs // Impact: Controls latency vs processing efficiency trade-offs
// AudioQualityLowFrameSize defines frame duration for low-quality audio. // AudioQualityLowFrameSize defines frame duration for low-quality audio.
// Used in: RV1106 efficiency-prioritized scenarios // Used in: Bandwidth-constrained scenarios prioritizing efficiency
// Impact: Balanced frame size for quality and efficiency. // Impact: Reduces processing overhead with acceptable latency increase.
// Reduced to 20ms for better responsiveness and reduced audio saccades. // Default 40ms provides efficiency for constrained scenarios.
AudioQualityLowFrameSize: 20 * time.Millisecond, AudioQualityLowFrameSize: 40 * time.Millisecond,
// AudioQualityMediumFrameSize defines frame duration for medium-quality audio. // AudioQualityMediumFrameSize defines frame duration for medium-quality audio.
// Used in: Balanced RV1106 real-time audio applications // Used in: Standard real-time audio applications
// Impact: Balances latency and processing efficiency for RV1106. // Impact: Provides good balance of latency and processing efficiency.
// Optimized to 20ms for RV1106 balanced performance. // Default 20ms standard for real-time audio applications.
AudioQualityMediumFrameSize: 20 * time.Millisecond, AudioQualityMediumFrameSize: 20 * time.Millisecond,
// AudioQualityHighFrameSize defines frame duration for high-quality audio. // AudioQualityHighFrameSize defines frame duration for high-quality audio.
// Used in: RV1106 high-quality scenarios with performance constraints // Used in: High-quality audio scenarios with balanced requirements
// Impact: Maintains acceptable latency while reducing RV1106 CPU load. // Impact: Maintains good latency while ensuring quality processing.
// Optimized to 20ms for RV1106 high-quality balance. // Default 20ms provides optimal balance for high-quality scenarios.
AudioQualityHighFrameSize: 20 * time.Millisecond, AudioQualityHighFrameSize: 20 * time.Millisecond,
// AudioQualityUltraFrameSize defines frame duration for ultra-quality audio. // AudioQualityUltraFrameSize defines frame duration for ultra-quality audio.
// Used in: Maximum RV1106 performance without KVM interference // Used in: Applications requiring immediate audio feedback
// Impact: Balances quality and processing efficiency for RV1106 stability. // Impact: Minimizes latency for ultra-responsive audio processing.
// Optimized to 20ms for RV1106 maximum stable performance. // Default 10ms ensures minimal latency for immediate feedback.
AudioQualityUltraFrameSize: 20 * time.Millisecond, AudioQualityUltraFrameSize: 10 * time.Millisecond,
// Audio Quality Channels - Optimized for RV1106 processing efficiency // Audio Quality Channels - Channel configuration for different quality levels
// Used in: Audio processing pipeline optimized for single-core ARM performance // Used in: Audio processing pipeline for channel handling and bandwidth control
AudioQualityLowChannels: 1, // Mono for minimal RV1106 processing AudioQualityLowChannels: 1,
AudioQualityMediumChannels: 2, // Stereo for balanced RV1106 performance AudioQualityMediumChannels: 2,
AudioQualityHighChannels: 2, // Stereo for RV1106 high-quality scenarios AudioQualityHighChannels: 2,
AudioQualityUltraChannels: 2, // Stereo for maximum RV1106 performance AudioQualityUltraChannels: 2,
// Audio Quality OPUS Encoder Parameters - Quality-specific encoder settings // Audio Quality OPUS Encoder Parameters - Quality-specific encoder settings
// Used in: Dynamic OPUS encoder configuration based on quality presets // Used in: Dynamic OPUS encoder configuration based on quality presets
// Impact: Controls encoding complexity, VBR, signal type, bandwidth, and DTX // Impact: Controls encoding complexity, VBR, signal type, bandwidth, and DTX
// Low Quality OPUS Parameters - Optimized for RV1106 minimal CPU usage // Low Quality OPUS Parameters - Optimized for bandwidth conservation
AudioQualityLowOpusComplexity: 0, // Minimum complexity to reduce CPU load AudioQualityLowOpusComplexity: 1, // Low complexity for minimal CPU usage
AudioQualityLowOpusVBR: 1, // VBR for better quality at same bitrate AudioQualityLowOpusVBR: 0, // CBR for predictable bandwidth
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for lower complexity AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND for efficiency AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND
AudioQualityLowOpusDTX: 1, // Enable DTX to reduce processing when silent AudioQualityLowOpusDTX: 1, // Enable DTX for silence suppression
// Medium Quality OPUS Parameters - Balanced for RV1106 performance // Medium Quality OPUS Parameters - Balanced performance and quality
AudioQualityMediumOpusComplexity: 1, // Very low complexity for RV1106 stability AudioQualityMediumOpusComplexity: 5, // Medium complexity for balanced performance
AudioQualityMediumOpusVBR: 1, // VBR for optimal quality AudioQualityMediumOpusVBR: 1, // VBR for better quality
AudioQualityMediumOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for efficiency AudioQualityMediumOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityMediumOpusBandwidth: 1102, // OPUS_BANDWIDTH_MEDIUMBAND for balance AudioQualityMediumOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND
AudioQualityMediumOpusDTX: 1, // Enable DTX for CPU savings AudioQualityMediumOpusDTX: 0, // Disable DTX for consistent quality
// High Quality OPUS Parameters - Optimized for RV1106 high performance // High Quality OPUS Parameters - High quality with good performance
AudioQualityHighOpusComplexity: 2, // Low complexity for RV1106 limits AudioQualityHighOpusComplexity: 8, // High complexity for better quality
AudioQualityHighOpusVBR: 1, // VBR for optimal quality AudioQualityHighOpusVBR: 1, // VBR for optimal quality
AudioQualityHighOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC AudioQualityHighOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityHighOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for good range AudioQualityHighOpusBandwidth: 1104, // OPUS_BANDWIDTH_SUPERWIDEBAND
AudioQualityHighOpusDTX: 0, // Disable DTX for consistent quality AudioQualityHighOpusDTX: 0, // Disable DTX for consistent quality
// Ultra Quality OPUS Parameters - Maximum RV1106 performance without KVM interference // Ultra Quality OPUS Parameters - Maximum quality settings
AudioQualityUltraOpusComplexity: 3, // Moderate complexity for RV1106 stability AudioQualityUltraOpusComplexity: 10, // Maximum complexity for best quality
AudioQualityUltraOpusVBR: 1, // VBR for optimal quality AudioQualityUltraOpusVBR: 1, // VBR for optimal quality
AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityUltraOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for stability AudioQualityUltraOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
AudioQualityUltraOpusDTX: 0, // Disable DTX for maximum quality AudioQualityUltraOpusDTX: 0, // Disable DTX for maximum quality
// CGO Audio Constants - Optimized for RV1106 native audio processing // CGO Audio Constants
CGOOpusBitrate: 64000, // Reduced for RV1106 efficiency CGOOpusBitrate: 96000,
CGOOpusComplexity: 2, // Minimal complexity for RV1106 CGOOpusComplexity: 3,
CGOOpusVBR: 1, CGOOpusVBR: 1,
CGOOpusVBRConstraint: 1, CGOOpusVBRConstraint: 1,
CGOOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC CGOOpusSignalType: 3, // OPUS_SIGNAL_MUSIC
CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for RV1106 CGOOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
CGOOpusDTX: 0, CGOOpusDTX: 0,
CGOSampleRate: 48000, CGOSampleRate: 48000,
CGOChannels: 2, CGOChannels: 2,
CGOFrameSize: 960, CGOFrameSize: 960,
CGOMaxPacketSize: 1200, // Reduced for RV1106 memory efficiency CGOMaxPacketSize: 1500,
// Input IPC Constants // Input IPC Constants
// InputIPCSampleRate defines sample rate for input IPC operations. // InputIPCSampleRate defines sample rate for input IPC operations.

View File

@ -208,30 +208,7 @@ func (aim *AudioInputManager) LogPerformanceStats() {
Msg("Audio input performance metrics") Msg("Audio input performance metrics")
} }
// IsRunning returns whether the audio input manager is running // Note: IsRunning() is inherited from BaseAudioManager
// This checks both the internal state and existing system processes
func (aim *AudioInputManager) IsRunning() bool {
// First check internal state
if aim.BaseAudioManager.IsRunning() {
return true
}
// If internal state says not running, check for existing system processes
// This prevents duplicate subprocess creation when a process already exists
if aim.ipcManager != nil {
supervisor := aim.ipcManager.GetSupervisor()
if supervisor != nil {
if existingPID, exists := supervisor.HasExistingProcess(); exists {
aim.logger.Info().Int("existing_pid", existingPID).Msg("Found existing audio input server process")
// Update internal state to reflect reality
aim.setRunning(true)
return true
}
}
}
return false
}
// IsReady returns whether the audio input manager is ready to receive frames // IsReady returns whether the audio input manager is ready to receive frames
// This checks both that it's running and that the IPC connection is established // This checks both that it's running and that the IPC connection is established

View File

@ -36,7 +36,6 @@ type InputMessageType uint8
const ( const (
InputMessageTypeOpusFrame InputMessageType = iota InputMessageTypeOpusFrame InputMessageType = iota
InputMessageTypeConfig InputMessageTypeConfig
InputMessageTypeOpusConfig
InputMessageTypeStop InputMessageTypeStop
InputMessageTypeHeartbeat InputMessageTypeHeartbeat
InputMessageTypeAck InputMessageTypeAck
@ -204,19 +203,6 @@ type InputIPCConfig struct {
FrameSize int FrameSize int
} }
// InputIPCOpusConfig contains complete Opus encoder configuration
type InputIPCOpusConfig struct {
SampleRate int
Channels int
FrameSize int
Bitrate int
Complexity int
VBR int
SignalType int
Bandwidth int
DTX int
}
// AudioInputServer handles IPC communication for audio input processing // AudioInputServer handles IPC communication for audio input processing
type AudioInputServer struct { type AudioInputServer struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment) // Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
@ -476,8 +462,6 @@ func (ais *AudioInputServer) processMessage(msg *InputIPCMessage) error {
return ais.processOpusFrame(msg.Data) return ais.processOpusFrame(msg.Data)
case InputMessageTypeConfig: case InputMessageTypeConfig:
return ais.processConfig(msg.Data) return ais.processConfig(msg.Data)
case InputMessageTypeOpusConfig:
return ais.processOpusConfig(msg.Data)
case InputMessageTypeStop: case InputMessageTypeStop:
return fmt.Errorf("stop message received") return fmt.Errorf("stop message received")
case InputMessageTypeHeartbeat: case InputMessageTypeHeartbeat:
@ -523,50 +507,6 @@ func (ais *AudioInputServer) processConfig(data []byte) error {
return ais.sendAck() return ais.sendAck()
} }
// processOpusConfig processes a complete Opus encoder configuration update
func (ais *AudioInputServer) processOpusConfig(data []byte) error {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputServerComponent).Logger()
// Validate configuration data size (9 * int32 = 36 bytes)
if len(data) != 36 {
return fmt.Errorf("invalid Opus configuration data size: expected 36 bytes, got %d", len(data))
}
// Deserialize Opus configuration
config := InputIPCOpusConfig{
SampleRate: int(binary.LittleEndian.Uint32(data[0:4])),
Channels: int(binary.LittleEndian.Uint32(data[4:8])),
FrameSize: int(binary.LittleEndian.Uint32(data[8:12])),
Bitrate: int(binary.LittleEndian.Uint32(data[12:16])),
Complexity: int(binary.LittleEndian.Uint32(data[16:20])),
VBR: int(binary.LittleEndian.Uint32(data[20:24])),
SignalType: int(binary.LittleEndian.Uint32(data[24:28])),
Bandwidth: int(binary.LittleEndian.Uint32(data[28:32])),
DTX: int(binary.LittleEndian.Uint32(data[32:36])),
}
logger.Info().Interface("config", config).Msg("applying dynamic Opus encoder configuration")
// Apply the Opus encoder configuration dynamically
err := CGOUpdateOpusEncoderParams(
config.Bitrate,
config.Complexity,
config.VBR,
0, // VBR constraint - using default
config.SignalType,
config.Bandwidth,
config.DTX,
)
if err != nil {
logger.Error().Err(err).Msg("failed to apply Opus encoder configuration")
return fmt.Errorf("failed to apply Opus configuration: %w", err)
}
logger.Info().Msg("Opus encoder configuration applied successfully")
return ais.sendAck()
}
// sendAck sends an acknowledgment message // sendAck sends an acknowledgment message
func (ais *AudioInputServer) sendAck() error { func (ais *AudioInputServer) sendAck() error {
ais.mtx.Lock() ais.mtx.Lock()
@ -785,44 +725,6 @@ func (aic *AudioInputClient) SendConfig(config InputIPCConfig) error {
return aic.writeMessage(msg) return aic.writeMessage(msg)
} }
// SendOpusConfig sends a complete Opus encoder configuration update to the audio input server
func (aic *AudioInputClient) SendOpusConfig(config InputIPCOpusConfig) error {
aic.mtx.Lock()
defer aic.mtx.Unlock()
if !aic.running || aic.conn == nil {
return fmt.Errorf("not connected to audio input server")
}
// Validate configuration parameters
if config.SampleRate <= 0 || config.Channels <= 0 || config.FrameSize <= 0 || config.Bitrate <= 0 {
return fmt.Errorf("invalid Opus configuration: SampleRate=%d, Channels=%d, FrameSize=%d, Bitrate=%d",
config.SampleRate, config.Channels, config.FrameSize, config.Bitrate)
}
// Serialize Opus configuration (9 * int32 = 36 bytes)
data := make([]byte, 36)
binary.LittleEndian.PutUint32(data[0:4], uint32(config.SampleRate))
binary.LittleEndian.PutUint32(data[4:8], uint32(config.Channels))
binary.LittleEndian.PutUint32(data[8:12], uint32(config.FrameSize))
binary.LittleEndian.PutUint32(data[12:16], uint32(config.Bitrate))
binary.LittleEndian.PutUint32(data[16:20], uint32(config.Complexity))
binary.LittleEndian.PutUint32(data[20:24], uint32(config.VBR))
binary.LittleEndian.PutUint32(data[24:28], uint32(config.SignalType))
binary.LittleEndian.PutUint32(data[28:32], uint32(config.Bandwidth))
binary.LittleEndian.PutUint32(data[32:36], uint32(config.DTX))
msg := &InputIPCMessage{
Magic: inputMagicNumber,
Type: InputMessageTypeOpusConfig,
Length: uint32(len(data)),
Timestamp: time.Now().UnixNano(),
Data: data,
}
return aic.writeMessage(msg)
}
// SendHeartbeat sends a heartbeat message // SendHeartbeat sends a heartbeat message
func (aic *AudioInputClient) SendHeartbeat() error { func (aic *AudioInputClient) SendHeartbeat() error {
aic.mtx.Lock() aic.mtx.Lock()

View File

@ -1,70 +1,21 @@
//go:build cgo
// +build cgo
package audio package audio
/*
#cgo pkg-config: alsa
#cgo LDFLAGS: -lopus
*/
import "C"
import ( import (
"context" "context"
"os" "os"
"os/signal" "os/signal"
"strconv"
"syscall" "syscall"
"time" "time"
"github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/logging"
) )
// getEnvInt reads an integer from environment variable with a default value
func getEnvIntInput(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
// parseOpusConfigInput reads OPUS configuration from environment variables
// with fallback to default config values for input server
func parseOpusConfigInput() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
// Read configuration from environment variables with config defaults
bitrate = getEnvIntInput("JETKVM_OPUS_BITRATE", GetConfig().CGOOpusBitrate)
complexity = getEnvIntInput("JETKVM_OPUS_COMPLEXITY", GetConfig().CGOOpusComplexity)
vbr = getEnvIntInput("JETKVM_OPUS_VBR", GetConfig().CGOOpusVBR)
signalType = getEnvIntInput("JETKVM_OPUS_SIGNAL_TYPE", GetConfig().CGOOpusSignalType)
bandwidth = getEnvIntInput("JETKVM_OPUS_BANDWIDTH", GetConfig().CGOOpusBandwidth)
dtx = getEnvIntInput("JETKVM_OPUS_DTX", GetConfig().CGOOpusDTX)
return bitrate, complexity, vbr, signalType, bandwidth, dtx
}
// applyOpusConfigInput applies OPUS configuration to the global config for input server
func applyOpusConfigInput(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
config := GetConfig()
config.CGOOpusBitrate = bitrate
config.CGOOpusComplexity = complexity
config.CGOOpusVBR = vbr
config.CGOOpusSignalType = signalType
config.CGOOpusBandwidth = bandwidth
config.CGOOpusDTX = dtx
}
// RunAudioInputServer runs the audio input server subprocess // RunAudioInputServer runs the audio input server subprocess
// This should be called from main() when the subprocess is detected // This should be called from main() when the subprocess is detected
func RunAudioInputServer() error { func RunAudioInputServer() error {
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger() logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
logger.Debug().Msg("audio input server subprocess starting") logger.Debug().Msg("audio input server subprocess starting")
// Parse OPUS configuration from environment variables
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfigInput()
applyOpusConfigInput(bitrate, complexity, vbr, signalType, bandwidth, dtx)
// Initialize validation cache for optimal performance // Initialize validation cache for optimal performance
InitValidationCache() InitValidationCache()
@ -72,16 +23,13 @@ func RunAudioInputServer() error {
StartAdaptiveBuffering() StartAdaptiveBuffering()
defer StopAdaptiveBuffering() defer StopAdaptiveBuffering()
// Initialize CGO audio playback (optional for input server) // Initialize CGO audio system
// This is used for audio loopback/monitoring features
err := CGOAudioPlaybackInit() err := CGOAudioPlaybackInit()
if err != nil { if err != nil {
logger.Warn().Err(err).Msg("failed to initialize CGO audio playback - audio monitoring disabled") logger.Error().Err(err).Msg("failed to initialize CGO audio playback")
// Continue without playback - input functionality doesn't require it return err
} else {
defer CGOAudioPlaybackClose()
logger.Debug().Msg("CGO audio playback initialized successfully")
} }
defer CGOAudioPlaybackClose()
// Create and start the IPC server // Create and start the IPC server
server, err := NewAudioInputServer() server, err := NewAudioInputServer()

View File

@ -1,15 +1,9 @@
//go:build cgo
// +build cgo
package audio package audio
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strconv"
"strings"
"syscall" "syscall"
"time" "time"
) )
@ -18,9 +12,6 @@ import (
type AudioInputSupervisor struct { type AudioInputSupervisor struct {
*BaseSupervisor *BaseSupervisor
client *AudioInputClient client *AudioInputClient
// Environment variables for OPUS configuration
opusEnv []string
} }
// NewAudioInputSupervisor creates a new audio input supervisor // NewAudioInputSupervisor creates a new audio input supervisor
@ -31,44 +22,14 @@ func NewAudioInputSupervisor() *AudioInputSupervisor {
} }
} }
// SetOpusConfig sets OPUS configuration parameters as environment variables
// for the audio input subprocess
func (ais *AudioInputSupervisor) SetOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
ais.mutex.Lock()
defer ais.mutex.Unlock()
// Store OPUS parameters as environment variables
ais.opusEnv = []string{
"JETKVM_OPUS_BITRATE=" + strconv.Itoa(bitrate),
"JETKVM_OPUS_COMPLEXITY=" + strconv.Itoa(complexity),
"JETKVM_OPUS_VBR=" + strconv.Itoa(vbr),
"JETKVM_OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType),
"JETKVM_OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth),
"JETKVM_OPUS_DTX=" + strconv.Itoa(dtx),
}
}
// Start starts the audio input server subprocess // Start starts the audio input server subprocess
func (ais *AudioInputSupervisor) Start() error { func (ais *AudioInputSupervisor) Start() error {
ais.mutex.Lock() ais.mutex.Lock()
defer ais.mutex.Unlock() defer ais.mutex.Unlock()
if ais.IsRunning() { if ais.IsRunning() {
if ais.cmd != nil && ais.cmd.Process != nil {
return fmt.Errorf("audio input supervisor already running with PID %d", ais.cmd.Process.Pid) return fmt.Errorf("audio input supervisor already running with PID %d", ais.cmd.Process.Pid)
} }
return fmt.Errorf("audio input supervisor already running")
}
// Check for existing audio input server process
if existingPID, err := ais.findExistingAudioInputProcess(); err == nil {
ais.logger.Info().Int("existing_pid", existingPID).Msg("Found existing audio input server process, connecting to it")
// Try to connect to the existing process
ais.setRunning(true)
go ais.connectClient()
return nil
}
// Create context for subprocess management // Create context for subprocess management
ais.createContext() ais.createContext()
@ -79,16 +40,11 @@ func (ais *AudioInputSupervisor) Start() error {
return fmt.Errorf("failed to get executable path: %w", err) return fmt.Errorf("failed to get executable path: %w", err)
} }
// Build command arguments (only subprocess flag)
args := []string{"--audio-input-server"}
// Create command for audio input server subprocess // Create command for audio input server subprocess
cmd := exec.CommandContext(ais.ctx, execPath, args...) cmd := exec.CommandContext(ais.ctx, execPath, "--audio-input-server")
cmd.Env = append(os.Environ(),
// Set environment variables for IPC and OPUS configuration "JETKVM_AUDIO_INPUT_IPC=true", // Enable IPC mode
env := append(os.Environ(), "JETKVM_AUDIO_INPUT_IPC=true") // Enable IPC mode )
env = append(env, ais.opusEnv...) // Add OPUS configuration
cmd.Env = env
// Set process group to allow clean termination // Set process group to allow clean termination
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
@ -106,7 +62,7 @@ func (ais *AudioInputSupervisor) Start() error {
return fmt.Errorf("failed to start audio input server process: %w", err) return fmt.Errorf("failed to start audio input server process: %w", err)
} }
ais.logger.Info().Int("pid", cmd.Process.Pid).Strs("args", args).Strs("opus_env", ais.opusEnv).Msg("Audio input server subprocess started") ais.logger.Info().Int("pid", cmd.Process.Pid).Msg("Audio input server subprocess started")
// Add process to monitoring // Add process to monitoring
ais.processMonitor.AddProcess(cmd.Process.Pid, "audio-input-server") ais.processMonitor.AddProcess(cmd.Process.Pid, "audio-input-server")
@ -141,8 +97,7 @@ func (ais *AudioInputSupervisor) Stop() {
// Try graceful termination first // Try graceful termination first
if ais.cmd != nil && ais.cmd.Process != nil { if ais.cmd != nil && ais.cmd.Process != nil {
pid := ais.cmd.Process.Pid ais.logger.Info().Int("pid", ais.cmd.Process.Pid).Msg("Stopping audio input server subprocess")
ais.logger.Info().Int("pid", pid).Msg("Stopping audio input server subprocess")
// Send SIGTERM // Send SIGTERM
err := ais.cmd.Process.Signal(syscall.SIGTERM) err := ais.cmd.Process.Signal(syscall.SIGTERM)
@ -152,49 +107,19 @@ func (ais *AudioInputSupervisor) Stop() {
// Wait for graceful shutdown with timeout // Wait for graceful shutdown with timeout
done := make(chan error, 1) done := make(chan error, 1)
var waitErr error
go func() { go func() {
waitErr = ais.cmd.Wait() done <- ais.cmd.Wait()
done <- waitErr
}() }()
select { select {
case <-done: case <-done:
if waitErr != nil {
ais.logger.Info().Err(waitErr).Msg("Audio input server subprocess stopped with error")
} else {
ais.logger.Info().Msg("Audio input server subprocess stopped gracefully") ais.logger.Info().Msg("Audio input server subprocess stopped gracefully")
}
case <-time.After(GetConfig().InputSupervisorTimeout): case <-time.After(GetConfig().InputSupervisorTimeout):
// Force kill if graceful shutdown failed // Force kill if graceful shutdown failed
ais.logger.Warn().Msg("Audio input server subprocess did not stop gracefully, force killing") ais.logger.Warn().Msg("Audio input server subprocess did not stop gracefully, force killing")
// Use a more robust approach to check if process is still alive err := ais.cmd.Process.Kill()
if ais.cmd != nil && ais.cmd.Process != nil { if err != nil {
// Try to send signal 0 to check if process exists ais.logger.Error().Err(err).Msg("Failed to kill audio input server subprocess")
if err := ais.cmd.Process.Signal(syscall.Signal(0)); err == nil {
// Process is still alive, force kill it
if killErr := ais.cmd.Process.Kill(); killErr != nil {
// Only log error if it's not "process already finished"
if !strings.Contains(killErr.Error(), "process already finished") {
ais.logger.Error().Err(killErr).Msg("Failed to kill audio input server subprocess")
} else {
ais.logger.Debug().Msg("Audio input server subprocess already finished during kill attempt")
}
} else {
ais.logger.Info().Msg("Audio input server subprocess force killed successfully")
}
} else {
ais.logger.Debug().Msg("Audio input server subprocess already finished")
}
// Wait a bit for the kill to take effect and collect the exit status
go func() {
select {
case <-done:
// Process finished
case <-time.After(1 * time.Second):
// Give up waiting
}
}()
} }
} }
} }
@ -253,7 +178,7 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
ais.client.Disconnect() ais.client.Disconnect()
} }
// Mark as not running first to prevent race conditions // Mark as not running
ais.setRunning(false) ais.setRunning(false)
ais.cmd = nil ais.cmd = nil
@ -313,72 +238,3 @@ func (ais *AudioInputSupervisor) SendConfig(config InputIPCConfig) error {
return ais.client.SendConfig(config) return ais.client.SendConfig(config)
} }
// SendOpusConfig sends a complete Opus encoder configuration to the audio input server
func (ais *AudioInputSupervisor) SendOpusConfig(config InputIPCOpusConfig) error {
if ais.client == nil {
return fmt.Errorf("client not initialized")
}
if !ais.client.IsConnected() {
return fmt.Errorf("client not connected")
}
return ais.client.SendOpusConfig(config)
}
// findExistingAudioInputProcess checks if there's already an audio input server process running
func (ais *AudioInputSupervisor) findExistingAudioInputProcess() (int, error) {
// Get current executable path
execPath, err := os.Executable()
if err != nil {
return 0, fmt.Errorf("failed to get executable path: %w", err)
}
execName := filepath.Base(execPath)
// Use ps to find processes with our executable name and audio-input-server argument
cmd := exec.Command("ps", "aux")
output, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("failed to run ps command: %w", err)
}
// Parse ps output to find audio input server processes
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, execName) && strings.Contains(line, "--audio-input-server") {
// Extract PID from ps output (second column)
fields := strings.Fields(line)
if len(fields) >= 2 {
if pid, err := strconv.Atoi(fields[1]); err == nil {
// Verify the process is still running and accessible
if ais.isProcessRunning(pid) {
return pid, nil
}
}
}
}
}
return 0, fmt.Errorf("no existing audio input server process found")
}
// isProcessRunning checks if a process with the given PID is still running
func (ais *AudioInputSupervisor) isProcessRunning(pid int) bool {
// Try to send signal 0 to check if process exists
process, err := os.FindProcess(pid)
if err != nil {
return false
}
err = process.Signal(syscall.Signal(0))
return err == nil
}
// HasExistingProcess checks if there's already an audio input server process running
// This is a public wrapper around findExistingAudioInputProcess for external access
func (ais *AudioInputSupervisor) HasExistingProcess() (int, bool) {
pid, err := ais.findExistingAudioInputProcess()
return pid, err == nil
}

View File

@ -4,69 +4,18 @@ import (
"context" "context"
"os" "os"
"os/signal" "os/signal"
"strconv"
"syscall" "syscall"
"time" "time"
"github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/logging"
) )
// getEnvInt reads an integer from environment variable with a default value
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
// parseOpusConfig reads OPUS configuration from environment variables
// with fallback to default config values
func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
// Read configuration from environment variables with config defaults
bitrate = getEnvInt("JETKVM_OPUS_BITRATE", GetConfig().CGOOpusBitrate)
complexity = getEnvInt("JETKVM_OPUS_COMPLEXITY", GetConfig().CGOOpusComplexity)
vbr = getEnvInt("JETKVM_OPUS_VBR", GetConfig().CGOOpusVBR)
signalType = getEnvInt("JETKVM_OPUS_SIGNAL_TYPE", GetConfig().CGOOpusSignalType)
bandwidth = getEnvInt("JETKVM_OPUS_BANDWIDTH", GetConfig().CGOOpusBandwidth)
dtx = getEnvInt("JETKVM_OPUS_DTX", GetConfig().CGOOpusDTX)
return bitrate, complexity, vbr, signalType, bandwidth, dtx
}
// applyOpusConfig applies OPUS configuration to the global config
func applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
config := GetConfig()
config.CGOOpusBitrate = bitrate
config.CGOOpusComplexity = complexity
config.CGOOpusVBR = vbr
config.CGOOpusSignalType = signalType
config.CGOOpusBandwidth = bandwidth
config.CGOOpusDTX = dtx
logger.Info().
Int("bitrate", bitrate).
Int("complexity", complexity).
Int("vbr", vbr).
Int("signal_type", signalType).
Int("bandwidth", bandwidth).
Int("dtx", dtx).
Msg("applied OPUS configuration")
}
// RunAudioOutputServer runs the audio output server subprocess // RunAudioOutputServer runs the audio output server subprocess
// This should be called from main() when the subprocess is detected // This should be called from main() when the subprocess is detected
func RunAudioOutputServer() error { func RunAudioOutputServer() error {
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger() logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
logger.Debug().Msg("audio output server subprocess starting") logger.Debug().Msg("audio output server subprocess starting")
// Parse OPUS configuration from environment variables
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfig()
applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx)
// Initialize validation cache for optimal performance // Initialize validation cache for optimal performance
InitValidationCache() InitValidationCache()

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"runtime" "runtime"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -312,18 +311,10 @@ func StartAudioOutputStreaming(send func([]byte)) error {
return ErrAudioAlreadyRunning return ErrAudioAlreadyRunning
} }
// Initialize CGO audio capture with retry logic // Initialize CGO audio capture
var initErr error if err := CGOAudioInit(); err != nil {
for attempt := 0; attempt < 3; attempt++ {
if initErr = CGOAudioInit(); initErr == nil {
break
}
getOutputStreamingLogger().Warn().Err(initErr).Int("attempt", attempt+1).Msg("Audio initialization failed, retrying")
time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)
}
if initErr != nil {
atomic.StoreInt32(&outputStreamingRunning, 0) atomic.StoreInt32(&outputStreamingRunning, 0)
return fmt.Errorf("failed to initialize audio after 3 attempts: %w", initErr) return err
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -350,7 +341,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
case <-ctx.Done(): case <-ctx.Done():
return return
default: default:
// Capture audio frame with enhanced error handling and initialization checking // Capture audio frame with enhanced error handling
n, err := CGOAudioReadEncode(buffer) n, err := CGOAudioReadEncode(buffer)
if err != nil { if err != nil {
consecutiveErrors++ consecutiveErrors++
@ -359,13 +350,6 @@ func StartAudioOutputStreaming(send func([]byte)) error {
Int("consecutive_errors", consecutiveErrors). Int("consecutive_errors", consecutiveErrors).
Msg("Failed to read/encode audio") Msg("Failed to read/encode audio")
// Check if this is an initialization error (C error code -1)
if strings.Contains(err.Error(), "C error code -1") {
getOutputStreamingLogger().Error().Msg("Audio system not initialized properly, forcing reinitialization")
// Force immediate reinitialization for init errors
consecutiveErrors = maxConsecutiveErrors
}
// Implement progressive backoff for consecutive errors // Implement progressive backoff for consecutive errors
if consecutiveErrors >= maxConsecutiveErrors { if consecutiveErrors >= maxConsecutiveErrors {
getOutputStreamingLogger().Error(). getOutputStreamingLogger().Error().

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"strconv"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time" "time"
@ -43,9 +42,6 @@ type AudioOutputSupervisor struct {
stopChanClosed bool // Track if stopChan is closed stopChanClosed bool // Track if stopChan is closed
processDoneClosed bool // Track if processDone is closed processDoneClosed bool // Track if processDone is closed
// Environment variables for OPUS configuration
opusEnv []string
// Callbacks // Callbacks
onProcessStart func(pid int) onProcessStart func(pid int)
onProcessExit func(pid int, exitCode int, crashed bool) onProcessExit func(pid int, exitCode int, crashed bool)
@ -76,23 +72,6 @@ func (s *AudioOutputSupervisor) SetCallbacks(
s.onRestart = onRestart s.onRestart = onRestart
} }
// SetOpusConfig sets OPUS configuration parameters as environment variables
// for the audio output subprocess
func (s *AudioOutputSupervisor) SetOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
s.mutex.Lock()
defer s.mutex.Unlock()
// Store OPUS parameters as environment variables
s.opusEnv = []string{
"JETKVM_OPUS_BITRATE=" + strconv.Itoa(bitrate),
"JETKVM_OPUS_COMPLEXITY=" + strconv.Itoa(complexity),
"JETKVM_OPUS_VBR=" + strconv.Itoa(vbr),
"JETKVM_OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType),
"JETKVM_OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth),
"JETKVM_OPUS_DTX=" + strconv.Itoa(dtx),
}
}
// Start begins supervising the audio output server process // Start begins supervising the audio output server process
func (s *AudioOutputSupervisor) Start() error { func (s *AudioOutputSupervisor) Start() error {
if !atomic.CompareAndSwapInt32(&s.running, 0, 1) { if !atomic.CompareAndSwapInt32(&s.running, 0, 1) {
@ -244,24 +223,18 @@ func (s *AudioOutputSupervisor) startProcess() error {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
// Build command arguments (only subprocess flag)
args := []string{"--audio-output-server"}
// Create new command // Create new command
s.cmd = exec.CommandContext(s.ctx, execPath, args...) s.cmd = exec.CommandContext(s.ctx, execPath, "--audio-output-server")
s.cmd.Stdout = os.Stdout s.cmd.Stdout = os.Stdout
s.cmd.Stderr = os.Stderr s.cmd.Stderr = os.Stderr
// Set environment variables for OPUS configuration
s.cmd.Env = append(os.Environ(), s.opusEnv...)
// Start the process // Start the process
if err := s.cmd.Start(); err != nil { if err := s.cmd.Start(); err != nil {
return fmt.Errorf("failed to start audio output server process: %w", err) return fmt.Errorf("failed to start audio output server process: %w", err)
} }
s.processPID = s.cmd.Process.Pid s.processPID = s.cmd.Process.Pid
s.logger.Info().Int("pid", s.processPID).Strs("args", args).Strs("opus_env", s.opusEnv).Msg("audio server process started") s.logger.Info().Int("pid", s.processPID).Msg("audio server process started")
// Add process to monitoring // Add process to monitoring
s.processMonitor.AddProcess(s.processPID, "audio-output-server") s.processMonitor.AddProcess(s.processPID, "audio-output-server")

19
main.go
View File

@ -44,25 +44,6 @@ func startAudioSubprocess() error {
// Set the global supervisor for access from audio package // Set the global supervisor for access from audio package
audio.SetAudioOutputSupervisor(audioSupervisor) audio.SetAudioOutputSupervisor(audioSupervisor)
// 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)
// Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106)
config := audio.GetConfig()
audioInputSupervisor.SetOpusConfig(
config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
config.AudioQualityLowOpusComplexity,
config.AudioQualityLowOpusVBR,
config.AudioQualityLowOpusSignalType,
config.AudioQualityLowOpusBandwidth,
config.AudioQualityLowOpusDTX,
)
// Note: Audio input supervisor is NOT started here - it will be started on-demand
// when the user activates microphone input through the UI
// Set up callbacks for process lifecycle events // Set up callbacks for process lifecycle events
audioSupervisor.SetCallbacks( audioSupervisor.SetCallbacks(
// onProcessStart // onProcessStart