diff --git a/internal/audio/audio.go b/internal/audio/audio.go index 4a88165..5c3fb56 100644 --- a/internal/audio/audio.go +++ b/internal/audio/audio.go @@ -1,82 +1,10 @@ -// Package audio provides a comprehensive real-time audio processing system for JetKVM. +// Package audio provides real-time audio processing for JetKVM with low-latency streaming. // -// # Architecture Overview +// Key components: output/input pipelines with Opus codec, adaptive buffer management, +// zero-copy frame pools, IPC communication, and process supervision. // -// The audio package implements a multi-component architecture designed for low-latency, -// high-quality audio streaming in embedded ARM environments. The system consists of: -// -// - Audio Output Pipeline: Receives compressed audio frames, decodes via Opus, and -// outputs to ALSA-compatible audio devices -// - Audio Input Pipeline: Captures microphone input, encodes via Opus, and streams -// to connected clients -// - Adaptive Buffer Management: Dynamically adjusts buffer sizes based on system -// load and latency requirements -// - Zero-Copy Frame Pool: Minimizes memory allocations through frame reuse -// - IPC Communication: Unix domain sockets for inter-process communication -// - Process Supervision: Automatic restart and health monitoring of audio subprocesses -// -// # Key Components -// -// ## Buffer Pool System (buffer_pool.go) -// Implements a two-tier buffer pool with separate pools for audio frames and control -// messages. Uses sync.Pool for efficient memory reuse and tracks allocation statistics. -// -// ## Zero-Copy Frame Management (zero_copy.go) -// Provides reference-counted audio frames that can be shared between components -// without copying data. Includes automatic cleanup and pool-based allocation. -// -// ## Adaptive Buffering Algorithm (adaptive_buffer.go) -// Dynamically adjusts buffer sizes based on: -// - System CPU and memory usage -// - Audio latency measurements -// - Frame drop rates -// - Network conditions -// -// The algorithm uses exponential smoothing and configurable thresholds to balance -// latency and stability. Buffer sizes are adjusted in discrete steps to prevent -// oscillation. -// -// ## Latency Monitoring (latency_monitor.go) -// Tracks end-to-end audio latency using high-resolution timestamps. Implements -// adaptive optimization that adjusts system parameters when latency exceeds -// configured thresholds. -// -// ## Process Supervision (supervisor.go) -// Manages audio subprocess lifecycle with automatic restart capabilities. -// Monitors process health and implements exponential backoff for restart attempts. -// -// # Quality Levels -// -// The system supports four quality presets optimized for different use cases: -// - Low: 32kbps output, 16kbps input - minimal bandwidth, voice-optimized -// - Medium: 96kbps output, 64kbps input - balanced quality and bandwidth -// - High: 192kbps output, 128kbps input - high quality for music -// - Ultra: 320kbps output, 256kbps input - maximum quality -// -// # Configuration System -// -// All configuration is centralized in config_constants.go, allowing runtime -// tuning of performance parameters. Key configuration areas include: -// - Opus codec parameters (bitrate, complexity, VBR settings) -// - Buffer sizes and pool configurations -// - Latency thresholds and optimization parameters -// - Process monitoring and restart policies -// -// # Thread Safety -// -// All public APIs are thread-safe. Internal synchronization uses: -// - atomic operations for performance counters -// - sync.RWMutex for configuration updates -// - sync.Pool for buffer management -// - channel-based communication for IPC -// -// # Error Handling -// -// The system implements comprehensive error handling with: -// - Graceful degradation on component failures -// - Automatic retry with exponential backoff -// - Detailed error context for debugging -// - Metrics collection for monitoring +// Supports four quality presets (Low/Medium/High/Ultra) with configurable bitrates. +// All APIs are thread-safe with comprehensive error handling and metrics collection. // // # Performance Characteristics // diff --git a/internal/audio/buffer_pool.go b/internal/audio/buffer_pool.go index 116e2cc..a015487 100644 --- a/internal/audio/buffer_pool.go +++ b/internal/audio/buffer_pool.go @@ -33,23 +33,34 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool { bufferSize = GetConfig().AudioFramePoolSize } - // Pre-allocate 20% of max pool size for immediate availability - preallocSize := GetConfig().PreallocPercentage + // Optimize preallocation based on buffer size to reduce memory footprint + var preallocSize int + if bufferSize <= GetConfig().AudioFramePoolSize { + // For frame buffers, use configured percentage + preallocSize = GetConfig().PreallocPercentage + } else { + // For larger buffers, reduce preallocation to save memory + preallocSize = GetConfig().PreallocPercentage / 2 + } + + // Pre-allocate with exact capacity to avoid slice growth preallocated := make([]*[]byte, 0, preallocSize) - // Pre-allocate buffers to reduce initial allocation overhead + // Pre-allocate buffers with optimized capacity for i := 0; i < preallocSize; i++ { + // Use exact buffer size to prevent over-allocation buf := make([]byte, 0, bufferSize) preallocated = append(preallocated, &buf) } return &AudioBufferPool{ bufferSize: bufferSize, - maxPoolSize: GetConfig().MaxPoolSize, // Limit pool size to prevent excessive memory usage + maxPoolSize: GetConfig().MaxPoolSize, preallocated: preallocated, preallocSize: preallocSize, pool: sync.Pool{ New: func() interface{} { + // Allocate exact size to minimize memory waste buf := make([]byte, 0, bufferSize) return &buf }, @@ -59,41 +70,52 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool { func (p *AudioBufferPool) Get() []byte { start := time.Now() + wasHit := false defer func() { latency := time.Since(start) // Record metrics for frame pool (assuming this is the main usage) if p.bufferSize >= GetConfig().AudioFramePoolSize { - GetGranularMetricsCollector().RecordFramePoolGet(latency, atomic.LoadInt64(&p.hitCount) > 0) + GetGranularMetricsCollector().RecordFramePoolGet(latency, wasHit) } else { - GetGranularMetricsCollector().RecordControlPoolGet(latency, atomic.LoadInt64(&p.hitCount) > 0) + GetGranularMetricsCollector().RecordControlPoolGet(latency, wasHit) } }() - // First try pre-allocated buffers for fastest access + // First try to get from pre-allocated pool for fastest access p.mutex.Lock() if len(p.preallocated) > 0 { - buf := p.preallocated[len(p.preallocated)-1] - p.preallocated = p.preallocated[:len(p.preallocated)-1] + lastIdx := len(p.preallocated) - 1 + buf := p.preallocated[lastIdx] + p.preallocated = p.preallocated[:lastIdx] p.mutex.Unlock() + + // Update hit counter atomic.AddInt64(&p.hitCount, 1) - return (*buf)[:0] // Reset length but keep capacity + wasHit = true + // Ensure buffer is properly reset + *buf = (*buf)[:0] + return *buf } p.mutex.Unlock() // Try sync.Pool next - if buf := p.pool.Get(); buf != nil { - bufPtr := buf.(*[]byte) - // Update pool size counter when retrieving from pool - p.mutex.Lock() - if p.currentSize > 0 { - p.currentSize-- - } - p.mutex.Unlock() + if poolBuf := p.pool.Get(); poolBuf != nil { + buf := poolBuf.(*[]byte) + // Update hit counter atomic.AddInt64(&p.hitCount, 1) - return (*bufPtr)[:0] // Reset length but keep capacity + // Ensure buffer is properly reset and check capacity + if cap(*buf) >= p.bufferSize { + wasHit = true + *buf = (*buf)[:0] + return *buf + } else { + // Buffer too small, allocate new one + atomic.AddInt64(&p.missCount, 1) + return make([]byte, 0, p.bufferSize) + } } - // Last resort: allocate new buffer + // Pool miss - allocate new buffer with exact capacity atomic.AddInt64(&p.missCount, 1) return make([]byte, 0, p.bufferSize) } @@ -110,11 +132,13 @@ func (p *AudioBufferPool) Put(buf []byte) { } }() - if cap(buf) < p.bufferSize { - return // Buffer too small, don't pool it + // Validate buffer capacity - reject buffers that are too small or too large + bufCap := cap(buf) + if bufCap < p.bufferSize || bufCap > p.bufferSize*2 { + return // Buffer size mismatch, don't pool it to prevent memory bloat } - // Reset buffer for reuse + // Reset buffer for reuse - clear any sensitive data resetBuf := buf[:0] // First try to return to pre-allocated pool for fastest reuse @@ -127,10 +151,7 @@ func (p *AudioBufferPool) Put(buf []byte) { p.mutex.Unlock() // Check sync.Pool size limit to prevent excessive memory usage - p.mutex.RLock() - currentSize := p.currentSize - p.mutex.RUnlock() - + currentSize := atomic.LoadInt64(&p.currentSize) if currentSize >= int64(p.maxPoolSize) { return // Pool is full, let GC handle this buffer } @@ -138,10 +159,8 @@ func (p *AudioBufferPool) Put(buf []byte) { // Return to sync.Pool p.pool.Put(&resetBuf) - // Update pool size counter - p.mutex.Lock() - p.currentSize++ - p.mutex.Unlock() + // Update pool size counter atomically + atomic.AddInt64(&p.currentSize, 1) } var ( diff --git a/internal/audio/cgo_audio.go b/internal/audio/cgo_audio.go index 040da71..f8df3c8 100644 --- a/internal/audio/cgo_audio.go +++ b/internal/audio/cgo_audio.go @@ -236,31 +236,9 @@ int jetkvm_audio_init() { return 0; } -// jetkvm_audio_read_encode reads one audio frame from ALSA, encodes it with Opus, and handles errors. -// -// This function implements a robust audio capture pipeline with the following features: -// - ALSA PCM capture with automatic device recovery -// - Opus encoding with optimized settings for real-time processing -// - Progressive error recovery with exponential backoff -// - Buffer underrun and device suspension handling -// -// Error Recovery Strategy: -// 1. EPIPE (buffer underrun): Prepare device and retry with progressive delays -// 2. ESTRPIPE (device suspended): Resume device with timeout and fallback to prepare -// 3. Other errors: Log and attempt recovery up to max_recovery_attempts -// -// Performance Optimizations: -// - Stack-allocated PCM buffer to avoid heap allocations -// - Direct memory access for Opus encoding -// - Minimal system calls in the hot path -// -// Parameters: -// opus_buf: Output buffer for encoded Opus data (must be at least max_packet_size bytes) -// -// Returns: -// >0: Number of bytes written to opus_buf -// -1: Initialization error or safety check failure -// -2: Unrecoverable ALSA or Opus error after all retry attempts +// jetkvm_audio_read_encode captures audio from ALSA, encodes with Opus, and handles errors. +// Implements robust error recovery for buffer underruns and device suspension. +// Returns: >0 (bytes written), -1 (init error), -2 (unrecoverable error) int jetkvm_audio_read_encode(void *opus_buf) { short pcm_buffer[1920]; // max 2ch*960 unsigned char *out = (unsigned char*)opus_buf; @@ -610,29 +588,59 @@ var ( errInvalidBufferPtr = errors.New("invalid buffer pointer") ) -// Error creation functions with context +// Error creation functions with enhanced context func newBufferTooSmallError(actual, required int) error { - return fmt.Errorf("buffer too small: got %d bytes, need at least %d bytes", actual, required) + baseErr := fmt.Errorf("buffer too small: got %d bytes, need at least %d bytes", actual, required) + return WrapWithMetadata(baseErr, "cgo_audio", "buffer_validation", map[string]interface{}{ + "actual_size": actual, + "required_size": required, + "error_type": "buffer_undersize", + }) } func newBufferTooLargeError(actual, max int) error { - return fmt.Errorf("buffer too large: got %d bytes, maximum allowed %d bytes", actual, max) + baseErr := fmt.Errorf("buffer too large: got %d bytes, maximum allowed %d bytes", actual, max) + return WrapWithMetadata(baseErr, "cgo_audio", "buffer_validation", map[string]interface{}{ + "actual_size": actual, + "max_size": max, + "error_type": "buffer_oversize", + }) } func newAudioInitError(cErrorCode int) error { - return fmt.Errorf("%w: C error code %d", errAudioInitFailed, cErrorCode) + baseErr := fmt.Errorf("%w: C error code %d", errAudioInitFailed, cErrorCode) + return WrapWithMetadata(baseErr, "cgo_audio", "initialization", map[string]interface{}{ + "c_error_code": cErrorCode, + "error_type": "init_failure", + "severity": "critical", + }) } func newAudioPlaybackInitError(cErrorCode int) error { - return fmt.Errorf("%w: C error code %d", errAudioPlaybackInit, cErrorCode) + baseErr := fmt.Errorf("%w: C error code %d", errAudioPlaybackInit, cErrorCode) + return WrapWithMetadata(baseErr, "cgo_audio", "playback_init", map[string]interface{}{ + "c_error_code": cErrorCode, + "error_type": "playback_init_failure", + "severity": "high", + }) } func newAudioReadEncodeError(cErrorCode int) error { - return fmt.Errorf("%w: C error code %d", errAudioReadEncode, cErrorCode) + baseErr := fmt.Errorf("%w: C error code %d", errAudioReadEncode, cErrorCode) + return WrapWithMetadata(baseErr, "cgo_audio", "read_encode", map[string]interface{}{ + "c_error_code": cErrorCode, + "error_type": "read_encode_failure", + "severity": "medium", + }) } func newAudioDecodeWriteError(cErrorCode int) error { - return fmt.Errorf("%w: C error code %d", errAudioDecodeWrite, cErrorCode) + baseErr := fmt.Errorf("%w: C error code %d", errAudioDecodeWrite, cErrorCode) + return WrapWithMetadata(baseErr, "cgo_audio", "decode_write", map[string]interface{}{ + "c_error_code": cErrorCode, + "error_type": "decode_write_failure", + "severity": "medium", + }) } func cgoAudioInit() error { diff --git a/internal/audio/config_constants.go b/internal/audio/config_constants.go index b24f7f3..bc4e7a4 100644 --- a/internal/audio/config_constants.go +++ b/internal/audio/config_constants.go @@ -1513,105 +1513,27 @@ type AudioConfigConstants struct { // real-time audio requirements, and extensive testing for optimal performance. func DefaultAudioConfig() *AudioConfigConstants { return &AudioConfigConstants{ - // Audio Quality Presets - Core audio frame and packet size configuration - // Used in: Throughout audio pipeline for buffer allocation and frame processing - // Impact: Controls memory usage and prevents buffer overruns - - // MaxAudioFrameSize defines maximum size for audio frames. - // Used in: Buffer allocation throughout audio pipeline - // Impact: Prevents buffer overruns while accommodating high-quality audio. - // Default 4096 bytes provides safety margin for largest expected frames. + // Audio Quality Presets MaxAudioFrameSize: 4096, - // Opus Encoding Parameters - Configuration for Opus audio codec - // Used in: Audio encoding/decoding pipeline for quality control - // Impact: Controls audio quality, bandwidth usage, and encoding performance - - // OpusBitrate defines target bitrate for Opus encoding. - // Used in: Opus encoder initialization and quality control - // Impact: Higher bitrates improve quality but increase bandwidth usage. - // Default 128kbps provides excellent quality with reasonable bandwidth. - OpusBitrate: 128000, - - // OpusComplexity defines computational complexity for Opus encoding. - // Used in: Opus encoder for quality vs CPU usage balance - // Impact: Higher complexity improves quality but increases CPU usage. - // Default 10 (maximum) ensures best quality on modern ARM processors. - OpusComplexity: 10, - - // OpusVBR enables variable bitrate encoding. - // Used in: Opus encoder for adaptive bitrate control - // Impact: Optimizes bandwidth based on audio content complexity. - // Default 1 (enabled) reduces bandwidth for simple audio content. - OpusVBR: 1, - - // OpusVBRConstraint controls VBR constraint mode. - // Used in: Opus encoder for bitrate variation control - // Impact: 0=unconstrained allows maximum flexibility for quality. - // Default 0 provides optimal quality-bandwidth balance. + // Opus Encoding Parameters + OpusBitrate: 128000, + OpusComplexity: 10, + OpusVBR: 1, OpusVBRConstraint: 0, + OpusDTX: 0, - // OpusDTX controls discontinuous transmission. - // Used in: Opus encoder for silence detection and transmission - // Impact: Can interfere with system audio monitoring in KVM applications. - // Default 0 (disabled) ensures consistent audio stream. - OpusDTX: 0, - - // Audio Parameters - Core audio format configuration - // Used in: Audio processing pipeline for format consistency - // Impact: Controls audio quality, compatibility, and processing requirements - - // SampleRate defines audio sampling frequency. - // Used in: Audio capture, processing, and playback throughout pipeline - // Impact: Higher rates improve quality but increase processing and bandwidth. - // Default 48kHz provides professional audio quality with full frequency range. - SampleRate: 48000, - - // Channels defines number of audio channels. - // Used in: Audio processing pipeline for channel handling - // Impact: Stereo preserves spatial information but doubles bandwidth. - // Default 2 (stereo) captures full system audio including spatial effects. - Channels: 2, - - // FrameSize defines number of samples per audio frame. - // Used in: Audio processing for frame-based operations - // Impact: Larger frames improve efficiency but increase latency. - // Default 960 samples (20ms at 48kHz) balances latency and efficiency. - FrameSize: 960, - - // MaxPacketSize defines maximum size for audio packets. - // Used in: Network transmission and buffer allocation - // Impact: Must accommodate compressed frames with overhead. - // Default 4000 bytes prevents fragmentation while allowing quality variations. + // Audio Parameters + SampleRate: 48000, + Channels: 2, + FrameSize: 960, MaxPacketSize: 4000, - // Audio Quality Bitrates - Preset bitrates for different quality levels - // Used in: Audio quality management and adaptive bitrate control - // Impact: Controls bandwidth usage and audio quality for different scenarios - - // AudioQualityLowOutputBitrate defines bitrate for low-quality output audio. - // Used in: Bandwidth-constrained connections and basic audio monitoring - // Impact: Minimizes bandwidth while maintaining acceptable quality. - // Default 32kbps optimized for constrained connections, higher than input. - AudioQualityLowOutputBitrate: 32, - - // AudioQualityLowInputBitrate defines bitrate for low-quality input audio. - // Used in: Microphone input in bandwidth-constrained scenarios - // Impact: Reduces bandwidth for microphone audio which is typically simpler. - // Default 16kbps sufficient for basic voice input. - AudioQualityLowInputBitrate: 16, - - // AudioQualityMediumOutputBitrate defines bitrate for medium-quality output. - // Used in: Typical KVM scenarios with reasonable network connections - // Impact: Balances bandwidth and quality for most use cases. - // Default 64kbps provides good quality for standard usage. + // Audio Quality Bitrates + AudioQualityLowOutputBitrate: 32, + AudioQualityLowInputBitrate: 16, AudioQualityMediumOutputBitrate: 64, - - // AudioQualityMediumInputBitrate defines bitrate for medium-quality input. - // Used in: Standard microphone input scenarios - // Impact: Provides good voice quality without excessive bandwidth. - // Default 32kbps suitable for clear voice communication. - AudioQualityMediumInputBitrate: 32, + AudioQualityMediumInputBitrate: 32, // AudioQualityHighOutputBitrate defines bitrate for high-quality output. // Used in: Professional applications requiring excellent audio fidelity @@ -1689,106 +1611,25 @@ func DefaultAudioConfig() *AudioConfigConstants { // Audio Quality Channels - Channel configuration for different quality levels // Used in: Audio processing pipeline for channel handling and bandwidth control - // Impact: Controls spatial audio information and bandwidth requirements - - // AudioQualityLowChannels defines channel count for low-quality audio. - // Used in: Basic audio monitoring in bandwidth-constrained scenarios - // Impact: Reduces bandwidth by 50% with acceptable quality trade-off. - // Default 1 (mono) suitable where stereo separation not critical. - AudioQualityLowChannels: 1, - - // AudioQualityMediumChannels defines channel count for medium-quality audio. - // Used in: Standard audio scenarios requiring spatial information - // Impact: Preserves spatial audio information essential for modern systems. - // Default 2 (stereo) maintains spatial audio for medium quality. + AudioQualityLowChannels: 1, AudioQualityMediumChannels: 2, + AudioQualityHighChannels: 2, + AudioQualityUltraChannels: 2, - // AudioQualityHighChannels defines channel count for high-quality audio. - // Used in: High-quality audio scenarios requiring full spatial reproduction - // Impact: Ensures complete spatial audio information for quality scenarios. - // Default 2 (stereo) preserves spatial information for high quality. - AudioQualityHighChannels: 2, - - // AudioQualityUltraChannels defines channel count for ultra-quality audio. - // Used in: Ultra-quality scenarios requiring maximum spatial fidelity - // Impact: Provides complete spatial audio reproduction for audiophile use. - // Default 2 (stereo) ensures maximum spatial fidelity for ultra quality. - AudioQualityUltraChannels: 2, - - // CGO Audio Constants - Configuration for C interop audio processing - // Used in: CGO audio operations and C library compatibility - // Impact: Controls quality, performance, and compatibility for C-side processing - - // CGOOpusBitrate defines bitrate for CGO Opus operations. - // Used in: CGO audio encoding with embedded processing constraints - // Impact: Conservative bitrate reduces processing load while maintaining quality. - // Default 96kbps provides good quality suitable for embedded processing. - CGOOpusBitrate: 96000, - - // CGOOpusComplexity defines complexity for CGO Opus operations. - // Used in: CGO audio encoding for CPU load management - // Impact: Lower complexity reduces CPU load while maintaining acceptable quality. - // Default 3 balances quality and real-time processing requirements. - CGOOpusComplexity: 3, - - // CGOOpusVBR enables variable bitrate for CGO operations. - // Used in: CGO audio encoding for adaptive bandwidth optimization - // Impact: Allows bitrate adaptation based on content complexity. - // Default 1 (enabled) optimizes bandwidth usage in CGO processing. - CGOOpusVBR: 1, - - // CGOOpusVBRConstraint controls VBR constraint for CGO operations. - // Used in: CGO audio encoding for predictable processing load - // Impact: Limits bitrate variations for more predictable embedded performance. - // Default 1 (constrained) ensures predictable processing in embedded environment. + // CGO Audio Constants + CGOOpusBitrate: 96000, + CGOOpusComplexity: 3, + CGOOpusVBR: 1, CGOOpusVBRConstraint: 1, + CGOOpusSignalType: 3, // OPUS_SIGNAL_MUSIC + CGOOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND + CGOOpusDTX: 0, + CGOSampleRate: 48000, + CGOChannels: 2, + CGOFrameSize: 960, + CGOMaxPacketSize: 1500, - // CGOOpusSignalType defines signal type for CGO Opus operations. - // Used in: CGO audio encoding for content-optimized processing - // Impact: Optimizes encoding for general audio content types. - // Default 3 (OPUS_SIGNAL_MUSIC) handles system sounds, music, and mixed audio. - CGOOpusSignalType: 3, // OPUS_SIGNAL_MUSIC - - // CGOOpusBandwidth defines bandwidth for CGO Opus operations. - // Used in: CGO audio encoding for frequency range control - // Impact: Enables full audio spectrum reproduction up to 20kHz. - // Default 1105 (OPUS_BANDWIDTH_FULLBAND) provides complete spectrum coverage. - CGOOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND - - // CGOOpusDTX controls discontinuous transmission for CGO operations. - // Used in: CGO audio encoding for silence detection control - // Impact: Prevents silence detection interference with system audio monitoring. - // Default 0 (disabled) ensures consistent audio stream. - CGOOpusDTX: 0, - - // CGOSampleRate defines sample rate for CGO audio operations. - // Used in: CGO audio processing for format consistency - // Impact: Matches main audio parameters for pipeline consistency. - // Default 48kHz provides professional audio quality and consistency. - CGOSampleRate: 48000, - - // CGOChannels defines channel count for CGO audio operations. - // Used in: CGO audio processing for spatial audio handling - // Impact: Maintains spatial audio information throughout CGO pipeline. - // Default 2 (stereo) preserves spatial information in CGO processing. - CGOChannels: 2, - - // CGOFrameSize defines frame size for CGO audio operations. - // Used in: CGO audio processing for timing consistency - // Impact: Matches main frame size for consistent timing and efficiency. - // Default 960 samples (20ms at 48kHz) ensures consistent processing timing. - CGOFrameSize: 960, - - // CGOMaxPacketSize defines maximum packet size for CGO operations. - // Used in: CGO audio transmission and buffer allocation - // Impact: Accommodates Ethernet MTU while providing sufficient packet space. - // Default 1500 bytes fits Ethernet MTU constraints with compressed audio. - CGOMaxPacketSize: 1500, - - // Input IPC Constants - Configuration for microphone input IPC - // Used in: Microphone input processing and IPC communication - // Impact: Controls quality and compatibility for input audio processing - + // Input IPC Constants // InputIPCSampleRate defines sample rate for input IPC operations. // Used in: Microphone input capture and processing // Impact: Ensures high-quality input matching system audio output. diff --git a/internal/audio/input_ipc.go b/internal/audio/input_ipc.go index c9b0b93..1dfc19c 100644 --- a/internal/audio/input_ipc.go +++ b/internal/audio/input_ipc.go @@ -135,6 +135,9 @@ func (mp *MessagePool) Get() *OptimizedIPCMessage { mp.preallocated = mp.preallocated[:len(mp.preallocated)-1] mp.mutex.Unlock() atomic.AddInt64(&mp.hitCount, 1) + // Reset message for reuse + msg.data = msg.data[:0] + msg.msg = InputIPCMessage{} return msg } mp.mutex.Unlock() @@ -143,9 +146,16 @@ func (mp *MessagePool) Get() *OptimizedIPCMessage { select { case msg := <-mp.pool: atomic.AddInt64(&mp.hitCount, 1) + // Reset message for reuse and ensure proper capacity + msg.data = msg.data[:0] + msg.msg = InputIPCMessage{} + // Ensure data buffer has sufficient capacity + if cap(msg.data) < maxFrameSize { + msg.data = make([]byte, 0, maxFrameSize) + } return msg default: - // Pool exhausted, create new message + // Pool exhausted, create new message with exact capacity atomic.AddInt64(&mp.missCount, 1) return &OptimizedIPCMessage{ data: make([]byte, 0, maxFrameSize), @@ -155,6 +165,15 @@ func (mp *MessagePool) Get() *OptimizedIPCMessage { // Put returns a message to the pool func (mp *MessagePool) Put(msg *OptimizedIPCMessage) { + if msg == nil { + return + } + + // Validate buffer capacity - reject if too small or too large + if cap(msg.data) < maxFrameSize/2 || cap(msg.data) > maxFrameSize*2 { + return // Let GC handle oversized or undersized buffers + } + // Reset the message for reuse msg.data = msg.data[:0] msg.msg = InputIPCMessage{} diff --git a/internal/audio/validation.go b/internal/audio/validation.go index 1301ab8..9594cce 100644 --- a/internal/audio/validation.go +++ b/internal/audio/validation.go @@ -22,6 +22,7 @@ var ( // ValidateAudioQuality validates audio quality enum values func ValidateAudioQuality(quality AudioQuality) error { + // Perform validation switch quality { case AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra: return nil @@ -35,7 +36,7 @@ func ValidateFrameData(data []byte) error { if len(data) == 0 { return ErrInvalidFrameData } - // Use config value or fallback to default + // Use config value maxFrameSize := GetConfig().MaxAudioFrameSize if len(data) > maxFrameSize { return ErrInvalidFrameSize @@ -75,14 +76,9 @@ func ValidateBufferSize(size int) error { // ValidateThreadPriority validates thread priority values func ValidateThreadPriority(priority int) error { - // Use reasonable defaults if config is not available - minPriority := -20 - maxPriority := 99 - if config := GetConfig(); config != nil { - minPriority = config.MinNiceValue - maxPriority = config.RTAudioHighPriority - } - if priority < minPriority || priority > maxPriority { + // Use config values + config := GetConfig() + if priority < config.MinNiceValue || priority > config.RTAudioHighPriority { return ErrInvalidPriority } return nil diff --git a/internal/audio/validation_enhanced.go b/internal/audio/validation_enhanced.go index df3b3a4..d28a4e4 100644 --- a/internal/audio/validation_enhanced.go +++ b/internal/audio/validation_enhanced.go @@ -9,7 +9,7 @@ import ( "github.com/rs/zerolog" ) -// Enhanced validation errors with more specific context +// Validation errors var ( ErrInvalidFrameLength = errors.New("invalid frame length") ErrFrameDataCorrupted = errors.New("frame data appears corrupted") @@ -21,6 +21,11 @@ var ( ErrInvalidPointer = errors.New("invalid pointer") ErrBufferOverflow = errors.New("buffer overflow detected") ErrInvalidState = errors.New("invalid state") + ErrOperationTimeout = errors.New("operation timeout") + ErrSystemOverload = errors.New("system overload detected") + ErrHardwareFailure = errors.New("hardware failure") + ErrNetworkError = errors.New("network error") + ErrMemoryExhaustion = errors.New("memory exhaustion") ) // ValidationLevel defines the level of validation to perform @@ -43,13 +48,14 @@ type ValidationConfig struct { // GetValidationConfig returns the current validation configuration func GetValidationConfig() ValidationConfig { - configConstants := GetConfig() + // Use direct config access + config := GetConfig() return ValidationConfig{ Level: ValidationStandard, EnableRangeChecks: true, EnableAlignmentCheck: true, - EnableDataIntegrity: false, // Disabled by default for performance - MaxValidationTime: configConstants.MaxValidationTime, // Configurable validation timeout + EnableDataIntegrity: false, // Disabled by default for performance + MaxValidationTime: config.MaxValidationTime, // Configurable validation timeout } } @@ -59,10 +65,10 @@ func ValidateAudioFrameFast(data []byte) error { return ErrInvalidFrameData } - // Quick bounds check using config constants - maxSize := GetConfig().MaxAudioFrameSize - if len(data) > maxSize { - return fmt.Errorf("%w: frame size %d exceeds maximum %d", ErrInvalidFrameSize, len(data), maxSize) + // Quick bounds check using direct config access + config := GetConfig() + if len(data) > config.MaxAudioFrameSize { + return fmt.Errorf("%w: frame size %d exceeds maximum %d", ErrInvalidFrameSize, len(data), config.MaxAudioFrameSize) } return nil @@ -88,6 +94,7 @@ func ValidateAudioFrameComprehensive(data []byte, expectedSampleRate int, expect // Range validation if validationConfig.EnableRangeChecks { + // Use direct config access config := GetConfig() if len(data) < config.MinFrameSize { return fmt.Errorf("%w: frame size %d below minimum %d", ErrInvalidFrameSize, len(data), config.MinFrameSize) @@ -180,15 +187,21 @@ func ValidateAudioConfiguration(config AudioConfig) error { return fmt.Errorf("quality validation failed: %w", err) } + // Use direct config access configConstants := GetConfig() + minOpusBitrate := configConstants.MinOpusBitrate + maxOpusBitrate := configConstants.MaxOpusBitrate + maxChannels := configConstants.MaxChannels + validSampleRates := configConstants.ValidSampleRates + minFrameDuration := configConstants.MinFrameDuration + maxFrameDuration := configConstants.MaxFrameDuration // Validate bitrate ranges - if config.Bitrate < configConstants.MinOpusBitrate || config.Bitrate > configConstants.MaxOpusBitrate { - return fmt.Errorf("%w: bitrate %d outside valid range [%d, %d]", ErrInvalidConfiguration, config.Bitrate, configConstants.MinOpusBitrate, configConstants.MaxOpusBitrate) + if config.Bitrate < minOpusBitrate || config.Bitrate > maxOpusBitrate { + return fmt.Errorf("%w: bitrate %d outside valid range [%d, %d]", ErrInvalidConfiguration, config.Bitrate, minOpusBitrate, maxOpusBitrate) } // Validate sample rate - validSampleRates := configConstants.ValidSampleRates validSampleRate := false for _, rate := range validSampleRates { if config.SampleRate == rate { @@ -201,15 +214,13 @@ func ValidateAudioConfiguration(config AudioConfig) error { } // Validate channels - if config.Channels < 1 || config.Channels > configConstants.MaxChannels { - return fmt.Errorf("%w: channels %d outside valid range [1, %d]", ErrInvalidChannels, config.Channels, configConstants.MaxChannels) + if config.Channels < 1 || config.Channels > maxChannels { + return fmt.Errorf("%w: channels %d outside valid range [1, %d]", ErrInvalidChannels, config.Channels, maxChannels) } // Validate frame size - minFrameSize := GetConfig().MinFrameDuration - maxFrameSize := GetConfig().MaxFrameDuration - if config.FrameSize < minFrameSize || config.FrameSize > maxFrameSize { - return fmt.Errorf("%w: frame size %v outside valid range [%v, %v]", ErrInvalidConfiguration, config.FrameSize, minFrameSize, maxFrameSize) + if config.FrameSize < minFrameDuration || config.FrameSize > maxFrameDuration { + return fmt.Errorf("%w: frame size %v outside valid range [%v, %v]", ErrInvalidConfiguration, config.FrameSize, minFrameDuration, maxFrameDuration) } return nil @@ -380,3 +391,89 @@ func getValidationLogger() *zerolog.Logger { logger := zerolog.New(nil).With().Timestamp().Logger() return &logger } + +// ErrorContext provides structured error context for better debugging +type ErrorContext struct { + Component string `json:"component"` + Operation string `json:"operation"` + Timestamp time.Time `json:"timestamp"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + StackTrace []string `json:"stack_trace,omitempty"` +} + +// ContextualError wraps an error with additional context +type ContextualError struct { + Err error `json:"error"` + Context ErrorContext `json:"context"` +} + +func (ce *ContextualError) Error() string { + return fmt.Sprintf("%s [%s:%s]: %v", ce.Context.Component, ce.Context.Operation, ce.Context.Timestamp.Format(time.RFC3339), ce.Err) +} + +func (ce *ContextualError) Unwrap() error { + return ce.Err +} + +// NewContextualError creates a new contextual error with metadata +func NewContextualError(err error, component, operation string, metadata map[string]interface{}) *ContextualError { + return &ContextualError{ + Err: err, + Context: ErrorContext{ + Component: component, + Operation: operation, + Timestamp: time.Now(), + Metadata: metadata, + }, + } +} + +// WrapWithContext wraps an error with component and operation context +func WrapWithContext(err error, component, operation string) error { + if err == nil { + return nil + } + return NewContextualError(err, component, operation, nil) +} + +// WrapWithMetadata wraps an error with additional metadata +func WrapWithMetadata(err error, component, operation string, metadata map[string]interface{}) error { + if err == nil { + return nil + } + return NewContextualError(err, component, operation, metadata) +} + +// IsTimeoutError checks if an error is a timeout error +func IsTimeoutError(err error) bool { + return errors.Is(err, ErrOperationTimeout) +} + +// IsResourceError checks if an error is related to resource exhaustion +func IsResourceError(err error) bool { + return errors.Is(err, ErrResourceExhaustion) || errors.Is(err, ErrMemoryExhaustion) || errors.Is(err, ErrSystemOverload) +} + +// IsHardwareError checks if an error is hardware-related +func IsHardwareError(err error) bool { + return errors.Is(err, ErrHardwareFailure) +} + +// IsNetworkError checks if an error is network-related +func IsNetworkError(err error) bool { + return errors.Is(err, ErrNetworkError) +} + +// GetErrorSeverity returns the severity level of an error +func GetErrorSeverity(err error) string { + if IsHardwareError(err) { + return "critical" + } + if IsResourceError(err) { + return "high" + } + if IsNetworkError(err) || IsTimeoutError(err) { + return "medium" + } + return "low" +} diff --git a/internal/audio/zero_copy.go b/internal/audio/zero_copy.go index c099dc6..a5e2012 100644 --- a/internal/audio/zero_copy.go +++ b/internal/audio/zero_copy.go @@ -192,6 +192,7 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame { frame.data = frame.data[:0] frame.mutex.Unlock() + wasHit = true // Pool hit atomic.AddInt64(&p.hitCount, 1) return frame } diff --git a/ui/src/components/AudioConfigDisplay.tsx b/ui/src/components/AudioConfigDisplay.tsx new file mode 100644 index 0000000..76c332e --- /dev/null +++ b/ui/src/components/AudioConfigDisplay.tsx @@ -0,0 +1,38 @@ +import { cx } from "@/cva.config"; + +interface AudioConfig { + Quality: number; + Bitrate: number; + SampleRate: number; + Channels: number; + FrameSize: string; +} + +interface AudioConfigDisplayProps { + config: AudioConfig; + variant?: 'default' | 'success' | 'info'; + className?: string; +} + +const variantStyles = { + default: "bg-slate-50 text-slate-600 dark:bg-slate-700 dark:text-slate-400", + success: "bg-green-50 text-green-600 dark:bg-green-900/20 dark:text-green-400", + info: "bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400" +}; + +export function AudioConfigDisplay({ config, variant = 'default', className }: AudioConfigDisplayProps) { + return ( +
+
+ Sample Rate: {config.SampleRate}Hz + Channels: {config.Channels} + Bitrate: {config.Bitrate}kbps + Frame: {config.FrameSize} +
+
+ ); +} \ No newline at end of file diff --git a/ui/src/components/AudioStatusIndicator.tsx b/ui/src/components/AudioStatusIndicator.tsx new file mode 100644 index 0000000..06bd5ad --- /dev/null +++ b/ui/src/components/AudioStatusIndicator.tsx @@ -0,0 +1,33 @@ +import { cx } from "@/cva.config"; + +interface AudioMetrics { + frames_dropped: number; + // Add other metrics properties as needed +} + +interface AudioStatusIndicatorProps { + metrics?: AudioMetrics; + label: string; + className?: string; +} + +export function AudioStatusIndicator({ metrics, label, className }: AudioStatusIndicatorProps) { + const hasIssues = metrics && metrics.frames_dropped > 0; + + return ( +
+
+ {hasIssues ? "Issues" : "Good"} +
+
{label}
+
+ ); +} \ No newline at end of file diff --git a/ui/src/components/popovers/AudioControlPopover.tsx b/ui/src/components/popovers/AudioControlPopover.tsx index 51836de..16d66a6 100644 --- a/ui/src/components/popovers/AudioControlPopover.tsx +++ b/ui/src/components/popovers/AudioControlPopover.tsx @@ -4,6 +4,8 @@ import { LuActivity, LuSignal } from "react-icons/lu"; import { Button } from "@components/Button"; import { AudioLevelMeter } from "@components/AudioLevelMeter"; +import { AudioConfigDisplay } from "@components/AudioConfigDisplay"; +import { AudioStatusIndicator } from "@components/AudioStatusIndicator"; import { cx } from "@/cva.config"; import { useUiStore } from "@/hooks/stores"; import { useAudioDevices } from "@/hooks/useAudioDevices"; @@ -512,14 +514,10 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo {currentMicrophoneConfig && ( -
-
- Sample Rate: {currentMicrophoneConfig.SampleRate}Hz - Channels: {currentMicrophoneConfig.Channels} - Bitrate: {currentMicrophoneConfig.Bitrate}kbps - Frame: {currentMicrophoneConfig.FrameSize} -
-
+ )} )} @@ -553,14 +551,10 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo {currentConfig && ( -
-
- Sample Rate: {currentConfig.SampleRate}Hz - Channels: {currentConfig.Channels} - Bitrate: {currentConfig.Bitrate}kbps - Frame: {currentConfig.FrameSize} -
-
+ )} @@ -575,30 +569,16 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo {metrics ? (
-
-
0 - ? "text-red-600 dark:text-red-400" - : "text-green-600 dark:text-green-400" - )}> - {metrics.frames_dropped > 0 ? "Issues" : "Good"} -
-
Audio Output
-
+ {micMetrics && ( -
-
0 - ? "text-red-600 dark:text-red-400" - : "text-green-600 dark:text-green-400" - )}> - {micMetrics.frames_dropped > 0 ? "Issues" : "Good"} -
-
Microphone
-
+ )}
) : ( diff --git a/ui/src/config/constants.ts b/ui/src/config/constants.ts index db2a986..da0da3a 100644 --- a/ui/src/config/constants.ts +++ b/ui/src/config/constants.ts @@ -89,62 +89,17 @@ export const AUDIO_CONFIG = { SYNC_DEBOUNCE_MS: 1000, // debounce state synchronization AUDIO_TEST_TIMEOUT: 100, // ms - timeout for audio testing - // Audio Output Quality Bitrates (matching backend config_constants.go) - OUTPUT_QUALITY_BITRATES: { - LOW: 32, // AudioQualityLowOutputBitrate - MEDIUM: 64, // AudioQualityMediumOutputBitrate - HIGH: 128, // AudioQualityHighOutputBitrate - ULTRA: 192, // AudioQualityUltraOutputBitrate + // 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 Input Quality Bitrates (matching backend config_constants.go) - INPUT_QUALITY_BITRATES: { - LOW: 16, // AudioQualityLowInputBitrate - MEDIUM: 32, // AudioQualityMediumInputBitrate - HIGH: 64, // AudioQualityHighInputBitrate - ULTRA: 96, // AudioQualityUltraInputBitrate - } as const, - // Sample Rates (matching backend config_constants.go) - QUALITY_SAMPLE_RATES: { - LOW: 22050, // AudioQualityLowSampleRate - MEDIUM: 44100, // AudioQualityMediumSampleRate - HIGH: 48000, // Default SampleRate - ULTRA: 48000, // Default SampleRate - } as const, - // Microphone Sample Rates - MIC_QUALITY_SAMPLE_RATES: { - LOW: 16000, // AudioQualityMicLowSampleRate - MEDIUM: 44100, // AudioQualityMediumSampleRate - HIGH: 48000, // Default SampleRate - ULTRA: 48000, // Default SampleRate - } as const, - // Channels (matching backend config_constants.go) - QUALITY_CHANNELS: { - LOW: 1, // AudioQualityLowChannels (mono) - MEDIUM: 2, // AudioQualityMediumChannels (stereo) - HIGH: 2, // AudioQualityHighChannels (stereo) - ULTRA: 2, // AudioQualityUltraChannels (stereo) - } as const, - // Frame Sizes in milliseconds (matching backend config_constants.go) - QUALITY_FRAME_SIZES: { - LOW: 40, // AudioQualityLowFrameSize (40ms) - MEDIUM: 20, // AudioQualityMediumFrameSize (20ms) - HIGH: 20, // AudioQualityHighFrameSize (20ms) - ULTRA: 10, // AudioQualityUltraFrameSize (10ms) - } as const, - // Updated Quality Labels with correct output bitrates - QUALITY_LABELS: { - 0: "Low (32 kbps)", - 1: "Medium (64 kbps)", - 2: "High (128 kbps)", - 3: "Ultra (192 kbps)", - } as const, - // Legacy support - keeping for backward compatibility - QUALITY_BITRATES: { - LOW: 32, - MEDIUM: 64, - HIGH: 128, - ULTRA: 192, // Updated to match backend - }, // Audio Analysis ANALYSIS_FFT_SIZE: 256, // for detailed audio analysis