mirror of
https://github.com/jetkvm/kvm.git
synced 2025-09-16 08:38:14 +00:00
This change replaces all instances of GetConfig() function calls with direct access to the Config variable throughout the audio package. The modification improves performance by eliminating function call overhead and simplifies the codebase by removing unnecessary indirection. The commit also includes minor optimizations in validation logic and connection handling, while maintaining all existing functionality. Error handling remains robust with appropriate fallbacks when config values are not available. Additional improvements include: - Enhanced connection health monitoring in UnifiedAudioClient - Optimized validation functions using cached config values - Reduced memory allocations in hot paths - Improved error recovery during quality changes
246 lines
7.1 KiB
Go
246 lines
7.1 KiB
Go
package audio
|
|
|
|
import (
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/logging"
|
|
)
|
|
|
|
// Component name constant for logging
|
|
const (
|
|
AudioInputManagerComponent = "audio-input-manager"
|
|
)
|
|
|
|
// 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)
|
|
|
|
// Flush any pending sampled metrics before stopping
|
|
aim.flushPendingMetrics()
|
|
|
|
// 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
|
|
}
|
|
|
|
// Check mute state - drop frames if microphone is muted (like audio output)
|
|
if IsMicrophoneMuted() {
|
|
return nil // Muted, 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(Config.InputProcessingTimeoutMS)*time.Millisecond {
|
|
latencyMs := float64(processingTime.Milliseconds())
|
|
aim.logger.Warn().
|
|
Float64("latency_ms", latencyMs).
|
|
Msg("High audio processing latency detected")
|
|
|
|
// Record latency for goroutine cleanup optimization
|
|
RecordAudioLatency(latencyMs)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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
|
|
}
|
|
|
|
// Check mute state - drop frames if microphone is muted (like audio output)
|
|
if IsMicrophoneMuted() {
|
|
return nil // Muted, 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(Config.InputProcessingTimeoutMS)*time.Millisecond {
|
|
latencyMs := float64(processingTime.Milliseconds())
|
|
aim.logger.Warn().
|
|
Float64("latency_ms", latencyMs).
|
|
Msg("High audio processing latency detected")
|
|
|
|
// Record latency for goroutine cleanup optimization
|
|
RecordAudioLatency(latencyMs)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// IsRunning returns whether the audio input manager is running
|
|
// 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
|
|
// 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()
|
|
}
|