kvm/internal/audio/input.go
Alex P ece36ce5fd feat(audio): optimize validation and add dynamic opus encoder configuration
Consolidate audio frame validation functions into a single optimized implementation and add dynamic OPUS encoder parameter updates based on quality settings. Initialize validation cache at startup for consistent performance.

Add latency profiler for end-to-end audio pipeline monitoring. Update test cases to use unified validation function and initialize cache.

The changes improve performance by reducing function call overhead and enabling runtime optimization of audio encoding parameters based on quality presets.
2025-08-27 23:44:16 +00:00

219 lines
6.7 KiB
Go

package audio
import (
"fmt"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
)
// AudioInputMetrics holds metrics for microphone input
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
type AudioInputMetrics struct {
// Atomic int64 field first for proper ARM32 alignment
FramesSent int64 `json:"frames_sent"` // Total frames sent (input-specific)
// Embedded struct with atomic fields properly aligned
BaseAudioMetrics
}
// AudioInputManager manages microphone input stream using IPC mode only
type AudioInputManager struct {
*BaseAudioManager
ipcManager *AudioInputIPCManager
framesSent int64 // Input-specific metric
}
// NewAudioInputManager creates a new audio input manager
func NewAudioInputManager() *AudioInputManager {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputManagerComponent).Logger()
return &AudioInputManager{
BaseAudioManager: NewBaseAudioManager(logger),
ipcManager: NewAudioInputIPCManager(),
}
}
// Start begins processing microphone input
func (aim *AudioInputManager) Start() error {
if !aim.setRunning(true) {
return fmt.Errorf("audio input manager is already running")
}
aim.logComponentStart(AudioInputManagerComponent)
// Start the IPC-based audio input
err := aim.ipcManager.Start()
if err != nil {
aim.logComponentError(AudioInputManagerComponent, err, "failed to start component")
// Ensure proper cleanup on error
aim.setRunning(false)
// Reset metrics on failed start
aim.resetMetrics()
return err
}
aim.logComponentStarted(AudioInputManagerComponent)
return nil
}
// Stop stops processing microphone input
func (aim *AudioInputManager) Stop() {
if !aim.setRunning(false) {
return // Already stopped
}
aim.logComponentStop(AudioInputManagerComponent)
// Stop the IPC-based audio input
aim.ipcManager.Stop()
aim.logComponentStopped(AudioInputManagerComponent)
}
// resetMetrics resets all metrics to zero
func (aim *AudioInputManager) resetMetrics() {
aim.BaseAudioManager.resetMetrics()
atomic.StoreInt64(&aim.framesSent, 0)
}
// WriteOpusFrame writes an Opus frame to the audio input system with latency tracking
func (aim *AudioInputManager) WriteOpusFrame(frame []byte) error {
if !aim.IsRunning() {
return nil // Not running, silently drop
}
// Use ultra-fast validation for critical audio path
if err := ValidateAudioFrame(frame); err != nil {
aim.logComponentError(AudioInputManagerComponent, err, "Frame validation failed")
return fmt.Errorf("input frame validation failed: %w", err)
}
// Track end-to-end latency from WebRTC to IPC
startTime := time.Now()
err := aim.ipcManager.WriteOpusFrame(frame)
processingTime := time.Since(startTime)
// Log high latency warnings
if processingTime > time.Duration(GetConfig().InputProcessingTimeoutMS)*time.Millisecond {
aim.logger.Warn().
Dur("latency_ms", processingTime).
Msg("High audio processing latency detected")
}
if err != nil {
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
return err
}
// Update metrics
atomic.AddInt64(&aim.framesSent, 1)
aim.recordFrameProcessed(len(frame))
aim.updateLatency(processingTime)
return nil
}
// WriteOpusFrameZeroCopy writes an Opus frame using zero-copy optimization
func (aim *AudioInputManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFrame) error {
if !aim.IsRunning() {
return nil // Not running, silently drop
}
if frame == nil {
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
return nil
}
// Track end-to-end latency from WebRTC to IPC
startTime := time.Now()
err := aim.ipcManager.WriteOpusFrameZeroCopy(frame)
processingTime := time.Since(startTime)
// Log high latency warnings
if processingTime > time.Duration(GetConfig().InputProcessingTimeoutMS)*time.Millisecond {
aim.logger.Warn().
Dur("latency_ms", processingTime).
Msg("High audio processing latency detected")
}
if err != nil {
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
return err
}
// Update metrics
atomic.AddInt64(&aim.framesSent, 1)
aim.recordFrameProcessed(frame.Length())
aim.updateLatency(processingTime)
return nil
}
// GetMetrics returns current metrics
func (aim *AudioInputManager) GetMetrics() AudioInputMetrics {
return AudioInputMetrics{
FramesSent: atomic.LoadInt64(&aim.framesSent),
BaseAudioMetrics: aim.getBaseMetrics(),
}
}
// GetComprehensiveMetrics returns detailed performance metrics across all components
func (aim *AudioInputManager) GetComprehensiveMetrics() map[string]interface{} {
// Get base metrics
baseMetrics := aim.GetMetrics()
// Get detailed IPC metrics
ipcMetrics, detailedStats := aim.ipcManager.GetDetailedMetrics()
comprehensiveMetrics := map[string]interface{}{
"manager": map[string]interface{}{
"frames_sent": baseMetrics.FramesSent,
"frames_dropped": baseMetrics.FramesDropped,
"bytes_processed": baseMetrics.BytesProcessed,
"average_latency_ms": float64(baseMetrics.AverageLatency.Nanoseconds()) / 1e6,
"last_frame_time": baseMetrics.LastFrameTime,
"running": aim.IsRunning(),
},
"ipc": map[string]interface{}{
"frames_sent": ipcMetrics.FramesSent,
"frames_dropped": ipcMetrics.FramesDropped,
"bytes_processed": ipcMetrics.BytesProcessed,
"average_latency_ms": float64(ipcMetrics.AverageLatency.Nanoseconds()) / 1e6,
"last_frame_time": ipcMetrics.LastFrameTime,
},
"detailed": detailedStats,
}
return comprehensiveMetrics
}
// LogPerformanceStats logs current performance statistics
func (aim *AudioInputManager) LogPerformanceStats() {
metrics := aim.GetComprehensiveMetrics()
managerStats := metrics["manager"].(map[string]interface{})
ipcStats := metrics["ipc"].(map[string]interface{})
detailedStats := metrics["detailed"].(map[string]interface{})
aim.logger.Info().
Int64("manager_frames_sent", managerStats["frames_sent"].(int64)).
Int64("manager_frames_dropped", managerStats["frames_dropped"].(int64)).
Float64("manager_latency_ms", managerStats["average_latency_ms"].(float64)).
Int64("ipc_frames_sent", ipcStats["frames_sent"].(int64)).
Int64("ipc_frames_dropped", ipcStats["frames_dropped"].(int64)).
Float64("ipc_latency_ms", ipcStats["average_latency_ms"].(float64)).
Float64("client_drop_rate", detailedStats["client_drop_rate"].(float64)).
Float64("frames_per_second", detailedStats["frames_per_second"].(float64)).
Msg("Audio input performance metrics")
}
// Note: IsRunning() is inherited from BaseAudioManager
// 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
func (aim *AudioInputManager) IsReady() bool {
if !aim.IsRunning() {
return false
}
return aim.ipcManager.IsReady()
}