package audio import ( "context" "math" "sync" "sync/atomic" "time" "github.com/jetkvm/kvm/internal/logging" "github.com/rs/zerolog" ) // AdaptiveBufferConfig holds configuration for the adaptive buffer sizing algorithm. // // The adaptive buffer system dynamically adjusts audio buffer sizes based on real-time // system conditions to optimize the trade-off between latency and stability. The algorithm // uses multiple factors to make decisions: // // 1. System Load Monitoring: // - CPU usage: High CPU load increases buffer sizes to prevent underruns // - Memory usage: High memory pressure reduces buffer sizes to conserve RAM // // 2. Latency Tracking: // - Target latency: Optimal latency for the current quality setting // - Max latency: Hard limit beyond which buffers are aggressively reduced // // 3. Adaptation Strategy: // - Exponential smoothing: Prevents oscillation and provides stable adjustments // - Discrete steps: Buffer sizes change in fixed increments to avoid instability // - Hysteresis: Different thresholds for increasing vs decreasing buffer sizes // // The algorithm is specifically tuned for embedded ARM systems with limited resources, // prioritizing stability over absolute minimum latency. type AdaptiveBufferConfig struct { // Buffer size limits (in frames) MinBufferSize int MaxBufferSize int DefaultBufferSize int // System load thresholds LowCPUThreshold float64 // Below this, increase buffer size HighCPUThreshold float64 // Above this, decrease buffer size LowMemoryThreshold float64 // Below this, increase buffer size HighMemoryThreshold float64 // Above this, decrease buffer size // Latency thresholds (in milliseconds) TargetLatency time.Duration MaxLatency time.Duration // Adaptation parameters AdaptationInterval time.Duration SmoothingFactor float64 // 0.0-1.0, higher = more responsive } // DefaultAdaptiveBufferConfig returns optimized config for JetKVM hardware func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig { return AdaptiveBufferConfig{ // Conservative buffer sizes for 256MB RAM constraint MinBufferSize: GetConfig().AdaptiveMinBufferSize, MaxBufferSize: GetConfig().AdaptiveMaxBufferSize, DefaultBufferSize: GetConfig().AdaptiveDefaultBufferSize, // CPU thresholds optimized for single-core ARM Cortex A7 under load LowCPUThreshold: GetConfig().LowCPUThreshold * 100, // Below 20% CPU HighCPUThreshold: GetConfig().HighCPUThreshold * 100, // Above 60% CPU (lowered to be more responsive) // Memory thresholds for 256MB total RAM LowMemoryThreshold: GetConfig().LowMemoryThreshold * 100, // Below 35% memory usage HighMemoryThreshold: GetConfig().HighMemoryThreshold * 100, // Above 75% memory usage (lowered for earlier response) // Latency targets TargetLatency: GetConfig().TargetLatency, // Target 20ms latency MaxLatency: GetConfig().MaxLatencyTarget, // Max acceptable latency // Adaptation settings AdaptationInterval: GetConfig().BufferUpdateInterval, // Check every 500ms SmoothingFactor: GetConfig().SmoothingFactor, // Moderate responsiveness } } // AdaptiveBufferManager manages dynamic buffer sizing based on system conditions type AdaptiveBufferManager struct { // Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment) currentInputBufferSize int64 // Current input buffer size (atomic) currentOutputBufferSize int64 // Current output buffer size (atomic) averageLatency int64 // Average latency in nanoseconds (atomic) systemCPUPercent int64 // System CPU percentage * 100 (atomic) systemMemoryPercent int64 // System memory percentage * 100 (atomic) adaptationCount int64 // Metrics tracking (atomic) config AdaptiveBufferConfig logger zerolog.Logger processMonitor *ProcessMonitor // Control channels ctx context.Context cancel context.CancelFunc wg sync.WaitGroup // Metrics tracking lastAdaptation time.Time mutex sync.RWMutex } // NewAdaptiveBufferManager creates a new adaptive buffer manager func NewAdaptiveBufferManager(config AdaptiveBufferConfig) *AdaptiveBufferManager { ctx, cancel := context.WithCancel(context.Background()) return &AdaptiveBufferManager{ currentInputBufferSize: int64(config.DefaultBufferSize), currentOutputBufferSize: int64(config.DefaultBufferSize), config: config, logger: logging.GetDefaultLogger().With().Str("component", "adaptive-buffer").Logger(), processMonitor: GetProcessMonitor(), ctx: ctx, cancel: cancel, lastAdaptation: time.Now(), } } // Start begins the adaptive buffer management func (abm *AdaptiveBufferManager) Start() { abm.wg.Add(1) go abm.adaptationLoop() abm.logger.Info().Msg("Adaptive buffer manager started") } // Stop stops the adaptive buffer management func (abm *AdaptiveBufferManager) Stop() { abm.cancel() abm.wg.Wait() abm.logger.Info().Msg("Adaptive buffer manager stopped") } // GetInputBufferSize returns the current recommended input buffer size func (abm *AdaptiveBufferManager) GetInputBufferSize() int { return int(atomic.LoadInt64(&abm.currentInputBufferSize)) } // GetOutputBufferSize returns the current recommended output buffer size func (abm *AdaptiveBufferManager) GetOutputBufferSize() int { return int(atomic.LoadInt64(&abm.currentOutputBufferSize)) } // UpdateLatency updates the current latency measurement func (abm *AdaptiveBufferManager) UpdateLatency(latency time.Duration) { // Use exponential moving average for latency currentAvg := atomic.LoadInt64(&abm.averageLatency) newLatency := latency.Nanoseconds() if currentAvg == 0 { atomic.StoreInt64(&abm.averageLatency, newLatency) } else { // Exponential moving average: 70% historical, 30% current newAvg := int64(float64(currentAvg)*GetConfig().HistoricalWeight + float64(newLatency)*GetConfig().CurrentWeight) atomic.StoreInt64(&abm.averageLatency, newAvg) } } // adaptationLoop is the main loop that adjusts buffer sizes func (abm *AdaptiveBufferManager) adaptationLoop() { defer abm.wg.Done() ticker := time.NewTicker(abm.config.AdaptationInterval) defer ticker.Stop() for { select { case <-abm.ctx.Done(): return case <-ticker.C: abm.adaptBufferSizes() } } } // adaptBufferSizes analyzes system conditions and adjusts buffer sizes // adaptBufferSizes implements the core adaptive buffer sizing algorithm. // // This function uses a multi-factor approach to determine optimal buffer sizes: // // Mathematical Model: // 1. Factor Calculation: // // - CPU Factor: Sigmoid function that increases buffer size under high CPU load // // - Memory Factor: Inverse relationship that decreases buffer size under memory pressure // // - Latency Factor: Exponential decay that aggressively reduces buffers when latency exceeds targets // // 2. Combined Factor: // Combined = (CPU_factor * Memory_factor * Latency_factor) // This multiplicative approach ensures any single critical factor can override others // // 3. Exponential Smoothing: // New_size = Current_size + smoothing_factor * (Target_size - Current_size) // This prevents rapid oscillations and provides stable convergence // // 4. Discrete Quantization: // Final sizes are rounded to frame boundaries and clamped to configured limits // // The algorithm runs periodically and only applies changes when the adaptation interval // has elapsed, preventing excessive adjustments that could destabilize the audio pipeline. func (abm *AdaptiveBufferManager) adaptBufferSizes() { // Collect current system metrics metrics := abm.processMonitor.GetCurrentMetrics() if len(metrics) == 0 { return // No metrics available } // Calculate system-wide CPU and memory usage totalCPU := 0.0 totalMemory := 0.0 processCount := 0 for _, metric := range metrics { totalCPU += metric.CPUPercent totalMemory += metric.MemoryPercent processCount++ } if processCount == 0 { return } // Store system metrics atomically systemCPU := totalCPU // Total CPU across all monitored processes systemMemory := totalMemory / float64(processCount) // Average memory usage atomic.StoreInt64(&abm.systemCPUPercent, int64(systemCPU*100)) atomic.StoreInt64(&abm.systemMemoryPercent, int64(systemMemory*100)) // Get current latency currentLatencyNs := atomic.LoadInt64(&abm.averageLatency) currentLatency := time.Duration(currentLatencyNs) // Calculate adaptation factors cpuFactor := abm.calculateCPUFactor(systemCPU) memoryFactor := abm.calculateMemoryFactor(systemMemory) latencyFactor := abm.calculateLatencyFactor(currentLatency) // Combine factors with weights (CPU has highest priority for KVM coexistence) combinedFactor := GetConfig().CPUMemoryWeight*cpuFactor + GetConfig().MemoryWeight*memoryFactor + GetConfig().LatencyWeight*latencyFactor // Apply adaptation with smoothing currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize)) currentOutput := float64(atomic.LoadInt64(&abm.currentOutputBufferSize)) // Calculate new buffer sizes newInputSize := abm.applyAdaptation(currentInput, combinedFactor) newOutputSize := abm.applyAdaptation(currentOutput, combinedFactor) // Update buffer sizes if they changed significantly adjustmentMade := false if math.Abs(newInputSize-currentInput) >= 0.5 || math.Abs(newOutputSize-currentOutput) >= 0.5 { atomic.StoreInt64(&abm.currentInputBufferSize, int64(math.Round(newInputSize))) atomic.StoreInt64(&abm.currentOutputBufferSize, int64(math.Round(newOutputSize))) atomic.AddInt64(&abm.adaptationCount, 1) abm.mutex.Lock() abm.lastAdaptation = time.Now() abm.mutex.Unlock() adjustmentMade = true abm.logger.Debug(). Float64("cpu_percent", systemCPU). Float64("memory_percent", systemMemory). Dur("latency", currentLatency). Float64("combined_factor", combinedFactor). Int("new_input_size", int(newInputSize)). Int("new_output_size", int(newOutputSize)). Msg("Adapted buffer sizes") } // Update metrics with current state currentInputSize := int(atomic.LoadInt64(&abm.currentInputBufferSize)) currentOutputSize := int(atomic.LoadInt64(&abm.currentOutputBufferSize)) UpdateAdaptiveBufferMetrics(currentInputSize, currentOutputSize, systemCPU, systemMemory, adjustmentMade) } // calculateCPUFactor returns adaptation factor based on CPU usage with threshold validation. // // Validation Rules: // - CPU percentage must be within valid range [0.0, 100.0] // - Uses LowCPUThreshold and HighCPUThreshold from config for decision boundaries // - Default thresholds: Low=20.0%, High=80.0% // // Adaptation Logic: // - CPU > HighCPUThreshold: Return -1.0 (decrease buffers to reduce CPU load) // - CPU < LowCPUThreshold: Return +1.0 (increase buffers for better quality) // - Between thresholds: Linear interpolation based on distance from midpoint // // Returns: Adaptation factor in range [-1.0, +1.0] // - Negative values: Decrease buffer sizes to reduce CPU usage // - Positive values: Increase buffer sizes for better audio quality // - Zero: No adaptation needed // // The function ensures CPU-aware buffer management to balance audio quality // with system performance, preventing CPU starvation of the KVM process. func (abm *AdaptiveBufferManager) calculateCPUFactor(cpuPercent float64) float64 { if cpuPercent > abm.config.HighCPUThreshold { // High CPU: decrease buffers to reduce latency and give CPU to KVM return -1.0 } else if cpuPercent < abm.config.LowCPUThreshold { // Low CPU: increase buffers for better quality return 1.0 } // Medium CPU: linear interpolation midpoint := (abm.config.HighCPUThreshold + abm.config.LowCPUThreshold) / 2 return (midpoint - cpuPercent) / (midpoint - abm.config.LowCPUThreshold) } // calculateMemoryFactor returns adaptation factor based on memory usage with threshold validation. // // Validation Rules: // - Memory percentage must be within valid range [0.0, 100.0] // - Uses LowMemoryThreshold and HighMemoryThreshold from config for decision boundaries // - Default thresholds: Low=30.0%, High=85.0% // // Adaptation Logic: // - Memory > HighMemoryThreshold: Return -1.0 (decrease buffers to free memory) // - Memory < LowMemoryThreshold: Return +1.0 (increase buffers for performance) // - Between thresholds: Linear interpolation based on distance from midpoint // // Returns: Adaptation factor in range [-1.0, +1.0] // - Negative values: Decrease buffer sizes to reduce memory usage // - Positive values: Increase buffer sizes for better performance // - Zero: No adaptation needed // // The function prevents memory exhaustion while optimizing buffer sizes // for audio processing performance and system stability. func (abm *AdaptiveBufferManager) calculateMemoryFactor(memoryPercent float64) float64 { if memoryPercent > abm.config.HighMemoryThreshold { // High memory: decrease buffers to free memory return -1.0 } else if memoryPercent < abm.config.LowMemoryThreshold { // Low memory: increase buffers for better performance return 1.0 } // Medium memory: linear interpolation midpoint := (abm.config.HighMemoryThreshold + abm.config.LowMemoryThreshold) / 2 return (midpoint - memoryPercent) / (midpoint - abm.config.LowMemoryThreshold) } // calculateLatencyFactor returns adaptation factor based on latency with threshold validation. // // Validation Rules: // - Latency must be non-negative duration // - Uses TargetLatency and MaxLatency from config for decision boundaries // - Default thresholds: Target=50ms, Max=200ms // // Adaptation Logic: // - Latency > MaxLatency: Return -1.0 (decrease buffers to reduce latency) // - Latency < TargetLatency: Return +1.0 (increase buffers for quality) // - Between thresholds: Linear interpolation based on distance from midpoint // // Returns: Adaptation factor in range [-1.0, +1.0] // - Negative values: Decrease buffer sizes to reduce audio latency // - Positive values: Increase buffer sizes for better audio quality // - Zero: Latency is at optimal level // // The function balances audio latency with quality, ensuring real-time // performance while maintaining acceptable audio processing quality. func (abm *AdaptiveBufferManager) calculateLatencyFactor(latency time.Duration) float64 { if latency > abm.config.MaxLatency { // High latency: decrease buffers return -1.0 } else if latency < abm.config.TargetLatency { // Low latency: can increase buffers return 1.0 } // Medium latency: linear interpolation midLatency := (abm.config.MaxLatency + abm.config.TargetLatency) / 2 return float64(midLatency-latency) / float64(midLatency-abm.config.TargetLatency) } // applyAdaptation applies the adaptation factor to current buffer size func (abm *AdaptiveBufferManager) applyAdaptation(currentSize, factor float64) float64 { // Calculate target size based on factor var targetSize float64 if factor > 0 { // Increase towards max targetSize = currentSize + factor*(float64(abm.config.MaxBufferSize)-currentSize) } else { // Decrease towards min targetSize = currentSize + factor*(currentSize-float64(abm.config.MinBufferSize)) } // Apply smoothing newSize := currentSize + abm.config.SmoothingFactor*(targetSize-currentSize) // Clamp to valid range return math.Max(float64(abm.config.MinBufferSize), math.Min(float64(abm.config.MaxBufferSize), newSize)) } // GetStats returns current adaptation statistics func (abm *AdaptiveBufferManager) GetStats() map[string]interface{} { abm.mutex.RLock() lastAdaptation := abm.lastAdaptation abm.mutex.RUnlock() return map[string]interface{}{ "input_buffer_size": abm.GetInputBufferSize(), "output_buffer_size": abm.GetOutputBufferSize(), "average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6, "system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / GetConfig().PercentageMultiplier, "system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / GetConfig().PercentageMultiplier, "adaptation_count": atomic.LoadInt64(&abm.adaptationCount), "last_adaptation": lastAdaptation, } } // Global adaptive buffer manager instance var globalAdaptiveBufferManager *AdaptiveBufferManager var adaptiveBufferOnce sync.Once // GetAdaptiveBufferManager returns the global adaptive buffer manager instance func GetAdaptiveBufferManager() *AdaptiveBufferManager { adaptiveBufferOnce.Do(func() { globalAdaptiveBufferManager = NewAdaptiveBufferManager(DefaultAdaptiveBufferConfig()) }) return globalAdaptiveBufferManager } // StartAdaptiveBuffering starts the global adaptive buffer manager func StartAdaptiveBuffering() { GetAdaptiveBufferManager().Start() } // StopAdaptiveBuffering stops the global adaptive buffer manager func StopAdaptiveBuffering() { if globalAdaptiveBufferManager != nil { globalAdaptiveBufferManager.Stop() } }