Compare commits

..

No commits in common. "0d4176cf98748a18481b5893c282980d30864836" and "8fb0b9f9c6790e7b4ed66a21cf75b0eedef97d54" have entirely different histories.

48 changed files with 1664 additions and 4706 deletions

View File

@ -105,20 +105,13 @@ type AdaptiveBufferManager struct {
// NewAdaptiveBufferManager creates a new adaptive buffer manager
func NewAdaptiveBufferManager(config AdaptiveBufferConfig) *AdaptiveBufferManager {
logger := logging.GetDefaultLogger().With().Str("component", "adaptive-buffer").Logger()
if err := ValidateAdaptiveBufferConfig(config.MinBufferSize, config.MaxBufferSize, config.DefaultBufferSize); err != nil {
logger.Warn().Err(err).Msg("invalid adaptive buffer config, using defaults")
config = DefaultAdaptiveBufferConfig()
}
ctx, cancel := context.WithCancel(context.Background())
return &AdaptiveBufferManager{
currentInputBufferSize: int64(config.DefaultBufferSize),
currentOutputBufferSize: int64(config.DefaultBufferSize),
config: config,
logger: logger,
logger: logging.GetDefaultLogger().With().Str("component", "adaptive-buffer").Logger(),
processMonitor: GetProcessMonitor(),
ctx: ctx,
cancel: cancel,
@ -130,14 +123,14 @@ func NewAdaptiveBufferManager(config AdaptiveBufferConfig) *AdaptiveBufferManage
func (abm *AdaptiveBufferManager) Start() {
abm.wg.Add(1)
go abm.adaptationLoop()
abm.logger.Info().Msg("adaptive buffer manager started")
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")
abm.logger.Info().Msg("Adaptive buffer manager stopped")
}
// GetInputBufferSize returns the current recommended input buffer size

View File

@ -72,14 +72,14 @@ func NewAdaptiveOptimizer(latencyMonitor *LatencyMonitor, bufferManager *Adaptiv
func (ao *AdaptiveOptimizer) Start() {
ao.wg.Add(1)
go ao.optimizationLoop()
ao.logger.Debug().Msg("adaptive optimizer started")
ao.logger.Info().Msg("Adaptive optimizer started")
}
// Stop stops the adaptive optimizer
func (ao *AdaptiveOptimizer) Stop() {
ao.cancel()
ao.wg.Wait()
ao.logger.Debug().Msg("adaptive optimizer stopped")
ao.logger.Info().Msg("Adaptive optimizer stopped")
}
// initializeStrategies sets up the available optimization strategies
@ -178,9 +178,9 @@ func (ao *AdaptiveOptimizer) checkStability() {
if metrics.Current > ao.config.RollbackThreshold {
currentLevel := int(atomic.LoadInt64(&ao.optimizationLevel))
if currentLevel > 0 {
ao.logger.Warn().Dur("current_latency", metrics.Current).Dur("threshold", ao.config.RollbackThreshold).Msg("rolling back optimizations due to excessive latency")
ao.logger.Warn().Dur("current_latency", metrics.Current).Dur("threshold", ao.config.RollbackThreshold).Msg("Rolling back optimizations due to excessive latency")
if err := ao.decreaseOptimization(currentLevel - 1); err != nil {
ao.logger.Error().Err(err).Msg("failed to decrease optimization level")
ao.logger.Error().Err(err).Msg("Failed to decrease optimization level")
}
}
}

View File

@ -1,10 +1,82 @@
// Package audio provides real-time audio processing for JetKVM with low-latency streaming.
// Package audio provides a comprehensive real-time audio processing system for JetKVM.
//
// Key components: output/input pipelines with Opus codec, adaptive buffer management,
// zero-copy frame pools, IPC communication, and process supervision.
// # Architecture Overview
//
// Supports four quality presets (Low/Medium/High/Ultra) with configurable bitrates.
// All APIs are thread-safe with comprehensive error handling and metrics collection.
// 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
//
// # Performance Characteristics
//
@ -28,8 +100,6 @@ import (
"errors"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
)
var (
@ -120,14 +190,13 @@ var qualityPresets = map[AudioQuality]struct {
func GetAudioQualityPresets() map[AudioQuality]AudioConfig {
result := make(map[AudioQuality]AudioConfig)
for quality, preset := range qualityPresets {
config := AudioConfig{
result[quality] = AudioConfig{
Quality: quality,
Bitrate: preset.outputBitrate,
SampleRate: preset.sampleRate,
Channels: preset.channels,
FrameSize: preset.frameSize,
}
result[quality] = config
}
return result
}
@ -136,7 +205,7 @@ func GetAudioQualityPresets() map[AudioQuality]AudioConfig {
func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
result := make(map[AudioQuality]AudioConfig)
for quality, preset := range qualityPresets {
config := AudioConfig{
result[quality] = AudioConfig{
Quality: quality,
Bitrate: preset.inputBitrate,
SampleRate: func() int {
@ -148,67 +217,15 @@ func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
Channels: 1, // Microphone is always mono
FrameSize: preset.frameSize,
}
result[quality] = config
}
return result
}
// SetAudioQuality updates the current audio quality configuration
func SetAudioQuality(quality AudioQuality) {
// Validate audio quality parameter
if err := ValidateAudioQuality(quality); err != nil {
// Log validation error but don't fail - maintain backward compatibility
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Warn().Err(err).Int("quality", int(quality)).Msg("invalid audio quality, using current config")
return
}
presets := GetAudioQualityPresets()
if config, exists := presets[quality]; exists {
currentConfig = config
// Update CGO OPUS encoder parameters based on 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
}
// Dynamically update CGO OPUS encoder parameters
// Use current VBR constraint setting from config
vbrConstraint := GetConfig().CGOOpusVBRConstraint
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")
}
}
}
@ -219,14 +236,6 @@ func GetAudioConfig() AudioConfig {
// SetMicrophoneQuality updates the current microphone quality configuration
func SetMicrophoneQuality(quality AudioQuality) {
// Validate audio quality parameter
if err := ValidateAudioQuality(quality); err != nil {
// Log validation error but don't fail - maintain backward compatibility
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Warn().Err(err).Int("quality", int(quality)).Msg("invalid microphone quality, using current config")
return
}
presets := GetMicrophoneQualityPresets()
if config, exists := presets[quality]; exists {
currentMicrophoneConfig = config

View File

@ -1,317 +0,0 @@
//go:build cgo
// +build cgo
package audio
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestAudioQualityEdgeCases tests edge cases for audio quality functions
// These tests ensure the recent validation removal doesn't introduce regressions
func TestAudioQualityEdgeCases(t *testing.T) {
tests := []struct {
name string
testFunc func(t *testing.T)
}{
{"AudioQualityBoundaryValues", testAudioQualityBoundaryValues},
{"MicrophoneQualityBoundaryValues", testMicrophoneQualityBoundaryValues},
{"AudioQualityPresetsConsistency", testAudioQualityPresetsConsistency},
{"MicrophoneQualityPresetsConsistency", testMicrophoneQualityPresetsConsistency},
{"QualitySettingsThreadSafety", testQualitySettingsThreadSafety},
{"QualityPresetsImmutability", testQualityPresetsImmutability},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.testFunc(t)
})
}
}
// testAudioQualityBoundaryValues tests boundary values for audio quality
func testAudioQualityBoundaryValues(t *testing.T) {
// Test minimum valid quality (0)
originalConfig := GetAudioConfig()
SetAudioQuality(AudioQualityLow)
assert.Equal(t, AudioQualityLow, GetAudioConfig().Quality, "Should accept minimum quality value")
// Test maximum valid quality (3)
SetAudioQuality(AudioQualityUltra)
assert.Equal(t, AudioQualityUltra, GetAudioConfig().Quality, "Should accept maximum quality value")
// Test that quality settings work correctly
SetAudioQuality(AudioQualityMedium)
currentConfig := GetAudioConfig()
assert.Equal(t, AudioQualityMedium, currentConfig.Quality, "Should set medium quality")
t.Logf("Medium quality config: %+v", currentConfig)
SetAudioQuality(AudioQualityHigh)
currentConfig = GetAudioConfig()
assert.Equal(t, AudioQualityHigh, currentConfig.Quality, "Should set high quality")
t.Logf("High quality config: %+v", currentConfig)
// Restore original quality
SetAudioQuality(originalConfig.Quality)
}
// testMicrophoneQualityBoundaryValues tests boundary values for microphone quality
func testMicrophoneQualityBoundaryValues(t *testing.T) {
// Test minimum valid quality
originalConfig := GetMicrophoneConfig()
SetMicrophoneQuality(AudioQualityLow)
assert.Equal(t, AudioQualityLow, GetMicrophoneConfig().Quality, "Should accept minimum microphone quality value")
// Test maximum valid quality
SetMicrophoneQuality(AudioQualityUltra)
assert.Equal(t, AudioQualityUltra, GetMicrophoneConfig().Quality, "Should accept maximum microphone quality value")
// Test that quality settings work correctly
SetMicrophoneQuality(AudioQualityMedium)
currentConfig := GetMicrophoneConfig()
assert.Equal(t, AudioQualityMedium, currentConfig.Quality, "Should set medium microphone quality")
t.Logf("Medium microphone quality config: %+v", currentConfig)
SetMicrophoneQuality(AudioQualityHigh)
currentConfig = GetMicrophoneConfig()
assert.Equal(t, AudioQualityHigh, currentConfig.Quality, "Should set high microphone quality")
t.Logf("High microphone quality config: %+v", currentConfig)
// Restore original quality
SetMicrophoneQuality(originalConfig.Quality)
}
// testAudioQualityPresetsConsistency tests consistency of audio quality presets
func testAudioQualityPresetsConsistency(t *testing.T) {
presets := GetAudioQualityPresets()
require.NotNil(t, presets, "Audio quality presets should not be nil")
require.NotEmpty(t, presets, "Audio quality presets should not be empty")
// Verify presets have expected structure
for i, preset := range presets {
t.Logf("Audio preset %d: %+v", i, preset)
// Each preset should have reasonable values
assert.GreaterOrEqual(t, preset.Bitrate, 0, "Bitrate should be non-negative")
assert.Greater(t, preset.SampleRate, 0, "Sample rate should be positive")
assert.Greater(t, preset.Channels, 0, "Channels should be positive")
}
// Test that presets are accessible by valid quality levels
qualityLevels := []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra}
for _, quality := range qualityLevels {
preset, exists := presets[quality]
assert.True(t, exists, "Preset should exist for quality %v", quality)
assert.Greater(t, preset.Bitrate, 0, "Preset bitrate should be positive for quality %v", quality)
}
}
// testMicrophoneQualityPresetsConsistency tests consistency of microphone quality presets
func testMicrophoneQualityPresetsConsistency(t *testing.T) {
presets := GetMicrophoneQualityPresets()
require.NotNil(t, presets, "Microphone quality presets should not be nil")
require.NotEmpty(t, presets, "Microphone quality presets should not be empty")
// Verify presets have expected structure
for i, preset := range presets {
t.Logf("Microphone preset %d: %+v", i, preset)
// Each preset should have reasonable values
assert.GreaterOrEqual(t, preset.Bitrate, 0, "Bitrate should be non-negative")
assert.Greater(t, preset.SampleRate, 0, "Sample rate should be positive")
assert.Greater(t, preset.Channels, 0, "Channels should be positive")
}
// Test that presets are accessible by valid quality levels
qualityLevels := []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra}
for _, quality := range qualityLevels {
preset, exists := presets[quality]
assert.True(t, exists, "Microphone preset should exist for quality %v", quality)
assert.Greater(t, preset.Bitrate, 0, "Microphone preset bitrate should be positive for quality %v", quality)
}
}
// testQualitySettingsThreadSafety tests thread safety of quality settings
func testQualitySettingsThreadSafety(t *testing.T) {
if testing.Short() {
t.Skip("Skipping thread safety test in short mode")
}
originalAudioConfig := GetAudioConfig()
originalMicConfig := GetMicrophoneConfig()
// Test concurrent access to quality settings
const numGoroutines = 50
const numOperations = 100
done := make(chan bool, numGoroutines*2)
// Audio quality goroutines
for i := 0; i < numGoroutines; i++ {
go func(id int) {
for j := 0; j < numOperations; j++ {
// Cycle through valid quality values
qualityIndex := j % 4
var quality AudioQuality
switch qualityIndex {
case 0:
quality = AudioQualityLow
case 1:
quality = AudioQualityMedium
case 2:
quality = AudioQualityHigh
case 3:
quality = AudioQualityUltra
}
SetAudioQuality(quality)
_ = GetAudioConfig()
}
done <- true
}(i)
}
// Microphone quality goroutines
for i := 0; i < numGoroutines; i++ {
go func(id int) {
for j := 0; j < numOperations; j++ {
// Cycle through valid quality values
qualityIndex := j % 4
var quality AudioQuality
switch qualityIndex {
case 0:
quality = AudioQualityLow
case 1:
quality = AudioQualityMedium
case 2:
quality = AudioQualityHigh
case 3:
quality = AudioQualityUltra
}
SetMicrophoneQuality(quality)
_ = GetMicrophoneConfig()
}
done <- true
}(i)
}
// Wait for all goroutines to complete
for i := 0; i < numGoroutines*2; i++ {
<-done
}
// Verify system is still functional
SetAudioQuality(AudioQualityHigh)
assert.Equal(t, AudioQualityHigh, GetAudioConfig().Quality, "Audio quality should be settable after concurrent access")
SetMicrophoneQuality(AudioQualityMedium)
assert.Equal(t, AudioQualityMedium, GetMicrophoneConfig().Quality, "Microphone quality should be settable after concurrent access")
// Restore original values
SetAudioQuality(originalAudioConfig.Quality)
SetMicrophoneQuality(originalMicConfig.Quality)
}
// testQualityPresetsImmutability tests that quality presets are not accidentally modified
func testQualityPresetsImmutability(t *testing.T) {
// Get presets multiple times and verify they're consistent
presets1 := GetAudioQualityPresets()
presets2 := GetAudioQualityPresets()
require.Equal(t, len(presets1), len(presets2), "Preset count should be consistent")
// Verify each preset is identical
for quality := range presets1 {
assert.Equal(t, presets1[quality].Bitrate, presets2[quality].Bitrate,
"Preset %v bitrate should be consistent", quality)
assert.Equal(t, presets1[quality].SampleRate, presets2[quality].SampleRate,
"Preset %v sample rate should be consistent", quality)
assert.Equal(t, presets1[quality].Channels, presets2[quality].Channels,
"Preset %v channels should be consistent", quality)
}
// Test microphone presets as well
micPresets1 := GetMicrophoneQualityPresets()
micPresets2 := GetMicrophoneQualityPresets()
require.Equal(t, len(micPresets1), len(micPresets2), "Microphone preset count should be consistent")
for quality := range micPresets1 {
assert.Equal(t, micPresets1[quality].Bitrate, micPresets2[quality].Bitrate,
"Microphone preset %v bitrate should be consistent", quality)
assert.Equal(t, micPresets1[quality].SampleRate, micPresets2[quality].SampleRate,
"Microphone preset %v sample rate should be consistent", quality)
assert.Equal(t, micPresets1[quality].Channels, micPresets2[quality].Channels,
"Microphone preset %v channels should be consistent", quality)
}
}
// TestQualityValidationRemovalRegression tests that validation removal doesn't cause regressions
func TestQualityValidationRemovalRegression(t *testing.T) {
// This test ensures that removing validation from GET endpoints doesn't break functionality
// Test that presets are still accessible
audioPresets := GetAudioQualityPresets()
assert.NotNil(t, audioPresets, "Audio presets should be accessible after validation removal")
assert.NotEmpty(t, audioPresets, "Audio presets should not be empty")
micPresets := GetMicrophoneQualityPresets()
assert.NotNil(t, micPresets, "Microphone presets should be accessible after validation removal")
assert.NotEmpty(t, micPresets, "Microphone presets should not be empty")
// Test that quality getters still work
audioConfig := GetAudioConfig()
assert.GreaterOrEqual(t, int(audioConfig.Quality), 0, "Audio quality should be non-negative")
micConfig := GetMicrophoneConfig()
assert.GreaterOrEqual(t, int(micConfig.Quality), 0, "Microphone quality should be non-negative")
// Test that setters still work (for valid values)
originalAudio := GetAudioConfig()
originalMic := GetMicrophoneConfig()
SetAudioQuality(AudioQualityMedium)
assert.Equal(t, AudioQualityMedium, GetAudioConfig().Quality, "Audio quality setter should work")
SetMicrophoneQuality(AudioQualityHigh)
assert.Equal(t, AudioQualityHigh, GetMicrophoneConfig().Quality, "Microphone quality setter should work")
// Restore original values
SetAudioQuality(originalAudio.Quality)
SetMicrophoneQuality(originalMic.Quality)
}
// TestPerformanceAfterValidationRemoval tests that performance improved after validation removal
func TestPerformanceAfterValidationRemoval(t *testing.T) {
if testing.Short() {
t.Skip("Skipping performance test in short mode")
}
// Benchmark preset access (should be faster without validation)
const iterations = 10000
// Time audio preset access
start := time.Now()
for i := 0; i < iterations; i++ {
_ = GetAudioQualityPresets()
}
audioDuration := time.Since(start)
// Time microphone preset access
start = time.Now()
for i := 0; i < iterations; i++ {
_ = GetMicrophoneQualityPresets()
}
micDuration := time.Since(start)
t.Logf("Audio presets access time for %d iterations: %v", iterations, audioDuration)
t.Logf("Microphone presets access time for %d iterations: %v", iterations, micDuration)
// Verify reasonable performance (should complete quickly without validation overhead)
maxExpectedDuration := time.Second // Very generous limit
assert.Less(t, audioDuration, maxExpectedDuration, "Audio preset access should be fast")
assert.Less(t, micDuration, maxExpectedDuration, "Microphone preset access should be fast")
}

View File

@ -1,120 +0,0 @@
package audio
import (
"sync/atomic"
"time"
"github.com/rs/zerolog"
)
// BaseAudioMetrics provides common metrics fields for both input and output
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
type BaseAudioMetrics struct {
// Atomic int64 fields first for proper ARM32 alignment
FramesProcessed int64 `json:"frames_processed"`
FramesDropped int64 `json:"frames_dropped"`
BytesProcessed int64 `json:"bytes_processed"`
ConnectionDrops int64 `json:"connection_drops"`
// Non-atomic fields after atomic fields
LastFrameTime time.Time `json:"last_frame_time"`
AverageLatency time.Duration `json:"average_latency"`
}
// BaseAudioManager provides common functionality for audio managers
type BaseAudioManager struct {
metrics BaseAudioMetrics
logger zerolog.Logger
running int32
}
// NewBaseAudioManager creates a new base audio manager
func NewBaseAudioManager(logger zerolog.Logger) *BaseAudioManager {
return &BaseAudioManager{
logger: logger,
}
}
// IsRunning returns whether the manager is running
func (bam *BaseAudioManager) IsRunning() bool {
return atomic.LoadInt32(&bam.running) == 1
}
// setRunning atomically sets the running state
func (bam *BaseAudioManager) setRunning(running bool) bool {
if running {
return atomic.CompareAndSwapInt32(&bam.running, 0, 1)
}
return atomic.CompareAndSwapInt32(&bam.running, 1, 0)
}
// resetMetrics resets all metrics to zero
func (bam *BaseAudioManager) resetMetrics() {
atomic.StoreInt64(&bam.metrics.FramesProcessed, 0)
atomic.StoreInt64(&bam.metrics.FramesDropped, 0)
atomic.StoreInt64(&bam.metrics.BytesProcessed, 0)
atomic.StoreInt64(&bam.metrics.ConnectionDrops, 0)
bam.metrics.LastFrameTime = time.Time{}
bam.metrics.AverageLatency = 0
}
// getBaseMetrics returns a copy of the base metrics
func (bam *BaseAudioManager) getBaseMetrics() BaseAudioMetrics {
return BaseAudioMetrics{
FramesProcessed: atomic.LoadInt64(&bam.metrics.FramesProcessed),
FramesDropped: atomic.LoadInt64(&bam.metrics.FramesDropped),
BytesProcessed: atomic.LoadInt64(&bam.metrics.BytesProcessed),
ConnectionDrops: atomic.LoadInt64(&bam.metrics.ConnectionDrops),
LastFrameTime: bam.metrics.LastFrameTime,
AverageLatency: bam.metrics.AverageLatency,
}
}
// recordFrameProcessed records a processed frame
func (bam *BaseAudioManager) recordFrameProcessed(bytes int) {
atomic.AddInt64(&bam.metrics.FramesProcessed, 1)
atomic.AddInt64(&bam.metrics.BytesProcessed, int64(bytes))
bam.metrics.LastFrameTime = time.Now()
}
// recordFrameDropped records a dropped frame
func (bam *BaseAudioManager) recordFrameDropped() {
atomic.AddInt64(&bam.metrics.FramesDropped, 1)
}
// updateLatency updates the average latency
func (bam *BaseAudioManager) updateLatency(latency time.Duration) {
// Simple moving average - could be enhanced with more sophisticated algorithms
currentAvg := bam.metrics.AverageLatency
if currentAvg == 0 {
bam.metrics.AverageLatency = latency
} else {
// Weighted average: 90% old + 10% new
bam.metrics.AverageLatency = time.Duration(float64(currentAvg)*0.9 + float64(latency)*0.1)
}
}
// logComponentStart logs component start with consistent format
func (bam *BaseAudioManager) logComponentStart(component string) {
bam.logger.Debug().Str("component", component).Msg("starting component")
}
// logComponentStarted logs component started with consistent format
func (bam *BaseAudioManager) logComponentStarted(component string) {
bam.logger.Debug().Str("component", component).Msg("component started successfully")
}
// logComponentStop logs component stop with consistent format
func (bam *BaseAudioManager) logComponentStop(component string) {
bam.logger.Debug().Str("component", component).Msg("stopping component")
}
// logComponentStopped logs component stopped with consistent format
func (bam *BaseAudioManager) logComponentStopped(component string) {
bam.logger.Debug().Str("component", component).Msg("component stopped")
}
// logComponentError logs component error with consistent format
func (bam *BaseAudioManager) logComponentError(component string, err error, msg string) {
bam.logger.Error().Err(err).Str("component", component).Msg(msg)
}

View File

@ -1,133 +0,0 @@
//go:build cgo
// +build cgo
package audio
import (
"context"
"os/exec"
"sync"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// BaseSupervisor provides common functionality for audio supervisors
type BaseSupervisor struct {
ctx context.Context
cancel context.CancelFunc
logger *zerolog.Logger
mutex sync.RWMutex
running int32
// Process management
cmd *exec.Cmd
processPID int
// Process monitoring
processMonitor *ProcessMonitor
// Exit tracking
lastExitCode int
lastExitTime time.Time
}
// NewBaseSupervisor creates a new base supervisor
func NewBaseSupervisor(componentName string) *BaseSupervisor {
logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger()
return &BaseSupervisor{
logger: &logger,
processMonitor: GetProcessMonitor(),
}
}
// IsRunning returns whether the supervisor is currently running
func (bs *BaseSupervisor) IsRunning() bool {
return atomic.LoadInt32(&bs.running) == 1
}
// setRunning atomically sets the running state
func (bs *BaseSupervisor) setRunning(running bool) {
if running {
atomic.StoreInt32(&bs.running, 1)
} else {
atomic.StoreInt32(&bs.running, 0)
}
}
// GetProcessPID returns the current process PID
func (bs *BaseSupervisor) GetProcessPID() int {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
return bs.processPID
}
// GetLastExitInfo returns the last exit code and time
func (bs *BaseSupervisor) GetLastExitInfo() (exitCode int, exitTime time.Time) {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
return bs.lastExitCode, bs.lastExitTime
}
// GetProcessMetrics returns process metrics if available
func (bs *BaseSupervisor) GetProcessMetrics() *ProcessMetrics {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
if bs.cmd == nil || bs.cmd.Process == nil {
return &ProcessMetrics{
PID: 0,
CPUPercent: 0.0,
MemoryRSS: 0,
MemoryVMS: 0,
MemoryPercent: 0.0,
Timestamp: time.Now(),
ProcessName: "audio-server",
}
}
pid := bs.cmd.Process.Pid
if bs.processMonitor != nil {
metrics := bs.processMonitor.GetCurrentMetrics()
for _, metric := range metrics {
if metric.PID == pid {
return &metric
}
}
}
// Return default metrics if process not found in monitor
return &ProcessMetrics{
PID: pid,
CPUPercent: 0.0,
MemoryRSS: 0,
MemoryVMS: 0,
MemoryPercent: 0.0,
Timestamp: time.Now(),
ProcessName: "audio-server",
}
}
// logSupervisorStart logs supervisor start event
func (bs *BaseSupervisor) logSupervisorStart() {
bs.logger.Info().Msg("Supervisor starting")
}
// logSupervisorStop logs supervisor stop event
func (bs *BaseSupervisor) logSupervisorStop() {
bs.logger.Info().Msg("Supervisor stopping")
}
// createContext creates a new context for the supervisor
func (bs *BaseSupervisor) createContext() {
bs.ctx, bs.cancel = context.WithCancel(context.Background())
}
// cancelContext cancels the supervisor context
func (bs *BaseSupervisor) cancelContext() {
if bs.cancel != nil {
bs.cancel()
}
}

View File

@ -60,18 +60,6 @@ type batchReadResult struct {
// NewBatchAudioProcessor creates a new batch audio processor
func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAudioProcessor {
// Validate input parameters
if err := ValidateBufferSize(batchSize); err != nil {
logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger()
logger.Warn().Err(err).Int("batchSize", batchSize).Msg("invalid batch size, using default")
batchSize = GetConfig().BatchProcessorFramesPerBatch
}
if batchDuration <= 0 {
logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger()
logger.Warn().Dur("batchDuration", batchDuration).Msg("invalid batch duration, using default")
batchDuration = GetConfig().BatchProcessingDelay
}
ctx, cancel := context.WithCancel(context.Background())
logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger()
@ -129,12 +117,6 @@ func (bap *BatchAudioProcessor) Stop() {
// BatchReadEncode performs batched audio read and encode operations
func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
// Validate buffer before processing
if err := ValidateBufferSize(len(buffer)); err != nil {
bap.logger.Debug().Err(err).Msg("invalid buffer for batch processing")
return 0, err
}
if atomic.LoadInt32(&bap.running) == 0 {
// Fallback to single operation if batch processor is not running
atomic.AddInt64(&bap.stats.SingleReads, 1)
@ -220,12 +202,12 @@ func (bap *BatchAudioProcessor) processBatchRead(batch []batchReadRequest) {
// Set high priority for batch audio processing
if err := SetAudioThreadPriority(); err != nil {
bap.logger.Warn().Err(err).Msg("failed to set batch audio processing priority")
bap.logger.Warn().Err(err).Msg("Failed to set batch audio processing priority")
}
defer func() {
if err := ResetThreadPriority(); err != nil {
bap.logger.Warn().Err(err).Msg("failed to reset thread priority")
bap.logger.Warn().Err(err).Msg("Failed to reset thread priority")
}
runtime.UnlockOSThread()
atomic.StoreInt32(&bap.threadPinned, 0)

View File

@ -1,43 +1,11 @@
package audio
import (
"runtime"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/jetkvm/kvm/internal/logging"
)
// Lock-free buffer cache for per-goroutine optimization
type lockFreeBufferCache struct {
buffers [4]*[]byte // Small fixed-size array for lock-free access
}
// Per-goroutine buffer cache using goroutine-local storage
var goroutineBufferCache = make(map[int64]*lockFreeBufferCache)
var goroutineCacheMutex sync.RWMutex
// getGoroutineID extracts goroutine ID from runtime stack for cache key
func getGoroutineID() int64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
// Parse "goroutine 123 [running]:" format
for i := 10; i < len(b); i++ {
if b[i] == ' ' {
id := int64(0)
for j := 10; j < i; j++ {
if b[j] >= '0' && b[j] <= '9' {
id = id*10 + int64(b[j]-'0')
}
}
return id
}
}
return 0
}
type AudioBufferPool struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
currentSize int64 // Current pool size (atomic)
@ -55,42 +23,23 @@ type AudioBufferPool struct {
}
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
// Validate buffer size parameter
if err := ValidateBufferSize(bufferSize); err != nil {
// 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
}
// 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
// Pre-allocate 20% of max pool size for immediate availability
preallocSize := GetConfig().PreallocPercentage
preallocated := make([]*[]byte, 0, preallocSize)
// Pre-allocate buffers with optimized capacity
// Pre-allocate buffers to reduce initial allocation overhead
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,
maxPoolSize: GetConfig().MaxPoolSize, // Limit pool size to prevent excessive memory usage
preallocated: preallocated,
preallocSize: preallocSize,
pool: sync.Pool{
New: func() interface{} {
// Allocate exact size to minimize memory waste
buf := make([]byte, 0, bufferSize)
return &buf
},
@ -100,72 +49,41 @@ 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, wasHit)
GetGranularMetricsCollector().RecordFramePoolGet(latency, atomic.LoadInt64(&p.hitCount) > 0)
} else {
GetGranularMetricsCollector().RecordControlPoolGet(latency, wasHit)
GetGranularMetricsCollector().RecordControlPoolGet(latency, atomic.LoadInt64(&p.hitCount) > 0)
}
}()
// Fast path: Try lock-free per-goroutine cache first
gid := getGoroutineID()
goroutineCacheMutex.RLock()
cache, exists := goroutineBufferCache[gid]
goroutineCacheMutex.RUnlock()
if exists && cache != nil {
// Try to get buffer from lock-free cache
for i := 0; i < len(cache.buffers); i++ {
bufPtr := (*unsafe.Pointer)(unsafe.Pointer(&cache.buffers[i]))
buf := (*[]byte)(atomic.LoadPointer(bufPtr))
if buf != nil && atomic.CompareAndSwapPointer(bufPtr, unsafe.Pointer(buf), nil) {
atomic.AddInt64(&p.hitCount, 1)
wasHit = true
*buf = (*buf)[:0]
return *buf
}
}
}
// Fallback: Try pre-allocated pool with mutex
// First try pre-allocated buffers for fastest access
p.mutex.Lock()
if len(p.preallocated) > 0 {
lastIdx := len(p.preallocated) - 1
buf := p.preallocated[lastIdx]
p.preallocated = p.preallocated[:lastIdx]
buf := p.preallocated[len(p.preallocated)-1]
p.preallocated = p.preallocated[:len(p.preallocated)-1]
p.mutex.Unlock()
// Update hit counter
atomic.AddInt64(&p.hitCount, 1)
wasHit = true
// Ensure buffer is properly reset
*buf = (*buf)[:0]
return *buf
return (*buf)[:0] // Reset length but keep capacity
}
p.mutex.Unlock()
// Try sync.Pool next
if poolBuf := p.pool.Get(); poolBuf != nil {
buf := poolBuf.(*[]byte)
// Update hit counter
atomic.AddInt64(&p.hitCount, 1)
// 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)
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()
atomic.AddInt64(&p.hitCount, 1)
return (*bufPtr)[:0] // Reset length but keep capacity
}
// Pool miss - allocate new buffer with exact capacity
// Last resort: allocate new buffer
atomic.AddInt64(&p.missCount, 1)
return make([]byte, 0, p.bufferSize)
}
@ -182,40 +100,14 @@ func (p *AudioBufferPool) Put(buf []byte) {
}
}()
// 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
if cap(buf) < p.bufferSize {
return // Buffer too small, don't pool it
}
// Reset buffer for reuse - clear any sensitive data
// Reset buffer for reuse
resetBuf := buf[:0]
// Fast path: Try to put in lock-free per-goroutine cache
gid := getGoroutineID()
goroutineCacheMutex.RLock()
cache, exists := goroutineBufferCache[gid]
goroutineCacheMutex.RUnlock()
if !exists {
// Create new cache for this goroutine
cache = &lockFreeBufferCache{}
goroutineCacheMutex.Lock()
goroutineBufferCache[gid] = cache
goroutineCacheMutex.Unlock()
}
if cache != nil {
// Try to store in lock-free cache
for i := 0; i < len(cache.buffers); i++ {
bufPtr := (*unsafe.Pointer)(unsafe.Pointer(&cache.buffers[i]))
if atomic.CompareAndSwapPointer(bufPtr, nil, unsafe.Pointer(&buf)) {
return // Successfully cached
}
}
}
// Fallback: Try to return to pre-allocated pool for fastest reuse
// First try to return to pre-allocated pool for fastest reuse
p.mutex.Lock()
if len(p.preallocated) < p.preallocSize {
p.preallocated = append(p.preallocated, &resetBuf)
@ -225,7 +117,10 @@ func (p *AudioBufferPool) Put(buf []byte) {
p.mutex.Unlock()
// Check sync.Pool size limit to prevent excessive memory usage
currentSize := atomic.LoadInt64(&p.currentSize)
p.mutex.RLock()
currentSize := p.currentSize
p.mutex.RUnlock()
if currentSize >= int64(p.maxPoolSize) {
return // Pool is full, let GC handle this buffer
}
@ -233,8 +128,10 @@ func (p *AudioBufferPool) Put(buf []byte) {
// Return to sync.Pool
p.pool.Put(&resetBuf)
// Update pool size counter atomically
atomic.AddInt64(&p.currentSize, 1)
// Update pool size counter
p.mutex.Lock()
p.currentSize++
p.mutex.Unlock()
}
var (

View File

@ -36,13 +36,11 @@ static int channels = 2; // Will be set from GetConfig().CGOChann
static int frame_size = 960; // Will be set from GetConfig().CGOFrameSize
static int max_packet_size = 1500; // Will be set from GetConfig().CGOMaxPacketSize
static int sleep_microseconds = 1000; // Will be set from GetConfig().CGOUsleepMicroseconds
static int max_attempts_global = 5; // Will be set from GetConfig().CGOMaxAttempts
static int max_backoff_us_global = 500000; // Will be set from GetConfig().CGOMaxBackoffMicroseconds
// Function to update constants from Go configuration
void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint,
int signal_type, int bandwidth, int dtx, int sr, int ch,
int fs, int max_pkt, int sleep_us, int max_attempts, int max_backoff) {
int fs, int max_pkt, int sleep_us) {
opus_bitrate = bitrate;
opus_complexity = complexity;
opus_vbr = vbr;
@ -55,8 +53,6 @@ void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constr
frame_size = fs;
max_packet_size = max_pkt;
sleep_microseconds = sleep_us;
max_attempts_global = max_attempts;
max_backoff_us_global = max_backoff;
}
// State tracking to prevent race conditions during rapid start/stop
@ -65,42 +61,15 @@ static volatile int capture_initialized = 0;
static volatile int playback_initializing = 0;
static volatile int playback_initialized = 0;
// Function to dynamically update Opus encoder parameters
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
int signal_type, int bandwidth, int dtx) {
if (!encoder || !capture_initialized) {
return -1; // Encoder not initialized
}
// Update the static variables
opus_bitrate = bitrate;
opus_complexity = complexity;
opus_vbr = vbr;
opus_vbr_constraint = vbr_constraint;
opus_signal_type = signal_type;
opus_bandwidth = bandwidth;
opus_dtx = dtx;
// Apply the new settings to the encoder
int result = 0;
result |= opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate));
result |= opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity));
result |= opus_encoder_ctl(encoder, OPUS_SET_VBR(opus_vbr));
result |= opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(opus_vbr_constraint));
result |= opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(opus_signal_type));
result |= opus_encoder_ctl(encoder, OPUS_SET_BANDWIDTH(opus_bandwidth));
result |= opus_encoder_ctl(encoder, OPUS_SET_DTX(opus_dtx));
return result; // 0 on success, non-zero on error
}
// Enhanced ALSA device opening with exponential backoff retry logic
static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream_t stream) {
int max_attempts = 5; // Increased from 3 to 5
int attempt = 0;
int err;
int backoff_us = sleep_microseconds; // Start with base sleep time
const int max_backoff_us = 500000; // Max 500ms backoff
while (attempt < max_attempts_global) {
while (attempt < max_attempts) {
err = snd_pcm_open(handle, device, stream, SND_PCM_NONBLOCK);
if (err >= 0) {
// Switch to blocking mode after successful open
@ -109,24 +78,24 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream
}
attempt++;
if (attempt >= max_attempts_global) break;
if (attempt >= max_attempts) break;
// Enhanced error handling with specific retry strategies
if (err == -EBUSY || err == -EAGAIN) {
// Device busy or temporarily unavailable - retry with backoff
usleep(backoff_us);
backoff_us = (backoff_us * 2 < max_backoff_us_global) ? backoff_us * 2 : max_backoff_us_global;
backoff_us = (backoff_us * 2 < max_backoff_us) ? backoff_us * 2 : max_backoff_us;
} else if (err == -ENODEV || err == -ENOENT) {
// Device not found - longer wait as device might be initializing
usleep(backoff_us * 2);
backoff_us = (backoff_us * 2 < max_backoff_us_global) ? backoff_us * 2 : max_backoff_us_global;
backoff_us = (backoff_us * 2 < max_backoff_us) ? backoff_us * 2 : max_backoff_us;
} else if (err == -EPERM || err == -EACCES) {
// Permission denied - shorter wait, likely persistent issue
usleep(backoff_us / 2);
} else {
// Other errors - standard backoff
usleep(backoff_us);
backoff_us = (backoff_us * 2 < max_backoff_us_global) ? backoff_us * 2 : max_backoff_us_global;
backoff_us = (backoff_us * 2 < max_backoff_us) ? backoff_us * 2 : max_backoff_us;
}
}
return err;
@ -265,9 +234,31 @@ int jetkvm_audio_init() {
return 0;
}
// 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)
// 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
int jetkvm_audio_read_encode(void *opus_buf) {
short pcm_buffer[1920]; // max 2ch*960
unsigned char *out = (unsigned char*)opus_buf;
@ -617,59 +608,29 @@ var (
errInvalidBufferPtr = errors.New("invalid buffer pointer")
)
// Error creation functions with enhanced context
// Error creation functions with context
func newBufferTooSmallError(actual, required int) error {
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",
})
return fmt.Errorf("buffer too small: got %d bytes, need at least %d bytes", actual, required)
}
func newBufferTooLargeError(actual, max int) error {
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",
})
return fmt.Errorf("buffer too large: got %d bytes, maximum allowed %d bytes", actual, max)
}
func newAudioInitError(cErrorCode int) error {
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",
})
return fmt.Errorf("%w: C error code %d", errAudioInitFailed, cErrorCode)
}
func newAudioPlaybackInitError(cErrorCode int) error {
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",
})
return fmt.Errorf("%w: C error code %d", errAudioPlaybackInit, cErrorCode)
}
func newAudioReadEncodeError(cErrorCode int) error {
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",
})
return fmt.Errorf("%w: C error code %d", errAudioReadEncode, cErrorCode)
}
func newAudioDecodeWriteError(cErrorCode int) error {
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",
})
return fmt.Errorf("%w: C error code %d", errAudioDecodeWrite, cErrorCode)
}
func cgoAudioInit() error {
@ -688,8 +649,6 @@ func cgoAudioInit() error {
C.int(config.CGOFrameSize),
C.int(config.CGOMaxPacketSize),
C.int(config.CGOUsleepMicroseconds),
C.int(config.CGOMaxAttempts),
C.int(config.CGOMaxBackoffMicroseconds),
)
result := C.jetkvm_audio_init()
@ -762,30 +721,12 @@ func cgoAudioDecodeWrite(buf []byte) (int, error) {
return int(n), nil
}
// updateOpusEncoderParams dynamically updates OPUS encoder parameters
func updateOpusEncoderParams(bitrate, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx int) error {
result := C.update_opus_encoder_params(
C.int(bitrate),
C.int(complexity),
C.int(vbr),
C.int(vbrConstraint),
C.int(signalType),
C.int(bandwidth),
C.int(dtx),
)
if result != 0 {
return fmt.Errorf("failed to update OPUS encoder parameters: C error code %d", result)
}
return nil
}
// CGO function aliases
var (
CGOAudioInit = cgoAudioInit
CGOAudioClose = cgoAudioClose
CGOAudioReadEncode = cgoAudioReadEncode
CGOAudioPlaybackInit = cgoAudioPlaybackInit
CGOAudioPlaybackClose = cgoAudioPlaybackClose
CGOAudioDecodeWrite = cgoAudioDecodeWrite
CGOUpdateOpusEncoderParams = updateOpusEncoderParams
CGOAudioInit = cgoAudioInit
CGOAudioClose = cgoAudioClose
CGOAudioReadEncode = cgoAudioReadEncode
CGOAudioPlaybackInit = cgoAudioPlaybackInit
CGOAudioPlaybackClose = cgoAudioPlaybackClose
CGOAudioDecodeWrite = cgoAudioDecodeWrite
)

View File

@ -1,85 +1,213 @@
package audio
import (
"time"
"github.com/jetkvm/kvm/internal/logging"
)
import "time"
// AudioConfigConstants centralizes all hardcoded values used across audio components.
// This configuration system allows runtime tuning of audio performance, quality, and resource usage.
// Each constant is documented with its purpose, usage location, and impact on system behavior.
type AudioConfigConstants struct {
// Audio Quality Presets
MaxAudioFrameSize int // Maximum audio frame size in bytes (default: 4096)
// MaxAudioFrameSize defines the maximum size of an audio frame in bytes.
// Used in: buffer_pool.go, adaptive_buffer.go
// Impact: Higher values allow larger audio chunks but increase memory usage and latency.
// Typical range: 1024-8192 bytes. Default 4096 provides good balance.
MaxAudioFrameSize int
// Opus Encoding Parameters
OpusBitrate int // Target bitrate for Opus encoding in bps (default: 128000)
OpusComplexity int // Computational complexity 0-10 (default: 10 for best quality)
OpusVBR int // Variable Bit Rate: 0=CBR, 1=VBR (default: 1)
OpusVBRConstraint int // VBR constraint: 0=unconstrained, 1=constrained (default: 0)
OpusDTX int // Discontinuous Transmission: 0=disabled, 1=enabled (default: 0)
// Opus Encoding Parameters - Core codec settings for audio compression
// OpusBitrate sets the target bitrate for Opus encoding in bits per second.
// Used in: cgo_audio.go for encoder initialization
// Impact: Higher bitrates improve audio quality but increase bandwidth usage.
// Range: 6000-510000 bps. 128000 (128kbps) provides high quality for most use cases.
OpusBitrate int
// Audio Parameters
SampleRate int // Audio sampling frequency in Hz (default: 48000)
Channels int // Number of audio channels: 1=mono, 2=stereo (default: 2)
FrameSize int // Samples per audio frame (default: 960 for 20ms at 48kHz)
MaxPacketSize int // Maximum encoded packet size in bytes (default: 4000)
// OpusComplexity controls the computational complexity of Opus encoding (0-10).
// Used in: cgo_audio.go for encoder configuration
// Impact: Higher values improve quality but increase CPU usage and encoding latency.
// Range: 0-10. Value 10 provides best quality, 0 fastest encoding.
OpusComplexity int
// Audio Quality Bitrates (kbps)
AudioQualityLowOutputBitrate int // Low-quality output bitrate (default: 32)
AudioQualityLowInputBitrate int // Low-quality input bitrate (default: 16)
AudioQualityMediumOutputBitrate int // Medium-quality output bitrate (default: 64)
AudioQualityMediumInputBitrate int // Medium-quality input bitrate (default: 32)
AudioQualityHighOutputBitrate int // High-quality output bitrate (default: 128)
AudioQualityHighInputBitrate int // High-quality input bitrate (default: 64)
AudioQualityUltraOutputBitrate int // Ultra-quality output bitrate (default: 192)
AudioQualityUltraInputBitrate int // Ultra-quality input bitrate (default: 96)
// OpusVBR enables Variable Bit Rate encoding (0=CBR, 1=VBR).
// Used in: cgo_audio.go for encoder mode selection
// Impact: VBR (1) adapts bitrate to content complexity, improving efficiency.
// CBR (0) maintains constant bitrate for predictable bandwidth usage.
OpusVBR int
// Audio Quality Sample Rates (Hz)
AudioQualityLowSampleRate int // Low-quality sample rate (default: 22050)
AudioQualityMediumSampleRate int // Medium-quality sample rate (default: 44100)
AudioQualityMicLowSampleRate int // Low-quality microphone sample rate (default: 16000)
// OpusVBRConstraint enables constrained VBR mode (0=unconstrained, 1=constrained).
// Used in: cgo_audio.go when VBR is enabled
// Impact: Constrained VBR (1) limits bitrate variation for more predictable bandwidth.
// Unconstrained (0) allows full bitrate adaptation for optimal quality.
OpusVBRConstraint int
// Audio Quality Frame Sizes
AudioQualityLowFrameSize time.Duration // Low-quality frame duration (default: 40ms)
AudioQualityMediumFrameSize time.Duration // Medium-quality frame duration (default: 20ms)
AudioQualityHighFrameSize time.Duration // High-quality frame duration (default: 20ms)
// OpusDTX enables Discontinuous Transmission (0=disabled, 1=enabled).
// Used in: cgo_audio.go for encoder optimization
// Impact: DTX (1) reduces bandwidth during silence but may cause audio artifacts.
// Disabled (0) maintains constant transmission for consistent quality.
OpusDTX int
AudioQualityUltraFrameSize time.Duration // Ultra-quality frame duration (default: 10ms)
// Audio Parameters - Fundamental audio stream characteristics
// SampleRate defines the number of audio samples per second in Hz.
// Used in: All audio processing components
// Impact: Higher rates improve frequency response but increase processing load.
// Common values: 16000 (voice), 44100 (CD quality), 48000 (professional).
SampleRate int
// Audio Quality Channels
AudioQualityLowChannels int // Low-quality channel count (default: 1)
AudioQualityMediumChannels int // Medium-quality channel count (default: 2)
AudioQualityHighChannels int // High-quality channel count (default: 2)
AudioQualityUltraChannels int // Ultra-quality channel count (default: 2)
// Channels specifies the number of audio channels (1=mono, 2=stereo).
// Used in: All audio processing and encoding/decoding operations
// Impact: Stereo (2) provides spatial audio but doubles bandwidth and processing.
// Mono (1) reduces resource usage but loses spatial information.
Channels int
// Audio Quality OPUS Encoder Parameters
AudioQualityLowOpusComplexity int // Low-quality OPUS complexity (default: 1)
AudioQualityLowOpusVBR int // Low-quality OPUS VBR setting (default: 0)
AudioQualityLowOpusSignalType int // Low-quality OPUS signal type (default: 3001)
AudioQualityLowOpusBandwidth int // Low-quality OPUS bandwidth (default: 1101)
AudioQualityLowOpusDTX int // Low-quality OPUS DTX setting (default: 1)
// FrameSize defines the number of samples per audio frame.
// Used in: Opus encoding/decoding, buffer management
// Impact: Larger frames reduce overhead but increase latency.
// Must match Opus frame sizes: 120, 240, 480, 960, 1920, 2880 samples.
FrameSize int
AudioQualityMediumOpusComplexity int // Medium-quality OPUS complexity (default: 5)
AudioQualityMediumOpusVBR int // Medium-quality OPUS VBR setting (default: 1)
AudioQualityMediumOpusSignalType int // Medium-quality OPUS signal type (default: 3002)
AudioQualityMediumOpusBandwidth int // Medium-quality OPUS bandwidth (default: 1103)
AudioQualityMediumOpusDTX int // Medium-quality OPUS DTX setting (default: 0)
// MaxPacketSize sets the maximum size of encoded audio packets in bytes.
// Used in: Network transmission, buffer allocation
// Impact: Larger packets reduce network overhead but increase burst bandwidth.
// Should accommodate worst-case Opus output plus protocol headers.
MaxPacketSize int
AudioQualityHighOpusComplexity int // High-quality OPUS complexity (default: 8)
AudioQualityHighOpusVBR int // High-quality OPUS VBR setting (default: 1)
AudioQualityHighOpusSignalType int // High-quality OPUS signal type (default: 3002)
AudioQualityHighOpusBandwidth int // High-quality OPUS bandwidth (default: 1104)
AudioQualityHighOpusDTX int // High-quality OPUS DTX setting (default: 0)
// Audio Quality Bitrates - Predefined quality presets for different use cases
// These bitrates are used in audio.go for quality level selection
// Impact: Higher bitrates improve audio fidelity but increase bandwidth usage
AudioQualityUltraOpusComplexity int // Ultra-quality OPUS complexity (default: 10)
AudioQualityUltraOpusVBR int // Ultra-quality OPUS VBR setting (default: 1)
AudioQualityUltraOpusSignalType int // Ultra-quality OPUS signal type (default: 3002)
AudioQualityUltraOpusBandwidth int // Ultra-quality OPUS bandwidth (default: 1105)
AudioQualityUltraOpusDTX int // Ultra-quality OPUS DTX setting (default: 0)
// AudioQualityLowOutputBitrate defines bitrate for low-quality audio output (kbps).
// Used in: audio.go for bandwidth-constrained scenarios
// Impact: Minimal bandwidth usage but reduced audio quality. Suitable for voice-only.
// Default 32kbps provides acceptable voice quality with very low bandwidth.
AudioQualityLowOutputBitrate int
// CGO Audio Constants
CGOOpusBitrate int // Native Opus encoder bitrate in bps (default: 96000)
// AudioQualityLowInputBitrate defines bitrate for low-quality audio input (kbps).
// Used in: audio.go for microphone input in low-bandwidth scenarios
// Impact: Reduces upload bandwidth but may affect voice clarity.
// Default 16kbps suitable for basic voice communication.
AudioQualityLowInputBitrate int
// AudioQualityMediumOutputBitrate defines bitrate for medium-quality audio output (kbps).
// Used in: audio.go for balanced quality/bandwidth scenarios
// Impact: Good balance between quality and bandwidth usage.
// Default 64kbps provides clear voice and acceptable music quality.
AudioQualityMediumOutputBitrate int
// AudioQualityMediumInputBitrate defines bitrate for medium-quality audio input (kbps).
// Used in: audio.go for microphone input with balanced quality
// Impact: Better voice quality than low setting with moderate bandwidth usage.
// Default 32kbps suitable for clear voice communication.
AudioQualityMediumInputBitrate int
// AudioQualityHighOutputBitrate defines bitrate for high-quality audio output (kbps).
// Used in: audio.go for high-fidelity audio scenarios
// Impact: Excellent audio quality but higher bandwidth requirements.
// Default 128kbps provides near-CD quality for music and crystal-clear voice.
AudioQualityHighOutputBitrate int
// AudioQualityHighInputBitrate defines bitrate for high-quality audio input (kbps).
// Used in: audio.go for high-quality microphone capture
// Impact: Superior voice quality but increased upload bandwidth usage.
// Default 64kbps suitable for professional voice communication.
AudioQualityHighInputBitrate int
// AudioQualityUltraOutputBitrate defines bitrate for ultra-high-quality audio output (kbps).
// Used in: audio.go for maximum quality scenarios
// Impact: Maximum audio fidelity but highest bandwidth consumption.
// Default 192kbps provides studio-quality audio for critical applications.
AudioQualityUltraOutputBitrate int
// AudioQualityUltraInputBitrate defines bitrate for ultra-high-quality audio input (kbps).
// Used in: audio.go for maximum quality microphone capture
// Impact: Best possible voice quality but maximum upload bandwidth usage.
// Default 96kbps suitable for broadcast-quality voice communication.
AudioQualityUltraInputBitrate int
// Audio Quality Sample Rates - Frequency sampling rates for different quality levels
// Used in: audio.go for configuring audio capture and playback sample rates
// Impact: Higher sample rates capture more frequency detail but increase processing load
// AudioQualityLowSampleRate defines sample rate for low-quality audio (Hz).
// Used in: audio.go for bandwidth-constrained scenarios
// Impact: Reduces frequency response but minimizes processing and bandwidth.
// Default 22050Hz captures frequencies up to 11kHz, adequate for voice.
AudioQualityLowSampleRate int
// AudioQualityMediumSampleRate defines sample rate for medium-quality audio (Hz).
// Used in: audio.go for balanced quality scenarios
// Impact: Good frequency response with moderate processing requirements.
// Default 44100Hz (CD quality) captures frequencies up to 22kHz.
AudioQualityMediumSampleRate int
// AudioQualityMicLowSampleRate defines sample rate for low-quality microphone input (Hz).
// Used in: audio.go for microphone capture in constrained scenarios
// Impact: Optimized for voice communication with minimal processing overhead.
// Default 16000Hz captures voice frequencies (300-3400Hz) efficiently.
AudioQualityMicLowSampleRate int
// Audio Quality Frame Sizes - Duration of audio frames for different quality levels
// Used in: audio.go for configuring Opus frame duration
// Impact: Larger frames reduce overhead but increase latency and memory usage
// AudioQualityLowFrameSize defines frame duration for low-quality audio.
// Used in: audio.go for low-latency scenarios with minimal processing
// Impact: Longer frames reduce CPU overhead but increase audio latency.
// Default 40ms provides good efficiency for voice communication.
AudioQualityLowFrameSize time.Duration
// AudioQualityMediumFrameSize defines frame duration for medium-quality audio.
// Used in: audio.go for balanced latency and efficiency
// Impact: Moderate frame size balances latency and processing efficiency.
// Default 20ms provides good balance for most applications.
AudioQualityMediumFrameSize time.Duration
// AudioQualityHighFrameSize defines frame duration for high-quality audio.
// Used in: audio.go for high-quality scenarios
// Impact: Optimized frame size for high-quality encoding efficiency.
// Default 20ms maintains low latency while supporting high bitrates.
AudioQualityHighFrameSize time.Duration
// AudioQualityUltraFrameSize defines frame duration for ultra-quality audio.
// Used in: audio.go for maximum quality scenarios
// Impact: Smaller frames reduce latency but increase processing overhead.
// Default 10ms provides minimal latency for real-time applications.
AudioQualityUltraFrameSize time.Duration
// Audio Quality Channels - Channel configuration for different quality levels
// Used in: audio.go for configuring mono/stereo audio
// Impact: Stereo doubles bandwidth and processing but provides spatial audio
// AudioQualityLowChannels defines channel count for low-quality audio.
// Used in: audio.go for bandwidth-constrained scenarios
// Impact: Mono (1) minimizes bandwidth and processing for voice communication.
// Default 1 (mono) suitable for voice-only applications.
AudioQualityLowChannels int
// AudioQualityMediumChannels defines channel count for medium-quality audio.
// Used in: audio.go for balanced quality scenarios
// Impact: Stereo (2) provides spatial audio with moderate bandwidth increase.
// Default 2 (stereo) suitable for general audio applications.
AudioQualityMediumChannels int
// AudioQualityHighChannels defines channel count for high-quality audio.
// Used in: audio.go for high-fidelity scenarios
// Impact: Stereo (2) essential for high-quality music and spatial audio.
// Default 2 (stereo) required for full audio experience.
AudioQualityHighChannels int
// AudioQualityUltraChannels defines channel count for ultra-quality audio.
// Used in: audio.go for maximum quality scenarios
// Impact: Stereo (2) mandatory for studio-quality audio reproduction.
// Default 2 (stereo) provides full spatial audio fidelity.
AudioQualityUltraChannels int
// CGO Audio Constants - Low-level C library configuration for audio processing
// These constants are passed to C code in cgo_audio.go for native audio operations
// Impact: Direct control over native audio library behavior and performance
// CGOOpusBitrate sets the bitrate for native Opus encoder (bits per second).
// Used in: cgo_audio.go update_audio_constants() function
// Impact: Controls quality vs bandwidth tradeoff in native encoding.
// Default 96000 (96kbps) provides good quality for real-time applications.
CGOOpusBitrate int
// CGOOpusComplexity sets computational complexity for native Opus encoder (0-10).
// Used in: cgo_audio.go for native encoder configuration
@ -1413,82 +1541,6 @@ type AudioConfigConstants struct {
// Default 8 channels provides reasonable upper bound for multi-channel audio.
MaxChannels int
// CGO Constants
// Used in: cgo_audio.go for CGO operation limits and retry logic
// Impact: Controls CGO retry behavior and backoff timing
// CGOMaxBackoffMicroseconds defines maximum backoff time in microseconds for CGO operations.
// Used in: safe_alsa_open for exponential backoff retry logic
// Impact: Prevents excessive wait times while allowing device recovery.
// Default 500000 microseconds (500ms) provides reasonable maximum wait time.
CGOMaxBackoffMicroseconds int
// CGOMaxAttempts defines maximum retry attempts for CGO operations.
// Used in: safe_alsa_open for retry limit enforcement
// Impact: Prevents infinite retry loops while allowing transient error recovery.
// Default 5 attempts provides good balance between reliability and performance.
CGOMaxAttempts int
// Validation Frame Size Limits
// Used in: validation_enhanced.go for frame duration validation
// Impact: Ensures frame sizes are within acceptable bounds for real-time audio
// MinFrameDuration defines minimum acceptable frame duration.
// Used in: ValidateAudioConfiguration for frame size validation
// Impact: Prevents excessively small frames that could impact performance.
// Default 10ms provides minimum viable frame duration for real-time audio.
MinFrameDuration time.Duration
// MaxFrameDuration defines maximum acceptable frame duration.
// Used in: ValidateAudioConfiguration for frame size validation
// Impact: Prevents excessively large frames that could impact latency.
// Default 100ms provides reasonable maximum frame duration.
MaxFrameDuration time.Duration
// Valid Sample Rates
// Used in: validation_enhanced.go for sample rate validation
// Impact: Defines the set of supported sample rates for audio processing
// ValidSampleRates defines the list of supported sample rates.
// Used in: ValidateAudioConfiguration for sample rate validation
// Impact: Ensures only supported sample rates are used in audio processing.
// Default rates support common audio standards from voice (8kHz) to professional (48kHz).
ValidSampleRates []int
// Opus Bitrate Validation Constants
// Used in: validation_enhanced.go for bitrate range validation
// Impact: Ensures bitrate values are within Opus codec specifications
// MinOpusBitrate defines the minimum valid Opus bitrate in bits per second.
// Used in: ValidateAudioConfiguration for bitrate validation
// Impact: Prevents bitrates below Opus codec minimum specification.
// Default 6000 bps is the minimum supported by Opus codec.
MinOpusBitrate int
// MaxOpusBitrate defines the maximum valid Opus bitrate in bits per second.
// Used in: ValidateAudioConfiguration for bitrate validation
// Impact: Prevents bitrates above Opus codec maximum specification.
// Default 510000 bps is the maximum supported by Opus codec.
MaxOpusBitrate int
// MaxValidationTime defines the maximum time allowed for validation operations.
// Used in: GetValidationConfig for timeout control
// Impact: Prevents validation operations from blocking indefinitely.
// Default 5s provides reasonable timeout for validation operations.
MaxValidationTime time.Duration
// MinFrameSize defines the minimum reasonable audio frame size in bytes.
// Used in: ValidateAudioFrameComprehensive for frame size validation
// Impact: Prevents processing of unreasonably small audio frames.
// Default 64 bytes ensures minimum viable audio data.
MinFrameSize int
// FrameSizeTolerance defines the tolerance for frame size validation in bytes.
// Used in: ValidateAudioFrameComprehensive for frame size matching
// Impact: Allows reasonable variation in frame sizes due to encoding.
// Default 512 bytes accommodates typical encoding variations.
FrameSizeTolerance int
// Device Health Monitoring Configuration
// Used in: device_health.go for proactive device monitoring and recovery
// Impact: Controls health check frequency and recovery thresholds
@ -1538,27 +1590,105 @@ type AudioConfigConstants struct {
// real-time audio requirements, and extensive testing for optimal performance.
func DefaultAudioConfig() *AudioConfigConstants {
return &AudioConfigConstants{
// Audio Quality Presets
// 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.
MaxAudioFrameSize: 4096,
// Opus Encoding Parameters
OpusBitrate: 128000,
OpusComplexity: 10,
OpusVBR: 1,
OpusVBRConstraint: 0,
OpusDTX: 0,
// 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
// Audio Parameters
SampleRate: 48000,
Channels: 2,
FrameSize: 960,
// 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.
OpusVBRConstraint: 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.
MaxPacketSize: 4000,
// Audio Quality Bitrates
AudioQualityLowOutputBitrate: 32,
AudioQualityLowInputBitrate: 16,
// 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.
AudioQualityMediumOutputBitrate: 64,
AudioQualityMediumInputBitrate: 32,
// 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,
// AudioQualityHighOutputBitrate defines bitrate for high-quality output.
// Used in: Professional applications requiring excellent audio fidelity
@ -1636,57 +1766,106 @@ func DefaultAudioConfig() *AudioConfigConstants {
// Audio Quality Channels - Channel configuration for different quality levels
// Used in: Audio processing pipeline for channel handling and bandwidth control
AudioQualityLowChannels: 1,
// 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.
AudioQualityMediumChannels: 2,
AudioQualityHighChannels: 2,
AudioQualityUltraChannels: 2,
// Audio Quality OPUS Encoder Parameters - Quality-specific encoder settings
// Used in: Dynamic OPUS encoder configuration based on quality presets
// Impact: Controls encoding complexity, VBR, signal type, bandwidth, and DTX
// 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,
// Low Quality OPUS Parameters - Optimized for bandwidth conservation
AudioQualityLowOpusComplexity: 1, // Low complexity for minimal CPU usage
AudioQualityLowOpusVBR: 0, // CBR for predictable bandwidth
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND
AudioQualityLowOpusDTX: 1, // Enable DTX for silence suppression
// 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,
// Medium Quality OPUS Parameters - Balanced performance and quality
AudioQualityMediumOpusComplexity: 5, // Medium complexity for balanced performance
AudioQualityMediumOpusVBR: 1, // VBR for better quality
AudioQualityMediumOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityMediumOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND
AudioQualityMediumOpusDTX: 0, // Disable DTX for consistent quality
// 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
// High Quality OPUS Parameters - High quality with good performance
AudioQualityHighOpusComplexity: 8, // High complexity for better quality
AudioQualityHighOpusVBR: 1, // VBR for optimal quality
AudioQualityHighOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityHighOpusBandwidth: 1104, // OPUS_BANDWIDTH_SUPERWIDEBAND
AudioQualityHighOpusDTX: 0, // Disable DTX for consistent quality
// 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,
// Ultra Quality OPUS Parameters - Maximum quality settings
AudioQualityUltraOpusComplexity: 10, // Maximum complexity for best quality
AudioQualityUltraOpusVBR: 1, // VBR for optimal quality
AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityUltraOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
AudioQualityUltraOpusDTX: 0, // Disable DTX for maximum quality
// 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,
// CGO Audio Constants
CGOOpusBitrate: 96000,
CGOOpusComplexity: 3,
CGOOpusVBR: 1,
// 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.
CGOOpusVBRConstraint: 1,
CGOOpusSignalType: 3, // OPUS_SIGNAL_MUSIC
CGOOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
CGOOpusDTX: 0,
CGOSampleRate: 48000,
CGOChannels: 2,
CGOFrameSize: 960,
CGOMaxPacketSize: 1500,
// Input IPC Constants
// 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
// InputIPCSampleRate defines sample rate for input IPC operations.
// Used in: Microphone input capture and processing
// Impact: Ensures high-quality input matching system audio output.
@ -2428,26 +2607,6 @@ func DefaultAudioConfig() *AudioConfigConstants {
MaxSampleRate: 48000, // 48kHz maximum sample rate
MaxChannels: 8, // 8 maximum audio channels
// CGO Constants
CGOMaxBackoffMicroseconds: 500000, // 500ms maximum backoff in microseconds
CGOMaxAttempts: 5, // 5 maximum retry attempts
// Validation Frame Size Limits
MinFrameDuration: 10 * time.Millisecond, // 10ms minimum frame duration
MaxFrameDuration: 100 * time.Millisecond, // 100ms maximum frame duration
// Valid Sample Rates
ValidSampleRates: []int{8000, 12000, 16000, 22050, 24000, 44100, 48000}, // Supported sample rates
// Opus Bitrate Validation Constants
MinOpusBitrate: 6000, // 6000 bps minimum Opus bitrate
MaxOpusBitrate: 510000, // 510000 bps maximum Opus bitrate
// Validation Configuration
MaxValidationTime: 5 * time.Second, // 5s maximum validation timeout
MinFrameSize: 1, // 1 byte minimum frame size (allow small frames)
FrameSizeTolerance: 512, // 512 bytes frame size tolerance
// Device Health Monitoring Configuration
HealthCheckIntervalMS: 5000, // 5000ms (5s) health check interval
HealthRecoveryThreshold: 3, // 3 consecutive successes for recovery
@ -2471,17 +2630,7 @@ var audioConfigInstance = DefaultAudioConfig()
// UpdateConfig allows runtime configuration updates
func UpdateConfig(newConfig *AudioConfigConstants) {
// Validate the new configuration before applying it
if err := ValidateAudioConfigConstants(newConfig); err != nil {
// Log validation error and keep current configuration
logger := logging.GetDefaultLogger().With().Str("component", "AudioConfig").Logger()
logger.Error().Err(err).Msg("Configuration validation failed, keeping current configuration")
return
}
audioConfigInstance = newConfig
logger := logging.GetDefaultLogger().With().Str("component", "AudioConfig").Logger()
logger.Info().Msg("Audio configuration updated successfully")
}
// GetConfig returns the current configuration

View File

@ -130,7 +130,7 @@ func (dhm *DeviceHealthMonitor) Start() error {
return fmt.Errorf("device health monitor already running")
}
dhm.logger.Debug().Msg("device health monitor starting")
dhm.logger.Info().Msg("starting device health monitor")
atomic.StoreInt32(&dhm.monitoringEnabled, 1)
go dhm.monitoringLoop()
@ -143,7 +143,7 @@ func (dhm *DeviceHealthMonitor) Stop() {
return
}
dhm.logger.Debug().Msg("device health monitor stopping")
dhm.logger.Info().Msg("stopping device health monitor")
atomic.StoreInt32(&dhm.monitoringEnabled, 0)
close(dhm.stopChan)
@ -152,7 +152,7 @@ func (dhm *DeviceHealthMonitor) Stop() {
// Wait for monitoring loop to finish
select {
case <-dhm.doneChan:
dhm.logger.Debug().Msg("device health monitor stopped")
dhm.logger.Info().Msg("device health monitor stopped")
case <-time.After(time.Duration(dhm.config.SupervisorTimeout)):
dhm.logger.Warn().Msg("device health monitor stop timeout")
}
@ -163,7 +163,7 @@ func (dhm *DeviceHealthMonitor) RegisterRecoveryCallback(component string, callb
dhm.callbackMutex.Lock()
defer dhm.callbackMutex.Unlock()
dhm.recoveryCallbacks[component] = callback
dhm.logger.Debug().Str("component", component).Msg("registered recovery callback")
dhm.logger.Info().Str("component", component).Msg("registered recovery callback")
}
// RecordError records an error for health tracking

View File

@ -144,7 +144,7 @@ func (aeb *AudioEventBroadcaster) Subscribe(connectionID string, conn *websocket
logger: logger,
}
aeb.logger.Debug().Str("connectionID", connectionID).Msg("audio events subscription added")
aeb.logger.Info().Str("connectionID", connectionID).Msg("audio events subscription added")
// Send initial state to new subscriber
go aeb.sendInitialState(connectionID)
@ -156,7 +156,7 @@ func (aeb *AudioEventBroadcaster) Unsubscribe(connectionID string) {
defer aeb.mutex.Unlock()
delete(aeb.subscribers, connectionID)
aeb.logger.Debug().Str("connectionID", connectionID).Msg("audio events subscription removed")
aeb.logger.Info().Str("connectionID", connectionID).Msg("audio events subscription removed")
}
// BroadcastAudioMuteChanged broadcasts audio mute state changes

View File

@ -1,6 +1,7 @@
package audio
import (
"sort"
"sync"
"sync/atomic"
"time"
@ -9,6 +10,24 @@ import (
"github.com/rs/zerolog"
)
// LatencyHistogram tracks latency distribution with percentile calculations
type LatencyHistogram struct {
// Atomic fields MUST be first for ARM32 alignment
sampleCount int64 // Total number of samples (atomic)
totalLatency int64 // Sum of all latencies in nanoseconds (atomic)
// Latency buckets for histogram (in nanoseconds)
buckets []int64 // Bucket boundaries
counts []int64 // Count for each bucket (atomic)
// Recent samples for percentile calculation
recentSamples []time.Duration
samplesMutex sync.RWMutex
maxSamples int
logger zerolog.Logger
}
// LatencyPercentiles holds calculated percentile values
type LatencyPercentiles struct {
P50 time.Duration `json:"p50"`
@ -40,6 +59,11 @@ type BufferPoolEfficiencyMetrics struct {
// GranularMetricsCollector aggregates all granular metrics
type GranularMetricsCollector struct {
// Latency histograms by source
inputLatencyHist *LatencyHistogram
outputLatencyHist *LatencyHistogram
processingLatencyHist *LatencyHistogram
// Buffer pool efficiency tracking
framePoolMetrics *BufferPoolEfficiencyTracker
controlPoolMetrics *BufferPoolEfficiencyTracker
@ -67,6 +91,92 @@ type BufferPoolEfficiencyTracker struct {
logger zerolog.Logger
}
// NewLatencyHistogram creates a new latency histogram with predefined buckets
func NewLatencyHistogram(maxSamples int, logger zerolog.Logger) *LatencyHistogram {
// Define latency buckets using configuration constants
buckets := []int64{
int64(1 * time.Millisecond),
int64(5 * time.Millisecond),
int64(GetConfig().LatencyBucket10ms),
int64(GetConfig().LatencyBucket25ms),
int64(GetConfig().LatencyBucket50ms),
int64(GetConfig().LatencyBucket100ms),
int64(GetConfig().LatencyBucket250ms),
int64(GetConfig().LatencyBucket500ms),
int64(GetConfig().LatencyBucket1s),
int64(GetConfig().LatencyBucket2s),
}
return &LatencyHistogram{
buckets: buckets,
counts: make([]int64, len(buckets)+1), // +1 for overflow bucket
recentSamples: make([]time.Duration, 0, maxSamples),
maxSamples: maxSamples,
logger: logger,
}
}
// RecordLatency adds a latency measurement to the histogram
func (lh *LatencyHistogram) RecordLatency(latency time.Duration) {
latencyNs := latency.Nanoseconds()
atomic.AddInt64(&lh.sampleCount, 1)
atomic.AddInt64(&lh.totalLatency, latencyNs)
// Find appropriate bucket
bucketIndex := len(lh.buckets) // Default to overflow bucket
for i, boundary := range lh.buckets {
if latencyNs <= boundary {
bucketIndex = i
break
}
}
atomic.AddInt64(&lh.counts[bucketIndex], 1)
// Store recent sample for percentile calculation
lh.samplesMutex.Lock()
if len(lh.recentSamples) >= lh.maxSamples {
// Remove oldest sample
lh.recentSamples = lh.recentSamples[1:]
}
lh.recentSamples = append(lh.recentSamples, latency)
lh.samplesMutex.Unlock()
}
// GetPercentiles calculates latency percentiles from recent samples
func (lh *LatencyHistogram) GetPercentiles() LatencyPercentiles {
lh.samplesMutex.RLock()
samples := make([]time.Duration, len(lh.recentSamples))
copy(samples, lh.recentSamples)
lh.samplesMutex.RUnlock()
if len(samples) == 0 {
return LatencyPercentiles{}
}
// Sort samples for percentile calculation
sort.Slice(samples, func(i, j int) bool {
return samples[i] < samples[j]
})
n := len(samples)
totalLatency := atomic.LoadInt64(&lh.totalLatency)
sampleCount := atomic.LoadInt64(&lh.sampleCount)
var avg time.Duration
if sampleCount > 0 {
avg = time.Duration(totalLatency / sampleCount)
}
return LatencyPercentiles{
P50: samples[n*50/100],
P95: samples[n*95/100],
P99: samples[n*99/100],
Min: samples[0],
Max: samples[n-1],
Avg: avg,
}
}
// NewBufferPoolEfficiencyTracker creates a new efficiency tracker
func NewBufferPoolEfficiencyTracker(poolName string, logger zerolog.Logger) *BufferPoolEfficiencyTracker {
return &BufferPoolEfficiencyTracker{
@ -164,14 +274,34 @@ func (bpet *BufferPoolEfficiencyTracker) GetEfficiencyMetrics() BufferPoolEffici
// NewGranularMetricsCollector creates a new granular metrics collector
func NewGranularMetricsCollector(logger zerolog.Logger) *GranularMetricsCollector {
maxSamples := GetConfig().LatencyHistorySize
return &GranularMetricsCollector{
framePoolMetrics: NewBufferPoolEfficiencyTracker("frame_pool", logger.With().Str("pool", "frame").Logger()),
controlPoolMetrics: NewBufferPoolEfficiencyTracker("control_pool", logger.With().Str("pool", "control").Logger()),
zeroCopyMetrics: NewBufferPoolEfficiencyTracker("zero_copy_pool", logger.With().Str("pool", "zero_copy").Logger()),
logger: logger,
inputLatencyHist: NewLatencyHistogram(maxSamples, logger.With().Str("histogram", "input").Logger()),
outputLatencyHist: NewLatencyHistogram(maxSamples, logger.With().Str("histogram", "output").Logger()),
processingLatencyHist: NewLatencyHistogram(maxSamples, logger.With().Str("histogram", "processing").Logger()),
framePoolMetrics: NewBufferPoolEfficiencyTracker("frame_pool", logger.With().Str("pool", "frame").Logger()),
controlPoolMetrics: NewBufferPoolEfficiencyTracker("control_pool", logger.With().Str("pool", "control").Logger()),
zeroCopyMetrics: NewBufferPoolEfficiencyTracker("zero_copy_pool", logger.With().Str("pool", "zero_copy").Logger()),
logger: logger,
}
}
// RecordInputLatency records latency for input operations
func (gmc *GranularMetricsCollector) RecordInputLatency(latency time.Duration) {
gmc.inputLatencyHist.RecordLatency(latency)
}
// RecordOutputLatency records latency for output operations
func (gmc *GranularMetricsCollector) RecordOutputLatency(latency time.Duration) {
gmc.outputLatencyHist.RecordLatency(latency)
}
// RecordProcessingLatency records latency for processing operations
func (gmc *GranularMetricsCollector) RecordProcessingLatency(latency time.Duration) {
gmc.processingLatencyHist.RecordLatency(latency)
}
// RecordFramePoolOperation records frame pool operations
func (gmc *GranularMetricsCollector) RecordFramePoolGet(latency time.Duration, wasHit bool) {
gmc.framePoolMetrics.RecordGetOperation(latency, wasHit)
@ -199,6 +329,18 @@ func (gmc *GranularMetricsCollector) RecordZeroCopyPut(latency time.Duration, bu
gmc.zeroCopyMetrics.RecordPutOperation(latency, bufferSize)
}
// GetLatencyPercentiles returns percentiles for all latency types
func (gmc *GranularMetricsCollector) GetLatencyPercentiles() map[string]LatencyPercentiles {
gmc.mutex.RLock()
defer gmc.mutex.RUnlock()
return map[string]LatencyPercentiles{
"input": gmc.inputLatencyHist.GetPercentiles(),
"output": gmc.outputLatencyHist.GetPercentiles(),
"processing": gmc.processingLatencyHist.GetPercentiles(),
}
}
// GetBufferPoolEfficiency returns efficiency metrics for all buffer pools
func (gmc *GranularMetricsCollector) GetBufferPoolEfficiency() map[string]BufferPoolEfficiencyMetrics {
gmc.mutex.RLock()
@ -213,8 +355,22 @@ func (gmc *GranularMetricsCollector) GetBufferPoolEfficiency() map[string]Buffer
// LogGranularMetrics logs comprehensive granular metrics
func (gmc *GranularMetricsCollector) LogGranularMetrics() {
latencyPercentiles := gmc.GetLatencyPercentiles()
bufferEfficiency := gmc.GetBufferPoolEfficiency()
// Log latency percentiles
for source, percentiles := range latencyPercentiles {
gmc.logger.Info().
Str("source", source).
Dur("p50", percentiles.P50).
Dur("p95", percentiles.P95).
Dur("p99", percentiles.P99).
Dur("min", percentiles.Min).
Dur("max", percentiles.Max).
Dur("avg", percentiles.Avg).
Msg("Latency percentiles")
}
// Log buffer pool efficiency
for poolName, efficiency := range bufferEfficiency {
gmc.logger.Info().

View File

@ -1,100 +0,0 @@
package audio
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestGranularMetricsCollector tests the GranularMetricsCollector functionality
func TestGranularMetricsCollector(t *testing.T) {
tests := []struct {
name string
testFunc func(t *testing.T)
}{
{"GetGranularMetricsCollector", testGetGranularMetricsCollector},
{"ConcurrentCollectorAccess", testConcurrentCollectorAccess},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.testFunc(t)
})
}
}
// testGetGranularMetricsCollector tests singleton behavior
func testGetGranularMetricsCollector(t *testing.T) {
collector1 := GetGranularMetricsCollector()
collector2 := GetGranularMetricsCollector()
require.NotNil(t, collector1)
require.NotNil(t, collector2)
assert.Same(t, collector1, collector2, "Should return the same singleton instance")
}
// testConcurrentCollectorAccess tests thread safety of the collector
func testConcurrentCollectorAccess(t *testing.T) {
collector := GetGranularMetricsCollector()
require.NotNil(t, collector)
const numGoroutines = 10
const operationsPerGoroutine = 50
var wg sync.WaitGroup
wg.Add(numGoroutines)
// Concurrent buffer pool operations
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
// Test buffer pool operations
latency := time.Duration(id*operationsPerGoroutine+j) * time.Microsecond
collector.RecordFramePoolGet(latency, true)
collector.RecordFramePoolPut(latency, 1024)
}
}(i)
}
wg.Wait()
// Verify collector is still functional
efficiency := collector.GetBufferPoolEfficiency()
assert.NotNil(t, efficiency)
}
func BenchmarkGranularMetricsCollector(b *testing.B) {
collector := GetGranularMetricsCollector()
b.Run("RecordFramePoolGet", func(b *testing.B) {
latency := 5 * time.Millisecond
b.ResetTimer()
for i := 0; i < b.N; i++ {
collector.RecordFramePoolGet(latency, true)
}
})
b.Run("RecordFramePoolPut", func(b *testing.B) {
latency := 5 * time.Millisecond
b.ResetTimer()
for i := 0; i < b.N; i++ {
collector.RecordFramePoolPut(latency, 1024)
}
})
b.Run("GetBufferPoolEfficiency", func(b *testing.B) {
// Pre-populate with some data
for i := 0; i < 100; i++ {
collector.RecordFramePoolGet(time.Duration(i)*time.Microsecond, true)
collector.RecordFramePoolPut(time.Duration(i)*time.Microsecond, 1024)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = collector.GetBufferPoolEfficiency()
}
})
}

View File

@ -6,75 +6,79 @@ import (
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// 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
FramesSent int64 // Total frames sent
FramesDropped int64 // Total frames dropped
BytesProcessed int64 // Total bytes processed
ConnectionDrops int64 // Connection drops
AverageLatency time.Duration // time.Duration is int64
LastFrameTime time.Time
}
// AudioInputManager manages microphone input stream using IPC mode only
type AudioInputManager struct {
*BaseAudioManager
metrics AudioInputMetrics
ipcManager *AudioInputIPCManager
framesSent int64 // Input-specific metric
logger zerolog.Logger
running int32
}
// NewAudioInputManager creates a new audio input manager
// NewAudioInputManager creates a new audio input manager (IPC mode only)
func NewAudioInputManager() *AudioInputManager {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputManagerComponent).Logger()
return &AudioInputManager{
BaseAudioManager: NewBaseAudioManager(logger),
ipcManager: NewAudioInputIPCManager(),
ipcManager: NewAudioInputIPCManager(),
logger: logging.GetDefaultLogger().With().Str("component", AudioInputManagerComponent).Logger(),
}
}
// Start begins processing microphone input
func (aim *AudioInputManager) Start() error {
if !aim.setRunning(true) {
if !atomic.CompareAndSwapInt32(&aim.running, 0, 1) {
return fmt.Errorf("audio input manager is already running")
}
aim.logComponentStart(AudioInputManagerComponent)
aim.logger.Info().Str("component", AudioInputManagerComponent).Msg("starting component")
// Start the IPC-based audio input
err := aim.ipcManager.Start()
if err != nil {
aim.logComponentError(AudioInputManagerComponent, err, "failed to start component")
aim.logger.Error().Err(err).Str("component", AudioInputManagerComponent).Msg("failed to start component")
// Ensure proper cleanup on error
aim.setRunning(false)
atomic.StoreInt32(&aim.running, 0)
// Reset metrics on failed start
aim.resetMetrics()
return err
}
aim.logComponentStarted(AudioInputManagerComponent)
aim.logger.Info().Str("component", AudioInputManagerComponent).Msg("component started successfully")
return nil
}
// Stop stops processing microphone input
func (aim *AudioInputManager) Stop() {
if !aim.setRunning(false) {
if !atomic.CompareAndSwapInt32(&aim.running, 1, 0) {
return // Already stopped
}
aim.logComponentStop(AudioInputManagerComponent)
aim.logger.Info().Str("component", AudioInputManagerComponent).Msg("stopping component")
// Stop the IPC-based audio input
aim.ipcManager.Stop()
aim.logComponentStopped(AudioInputManagerComponent)
aim.logger.Info().Str("component", AudioInputManagerComponent).Msg("component stopped")
}
// resetMetrics resets all metrics to zero
func (aim *AudioInputManager) resetMetrics() {
aim.BaseAudioManager.resetMetrics()
atomic.StoreInt64(&aim.framesSent, 0)
atomic.StoreInt64(&aim.metrics.FramesSent, 0)
atomic.StoreInt64(&aim.metrics.FramesDropped, 0)
atomic.StoreInt64(&aim.metrics.BytesProcessed, 0)
atomic.StoreInt64(&aim.metrics.ConnectionDrops, 0)
}
// WriteOpusFrame writes an Opus frame to the audio input system with latency tracking
@ -83,12 +87,6 @@ func (aim *AudioInputManager) WriteOpusFrame(frame []byte) error {
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)
@ -107,10 +105,10 @@ func (aim *AudioInputManager) WriteOpusFrame(frame []byte) error {
}
// Update metrics
atomic.AddInt64(&aim.framesSent, 1)
aim.recordFrameProcessed(len(frame))
aim.updateLatency(processingTime)
atomic.AddInt64(&aim.metrics.FramesSent, 1)
atomic.AddInt64(&aim.metrics.BytesProcessed, int64(len(frame)))
aim.metrics.LastFrameTime = time.Now()
aim.metrics.AverageLatency = processingTime
return nil
}
@ -143,18 +141,21 @@ func (aim *AudioInputManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFrame)
}
// Update metrics
atomic.AddInt64(&aim.framesSent, 1)
aim.recordFrameProcessed(frame.Length())
aim.updateLatency(processingTime)
atomic.AddInt64(&aim.metrics.FramesSent, 1)
atomic.AddInt64(&aim.metrics.BytesProcessed, int64(frame.Length()))
aim.metrics.LastFrameTime = time.Now()
aim.metrics.AverageLatency = processingTime
return nil
}
// GetMetrics returns current metrics
// GetMetrics returns current audio input metrics
func (aim *AudioInputManager) GetMetrics() AudioInputMetrics {
return AudioInputMetrics{
FramesSent: atomic.LoadInt64(&aim.framesSent),
BaseAudioMetrics: aim.getBaseMetrics(),
FramesSent: atomic.LoadInt64(&aim.metrics.FramesSent),
FramesDropped: atomic.LoadInt64(&aim.metrics.FramesDropped),
BytesProcessed: atomic.LoadInt64(&aim.metrics.BytesProcessed),
AverageLatency: aim.metrics.AverageLatency,
LastFrameTime: aim.metrics.LastFrameTime,
}
}
@ -208,7 +209,10 @@ func (aim *AudioInputManager) LogPerformanceStats() {
Msg("Audio input performance metrics")
}
// Note: IsRunning() is inherited from BaseAudioManager
// IsRunning returns whether the audio input manager is running
func (aim *AudioInputManager) IsRunning() bool {
return atomic.LoadInt32(&aim.running) == 1
}
// 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

View File

@ -135,9 +135,6 @@ 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()
@ -146,16 +143,9 @@ 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 with exact capacity
// Pool exhausted, create new message
atomic.AddInt64(&mp.missCount, 1)
return &OptimizedIPCMessage{
data: make([]byte, 0, maxFrameSize),
@ -165,15 +155,6 @@ 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{}
@ -320,8 +301,8 @@ func (ais *AudioInputServer) acceptConnections() {
if err != nil {
if ais.running {
// Log error and continue accepting
logger := logging.GetDefaultLogger().With().Str("component", "audio-input").Logger()
logger.Warn().Err(err).Msg("failed to accept connection, retrying")
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
logger.Warn().Err(err).Msg("Failed to accept connection, retrying")
continue
}
return
@ -330,8 +311,8 @@ func (ais *AudioInputServer) acceptConnections() {
// Configure socket buffers for optimal performance
if err := ConfigureSocketBuffers(conn, ais.socketBufferConfig); err != nil {
// Log warning but don't fail - socket buffer optimization is not critical
logger := logging.GetDefaultLogger().With().Str("component", "audio-input").Logger()
logger.Warn().Err(err).Msg("failed to configure socket buffers, using defaults")
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
logger.Warn().Err(err).Msg("Failed to configure socket buffers, continuing with defaults")
} else {
// Record socket buffer metrics for monitoring
RecordSocketBufferMetrics(conn, "audio-input")
@ -477,13 +458,6 @@ func (ais *AudioInputServer) processOpusFrame(data []byte) error {
return nil // Empty frame, ignore
}
// Use ultra-fast validation for critical audio path
if err := ValidateAudioFrame(data); err != nil {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputServerComponent).Logger()
logger.Error().Err(err).Msg("Frame validation failed")
return fmt.Errorf("input frame validation failed: %w", err)
}
// Process the Opus frame using CGO
_, err := CGOAudioDecodeWrite(data)
return err
@ -491,18 +465,6 @@ func (ais *AudioInputServer) processOpusFrame(data []byte) error {
// processConfig processes a configuration update
func (ais *AudioInputServer) processConfig(data []byte) error {
// Validate configuration data
if len(data) == 0 {
return fmt.Errorf("empty configuration data")
}
// Basic validation for configuration size
if err := ValidateBufferSize(len(data)); err != nil {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputServerComponent).Logger()
logger.Error().Err(err).Msg("Configuration buffer validation failed")
return fmt.Errorf("configuration validation failed: %w", err)
}
// Acknowledge configuration receipt
return ais.sendAck()
}
@ -634,13 +596,6 @@ func (aic *AudioInputClient) SendFrame(frame []byte) error {
return nil // Empty frame, ignore
}
// Validate frame data before sending
if err := ValidateAudioFrame(frame); err != nil {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
logger.Error().Err(err).Msg("Frame validation failed")
return fmt.Errorf("input frame validation failed: %w", err)
}
if len(frame) > maxFrameSize {
return fmt.Errorf("frame too large: got %d bytes, maximum allowed %d bytes", len(frame), maxFrameSize)
}
@ -669,13 +624,6 @@ func (aic *AudioInputClient) SendFrameZeroCopy(frame *ZeroCopyAudioFrame) error
return nil // Empty frame, ignore
}
// Validate zero-copy frame before sending
if err := ValidateZeroCopyFrame(frame); err != nil {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
logger.Error().Err(err).Msg("Zero-copy frame validation failed")
return fmt.Errorf("input frame validation failed: %w", err)
}
if frame.Length() > maxFrameSize {
return fmt.Errorf("frame too large: got %d bytes, maximum allowed %d bytes", frame.Length(), maxFrameSize)
}
@ -701,13 +649,6 @@ func (aic *AudioInputClient) SendConfig(config InputIPCConfig) error {
return fmt.Errorf("not connected to audio input server")
}
// Validate configuration parameters
if err := ValidateInputIPCConfig(config.SampleRate, config.Channels, config.FrameSize); err != nil {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
logger.Error().Err(err).Msg("Configuration validation failed")
return fmt.Errorf("input configuration validation failed: %w", err)
}
// Serialize config (simple binary format)
data := make([]byte, 12) // 3 * int32
binary.LittleEndian.PutUint32(data[0:4], uint32(config.SampleRate))
@ -794,7 +735,7 @@ func (ais *AudioInputServer) startReaderGoroutine() {
baseBackoffDelay := GetConfig().RetryDelay
maxBackoffDelay := GetConfig().MaxRetryDelay
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-reader").Logger()
for {
select {
@ -879,7 +820,7 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
defer runtime.UnlockOSThread()
// Set high priority for audio processing
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-processor").Logger()
if err := SetAudioThreadPriority(); err != nil {
logger.Warn().Err(err).Msg("Failed to set audio processing priority")
}
@ -996,7 +937,7 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
defer runtime.UnlockOSThread()
// Set I/O priority for monitoring
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-monitor").Logger()
if err := SetAudioIOThreadPriority(); err != nil {
logger.Warn().Err(err).Msg("Failed to set audio I/O priority")
}

View File

@ -31,7 +31,7 @@ func (aim *AudioInputIPCManager) Start() error {
return nil
}
aim.logger.Debug().Str("component", AudioInputIPCComponent).Msg("starting component")
aim.logger.Info().Str("component", AudioInputIPCComponent).Msg("starting component")
err := aim.supervisor.Start()
if err != nil {
@ -49,17 +49,6 @@ func (aim *AudioInputIPCManager) Start() error {
FrameSize: GetConfig().InputIPCFrameSize,
}
// Validate configuration before using it
if err := ValidateInputIPCConfig(config.SampleRate, config.Channels, config.FrameSize); err != nil {
aim.logger.Warn().Err(err).Msg("invalid input IPC config from constants, using defaults")
// Use safe defaults if config validation fails
config = InputIPCConfig{
SampleRate: 48000,
Channels: 2,
FrameSize: 960,
}
}
// Wait for subprocess readiness
time.Sleep(GetConfig().LongSleepDuration)
@ -69,7 +58,7 @@ func (aim *AudioInputIPCManager) Start() error {
aim.logger.Warn().Err(err).Str("component", AudioInputIPCComponent).Msg("failed to send initial config, will retry later")
}
aim.logger.Debug().Str("component", AudioInputIPCComponent).Msg("component started successfully")
aim.logger.Info().Str("component", AudioInputIPCComponent).Msg("component started successfully")
return nil
}
@ -79,9 +68,9 @@ func (aim *AudioInputIPCManager) Stop() {
return
}
aim.logger.Debug().Str("component", AudioInputIPCComponent).Msg("stopping component")
aim.logger.Info().Str("component", AudioInputIPCComponent).Msg("stopping component")
aim.supervisor.Stop()
aim.logger.Debug().Str("component", AudioInputIPCComponent).Msg("component stopped")
aim.logger.Info().Str("component", AudioInputIPCComponent).Msg("component stopped")
}
// resetMetrics resets all metrics to zero
@ -102,13 +91,6 @@ func (aim *AudioInputIPCManager) WriteOpusFrame(frame []byte) error {
return nil // Empty frame, ignore
}
// Validate frame data
if err := ValidateAudioFrame(frame); err != nil {
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
aim.logger.Debug().Err(err).Msg("invalid frame data")
return err
}
// Start latency measurement
startTime := time.Now()
@ -122,7 +104,7 @@ func (aim *AudioInputIPCManager) WriteOpusFrame(frame []byte) error {
if err != nil {
// Count as dropped frame
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
aim.logger.Debug().Err(err).Msg("failed to send frame via IPC")
aim.logger.Debug().Err(err).Msg("Failed to send frame via IPC")
return err
}
@ -143,13 +125,6 @@ func (aim *AudioInputIPCManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFram
return nil // Empty frame, ignore
}
// Validate zero-copy frame
if err := ValidateZeroCopyFrame(frame); err != nil {
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
aim.logger.Debug().Err(err).Msg("invalid zero-copy frame")
return err
}
// Start latency measurement
startTime := time.Now()
@ -163,7 +138,7 @@ func (aim *AudioInputIPCManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFram
if err != nil {
// Count as dropped frame
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
aim.logger.Debug().Err(err).Msg("failed to send zero-copy frame via IPC")
aim.logger.Debug().Err(err).Msg("Failed to send zero-copy frame via IPC")
return err
}
@ -191,15 +166,12 @@ func (aim *AudioInputIPCManager) IsReady() bool {
// GetMetrics returns current metrics
func (aim *AudioInputIPCManager) GetMetrics() AudioInputMetrics {
return AudioInputMetrics{
FramesSent: atomic.LoadInt64(&aim.metrics.FramesSent),
BaseAudioMetrics: BaseAudioMetrics{
FramesProcessed: atomic.LoadInt64(&aim.metrics.FramesProcessed),
FramesDropped: atomic.LoadInt64(&aim.metrics.FramesDropped),
BytesProcessed: atomic.LoadInt64(&aim.metrics.BytesProcessed),
ConnectionDrops: atomic.LoadInt64(&aim.metrics.ConnectionDrops),
AverageLatency: aim.metrics.AverageLatency,
LastFrameTime: aim.metrics.LastFrameTime,
},
FramesSent: atomic.LoadInt64(&aim.metrics.FramesSent),
FramesDropped: atomic.LoadInt64(&aim.metrics.FramesDropped),
BytesProcessed: atomic.LoadInt64(&aim.metrics.BytesProcessed),
ConnectionDrops: atomic.LoadInt64(&aim.metrics.ConnectionDrops),
AverageLatency: aim.metrics.AverageLatency,
LastFrameTime: aim.metrics.LastFrameTime,
}
}

View File

@ -14,10 +14,7 @@ import (
// This should be called from main() when the subprocess is detected
func RunAudioInputServer() error {
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
logger.Debug().Msg("audio input server subprocess starting")
// Initialize validation cache for optimal performance
InitValidationCache()
logger.Info().Msg("Starting audio input server subprocess")
// Start adaptive buffer management for optimal performance
StartAdaptiveBuffering()
@ -26,7 +23,7 @@ func RunAudioInputServer() error {
// Initialize CGO audio system
err := CGOAudioPlaybackInit()
if err != nil {
logger.Error().Err(err).Msg("failed to initialize CGO audio playback")
logger.Error().Err(err).Msg("Failed to initialize CGO audio playback")
return err
}
defer CGOAudioPlaybackClose()
@ -34,18 +31,18 @@ func RunAudioInputServer() error {
// Create and start the IPC server
server, err := NewAudioInputServer()
if err != nil {
logger.Error().Err(err).Msg("failed to create audio input server")
logger.Error().Err(err).Msg("Failed to create audio input server")
return err
}
defer server.Close()
err = server.Start()
if err != nil {
logger.Error().Err(err).Msg("failed to start audio input server")
logger.Error().Err(err).Msg("Failed to start audio input server")
return err
}
logger.Debug().Msg("audio input server started, waiting for connections")
logger.Info().Msg("Audio input server started, waiting for connections")
// Set up signal handling for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
@ -57,18 +54,18 @@ func RunAudioInputServer() error {
// Wait for shutdown signal
select {
case sig := <-sigChan:
logger.Info().Str("signal", sig.String()).Msg("received shutdown signal")
logger.Info().Str("signal", sig.String()).Msg("Received shutdown signal")
case <-ctx.Done():
logger.Debug().Msg("context cancelled")
logger.Info().Msg("Context cancelled")
}
// Graceful shutdown
logger.Debug().Msg("shutting down audio input server")
logger.Info().Msg("Shutting down audio input server")
server.Stop()
// Give some time for cleanup
time.Sleep(GetConfig().DefaultSleepDuration)
logger.Debug().Msg("audio input server subprocess stopped")
logger.Info().Msg("Audio input server subprocess stopped")
return nil
}

View File

@ -1,38 +1,50 @@
package audio
import (
"context"
"fmt"
"os"
"os/exec"
"sync"
"syscall"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// AudioInputSupervisor manages the audio input server subprocess
type AudioInputSupervisor struct {
*BaseSupervisor
client *AudioInputClient
cmd *exec.Cmd
cancel context.CancelFunc
mtx sync.Mutex
running bool
logger zerolog.Logger
client *AudioInputClient
processMonitor *ProcessMonitor
}
// NewAudioInputSupervisor creates a new audio input supervisor
func NewAudioInputSupervisor() *AudioInputSupervisor {
return &AudioInputSupervisor{
BaseSupervisor: NewBaseSupervisor("audio-input-supervisor"),
logger: logging.GetDefaultLogger().With().Str("component", "audio-input-supervisor").Logger(),
client: NewAudioInputClient(),
processMonitor: GetProcessMonitor(),
}
}
// Start starts the audio input server subprocess
func (ais *AudioInputSupervisor) Start() error {
ais.mutex.Lock()
defer ais.mutex.Unlock()
ais.mtx.Lock()
defer ais.mtx.Unlock()
if ais.IsRunning() {
if ais.running {
return fmt.Errorf("audio input supervisor already running with PID %d", ais.cmd.Process.Pid)
}
// Create context for subprocess management
ais.createContext()
ctx, cancel := context.WithCancel(context.Background())
ais.cancel = cancel
// Get current executable path
execPath, err := os.Executable()
@ -41,7 +53,7 @@ func (ais *AudioInputSupervisor) Start() error {
}
// Create command for audio input server subprocess
cmd := exec.CommandContext(ais.ctx, execPath, "--audio-input-server")
cmd := exec.CommandContext(ctx, execPath, "--audio-input-server")
cmd.Env = append(os.Environ(),
"JETKVM_AUDIO_INPUT_IPC=true", // Enable IPC mode
)
@ -52,13 +64,13 @@ func (ais *AudioInputSupervisor) Start() error {
}
ais.cmd = cmd
ais.setRunning(true)
ais.running = true
// Start the subprocess
err = cmd.Start()
if err != nil {
ais.setRunning(false)
ais.cancelContext()
ais.running = false
cancel()
return fmt.Errorf("failed to start audio input server process: %w", err)
}
@ -78,14 +90,14 @@ func (ais *AudioInputSupervisor) Start() error {
// Stop stops the audio input server subprocess
func (ais *AudioInputSupervisor) Stop() {
ais.mutex.Lock()
defer ais.mutex.Unlock()
ais.mtx.Lock()
defer ais.mtx.Unlock()
if !ais.IsRunning() {
if !ais.running {
return
}
ais.logSupervisorStop()
ais.running = false
// Disconnect client first
if ais.client != nil {
@ -93,7 +105,9 @@ func (ais *AudioInputSupervisor) Stop() {
}
// Cancel context to signal subprocess to stop
ais.cancelContext()
if ais.cancel != nil {
ais.cancel()
}
// Try graceful termination first
if ais.cmd != nil && ais.cmd.Process != nil {
@ -124,14 +138,19 @@ func (ais *AudioInputSupervisor) Stop() {
}
}
ais.setRunning(false)
ais.cmd = nil
ais.cancel = nil
}
// IsRunning returns whether the supervisor is running
func (ais *AudioInputSupervisor) IsRunning() bool {
ais.mtx.Lock()
defer ais.mtx.Unlock()
return ais.running
}
// IsConnected returns whether the client is connected to the audio input server
func (ais *AudioInputSupervisor) IsConnected() bool {
ais.mutex.Lock()
defer ais.mutex.Unlock()
if !ais.IsRunning() {
return false
}
@ -143,11 +162,41 @@ func (ais *AudioInputSupervisor) GetClient() *AudioInputClient {
return ais.client
}
// GetProcessMetrics returns current process metrics with audio-input-server name
// GetProcessMetrics returns current process metrics if the process is running
func (ais *AudioInputSupervisor) GetProcessMetrics() *ProcessMetrics {
metrics := ais.BaseSupervisor.GetProcessMetrics()
metrics.ProcessName = "audio-input-server"
return metrics
ais.mtx.Lock()
defer ais.mtx.Unlock()
if ais.cmd == nil || ais.cmd.Process == nil {
// Return default metrics when no process is running
return &ProcessMetrics{
PID: 0,
CPUPercent: 0.0,
MemoryRSS: 0,
MemoryVMS: 0,
MemoryPercent: 0.0,
Timestamp: time.Now(),
ProcessName: "audio-input-server",
}
}
pid := ais.cmd.Process.Pid
metrics := ais.processMonitor.GetCurrentMetrics()
for _, metric := range metrics {
if metric.PID == pid {
return &metric
}
}
// Return default metrics if process not found in monitoring
return &ProcessMetrics{
PID: pid,
CPUPercent: 0.0,
MemoryRSS: 0,
MemoryVMS: 0,
MemoryPercent: 0.0,
Timestamp: time.Now(),
ProcessName: "audio-input-server",
}
}
// monitorSubprocess monitors the subprocess and handles unexpected exits
@ -162,10 +211,10 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
// Remove process from monitoring
ais.processMonitor.RemoveProcess(pid)
ais.mutex.Lock()
defer ais.mutex.Unlock()
ais.mtx.Lock()
defer ais.mtx.Unlock()
if ais.IsRunning() {
if ais.running {
// Unexpected exit
if err != nil {
ais.logger.Error().Err(err).Int("pid", pid).Msg("Audio input server subprocess exited unexpectedly")
@ -179,7 +228,7 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
}
// Mark as not running
ais.setRunning(false)
ais.running = false
ais.cmd = nil
ais.logger.Info().Int("pid", pid).Msg("Audio input server subprocess monitoring stopped")

View File

@ -181,15 +181,12 @@ func TestAudioInputManagerMultipleStartStop(t *testing.T) {
func TestAudioInputMetrics(t *testing.T) {
metrics := &AudioInputMetrics{
BaseAudioMetrics: BaseAudioMetrics{
FramesProcessed: 100,
FramesDropped: 5,
BytesProcessed: 1024,
ConnectionDrops: 2,
AverageLatency: time.Millisecond * 10,
LastFrameTime: time.Now(),
},
FramesSent: 100,
FramesSent: 100,
FramesDropped: 5,
BytesProcessed: 1024,
ConnectionDrops: 2,
AverageLatency: time.Millisecond * 10,
LastFrameTime: time.Now(),
}
assert.Equal(t, int64(100), metrics.FramesSent)

View File

@ -23,13 +23,6 @@ var (
// Output IPC constants are now centralized in config_constants.go
// outputMaxFrameSize, outputWriteTimeout, outputMaxDroppedFrames, outputHeaderSize, outputMessagePoolSize
// OutputIPCConfig represents configuration for audio output
type OutputIPCConfig struct {
SampleRate int
Channels int
FrameSize int
}
// OutputMessageType represents the type of IPC message
type OutputMessageType uint8
@ -113,7 +106,7 @@ func NewAudioOutputServer() (*AudioOutputServer, error) {
// Initialize latency monitoring
latencyConfig := DefaultLatencyConfig()
logger := zerolog.New(os.Stderr).With().Timestamp().Str("component", AudioOutputServerComponent).Logger()
logger := zerolog.New(os.Stderr).With().Timestamp().Str("component", "audio-server").Logger()
latencyMonitor := NewLatencyMonitor(latencyConfig, logger)
// Initialize adaptive buffer manager with default config
@ -167,7 +160,7 @@ func (s *AudioOutputServer) Start() error {
// acceptConnections accepts incoming connections
func (s *AudioOutputServer) acceptConnections() {
logger := logging.GetDefaultLogger().With().Str("component", AudioOutputServerComponent).Logger()
logger := logging.GetDefaultLogger().With().Str("component", "audio-server").Logger()
for s.running {
conn, err := s.listener.Accept()
if err != nil {
@ -260,14 +253,6 @@ func (s *AudioOutputServer) Close() error {
}
func (s *AudioOutputServer) SendFrame(frame []byte) error {
// Use ultra-fast validation for critical audio path
if err := ValidateAudioFrame(frame); err != nil {
logger := logging.GetDefaultLogger().With().Str("component", AudioOutputServerComponent).Logger()
logger.Error().Err(err).Msg("Frame validation failed")
return fmt.Errorf("output frame validation failed: %w", err)
}
// Additional output-specific size check
maxFrameSize := GetConfig().OutputMaxFrameSize
if len(frame) > maxFrameSize {
return fmt.Errorf("output frame size validation failed: got %d bytes, maximum allowed %d bytes", len(frame), maxFrameSize)

View File

@ -94,13 +94,6 @@ func DefaultLatencyConfig() LatencyConfig {
// NewLatencyMonitor creates a new latency monitoring system
func NewLatencyMonitor(config LatencyConfig, logger zerolog.Logger) *LatencyMonitor {
// Validate latency configuration
if err := ValidateLatencyConfig(config); err != nil {
// Log validation error and use default configuration
logger.Error().Err(err).Msg("Invalid latency configuration provided, using defaults")
config = DefaultLatencyConfig()
}
ctx, cancel := context.WithCancel(context.Background())
return &LatencyMonitor{
@ -117,14 +110,14 @@ func NewLatencyMonitor(config LatencyConfig, logger zerolog.Logger) *LatencyMoni
func (lm *LatencyMonitor) Start() {
lm.wg.Add(1)
go lm.monitoringLoop()
lm.logger.Debug().Msg("latency monitor started")
lm.logger.Info().Msg("Latency monitor started")
}
// Stop stops the latency monitor
func (lm *LatencyMonitor) Stop() {
lm.cancel()
lm.wg.Wait()
lm.logger.Debug().Msg("latency monitor stopped")
lm.logger.Info().Msg("Latency monitor stopped")
}
// RecordLatency records a new latency measurement
@ -132,6 +125,9 @@ func (lm *LatencyMonitor) RecordLatency(latency time.Duration, source string) {
now := time.Now()
latencyNanos := latency.Nanoseconds()
// Record in granular metrics histogram
GetGranularMetricsCollector().RecordProcessingLatency(latency)
// Update atomic counters
atomic.StoreInt64(&lm.currentLatency, latencyNanos)
atomic.AddInt64(&lm.latencySamples, 1)
@ -260,20 +256,20 @@ func (lm *LatencyMonitor) runOptimization() {
// Check if current latency exceeds threshold
if metrics.Current > lm.config.MaxLatency {
needsOptimization = true
lm.logger.Warn().Dur("current_latency", metrics.Current).Dur("max_latency", lm.config.MaxLatency).Msg("latency exceeds maximum threshold")
lm.logger.Warn().Dur("current_latency", metrics.Current).Dur("max_latency", lm.config.MaxLatency).Msg("Latency exceeds maximum threshold")
}
// Check if average latency is above adaptive threshold
adaptiveThreshold := time.Duration(float64(lm.config.TargetLatency.Nanoseconds()) * (1.0 + lm.config.AdaptiveThreshold))
if metrics.Average > adaptiveThreshold {
needsOptimization = true
lm.logger.Debug().Dur("average_latency", metrics.Average).Dur("threshold", adaptiveThreshold).Msg("average latency above adaptive threshold")
lm.logger.Info().Dur("average_latency", metrics.Average).Dur("threshold", adaptiveThreshold).Msg("Average latency above adaptive threshold")
}
// Check if jitter is too high
if metrics.Jitter > lm.config.JitterThreshold {
needsOptimization = true
lm.logger.Debug().Dur("jitter", metrics.Jitter).Dur("threshold", lm.config.JitterThreshold).Msg("jitter above threshold")
lm.logger.Info().Dur("jitter", metrics.Jitter).Dur("threshold", lm.config.JitterThreshold).Msg("Jitter above threshold")
}
if needsOptimization {
@ -287,11 +283,11 @@ func (lm *LatencyMonitor) runOptimization() {
for _, callback := range callbacks {
if err := callback(metrics); err != nil {
lm.logger.Error().Err(err).Msg("optimization callback failed")
lm.logger.Error().Err(err).Msg("Optimization callback failed")
}
}
lm.logger.Debug().Interface("metrics", metrics).Msg("latency optimization triggered")
lm.logger.Info().Interface("metrics", metrics).Msg("Latency optimization triggered")
}
}

View File

@ -1,535 +0,0 @@
package audio
import (
"context"
"fmt"
"runtime"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// LatencyProfiler provides comprehensive end-to-end audio latency profiling
// with nanosecond precision across the entire WebRTC->IPC->CGO->ALSA pipeline
type LatencyProfiler struct {
// Atomic counters for thread-safe access (MUST be first for ARM32 alignment)
totalMeasurements int64 // Total number of measurements taken
webrtcLatencySum int64 // Sum of WebRTC processing latencies (nanoseconds)
ipcLatencySum int64 // Sum of IPC communication latencies (nanoseconds)
cgoLatencySum int64 // Sum of CGO call latencies (nanoseconds)
alsaLatencySum int64 // Sum of ALSA device latencies (nanoseconds)
endToEndLatencySum int64 // Sum of complete end-to-end latencies (nanoseconds)
validationLatencySum int64 // Sum of validation overhead (nanoseconds)
serializationLatencySum int64 // Sum of serialization overhead (nanoseconds)
// Peak latency tracking
maxWebrtcLatency int64 // Maximum WebRTC latency observed (nanoseconds)
maxIpcLatency int64 // Maximum IPC latency observed (nanoseconds)
maxCgoLatency int64 // Maximum CGO latency observed (nanoseconds)
maxAlsaLatency int64 // Maximum ALSA latency observed (nanoseconds)
maxEndToEndLatency int64 // Maximum end-to-end latency observed (nanoseconds)
// Configuration and control
config LatencyProfilerConfig
logger zerolog.Logger
ctx context.Context
cancel context.CancelFunc
running int32 // Atomic flag for profiler state
enabled int32 // Atomic flag for measurement collection
// Detailed measurement storage
measurements []DetailedLatencyMeasurement
measurementMutex sync.RWMutex
measurementIndex int
// High-resolution timing
timeSource func() int64 // Nanosecond precision time source
}
// LatencyProfilerConfig defines profiler configuration
type LatencyProfilerConfig struct {
MaxMeasurements int // Maximum measurements to store in memory
SamplingRate float64 // Sampling rate (0.0-1.0, 1.0 = profile every frame)
ReportingInterval time.Duration // How often to log profiling reports
ThresholdWarning time.Duration // Latency threshold for warnings
ThresholdCritical time.Duration // Latency threshold for critical alerts
EnableDetailedTrace bool // Enable detailed per-component tracing
EnableHistogram bool // Enable latency histogram collection
}
// DetailedLatencyMeasurement captures comprehensive latency breakdown
type DetailedLatencyMeasurement struct {
Timestamp time.Time // When the measurement was taken
FrameID uint64 // Unique frame identifier for tracing
WebRTCLatency time.Duration // WebRTC processing time
IPCLatency time.Duration // IPC communication time
CGOLatency time.Duration // CGO call overhead
ALSALatency time.Duration // ALSA device processing time
ValidationLatency time.Duration // Frame validation overhead
SerializationLatency time.Duration // Data serialization overhead
EndToEndLatency time.Duration // Complete pipeline latency
Source string // Source component (input/output)
FrameSize int // Size of the audio frame in bytes
CPUUsage float64 // CPU usage at time of measurement
MemoryUsage uint64 // Memory usage at time of measurement
}
// LatencyProfileReport contains aggregated profiling results
type LatencyProfileReport struct {
TotalMeasurements int64 // Total measurements taken
TimeRange time.Duration // Time span of measurements
// Average latencies
AvgWebRTCLatency time.Duration
AvgIPCLatency time.Duration
AvgCGOLatency time.Duration
AvgALSALatency time.Duration
AvgEndToEndLatency time.Duration
AvgValidationLatency time.Duration
AvgSerializationLatency time.Duration
// Peak latencies
MaxWebRTCLatency time.Duration
MaxIPCLatency time.Duration
MaxCGOLatency time.Duration
MaxALSALatency time.Duration
MaxEndToEndLatency time.Duration
// Performance analysis
BottleneckComponent string // Component with highest average latency
LatencyDistribution map[string]int // Histogram of latency ranges
Throughput float64 // Frames per second processed
}
// FrameLatencyTracker tracks latency for a single audio frame through the pipeline
type FrameLatencyTracker struct {
frameID uint64
startTime int64 // Nanosecond timestamp
webrtcStartTime int64
ipcStartTime int64
cgoStartTime int64
alsaStartTime int64
validationStartTime int64
serializationStartTime int64
frameSize int
source string
}
// Global profiler instance
var (
globalLatencyProfiler unsafe.Pointer // *LatencyProfiler
profilerInitialized int32
)
// DefaultLatencyProfilerConfig returns default profiler configuration
func DefaultLatencyProfilerConfig() LatencyProfilerConfig {
return LatencyProfilerConfig{
MaxMeasurements: 10000,
SamplingRate: 0.1, // Profile 10% of frames to minimize overhead
ReportingInterval: 30 * time.Second,
ThresholdWarning: 50 * time.Millisecond,
ThresholdCritical: 100 * time.Millisecond,
EnableDetailedTrace: false, // Disabled by default for performance
EnableHistogram: true,
}
}
// NewLatencyProfiler creates a new latency profiler
func NewLatencyProfiler(config LatencyProfilerConfig) *LatencyProfiler {
ctx, cancel := context.WithCancel(context.Background())
logger := logging.GetDefaultLogger().With().Str("component", "latency-profiler").Logger()
// Validate configuration
if config.MaxMeasurements <= 0 {
config.MaxMeasurements = 10000
}
if config.SamplingRate < 0.0 || config.SamplingRate > 1.0 {
config.SamplingRate = 0.1
}
if config.ReportingInterval <= 0 {
config.ReportingInterval = 30 * time.Second
}
profiler := &LatencyProfiler{
config: config,
logger: logger,
ctx: ctx,
cancel: cancel,
measurements: make([]DetailedLatencyMeasurement, config.MaxMeasurements),
timeSource: func() int64 { return time.Now().UnixNano() },
}
// Initialize peak latencies to zero
atomic.StoreInt64(&profiler.maxWebrtcLatency, 0)
atomic.StoreInt64(&profiler.maxIpcLatency, 0)
atomic.StoreInt64(&profiler.maxCgoLatency, 0)
atomic.StoreInt64(&profiler.maxAlsaLatency, 0)
atomic.StoreInt64(&profiler.maxEndToEndLatency, 0)
return profiler
}
// Start begins latency profiling
func (lp *LatencyProfiler) Start() error {
if !atomic.CompareAndSwapInt32(&lp.running, 0, 1) {
return fmt.Errorf("latency profiler already running")
}
// Enable measurement collection
atomic.StoreInt32(&lp.enabled, 1)
// Start reporting goroutine
go lp.reportingLoop()
lp.logger.Info().Float64("sampling_rate", lp.config.SamplingRate).Msg("latency profiler started")
return nil
}
// Stop stops latency profiling
func (lp *LatencyProfiler) Stop() {
if !atomic.CompareAndSwapInt32(&lp.running, 1, 0) {
return
}
// Disable measurement collection
atomic.StoreInt32(&lp.enabled, 0)
// Cancel context to stop reporting
lp.cancel()
lp.logger.Info().Msg("latency profiler stopped")
}
// IsEnabled returns whether profiling is currently enabled
func (lp *LatencyProfiler) IsEnabled() bool {
return atomic.LoadInt32(&lp.enabled) == 1
}
// StartFrameTracking begins tracking latency for a new audio frame
func (lp *LatencyProfiler) StartFrameTracking(frameID uint64, frameSize int, source string) *FrameLatencyTracker {
if !lp.IsEnabled() {
return nil
}
// Apply sampling rate to reduce profiling overhead
if lp.config.SamplingRate < 1.0 {
// Simple sampling based on frame ID
if float64(frameID%100)/100.0 > lp.config.SamplingRate {
return nil
}
}
now := lp.timeSource()
return &FrameLatencyTracker{
frameID: frameID,
startTime: now,
frameSize: frameSize,
source: source,
}
}
// TrackWebRTCStart marks the start of WebRTC processing
func (tracker *FrameLatencyTracker) TrackWebRTCStart() {
if tracker != nil {
tracker.webrtcStartTime = time.Now().UnixNano()
}
}
// TrackIPCStart marks the start of IPC communication
func (tracker *FrameLatencyTracker) TrackIPCStart() {
if tracker != nil {
tracker.ipcStartTime = time.Now().UnixNano()
}
}
// TrackCGOStart marks the start of CGO processing
func (tracker *FrameLatencyTracker) TrackCGOStart() {
if tracker != nil {
tracker.cgoStartTime = time.Now().UnixNano()
}
}
// TrackALSAStart marks the start of ALSA device processing
func (tracker *FrameLatencyTracker) TrackALSAStart() {
if tracker != nil {
tracker.alsaStartTime = time.Now().UnixNano()
}
}
// TrackValidationStart marks the start of frame validation
func (tracker *FrameLatencyTracker) TrackValidationStart() {
if tracker != nil {
tracker.validationStartTime = time.Now().UnixNano()
}
}
// TrackSerializationStart marks the start of data serialization
func (tracker *FrameLatencyTracker) TrackSerializationStart() {
if tracker != nil {
tracker.serializationStartTime = time.Now().UnixNano()
}
}
// FinishTracking completes frame tracking and records the measurement
func (lp *LatencyProfiler) FinishTracking(tracker *FrameLatencyTracker) {
if tracker == nil || !lp.IsEnabled() {
return
}
endTime := lp.timeSource()
// Calculate component latencies
var webrtcLatency, ipcLatency, cgoLatency, alsaLatency, validationLatency, serializationLatency time.Duration
if tracker.webrtcStartTime > 0 {
webrtcLatency = time.Duration(tracker.ipcStartTime - tracker.webrtcStartTime)
}
if tracker.ipcStartTime > 0 {
ipcLatency = time.Duration(tracker.cgoStartTime - tracker.ipcStartTime)
}
if tracker.cgoStartTime > 0 {
cgoLatency = time.Duration(tracker.alsaStartTime - tracker.cgoStartTime)
}
if tracker.alsaStartTime > 0 {
alsaLatency = time.Duration(endTime - tracker.alsaStartTime)
}
if tracker.validationStartTime > 0 {
validationLatency = time.Duration(tracker.ipcStartTime - tracker.validationStartTime)
}
if tracker.serializationStartTime > 0 {
serializationLatency = time.Duration(tracker.cgoStartTime - tracker.serializationStartTime)
}
endToEndLatency := time.Duration(endTime - tracker.startTime)
// Update atomic counters
atomic.AddInt64(&lp.totalMeasurements, 1)
atomic.AddInt64(&lp.webrtcLatencySum, webrtcLatency.Nanoseconds())
atomic.AddInt64(&lp.ipcLatencySum, ipcLatency.Nanoseconds())
atomic.AddInt64(&lp.cgoLatencySum, cgoLatency.Nanoseconds())
atomic.AddInt64(&lp.alsaLatencySum, alsaLatency.Nanoseconds())
atomic.AddInt64(&lp.endToEndLatencySum, endToEndLatency.Nanoseconds())
atomic.AddInt64(&lp.validationLatencySum, validationLatency.Nanoseconds())
atomic.AddInt64(&lp.serializationLatencySum, serializationLatency.Nanoseconds())
// Update peak latencies
lp.updatePeakLatency(&lp.maxWebrtcLatency, webrtcLatency.Nanoseconds())
lp.updatePeakLatency(&lp.maxIpcLatency, ipcLatency.Nanoseconds())
lp.updatePeakLatency(&lp.maxCgoLatency, cgoLatency.Nanoseconds())
lp.updatePeakLatency(&lp.maxAlsaLatency, alsaLatency.Nanoseconds())
lp.updatePeakLatency(&lp.maxEndToEndLatency, endToEndLatency.Nanoseconds())
// Store detailed measurement if enabled
if lp.config.EnableDetailedTrace {
lp.storeMeasurement(DetailedLatencyMeasurement{
Timestamp: time.Now(),
FrameID: tracker.frameID,
WebRTCLatency: webrtcLatency,
IPCLatency: ipcLatency,
CGOLatency: cgoLatency,
ALSALatency: alsaLatency,
ValidationLatency: validationLatency,
SerializationLatency: serializationLatency,
EndToEndLatency: endToEndLatency,
Source: tracker.source,
FrameSize: tracker.frameSize,
CPUUsage: lp.getCurrentCPUUsage(),
MemoryUsage: lp.getCurrentMemoryUsage(),
})
}
// Check for threshold violations
if endToEndLatency > lp.config.ThresholdCritical {
lp.logger.Error().Dur("latency", endToEndLatency).Uint64("frame_id", tracker.frameID).
Str("source", tracker.source).Msg("critical latency threshold exceeded")
} else if endToEndLatency > lp.config.ThresholdWarning {
lp.logger.Warn().Dur("latency", endToEndLatency).Uint64("frame_id", tracker.frameID).
Str("source", tracker.source).Msg("warning latency threshold exceeded")
}
}
// updatePeakLatency atomically updates peak latency if new value is higher
func (lp *LatencyProfiler) updatePeakLatency(peakPtr *int64, newLatency int64) {
for {
current := atomic.LoadInt64(peakPtr)
if newLatency <= current || atomic.CompareAndSwapInt64(peakPtr, current, newLatency) {
break
}
}
}
// storeMeasurement stores a detailed measurement in the circular buffer
func (lp *LatencyProfiler) storeMeasurement(measurement DetailedLatencyMeasurement) {
lp.measurementMutex.Lock()
defer lp.measurementMutex.Unlock()
lp.measurements[lp.measurementIndex] = measurement
lp.measurementIndex = (lp.measurementIndex + 1) % len(lp.measurements)
}
// GetReport generates a comprehensive latency profiling report
func (lp *LatencyProfiler) GetReport() LatencyProfileReport {
totalMeasurements := atomic.LoadInt64(&lp.totalMeasurements)
if totalMeasurements == 0 {
return LatencyProfileReport{}
}
// Calculate averages
avgWebRTC := time.Duration(atomic.LoadInt64(&lp.webrtcLatencySum) / totalMeasurements)
avgIPC := time.Duration(atomic.LoadInt64(&lp.ipcLatencySum) / totalMeasurements)
avgCGO := time.Duration(atomic.LoadInt64(&lp.cgoLatencySum) / totalMeasurements)
avgALSA := time.Duration(atomic.LoadInt64(&lp.alsaLatencySum) / totalMeasurements)
avgEndToEnd := time.Duration(atomic.LoadInt64(&lp.endToEndLatencySum) / totalMeasurements)
avgValidation := time.Duration(atomic.LoadInt64(&lp.validationLatencySum) / totalMeasurements)
avgSerialization := time.Duration(atomic.LoadInt64(&lp.serializationLatencySum) / totalMeasurements)
// Get peak latencies
maxWebRTC := time.Duration(atomic.LoadInt64(&lp.maxWebrtcLatency))
maxIPC := time.Duration(atomic.LoadInt64(&lp.maxIpcLatency))
maxCGO := time.Duration(atomic.LoadInt64(&lp.maxCgoLatency))
maxALSA := time.Duration(atomic.LoadInt64(&lp.maxAlsaLatency))
maxEndToEnd := time.Duration(atomic.LoadInt64(&lp.maxEndToEndLatency))
// Determine bottleneck component
bottleneck := "WebRTC"
maxAvg := avgWebRTC
if avgIPC > maxAvg {
bottleneck = "IPC"
maxAvg = avgIPC
}
if avgCGO > maxAvg {
bottleneck = "CGO"
maxAvg = avgCGO
}
if avgALSA > maxAvg {
bottleneck = "ALSA"
}
return LatencyProfileReport{
TotalMeasurements: totalMeasurements,
AvgWebRTCLatency: avgWebRTC,
AvgIPCLatency: avgIPC,
AvgCGOLatency: avgCGO,
AvgALSALatency: avgALSA,
AvgEndToEndLatency: avgEndToEnd,
AvgValidationLatency: avgValidation,
AvgSerializationLatency: avgSerialization,
MaxWebRTCLatency: maxWebRTC,
MaxIPCLatency: maxIPC,
MaxCGOLatency: maxCGO,
MaxALSALatency: maxALSA,
MaxEndToEndLatency: maxEndToEnd,
BottleneckComponent: bottleneck,
}
}
// reportingLoop periodically logs profiling reports
func (lp *LatencyProfiler) reportingLoop() {
ticker := time.NewTicker(lp.config.ReportingInterval)
defer ticker.Stop()
for {
select {
case <-lp.ctx.Done():
return
case <-ticker.C:
report := lp.GetReport()
if report.TotalMeasurements > 0 {
lp.logReport(report)
}
}
}
}
// logReport logs a comprehensive profiling report
func (lp *LatencyProfiler) logReport(report LatencyProfileReport) {
lp.logger.Info().
Int64("total_measurements", report.TotalMeasurements).
Dur("avg_webrtc_latency", report.AvgWebRTCLatency).
Dur("avg_ipc_latency", report.AvgIPCLatency).
Dur("avg_cgo_latency", report.AvgCGOLatency).
Dur("avg_alsa_latency", report.AvgALSALatency).
Dur("avg_end_to_end_latency", report.AvgEndToEndLatency).
Dur("avg_validation_latency", report.AvgValidationLatency).
Dur("avg_serialization_latency", report.AvgSerializationLatency).
Dur("max_webrtc_latency", report.MaxWebRTCLatency).
Dur("max_ipc_latency", report.MaxIPCLatency).
Dur("max_cgo_latency", report.MaxCGOLatency).
Dur("max_alsa_latency", report.MaxALSALatency).
Dur("max_end_to_end_latency", report.MaxEndToEndLatency).
Str("bottleneck_component", report.BottleneckComponent).
Msg("latency profiling report")
}
// getCurrentCPUUsage returns current CPU usage percentage
func (lp *LatencyProfiler) getCurrentCPUUsage() float64 {
// Simplified CPU usage - in production, this would use more sophisticated monitoring
var m runtime.MemStats
runtime.ReadMemStats(&m)
return float64(runtime.NumGoroutine()) / 100.0 // Rough approximation
}
// getCurrentMemoryUsage returns current memory usage in bytes
func (lp *LatencyProfiler) getCurrentMemoryUsage() uint64 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.Alloc
}
// GetGlobalLatencyProfiler returns the global latency profiler instance
func GetGlobalLatencyProfiler() *LatencyProfiler {
ptr := atomic.LoadPointer(&globalLatencyProfiler)
if ptr != nil {
return (*LatencyProfiler)(ptr)
}
// Initialize on first use
if atomic.CompareAndSwapInt32(&profilerInitialized, 0, 1) {
config := DefaultLatencyProfilerConfig()
profiler := NewLatencyProfiler(config)
atomic.StorePointer(&globalLatencyProfiler, unsafe.Pointer(profiler))
return profiler
}
// Another goroutine initialized it, try again
ptr = atomic.LoadPointer(&globalLatencyProfiler)
if ptr != nil {
return (*LatencyProfiler)(ptr)
}
// Fallback: create a new profiler
config := DefaultLatencyProfilerConfig()
return NewLatencyProfiler(config)
}
// EnableLatencyProfiling enables the global latency profiler
func EnableLatencyProfiling() error {
profiler := GetGlobalLatencyProfiler()
return profiler.Start()
}
// DisableLatencyProfiling disables the global latency profiler
func DisableLatencyProfiling() {
ptr := atomic.LoadPointer(&globalLatencyProfiler)
if ptr != nil {
profiler := (*LatencyProfiler)(ptr)
profiler.Stop()
}
}
// ProfileFrameLatency is a convenience function to profile a single frame's latency
func ProfileFrameLatency(frameID uint64, frameSize int, source string, fn func(*FrameLatencyTracker)) {
profiler := GetGlobalLatencyProfiler()
if !profiler.IsEnabled() {
fn(nil)
return
}
tracker := profiler.StartFrameTracking(frameID, frameSize, source)
defer profiler.FinishTracking(tracker)
fn(tracker)
}

View File

@ -1,323 +0,0 @@
package audio
import (
"time"
"github.com/rs/zerolog"
)
// AudioLoggerStandards provides standardized logging patterns for audio components
type AudioLoggerStandards struct {
logger zerolog.Logger
component string
}
// NewAudioLogger creates a new standardized logger for an audio component
func NewAudioLogger(logger zerolog.Logger, component string) *AudioLoggerStandards {
return &AudioLoggerStandards{
logger: logger.With().Str("component", component).Logger(),
component: component,
}
}
// Component Lifecycle Logging
// LogComponentStarting logs component initialization start
func (als *AudioLoggerStandards) LogComponentStarting() {
als.logger.Debug().Msg("starting component")
}
// LogComponentStarted logs successful component start
func (als *AudioLoggerStandards) LogComponentStarted() {
als.logger.Debug().Msg("component started successfully")
}
// LogComponentStopping logs component shutdown start
func (als *AudioLoggerStandards) LogComponentStopping() {
als.logger.Debug().Msg("stopping component")
}
// LogComponentStopped logs successful component stop
func (als *AudioLoggerStandards) LogComponentStopped() {
als.logger.Debug().Msg("component stopped")
}
// LogComponentReady logs component ready state
func (als *AudioLoggerStandards) LogComponentReady() {
als.logger.Info().Msg("component ready")
}
// Error Logging with Context
// LogError logs a general error with context
func (als *AudioLoggerStandards) LogError(err error, msg string) {
als.logger.Error().Err(err).Msg(msg)
}
// LogErrorWithContext logs an error with additional context fields
func (als *AudioLoggerStandards) LogErrorWithContext(err error, msg string, fields map[string]interface{}) {
event := als.logger.Error().Err(err)
for key, value := range fields {
event = event.Interface(key, value)
}
event.Msg(msg)
}
// LogValidationError logs validation failures with specific context
func (als *AudioLoggerStandards) LogValidationError(err error, validationType string, value interface{}) {
als.logger.Error().Err(err).
Str("validation_type", validationType).
Interface("invalid_value", value).
Msg("validation failed")
}
// LogConnectionError logs connection-related errors
func (als *AudioLoggerStandards) LogConnectionError(err error, endpoint string, retryCount int) {
als.logger.Error().Err(err).
Str("endpoint", endpoint).
Int("retry_count", retryCount).
Msg("connection failed")
}
// LogProcessError logs process-related errors with PID context
func (als *AudioLoggerStandards) LogProcessError(err error, pid int, msg string) {
als.logger.Error().Err(err).
Int("pid", pid).
Msg(msg)
}
// Performance and Metrics Logging
// LogPerformanceMetrics logs standardized performance metrics
func (als *AudioLoggerStandards) LogPerformanceMetrics(metrics map[string]interface{}) {
event := als.logger.Info()
for key, value := range metrics {
event = event.Interface(key, value)
}
event.Msg("performance metrics")
}
// LogLatencyMetrics logs latency-specific metrics
func (als *AudioLoggerStandards) LogLatencyMetrics(current, average, max time.Duration, jitter time.Duration) {
als.logger.Info().
Dur("current_latency", current).
Dur("average_latency", average).
Dur("max_latency", max).
Dur("jitter", jitter).
Msg("latency metrics")
}
// LogFrameMetrics logs frame processing metrics
func (als *AudioLoggerStandards) LogFrameMetrics(processed, dropped int64, rate float64) {
als.logger.Info().
Int64("frames_processed", processed).
Int64("frames_dropped", dropped).
Float64("processing_rate", rate).
Msg("frame processing metrics")
}
// LogBufferMetrics logs buffer utilization metrics
func (als *AudioLoggerStandards) LogBufferMetrics(size, used, peak int, utilizationPercent float64) {
als.logger.Info().
Int("buffer_size", size).
Int("buffer_used", used).
Int("buffer_peak", peak).
Float64("utilization_percent", utilizationPercent).
Msg("buffer metrics")
}
// Warning Logging
// LogWarning logs a general warning
func (als *AudioLoggerStandards) LogWarning(msg string) {
als.logger.Warn().Msg(msg)
}
// LogWarningWithError logs a warning with error context
func (als *AudioLoggerStandards) LogWarningWithError(err error, msg string) {
als.logger.Warn().Err(err).Msg(msg)
}
// LogThresholdWarning logs warnings when thresholds are exceeded
func (als *AudioLoggerStandards) LogThresholdWarning(metric string, current, threshold interface{}, msg string) {
als.logger.Warn().
Str("metric", metric).
Interface("current_value", current).
Interface("threshold", threshold).
Msg(msg)
}
// LogRetryWarning logs retry attempts with context
func (als *AudioLoggerStandards) LogRetryWarning(operation string, attempt, maxAttempts int, delay time.Duration) {
als.logger.Warn().
Str("operation", operation).
Int("attempt", attempt).
Int("max_attempts", maxAttempts).
Dur("retry_delay", delay).
Msg("retrying operation")
}
// LogRecoveryWarning logs recovery from error conditions
func (als *AudioLoggerStandards) LogRecoveryWarning(condition string, duration time.Duration) {
als.logger.Warn().
Str("condition", condition).
Dur("recovery_time", duration).
Msg("recovered from error condition")
}
// Debug and Trace Logging
// LogDebug logs debug information
func (als *AudioLoggerStandards) LogDebug(msg string) {
als.logger.Debug().Msg(msg)
}
// LogDebugWithFields logs debug information with structured fields
func (als *AudioLoggerStandards) LogDebugWithFields(msg string, fields map[string]interface{}) {
event := als.logger.Debug()
for key, value := range fields {
event = event.Interface(key, value)
}
event.Msg(msg)
}
// LogOperationTrace logs operation tracing for debugging
func (als *AudioLoggerStandards) LogOperationTrace(operation string, duration time.Duration, success bool) {
als.logger.Debug().
Str("operation", operation).
Dur("duration", duration).
Bool("success", success).
Msg("operation trace")
}
// LogDataFlow logs data flow for debugging
func (als *AudioLoggerStandards) LogDataFlow(source, destination string, bytes int, frameCount int) {
als.logger.Debug().
Str("source", source).
Str("destination", destination).
Int("bytes", bytes).
Int("frame_count", frameCount).
Msg("data flow")
}
// Configuration and State Logging
// LogConfigurationChange logs configuration updates
func (als *AudioLoggerStandards) LogConfigurationChange(configType string, oldValue, newValue interface{}) {
als.logger.Info().
Str("config_type", configType).
Interface("old_value", oldValue).
Interface("new_value", newValue).
Msg("configuration changed")
}
// LogStateTransition logs component state changes
func (als *AudioLoggerStandards) LogStateTransition(fromState, toState string, reason string) {
als.logger.Info().
Str("from_state", fromState).
Str("to_state", toState).
Str("reason", reason).
Msg("state transition")
}
// LogResourceAllocation logs resource allocation/deallocation
func (als *AudioLoggerStandards) LogResourceAllocation(resourceType string, allocated bool, amount interface{}) {
level := als.logger.Debug()
if allocated {
level.Str("action", "allocated")
} else {
level.Str("action", "deallocated")
}
level.Str("resource_type", resourceType).
Interface("amount", amount).
Msg("resource allocation")
}
// Network and IPC Logging
// LogConnectionEvent logs connection lifecycle events
func (als *AudioLoggerStandards) LogConnectionEvent(event, endpoint string, connectionID string) {
als.logger.Info().
Str("event", event).
Str("endpoint", endpoint).
Str("connection_id", connectionID).
Msg("connection event")
}
// LogIPCEvent logs IPC communication events
func (als *AudioLoggerStandards) LogIPCEvent(event, socketPath string, bytes int) {
als.logger.Debug().
Str("event", event).
Str("socket_path", socketPath).
Int("bytes", bytes).
Msg("IPC event")
}
// LogNetworkStats logs network statistics
func (als *AudioLoggerStandards) LogNetworkStats(sent, received int64, latency time.Duration, packetLoss float64) {
als.logger.Info().
Int64("bytes_sent", sent).
Int64("bytes_received", received).
Dur("network_latency", latency).
Float64("packet_loss_percent", packetLoss).
Msg("network statistics")
}
// Process and System Logging
// LogProcessEvent logs process lifecycle events
func (als *AudioLoggerStandards) LogProcessEvent(event string, pid int, exitCode *int) {
event_log := als.logger.Info().
Str("event", event).
Int("pid", pid)
if exitCode != nil {
event_log = event_log.Int("exit_code", *exitCode)
}
event_log.Msg("process event")
}
// LogSystemResource logs system resource usage
func (als *AudioLoggerStandards) LogSystemResource(cpuPercent, memoryMB float64, goroutines int) {
als.logger.Info().
Float64("cpu_percent", cpuPercent).
Float64("memory_mb", memoryMB).
Int("goroutines", goroutines).
Msg("system resources")
}
// LogPriorityChange logs thread priority changes
func (als *AudioLoggerStandards) LogPriorityChange(tid, oldPriority, newPriority int, policy string) {
als.logger.Debug().
Int("tid", tid).
Int("old_priority", oldPriority).
Int("new_priority", newPriority).
Str("policy", policy).
Msg("thread priority changed")
}
// Utility Functions
// GetLogger returns the underlying zerolog.Logger for advanced usage
func (als *AudioLoggerStandards) GetLogger() zerolog.Logger {
return als.logger
}
// WithFields returns a new logger with additional persistent fields
func (als *AudioLoggerStandards) WithFields(fields map[string]interface{}) *AudioLoggerStandards {
event := als.logger.With()
for key, value := range fields {
event = event.Interface(key, value)
}
return &AudioLoggerStandards{
logger: event.Logger(),
component: als.component,
}
}
// WithSubComponent creates a logger for a sub-component
func (als *AudioLoggerStandards) WithSubComponent(subComponent string) *AudioLoggerStandards {
return &AudioLoggerStandards{
logger: als.logger.With().Str("sub_component", subComponent).Logger(),
component: als.component + "." + subComponent,
}
}

View File

@ -156,10 +156,7 @@ func HandleMemoryMetrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache")
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
if err := encoder.Encode(metrics); err != nil {
if err := json.NewEncoder(w).Encode(metrics); err != nil {
logger.Error().Err(err).Msg("failed to encode memory metrics")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
@ -188,7 +185,7 @@ func LogMemoryMetrics() {
// StartMemoryMetricsLogging starts periodic memory metrics logging
func StartMemoryMetricsLogging(interval time.Duration) {
logger := getMemoryMetricsLogger()
logger.Debug().Dur("interval", interval).Msg("memory metrics logging started")
logger.Info().Dur("interval", interval).Msg("starting memory metrics logging")
go func() {
ticker := time.NewTicker(interval)

View File

@ -1,7 +1,6 @@
package audio
import (
"runtime"
"sync"
"sync/atomic"
"time"
@ -101,10 +100,10 @@ var (
},
)
audioAverageLatencyMilliseconds = promauto.NewGauge(
audioAverageLatencySeconds = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_average_latency_milliseconds",
Help: "Average audio latency in milliseconds",
Name: "jetkvm_audio_average_latency_seconds",
Help: "Average audio latency in seconds",
},
)
@ -144,10 +143,10 @@ var (
},
)
microphoneAverageLatencyMilliseconds = promauto.NewGauge(
microphoneAverageLatencySeconds = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_microphone_average_latency_milliseconds",
Help: "Average microphone latency in milliseconds",
Name: "jetkvm_microphone_average_latency_seconds",
Help: "Average microphone latency in seconds",
},
)
@ -287,141 +286,6 @@ var (
},
)
// Device health metrics
deviceHealthStatus = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_device_health_status",
Help: "Current device health status (0=Healthy, 1=Degraded, 2=Failing, 3=Critical)",
},
[]string{"device_type"}, // device_type: capture, playback
)
deviceHealthScore = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_device_health_score",
Help: "Device health score (0.0-1.0, higher is better)",
},
[]string{"device_type"}, // device_type: capture, playback
)
deviceConsecutiveErrors = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_device_consecutive_errors",
Help: "Number of consecutive errors for device",
},
[]string{"device_type"}, // device_type: capture, playback
)
deviceTotalErrors = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_audio_device_total_errors",
Help: "Total number of errors for device",
},
[]string{"device_type"}, // device_type: capture, playback
)
deviceLatencySpikes = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_audio_device_latency_spikes_total",
Help: "Total number of latency spikes for device",
},
[]string{"device_type"}, // device_type: capture, playback
)
// Memory metrics
memoryHeapAllocBytes = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_memory_heap_alloc_bytes",
Help: "Current heap allocation in bytes",
},
)
memoryHeapSysBytes = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_memory_heap_sys_bytes",
Help: "Total heap system memory in bytes",
},
)
memoryHeapObjects = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_memory_heap_objects",
Help: "Number of heap objects",
},
)
memoryGCCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_audio_memory_gc_total",
Help: "Total number of garbage collections",
},
)
memoryGCCPUFraction = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_memory_gc_cpu_fraction",
Help: "Fraction of CPU time spent in garbage collection",
},
)
// Buffer pool efficiency metrics
bufferPoolHitRate = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_buffer_pool_hit_rate_percent",
Help: "Buffer pool hit rate percentage",
},
[]string{"pool_name"}, // pool_name: frame_pool, control_pool, zero_copy_pool
)
bufferPoolMissRate = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_buffer_pool_miss_rate_percent",
Help: "Buffer pool miss rate percentage",
},
[]string{"pool_name"}, // pool_name: frame_pool, control_pool, zero_copy_pool
)
bufferPoolUtilization = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_buffer_pool_utilization_percent",
Help: "Buffer pool utilization percentage",
},
[]string{"pool_name"}, // pool_name: frame_pool, control_pool, zero_copy_pool
)
bufferPoolThroughput = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_buffer_pool_throughput_ops_per_sec",
Help: "Buffer pool throughput in operations per second",
},
[]string{"pool_name"}, // pool_name: frame_pool, control_pool, zero_copy_pool
)
bufferPoolGetLatency = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_buffer_pool_get_latency_seconds",
Help: "Average buffer pool get operation latency in seconds",
},
[]string{"pool_name"}, // pool_name: frame_pool, control_pool, zero_copy_pool
)
bufferPoolPutLatency = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_buffer_pool_put_latency_seconds",
Help: "Average buffer pool put operation latency in seconds",
},
[]string{"pool_name"}, // pool_name: frame_pool, control_pool, zero_copy_pool
)
// Latency percentile metrics
latencyPercentile = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_latency_percentile_milliseconds",
Help: "Audio latency percentiles in milliseconds",
},
[]string{"source", "percentile"}, // source: input, output, processing; percentile: p50, p95, p99, min, max, avg
)
// Metrics update tracking
metricsUpdateMutex sync.RWMutex
lastMetricsUpdate int64
@ -435,15 +299,6 @@ var (
micFramesDroppedValue int64
micBytesProcessedValue int64
micConnectionDropsValue int64
// Atomic counters for device health metrics
deviceCaptureErrorsValue int64
devicePlaybackErrorsValue int64
deviceCaptureSpikesValue int64
devicePlaybackSpikesValue int64
// Atomic counter for memory GC
memoryGCCountValue uint32
)
// UnifiedAudioMetrics provides a common structure for both input and output audio streams
@ -506,7 +361,7 @@ func UpdateAudioMetrics(metrics UnifiedAudioMetrics) {
}
// Update gauges
audioAverageLatencyMilliseconds.Set(float64(metrics.AverageLatency.Nanoseconds()) / 1e6)
audioAverageLatencySeconds.Set(float64(metrics.AverageLatency.Nanoseconds()) / 1e9)
if !metrics.LastFrameTime.IsZero() {
audioLastFrameTimestamp.Set(float64(metrics.LastFrameTime.Unix()))
}
@ -537,7 +392,7 @@ func UpdateMicrophoneMetrics(metrics UnifiedAudioMetrics) {
}
// Update gauges
microphoneAverageLatencyMilliseconds.Set(float64(metrics.AverageLatency.Nanoseconds()) / 1e6)
microphoneAverageLatencySeconds.Set(float64(metrics.AverageLatency.Nanoseconds()) / 1e9)
if !metrics.LastFrameTime.IsZero() {
microphoneLastFrameTimestamp.Set(float64(metrics.LastFrameTime.Unix()))
}
@ -624,95 +479,6 @@ func UpdateAdaptiveBufferMetrics(inputBufferSize, outputBufferSize int, cpuPerce
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// UpdateSocketBufferMetrics updates socket buffer metrics
func UpdateSocketBufferMetrics(component, bufferType string, size, utilization float64, overflowOccurred bool) {
metricsUpdateMutex.Lock()
defer metricsUpdateMutex.Unlock()
socketBufferSizeGauge.WithLabelValues(component, bufferType).Set(size)
socketBufferUtilizationGauge.WithLabelValues(component, bufferType).Set(utilization)
if overflowOccurred {
socketBufferOverflowCounter.WithLabelValues(component, bufferType).Inc()
}
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// UpdateDeviceHealthMetrics updates device health metrics
func UpdateDeviceHealthMetrics(deviceType string, status int, healthScore float64, consecutiveErrors, totalErrors, latencySpikes int64) {
metricsUpdateMutex.Lock()
defer metricsUpdateMutex.Unlock()
deviceHealthStatus.WithLabelValues(deviceType).Set(float64(status))
deviceHealthScore.WithLabelValues(deviceType).Set(healthScore)
deviceConsecutiveErrors.WithLabelValues(deviceType).Set(float64(consecutiveErrors))
// Update error counters with delta calculation
var prevErrors, prevSpikes int64
if deviceType == "capture" {
prevErrors = atomic.SwapInt64(&deviceCaptureErrorsValue, totalErrors)
prevSpikes = atomic.SwapInt64(&deviceCaptureSpikesValue, latencySpikes)
} else {
prevErrors = atomic.SwapInt64(&devicePlaybackErrorsValue, totalErrors)
prevSpikes = atomic.SwapInt64(&devicePlaybackSpikesValue, latencySpikes)
}
if prevErrors > 0 && totalErrors > prevErrors {
deviceTotalErrors.WithLabelValues(deviceType).Add(float64(totalErrors - prevErrors))
}
if prevSpikes > 0 && latencySpikes > prevSpikes {
deviceLatencySpikes.WithLabelValues(deviceType).Add(float64(latencySpikes - prevSpikes))
}
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// UpdateMemoryMetrics updates memory metrics
func UpdateMemoryMetrics() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
memoryHeapAllocBytes.Set(float64(m.HeapAlloc))
memoryHeapSysBytes.Set(float64(m.HeapSys))
memoryHeapObjects.Set(float64(m.HeapObjects))
memoryGCCPUFraction.Set(m.GCCPUFraction)
// Update GC count with delta calculation
currentGCCount := uint32(m.NumGC)
prevGCCount := atomic.SwapUint32(&memoryGCCountValue, currentGCCount)
if prevGCCount > 0 && currentGCCount > prevGCCount {
memoryGCCount.Add(float64(currentGCCount - prevGCCount))
}
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// UpdateBufferPoolMetrics updates buffer pool efficiency metrics
func UpdateBufferPoolMetrics(poolName string, hitRate, missRate, utilization, throughput, getLatency, putLatency float64) {
metricsUpdateMutex.Lock()
defer metricsUpdateMutex.Unlock()
bufferPoolHitRate.WithLabelValues(poolName).Set(hitRate * 100)
bufferPoolMissRate.WithLabelValues(poolName).Set(missRate * 100)
bufferPoolUtilization.WithLabelValues(poolName).Set(utilization * 100)
bufferPoolThroughput.WithLabelValues(poolName).Set(throughput)
bufferPoolGetLatency.WithLabelValues(poolName).Set(getLatency)
bufferPoolPutLatency.WithLabelValues(poolName).Set(putLatency)
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// UpdateLatencyMetrics updates latency percentile metrics
func UpdateLatencyMetrics(source, percentile string, latencyMilliseconds float64) {
metricsUpdateMutex.Lock()
defer metricsUpdateMutex.Unlock()
latencyPercentile.WithLabelValues(source, percentile).Set(latencyMilliseconds)
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// GetLastMetricsUpdate returns the timestamp of the last metrics update
func GetLastMetricsUpdate() time.Time {
timestamp := atomic.LoadInt64(&lastMetricsUpdate)
@ -721,18 +487,31 @@ func GetLastMetricsUpdate() time.Time {
// StartMetricsUpdater starts a goroutine that periodically updates Prometheus metrics
func StartMetricsUpdater() {
// Start the centralized metrics collector
registry := GetMetricsRegistry()
registry.StartMetricsCollector()
// Start a separate goroutine for periodic updates
go func() {
ticker := time.NewTicker(5 * time.Second) // Update every 5 seconds
ticker := time.NewTicker(GetConfig().StatsUpdateInterval) // Update every 5 seconds
defer ticker.Stop()
for range ticker.C {
// Update memory metrics (not part of centralized registry)
UpdateMemoryMetrics()
// Update audio output metrics
audioMetrics := GetAudioMetrics()
UpdateAudioMetrics(convertAudioMetricsToUnified(audioMetrics))
// Update microphone input metrics
micMetrics := GetAudioInputMetrics()
UpdateMicrophoneMetrics(convertAudioInputMetricsToUnified(micMetrics))
// Update microphone subprocess process metrics
if inputSupervisor := GetAudioInputIPCSupervisor(); inputSupervisor != nil {
if processMetrics := inputSupervisor.GetProcessMetrics(); processMetrics != nil {
UpdateMicrophoneProcessMetrics(*processMetrics, inputSupervisor.IsRunning())
}
}
// Update audio configuration metrics
audioConfig := GetAudioConfig()
UpdateAudioConfigMetrics(audioConfig)
micConfig := GetMicrophoneConfig()
UpdateMicrophoneConfigMetrics(micConfig)
}
}()
}

View File

@ -1,151 +0,0 @@
//go:build cgo
package audio
import (
"sync"
"sync/atomic"
"time"
)
// MetricsRegistry provides a centralized source of truth for all audio metrics
// This eliminates duplication between session-specific and global managers
type MetricsRegistry struct {
mu sync.RWMutex
audioMetrics AudioMetrics
audioInputMetrics AudioInputMetrics
audioConfig AudioConfig
microphoneConfig AudioConfig
lastUpdate int64 // Unix timestamp
}
var (
globalMetricsRegistry *MetricsRegistry
registryOnce sync.Once
)
// GetMetricsRegistry returns the global metrics registry instance
func GetMetricsRegistry() *MetricsRegistry {
registryOnce.Do(func() {
globalMetricsRegistry = &MetricsRegistry{
lastUpdate: time.Now().Unix(),
}
})
return globalMetricsRegistry
}
// UpdateAudioMetrics updates the centralized audio output metrics
func (mr *MetricsRegistry) UpdateAudioMetrics(metrics AudioMetrics) {
mr.mu.Lock()
mr.audioMetrics = metrics
mr.lastUpdate = time.Now().Unix()
mr.mu.Unlock()
// Update Prometheus metrics directly to avoid circular dependency
UpdateAudioMetrics(convertAudioMetricsToUnified(metrics))
}
// UpdateAudioInputMetrics updates the centralized audio input metrics
func (mr *MetricsRegistry) UpdateAudioInputMetrics(metrics AudioInputMetrics) {
mr.mu.Lock()
mr.audioInputMetrics = metrics
mr.lastUpdate = time.Now().Unix()
mr.mu.Unlock()
// Update Prometheus metrics directly to avoid circular dependency
UpdateMicrophoneMetrics(convertAudioInputMetricsToUnified(metrics))
}
// UpdateAudioConfig updates the centralized audio configuration
func (mr *MetricsRegistry) UpdateAudioConfig(config AudioConfig) {
mr.mu.Lock()
mr.audioConfig = config
mr.lastUpdate = time.Now().Unix()
mr.mu.Unlock()
// Update Prometheus metrics directly
UpdateAudioConfigMetrics(config)
}
// UpdateMicrophoneConfig updates the centralized microphone configuration
func (mr *MetricsRegistry) UpdateMicrophoneConfig(config AudioConfig) {
mr.mu.Lock()
mr.microphoneConfig = config
mr.lastUpdate = time.Now().Unix()
mr.mu.Unlock()
// Update Prometheus metrics directly
UpdateMicrophoneConfigMetrics(config)
}
// GetAudioMetrics returns the current audio output metrics
func (mr *MetricsRegistry) GetAudioMetrics() AudioMetrics {
mr.mu.RLock()
defer mr.mu.RUnlock()
return mr.audioMetrics
}
// GetAudioInputMetrics returns the current audio input metrics
func (mr *MetricsRegistry) GetAudioInputMetrics() AudioInputMetrics {
mr.mu.RLock()
defer mr.mu.RUnlock()
return mr.audioInputMetrics
}
// GetAudioConfig returns the current audio configuration
func (mr *MetricsRegistry) GetAudioConfig() AudioConfig {
mr.mu.RLock()
defer mr.mu.RUnlock()
return mr.audioConfig
}
// GetMicrophoneConfig returns the current microphone configuration
func (mr *MetricsRegistry) GetMicrophoneConfig() AudioConfig {
mr.mu.RLock()
defer mr.mu.RUnlock()
return mr.microphoneConfig
}
// GetLastUpdate returns the timestamp of the last metrics update
func (mr *MetricsRegistry) GetLastUpdate() time.Time {
timestamp := atomic.LoadInt64(&mr.lastUpdate)
return time.Unix(timestamp, 0)
}
// StartMetricsCollector starts a background goroutine to collect metrics
func (mr *MetricsRegistry) StartMetricsCollector() {
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
// Collect from session-specific manager if available
if sessionProvider := GetSessionProvider(); sessionProvider != nil && sessionProvider.IsSessionActive() {
if inputManager := sessionProvider.GetAudioInputManager(); inputManager != nil {
metrics := inputManager.GetMetrics()
mr.UpdateAudioInputMetrics(metrics)
}
} else {
// Fallback to global manager if no session is active
globalManager := getAudioInputManager()
metrics := globalManager.GetMetrics()
mr.UpdateAudioInputMetrics(metrics)
}
// Collect audio output metrics directly from global metrics variable to avoid circular dependency
audioMetrics := AudioMetrics{
FramesReceived: atomic.LoadInt64(&metrics.FramesReceived),
FramesDropped: atomic.LoadInt64(&metrics.FramesDropped),
BytesProcessed: atomic.LoadInt64(&metrics.BytesProcessed),
ConnectionDrops: atomic.LoadInt64(&metrics.ConnectionDrops),
LastFrameTime: metrics.LastFrameTime,
AverageLatency: metrics.AverageLatency,
}
mr.UpdateAudioMetrics(audioMetrics)
// Collect configuration directly from global variables to avoid circular dependency
mr.UpdateAudioConfig(currentConfig)
mr.UpdateMicrophoneConfig(currentMicrophoneConfig)
}
}()
}

View File

@ -1,211 +0,0 @@
package audio
import (
"fmt"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
)
// AudioOutputIPCManager manages audio output using IPC when enabled
type AudioOutputIPCManager struct {
*BaseAudioManager
server *AudioOutputServer
}
// NewAudioOutputIPCManager creates a new IPC-based audio output manager
func NewAudioOutputIPCManager() *AudioOutputIPCManager {
return &AudioOutputIPCManager{
BaseAudioManager: NewBaseAudioManager(logging.GetDefaultLogger().With().Str("component", AudioOutputIPCComponent).Logger()),
}
}
// Start initializes and starts the audio output IPC manager
func (aom *AudioOutputIPCManager) Start() error {
aom.logComponentStart(AudioOutputIPCComponent)
// Create and start the IPC server
server, err := NewAudioOutputServer()
if err != nil {
aom.logComponentError(AudioOutputIPCComponent, err, "failed to create IPC server")
return err
}
if err := server.Start(); err != nil {
aom.logComponentError(AudioOutputIPCComponent, err, "failed to start IPC server")
return err
}
aom.server = server
aom.setRunning(true)
aom.logComponentStarted(AudioOutputIPCComponent)
// Send initial configuration
config := OutputIPCConfig{
SampleRate: GetConfig().SampleRate,
Channels: GetConfig().Channels,
FrameSize: int(GetConfig().AudioQualityMediumFrameSize.Milliseconds()),
}
if err := aom.SendConfig(config); err != nil {
aom.logger.Warn().Err(err).Msg("Failed to send initial configuration")
}
return nil
}
// Stop gracefully shuts down the audio output IPC manager
func (aom *AudioOutputIPCManager) Stop() {
aom.logComponentStop(AudioOutputIPCComponent)
if aom.server != nil {
aom.server.Stop()
aom.server = nil
}
aom.setRunning(false)
aom.resetMetrics()
aom.logComponentStopped(AudioOutputIPCComponent)
}
// resetMetrics resets all metrics to zero
func (aom *AudioOutputIPCManager) resetMetrics() {
aom.BaseAudioManager.resetMetrics()
}
// WriteOpusFrame sends an Opus frame to the output server
func (aom *AudioOutputIPCManager) WriteOpusFrame(frame *ZeroCopyAudioFrame) error {
if !aom.IsRunning() {
return fmt.Errorf("audio output IPC manager not running")
}
if aom.server == nil {
return fmt.Errorf("audio output server not initialized")
}
// Validate frame before processing
if err := ValidateZeroCopyFrame(frame); err != nil {
aom.logComponentError(AudioOutputIPCComponent, err, "Frame validation failed")
return fmt.Errorf("output frame validation failed: %w", err)
}
start := time.Now()
// Send frame to IPC server
if err := aom.server.SendFrame(frame.Data()); err != nil {
aom.recordFrameDropped()
return err
}
// Update metrics
processingTime := time.Since(start)
aom.recordFrameProcessed(frame.Length())
aom.updateLatency(processingTime)
return nil
}
// WriteOpusFrameZeroCopy writes an Opus audio frame using zero-copy optimization
func (aom *AudioOutputIPCManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFrame) error {
if !aom.IsRunning() {
return fmt.Errorf("audio output IPC manager not running")
}
if aom.server == nil {
return fmt.Errorf("audio output server not initialized")
}
start := time.Now()
// Extract frame data
frameData := frame.Data()
// Send frame to IPC server (zero-copy not available, use regular send)
if err := aom.server.SendFrame(frameData); err != nil {
aom.recordFrameDropped()
return err
}
// Update metrics
processingTime := time.Since(start)
aom.recordFrameProcessed(len(frameData))
aom.updateLatency(processingTime)
return nil
}
// IsReady returns true if the IPC manager is ready to process frames
func (aom *AudioOutputIPCManager) IsReady() bool {
return aom.IsRunning() && aom.server != nil
}
// GetMetrics returns current audio output metrics
func (aom *AudioOutputIPCManager) GetMetrics() AudioOutputMetrics {
baseMetrics := aom.getBaseMetrics()
return AudioOutputMetrics{
FramesReceived: atomic.LoadInt64(&baseMetrics.FramesProcessed), // For output, processed = received
BaseAudioMetrics: baseMetrics,
}
}
// GetDetailedMetrics returns detailed metrics including server statistics
func (aom *AudioOutputIPCManager) GetDetailedMetrics() (AudioOutputMetrics, map[string]interface{}) {
metrics := aom.GetMetrics()
detailed := make(map[string]interface{})
if aom.server != nil {
total, dropped, bufferSize := aom.server.GetServerStats()
detailed["server_total_frames"] = total
detailed["server_dropped_frames"] = dropped
detailed["server_buffer_size"] = bufferSize
detailed["server_frame_rate"] = aom.calculateFrameRate()
}
return metrics, detailed
}
// calculateFrameRate calculates the current frame processing rate
func (aom *AudioOutputIPCManager) calculateFrameRate() float64 {
baseMetrics := aom.getBaseMetrics()
framesProcessed := atomic.LoadInt64(&baseMetrics.FramesProcessed)
if framesProcessed == 0 {
return 0.0
}
// Calculate rate based on last frame time
baseMetrics = aom.getBaseMetrics()
if baseMetrics.LastFrameTime.IsZero() {
return 0.0
}
elapsed := time.Since(baseMetrics.LastFrameTime)
if elapsed.Seconds() == 0 {
return 0.0
}
return float64(framesProcessed) / elapsed.Seconds()
}
// SendConfig sends configuration to the IPC server
func (aom *AudioOutputIPCManager) SendConfig(config OutputIPCConfig) error {
if aom.server == nil {
return fmt.Errorf("audio output server not initialized")
}
// Validate configuration parameters
if err := ValidateOutputIPCConfig(config.SampleRate, config.Channels, config.FrameSize); err != nil {
aom.logger.Error().Err(err).Msg("Configuration validation failed")
return fmt.Errorf("output configuration validation failed: %w", err)
}
// Note: AudioOutputServer doesn't have SendConfig method yet
// This is a placeholder for future implementation
aom.logger.Info().Interface("config", config).Msg("configuration received")
return nil
}
// GetServer returns the underlying IPC server (for testing)
func (aom *AudioOutputIPCManager) GetServer() *AudioOutputServer {
return aom.server
}

View File

@ -2,56 +2,60 @@ package audio
import (
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// AudioOutputManager manages audio output stream using IPC mode
type AudioOutputManager struct {
*BaseAudioManager
streamer *AudioOutputStreamer
framesReceived int64 // Output-specific metric
metrics AudioOutputMetrics
streamer *AudioOutputStreamer
logger zerolog.Logger
running int32
}
// AudioOutputMetrics tracks output-specific metrics
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
type AudioOutputMetrics struct {
// Atomic int64 field first for proper ARM32 alignment
FramesReceived int64 `json:"frames_received"` // Total frames received (output-specific)
// Embedded struct with atomic fields properly aligned
BaseAudioMetrics
FramesReceived int64
FramesDropped int64
BytesProcessed int64
ConnectionDrops int64
LastFrameTime time.Time
AverageLatency time.Duration
}
// NewAudioOutputManager creates a new audio output manager
func NewAudioOutputManager() *AudioOutputManager {
logger := logging.GetDefaultLogger().With().Str("component", AudioOutputManagerComponent).Logger()
streamer, err := NewAudioOutputStreamer()
if err != nil {
// Log error but continue with nil streamer - will be handled gracefully
logger := logging.GetDefaultLogger().With().Str("component", AudioOutputManagerComponent).Logger()
logger.Error().Err(err).Msg("Failed to create audio output streamer")
}
return &AudioOutputManager{
BaseAudioManager: NewBaseAudioManager(logger),
streamer: streamer,
streamer: streamer,
logger: logging.GetDefaultLogger().With().Str("component", AudioOutputManagerComponent).Logger(),
}
}
// Start starts the audio output manager
func (aom *AudioOutputManager) Start() error {
if !aom.setRunning(true) {
if !atomic.CompareAndSwapInt32(&aom.running, 0, 1) {
return nil // Already running
}
aom.logComponentStart(AudioOutputManagerComponent)
aom.logger.Info().Str("component", AudioOutputManagerComponent).Msg("starting component")
if aom.streamer == nil {
// Try to recreate streamer if it was nil
streamer, err := NewAudioOutputStreamer()
if err != nil {
aom.setRunning(false)
aom.logComponentError(AudioOutputManagerComponent, err, "failed to create audio output streamer")
atomic.StoreInt32(&aom.running, 0)
aom.logger.Error().Err(err).Str("component", AudioOutputManagerComponent).Msg("failed to create audio output streamer")
return err
}
aom.streamer = streamer
@ -59,39 +63,44 @@ func (aom *AudioOutputManager) Start() error {
err := aom.streamer.Start()
if err != nil {
aom.setRunning(false)
atomic.StoreInt32(&aom.running, 0)
// Reset metrics on failed start
aom.resetMetrics()
aom.logComponentError(AudioOutputManagerComponent, err, "failed to start component")
aom.logger.Error().Err(err).Str("component", AudioOutputManagerComponent).Msg("failed to start component")
return err
}
aom.logComponentStarted(AudioOutputManagerComponent)
aom.logger.Info().Str("component", AudioOutputManagerComponent).Msg("component started successfully")
return nil
}
// Stop stops the audio output manager
func (aom *AudioOutputManager) Stop() {
if !aom.setRunning(false) {
if !atomic.CompareAndSwapInt32(&aom.running, 1, 0) {
return // Already stopped
}
aom.logComponentStop(AudioOutputManagerComponent)
aom.logger.Info().Str("component", AudioOutputManagerComponent).Msg("stopping component")
if aom.streamer != nil {
aom.streamer.Stop()
}
aom.logComponentStopped(AudioOutputManagerComponent)
aom.logger.Info().Str("component", AudioOutputManagerComponent).Msg("component stopped")
}
// resetMetrics resets all metrics to zero
func (aom *AudioOutputManager) resetMetrics() {
aom.BaseAudioManager.resetMetrics()
atomic.StoreInt64(&aom.framesReceived, 0)
atomic.StoreInt64(&aom.metrics.FramesReceived, 0)
atomic.StoreInt64(&aom.metrics.FramesDropped, 0)
atomic.StoreInt64(&aom.metrics.BytesProcessed, 0)
atomic.StoreInt64(&aom.metrics.ConnectionDrops, 0)
}
// Note: IsRunning() is inherited from BaseAudioManager
// IsRunning returns whether the audio output manager is running
func (aom *AudioOutputManager) IsRunning() bool {
return atomic.LoadInt32(&aom.running) == 1
}
// IsReady returns whether the audio output manager is ready to receive frames
func (aom *AudioOutputManager) IsReady() bool {
@ -106,8 +115,12 @@ func (aom *AudioOutputManager) IsReady() bool {
// GetMetrics returns current metrics
func (aom *AudioOutputManager) GetMetrics() AudioOutputMetrics {
return AudioOutputMetrics{
FramesReceived: atomic.LoadInt64(&aom.framesReceived),
BaseAudioMetrics: aom.getBaseMetrics(),
FramesReceived: atomic.LoadInt64(&aom.metrics.FramesReceived),
FramesDropped: atomic.LoadInt64(&aom.metrics.FramesDropped),
BytesProcessed: atomic.LoadInt64(&aom.metrics.BytesProcessed),
ConnectionDrops: atomic.LoadInt64(&aom.metrics.ConnectionDrops),
AverageLatency: aom.metrics.AverageLatency,
LastFrameTime: aom.metrics.LastFrameTime,
}
}
@ -118,7 +131,6 @@ func (aom *AudioOutputManager) GetComprehensiveMetrics() map[string]interface{}
comprehensiveMetrics := map[string]interface{}{
"manager": map[string]interface{}{
"frames_received": baseMetrics.FramesReceived,
"frames_processed": baseMetrics.FramesProcessed,
"frames_dropped": baseMetrics.FramesDropped,
"bytes_processed": baseMetrics.BytesProcessed,
"connection_drops": baseMetrics.ConnectionDrops,

View File

@ -14,10 +14,7 @@ import (
// This should be called from main() when the subprocess is detected
func RunAudioOutputServer() error {
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
logger.Debug().Msg("audio output server subprocess starting")
// Initialize validation cache for optimal performance
InitValidationCache()
logger.Info().Msg("Starting audio output server subprocess")
// Create audio server
server, err := NewAudioOutputServer()
@ -45,7 +42,7 @@ func RunAudioOutputServer() error {
return err
}
logger.Debug().Msg("audio output server started, waiting for connections")
logger.Info().Msg("Audio output server started, waiting for connections")
// Set up signal handling for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
@ -57,18 +54,18 @@ func RunAudioOutputServer() error {
// Wait for shutdown signal
select {
case sig := <-sigChan:
logger.Info().Str("signal", sig.String()).Msg("received shutdown signal")
logger.Info().Str("signal", sig.String()).Msg("Received shutdown signal")
case <-ctx.Done():
logger.Debug().Msg("context cancelled")
logger.Info().Msg("Context cancelled")
}
// Graceful shutdown
logger.Debug().Msg("shutting down audio output server")
logger.Info().Msg("Shutting down audio output server")
StopNonBlockingAudioStreaming()
// Give some time for cleanup
time.Sleep(GetConfig().DefaultSleepDuration)
logger.Debug().Msg("audio output server subprocess stopped")
logger.Info().Msg("Audio output server subprocess stopped")
return nil
}

View File

@ -391,14 +391,6 @@ func StartAudioOutputStreaming(send func([]byte)) error {
frame := GetAudioFrameBuffer()
frame = frame[:n] // Resize to actual frame size
copy(frame, buffer[:n])
// Validate frame before sending
if err := ValidateAudioFrame(frame); err != nil {
getOutputStreamingLogger().Warn().Err(err).Msg("Frame validation failed, dropping frame")
PutAudioFrameBuffer(frame)
continue
}
send(frame)
// Return buffer to pool after sending
PutAudioFrameBuffer(frame)

View File

@ -1,393 +0,0 @@
//go:build cgo
// +build cgo
package audio
import (
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestPerformanceCriticalPaths tests the most frequently executed code paths
// to ensure they remain efficient and don't interfere with KVM functionality
func TestPerformanceCriticalPaths(t *testing.T) {
if testing.Short() {
t.Skip("Skipping performance tests in short mode")
}
// Initialize validation cache for performance testing
InitValidationCache()
tests := []struct {
name string
testFunc func(t *testing.T)
}{
{"AudioFrameProcessingLatency", testAudioFrameProcessingLatency},
{"MetricsUpdateOverhead", testMetricsUpdateOverhead},
{"ConfigurationAccessSpeed", testConfigurationAccessSpeed},
{"ValidationFunctionSpeed", testValidationFunctionSpeed},
{"MemoryAllocationPatterns", testMemoryAllocationPatterns},
{"ConcurrentAccessPerformance", testConcurrentAccessPerformance},
{"BufferPoolEfficiency", testBufferPoolEfficiency},
{"AtomicOperationOverhead", testAtomicOperationOverhead},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.testFunc(t)
})
}
}
// testAudioFrameProcessingLatency tests the latency of audio frame processing
// This is the most critical path that must not interfere with KVM
func testAudioFrameProcessingLatency(t *testing.T) {
const (
frameCount = 1000
maxLatencyPerFrame = 100 * time.Microsecond // Very strict requirement
)
// Create test frame data
frameData := make([]byte, 1920) // Typical frame size
for i := range frameData {
frameData[i] = byte(i % 256)
}
// Measure frame processing latency
start := time.Now()
for i := 0; i < frameCount; i++ {
// Simulate the critical path: validation + metrics update
err := ValidateAudioFrame(frameData)
require.NoError(t, err)
// Record frame received (atomic operation)
RecordFrameReceived(len(frameData))
}
elapsed := time.Since(start)
avgLatencyPerFrame := elapsed / frameCount
t.Logf("Average frame processing latency: %v", avgLatencyPerFrame)
// Ensure frame processing is fast enough to not interfere with KVM
assert.Less(t, avgLatencyPerFrame, maxLatencyPerFrame,
"Frame processing latency %v exceeds maximum %v - may interfere with KVM",
avgLatencyPerFrame, maxLatencyPerFrame)
// Ensure total processing time is reasonable
maxTotalTime := 50 * time.Millisecond
assert.Less(t, elapsed, maxTotalTime,
"Total processing time %v exceeds maximum %v", elapsed, maxTotalTime)
}
// testMetricsUpdateOverhead tests the overhead of metrics updates
func testMetricsUpdateOverhead(t *testing.T) {
const iterations = 10000
// Test RecordFrameReceived performance
start := time.Now()
for i := 0; i < iterations; i++ {
RecordFrameReceived(1024)
}
recordLatency := time.Since(start) / iterations
// Test GetAudioMetrics performance
start = time.Now()
for i := 0; i < iterations; i++ {
_ = GetAudioMetrics()
}
getLatency := time.Since(start) / iterations
t.Logf("RecordFrameReceived latency: %v", recordLatency)
t.Logf("GetAudioMetrics latency: %v", getLatency)
// Metrics operations should be optimized for JetKVM's ARM Cortex-A7 @ 1GHz
// With 256MB RAM, we need to be conservative with performance expectations
assert.Less(t, recordLatency, 50*time.Microsecond, "RecordFrameReceived too slow")
assert.Less(t, getLatency, 20*time.Microsecond, "GetAudioMetrics too slow")
}
// testConfigurationAccessSpeed tests configuration access performance
func testConfigurationAccessSpeed(t *testing.T) {
const iterations = 10000
// Test GetAudioConfig performance
start := time.Now()
for i := 0; i < iterations; i++ {
_ = GetAudioConfig()
}
configLatency := time.Since(start) / iterations
// Test GetConfig performance
start = time.Now()
for i := 0; i < iterations; i++ {
_ = GetConfig()
}
constantsLatency := time.Since(start) / iterations
t.Logf("GetAudioConfig latency: %v", configLatency)
t.Logf("GetConfig latency: %v", constantsLatency)
// Configuration access should be very fast
assert.Less(t, configLatency, 100*time.Nanosecond, "GetAudioConfig too slow")
assert.Less(t, constantsLatency, 100*time.Nanosecond, "GetConfig too slow")
}
// testValidationFunctionSpeed tests validation function performance
func testValidationFunctionSpeed(t *testing.T) {
const iterations = 10000
frameData := make([]byte, 1920)
// Test ValidateAudioFrame (most critical)
start := time.Now()
for i := 0; i < iterations; i++ {
err := ValidateAudioFrame(frameData)
require.NoError(t, err)
}
fastValidationLatency := time.Since(start) / iterations
// Test ValidateAudioQuality
start = time.Now()
for i := 0; i < iterations; i++ {
err := ValidateAudioQuality(AudioQualityMedium)
require.NoError(t, err)
}
qualityValidationLatency := time.Since(start) / iterations
// Test ValidateBufferSize
start = time.Now()
for i := 0; i < iterations; i++ {
err := ValidateBufferSize(1024)
require.NoError(t, err)
}
bufferValidationLatency := time.Since(start) / iterations
t.Logf("ValidateAudioFrame latency: %v", fastValidationLatency)
t.Logf("ValidateAudioQuality latency: %v", qualityValidationLatency)
t.Logf("ValidateBufferSize latency: %v", bufferValidationLatency)
// Validation functions optimized for ARM Cortex-A7 single core @ 1GHz
// Conservative thresholds to ensure KVM functionality isn't impacted
assert.Less(t, fastValidationLatency, 100*time.Microsecond, "ValidateAudioFrame too slow")
assert.Less(t, qualityValidationLatency, 50*time.Microsecond, "ValidateAudioQuality too slow")
assert.Less(t, bufferValidationLatency, 50*time.Microsecond, "ValidateBufferSize too slow")
}
// testMemoryAllocationPatterns tests memory allocation efficiency
func testMemoryAllocationPatterns(t *testing.T) {
// Test that frequent operations don't cause excessive allocations
var m1, m2 runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m1)
// Perform operations that should minimize allocations
for i := 0; i < 1000; i++ {
_ = GetAudioConfig()
_ = GetAudioMetrics()
RecordFrameReceived(1024)
_ = ValidateAudioQuality(AudioQualityMedium)
}
runtime.GC()
runtime.ReadMemStats(&m2)
allocations := m2.Mallocs - m1.Mallocs
t.Logf("Memory allocations for 1000 operations: %d", allocations)
// Should have minimal allocations for these hot path operations
assert.Less(t, allocations, uint64(100), "Too many memory allocations in hot path")
}
// testConcurrentAccessPerformance tests performance under concurrent access
func testConcurrentAccessPerformance(t *testing.T) {
const (
numGoroutines = 10
operationsPerGoroutine = 1000
)
var wg sync.WaitGroup
start := time.Now()
// Launch concurrent goroutines performing audio operations
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
frameData := make([]byte, 1920)
for j := 0; j < operationsPerGoroutine; j++ {
// Simulate concurrent audio processing
_ = ValidateAudioFrame(frameData)
RecordFrameReceived(len(frameData))
_ = GetAudioMetrics()
_ = GetAudioConfig()
}
}()
}
wg.Wait()
elapsed := time.Since(start)
totalOperations := numGoroutines * operationsPerGoroutine * 4 // 4 operations per iteration
avgLatency := elapsed / time.Duration(totalOperations)
t.Logf("Concurrent access: %d operations in %v (avg: %v per operation)",
totalOperations, elapsed, avgLatency)
// Concurrent access should not significantly degrade performance
assert.Less(t, avgLatency, 1*time.Microsecond, "Concurrent access too slow")
}
// testBufferPoolEfficiency tests buffer pool performance
func testBufferPoolEfficiency(t *testing.T) {
// Test buffer acquisition and release performance
const iterations = 1000
start := time.Now()
for i := 0; i < iterations; i++ {
// Simulate buffer pool usage (if available)
buffer := make([]byte, 1920) // Fallback to allocation
_ = buffer
// In real implementation, this would be pool.Get() and pool.Put()
}
elapsed := time.Since(start)
avgLatency := elapsed / iterations
t.Logf("Buffer allocation latency: %v per buffer", avgLatency)
// Buffer operations should be fast
assert.Less(t, avgLatency, 1*time.Microsecond, "Buffer allocation too slow")
}
// testAtomicOperationOverhead tests atomic operation performance
func testAtomicOperationOverhead(t *testing.T) {
const iterations = 10000
var counter int64
// Test atomic increment performance
start := time.Now()
for i := 0; i < iterations; i++ {
atomic.AddInt64(&counter, 1)
}
atomicLatency := time.Since(start) / iterations
// Test atomic load performance
start = time.Now()
for i := 0; i < iterations; i++ {
_ = atomic.LoadInt64(&counter)
}
loadLatency := time.Since(start) / iterations
t.Logf("Atomic add latency: %v", atomicLatency)
t.Logf("Atomic load latency: %v", loadLatency)
// Atomic operations on ARM Cortex-A7 - realistic expectations
assert.Less(t, atomicLatency, 1*time.Microsecond, "Atomic add too slow")
assert.Less(t, loadLatency, 500*time.Nanosecond, "Atomic load too slow")
}
// TestRegressionDetection tests for performance regressions
func TestRegressionDetection(t *testing.T) {
if testing.Short() {
t.Skip("Skipping regression test in short mode")
}
// Baseline performance expectations
baselines := map[string]time.Duration{
"frame_processing": 100 * time.Microsecond,
"metrics_update": 500 * time.Nanosecond,
"config_access": 100 * time.Nanosecond,
"validation": 200 * time.Nanosecond,
}
// Test frame processing
frameData := make([]byte, 1920)
start := time.Now()
for i := 0; i < 100; i++ {
_ = ValidateAudioFrame(frameData)
RecordFrameReceived(len(frameData))
}
frameProcessingTime := time.Since(start) / 100
// Test metrics update
start = time.Now()
for i := 0; i < 1000; i++ {
RecordFrameReceived(1024)
}
metricsUpdateTime := time.Since(start) / 1000
// Test config access
start = time.Now()
for i := 0; i < 1000; i++ {
_ = GetAudioConfig()
}
configAccessTime := time.Since(start) / 1000
// Test validation
start = time.Now()
for i := 0; i < 1000; i++ {
_ = ValidateAudioQuality(AudioQualityMedium)
}
validationTime := time.Since(start) / 1000
// Performance regression thresholds for JetKVM hardware:
// - ARM Cortex-A7 @ 1GHz single core
// - 256MB DDR3L RAM
// - Must not interfere with primary KVM functionality
assert.Less(t, frameProcessingTime, baselines["frame_processing"],
"Frame processing regression: %v > %v", frameProcessingTime, baselines["frame_processing"])
assert.Less(t, metricsUpdateTime, 100*time.Microsecond,
"Metrics update regression: %v > 100μs", metricsUpdateTime)
assert.Less(t, configAccessTime, 10*time.Microsecond,
"Config access regression: %v > 10μs", configAccessTime)
assert.Less(t, validationTime, 10*time.Microsecond,
"Validation regression: %v > 10μs", validationTime)
t.Logf("Performance results:")
t.Logf(" Frame processing: %v (baseline: %v)", frameProcessingTime, baselines["frame_processing"])
t.Logf(" Metrics update: %v (baseline: %v)", metricsUpdateTime, baselines["metrics_update"])
t.Logf(" Config access: %v (baseline: %v)", configAccessTime, baselines["config_access"])
t.Logf(" Validation: %v (baseline: %v)", validationTime, baselines["validation"])
}
// TestMemoryLeakDetection tests for memory leaks in critical paths
func TestMemoryLeakDetection(t *testing.T) {
if testing.Short() {
t.Skip("Skipping memory leak test in short mode")
}
var m1, m2 runtime.MemStats
// Baseline measurement
runtime.GC()
runtime.ReadMemStats(&m1)
// Perform many operations that should not leak memory
for cycle := 0; cycle < 10; cycle++ {
for i := 0; i < 1000; i++ {
frameData := make([]byte, 1920)
_ = ValidateAudioFrame(frameData)
RecordFrameReceived(len(frameData))
_ = GetAudioMetrics()
_ = GetAudioConfig()
}
// Force garbage collection between cycles
runtime.GC()
}
// Final measurement
runtime.GC()
runtime.ReadMemStats(&m2)
memoryGrowth := int64(m2.Alloc) - int64(m1.Alloc)
t.Logf("Memory growth after 10,000 operations: %d bytes", memoryGrowth)
// Memory growth should be minimal (less than 1MB)
assert.Less(t, memoryGrowth, int64(1024*1024),
"Excessive memory growth detected: %d bytes", memoryGrowth)
}

View File

@ -69,13 +69,13 @@ func (ps *PriorityScheduler) SetThreadPriority(priority int, policy int) error {
// If we can't set real-time priority, try nice value instead
schedNormal, _, _ := getSchedulingPolicies()
if policy != schedNormal {
ps.logger.Warn().Int("errno", int(errno)).Msg("failed to set real-time priority, falling back to nice")
ps.logger.Warn().Int("errno", int(errno)).Msg("Failed to set real-time priority, falling back to nice")
return ps.setNicePriority(priority)
}
return errno
}
ps.logger.Debug().Int("tid", tid).Int("priority", priority).Int("policy", policy).Msg("thread priority set")
ps.logger.Debug().Int("tid", tid).Int("priority", priority).Int("policy", policy).Msg("Thread priority set")
return nil
}
@ -93,11 +93,11 @@ func (ps *PriorityScheduler) setNicePriority(rtPriority int) error {
err := syscall.Setpriority(syscall.PRIO_PROCESS, 0, niceValue)
if err != nil {
ps.logger.Warn().Err(err).Int("nice", niceValue).Msg("failed to set nice priority")
ps.logger.Warn().Err(err).Int("nice", niceValue).Msg("Failed to set nice priority")
return err
}
ps.logger.Debug().Int("nice", niceValue).Msg("nice priority set as fallback")
ps.logger.Debug().Int("nice", niceValue).Msg("Nice priority set as fallback")
return nil
}
@ -132,13 +132,13 @@ func (ps *PriorityScheduler) ResetPriority() error {
// Disable disables priority scheduling (useful for testing or fallback)
func (ps *PriorityScheduler) Disable() {
ps.enabled = false
ps.logger.Debug().Msg("priority scheduling disabled")
ps.logger.Info().Msg("Priority scheduling disabled")
}
// Enable enables priority scheduling
func (ps *PriorityScheduler) Enable() {
ps.enabled = true
ps.logger.Debug().Msg("priority scheduling enabled")
ps.logger.Info().Msg("Priority scheduling enabled")
}
// Global priority scheduler instance

View File

@ -95,7 +95,7 @@ func (pm *ProcessMonitor) Start() {
pm.running = true
go pm.monitorLoop()
pm.logger.Debug().Msg("process monitor started")
pm.logger.Info().Msg("Process monitor started")
}
// Stop stops monitoring processes
@ -109,7 +109,7 @@ func (pm *ProcessMonitor) Stop() {
pm.running = false
close(pm.stopChan)
pm.logger.Debug().Msg("process monitor stopped")
pm.logger.Info().Msg("Process monitor stopped")
}
// AddProcess adds a process to monitor

View File

@ -1,362 +0,0 @@
//go:build cgo
// +build cgo
package audio
import (
"fmt"
"net"
"os"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestRegressionScenarios tests critical edge cases and error conditions
// that could cause system instability in production
func TestRegressionScenarios(t *testing.T) {
tests := []struct {
name string
testFunc func(t *testing.T)
description string
}{
{
name: "IPCConnectionFailure",
testFunc: testIPCConnectionFailureRecovery,
description: "Test IPC connection failure and recovery scenarios",
},
{
name: "BufferOverflow",
testFunc: testBufferOverflowHandling,
description: "Test buffer overflow protection and recovery",
},
{
name: "SupervisorRapidRestart",
testFunc: testSupervisorRapidRestartScenario,
description: "Test supervisor behavior under rapid restart conditions",
},
{
name: "ConcurrentStartStop",
testFunc: testConcurrentStartStopOperations,
description: "Test concurrent start/stop operations for race conditions",
},
{
name: "MemoryLeakPrevention",
testFunc: testMemoryLeakPrevention,
description: "Test memory leak prevention in long-running scenarios",
},
{
name: "ConfigValidationEdgeCases",
testFunc: testConfigValidationEdgeCases,
description: "Test configuration validation with edge case values",
},
{
name: "AtomicOperationConsistency",
testFunc: testAtomicOperationConsistency,
description: "Test atomic operations consistency under high concurrency",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Logf("Running regression test: %s - %s", tt.name, tt.description)
tt.testFunc(t)
})
}
}
// testIPCConnectionFailureRecovery tests IPC connection failure scenarios
func testIPCConnectionFailureRecovery(t *testing.T) {
manager := NewAudioInputIPCManager()
require.NotNil(t, manager)
// Test start with no IPC server available (should handle gracefully)
err := manager.Start()
// Should not panic or crash, may return error depending on implementation
if err != nil {
t.Logf("Expected error when no IPC server available: %v", err)
}
// Test that manager can recover after IPC becomes available
if manager.IsRunning() {
manager.Stop()
}
// Verify clean state after failure
assert.False(t, manager.IsRunning())
assert.False(t, manager.IsReady())
}
// testBufferOverflowHandling tests buffer overflow protection
func testBufferOverflowHandling(t *testing.T) {
// Test with extremely large buffer sizes
extremelyLargeSize := 1024 * 1024 * 100 // 100MB
err := ValidateBufferSize(extremelyLargeSize)
assert.Error(t, err, "Should reject extremely large buffer sizes")
// Test with negative buffer sizes
err = ValidateBufferSize(-1)
assert.Error(t, err, "Should reject negative buffer sizes")
// Test with zero buffer size
err = ValidateBufferSize(0)
assert.Error(t, err, "Should reject zero buffer size")
// Test with maximum valid buffer size
maxValidSize := GetConfig().SocketMaxBuffer
err = ValidateBufferSize(int(maxValidSize))
assert.NoError(t, err, "Should accept maximum valid buffer size")
}
// testSupervisorRapidRestartScenario tests supervisor under rapid restart conditions
func testSupervisorRapidRestartScenario(t *testing.T) {
if testing.Short() {
t.Skip("Skipping rapid restart test in short mode")
}
supervisor := NewAudioOutputSupervisor()
require.NotNil(t, supervisor)
// Perform rapid start/stop cycles to test for race conditions
for i := 0; i < 10; i++ {
err := supervisor.Start()
if err != nil {
t.Logf("Start attempt %d failed (expected in test environment): %v", i, err)
}
// Very short delay to stress test
time.Sleep(10 * time.Millisecond)
supervisor.Stop()
time.Sleep(10 * time.Millisecond)
}
// Verify supervisor is in clean state after rapid cycling
assert.False(t, supervisor.IsRunning())
}
// testConcurrentStartStopOperations tests concurrent operations for race conditions
func testConcurrentStartStopOperations(t *testing.T) {
manager := NewAudioInputIPCManager()
require.NotNil(t, manager)
var wg sync.WaitGroup
const numGoroutines = 10
// Launch multiple goroutines trying to start/stop concurrently
for i := 0; i < numGoroutines; i++ {
wg.Add(2)
// Start goroutine
go func(id int) {
defer wg.Done()
err := manager.Start()
if err != nil {
t.Logf("Concurrent start %d: %v", id, err)
}
}(i)
// Stop goroutine
go func(id int) {
defer wg.Done()
time.Sleep(5 * time.Millisecond) // Small delay
manager.Stop()
}(i)
}
wg.Wait()
// Ensure final state is consistent
manager.Stop() // Final cleanup
assert.False(t, manager.IsRunning())
}
// testMemoryLeakPrevention tests for memory leaks in long-running scenarios
func testMemoryLeakPrevention(t *testing.T) {
if testing.Short() {
t.Skip("Skipping memory leak test in short mode")
}
manager := NewAudioInputIPCManager()
require.NotNil(t, manager)
// Simulate long-running operation with periodic restarts
for cycle := 0; cycle < 5; cycle++ {
err := manager.Start()
if err != nil {
t.Logf("Start cycle %d failed (expected): %v", cycle, err)
}
// Simulate some activity
time.Sleep(100 * time.Millisecond)
// Get metrics to ensure they're not accumulating indefinitely
metrics := manager.GetMetrics()
assert.NotNil(t, metrics, "Metrics should be available")
manager.Stop()
time.Sleep(50 * time.Millisecond)
}
// Final verification
assert.False(t, manager.IsRunning())
}
// testConfigValidationEdgeCases tests configuration validation with edge cases
func testConfigValidationEdgeCases(t *testing.T) {
// Test sample rate edge cases
testCases := []struct {
sampleRate int
channels int
frameSize int
shouldPass bool
description string
}{
{0, 2, 960, false, "zero sample rate"},
{-1, 2, 960, false, "negative sample rate"},
{1, 2, 960, false, "extremely low sample rate"},
{999999, 2, 960, false, "extremely high sample rate"},
{48000, 0, 960, false, "zero channels"},
{48000, -1, 960, false, "negative channels"},
{48000, 100, 960, false, "too many channels"},
{48000, 2, 0, false, "zero frame size"},
{48000, 2, -1, false, "negative frame size"},
{48000, 2, 999999, true, "extremely large frame size"},
{48000, 2, 960, true, "valid configuration"},
{44100, 1, 441, true, "valid mono configuration"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
err := ValidateInputIPCConfig(tc.sampleRate, tc.channels, tc.frameSize)
if tc.shouldPass {
assert.NoError(t, err, "Should accept valid config: %s", tc.description)
} else {
assert.Error(t, err, "Should reject invalid config: %s", tc.description)
}
})
}
}
// testAtomicOperationConsistency tests atomic operations under high concurrency
func testAtomicOperationConsistency(t *testing.T) {
var counter int64
var wg sync.WaitGroup
const numGoroutines = 100
const incrementsPerGoroutine = 1000
// Launch multiple goroutines performing atomic operations
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < incrementsPerGoroutine; j++ {
atomic.AddInt64(&counter, 1)
}
}()
}
wg.Wait()
// Verify final count is correct
expected := int64(numGoroutines * incrementsPerGoroutine)
actual := atomic.LoadInt64(&counter)
assert.Equal(t, expected, actual, "Atomic operations should be consistent")
}
// TestErrorRecoveryScenarios tests various error recovery scenarios
func TestErrorRecoveryScenarios(t *testing.T) {
tests := []struct {
name string
testFunc func(t *testing.T)
}{
{"NetworkConnectionLoss", testNetworkConnectionLossRecovery},
{"ProcessCrashRecovery", testProcessCrashRecovery},
{"ResourceExhaustionRecovery", testResourceExhaustionRecovery},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.testFunc(t)
})
}
}
// testNetworkConnectionLossRecovery tests recovery from network connection loss
func testNetworkConnectionLossRecovery(t *testing.T) {
// Create a temporary socket that we can close to simulate connection loss
tempDir := t.TempDir()
socketPath := fmt.Sprintf("%s/test_recovery.sock", tempDir)
// Create and immediately close a socket to test connection failure
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Skipf("Cannot create test socket: %v", err)
}
listener.Close() // Close immediately to simulate connection loss
// Remove socket file to ensure connection will fail
os.Remove(socketPath)
// Test that components handle connection loss gracefully
manager := NewAudioInputIPCManager()
require.NotNil(t, manager)
// This should handle the connection failure gracefully
err = manager.Start()
if err != nil {
t.Logf("Expected connection failure handled: %v", err)
}
// Cleanup
manager.Stop()
}
// testProcessCrashRecovery tests recovery from process crashes
func testProcessCrashRecovery(t *testing.T) {
if testing.Short() {
t.Skip("Skipping process crash test in short mode")
}
supervisor := NewAudioOutputSupervisor()
require.NotNil(t, supervisor)
// Start supervisor (will likely fail in test environment, but should handle gracefully)
err := supervisor.Start()
if err != nil {
t.Logf("Supervisor start failed as expected in test environment: %v", err)
}
// Verify supervisor can be stopped cleanly even after start failure
supervisor.Stop()
assert.False(t, supervisor.IsRunning())
}
// testResourceExhaustionRecovery tests recovery from resource exhaustion
func testResourceExhaustionRecovery(t *testing.T) {
// Test with resource constraints
manager := NewAudioInputIPCManager()
require.NotNil(t, manager)
// Simulate resource exhaustion by rapid start/stop cycles
for i := 0; i < 20; i++ {
err := manager.Start()
if err != nil {
t.Logf("Resource exhaustion cycle %d: %v", i, err)
}
manager.Stop()
// No delay to stress test resource management
}
// Verify system can still function after resource stress
err := manager.Start()
if err != nil {
t.Logf("Final start after resource stress: %v", err)
}
manager.Stop()
assert.False(t, manager.IsRunning())
}

View File

@ -140,17 +140,17 @@ func (r *AudioRelay) relayLoop() {
for {
select {
case <-r.ctx.Done():
r.logger.Debug().Msg("audio relay loop stopping")
r.logger.Debug().Msg("Audio relay loop stopping")
return
default:
frame, err := r.client.ReceiveFrame()
if err != nil {
consecutiveErrors++
r.logger.Error().Err(err).Int("consecutive_errors", consecutiveErrors).Msg("error reading frame from audio output server")
r.logger.Error().Err(err).Int("consecutive_errors", consecutiveErrors).Msg("Error reading frame from audio output server")
r.incrementDropped()
if consecutiveErrors >= maxConsecutiveErrors {
r.logger.Error().Int("consecutive_errors", consecutiveErrors).Int("max_errors", maxConsecutiveErrors).Msg("too many consecutive read errors, stopping audio relay")
r.logger.Error().Msgf("Too many consecutive read errors (%d/%d), stopping audio relay", consecutiveErrors, maxConsecutiveErrors)
return
}
time.Sleep(GetConfig().ShortSleepDuration)
@ -159,7 +159,7 @@ func (r *AudioRelay) relayLoop() {
consecutiveErrors = 0
if err := r.forwardToWebRTC(frame); err != nil {
r.logger.Warn().Err(err).Msg("failed to forward frame to webrtc")
r.logger.Warn().Err(err).Msg("Failed to forward frame to WebRTC")
r.incrementDropped()
} else {
r.incrementRelayed()
@ -170,13 +170,6 @@ func (r *AudioRelay) relayLoop() {
// forwardToWebRTC forwards a frame to the WebRTC audio track
func (r *AudioRelay) forwardToWebRTC(frame []byte) error {
// Use ultra-fast validation for critical audio path
if err := ValidateAudioFrame(frame); err != nil {
r.incrementDropped()
r.logger.Debug().Err(err).Msg("invalid frame data in relay")
return err
}
r.mutex.RLock()
defer r.mutex.RUnlock()

View File

@ -4,12 +4,17 @@
package audio
import (
"context"
"fmt"
"os"
"os/exec"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// Restart configuration is now retrieved from centralized config
@ -31,17 +36,30 @@ func getMaxRestartDelay() time.Duration {
// AudioOutputSupervisor manages the audio output server subprocess lifecycle
type AudioOutputSupervisor struct {
*BaseSupervisor
ctx context.Context
cancel context.CancelFunc
logger *zerolog.Logger
mutex sync.RWMutex
running int32
// Process management
cmd *exec.Cmd
processPID int
// Restart management
restartAttempts []time.Time
lastExitCode int
lastExitTime time.Time
// Channel management
stopChan chan struct{}
// Channels for coordination
processDone chan struct{}
stopChan chan struct{}
stopChanClosed bool // Track if stopChan is closed
processDoneClosed bool // Track if processDone is closed
// Process monitoring
processMonitor *ProcessMonitor
// Callbacks
onProcessStart func(pid int)
onProcessExit func(pid int, exitCode int, crashed bool)
@ -50,11 +68,16 @@ type AudioOutputSupervisor struct {
// NewAudioOutputSupervisor creates a new audio output server supervisor
func NewAudioOutputSupervisor() *AudioOutputSupervisor {
ctx, cancel := context.WithCancel(context.Background())
logger := logging.GetDefaultLogger().With().Str("component", AudioOutputSupervisorComponent).Logger()
return &AudioOutputSupervisor{
BaseSupervisor: NewBaseSupervisor("audio-output-supervisor"),
restartAttempts: make([]time.Time, 0),
stopChan: make(chan struct{}),
processDone: make(chan struct{}),
ctx: ctx,
cancel: cancel,
logger: &logger,
processDone: make(chan struct{}),
stopChan: make(chan struct{}),
processMonitor: GetProcessMonitor(),
}
}
@ -78,8 +101,7 @@ func (s *AudioOutputSupervisor) Start() error {
return fmt.Errorf("audio output supervisor is already running")
}
s.logSupervisorStart()
s.createContext()
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("starting component")
// Recreate channels in case they were closed by a previous Stop() call
s.mutex.Lock()
@ -87,8 +109,12 @@ func (s *AudioOutputSupervisor) Start() error {
s.stopChan = make(chan struct{})
s.stopChanClosed = false // Reset channel closed flag
s.processDoneClosed = false // Reset channel closed flag
// Recreate context as well since it might have been cancelled
s.ctx, s.cancel = context.WithCancel(context.Background())
// Reset restart tracking on start
s.restartAttempts = s.restartAttempts[:0]
s.lastExitCode = 0
s.lastExitTime = time.Time{}
s.mutex.Unlock()
// Start the supervision loop
@ -104,7 +130,7 @@ func (s *AudioOutputSupervisor) Stop() {
return // Already stopped
}
s.logSupervisorStop()
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("stopping component")
// Signal stop and wait for cleanup
s.mutex.Lock()
@ -113,7 +139,7 @@ func (s *AudioOutputSupervisor) Stop() {
s.stopChanClosed = true
}
s.mutex.Unlock()
s.cancelContext()
s.cancel()
// Wait for process to exit
select {
@ -127,11 +153,61 @@ func (s *AudioOutputSupervisor) Stop() {
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component stopped")
}
// GetProcessMetrics returns current process metrics with audio-output-server name
// IsRunning returns true if the supervisor is running
func (s *AudioOutputSupervisor) IsRunning() bool {
return atomic.LoadInt32(&s.running) == 1
}
// GetProcessPID returns the current process PID (0 if not running)
func (s *AudioOutputSupervisor) GetProcessPID() int {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.processPID
}
// GetLastExitInfo returns information about the last process exit
func (s *AudioOutputSupervisor) GetLastExitInfo() (exitCode int, exitTime time.Time) {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.lastExitCode, s.lastExitTime
}
// GetProcessMetrics returns current process metrics if the process is running
func (s *AudioOutputSupervisor) GetProcessMetrics() *ProcessMetrics {
metrics := s.BaseSupervisor.GetProcessMetrics()
metrics.ProcessName = "audio-output-server"
return metrics
s.mutex.RLock()
pid := s.processPID
s.mutex.RUnlock()
if pid == 0 {
// Return default metrics when no process is running
return &ProcessMetrics{
PID: 0,
CPUPercent: 0.0,
MemoryRSS: 0,
MemoryVMS: 0,
MemoryPercent: 0.0,
Timestamp: time.Now(),
ProcessName: "audio-output-server",
}
}
metrics := s.processMonitor.GetCurrentMetrics()
for _, metric := range metrics {
if metric.PID == pid {
return &metric
}
}
// Return default metrics if process not found in monitor
return &ProcessMetrics{
PID: pid,
CPUPercent: 0.0,
MemoryRSS: 0,
MemoryVMS: 0,
MemoryPercent: 0.0,
Timestamp: time.Now(),
ProcessName: "audio-output-server",
}
}
// supervisionLoop is the main supervision loop
@ -288,10 +364,10 @@ func (s *AudioOutputSupervisor) waitForProcessExit() {
s.processMonitor.RemoveProcess(pid)
if crashed {
s.logger.Error().Int("pid", pid).Int("exit_code", exitCode).Msg("audio output server process crashed")
s.logger.Error().Int("pid", pid).Int("exit_code", exitCode).Msg("audio server process crashed")
s.recordRestartAttempt()
} else {
s.logger.Info().Int("pid", pid).Msg("audio output server process exited gracefully")
s.logger.Info().Int("pid", pid).Msg("audio server process exited gracefully")
}
if s.onProcessExit != nil {
@ -310,11 +386,11 @@ func (s *AudioOutputSupervisor) terminateProcess() {
return
}
s.logger.Info().Int("pid", pid).Msg("terminating audio output server process")
s.logger.Info().Int("pid", pid).Msg("terminating audio server process")
// Send SIGTERM first
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
s.logger.Warn().Err(err).Int("pid", pid).Msg("failed to send SIGTERM to audio output server process")
s.logger.Warn().Err(err).Int("pid", pid).Msg("failed to send SIGTERM")
}
// Wait for graceful shutdown

View File

@ -1,11 +1,7 @@
//go:build cgo || arm
// +build cgo arm
package audio
import (
"errors"
"fmt"
"time"
)
@ -14,8 +10,6 @@ var (
ErrInvalidAudioQuality = errors.New("invalid audio quality level")
ErrInvalidFrameSize = errors.New("invalid frame size")
ErrInvalidFrameData = errors.New("invalid frame data")
ErrFrameDataEmpty = errors.New("invalid frame data: frame data is empty")
ErrFrameDataTooLarge = errors.New("invalid frame data: exceeds maximum")
ErrInvalidBufferSize = errors.New("invalid buffer size")
ErrInvalidPriority = errors.New("invalid priority value")
ErrInvalidLatency = errors.New("invalid latency value")
@ -24,18 +18,30 @@ var (
ErrInvalidMetricsInterval = errors.New("invalid metrics interval")
ErrInvalidSampleRate = errors.New("invalid sample rate")
ErrInvalidChannels = errors.New("invalid channels")
ErrInvalidBitrate = errors.New("invalid bitrate")
ErrInvalidFrameDuration = errors.New("invalid frame duration")
ErrInvalidOffset = errors.New("invalid offset")
ErrInvalidLength = errors.New("invalid length")
)
// ValidateAudioQuality validates audio quality enum values with enhanced checks
// ValidateAudioQuality validates audio quality enum values
func ValidateAudioQuality(quality AudioQuality) error {
// Validate enum range
if quality < AudioQualityLow || quality > AudioQualityUltra {
return fmt.Errorf("%w: quality value %d outside valid range [%d, %d]",
ErrInvalidAudioQuality, int(quality), int(AudioQualityLow), int(AudioQualityUltra))
switch quality {
case AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra:
return nil
default:
return ErrInvalidAudioQuality
}
}
// ValidateFrameData validates audio frame data
func ValidateFrameData(data []byte) error {
if len(data) == 0 {
return ErrInvalidFrameData
}
// Use a reasonable default if config is not available
maxFrameSize := 4096
if config := GetConfig(); config != nil {
maxFrameSize = config.MaxAudioFrameSize
}
if len(data) > maxFrameSize {
return ErrInvalidFrameSize
}
return nil
}
@ -49,63 +55,73 @@ func ValidateZeroCopyFrame(frame *ZeroCopyAudioFrame) error {
if len(data) == 0 {
return ErrInvalidFrameData
}
// Use config value
maxFrameSize := GetConfig().MaxAudioFrameSize
// Use a reasonable default if config is not available
maxFrameSize := 4096
if config := GetConfig(); config != nil {
maxFrameSize = config.MaxAudioFrameSize
}
if len(data) > maxFrameSize {
return ErrInvalidFrameSize
}
return nil
}
// ValidateBufferSize validates buffer size parameters with enhanced boundary checks
// ValidateBufferSize validates buffer size parameters
func ValidateBufferSize(size int) error {
if size <= 0 {
return fmt.Errorf("%w: buffer size %d must be positive", ErrInvalidBufferSize, size)
return ErrInvalidBufferSize
}
config := GetConfig()
// Use SocketMaxBuffer as the upper limit for general buffer validation
// This allows for socket buffers while still preventing extremely large allocations
if size > config.SocketMaxBuffer {
return fmt.Errorf("%w: buffer size %d exceeds maximum %d",
ErrInvalidBufferSize, size, config.SocketMaxBuffer)
// Use a reasonable default if config is not available
maxBuffer := 262144 // 256KB default
if config := GetConfig(); config != nil {
maxBuffer = config.SocketMaxBuffer
}
if size > maxBuffer {
return ErrInvalidBufferSize
}
return nil
}
// ValidateThreadPriority validates thread priority values with system limits
// ValidateThreadPriority validates thread priority values
func ValidateThreadPriority(priority int) error {
const minPriority, maxPriority = -20, 19
// 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 {
return fmt.Errorf("%w: priority %d outside valid range [%d, %d]",
ErrInvalidPriority, priority, minPriority, maxPriority)
return ErrInvalidPriority
}
return nil
}
// ValidateLatency validates latency duration values with reasonable bounds
// ValidateLatency validates latency values
func ValidateLatency(latency time.Duration) error {
if latency < 0 {
return fmt.Errorf("%w: latency %v cannot be negative", ErrInvalidLatency, latency)
return ErrInvalidLatency
}
config := GetConfig()
minLatency := time.Millisecond // Minimum reasonable latency
if latency > 0 && latency < minLatency {
return fmt.Errorf("%w: latency %v below minimum %v",
ErrInvalidLatency, latency, minLatency)
// Use a reasonable default if config is not available
maxLatency := 500 * time.Millisecond
if config := GetConfig(); config != nil {
maxLatency = config.MaxLatency
}
if latency > config.MaxLatency {
return fmt.Errorf("%w: latency %v exceeds maximum %v",
ErrInvalidLatency, latency, config.MaxLatency)
if latency > maxLatency {
return ErrInvalidLatency
}
return nil
}
// ValidateMetricsInterval validates metrics update interval
func ValidateMetricsInterval(interval time.Duration) error {
// Use config values
config := GetConfig()
minInterval := config.MinMetricsUpdateInterval
maxInterval := config.MaxMetricsUpdateInterval
// Use reasonable defaults if config is not available
minInterval := 100 * time.Millisecond
maxInterval := 10 * time.Second
if config := GetConfig(); config != nil {
minInterval = config.MinMetricsUpdateInterval
maxInterval = config.MaxMetricsUpdateInterval
}
if interval < minInterval {
return ErrInvalidMetricsInterval
}
@ -127,7 +143,10 @@ func ValidateAdaptiveBufferConfig(minSize, maxSize, defaultSize int) error {
return ErrInvalidBufferSize
}
// Validate against global limits
maxBuffer := GetConfig().SocketMaxBuffer
maxBuffer := 262144 // 256KB default
if config := GetConfig(); config != nil {
maxBuffer = config.SocketMaxBuffer
}
if maxSize > maxBuffer {
return ErrInvalidBufferSize
}
@ -136,11 +155,15 @@ func ValidateAdaptiveBufferConfig(minSize, maxSize, defaultSize int) error {
// ValidateInputIPCConfig validates input IPC configuration
func ValidateInputIPCConfig(sampleRate, channels, frameSize int) error {
// Use config values
config := GetConfig()
minSampleRate := config.MinSampleRate
maxSampleRate := config.MaxSampleRate
maxChannels := config.MaxChannels
// Use reasonable defaults if config is not available
minSampleRate := 8000
maxSampleRate := 48000
maxChannels := 8
if config := GetConfig(); config != nil {
minSampleRate = config.MinSampleRate
maxSampleRate = config.MaxSampleRate
maxChannels = config.MaxChannels
}
if sampleRate < minSampleRate || sampleRate > maxSampleRate {
return ErrInvalidSampleRate
}
@ -152,196 +175,3 @@ func ValidateInputIPCConfig(sampleRate, channels, frameSize int) error {
}
return nil
}
// ValidateOutputIPCConfig validates output IPC configuration
func ValidateOutputIPCConfig(sampleRate, channels, frameSize int) error {
// Use config values
config := GetConfig()
minSampleRate := config.MinSampleRate
maxSampleRate := config.MaxSampleRate
maxChannels := config.MaxChannels
if sampleRate < minSampleRate || sampleRate > maxSampleRate {
return ErrInvalidSampleRate
}
if channels < 1 || channels > maxChannels {
return ErrInvalidChannels
}
if frameSize <= 0 {
return ErrInvalidFrameSize
}
return nil
}
// ValidateLatencyConfig validates latency monitor configuration
func ValidateLatencyConfig(config LatencyConfig) error {
if err := ValidateLatency(config.TargetLatency); err != nil {
return err
}
if err := ValidateLatency(config.MaxLatency); err != nil {
return err
}
if config.TargetLatency >= config.MaxLatency {
return ErrInvalidLatency
}
if err := ValidateMetricsInterval(config.OptimizationInterval); err != nil {
return err
}
if config.HistorySize <= 0 {
return ErrInvalidBufferSize
}
if config.JitterThreshold < 0 {
return ErrInvalidLatency
}
if config.AdaptiveThreshold < 0 || config.AdaptiveThreshold > 1.0 {
return ErrInvalidConfiguration
}
return nil
}
// ValidateSampleRate validates audio sample rate values
func ValidateSampleRate(sampleRate int) error {
if sampleRate <= 0 {
return fmt.Errorf("%w: sample rate %d must be positive", ErrInvalidSampleRate, sampleRate)
}
config := GetConfig()
validRates := config.ValidSampleRates
for _, rate := range validRates {
if sampleRate == rate {
return nil
}
}
return fmt.Errorf("%w: sample rate %d not in supported rates %v",
ErrInvalidSampleRate, sampleRate, validRates)
}
// ValidateChannelCount validates audio channel count
func ValidateChannelCount(channels int) error {
if channels <= 0 {
return fmt.Errorf("%w: channel count %d must be positive", ErrInvalidChannels, channels)
}
config := GetConfig()
if channels > config.MaxChannels {
return fmt.Errorf("%w: channel count %d exceeds maximum %d",
ErrInvalidChannels, channels, config.MaxChannels)
}
return nil
}
// ValidateBitrate validates audio bitrate values (expects kbps)
func ValidateBitrate(bitrate int) error {
if bitrate <= 0 {
return fmt.Errorf("%w: bitrate %d must be positive", ErrInvalidBitrate, bitrate)
}
config := GetConfig()
// Convert kbps to bps for comparison with config limits
bitrateInBps := bitrate * 1000
if bitrateInBps < config.MinOpusBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, config.MinOpusBitrate)
}
if bitrateInBps > config.MaxOpusBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) exceeds maximum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, config.MaxOpusBitrate)
}
return nil
}
// ValidateFrameDuration validates frame duration values
func ValidateFrameDuration(duration time.Duration) error {
if duration <= 0 {
return fmt.Errorf("%w: frame duration %v must be positive", ErrInvalidFrameDuration, duration)
}
config := GetConfig()
if duration < config.MinFrameDuration {
return fmt.Errorf("%w: frame duration %v below minimum %v",
ErrInvalidFrameDuration, duration, config.MinFrameDuration)
}
if duration > config.MaxFrameDuration {
return fmt.Errorf("%w: frame duration %v exceeds maximum %v",
ErrInvalidFrameDuration, duration, config.MaxFrameDuration)
}
return nil
}
// ValidateAudioConfigComplete performs comprehensive audio configuration validation
func ValidateAudioConfigComplete(config AudioConfig) error {
if err := ValidateAudioQuality(config.Quality); err != nil {
return fmt.Errorf("quality validation failed: %w", err)
}
if err := ValidateBitrate(config.Bitrate); err != nil {
return fmt.Errorf("bitrate validation failed: %w", err)
}
if err := ValidateSampleRate(config.SampleRate); err != nil {
return fmt.Errorf("sample rate validation failed: %w", err)
}
if err := ValidateChannelCount(config.Channels); err != nil {
return fmt.Errorf("channel count validation failed: %w", err)
}
if err := ValidateFrameDuration(config.FrameSize); err != nil {
return fmt.Errorf("frame duration validation failed: %w", err)
}
return nil
}
// ValidateAudioConfigConstants validates audio configuration constants
func ValidateAudioConfigConstants(config *AudioConfigConstants) error {
// Validate that audio quality constants are within valid ranges
for _, quality := range []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} {
if err := ValidateAudioQuality(quality); err != nil {
return fmt.Errorf("invalid audio quality constant %v: %w", quality, err)
}
}
// Validate configuration values if config is provided
if config != nil {
if config.MaxFrameSize <= 0 {
return fmt.Errorf("invalid MaxFrameSize: %d", config.MaxFrameSize)
}
if config.SampleRate <= 0 {
return fmt.Errorf("invalid SampleRate: %d", config.SampleRate)
}
}
return nil
}
// Cached max frame size to avoid function call overhead in hot paths
var cachedMaxFrameSize int
// Note: Validation cache is initialized on first use to avoid init function
// InitValidationCache initializes cached validation values with actual config
func InitValidationCache() {
cachedMaxFrameSize = GetConfig().MaxAudioFrameSize
}
// ValidateAudioFrame provides optimized validation for audio frame data
// This is the primary validation function used in all audio processing paths
//
// Performance optimizations:
// - Uses cached config value to eliminate function call overhead
// - Single branch condition for optimal CPU pipeline efficiency
// - Inlined length checks for minimal overhead
//
//go:inline
func ValidateAudioFrame(data []byte) error {
// Initialize cache on first use if not already done
if cachedMaxFrameSize == 0 {
InitValidationCache()
}
// Optimized validation with pre-allocated error messages for minimal overhead
dataLen := len(data)
if dataLen == 0 {
return ErrFrameDataEmpty
}
if dataLen > cachedMaxFrameSize {
return ErrFrameDataTooLarge
}
return nil
}
// WrapWithMetadata wraps error with metadata for enhanced validation context
func WrapWithMetadata(err error, component, operation string, metadata map[string]interface{}) error {
if err == nil {
return nil
}
return fmt.Errorf("%s.%s: %w (metadata: %+v)", component, operation, err, metadata)
}

View File

@ -0,0 +1,290 @@
package audio
import (
"errors"
"fmt"
"time"
"unsafe"
"github.com/rs/zerolog"
)
// Enhanced validation errors with more specific context
var (
ErrInvalidFrameLength = errors.New("invalid frame length")
ErrFrameDataCorrupted = errors.New("frame data appears corrupted")
ErrBufferAlignment = errors.New("buffer alignment invalid")
ErrInvalidSampleFormat = errors.New("invalid sample format")
ErrInvalidTimestamp = errors.New("invalid timestamp")
ErrConfigurationMismatch = errors.New("configuration mismatch")
ErrResourceExhaustion = errors.New("resource exhaustion detected")
ErrInvalidPointer = errors.New("invalid pointer")
ErrBufferOverflow = errors.New("buffer overflow detected")
ErrInvalidState = errors.New("invalid state")
)
// ValidationLevel defines the level of validation to perform
type ValidationLevel int
const (
ValidationMinimal ValidationLevel = iota // Only critical safety checks
ValidationStandard // Standard validation for production
ValidationStrict // Comprehensive validation for debugging
)
// ValidationConfig controls validation behavior
type ValidationConfig struct {
Level ValidationLevel
EnableRangeChecks bool
EnableAlignmentCheck bool
EnableDataIntegrity bool
MaxValidationTime time.Duration
}
// GetValidationConfig returns the current validation configuration
func GetValidationConfig() ValidationConfig {
return ValidationConfig{
Level: ValidationStandard,
EnableRangeChecks: true,
EnableAlignmentCheck: true,
EnableDataIntegrity: false, // Disabled by default for performance
MaxValidationTime: 5 * time.Second, // Default validation timeout
}
}
// ValidateAudioFrameFast performs minimal validation for performance-critical paths
func ValidateAudioFrameFast(data []byte) error {
if len(data) == 0 {
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)
}
return nil
}
// ValidateAudioFrameComprehensive performs thorough validation
func ValidateAudioFrameComprehensive(data []byte, expectedSampleRate int, expectedChannels int) error {
validationConfig := GetValidationConfig()
start := time.Now()
// Timeout protection for validation
defer func() {
if time.Since(start) > validationConfig.MaxValidationTime {
// Log validation timeout but don't fail
getValidationLogger().Warn().Dur("duration", time.Since(start)).Msg("validation timeout exceeded")
}
}()
// Basic validation first
if err := ValidateAudioFrameFast(data); err != nil {
return err
}
// Range validation
if validationConfig.EnableRangeChecks {
config := GetConfig()
minFrameSize := 64 // Minimum reasonable frame size
if len(data) < minFrameSize {
return fmt.Errorf("%w: frame size %d below minimum %d", ErrInvalidFrameSize, len(data), minFrameSize)
}
// Validate frame length matches expected sample format
expectedFrameSize := (expectedSampleRate * expectedChannels * 2) / 1000 * int(config.AudioQualityMediumFrameSize/time.Millisecond)
tolerance := 512 // Frame size tolerance in bytes
if abs(len(data)-expectedFrameSize) > tolerance {
return fmt.Errorf("%w: frame size %d doesn't match expected %d (±%d)", ErrInvalidFrameLength, len(data), expectedFrameSize, tolerance)
}
}
// Alignment validation for ARM32 compatibility
if validationConfig.EnableAlignmentCheck {
if uintptr(unsafe.Pointer(&data[0]))%4 != 0 {
return fmt.Errorf("%w: buffer not 4-byte aligned for ARM32", ErrBufferAlignment)
}
}
// Data integrity checks (expensive, only for debugging)
if validationConfig.EnableDataIntegrity && validationConfig.Level == ValidationStrict {
if err := validateAudioDataIntegrity(data, expectedChannels); err != nil {
return err
}
}
return nil
}
// ValidateZeroCopyFrameEnhanced performs enhanced zero-copy frame validation
func ValidateZeroCopyFrameEnhanced(frame *ZeroCopyAudioFrame) error {
if frame == nil {
return fmt.Errorf("%w: frame is nil", ErrInvalidPointer)
}
// Check reference count validity
frame.mutex.RLock()
refCount := frame.refCount
length := frame.length
capacity := frame.capacity
frame.mutex.RUnlock()
if refCount <= 0 {
return fmt.Errorf("%w: invalid reference count %d", ErrInvalidState, refCount)
}
if length < 0 || capacity < 0 {
return fmt.Errorf("%w: negative length (%d) or capacity (%d)", ErrInvalidState, length, capacity)
}
if length > capacity {
return fmt.Errorf("%w: length %d exceeds capacity %d", ErrBufferOverflow, length, capacity)
}
// Validate the underlying data
data := frame.Data()
return ValidateAudioFrameFast(data)
}
// ValidateBufferBounds performs bounds checking with overflow protection
func ValidateBufferBounds(buffer []byte, offset, length int) error {
if buffer == nil {
return fmt.Errorf("%w: buffer is nil", ErrInvalidPointer)
}
if offset < 0 {
return fmt.Errorf("%w: negative offset %d", ErrInvalidState, offset)
}
if length < 0 {
return fmt.Errorf("%w: negative length %d", ErrInvalidState, length)
}
// Check for integer overflow
if offset > len(buffer) {
return fmt.Errorf("%w: offset %d exceeds buffer length %d", ErrBufferOverflow, offset, len(buffer))
}
// Safe addition check for overflow
if offset+length < offset || offset+length > len(buffer) {
return fmt.Errorf("%w: range [%d:%d] exceeds buffer length %d", ErrBufferOverflow, offset, offset+length, len(buffer))
}
return nil
}
// ValidateAudioConfiguration performs comprehensive configuration validation
func ValidateAudioConfiguration(config AudioConfig) error {
if err := ValidateAudioQuality(config.Quality); err != nil {
return fmt.Errorf("quality validation failed: %w", err)
}
configConstants := GetConfig()
// Validate bitrate ranges
minBitrate := 6000 // Minimum Opus bitrate
maxBitrate := 510000 // Maximum Opus bitrate
if config.Bitrate < minBitrate || config.Bitrate > maxBitrate {
return fmt.Errorf("%w: bitrate %d outside valid range [%d, %d]", ErrInvalidConfiguration, config.Bitrate, minBitrate, maxBitrate)
}
// Validate sample rate
validSampleRates := []int{8000, 12000, 16000, 24000, 48000}
validSampleRate := false
for _, rate := range validSampleRates {
if config.SampleRate == rate {
validSampleRate = true
break
}
}
if !validSampleRate {
return fmt.Errorf("%w: sample rate %d not in supported rates %v", ErrInvalidSampleRate, config.SampleRate, validSampleRates)
}
// 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)
}
// Validate frame size
minFrameSize := 10 * time.Millisecond // Minimum frame duration
maxFrameSize := 100 * time.Millisecond // Maximum frame duration
if config.FrameSize < minFrameSize || config.FrameSize > maxFrameSize {
return fmt.Errorf("%w: frame size %v outside valid range [%v, %v]", ErrInvalidConfiguration, config.FrameSize, minFrameSize, maxFrameSize)
}
return nil
}
// ValidateResourceLimits checks if system resources are within acceptable limits
func ValidateResourceLimits() error {
config := GetConfig()
// Check buffer pool sizes
framePoolStats := GetAudioBufferPoolStats()
if framePoolStats.FramePoolSize > int64(config.MaxPoolSize*2) {
return fmt.Errorf("%w: frame pool size %d exceeds safe limit %d", ErrResourceExhaustion, framePoolStats.FramePoolSize, config.MaxPoolSize*2)
}
// Check zero-copy pool allocation count
zeroCopyStats := GetGlobalZeroCopyPoolStats()
if zeroCopyStats.AllocationCount > int64(config.MaxPoolSize*3) {
return fmt.Errorf("%w: zero-copy allocations %d exceed safe limit %d", ErrResourceExhaustion, zeroCopyStats.AllocationCount, config.MaxPoolSize*3)
}
return nil
}
// validateAudioDataIntegrity performs expensive data integrity checks
func validateAudioDataIntegrity(data []byte, channels int) error {
if len(data)%2 != 0 {
return fmt.Errorf("%w: odd number of bytes for 16-bit samples", ErrInvalidSampleFormat)
}
if len(data)%(channels*2) != 0 {
return fmt.Errorf("%w: data length %d not aligned to channel count %d", ErrInvalidSampleFormat, len(data), channels)
}
// Check for obvious corruption patterns (all zeros, all max values)
sampleCount := len(data) / 2
zeroCount := 0
maxCount := 0
for i := 0; i < len(data); i += 2 {
sample := int16(data[i]) | int16(data[i+1])<<8
switch sample {
case 0:
zeroCount++
case 32767, -32768:
maxCount++
}
}
// Flag suspicious patterns
if zeroCount > sampleCount*9/10 {
return fmt.Errorf("%w: %d%% zero samples suggests silence or corruption", ErrFrameDataCorrupted, (zeroCount*100)/sampleCount)
}
if maxCount > sampleCount/10 {
return fmt.Errorf("%w: %d%% max-value samples suggests clipping or corruption", ErrFrameDataCorrupted, (maxCount*100)/sampleCount)
}
return nil
}
// Helper function for absolute value
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
// getValidationLogger returns a logger for validation operations
func getValidationLogger() *zerolog.Logger {
// Return a basic logger for validation
logger := zerolog.New(nil).With().Timestamp().Logger()
return &logger
}

View File

@ -1,541 +0,0 @@
//go:build cgo
// +build cgo
package audio
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestValidationFunctions provides comprehensive testing of all validation functions
// to ensure they catch breaking changes and regressions effectively
func TestValidationFunctions(t *testing.T) {
// Initialize validation cache for testing
InitValidationCache()
tests := []struct {
name string
testFunc func(t *testing.T)
}{
{"AudioQualityValidation", testAudioQualityValidation},
{"FrameDataValidation", testFrameDataValidation},
{"BufferSizeValidation", testBufferSizeValidation},
{"ThreadPriorityValidation", testThreadPriorityValidation},
{"LatencyValidation", testLatencyValidation},
{"MetricsIntervalValidation", testMetricsIntervalValidation},
{"SampleRateValidation", testSampleRateValidation},
{"ChannelCountValidation", testChannelCountValidation},
{"BitrateValidation", testBitrateValidation},
{"FrameDurationValidation", testFrameDurationValidation},
{"IPCConfigValidation", testIPCConfigValidation},
{"AdaptiveBufferConfigValidation", testAdaptiveBufferConfigValidation},
{"AudioConfigCompleteValidation", testAudioConfigCompleteValidation},
{"ZeroCopyFrameValidation", testZeroCopyFrameValidation},
{"AudioFrameFastValidation", testAudioFrameFastValidation},
{"ErrorWrappingValidation", testErrorWrappingValidation},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.testFunc(t)
})
}
}
// testAudioQualityValidation tests audio quality validation with boundary conditions
func testAudioQualityValidation(t *testing.T) {
// Test valid quality levels
validQualities := []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra}
for _, quality := range validQualities {
err := ValidateAudioQuality(quality)
assert.NoError(t, err, "Valid quality %d should pass validation", quality)
}
// Test invalid quality levels
invalidQualities := []AudioQuality{-1, 4, 100, -100}
for _, quality := range invalidQualities {
err := ValidateAudioQuality(quality)
assert.Error(t, err, "Invalid quality %d should fail validation", quality)
assert.Contains(t, err.Error(), "invalid audio quality level", "Error should mention audio quality")
}
}
// testFrameDataValidation tests frame data validation with various edge cases using modern validation
func testFrameDataValidation(t *testing.T) {
config := GetConfig()
// Test empty data
err := ValidateAudioFrame([]byte{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "frame data is empty")
// Test data above maximum size
largeData := make([]byte, config.MaxAudioFrameSize+1)
err = ValidateAudioFrame(largeData)
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum")
// Test valid data
validData := make([]byte, 1000) // Within bounds
if len(validData) <= config.MaxAudioFrameSize {
err = ValidateAudioFrame(validData)
assert.NoError(t, err)
}
}
// testBufferSizeValidation tests buffer size validation
func testBufferSizeValidation(t *testing.T) {
config := GetConfig()
// Test negative and zero sizes
invalidSizes := []int{-1, -100, 0}
for _, size := range invalidSizes {
err := ValidateBufferSize(size)
assert.Error(t, err, "Buffer size %d should be invalid", size)
assert.Contains(t, err.Error(), "must be positive")
}
// Test size exceeding maximum
err := ValidateBufferSize(config.SocketMaxBuffer + 1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum")
// Test valid sizes
validSizes := []int{1, 1024, 4096, config.SocketMaxBuffer}
for _, size := range validSizes {
err := ValidateBufferSize(size)
assert.NoError(t, err, "Buffer size %d should be valid", size)
}
}
// testThreadPriorityValidation tests thread priority validation
func testThreadPriorityValidation(t *testing.T) {
// Test valid priorities
validPriorities := []int{-20, -10, 0, 10, 19}
for _, priority := range validPriorities {
err := ValidateThreadPriority(priority)
assert.NoError(t, err, "Priority %d should be valid", priority)
}
// Test invalid priorities
invalidPriorities := []int{-21, -100, 20, 100}
for _, priority := range invalidPriorities {
err := ValidateThreadPriority(priority)
assert.Error(t, err, "Priority %d should be invalid", priority)
assert.Contains(t, err.Error(), "outside valid range")
}
}
// testLatencyValidation tests latency validation
func testLatencyValidation(t *testing.T) {
config := GetConfig()
// Test negative latency
err := ValidateLatency(-1 * time.Millisecond)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot be negative")
// Test zero latency (should be valid)
err = ValidateLatency(0)
assert.NoError(t, err)
// Test very small positive latency
err = ValidateLatency(500 * time.Microsecond)
assert.Error(t, err)
assert.Contains(t, err.Error(), "below minimum")
// Test latency exceeding maximum
err = ValidateLatency(config.MaxLatency + time.Second)
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum")
// Test valid latencies
validLatencies := []time.Duration{
1 * time.Millisecond,
10 * time.Millisecond,
100 * time.Millisecond,
config.MaxLatency,
}
for _, latency := range validLatencies {
err := ValidateLatency(latency)
assert.NoError(t, err, "Latency %v should be valid", latency)
}
}
// testMetricsIntervalValidation tests metrics interval validation
func testMetricsIntervalValidation(t *testing.T) {
config := GetConfig()
// Test interval below minimum
err := ValidateMetricsInterval(config.MinMetricsUpdateInterval - time.Millisecond)
assert.Error(t, err)
// Test interval above maximum
err = ValidateMetricsInterval(config.MaxMetricsUpdateInterval + time.Second)
assert.Error(t, err)
// Test valid intervals
validIntervals := []time.Duration{
config.MinMetricsUpdateInterval,
config.MaxMetricsUpdateInterval,
(config.MinMetricsUpdateInterval + config.MaxMetricsUpdateInterval) / 2,
}
for _, interval := range validIntervals {
err := ValidateMetricsInterval(interval)
assert.NoError(t, err, "Interval %v should be valid", interval)
}
}
// testSampleRateValidation tests sample rate validation
func testSampleRateValidation(t *testing.T) {
config := GetConfig()
// Test negative and zero sample rates
invalidRates := []int{-1, -48000, 0}
for _, rate := range invalidRates {
err := ValidateSampleRate(rate)
assert.Error(t, err, "Sample rate %d should be invalid", rate)
assert.Contains(t, err.Error(), "must be positive")
}
// Test unsupported sample rates
unsupportedRates := []int{1000, 12345, 96001}
for _, rate := range unsupportedRates {
err := ValidateSampleRate(rate)
assert.Error(t, err, "Sample rate %d should be unsupported", rate)
assert.Contains(t, err.Error(), "not in supported rates")
}
// Test valid sample rates
for _, rate := range config.ValidSampleRates {
err := ValidateSampleRate(rate)
assert.NoError(t, err, "Sample rate %d should be valid", rate)
}
}
// testChannelCountValidation tests channel count validation
func testChannelCountValidation(t *testing.T) {
config := GetConfig()
// Test invalid channel counts
invalidCounts := []int{-1, -10, 0}
for _, count := range invalidCounts {
err := ValidateChannelCount(count)
assert.Error(t, err, "Channel count %d should be invalid", count)
assert.Contains(t, err.Error(), "must be positive")
}
// Test channel count exceeding maximum
err := ValidateChannelCount(config.MaxChannels + 1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum")
// Test valid channel counts
validCounts := []int{1, 2, config.MaxChannels}
for _, count := range validCounts {
err := ValidateChannelCount(count)
assert.NoError(t, err, "Channel count %d should be valid", count)
}
}
// testBitrateValidation tests bitrate validation
func testBitrateValidation(t *testing.T) {
// Test invalid bitrates
invalidBitrates := []int{-1, -1000, 0}
for _, bitrate := range invalidBitrates {
err := ValidateBitrate(bitrate)
assert.Error(t, err, "Bitrate %d should be invalid", bitrate)
assert.Contains(t, err.Error(), "must be positive")
}
// Test bitrate below minimum (in kbps)
err := ValidateBitrate(5) // 5 kbps = 5000 bps < 6000 bps minimum
assert.Error(t, err)
assert.Contains(t, err.Error(), "below minimum")
// Test bitrate above maximum (in kbps)
err = ValidateBitrate(511) // 511 kbps = 511000 bps > 510000 bps maximum
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum")
// Test valid bitrates (in kbps)
validBitrates := []int{
6, // 6 kbps = 6000 bps (minimum)
64, // Medium quality preset
128, // High quality preset
192, // Ultra quality preset
510, // 510 kbps = 510000 bps (maximum)
}
for _, bitrate := range validBitrates {
err := ValidateBitrate(bitrate)
assert.NoError(t, err, "Bitrate %d kbps should be valid", bitrate)
}
}
// testFrameDurationValidation tests frame duration validation
func testFrameDurationValidation(t *testing.T) {
config := GetConfig()
// Test invalid durations
invalidDurations := []time.Duration{-1 * time.Millisecond, -1 * time.Second, 0}
for _, duration := range invalidDurations {
err := ValidateFrameDuration(duration)
assert.Error(t, err, "Duration %v should be invalid", duration)
assert.Contains(t, err.Error(), "must be positive")
}
// Test duration below minimum
err := ValidateFrameDuration(config.MinFrameDuration - time.Microsecond)
assert.Error(t, err)
assert.Contains(t, err.Error(), "below minimum")
// Test duration above maximum
err = ValidateFrameDuration(config.MaxFrameDuration + time.Second)
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum")
// Test valid durations
validDurations := []time.Duration{
config.MinFrameDuration,
config.MaxFrameDuration,
20 * time.Millisecond, // Common frame duration
}
for _, duration := range validDurations {
err := ValidateFrameDuration(duration)
assert.NoError(t, err, "Duration %v should be valid", duration)
}
}
// testIPCConfigValidation tests IPC configuration validation
func testIPCConfigValidation(t *testing.T) {
config := GetConfig()
// Test invalid configurations for input IPC
invalidConfigs := []struct {
sampleRate, channels, frameSize int
description string
}{
{0, 2, 960, "zero sample rate"},
{48000, 0, 960, "zero channels"},
{48000, 2, 0, "zero frame size"},
{config.MinSampleRate - 1, 2, 960, "sample rate below minimum"},
{config.MaxSampleRate + 1, 2, 960, "sample rate above maximum"},
{48000, config.MaxChannels + 1, 960, "too many channels"},
{48000, -1, 960, "negative channels"},
{48000, 2, -1, "negative frame size"},
}
for _, tc := range invalidConfigs {
// Test input IPC validation
err := ValidateInputIPCConfig(tc.sampleRate, tc.channels, tc.frameSize)
assert.Error(t, err, "Input IPC config should be invalid: %s", tc.description)
// Test output IPC validation
err = ValidateOutputIPCConfig(tc.sampleRate, tc.channels, tc.frameSize)
assert.Error(t, err, "Output IPC config should be invalid: %s", tc.description)
}
// Test valid configuration
err := ValidateInputIPCConfig(48000, 2, 960)
assert.NoError(t, err)
err = ValidateOutputIPCConfig(48000, 2, 960)
assert.NoError(t, err)
}
// testAdaptiveBufferConfigValidation tests adaptive buffer configuration validation
func testAdaptiveBufferConfigValidation(t *testing.T) {
config := GetConfig()
// Test invalid configurations
invalidConfigs := []struct {
minSize, maxSize, defaultSize int
description string
}{
{0, 1024, 512, "zero min size"},
{-1, 1024, 512, "negative min size"},
{512, 0, 256, "zero max size"},
{512, -1, 256, "negative max size"},
{512, 1024, 0, "zero default size"},
{512, 1024, -1, "negative default size"},
{1024, 512, 768, "min >= max"},
{512, 1024, 256, "default < min"},
{512, 1024, 2048, "default > max"},
{512, config.SocketMaxBuffer + 1, 1024, "max exceeds global limit"},
}
for _, tc := range invalidConfigs {
err := ValidateAdaptiveBufferConfig(tc.minSize, tc.maxSize, tc.defaultSize)
assert.Error(t, err, "Config should be invalid: %s", tc.description)
}
// Test valid configuration
err := ValidateAdaptiveBufferConfig(512, 4096, 1024)
assert.NoError(t, err)
}
// testAudioConfigCompleteValidation tests complete audio configuration validation
func testAudioConfigCompleteValidation(t *testing.T) {
// Test valid configuration using actual preset values
validConfig := AudioConfig{
Quality: AudioQualityMedium,
Bitrate: 64, // kbps - matches medium quality preset
SampleRate: 48000,
Channels: 2,
FrameSize: 20 * time.Millisecond,
}
err := ValidateAudioConfigComplete(validConfig)
assert.NoError(t, err)
// Test invalid quality
invalidQualityConfig := validConfig
invalidQualityConfig.Quality = AudioQuality(99)
err = ValidateAudioConfigComplete(invalidQualityConfig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "quality validation failed")
// Test invalid bitrate
invalidBitrateConfig := validConfig
invalidBitrateConfig.Bitrate = -1
err = ValidateAudioConfigComplete(invalidBitrateConfig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "bitrate validation failed")
// Test invalid sample rate
invalidSampleRateConfig := validConfig
invalidSampleRateConfig.SampleRate = 12345
err = ValidateAudioConfigComplete(invalidSampleRateConfig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "sample rate validation failed")
// Test invalid channels
invalidChannelsConfig := validConfig
invalidChannelsConfig.Channels = 0
err = ValidateAudioConfigComplete(invalidChannelsConfig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "channel count validation failed")
// Test invalid frame duration
invalidFrameDurationConfig := validConfig
invalidFrameDurationConfig.FrameSize = -1 * time.Millisecond
err = ValidateAudioConfigComplete(invalidFrameDurationConfig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "frame duration validation failed")
}
// testZeroCopyFrameValidation tests zero-copy frame validation
func testZeroCopyFrameValidation(t *testing.T) {
// Test nil frame
err := ValidateZeroCopyFrame(nil)
assert.Error(t, err)
// Note: We can't easily test ZeroCopyAudioFrame without creating actual instances
// This would require more complex setup, but the validation logic is tested
}
// testAudioFrameFastValidation tests fast audio frame validation
func testAudioFrameFastValidation(t *testing.T) {
config := GetConfig()
// Test empty data
err := ValidateAudioFrame([]byte{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "frame data is empty")
// Test data exceeding maximum size
largeData := make([]byte, config.MaxAudioFrameSize+1)
err = ValidateAudioFrame(largeData)
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum")
// Test valid data
validData := make([]byte, 1000)
err = ValidateAudioFrame(validData)
assert.NoError(t, err)
}
// testErrorWrappingValidation tests error wrapping functionality
func testErrorWrappingValidation(t *testing.T) {
// Test wrapping nil error
wrapped := WrapWithMetadata(nil, "component", "operation", map[string]interface{}{"key": "value"})
assert.Nil(t, wrapped)
// Test wrapping actual error
originalErr := assert.AnError
metadata := map[string]interface{}{
"frame_size": 1024,
"quality": "high",
}
wrapped = WrapWithMetadata(originalErr, "audio", "decode", metadata)
require.NotNil(t, wrapped)
assert.Contains(t, wrapped.Error(), "audio.decode")
assert.Contains(t, wrapped.Error(), "assert.AnError")
assert.Contains(t, wrapped.Error(), "metadata")
assert.Contains(t, wrapped.Error(), "frame_size")
assert.Contains(t, wrapped.Error(), "quality")
}
// TestValidationIntegration tests validation functions working together
func TestValidationIntegration(t *testing.T) {
// Test that validation functions work correctly with actual audio configurations
presets := GetAudioQualityPresets()
require.NotEmpty(t, presets)
for quality, config := range presets {
t.Run(fmt.Sprintf("Quality_%d", quality), func(t *testing.T) {
// Validate the preset configuration
err := ValidateAudioConfigComplete(config)
assert.NoError(t, err, "Preset configuration for quality %d should be valid", quality)
// Validate individual components
err = ValidateAudioQuality(config.Quality)
assert.NoError(t, err, "Quality should be valid")
err = ValidateBitrate(config.Bitrate)
assert.NoError(t, err, "Bitrate should be valid")
err = ValidateSampleRate(config.SampleRate)
assert.NoError(t, err, "Sample rate should be valid")
err = ValidateChannelCount(config.Channels)
assert.NoError(t, err, "Channel count should be valid")
err = ValidateFrameDuration(config.FrameSize)
assert.NoError(t, err, "Frame duration should be valid")
})
}
}
// TestValidationPerformance ensures validation functions are efficient
func TestValidationPerformance(t *testing.T) {
if testing.Short() {
t.Skip("Skipping performance test in short mode")
}
// Initialize validation cache for performance testing
InitValidationCache()
// Test that validation functions complete quickly
start := time.Now()
iterations := 10000
for i := 0; i < iterations; i++ {
_ = ValidateAudioQuality(AudioQualityMedium)
_ = ValidateBufferSize(1024)
_ = ValidateChannelCount(2)
_ = ValidateSampleRate(48000)
_ = ValidateBitrate(96) // 96 kbps
}
elapsed := time.Since(start)
perIteration := elapsed / time.Duration(iterations)
// Performance expectations for JetKVM (ARM Cortex-A7 @ 1GHz, 256MB RAM)
// Audio processing must not interfere with primary KVM functionality
assert.Less(t, perIteration, 200*time.Microsecond, "Validation should not impact KVM performance")
t.Logf("Validation performance: %v per iteration", perIteration)
}

View File

@ -192,7 +192,6 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame {
frame.data = frame.data[:0]
frame.mutex.Unlock()
wasHit = true // Pool hit
atomic.AddInt64(&p.hitCount, 1)
return frame
}

View File

@ -32,9 +32,6 @@ func runAudioServer() {
}
func startAudioSubprocess() error {
// Initialize validation cache for optimal performance
audio.InitValidationCache()
// Start adaptive buffer management for optimal performance
audio.StartAdaptiveBuffering()

View File

@ -1,38 +0,0 @@
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 (
<div className={cx(
"rounded-md p-2 text-xs",
variantStyles[variant],
className
)}>
<div className="grid grid-cols-2 gap-1">
<span>Sample Rate: {config.SampleRate}Hz</span>
<span>Channels: {config.Channels}</span>
<span>Bitrate: {config.Bitrate}kbps</span>
<span>Frame: {config.FrameSize}</span>
</div>
</div>
);
}

View File

@ -470,7 +470,7 @@ export default function AudioMetricsDashboard() {
<div className="mb-2 flex items-center gap-2">
<MdMic className="h-4 w-4 text-green-600 dark:text-green-400" />
<span className="font-medium text-slate-900 dark:text-slate-100">
Audio Input Config
Microphone Input Config
</span>
</div>
<div className="space-y-2 text-sm">
@ -503,8 +503,6 @@ export default function AudioMetricsDashboard() {
)}
</div>
{/* Subprocess Resource Usage - Histogram View */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Audio Output Subprocess */}

View File

@ -1,33 +0,0 @@
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 (
<div className={cx(
"text-center p-2 bg-slate-50 dark:bg-slate-800 rounded",
className
)}>
<div className={cx(
"font-medium",
hasIssues
? "text-red-600 dark:text-red-400"
: "text-green-600 dark:text-green-400"
)}>
{hasIssues ? "Issues" : "Good"}
</div>
<div className="text-slate-500 dark:text-slate-400">{label}</div>
</div>
);
}

View File

@ -1,11 +1,9 @@
import { useEffect, useState } from "react";
import { MdVolumeOff, MdVolumeUp, MdGraphicEq, MdMic, MdMicOff, MdRefresh } from "react-icons/md";
import { LuActivity, LuSignal } from "react-icons/lu";
import { LuActivity, LuSettings, 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";
@ -13,6 +11,7 @@ import { useAudioLevel } from "@/hooks/useAudioLevel";
import { useAudioEvents } from "@/hooks/useAudioEvents";
import api from "@/api";
import notifications from "@/notifications";
import { AUDIO_CONFIG } from "@/config/constants";
import audioQualityService from "@/services/audioQualityService";
// Type for microphone error
@ -55,7 +54,7 @@ interface AudioControlPopoverProps {
export default function AudioControlPopover({ microphone, open }: AudioControlPopoverProps) {
const [currentConfig, setCurrentConfig] = useState<AudioConfig | null>(null);
const [currentMicrophoneConfig, setCurrentMicrophoneConfig] = useState<AudioConfig | null>(null);
const [showAdvanced, setShowAdvanced] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// Add cache flags to prevent unnecessary API calls
@ -275,7 +274,17 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo
}
};
const formatBytes = (bytes: number) => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
const formatNumber = (num: number) => {
return new Intl.NumberFormat().format(num);
};
return (
<div className="w-full max-w-md rounded-lg border border-slate-200 bg-white p-4 shadow-lg dark:border-slate-700 dark:bg-slate-800">
@ -514,10 +523,14 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo
</div>
{currentMicrophoneConfig && (
<AudioConfigDisplay
config={currentMicrophoneConfig}
variant="success"
/>
<div className="rounded-md bg-green-50 p-2 text-xs text-green-600 dark:bg-green-900/20 dark:text-green-400">
<div className="grid grid-cols-2 gap-1">
<span>Sample Rate: {currentMicrophoneConfig.SampleRate}Hz</span>
<span>Channels: {currentMicrophoneConfig.Channels}</span>
<span>Bitrate: {currentMicrophoneConfig.Bitrate}kbps</span>
<span>Frame: {currentMicrophoneConfig.FrameSize}</span>
</div>
</div>
)}
</div>
)}
@ -551,45 +564,164 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo
</div>
{currentConfig && (
<AudioConfigDisplay
config={currentConfig}
variant="default"
/>
)}
</div>
{/* Quick Status Summary */}
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-600">
<div className="flex items-center gap-2 mb-2">
<LuActivity className="h-4 w-4 text-slate-600 dark:text-slate-400" />
<span className="font-medium text-slate-900 dark:text-slate-100">
Quick Status
</span>
</div>
{metrics ? (
<div className="grid grid-cols-2 gap-3 text-xs">
<AudioStatusIndicator
metrics={metrics}
label="Audio Output"
/>
{micMetrics && (
<AudioStatusIndicator
metrics={micMetrics}
label="Microphone"
/>
)}
</div>
) : (
<div className="text-center py-2">
<div className="text-sm text-slate-500 dark:text-slate-400">
No data available
<div className="rounded-md bg-slate-50 p-2 text-xs text-slate-600 dark:bg-slate-700 dark:text-slate-400">
<div className="grid grid-cols-2 gap-1">
<span>Sample Rate: {currentConfig.SampleRate}Hz</span>
<span>Channels: {currentConfig.Channels}</span>
<span>Bitrate: {currentConfig.Bitrate}kbps</span>
<span>Frame: {currentConfig.FrameSize}</span>
</div>
</div>
)}
</div>
{/* Advanced Controls Toggle */}
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="flex w-full items-center justify-between rounded-md border border-slate-200 p-2 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-700"
>
<div className="flex items-center gap-2">
<LuSettings className="h-4 w-4" />
<span>Advanced Metrics</span>
</div>
<span className={cx(
"transition-transform",
showAdvanced ? "rotate-180" : "rotate-0"
)}>
</span>
</button>
{/* Advanced Metrics */}
{showAdvanced && (
<div className="space-y-3 rounded-lg border border-slate-200 p-3 dark:border-slate-600">
<div className="flex items-center gap-2">
<LuActivity className="h-4 w-4 text-slate-600 dark:text-slate-400" />
<span className="font-medium text-slate-900 dark:text-slate-100">
Performance Metrics
</span>
</div>
{metrics ? (
<>
<div className="mb-4">
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Audio Output</h4>
<div className="grid grid-cols-2 gap-3 text-xs">
<div className="space-y-1">
<div className="text-slate-500 dark:text-slate-400">Frames Received</div>
<div className="font-mono text-green-600 dark:text-green-400">
{formatNumber(metrics.frames_received)}
</div>
</div>
<div className="space-y-1">
<div className="text-slate-500 dark:text-slate-400">Frames Dropped</div>
<div className={cx(
"font-mono",
metrics.frames_dropped > 0
? "text-red-600 dark:text-red-400"
: "text-green-600 dark:text-green-400"
)}>
{formatNumber(metrics.frames_dropped)}
</div>
</div>
<div className="space-y-1">
<div className="text-slate-500 dark:text-slate-400">Data Processed</div>
<div className="font-mono text-blue-600 dark:text-blue-400">
{formatBytes(metrics.bytes_processed)}
</div>
</div>
<div className="space-y-1">
<div className="text-slate-500 dark:text-slate-400">Connection Drops</div>
<div className={cx(
"font-mono",
metrics.connection_drops > 0
? "text-red-600 dark:text-red-400"
: "text-green-600 dark:text-green-400"
)}>
{formatNumber(metrics.connection_drops)}
</div>
</div>
</div>
</div>
{micMetrics && (
<div className="mb-4">
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Microphone Input</h4>
<div className="grid grid-cols-2 gap-3 text-xs">
<div className="space-y-1">
<div className="text-slate-500 dark:text-slate-400">Frames Sent</div>
<div className="font-mono text-green-600 dark:text-green-400">
{formatNumber(micMetrics.frames_sent)}
</div>
</div>
<div className="space-y-1">
<div className="text-slate-500 dark:text-slate-400">Frames Dropped</div>
<div className={cx(
"font-mono",
micMetrics.frames_dropped > 0
? "text-red-600 dark:text-red-400"
: "text-green-600 dark:text-green-400"
)}>
{formatNumber(micMetrics.frames_dropped)}
</div>
</div>
<div className="space-y-1">
<div className="text-slate-500 dark:text-slate-400">Data Processed</div>
<div className="font-mono text-blue-600 dark:text-blue-400">
{formatBytes(micMetrics.bytes_processed)}
</div>
</div>
<div className="space-y-1">
<div className="text-slate-500 dark:text-slate-400">Connection Drops</div>
<div className={cx(
"font-mono",
micMetrics.connection_drops > 0
? "text-red-600 dark:text-red-400"
: "text-green-600 dark:text-green-400"
)}>
{formatNumber(micMetrics.connection_drops)}
</div>
</div>
</div>
</div>
)}
{metrics.frames_received > 0 && (
<div className="mt-3 rounded-md bg-slate-50 p-2 dark:bg-slate-700">
<div className="text-xs text-slate-500 dark:text-slate-400">Drop Rate</div>
<div className={cx(
"font-mono text-sm",
((metrics.frames_dropped / metrics.frames_received) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER) > AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD
? "text-red-600 dark:text-red-400"
: ((metrics.frames_dropped / metrics.frames_received) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER) > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD
? "text-yellow-600 dark:text-yellow-400"
: "text-green-600 dark:text-green-400"
)}>
{((metrics.frames_dropped / metrics.frames_received) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER).toFixed(AUDIO_CONFIG.PERCENTAGE_DECIMAL_PLACES)}%
</div>
</div>
)}
<div className="text-xs text-slate-500 dark:text-slate-400">
Last updated: {new Date().toLocaleTimeString()}
</div>
</>
) : (
<div className="text-center py-4">
<div className="text-sm text-slate-500 dark:text-slate-400">
Loading metrics...
</div>
</div>
)}
</div>
)}
{/* Audio Metrics Dashboard Button */}
<div className="pt-2 border-t border-slate-200 dark:border-slate-600">
<div className="flex justify-center">

View File

@ -89,17 +89,62 @@ export const AUDIO_CONFIG = {
SYNC_DEBOUNCE_MS: 1000, // debounce state synchronization
AUDIO_TEST_TIMEOUT: 100, // ms - timeout for audio testing
// 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",
// Audio Output Quality Bitrates (matching backend config_constants.go)
OUTPUT_QUALITY_BITRATES: {
LOW: 32, // AudioQualityLowOutputBitrate
MEDIUM: 64, // AudioQualityMediumOutputBitrate
HIGH: 128, // AudioQualityHighOutputBitrate
ULTRA: 192, // AudioQualityUltraOutputBitrate
} 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

18
web.go
View File

@ -212,8 +212,7 @@ func setupRouter() *gin.Engine {
})
protected.GET("/audio/metrics", func(c *gin.Context) {
registry := audio.GetMetricsRegistry()
metrics := registry.GetAudioMetrics()
metrics := audio.GetAudioMetrics()
c.JSON(200, gin.H{
"frames_received": metrics.FramesReceived,
"frames_dropped": metrics.FramesDropped,
@ -400,8 +399,19 @@ func setupRouter() *gin.Engine {
})
protected.GET("/microphone/metrics", func(c *gin.Context) {
registry := audio.GetMetricsRegistry()
metrics := registry.GetAudioInputMetrics()
if currentSession == nil || currentSession.AudioInputManager == nil {
c.JSON(200, gin.H{
"frames_sent": 0,
"frames_dropped": 0,
"bytes_processed": 0,
"last_frame_time": "",
"connection_drops": 0,
"average_latency": "0.0ms",
})
return
}
metrics := currentSession.AudioInputManager.GetMetrics()
c.JSON(200, gin.H{
"frames_sent": metrics.FramesSent,
"frames_dropped": metrics.FramesDropped,