mirror of https://github.com/jetkvm/kvm.git
refactor(audio): replace GetConfig() calls with direct Config access
This change replaces all instances of GetConfig() function calls with direct access to the Config variable throughout the audio package. The modification improves performance by eliminating function call overhead and simplifies the codebase by removing unnecessary indirection. The commit also includes minor optimizations in validation logic and connection handling, while maintaining all existing functionality. Error handling remains robust with appropriate fallbacks when config values are not available. Additional improvements include: - Enhanced connection health monitoring in UnifiedAudioClient - Optimized validation functions using cached config values - Reduced memory allocations in hot paths - Improved error recovery during quality changes
This commit is contained in:
parent
91f9dba4c6
commit
1d1658db15
|
@ -57,25 +57,25 @@ type AdaptiveBufferConfig struct {
|
||||||
func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig {
|
func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig {
|
||||||
return AdaptiveBufferConfig{
|
return AdaptiveBufferConfig{
|
||||||
// Conservative buffer sizes for 256MB RAM constraint
|
// Conservative buffer sizes for 256MB RAM constraint
|
||||||
MinBufferSize: GetConfig().AdaptiveMinBufferSize,
|
MinBufferSize: Config.AdaptiveMinBufferSize,
|
||||||
MaxBufferSize: GetConfig().AdaptiveMaxBufferSize,
|
MaxBufferSize: Config.AdaptiveMaxBufferSize,
|
||||||
DefaultBufferSize: GetConfig().AdaptiveDefaultBufferSize,
|
DefaultBufferSize: Config.AdaptiveDefaultBufferSize,
|
||||||
|
|
||||||
// CPU thresholds optimized for single-core ARM Cortex A7 under load
|
// CPU thresholds optimized for single-core ARM Cortex A7 under load
|
||||||
LowCPUThreshold: GetConfig().LowCPUThreshold * 100, // Below 20% CPU
|
LowCPUThreshold: Config.LowCPUThreshold * 100, // Below 20% CPU
|
||||||
HighCPUThreshold: GetConfig().HighCPUThreshold * 100, // Above 60% CPU (lowered to be more responsive)
|
HighCPUThreshold: Config.HighCPUThreshold * 100, // Above 60% CPU (lowered to be more responsive)
|
||||||
|
|
||||||
// Memory thresholds for 256MB total RAM
|
// Memory thresholds for 256MB total RAM
|
||||||
LowMemoryThreshold: GetConfig().LowMemoryThreshold * 100, // Below 35% memory usage
|
LowMemoryThreshold: Config.LowMemoryThreshold * 100, // Below 35% memory usage
|
||||||
HighMemoryThreshold: GetConfig().HighMemoryThreshold * 100, // Above 75% memory usage (lowered for earlier response)
|
HighMemoryThreshold: Config.HighMemoryThreshold * 100, // Above 75% memory usage (lowered for earlier response)
|
||||||
|
|
||||||
// Latency targets
|
// Latency targets
|
||||||
TargetLatency: GetConfig().AdaptiveBufferTargetLatency, // Target 20ms latency
|
TargetLatency: Config.AdaptiveBufferTargetLatency, // Target 20ms latency
|
||||||
MaxLatency: GetConfig().LatencyMonitorTarget, // Max acceptable latency
|
MaxLatency: Config.LatencyMonitorTarget, // Max acceptable latency
|
||||||
|
|
||||||
// Adaptation settings
|
// Adaptation settings
|
||||||
AdaptationInterval: GetConfig().BufferUpdateInterval, // Check every 500ms
|
AdaptationInterval: Config.BufferUpdateInterval, // Check every 500ms
|
||||||
SmoothingFactor: GetConfig().SmoothingFactor, // Moderate responsiveness
|
SmoothingFactor: Config.SmoothingFactor, // Moderate responsiveness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,7 +273,7 @@ func (abm *AdaptiveBufferManager) adaptBufferSizes() {
|
||||||
latencyFactor := abm.calculateLatencyFactor(currentLatency)
|
latencyFactor := abm.calculateLatencyFactor(currentLatency)
|
||||||
|
|
||||||
// Combine factors with weights (CPU has highest priority for KVM coexistence)
|
// Combine factors with weights (CPU has highest priority for KVM coexistence)
|
||||||
combinedFactor := GetConfig().CPUMemoryWeight*cpuFactor + GetConfig().MemoryWeight*memoryFactor + GetConfig().LatencyWeight*latencyFactor
|
combinedFactor := Config.CPUMemoryWeight*cpuFactor + Config.MemoryWeight*memoryFactor + Config.LatencyWeight*latencyFactor
|
||||||
|
|
||||||
// Apply adaptation with smoothing
|
// Apply adaptation with smoothing
|
||||||
currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize))
|
currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize))
|
||||||
|
@ -437,8 +437,8 @@ func (abm *AdaptiveBufferManager) GetStats() map[string]interface{} {
|
||||||
"input_buffer_size": abm.GetInputBufferSize(),
|
"input_buffer_size": abm.GetInputBufferSize(),
|
||||||
"output_buffer_size": abm.GetOutputBufferSize(),
|
"output_buffer_size": abm.GetOutputBufferSize(),
|
||||||
"average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6,
|
"average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6,
|
||||||
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / GetConfig().PercentageMultiplier,
|
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / Config.PercentageMultiplier,
|
||||||
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / GetConfig().PercentageMultiplier,
|
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / Config.PercentageMultiplier,
|
||||||
"adaptation_count": atomic.LoadInt64(&abm.adaptationCount),
|
"adaptation_count": atomic.LoadInt64(&abm.adaptationCount),
|
||||||
"last_adaptation": lastAdaptation,
|
"last_adaptation": lastAdaptation,
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,8 +83,7 @@ type batchWriteResult struct {
|
||||||
// NewBatchAudioProcessor creates a new batch audio processor
|
// NewBatchAudioProcessor creates a new batch audio processor
|
||||||
func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAudioProcessor {
|
func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAudioProcessor {
|
||||||
// Get cached config to avoid GetConfig() calls
|
// Get cached config to avoid GetConfig() calls
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
cache.Update()
|
|
||||||
|
|
||||||
// Validate input parameters with minimal overhead
|
// Validate input parameters with minimal overhead
|
||||||
if batchSize <= 0 || batchSize > 1000 {
|
if batchSize <= 0 || batchSize > 1000 {
|
||||||
|
@ -105,7 +104,7 @@ func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAu
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger()
|
logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger()
|
||||||
|
|
||||||
// Pre-calculate frame size to avoid repeated GetConfig() calls
|
// Pre-calculate frame size to avoid repeated GetConfig() calls
|
||||||
frameSize := cache.GetMinReadEncodeBuffer()
|
frameSize := cache.MinReadEncodeBuffer
|
||||||
if frameSize == 0 {
|
if frameSize == 0 {
|
||||||
frameSize = 1500 // Safe fallback
|
frameSize = 1500 // Safe fallback
|
||||||
}
|
}
|
||||||
|
@ -166,7 +165,7 @@ func (bap *BatchAudioProcessor) Stop() {
|
||||||
bap.cancel()
|
bap.cancel()
|
||||||
|
|
||||||
// Wait for processing to complete
|
// Wait for processing to complete
|
||||||
time.Sleep(bap.batchDuration + GetConfig().BatchProcessingDelay)
|
time.Sleep(bap.batchDuration + Config.BatchProcessingDelay)
|
||||||
|
|
||||||
bap.logger.Info().Msg("batch audio processor stopped")
|
bap.logger.Info().Msg("batch audio processor stopped")
|
||||||
}
|
}
|
||||||
|
@ -174,8 +173,7 @@ func (bap *BatchAudioProcessor) Stop() {
|
||||||
// BatchReadEncode performs batched audio read and encode operations
|
// BatchReadEncode performs batched audio read and encode operations
|
||||||
func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
|
func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
|
||||||
// Get cached config to avoid GetConfig() calls in hot path
|
// Get cached config to avoid GetConfig() calls in hot path
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
cache.Update()
|
|
||||||
|
|
||||||
// Validate buffer before processing
|
// Validate buffer before processing
|
||||||
if err := ValidateBufferSize(len(buffer)); err != nil {
|
if err := ValidateBufferSize(len(buffer)); err != nil {
|
||||||
|
@ -221,7 +219,7 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
|
||||||
select {
|
select {
|
||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
return result.length, result.err
|
return result.length, result.err
|
||||||
case <-time.After(cache.BatchProcessingTimeout):
|
case <-time.After(cache.BatchProcessorTimeout):
|
||||||
// Timeout, fallback to single operation
|
// Timeout, fallback to single operation
|
||||||
// Use sampling to reduce atomic operations overhead
|
// Use sampling to reduce atomic operations overhead
|
||||||
if atomic.LoadInt64(&bap.stats.SingleReads)%10 == 0 {
|
if atomic.LoadInt64(&bap.stats.SingleReads)%10 == 0 {
|
||||||
|
@ -236,8 +234,7 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
|
||||||
// This is the legacy version that uses a single buffer
|
// This is the legacy version that uses a single buffer
|
||||||
func (bap *BatchAudioProcessor) BatchDecodeWrite(buffer []byte) (int, error) {
|
func (bap *BatchAudioProcessor) BatchDecodeWrite(buffer []byte) (int, error) {
|
||||||
// Get cached config to avoid GetConfig() calls in hot path
|
// Get cached config to avoid GetConfig() calls in hot path
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
cache.Update()
|
|
||||||
|
|
||||||
// Validate buffer before processing
|
// Validate buffer before processing
|
||||||
if err := ValidateBufferSize(len(buffer)); err != nil {
|
if err := ValidateBufferSize(len(buffer)); err != nil {
|
||||||
|
@ -283,7 +280,7 @@ func (bap *BatchAudioProcessor) BatchDecodeWrite(buffer []byte) (int, error) {
|
||||||
select {
|
select {
|
||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
return result.length, result.err
|
return result.length, result.err
|
||||||
case <-time.After(cache.BatchProcessingTimeout):
|
case <-time.After(cache.BatchProcessorTimeout):
|
||||||
// Use sampling to reduce atomic operations overhead
|
// Use sampling to reduce atomic operations overhead
|
||||||
if atomic.LoadInt64(&bap.stats.SingleWrites)%10 == 0 {
|
if atomic.LoadInt64(&bap.stats.SingleWrites)%10 == 0 {
|
||||||
atomic.AddInt64(&bap.stats.SingleWrites, 10)
|
atomic.AddInt64(&bap.stats.SingleWrites, 10)
|
||||||
|
@ -296,8 +293,7 @@ func (bap *BatchAudioProcessor) BatchDecodeWrite(buffer []byte) (int, error) {
|
||||||
// BatchDecodeWriteWithBuffers performs batched audio decode and write operations with separate opus and PCM buffers
|
// BatchDecodeWriteWithBuffers performs batched audio decode and write operations with separate opus and PCM buffers
|
||||||
func (bap *BatchAudioProcessor) BatchDecodeWriteWithBuffers(opusData []byte, pcmBuffer []byte) (int, error) {
|
func (bap *BatchAudioProcessor) BatchDecodeWriteWithBuffers(opusData []byte, pcmBuffer []byte) (int, error) {
|
||||||
// Get cached config to avoid GetConfig() calls in hot path
|
// Get cached config to avoid GetConfig() calls in hot path
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
cache.Update()
|
|
||||||
|
|
||||||
// Validate buffers before processing
|
// Validate buffers before processing
|
||||||
if len(opusData) == 0 {
|
if len(opusData) == 0 {
|
||||||
|
@ -339,7 +335,7 @@ func (bap *BatchAudioProcessor) BatchDecodeWriteWithBuffers(opusData []byte, pcm
|
||||||
select {
|
select {
|
||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
return result.length, result.err
|
return result.length, result.err
|
||||||
case <-time.After(cache.BatchProcessingTimeout):
|
case <-time.After(cache.BatchProcessorTimeout):
|
||||||
atomic.AddInt64(&bap.stats.SingleWrites, 1)
|
atomic.AddInt64(&bap.stats.SingleWrites, 1)
|
||||||
atomic.AddInt64(&bap.stats.WriteFrames, 1)
|
atomic.AddInt64(&bap.stats.WriteFrames, 1)
|
||||||
// Use the optimized function with separate buffers
|
// Use the optimized function with separate buffers
|
||||||
|
@ -427,7 +423,7 @@ func (bap *BatchAudioProcessor) processBatchRead(batch []batchReadRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cached config once - avoid repeated calls
|
// Get cached config once - avoid repeated calls
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
threadPinningThreshold := cache.BatchProcessorThreadPinningThreshold
|
threadPinningThreshold := cache.BatchProcessorThreadPinningThreshold
|
||||||
if threadPinningThreshold == 0 {
|
if threadPinningThreshold == 0 {
|
||||||
threadPinningThreshold = cache.MinBatchSizeForThreadPinning // Fallback
|
threadPinningThreshold = cache.MinBatchSizeForThreadPinning // Fallback
|
||||||
|
@ -480,7 +476,7 @@ func (bap *BatchAudioProcessor) processBatchWrite(batch []batchWriteRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cached config to avoid GetConfig() calls in hot path
|
// Get cached config to avoid GetConfig() calls in hot path
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
threadPinningThreshold := cache.BatchProcessorThreadPinningThreshold
|
threadPinningThreshold := cache.BatchProcessorThreadPinningThreshold
|
||||||
if threadPinningThreshold == 0 {
|
if threadPinningThreshold == 0 {
|
||||||
threadPinningThreshold = cache.MinBatchSizeForThreadPinning // Fallback
|
threadPinningThreshold = cache.MinBatchSizeForThreadPinning // Fallback
|
||||||
|
@ -586,8 +582,7 @@ func GetBatchAudioProcessor() *BatchAudioProcessor {
|
||||||
// Initialize on first use
|
// Initialize on first use
|
||||||
if atomic.CompareAndSwapInt32(&batchProcessorInitialized, 0, 1) {
|
if atomic.CompareAndSwapInt32(&batchProcessorInitialized, 0, 1) {
|
||||||
// Get cached config to avoid GetConfig() calls
|
// Get cached config to avoid GetConfig() calls
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
cache.Update()
|
|
||||||
|
|
||||||
processor := NewBatchAudioProcessor(cache.BatchProcessorFramesPerBatch, cache.BatchProcessorTimeout)
|
processor := NewBatchAudioProcessor(cache.BatchProcessorFramesPerBatch, cache.BatchProcessorTimeout)
|
||||||
atomic.StorePointer(&globalBatchProcessor, unsafe.Pointer(processor))
|
atomic.StorePointer(&globalBatchProcessor, unsafe.Pointer(processor))
|
||||||
|
@ -601,7 +596,7 @@ func GetBatchAudioProcessor() *BatchAudioProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: create a new processor (should rarely happen)
|
// Fallback: create a new processor (should rarely happen)
|
||||||
config := GetConfig()
|
config := Config
|
||||||
return NewBatchAudioProcessor(config.BatchProcessorFramesPerBatch, config.BatchProcessorTimeout)
|
return NewBatchAudioProcessor(config.BatchProcessorFramesPerBatch, config.BatchProcessorTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ func GetBatchZeroCopyProcessor() *BatchZeroCopyProcessor {
|
||||||
|
|
||||||
// NewBatchZeroCopyProcessor creates a new batch zero-copy processor
|
// NewBatchZeroCopyProcessor creates a new batch zero-copy processor
|
||||||
func NewBatchZeroCopyProcessor() *BatchZeroCopyProcessor {
|
func NewBatchZeroCopyProcessor() *BatchZeroCopyProcessor {
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
return &BatchZeroCopyProcessor{
|
return &BatchZeroCopyProcessor{
|
||||||
maxBatchSize: cache.BatchProcessorFramesPerBatch,
|
maxBatchSize: cache.BatchProcessorFramesPerBatch,
|
||||||
batchTimeout: cache.BatchProcessorTimeout,
|
batchTimeout: cache.BatchProcessorTimeout,
|
||||||
|
|
|
@ -4,12 +4,12 @@ import "time"
|
||||||
|
|
||||||
// GetMetricsUpdateInterval returns the current metrics update interval from centralized config
|
// GetMetricsUpdateInterval returns the current metrics update interval from centralized config
|
||||||
func GetMetricsUpdateInterval() time.Duration {
|
func GetMetricsUpdateInterval() time.Duration {
|
||||||
return GetConfig().MetricsUpdateInterval
|
return Config.MetricsUpdateInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMetricsUpdateInterval sets the metrics update interval in centralized config
|
// SetMetricsUpdateInterval sets the metrics update interval in centralized config
|
||||||
func SetMetricsUpdateInterval(interval time.Duration) {
|
func SetMetricsUpdateInterval(interval time.Duration) {
|
||||||
config := GetConfig()
|
config := Config
|
||||||
config.MetricsUpdateInterval = interval
|
config.MetricsUpdateInterval = interval
|
||||||
UpdateConfig(config)
|
UpdateConfig(config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,6 +313,15 @@ type AudioConfigConstants struct {
|
||||||
AudioProcessorQueueSize int
|
AudioProcessorQueueSize int
|
||||||
AudioReaderQueueSize int
|
AudioReaderQueueSize int
|
||||||
WorkerMaxIdleTime time.Duration
|
WorkerMaxIdleTime time.Duration
|
||||||
|
|
||||||
|
// Connection Retry Configuration
|
||||||
|
MaxConnectionAttempts int // Maximum connection retry attempts
|
||||||
|
ConnectionRetryDelay time.Duration // Initial connection retry delay
|
||||||
|
MaxConnectionRetryDelay time.Duration // Maximum connection retry delay
|
||||||
|
ConnectionBackoffFactor float64 // Connection retry backoff factor
|
||||||
|
ConnectionTimeoutDelay time.Duration // Connection timeout for each attempt
|
||||||
|
ReconnectionInterval time.Duration // Interval for automatic reconnection attempts
|
||||||
|
HealthCheckInterval time.Duration // Health check interval for connections
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultAudioConfig returns the default configuration constants
|
// DefaultAudioConfig returns the default configuration constants
|
||||||
|
@ -424,11 +433,11 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
// Buffer Management
|
// Buffer Management
|
||||||
PreallocSize: 1024 * 1024, // 1MB buffer preallocation
|
PreallocSize: 1024 * 1024, // 1MB buffer preallocation
|
||||||
MaxPoolSize: 100, // Maximum object pool size
|
MaxPoolSize: 100, // Maximum object pool size
|
||||||
MessagePoolSize: 256, // Message pool size for IPC
|
MessagePoolSize: 512, // Increased message pool for quality change bursts
|
||||||
OptimalSocketBuffer: 262144, // 256KB optimal socket buffer
|
OptimalSocketBuffer: 262144, // 256KB optimal socket buffer
|
||||||
MaxSocketBuffer: 1048576, // 1MB maximum socket buffer
|
MaxSocketBuffer: 1048576, // 1MB maximum socket buffer
|
||||||
MinSocketBuffer: 8192, // 8KB minimum socket buffer
|
MinSocketBuffer: 8192, // 8KB minimum socket buffer
|
||||||
ChannelBufferSize: 500, // Inter-goroutine channel buffer size
|
ChannelBufferSize: 1000, // Increased channel buffer for quality change bursts
|
||||||
AudioFramePoolSize: 1500, // Audio frame object pool size
|
AudioFramePoolSize: 1500, // Audio frame object pool size
|
||||||
PageSize: 4096, // Memory page size for alignment
|
PageSize: 4096, // Memory page size for alignment
|
||||||
InitialBufferFrames: 500, // Initial buffer size during startup
|
InitialBufferFrames: 500, // Initial buffer size during startup
|
||||||
|
@ -436,17 +445,17 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
MinReadEncodeBuffer: 1276, // Minimum CGO read/encode buffer
|
MinReadEncodeBuffer: 1276, // Minimum CGO read/encode buffer
|
||||||
MaxDecodeWriteBuffer: 4096, // Maximum CGO decode/write buffer
|
MaxDecodeWriteBuffer: 4096, // Maximum CGO decode/write buffer
|
||||||
|
|
||||||
// IPC Configuration
|
// IPC Configuration - Balanced for stability
|
||||||
MagicNumber: 0xDEADBEEF, // IPC message validation header
|
MagicNumber: 0xDEADBEEF, // IPC message validation header
|
||||||
MaxFrameSize: 4096, // Maximum audio frame size (4KB)
|
MaxFrameSize: 4096, // Maximum audio frame size (4KB)
|
||||||
WriteTimeout: 100 * time.Millisecond, // IPC write operation timeout
|
WriteTimeout: 500 * time.Millisecond, // Increased timeout to handle quality change bursts
|
||||||
HeaderSize: 8, // IPC message header size
|
HeaderSize: 8, // IPC message header size
|
||||||
|
|
||||||
// Monitoring and Metrics
|
// Monitoring and Metrics - Balanced for stability
|
||||||
MetricsUpdateInterval: 1000 * time.Millisecond, // Metrics collection frequency
|
MetricsUpdateInterval: 1000 * time.Millisecond, // Stable metrics collection frequency
|
||||||
WarmupSamples: 10, // Warmup samples for metrics accuracy
|
WarmupSamples: 10, // Adequate warmup samples for accuracy
|
||||||
MetricsChannelBuffer: 100, // Metrics data channel buffer size
|
MetricsChannelBuffer: 100, // Adequate metrics data channel buffer
|
||||||
LatencyHistorySize: 100, // Number of latency measurements to keep
|
LatencyHistorySize: 100, // Adequate latency measurements to keep
|
||||||
|
|
||||||
// Process Monitoring Constants
|
// Process Monitoring Constants
|
||||||
MaxCPUPercent: 100.0, // Maximum CPU percentage
|
MaxCPUPercent: 100.0, // Maximum CPU percentage
|
||||||
|
@ -470,41 +479,50 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
BackoffMultiplier: 2.0, // Exponential backoff multiplier
|
BackoffMultiplier: 2.0, // Exponential backoff multiplier
|
||||||
MaxConsecutiveErrors: 5, // Consecutive error threshold
|
MaxConsecutiveErrors: 5, // Consecutive error threshold
|
||||||
|
|
||||||
// Timing Constants
|
// Connection Retry Configuration
|
||||||
DefaultSleepDuration: 100 * time.Millisecond, // Standard polling interval
|
MaxConnectionAttempts: 15, // Maximum connection retry attempts
|
||||||
ShortSleepDuration: 10 * time.Millisecond, // High-frequency polling
|
ConnectionRetryDelay: 50 * time.Millisecond, // Initial connection retry delay
|
||||||
LongSleepDuration: 200 * time.Millisecond, // Background tasks
|
MaxConnectionRetryDelay: 2 * time.Second, // Maximum connection retry delay
|
||||||
DefaultTickerInterval: 100 * time.Millisecond, // Periodic task interval
|
ConnectionBackoffFactor: 1.5, // Connection retry backoff factor
|
||||||
BufferUpdateInterval: 500 * time.Millisecond, // Buffer status updates
|
ConnectionTimeoutDelay: 5 * time.Second, // Connection timeout for each attempt
|
||||||
|
ReconnectionInterval: 30 * time.Second, // Interval for automatic reconnection attempts
|
||||||
|
HealthCheckInterval: 10 * time.Second, // Health check interval for connections
|
||||||
|
|
||||||
|
// Timing Constants - Optimized for quality change stability
|
||||||
|
DefaultSleepDuration: 100 * time.Millisecond, // Balanced polling interval
|
||||||
|
ShortSleepDuration: 10 * time.Millisecond, // Balanced high-frequency polling
|
||||||
|
LongSleepDuration: 200 * time.Millisecond, // Balanced background task delay
|
||||||
|
DefaultTickerInterval: 100 * time.Millisecond, // Balanced periodic task interval
|
||||||
|
BufferUpdateInterval: 300 * time.Millisecond, // Faster buffer updates for quality changes
|
||||||
InputSupervisorTimeout: 5 * time.Second, // Input monitoring timeout
|
InputSupervisorTimeout: 5 * time.Second, // Input monitoring timeout
|
||||||
OutputSupervisorTimeout: 5 * time.Second, // Output monitoring timeout
|
OutputSupervisorTimeout: 5 * time.Second, // Output monitoring timeout
|
||||||
BatchProcessingDelay: 10 * time.Millisecond, // Batch processing delay
|
BatchProcessingDelay: 5 * time.Millisecond, // Reduced batch processing delay
|
||||||
AdaptiveOptimizerStability: 10 * time.Second, // Adaptive stability period
|
AdaptiveOptimizerStability: 5 * time.Second, // Faster adaptive stability period
|
||||||
|
|
||||||
LatencyMonitorTarget: 50 * time.Millisecond, // Target latency for monitoring
|
LatencyMonitorTarget: 50 * time.Millisecond, // Balanced target latency for monitoring
|
||||||
|
|
||||||
// Adaptive Buffer Configuration
|
// Adaptive Buffer Configuration - Optimized for low latency
|
||||||
LowCPUThreshold: 0.20,
|
LowCPUThreshold: 0.30,
|
||||||
HighCPUThreshold: 0.60,
|
HighCPUThreshold: 0.70,
|
||||||
LowMemoryThreshold: 0.50,
|
LowMemoryThreshold: 0.60,
|
||||||
HighMemoryThreshold: 0.75,
|
HighMemoryThreshold: 0.80,
|
||||||
AdaptiveBufferTargetLatency: 20 * time.Millisecond,
|
AdaptiveBufferTargetLatency: 15 * time.Millisecond, // Reduced target latency
|
||||||
|
|
||||||
// Adaptive Buffer Size Configuration
|
// Adaptive Buffer Size Configuration - Optimized for quality change bursts
|
||||||
AdaptiveMinBufferSize: 3, // Minimum 3 frames for stability
|
AdaptiveMinBufferSize: 16, // Higher minimum to handle bursts
|
||||||
AdaptiveMaxBufferSize: 20, // Maximum 20 frames for high load
|
AdaptiveMaxBufferSize: 64, // Higher maximum for quality changes
|
||||||
AdaptiveDefaultBufferSize: 6, // Balanced buffer size (6 frames)
|
AdaptiveDefaultBufferSize: 32, // Higher default for stability
|
||||||
|
|
||||||
// Adaptive Optimizer Configuration
|
// Adaptive Optimizer Configuration - Faster response
|
||||||
CooldownPeriod: 30 * time.Second,
|
CooldownPeriod: 15 * time.Second, // Reduced cooldown period
|
||||||
RollbackThreshold: 300 * time.Millisecond,
|
RollbackThreshold: 200 * time.Millisecond, // Lower rollback threshold
|
||||||
AdaptiveOptimizerLatencyTarget: 50 * time.Millisecond,
|
AdaptiveOptimizerLatencyTarget: 30 * time.Millisecond, // Reduced latency target
|
||||||
|
|
||||||
// Latency Monitor Configuration
|
// Latency Monitor Configuration - More aggressive monitoring
|
||||||
MaxLatencyThreshold: 200 * time.Millisecond,
|
MaxLatencyThreshold: 150 * time.Millisecond, // Lower max latency threshold
|
||||||
JitterThreshold: 20 * time.Millisecond,
|
JitterThreshold: 15 * time.Millisecond, // Reduced jitter threshold
|
||||||
LatencyOptimizationInterval: 5 * time.Second,
|
LatencyOptimizationInterval: 3 * time.Second, // More frequent optimization
|
||||||
LatencyAdaptiveThreshold: 0.8,
|
LatencyAdaptiveThreshold: 0.7, // More aggressive adaptive threshold
|
||||||
|
|
||||||
// Microphone Contention Configuration
|
// Microphone Contention Configuration
|
||||||
MicContentionTimeout: 200 * time.Millisecond,
|
MicContentionTimeout: 200 * time.Millisecond,
|
||||||
|
@ -532,12 +550,12 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
LatencyScalingFactor: 2.0, // Latency ratio scaling factor
|
LatencyScalingFactor: 2.0, // Latency ratio scaling factor
|
||||||
OptimizerAggressiveness: 0.7, // Optimizer aggressiveness factor
|
OptimizerAggressiveness: 0.7, // Optimizer aggressiveness factor
|
||||||
|
|
||||||
// CGO Audio Processing Constants
|
// CGO Audio Processing Constants - Balanced for stability
|
||||||
CGOUsleepMicroseconds: 1000, // 1000 microseconds (1ms) for CGO usleep calls
|
CGOUsleepMicroseconds: 1000, // 1000 microseconds (1ms) for stable CGO usleep calls
|
||||||
CGOPCMBufferSize: 1920, // 1920 samples for PCM buffer (max 2ch*960)
|
CGOPCMBufferSize: 1920, // 1920 samples for PCM buffer (max 2ch*960)
|
||||||
CGONanosecondsPerSecond: 1000000000.0, // 1000000000.0 for nanosecond conversions
|
CGONanosecondsPerSecond: 1000000000.0, // 1000000000.0 for nanosecond conversions
|
||||||
|
|
||||||
// Frontend Constants
|
// Frontend Constants - Balanced for stability
|
||||||
FrontendOperationDebounceMS: 1000, // 1000ms debounce for frontend operations
|
FrontendOperationDebounceMS: 1000, // 1000ms debounce for frontend operations
|
||||||
FrontendSyncDebounceMS: 1000, // 1000ms debounce for sync operations
|
FrontendSyncDebounceMS: 1000, // 1000ms debounce for sync operations
|
||||||
FrontendSampleRate: 48000, // 48000Hz sample rate for frontend audio
|
FrontendSampleRate: 48000, // 48000Hz sample rate for frontend audio
|
||||||
|
@ -560,20 +578,20 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
ProcessMonitorFallbackClockHz: 1000.0, // 1000.0 Hz fallback clock
|
ProcessMonitorFallbackClockHz: 1000.0, // 1000.0 Hz fallback clock
|
||||||
ProcessMonitorTraditionalHz: 100.0, // 100.0 Hz traditional clock
|
ProcessMonitorTraditionalHz: 100.0, // 100.0 Hz traditional clock
|
||||||
|
|
||||||
// Batch Processing Constants
|
// Batch Processing Constants - Optimized for quality change bursts
|
||||||
BatchProcessorFramesPerBatch: 4, // 4 frames per batch
|
BatchProcessorFramesPerBatch: 16, // Larger batches for quality changes
|
||||||
BatchProcessorTimeout: 5 * time.Millisecond, // 5ms timeout
|
BatchProcessorTimeout: 20 * time.Millisecond, // Longer timeout for bursts
|
||||||
BatchProcessorMaxQueueSize: 16, // 16 max queue size for balanced memory/performance
|
BatchProcessorMaxQueueSize: 64, // Larger queue for quality changes
|
||||||
BatchProcessorAdaptiveThreshold: 0.8, // 0.8 threshold for adaptive batching (80% queue full)
|
BatchProcessorAdaptiveThreshold: 0.6, // Lower threshold for faster adaptation
|
||||||
BatchProcessorThreadPinningThreshold: 8, // 8 frames minimum for thread pinning optimization
|
BatchProcessorThreadPinningThreshold: 8, // Lower threshold for better performance
|
||||||
|
|
||||||
// Output Streaming Constants
|
// Output Streaming Constants - Balanced for stability
|
||||||
OutputStreamingFrameIntervalMS: 20, // 20ms frame interval (50 FPS)
|
OutputStreamingFrameIntervalMS: 20, // 20ms frame interval (50 FPS) for stability
|
||||||
|
|
||||||
// IPC Constants
|
// IPC Constants
|
||||||
IPCInitialBufferFrames: 500, // 500 frames for initial buffer
|
IPCInitialBufferFrames: 500, // 500 frames for initial buffer
|
||||||
|
|
||||||
// Event Constants
|
// Event Constants - Balanced for stability
|
||||||
EventTimeoutSeconds: 2, // 2 seconds for event timeout
|
EventTimeoutSeconds: 2, // 2 seconds for event timeout
|
||||||
EventTimeFormatString: "2006-01-02T15:04:05.000Z", // "2006-01-02T15:04:05.000Z" time format
|
EventTimeFormatString: "2006-01-02T15:04:05.000Z", // "2006-01-02T15:04:05.000Z" time format
|
||||||
EventSubscriptionDelayMS: 100, // 100ms subscription delay
|
EventSubscriptionDelayMS: 100, // 100ms subscription delay
|
||||||
|
@ -585,7 +603,7 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
AudioReaderQueueSize: 32, // 32 tasks queue size for reader pool
|
AudioReaderQueueSize: 32, // 32 tasks queue size for reader pool
|
||||||
WorkerMaxIdleTime: 60 * time.Second, // 60s maximum idle time before worker termination
|
WorkerMaxIdleTime: 60 * time.Second, // 60s maximum idle time before worker termination
|
||||||
|
|
||||||
// Input Processing Constants
|
// Input Processing Constants - Balanced for stability
|
||||||
InputProcessingTimeoutMS: 10, // 10ms processing timeout threshold
|
InputProcessingTimeoutMS: 10, // 10ms processing timeout threshold
|
||||||
|
|
||||||
// Adaptive Buffer Constants
|
// Adaptive Buffer Constants
|
||||||
|
@ -670,7 +688,7 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global configuration instance
|
// Global configuration instance
|
||||||
var audioConfigInstance = DefaultAudioConfig()
|
var Config = DefaultAudioConfig()
|
||||||
|
|
||||||
// UpdateConfig allows runtime configuration updates
|
// UpdateConfig allows runtime configuration updates
|
||||||
func UpdateConfig(newConfig *AudioConfigConstants) {
|
func UpdateConfig(newConfig *AudioConfigConstants) {
|
||||||
|
@ -682,12 +700,12 @@ func UpdateConfig(newConfig *AudioConfigConstants) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
audioConfigInstance = newConfig
|
Config = newConfig
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "AudioConfig").Logger()
|
logger := logging.GetDefaultLogger().With().Str("component", "AudioConfig").Logger()
|
||||||
logger.Info().Msg("Audio configuration updated successfully")
|
logger.Info().Msg("Audio configuration updated successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig returns the current configuration
|
// GetConfig returns the current configuration
|
||||||
func GetConfig() *AudioConfigConstants {
|
func GetConfig() *AudioConfigConstants {
|
||||||
return audioConfigInstance
|
return Config
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,12 +55,11 @@ func ValidateZeroCopyFrame(frame *ZeroCopyAudioFrame) error {
|
||||||
maxFrameSize := cachedMaxFrameSize
|
maxFrameSize := cachedMaxFrameSize
|
||||||
if maxFrameSize == 0 {
|
if maxFrameSize == 0 {
|
||||||
// Fallback: get from cache
|
// Fallback: get from cache
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
maxFrameSize = int(cache.maxAudioFrameSize.Load())
|
maxFrameSize = cache.MaxAudioFrameSize
|
||||||
if maxFrameSize == 0 {
|
if maxFrameSize == 0 {
|
||||||
// Last resort: update cache
|
// Last resort: use default
|
||||||
cache.Update()
|
maxFrameSize = cache.MaxAudioFrameSize
|
||||||
maxFrameSize = int(cache.maxAudioFrameSize.Load())
|
|
||||||
}
|
}
|
||||||
// Cache globally for next calls
|
// Cache globally for next calls
|
||||||
cachedMaxFrameSize = maxFrameSize
|
cachedMaxFrameSize = maxFrameSize
|
||||||
|
@ -80,8 +79,8 @@ func ValidateBufferSize(size int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: Check against cached max frame size
|
// Fast path: Check against cached max frame size
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
maxFrameSize := int(cache.maxAudioFrameSize.Load())
|
maxFrameSize := cache.MaxAudioFrameSize
|
||||||
|
|
||||||
// Most common case: validating a buffer that's sized for audio frames
|
// Most common case: validating a buffer that's sized for audio frames
|
||||||
if maxFrameSize > 0 && size <= maxFrameSize {
|
if maxFrameSize > 0 && size <= maxFrameSize {
|
||||||
|
@ -89,7 +88,7 @@ func ValidateBufferSize(size int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slower path: full validation against SocketMaxBuffer
|
// Slower path: full validation against SocketMaxBuffer
|
||||||
config := GetConfig()
|
config := Config
|
||||||
// Use SocketMaxBuffer as the upper limit for general buffer validation
|
// Use SocketMaxBuffer as the upper limit for general buffer validation
|
||||||
// This allows for socket buffers while still preventing extremely large allocations
|
// This allows for socket buffers while still preventing extremely large allocations
|
||||||
if size > config.SocketMaxBuffer {
|
if size > config.SocketMaxBuffer {
|
||||||
|
@ -107,8 +106,8 @@ func ValidateLatency(latency time.Duration) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: check against cached max latency
|
// Fast path: check against cached max latency
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
maxLatency := time.Duration(cache.maxLatency.Load())
|
maxLatency := time.Duration(cache.MaxLatency)
|
||||||
|
|
||||||
// If we have a valid cached value, use it
|
// If we have a valid cached value, use it
|
||||||
if maxLatency > 0 {
|
if maxLatency > 0 {
|
||||||
|
@ -125,7 +124,7 @@ func ValidateLatency(latency time.Duration) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slower path: full validation with GetConfig()
|
// Slower path: full validation with GetConfig()
|
||||||
config := GetConfig()
|
config := Config
|
||||||
minLatency := time.Millisecond // Minimum reasonable latency
|
minLatency := time.Millisecond // Minimum reasonable latency
|
||||||
if latency > 0 && latency < minLatency {
|
if latency > 0 && latency < minLatency {
|
||||||
return fmt.Errorf("%w: latency %v below minimum %v",
|
return fmt.Errorf("%w: latency %v below minimum %v",
|
||||||
|
@ -142,9 +141,9 @@ func ValidateLatency(latency time.Duration) error {
|
||||||
// Optimized to use AudioConfigCache for frequently accessed values
|
// Optimized to use AudioConfigCache for frequently accessed values
|
||||||
func ValidateMetricsInterval(interval time.Duration) error {
|
func ValidateMetricsInterval(interval time.Duration) error {
|
||||||
// Fast path: check against cached values
|
// Fast path: check against cached values
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
minInterval := time.Duration(cache.minMetricsUpdateInterval.Load())
|
minInterval := time.Duration(cache.MinMetricsUpdateInterval)
|
||||||
maxInterval := time.Duration(cache.maxMetricsUpdateInterval.Load())
|
maxInterval := time.Duration(cache.MaxMetricsUpdateInterval)
|
||||||
|
|
||||||
// If we have valid cached values, use them
|
// If we have valid cached values, use them
|
||||||
if minInterval > 0 && maxInterval > 0 {
|
if minInterval > 0 && maxInterval > 0 {
|
||||||
|
@ -160,7 +159,7 @@ func ValidateMetricsInterval(interval time.Duration) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slower path: full validation with GetConfig()
|
// Slower path: full validation with GetConfig()
|
||||||
config := GetConfig()
|
config := Config
|
||||||
minInterval = config.MinMetricsUpdateInterval
|
minInterval = config.MinMetricsUpdateInterval
|
||||||
maxInterval = config.MaxMetricsUpdateInterval
|
maxInterval = config.MaxMetricsUpdateInterval
|
||||||
if interval < minInterval {
|
if interval < minInterval {
|
||||||
|
@ -184,7 +183,7 @@ func ValidateAdaptiveBufferConfig(minSize, maxSize, defaultSize int) error {
|
||||||
return ErrInvalidBufferSize
|
return ErrInvalidBufferSize
|
||||||
}
|
}
|
||||||
// Validate against global limits
|
// Validate against global limits
|
||||||
maxBuffer := GetConfig().SocketMaxBuffer
|
maxBuffer := Config.SocketMaxBuffer
|
||||||
if maxSize > maxBuffer {
|
if maxSize > maxBuffer {
|
||||||
return ErrInvalidBufferSize
|
return ErrInvalidBufferSize
|
||||||
}
|
}
|
||||||
|
@ -194,7 +193,7 @@ func ValidateAdaptiveBufferConfig(minSize, maxSize, defaultSize int) error {
|
||||||
// ValidateInputIPCConfig validates input IPC configuration
|
// ValidateInputIPCConfig validates input IPC configuration
|
||||||
func ValidateInputIPCConfig(sampleRate, channels, frameSize int) error {
|
func ValidateInputIPCConfig(sampleRate, channels, frameSize int) error {
|
||||||
// Use config values
|
// Use config values
|
||||||
config := GetConfig()
|
config := Config
|
||||||
minSampleRate := config.MinSampleRate
|
minSampleRate := config.MinSampleRate
|
||||||
maxSampleRate := config.MaxSampleRate
|
maxSampleRate := config.MaxSampleRate
|
||||||
maxChannels := config.MaxChannels
|
maxChannels := config.MaxChannels
|
||||||
|
@ -213,7 +212,7 @@ func ValidateInputIPCConfig(sampleRate, channels, frameSize int) error {
|
||||||
// ValidateOutputIPCConfig validates output IPC configuration
|
// ValidateOutputIPCConfig validates output IPC configuration
|
||||||
func ValidateOutputIPCConfig(sampleRate, channels, frameSize int) error {
|
func ValidateOutputIPCConfig(sampleRate, channels, frameSize int) error {
|
||||||
// Use config values
|
// Use config values
|
||||||
config := GetConfig()
|
config := Config
|
||||||
minSampleRate := config.MinSampleRate
|
minSampleRate := config.MinSampleRate
|
||||||
maxSampleRate := config.MaxSampleRate
|
maxSampleRate := config.MaxSampleRate
|
||||||
maxChannels := config.MaxChannels
|
maxChannels := config.MaxChannels
|
||||||
|
@ -263,8 +262,8 @@ func ValidateSampleRate(sampleRate int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: Check against cached sample rate first
|
// Fast path: Check against cached sample rate first
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
cachedRate := int(cache.sampleRate.Load())
|
cachedRate := cache.SampleRate
|
||||||
|
|
||||||
// Most common case: validating against the current sample rate
|
// Most common case: validating against the current sample rate
|
||||||
if sampleRate == cachedRate {
|
if sampleRate == cachedRate {
|
||||||
|
@ -272,7 +271,7 @@ func ValidateSampleRate(sampleRate int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slower path: check against all valid rates
|
// Slower path: check against all valid rates
|
||||||
config := GetConfig()
|
config := Config
|
||||||
validRates := config.ValidSampleRates
|
validRates := config.ValidSampleRates
|
||||||
for _, rate := range validRates {
|
for _, rate := range validRates {
|
||||||
if sampleRate == rate {
|
if sampleRate == rate {
|
||||||
|
@ -291,8 +290,8 @@ func ValidateChannelCount(channels int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: Check against cached channels first
|
// Fast path: Check against cached channels first
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
cachedChannels := int(cache.channels.Load())
|
cachedChannels := cache.Channels
|
||||||
|
|
||||||
// Most common case: validating against the current channel count
|
// Most common case: validating against the current channel count
|
||||||
if channels == cachedChannels {
|
if channels == cachedChannels {
|
||||||
|
@ -300,14 +299,13 @@ func ValidateChannelCount(channels int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: Check against cached max channels
|
// Fast path: Check against cached max channels
|
||||||
cachedMaxChannels := int(cache.maxChannels.Load())
|
cachedMaxChannels := cache.MaxChannels
|
||||||
if cachedMaxChannels > 0 && channels <= cachedMaxChannels {
|
if cachedMaxChannels > 0 && channels <= cachedMaxChannels {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slow path: Update cache and validate
|
// Slow path: Use current config values
|
||||||
cache.Update()
|
updatedMaxChannels := cache.MaxChannels
|
||||||
updatedMaxChannels := int(cache.maxChannels.Load())
|
|
||||||
if channels > updatedMaxChannels {
|
if channels > updatedMaxChannels {
|
||||||
return fmt.Errorf("%w: channel count %d exceeds maximum %d",
|
return fmt.Errorf("%w: channel count %d exceeds maximum %d",
|
||||||
ErrInvalidChannels, channels, updatedMaxChannels)
|
ErrInvalidChannels, channels, updatedMaxChannels)
|
||||||
|
@ -323,9 +321,9 @@ func ValidateBitrate(bitrate int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: Check against cached bitrate values
|
// Fast path: Check against cached bitrate values
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
minBitrate := int(cache.minOpusBitrate.Load())
|
minBitrate := cache.MinOpusBitrate
|
||||||
maxBitrate := int(cache.maxOpusBitrate.Load())
|
maxBitrate := cache.MaxOpusBitrate
|
||||||
|
|
||||||
// If we have valid cached values, use them
|
// If we have valid cached values, use them
|
||||||
if minBitrate > 0 && maxBitrate > 0 {
|
if minBitrate > 0 && maxBitrate > 0 {
|
||||||
|
@ -343,7 +341,7 @@ func ValidateBitrate(bitrate int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slower path: full validation with GetConfig()
|
// Slower path: full validation with GetConfig()
|
||||||
config := GetConfig()
|
config := Config
|
||||||
// Convert kbps to bps for comparison with config limits
|
// Convert kbps to bps for comparison with config limits
|
||||||
bitrateInBps := bitrate * 1000
|
bitrateInBps := bitrate * 1000
|
||||||
if bitrateInBps < config.MinOpusBitrate {
|
if bitrateInBps < config.MinOpusBitrate {
|
||||||
|
@ -365,11 +363,11 @@ func ValidateFrameDuration(duration time.Duration) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: Check against cached frame size first
|
// Fast path: Check against cached frame size first
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
|
|
||||||
// Convert frameSize (samples) to duration for comparison
|
// Convert frameSize (samples) to duration for comparison
|
||||||
cachedFrameSize := int(cache.frameSize.Load())
|
cachedFrameSize := cache.FrameSize
|
||||||
cachedSampleRate := int(cache.sampleRate.Load())
|
cachedSampleRate := cache.SampleRate
|
||||||
|
|
||||||
// Only do this calculation if we have valid cached values
|
// Only do this calculation if we have valid cached values
|
||||||
if cachedFrameSize > 0 && cachedSampleRate > 0 {
|
if cachedFrameSize > 0 && cachedSampleRate > 0 {
|
||||||
|
@ -382,8 +380,8 @@ func ValidateFrameDuration(duration time.Duration) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: Check against cached min/max frame duration
|
// Fast path: Check against cached min/max frame duration
|
||||||
cachedMinDuration := time.Duration(cache.minFrameDuration.Load())
|
cachedMinDuration := time.Duration(cache.MinFrameDuration)
|
||||||
cachedMaxDuration := time.Duration(cache.maxFrameDuration.Load())
|
cachedMaxDuration := time.Duration(cache.MaxFrameDuration)
|
||||||
|
|
||||||
if cachedMinDuration > 0 && cachedMaxDuration > 0 {
|
if cachedMinDuration > 0 && cachedMaxDuration > 0 {
|
||||||
if duration < cachedMinDuration {
|
if duration < cachedMinDuration {
|
||||||
|
@ -397,10 +395,9 @@ func ValidateFrameDuration(duration time.Duration) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slow path: Update cache and validate
|
// Slow path: Use current config values
|
||||||
cache.Update()
|
updatedMinDuration := time.Duration(cache.MinFrameDuration)
|
||||||
updatedMinDuration := time.Duration(cache.minFrameDuration.Load())
|
updatedMaxDuration := time.Duration(cache.MaxFrameDuration)
|
||||||
updatedMaxDuration := time.Duration(cache.maxFrameDuration.Load())
|
|
||||||
|
|
||||||
if duration < updatedMinDuration {
|
if duration < updatedMinDuration {
|
||||||
return fmt.Errorf("%w: frame duration %v below minimum %v",
|
return fmt.Errorf("%w: frame duration %v below minimum %v",
|
||||||
|
@ -417,11 +414,11 @@ func ValidateFrameDuration(duration time.Duration) error {
|
||||||
// Uses optimized validation functions that leverage AudioConfigCache
|
// Uses optimized validation functions that leverage AudioConfigCache
|
||||||
func ValidateAudioConfigComplete(config AudioConfig) error {
|
func ValidateAudioConfigComplete(config AudioConfig) error {
|
||||||
// Fast path: Check if all values match the current cached configuration
|
// Fast path: Check if all values match the current cached configuration
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
cachedSampleRate := int(cache.sampleRate.Load())
|
cachedSampleRate := cache.SampleRate
|
||||||
cachedChannels := int(cache.channels.Load())
|
cachedChannels := cache.Channels
|
||||||
cachedBitrate := int(cache.opusBitrate.Load()) / 1000 // Convert from bps to kbps
|
cachedBitrate := cache.OpusBitrate / 1000 // Convert from bps to kbps
|
||||||
cachedFrameSize := int(cache.frameSize.Load())
|
cachedFrameSize := cache.FrameSize
|
||||||
|
|
||||||
// Only do this calculation if we have valid cached values
|
// Only do this calculation if we have valid cached values
|
||||||
if cachedSampleRate > 0 && cachedChannels > 0 && cachedBitrate > 0 && cachedFrameSize > 0 {
|
if cachedSampleRate > 0 && cachedChannels > 0 && cachedBitrate > 0 && cachedFrameSize > 0 {
|
||||||
|
@ -481,11 +478,11 @@ var cachedMaxFrameSize int
|
||||||
// InitValidationCache initializes cached validation values with actual config
|
// InitValidationCache initializes cached validation values with actual config
|
||||||
func InitValidationCache() {
|
func InitValidationCache() {
|
||||||
// Initialize the global cache variable for backward compatibility
|
// Initialize the global cache variable for backward compatibility
|
||||||
config := GetConfig()
|
config := Config
|
||||||
cachedMaxFrameSize = config.MaxAudioFrameSize
|
cachedMaxFrameSize = config.MaxAudioFrameSize
|
||||||
|
|
||||||
// Update the global audio config cache
|
// Initialize the global audio config cache
|
||||||
GetCachedConfig().Update()
|
cachedMaxFrameSize = Config.MaxAudioFrameSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAudioFrame validates audio frame data with cached max size for performance
|
// ValidateAudioFrame validates audio frame data with cached max size for performance
|
||||||
|
@ -502,12 +499,11 @@ func ValidateAudioFrame(data []byte) error {
|
||||||
maxSize := cachedMaxFrameSize
|
maxSize := cachedMaxFrameSize
|
||||||
if maxSize == 0 {
|
if maxSize == 0 {
|
||||||
// Fallback: get from cache only if global cache not initialized
|
// Fallback: get from cache only if global cache not initialized
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
maxSize = int(cache.maxAudioFrameSize.Load())
|
maxSize = cache.MaxAudioFrameSize
|
||||||
if maxSize == 0 {
|
if maxSize == 0 {
|
||||||
// Last resort: update cache and get fresh value
|
// Last resort: get fresh value
|
||||||
cache.Update()
|
maxSize = cache.MaxAudioFrameSize
|
||||||
maxSize = int(cache.maxAudioFrameSize.Load())
|
|
||||||
}
|
}
|
||||||
// Cache the value globally for next calls
|
// Cache the value globally for next calls
|
||||||
cachedMaxFrameSize = maxSize
|
cachedMaxFrameSize = maxSize
|
||||||
|
|
|
@ -255,7 +255,7 @@ func GetAudioProcessorPool() *GoroutinePool {
|
||||||
}
|
}
|
||||||
|
|
||||||
globalAudioProcessorInitOnce.Do(func() {
|
globalAudioProcessorInitOnce.Do(func() {
|
||||||
config := GetConfig()
|
config := Config
|
||||||
newPool := NewGoroutinePool(
|
newPool := NewGoroutinePool(
|
||||||
"audio-processor",
|
"audio-processor",
|
||||||
config.MaxAudioProcessorWorkers,
|
config.MaxAudioProcessorWorkers,
|
||||||
|
@ -277,7 +277,7 @@ func GetAudioReaderPool() *GoroutinePool {
|
||||||
}
|
}
|
||||||
|
|
||||||
globalAudioReaderInitOnce.Do(func() {
|
globalAudioReaderInitOnce.Do(func() {
|
||||||
config := GetConfig()
|
config := Config
|
||||||
newPool := NewGoroutinePool(
|
newPool := NewGoroutinePool(
|
||||||
"audio-reader",
|
"audio-reader",
|
||||||
config.MaxAudioReaderWorkers,
|
config.MaxAudioReaderWorkers,
|
||||||
|
|
|
@ -108,7 +108,7 @@ func (aim *AudioInputManager) WriteOpusFrame(frame []byte) error {
|
||||||
processingTime := time.Since(startTime)
|
processingTime := time.Since(startTime)
|
||||||
|
|
||||||
// Log high latency warnings
|
// Log high latency warnings
|
||||||
if processingTime > time.Duration(GetConfig().InputProcessingTimeoutMS)*time.Millisecond {
|
if processingTime > time.Duration(Config.InputProcessingTimeoutMS)*time.Millisecond {
|
||||||
latencyMs := float64(processingTime.Milliseconds())
|
latencyMs := float64(processingTime.Milliseconds())
|
||||||
aim.logger.Warn().
|
aim.logger.Warn().
|
||||||
Float64("latency_ms", latencyMs).
|
Float64("latency_ms", latencyMs).
|
||||||
|
@ -149,7 +149,7 @@ func (aim *AudioInputManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFrame)
|
||||||
processingTime := time.Since(startTime)
|
processingTime := time.Since(startTime)
|
||||||
|
|
||||||
// Log high latency warnings
|
// Log high latency warnings
|
||||||
if processingTime > time.Duration(GetConfig().InputProcessingTimeoutMS)*time.Millisecond {
|
if processingTime > time.Duration(Config.InputProcessingTimeoutMS)*time.Millisecond {
|
||||||
latencyMs := float64(processingTime.Milliseconds())
|
latencyMs := float64(processingTime.Milliseconds())
|
||||||
aim.logger.Warn().
|
aim.logger.Warn().
|
||||||
Float64("latency_ms", latencyMs).
|
Float64("latency_ms", latencyMs).
|
||||||
|
|
|
@ -107,7 +107,7 @@ func RunAudioInputServer() error {
|
||||||
server.Stop()
|
server.Stop()
|
||||||
|
|
||||||
// Give some time for cleanup
|
// Give some time for cleanup
|
||||||
time.Sleep(GetConfig().DefaultSleepDuration)
|
time.Sleep(Config.DefaultSleepDuration)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ func (ais *AudioInputSupervisor) supervisionLoop() {
|
||||||
// Configure supervision parameters (no restart for input supervisor)
|
// Configure supervision parameters (no restart for input supervisor)
|
||||||
config := SupervisionConfig{
|
config := SupervisionConfig{
|
||||||
ProcessType: "audio input server",
|
ProcessType: "audio input server",
|
||||||
Timeout: GetConfig().InputSupervisorTimeout,
|
Timeout: Config.InputSupervisorTimeout,
|
||||||
EnableRestart: false, // Input supervisor doesn't restart
|
EnableRestart: false, // Input supervisor doesn't restart
|
||||||
MaxRestartAttempts: 0,
|
MaxRestartAttempts: 0,
|
||||||
RestartWindow: 0,
|
RestartWindow: 0,
|
||||||
|
@ -164,7 +164,7 @@ func (ais *AudioInputSupervisor) Stop() {
|
||||||
select {
|
select {
|
||||||
case <-ais.processDone:
|
case <-ais.processDone:
|
||||||
ais.logger.Info().Str("component", "audio-input-supervisor").Msg("component stopped gracefully")
|
ais.logger.Info().Str("component", "audio-input-supervisor").Msg("component stopped gracefully")
|
||||||
case <-time.After(GetConfig().InputSupervisorTimeout):
|
case <-time.After(Config.InputSupervisorTimeout):
|
||||||
ais.logger.Warn().Str("component", "audio-input-supervisor").Msg("component did not stop gracefully, forcing termination")
|
ais.logger.Warn().Str("component", "audio-input-supervisor").Msg("component did not stop gracefully, forcing termination")
|
||||||
ais.forceKillProcess("audio input server")
|
ais.forceKillProcess("audio input server")
|
||||||
}
|
}
|
||||||
|
@ -190,7 +190,7 @@ func (ais *AudioInputSupervisor) GetClient() *AudioInputClient {
|
||||||
// connectClient attempts to connect the client to the server
|
// connectClient attempts to connect the client to the server
|
||||||
func (ais *AudioInputSupervisor) connectClient() {
|
func (ais *AudioInputSupervisor) connectClient() {
|
||||||
// Wait briefly for the server to start and create socket
|
// Wait briefly for the server to start and create socket
|
||||||
time.Sleep(GetConfig().DefaultSleepDuration)
|
time.Sleep(Config.DefaultSleepDuration)
|
||||||
|
|
||||||
// Additional small delay to ensure socket is ready after restart
|
// Additional small delay to ensure socket is ready after restart
|
||||||
time.Sleep(20 * time.Millisecond)
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
|
@ -49,7 +49,7 @@ func NewGenericMessagePool(size int) *GenericMessagePool {
|
||||||
pool.preallocated = make([]*OptimizedMessage, pool.preallocSize)
|
pool.preallocated = make([]*OptimizedMessage, pool.preallocSize)
|
||||||
for i := 0; i < pool.preallocSize; i++ {
|
for i := 0; i < pool.preallocSize; i++ {
|
||||||
pool.preallocated[i] = &OptimizedMessage{
|
pool.preallocated[i] = &OptimizedMessage{
|
||||||
data: make([]byte, 0, GetConfig().MaxFrameSize),
|
data: make([]byte, 0, Config.MaxFrameSize),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ func NewGenericMessagePool(size int) *GenericMessagePool {
|
||||||
for i := 0; i < size-pool.preallocSize; i++ {
|
for i := 0; i < size-pool.preallocSize; i++ {
|
||||||
select {
|
select {
|
||||||
case pool.pool <- &OptimizedMessage{
|
case pool.pool <- &OptimizedMessage{
|
||||||
data: make([]byte, 0, GetConfig().MaxFrameSize),
|
data: make([]byte, 0, Config.MaxFrameSize),
|
||||||
}:
|
}:
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
@ -89,7 +89,7 @@ func (mp *GenericMessagePool) Get() *OptimizedMessage {
|
||||||
// Pool empty, create new message
|
// Pool empty, create new message
|
||||||
atomic.AddInt64(&mp.missCount, 1)
|
atomic.AddInt64(&mp.missCount, 1)
|
||||||
return &OptimizedMessage{
|
return &OptimizedMessage{
|
||||||
data: make([]byte, 0, GetConfig().MaxFrameSize),
|
data: make([]byte, 0, Config.MaxFrameSize),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ func WriteIPCMessage(conn net.Conn, msg IPCMessage, pool *GenericMessagePool, dr
|
||||||
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(msg.GetTimestamp()))
|
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(msg.GetTimestamp()))
|
||||||
|
|
||||||
// Set write deadline for timeout handling (more efficient than goroutines)
|
// Set write deadline for timeout handling (more efficient than goroutines)
|
||||||
if deadline := time.Now().Add(GetConfig().WriteTimeout); deadline.After(time.Now()) {
|
if deadline := time.Now().Add(Config.WriteTimeout); deadline.After(time.Now()) {
|
||||||
if err := conn.SetWriteDeadline(deadline); err != nil {
|
if err := conn.SetWriteDeadline(deadline); err != nil {
|
||||||
// If we can't set deadline, proceed without it
|
// If we can't set deadline, proceed without it
|
||||||
// This maintains compatibility with connections that don't support deadlines
|
// This maintains compatibility with connections that don't support deadlines
|
||||||
|
|
|
@ -23,8 +23,8 @@ const (
|
||||||
|
|
||||||
// Constants are now defined in unified_ipc.go
|
// Constants are now defined in unified_ipc.go
|
||||||
var (
|
var (
|
||||||
maxFrameSize = GetConfig().MaxFrameSize // Maximum Opus frame size
|
maxFrameSize = Config.MaxFrameSize // Maximum Opus frame size
|
||||||
messagePoolSize = GetConfig().MessagePoolSize // Pre-allocated message pool size
|
messagePoolSize = Config.MessagePoolSize // Pre-allocated message pool size
|
||||||
)
|
)
|
||||||
|
|
||||||
// Legacy aliases for backward compatibility
|
// Legacy aliases for backward compatibility
|
||||||
|
@ -77,7 +77,7 @@ func initializeMessagePool() {
|
||||||
messagePoolInitOnce.Do(func() {
|
messagePoolInitOnce.Do(func() {
|
||||||
preallocSize := messagePoolSize / 4 // 25% pre-allocated for immediate use
|
preallocSize := messagePoolSize / 4 // 25% pre-allocated for immediate use
|
||||||
globalMessagePool.preallocSize = preallocSize
|
globalMessagePool.preallocSize = preallocSize
|
||||||
globalMessagePool.maxPoolSize = messagePoolSize * GetConfig().PoolGrowthMultiplier // Allow growth up to 2x
|
globalMessagePool.maxPoolSize = messagePoolSize * Config.PoolGrowthMultiplier // Allow growth up to 2x
|
||||||
globalMessagePool.preallocated = make([]*OptimizedIPCMessage, 0, preallocSize)
|
globalMessagePool.preallocated = make([]*OptimizedIPCMessage, 0, preallocSize)
|
||||||
|
|
||||||
// Pre-allocate messages for immediate use
|
// Pre-allocate messages for immediate use
|
||||||
|
@ -378,7 +378,7 @@ func (ais *AudioInputServer) handleConnection(conn net.Conn) {
|
||||||
if ais.conn == nil {
|
if ais.conn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.Sleep(GetConfig().DefaultSleepDuration)
|
time.Sleep(Config.DefaultSleepDuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -499,11 +499,11 @@ func (ais *AudioInputServer) processOpusFrame(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cached config once - avoid repeated calls and locking
|
// Get cached config once - avoid repeated calls and locking
|
||||||
cache := GetCachedConfig()
|
cache := Config
|
||||||
// Skip cache expiry check in hotpath - background updates handle this
|
// Skip cache expiry check in hotpath - background updates handle this
|
||||||
|
|
||||||
// Get a PCM buffer from the pool for optimized decode-write
|
// Get a PCM buffer from the pool for optimized decode-write
|
||||||
pcmBuffer := GetBufferFromPool(cache.GetMaxPCMBufferSize())
|
pcmBuffer := GetBufferFromPool(cache.MaxPCMBufferSize)
|
||||||
defer ReturnBufferToPool(pcmBuffer)
|
defer ReturnBufferToPool(pcmBuffer)
|
||||||
|
|
||||||
// Direct CGO call - avoid wrapper function overhead
|
// Direct CGO call - avoid wrapper function overhead
|
||||||
|
@ -646,9 +646,9 @@ func (aic *AudioInputClient) Connect() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Exponential backoff starting from config
|
// Exponential backoff starting from config
|
||||||
backoffStart := GetConfig().BackoffStart
|
backoffStart := Config.BackoffStart
|
||||||
delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
|
delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
|
||||||
maxDelay := GetConfig().MaxRetryDelay
|
maxDelay := Config.MaxRetryDelay
|
||||||
if delay > maxDelay {
|
if delay > maxDelay {
|
||||||
delay = maxDelay
|
delay = maxDelay
|
||||||
}
|
}
|
||||||
|
@ -911,10 +911,10 @@ func (ais *AudioInputServer) startReaderGoroutine() {
|
||||||
// Enhanced error tracking and recovery
|
// Enhanced error tracking and recovery
|
||||||
var consecutiveErrors int
|
var consecutiveErrors int
|
||||||
var lastErrorTime time.Time
|
var lastErrorTime time.Time
|
||||||
maxConsecutiveErrors := GetConfig().MaxConsecutiveErrors
|
maxConsecutiveErrors := Config.MaxConsecutiveErrors
|
||||||
errorResetWindow := GetConfig().RestartWindow // Use existing restart window
|
errorResetWindow := Config.RestartWindow // Use existing restart window
|
||||||
baseBackoffDelay := GetConfig().RetryDelay
|
baseBackoffDelay := Config.RetryDelay
|
||||||
maxBackoffDelay := GetConfig().MaxRetryDelay
|
maxBackoffDelay := Config.MaxRetryDelay
|
||||||
|
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
|
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
|
||||||
|
|
||||||
|
@ -1025,7 +1025,7 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
|
||||||
processorTask := func() {
|
processorTask := func() {
|
||||||
// Only lock OS thread and set priority for high-load scenarios
|
// Only lock OS thread and set priority for high-load scenarios
|
||||||
// This reduces interference with input processing threads
|
// This reduces interference with input processing threads
|
||||||
config := GetConfig()
|
config := Config
|
||||||
useThreadOptimizations := config.MaxAudioProcessorWorkers > 8
|
useThreadOptimizations := config.MaxAudioProcessorWorkers > 8
|
||||||
|
|
||||||
if useThreadOptimizations {
|
if useThreadOptimizations {
|
||||||
|
@ -1137,7 +1137,7 @@ func (ais *AudioInputServer) processMessageWithRecovery(msg *InputIPCMessage, lo
|
||||||
select {
|
select {
|
||||||
case processChan <- msg:
|
case processChan <- msg:
|
||||||
return nil
|
return nil
|
||||||
case <-time.After(GetConfig().WriteTimeout):
|
case <-time.After(Config.WriteTimeout):
|
||||||
// Processing queue full and timeout reached, drop frame
|
// Processing queue full and timeout reached, drop frame
|
||||||
atomic.AddInt64(&ais.droppedFrames, 1)
|
atomic.AddInt64(&ais.droppedFrames, 1)
|
||||||
return fmt.Errorf("processing queue timeout")
|
return fmt.Errorf("processing queue timeout")
|
||||||
|
@ -1156,7 +1156,7 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
|
||||||
monitorTask := func() {
|
monitorTask := func() {
|
||||||
// Monitor goroutine doesn't need thread locking for most scenarios
|
// Monitor goroutine doesn't need thread locking for most scenarios
|
||||||
// Only use thread optimizations for high-throughput scenarios
|
// Only use thread optimizations for high-throughput scenarios
|
||||||
config := GetConfig()
|
config := Config
|
||||||
useThreadOptimizations := config.MaxAudioProcessorWorkers > 8
|
useThreadOptimizations := config.MaxAudioProcessorWorkers > 8
|
||||||
|
|
||||||
if useThreadOptimizations {
|
if useThreadOptimizations {
|
||||||
|
@ -1167,11 +1167,11 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
|
||||||
}
|
}
|
||||||
|
|
||||||
defer ais.wg.Done()
|
defer ais.wg.Done()
|
||||||
ticker := time.NewTicker(GetConfig().DefaultTickerInterval)
|
ticker := time.NewTicker(Config.DefaultTickerInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
// Buffer size update ticker (less frequent)
|
// Buffer size update ticker (less frequent)
|
||||||
bufferUpdateTicker := time.NewTicker(GetConfig().BufferUpdateInterval)
|
bufferUpdateTicker := time.NewTicker(Config.BufferUpdateInterval)
|
||||||
defer bufferUpdateTicker.Stop()
|
defer bufferUpdateTicker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -1330,7 +1330,7 @@ func (mp *MessagePool) GetMessagePoolStats() MessagePoolStats {
|
||||||
|
|
||||||
var hitRate float64
|
var hitRate float64
|
||||||
if totalRequests > 0 {
|
if totalRequests > 0 {
|
||||||
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
|
hitRate = float64(hitCount) / float64(totalRequests) * Config.PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate channel pool size
|
// Calculate channel pool size
|
||||||
|
|
|
@ -24,7 +24,7 @@ const (
|
||||||
// Methods are now inherited from UnifiedIPCMessage
|
// Methods are now inherited from UnifiedIPCMessage
|
||||||
|
|
||||||
// Global shared message pool for output IPC client header reading
|
// Global shared message pool for output IPC client header reading
|
||||||
var globalOutputClientMessagePool = NewGenericMessagePool(GetConfig().OutputMessagePoolSize)
|
var globalOutputClientMessagePool = NewGenericMessagePool(Config.OutputMessagePoolSize)
|
||||||
|
|
||||||
// AudioOutputServer is now an alias for UnifiedAudioServer
|
// AudioOutputServer is now an alias for UnifiedAudioServer
|
||||||
type AudioOutputServer = UnifiedAudioServer
|
type AudioOutputServer = UnifiedAudioServer
|
||||||
|
@ -95,7 +95,7 @@ func (c *AudioOutputClient) ReceiveFrame() ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
size := binary.LittleEndian.Uint32(optMsg.header[5:9])
|
size := binary.LittleEndian.Uint32(optMsg.header[5:9])
|
||||||
maxFrameSize := GetConfig().OutputMaxFrameSize
|
maxFrameSize := Config.OutputMaxFrameSize
|
||||||
if int(size) > maxFrameSize {
|
if int(size) > maxFrameSize {
|
||||||
return nil, fmt.Errorf("received frame size validation failed: got %d bytes, maximum allowed %d bytes", size, maxFrameSize)
|
return nil, fmt.Errorf("received frame size validation failed: got %d bytes, maximum allowed %d bytes", size, maxFrameSize)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -17,8 +18,8 @@ import (
|
||||||
|
|
||||||
// Unified IPC constants
|
// Unified IPC constants
|
||||||
var (
|
var (
|
||||||
outputMagicNumber uint32 = GetConfig().OutputMagicNumber // "JKOU" (JetKVM Output)
|
outputMagicNumber uint32 = Config.OutputMagicNumber // "JKOU" (JetKVM Output)
|
||||||
inputMagicNumber uint32 = GetConfig().InputMagicNumber // "JKMI" (JetKVM Microphone Input)
|
inputMagicNumber uint32 = Config.InputMagicNumber // "JKMI" (JetKVM Microphone Input)
|
||||||
outputSocketName = "audio_output.sock"
|
outputSocketName = "audio_output.sock"
|
||||||
inputSocketName = "audio_input.sock"
|
inputSocketName = "audio_input.sock"
|
||||||
headerSize = 17 // Fixed header size: 4+1+4+8 bytes
|
headerSize = 17 // Fixed header size: 4+1+4+8 bytes
|
||||||
|
@ -144,8 +145,8 @@ func NewUnifiedAudioServer(isInput bool) (*UnifiedAudioServer, error) {
|
||||||
logger: logger,
|
logger: logger,
|
||||||
socketPath: socketPath,
|
socketPath: socketPath,
|
||||||
magicNumber: magicNumber,
|
magicNumber: magicNumber,
|
||||||
messageChan: make(chan *UnifiedIPCMessage, GetConfig().ChannelBufferSize),
|
messageChan: make(chan *UnifiedIPCMessage, Config.ChannelBufferSize),
|
||||||
processChan: make(chan *UnifiedIPCMessage, GetConfig().ChannelBufferSize),
|
processChan: make(chan *UnifiedIPCMessage, Config.ChannelBufferSize),
|
||||||
socketBufferConfig: DefaultSocketBufferConfig(),
|
socketBufferConfig: DefaultSocketBufferConfig(),
|
||||||
latencyMonitor: nil,
|
latencyMonitor: nil,
|
||||||
adaptiveOptimizer: nil,
|
adaptiveOptimizer: nil,
|
||||||
|
@ -311,7 +312,7 @@ func (s *UnifiedAudioServer) readMessage(conn net.Conn) (*UnifiedIPCMessage, err
|
||||||
timestamp := int64(binary.LittleEndian.Uint64(header[9:17]))
|
timestamp := int64(binary.LittleEndian.Uint64(header[9:17]))
|
||||||
|
|
||||||
// Validate length
|
// Validate length
|
||||||
if length > uint32(GetConfig().MaxFrameSize) {
|
if length > uint32(Config.MaxFrameSize) {
|
||||||
return nil, fmt.Errorf("message too large: %d bytes", length)
|
return nil, fmt.Errorf("message too large: %d bytes", length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,7 +340,10 @@ func (s *UnifiedAudioServer) SendFrame(frame []byte) error {
|
||||||
defer s.mtx.Unlock()
|
defer s.mtx.Unlock()
|
||||||
|
|
||||||
if !s.running || s.conn == nil {
|
if !s.running || s.conn == nil {
|
||||||
return fmt.Errorf("no client connected")
|
// Silently drop frames when no client is connected
|
||||||
|
// This prevents "no client connected" warnings during startup and quality changes
|
||||||
|
atomic.AddInt64(&s.droppedFrames, 1)
|
||||||
|
return nil // Return nil to avoid flooding logs with connection warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
@ -398,7 +402,7 @@ func (s *UnifiedAudioServer) writeMessage(conn net.Conn, msg *UnifiedIPCMessage)
|
||||||
|
|
||||||
// UnifiedAudioClient provides common functionality for both input and output clients
|
// UnifiedAudioClient provides common functionality for both input and output clients
|
||||||
type UnifiedAudioClient struct {
|
type UnifiedAudioClient struct {
|
||||||
// Atomic fields first for ARM32 alignment
|
// Atomic counters for frame statistics
|
||||||
droppedFrames int64 // Atomic counter for dropped frames
|
droppedFrames int64 // Atomic counter for dropped frames
|
||||||
totalFrames int64 // Atomic counter for total frames
|
totalFrames int64 // Atomic counter for total frames
|
||||||
|
|
||||||
|
@ -409,6 +413,13 @@ type UnifiedAudioClient struct {
|
||||||
socketPath string
|
socketPath string
|
||||||
magicNumber uint32
|
magicNumber uint32
|
||||||
bufferPool *AudioBufferPool // Buffer pool for memory optimization
|
bufferPool *AudioBufferPool // Buffer pool for memory optimization
|
||||||
|
|
||||||
|
// Connection health monitoring
|
||||||
|
lastHealthCheck time.Time
|
||||||
|
connectionErrors int64 // Atomic counter for connection errors
|
||||||
|
autoReconnect bool // Enable automatic reconnection
|
||||||
|
healthCheckTicker *time.Ticker
|
||||||
|
stopHealthCheck chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUnifiedAudioClient creates a new unified audio client
|
// NewUnifiedAudioClient creates a new unified audio client
|
||||||
|
@ -430,10 +441,12 @@ func NewUnifiedAudioClient(isInput bool) *UnifiedAudioClient {
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger()
|
logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger()
|
||||||
|
|
||||||
return &UnifiedAudioClient{
|
return &UnifiedAudioClient{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
socketPath: socketPath,
|
socketPath: socketPath,
|
||||||
magicNumber: magicNumber,
|
magicNumber: magicNumber,
|
||||||
bufferPool: NewAudioBufferPool(GetConfig().MaxFrameSize),
|
bufferPool: NewAudioBufferPool(Config.MaxFrameSize),
|
||||||
|
autoReconnect: true, // Enable automatic reconnection by default
|
||||||
|
stopHealthCheck: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,32 +466,46 @@ func (c *UnifiedAudioClient) Connect() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try connecting multiple times as the server might not be ready
|
// Try connecting multiple times as the server might not be ready
|
||||||
// Reduced retry count and delay for faster startup
|
// Use configurable retry parameters for better control
|
||||||
for i := 0; i < 10; i++ {
|
maxAttempts := Config.MaxConnectionAttempts
|
||||||
conn, err := net.Dial("unix", c.socketPath)
|
initialDelay := Config.ConnectionRetryDelay
|
||||||
|
maxDelay := Config.MaxConnectionRetryDelay
|
||||||
|
backoffFactor := Config.ConnectionBackoffFactor
|
||||||
|
|
||||||
|
for i := 0; i < maxAttempts; i++ {
|
||||||
|
// Set connection timeout for each attempt
|
||||||
|
conn, err := net.DialTimeout("unix", c.socketPath, Config.ConnectionTimeoutDelay)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.conn = conn
|
c.conn = conn
|
||||||
c.running = true
|
c.running = true
|
||||||
// Reset frame counters on successful connection
|
// Reset frame counters on successful connection
|
||||||
atomic.StoreInt64(&c.totalFrames, 0)
|
atomic.StoreInt64(&c.totalFrames, 0)
|
||||||
atomic.StoreInt64(&c.droppedFrames, 0)
|
atomic.StoreInt64(&c.droppedFrames, 0)
|
||||||
c.logger.Info().Str("socket_path", c.socketPath).Msg("Connected to server")
|
atomic.StoreInt64(&c.connectionErrors, 0)
|
||||||
|
c.lastHealthCheck = time.Now()
|
||||||
|
// Start health check monitoring if auto-reconnect is enabled
|
||||||
|
if c.autoReconnect {
|
||||||
|
c.startHealthCheck()
|
||||||
|
}
|
||||||
|
c.logger.Info().Str("socket_path", c.socketPath).Int("attempt", i+1).Msg("Connected to server")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Exponential backoff starting from config
|
|
||||||
backoffStart := GetConfig().BackoffStart
|
// Log connection attempt failure
|
||||||
delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
|
c.logger.Debug().Err(err).Str("socket_path", c.socketPath).Int("attempt", i+1).Int("max_attempts", maxAttempts).Msg("Connection attempt failed")
|
||||||
maxDelay := GetConfig().MaxRetryDelay
|
|
||||||
if delay > maxDelay {
|
// Don't sleep after the last attempt
|
||||||
delay = maxDelay
|
if i < maxAttempts-1 {
|
||||||
|
// Calculate adaptive delay based on connection failure patterns
|
||||||
|
delay := c.calculateAdaptiveDelay(i, initialDelay, maxDelay, backoffFactor)
|
||||||
|
time.Sleep(delay)
|
||||||
}
|
}
|
||||||
time.Sleep(delay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure clean state on connection failure
|
// Ensure clean state on connection failure
|
||||||
c.conn = nil
|
c.conn = nil
|
||||||
c.running = false
|
c.running = false
|
||||||
return fmt.Errorf("failed to connect to audio server after 10 attempts")
|
return fmt.Errorf("failed to connect to audio server after %d attempts", Config.MaxConnectionAttempts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect disconnects the client from the server
|
// Disconnect disconnects the client from the server
|
||||||
|
@ -492,6 +519,9 @@ func (c *UnifiedAudioClient) Disconnect() {
|
||||||
|
|
||||||
c.running = false
|
c.running = false
|
||||||
|
|
||||||
|
// Stop health check monitoring
|
||||||
|
c.stopHealthCheckMonitoring()
|
||||||
|
|
||||||
if c.conn != nil {
|
if c.conn != nil {
|
||||||
c.conn.Close()
|
c.conn.Close()
|
||||||
c.conn = nil
|
c.conn = nil
|
||||||
|
@ -511,7 +541,122 @@ func (c *UnifiedAudioClient) IsConnected() bool {
|
||||||
func (c *UnifiedAudioClient) GetFrameStats() (total, dropped int64) {
|
func (c *UnifiedAudioClient) GetFrameStats() (total, dropped int64) {
|
||||||
total = atomic.LoadInt64(&c.totalFrames)
|
total = atomic.LoadInt64(&c.totalFrames)
|
||||||
dropped = atomic.LoadInt64(&c.droppedFrames)
|
dropped = atomic.LoadInt64(&c.droppedFrames)
|
||||||
return total, dropped
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// startHealthCheck starts the connection health monitoring
|
||||||
|
func (c *UnifiedAudioClient) startHealthCheck() {
|
||||||
|
if c.healthCheckTicker != nil {
|
||||||
|
c.healthCheckTicker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.healthCheckTicker = time.NewTicker(Config.HealthCheckInterval)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.healthCheckTicker.C:
|
||||||
|
c.performHealthCheck()
|
||||||
|
case <-c.stopHealthCheck:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopHealthCheckMonitoring stops the health check monitoring
|
||||||
|
func (c *UnifiedAudioClient) stopHealthCheckMonitoring() {
|
||||||
|
if c.healthCheckTicker != nil {
|
||||||
|
c.healthCheckTicker.Stop()
|
||||||
|
c.healthCheckTicker = nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case c.stopHealthCheck <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// performHealthCheck checks the connection health and attempts reconnection if needed
|
||||||
|
func (c *UnifiedAudioClient) performHealthCheck() {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
if !c.running || c.conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple health check: try to get connection info
|
||||||
|
if tcpConn, ok := c.conn.(*net.UnixConn); ok {
|
||||||
|
if _, err := tcpConn.File(); err != nil {
|
||||||
|
// Connection is broken
|
||||||
|
atomic.AddInt64(&c.connectionErrors, 1)
|
||||||
|
c.logger.Warn().Err(err).Msg("Connection health check failed, attempting reconnection")
|
||||||
|
|
||||||
|
// Close the broken connection
|
||||||
|
c.conn.Close()
|
||||||
|
c.conn = nil
|
||||||
|
c.running = false
|
||||||
|
|
||||||
|
// Attempt reconnection
|
||||||
|
go func() {
|
||||||
|
time.Sleep(Config.ReconnectionInterval)
|
||||||
|
if err := c.Connect(); err != nil {
|
||||||
|
c.logger.Error().Err(err).Msg("Failed to reconnect during health check")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lastHealthCheck = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoReconnect enables or disables automatic reconnection
|
||||||
|
func (c *UnifiedAudioClient) SetAutoReconnect(enabled bool) {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
c.autoReconnect = enabled
|
||||||
|
if !enabled {
|
||||||
|
c.stopHealthCheckMonitoring()
|
||||||
|
} else if c.running {
|
||||||
|
c.startHealthCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnectionErrors returns the number of connection errors
|
||||||
|
func (c *UnifiedAudioClient) GetConnectionErrors() int64 {
|
||||||
|
return atomic.LoadInt64(&c.connectionErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateAdaptiveDelay calculates retry delay based on system load and failure patterns
|
||||||
|
func (c *UnifiedAudioClient) calculateAdaptiveDelay(attempt int, initialDelay, maxDelay time.Duration, backoffFactor float64) time.Duration {
|
||||||
|
// Base exponential backoff
|
||||||
|
baseDelay := time.Duration(float64(initialDelay.Nanoseconds()) * math.Pow(backoffFactor, float64(attempt)))
|
||||||
|
|
||||||
|
// Get connection error history for adaptive adjustment
|
||||||
|
errorCount := atomic.LoadInt64(&c.connectionErrors)
|
||||||
|
|
||||||
|
// Adjust delay based on recent connection errors
|
||||||
|
// More errors = longer delays to avoid overwhelming the server
|
||||||
|
adaptiveFactor := 1.0
|
||||||
|
if errorCount > 5 {
|
||||||
|
adaptiveFactor = 1.5 // 50% longer delays after many errors
|
||||||
|
} else if errorCount > 10 {
|
||||||
|
adaptiveFactor = 2.0 // Double delays after excessive errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply adaptive factor
|
||||||
|
adaptiveDelay := time.Duration(float64(baseDelay.Nanoseconds()) * adaptiveFactor)
|
||||||
|
|
||||||
|
// Ensure we don't exceed maximum delay
|
||||||
|
if adaptiveDelay > maxDelay {
|
||||||
|
adaptiveDelay = maxDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add small random jitter to avoid thundering herd
|
||||||
|
jitter := time.Duration(float64(adaptiveDelay.Nanoseconds()) * 0.1 * (0.5 + float64(attempt%3)/6.0))
|
||||||
|
adaptiveDelay += jitter
|
||||||
|
|
||||||
|
return adaptiveDelay
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for socket paths
|
// Helper functions for socket paths
|
||||||
|
|
|
@ -63,9 +63,9 @@ func (aim *AudioInputIPCManager) Start() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
config := InputIPCConfig{
|
config := InputIPCConfig{
|
||||||
SampleRate: GetConfig().InputIPCSampleRate,
|
SampleRate: Config.InputIPCSampleRate,
|
||||||
Channels: GetConfig().InputIPCChannels,
|
Channels: Config.InputIPCChannels,
|
||||||
FrameSize: GetConfig().InputIPCFrameSize,
|
FrameSize: Config.InputIPCFrameSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate configuration before using it
|
// Validate configuration before using it
|
||||||
|
@ -80,7 +80,7 @@ func (aim *AudioInputIPCManager) Start() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for subprocess readiness
|
// Wait for subprocess readiness
|
||||||
time.Sleep(GetConfig().LongSleepDuration)
|
time.Sleep(Config.LongSleepDuration)
|
||||||
|
|
||||||
err = aim.supervisor.SendConfig(config)
|
err = aim.supervisor.SendConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -57,9 +57,9 @@ func (aom *AudioOutputIPCManager) Start() error {
|
||||||
|
|
||||||
// Send initial configuration
|
// Send initial configuration
|
||||||
config := OutputIPCConfig{
|
config := OutputIPCConfig{
|
||||||
SampleRate: GetConfig().SampleRate,
|
SampleRate: Config.SampleRate,
|
||||||
Channels: GetConfig().Channels,
|
Channels: Config.Channels,
|
||||||
FrameSize: int(GetConfig().AudioQualityMediumFrameSize.Milliseconds()),
|
FrameSize: int(Config.AudioQualityMediumFrameSize.Milliseconds()),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := aom.SendConfig(config); err != nil {
|
if err := aom.SendConfig(config); err != nil {
|
||||||
|
|
|
@ -105,7 +105,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if atomic.CompareAndSwapInt32(&micContentionInitialized, 0, 1) {
|
if atomic.CompareAndSwapInt32(&micContentionInitialized, 0, 1) {
|
||||||
manager := NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
|
manager := NewMicrophoneContentionManager(Config.MicContentionTimeout)
|
||||||
atomic.StorePointer(&globalMicContentionManager, unsafe.Pointer(manager))
|
atomic.StorePointer(&globalMicContentionManager, unsafe.Pointer(manager))
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
|
||||||
return (*MicrophoneContentionManager)(ptr)
|
return (*MicrophoneContentionManager)(ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
|
return NewMicrophoneContentionManager(Config.MicContentionTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TryMicrophoneOperation() OperationResult {
|
func TryMicrophoneOperation() OperationResult {
|
||||||
|
|
|
@ -64,10 +64,10 @@ type OptimizerConfig struct {
|
||||||
func DefaultOptimizerConfig() OptimizerConfig {
|
func DefaultOptimizerConfig() OptimizerConfig {
|
||||||
return OptimizerConfig{
|
return OptimizerConfig{
|
||||||
MaxOptimizationLevel: 8,
|
MaxOptimizationLevel: 8,
|
||||||
CooldownPeriod: GetConfig().CooldownPeriod,
|
CooldownPeriod: Config.CooldownPeriod,
|
||||||
Aggressiveness: GetConfig().OptimizerAggressiveness,
|
Aggressiveness: Config.OptimizerAggressiveness,
|
||||||
RollbackThreshold: GetConfig().RollbackThreshold,
|
RollbackThreshold: Config.RollbackThreshold,
|
||||||
StabilityPeriod: GetConfig().AdaptiveOptimizerStability,
|
StabilityPeriod: Config.AdaptiveOptimizerStability,
|
||||||
|
|
||||||
// Adaptive interval defaults
|
// Adaptive interval defaults
|
||||||
MinOptimizationInterval: 100 * time.Millisecond, // High stability: check every 100ms
|
MinOptimizationInterval: 100 * time.Millisecond, // High stability: check every 100ms
|
||||||
|
@ -142,7 +142,7 @@ func (ao *AdaptiveOptimizer) handleLatencyOptimization(metrics LatencyMetrics) e
|
||||||
// calculateTargetOptimizationLevel determines the appropriate optimization level
|
// calculateTargetOptimizationLevel determines the appropriate optimization level
|
||||||
func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMetrics) int64 {
|
func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMetrics) int64 {
|
||||||
// Base calculation on current latency vs target
|
// Base calculation on current latency vs target
|
||||||
latencyRatio := float64(metrics.Current) / float64(GetConfig().AdaptiveOptimizerLatencyTarget) // 50ms target
|
latencyRatio := float64(metrics.Current) / float64(Config.AdaptiveOptimizerLatencyTarget) // 50ms target
|
||||||
|
|
||||||
// Adjust based on trend
|
// Adjust based on trend
|
||||||
switch metrics.Trend {
|
switch metrics.Trend {
|
||||||
|
@ -158,7 +158,7 @@ func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMet
|
||||||
latencyRatio *= ao.config.Aggressiveness
|
latencyRatio *= ao.config.Aggressiveness
|
||||||
|
|
||||||
// Convert to optimization level
|
// Convert to optimization level
|
||||||
targetLevel := int64(latencyRatio * GetConfig().LatencyScalingFactor) // Scale to 0-10 range
|
targetLevel := int64(latencyRatio * Config.LatencyScalingFactor) // Scale to 0-10 range
|
||||||
if targetLevel > int64(ao.config.MaxOptimizationLevel) {
|
if targetLevel > int64(ao.config.MaxOptimizationLevel) {
|
||||||
targetLevel = int64(ao.config.MaxOptimizationLevel)
|
targetLevel = int64(ao.config.MaxOptimizationLevel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,7 +126,7 @@ func (gm *GoroutineMonitor) GetGoroutineStats() map[string]interface{} {
|
||||||
// GetGoroutineMonitor returns the global goroutine monitor instance
|
// GetGoroutineMonitor returns the global goroutine monitor instance
|
||||||
func GetGoroutineMonitor() *GoroutineMonitor {
|
func GetGoroutineMonitor() *GoroutineMonitor {
|
||||||
if globalGoroutineMonitor == nil {
|
if globalGoroutineMonitor == nil {
|
||||||
globalGoroutineMonitor = NewGoroutineMonitor(GetConfig().GoroutineMonitorInterval)
|
globalGoroutineMonitor = NewGoroutineMonitor(Config.GoroutineMonitorInterval)
|
||||||
}
|
}
|
||||||
return globalGoroutineMonitor
|
return globalGoroutineMonitor
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ const (
|
||||||
|
|
||||||
// DefaultLatencyConfig returns a sensible default configuration
|
// DefaultLatencyConfig returns a sensible default configuration
|
||||||
func DefaultLatencyConfig() LatencyConfig {
|
func DefaultLatencyConfig() LatencyConfig {
|
||||||
config := GetConfig()
|
config := Config
|
||||||
return LatencyConfig{
|
return LatencyConfig{
|
||||||
TargetLatency: config.LatencyMonitorTarget,
|
TargetLatency: config.LatencyMonitorTarget,
|
||||||
MaxLatency: config.MaxLatencyThreshold,
|
MaxLatency: config.MaxLatencyThreshold,
|
||||||
|
|
|
@ -16,26 +16,26 @@ import (
|
||||||
// Variables for process monitoring (using configuration)
|
// Variables for process monitoring (using configuration)
|
||||||
var (
|
var (
|
||||||
// System constants
|
// System constants
|
||||||
maxCPUPercent = GetConfig().MaxCPUPercent
|
maxCPUPercent = Config.MaxCPUPercent
|
||||||
minCPUPercent = GetConfig().MinCPUPercent
|
minCPUPercent = Config.MinCPUPercent
|
||||||
defaultClockTicks = GetConfig().DefaultClockTicks
|
defaultClockTicks = Config.DefaultClockTicks
|
||||||
defaultMemoryGB = GetConfig().DefaultMemoryGB
|
defaultMemoryGB = Config.DefaultMemoryGB
|
||||||
|
|
||||||
// Monitoring thresholds
|
// Monitoring thresholds
|
||||||
maxWarmupSamples = GetConfig().MaxWarmupSamples
|
maxWarmupSamples = Config.MaxWarmupSamples
|
||||||
warmupCPUSamples = GetConfig().WarmupCPUSamples
|
warmupCPUSamples = Config.WarmupCPUSamples
|
||||||
|
|
||||||
// Channel buffer size
|
// Channel buffer size
|
||||||
metricsChannelBuffer = GetConfig().MetricsChannelBuffer
|
metricsChannelBuffer = Config.MetricsChannelBuffer
|
||||||
|
|
||||||
// Clock tick detection ranges
|
// Clock tick detection ranges
|
||||||
minValidClockTicks = float64(GetConfig().MinValidClockTicks)
|
minValidClockTicks = float64(Config.MinValidClockTicks)
|
||||||
maxValidClockTicks = float64(GetConfig().MaxValidClockTicks)
|
maxValidClockTicks = float64(Config.MaxValidClockTicks)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Variables for process monitoring
|
// Variables for process monitoring
|
||||||
var (
|
var (
|
||||||
pageSize = GetConfig().PageSize
|
pageSize = Config.PageSize
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProcessMetrics represents CPU and memory usage metrics for a process
|
// ProcessMetrics represents CPU and memory usage metrics for a process
|
||||||
|
@ -233,7 +233,7 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
|
||||||
|
|
||||||
// Calculate memory percentage (RSS / total system memory)
|
// Calculate memory percentage (RSS / total system memory)
|
||||||
if totalMem := pm.getTotalMemory(); totalMem > 0 {
|
if totalMem := pm.getTotalMemory(); totalMem > 0 {
|
||||||
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * GetConfig().PercentageMultiplier
|
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * Config.PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state for next calculation
|
// Update state for next calculation
|
||||||
|
@ -283,7 +283,7 @@ func (pm *ProcessMonitor) calculateCPUPercent(totalCPUTime int64, state *process
|
||||||
// Convert from clock ticks to seconds using actual system clock ticks
|
// Convert from clock ticks to seconds using actual system clock ticks
|
||||||
clockTicks := pm.getClockTicks()
|
clockTicks := pm.getClockTicks()
|
||||||
cpuSeconds := cpuDelta / clockTicks
|
cpuSeconds := cpuDelta / clockTicks
|
||||||
cpuPercent := (cpuSeconds / timeDelta) * GetConfig().PercentageMultiplier
|
cpuPercent := (cpuSeconds / timeDelta) * Config.PercentageMultiplier
|
||||||
|
|
||||||
// Apply bounds
|
// Apply bounds
|
||||||
if cpuPercent > maxCPUPercent {
|
if cpuPercent > maxCPUPercent {
|
||||||
|
@ -335,7 +335,7 @@ func (pm *ProcessMonitor) getClockTicks() float64 {
|
||||||
if len(fields) >= 2 {
|
if len(fields) >= 2 {
|
||||||
if period, err := strconv.ParseInt(fields[1], 10, 64); err == nil && period > 0 {
|
if period, err := strconv.ParseInt(fields[1], 10, 64); err == nil && period > 0 {
|
||||||
// Convert nanoseconds to Hz
|
// Convert nanoseconds to Hz
|
||||||
hz := GetConfig().CGONanosecondsPerSecond / float64(period)
|
hz := Config.CGONanosecondsPerSecond / float64(period)
|
||||||
if hz >= minValidClockTicks && hz <= maxValidClockTicks {
|
if hz >= minValidClockTicks && hz <= maxValidClockTicks {
|
||||||
pm.clockTicks = hz
|
pm.clockTicks = hz
|
||||||
return
|
return
|
||||||
|
@ -363,7 +363,7 @@ func (pm *ProcessMonitor) getTotalMemory() int64 {
|
||||||
pm.memoryOnce.Do(func() {
|
pm.memoryOnce.Do(func() {
|
||||||
file, err := os.Open("/proc/meminfo")
|
file, err := os.Open("/proc/meminfo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pm.totalMemory = int64(defaultMemoryGB) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes)
|
pm.totalMemory = int64(defaultMemoryGB) * int64(Config.ProcessMonitorKBToBytes) * int64(Config.ProcessMonitorKBToBytes) * int64(Config.ProcessMonitorKBToBytes)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
@ -375,14 +375,14 @@ func (pm *ProcessMonitor) getTotalMemory() int64 {
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
if len(fields) >= 2 {
|
if len(fields) >= 2 {
|
||||||
if kb, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
|
if kb, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
|
||||||
pm.totalMemory = kb * int64(GetConfig().ProcessMonitorKBToBytes)
|
pm.totalMemory = kb * int64(Config.ProcessMonitorKBToBytes)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pm.totalMemory = int64(defaultMemoryGB) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes) // Fallback
|
pm.totalMemory = int64(defaultMemoryGB) * int64(Config.ProcessMonitorKBToBytes) * int64(Config.ProcessMonitorKBToBytes) * int64(Config.ProcessMonitorKBToBytes) // Fallback
|
||||||
})
|
})
|
||||||
return pm.totalMemory
|
return pm.totalMemory
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ func RunAudioOutputServer() error {
|
||||||
StopNonBlockingAudioStreaming()
|
StopNonBlockingAudioStreaming()
|
||||||
|
|
||||||
// Give some time for cleanup
|
// Give some time for cleanup
|
||||||
time.Sleep(GetConfig().DefaultSleepDuration)
|
time.Sleep(Config.DefaultSleepDuration)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,9 +84,9 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
buffer := make([]byte, GetMaxAudioFrameSize())
|
buffer := make([]byte, GetMaxAudioFrameSize())
|
||||||
|
|
||||||
consecutiveErrors := 0
|
consecutiveErrors := 0
|
||||||
maxConsecutiveErrors := GetConfig().MaxConsecutiveErrors
|
maxConsecutiveErrors := Config.MaxConsecutiveErrors
|
||||||
errorBackoffDelay := GetConfig().RetryDelay
|
errorBackoffDelay := Config.RetryDelay
|
||||||
maxErrorBackoff := GetConfig().MaxRetryDelay
|
maxErrorBackoff := Config.MaxRetryDelay
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -123,18 +123,18 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
Err(initErr).
|
Err(initErr).
|
||||||
Msg("Failed to reinitialize audio system")
|
Msg("Failed to reinitialize audio system")
|
||||||
// Exponential backoff for reinitialization failures
|
// Exponential backoff for reinitialization failures
|
||||||
errorBackoffDelay = time.Duration(float64(errorBackoffDelay) * GetConfig().BackoffMultiplier)
|
errorBackoffDelay = time.Duration(float64(errorBackoffDelay) * Config.BackoffMultiplier)
|
||||||
if errorBackoffDelay > maxErrorBackoff {
|
if errorBackoffDelay > maxErrorBackoff {
|
||||||
errorBackoffDelay = maxErrorBackoff
|
errorBackoffDelay = maxErrorBackoff
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getOutputStreamingLogger().Info().Msg("Audio system reinitialized successfully")
|
getOutputStreamingLogger().Info().Msg("Audio system reinitialized successfully")
|
||||||
consecutiveErrors = 0
|
consecutiveErrors = 0
|
||||||
errorBackoffDelay = GetConfig().RetryDelay // Reset backoff
|
errorBackoffDelay = Config.RetryDelay // Reset backoff
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Brief delay for transient errors
|
// Brief delay for transient errors
|
||||||
time.Sleep(GetConfig().ShortSleepDuration)
|
time.Sleep(Config.ShortSleepDuration)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -142,7 +142,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
// Success - reset error counters
|
// Success - reset error counters
|
||||||
if consecutiveErrors > 0 {
|
if consecutiveErrors > 0 {
|
||||||
consecutiveErrors = 0
|
consecutiveErrors = 0
|
||||||
errorBackoffDelay = GetConfig().RetryDelay
|
errorBackoffDelay = Config.RetryDelay
|
||||||
}
|
}
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
|
@ -164,7 +164,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
RecordFrameReceived(n)
|
RecordFrameReceived(n)
|
||||||
}
|
}
|
||||||
// Small delay to prevent busy waiting
|
// Small delay to prevent busy waiting
|
||||||
time.Sleep(GetConfig().ShortSleepDuration)
|
time.Sleep(Config.ShortSleepDuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -185,6 +185,6 @@ func StopAudioOutputStreaming() {
|
||||||
|
|
||||||
// Wait for streaming to stop
|
// Wait for streaming to stop
|
||||||
for atomic.LoadInt32(&outputStreamingRunning) == 1 {
|
for atomic.LoadInt32(&outputStreamingRunning) == 1 {
|
||||||
time.Sleep(GetConfig().ShortSleepDuration)
|
time.Sleep(Config.ShortSleepDuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,19 +19,19 @@ const (
|
||||||
|
|
||||||
// Restart configuration is now retrieved from centralized config
|
// Restart configuration is now retrieved from centralized config
|
||||||
func getMaxRestartAttempts() int {
|
func getMaxRestartAttempts() int {
|
||||||
return GetConfig().MaxRestartAttempts
|
return Config.MaxRestartAttempts
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRestartWindow() time.Duration {
|
func getRestartWindow() time.Duration {
|
||||||
return GetConfig().RestartWindow
|
return Config.RestartWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRestartDelay() time.Duration {
|
func getRestartDelay() time.Duration {
|
||||||
return GetConfig().RestartDelay
|
return Config.RestartDelay
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMaxRestartDelay() time.Duration {
|
func getMaxRestartDelay() time.Duration {
|
||||||
return GetConfig().MaxRestartDelay
|
return Config.MaxRestartDelay
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioOutputSupervisor manages the audio output server subprocess lifecycle
|
// AudioOutputSupervisor manages the audio output server subprocess lifecycle
|
||||||
|
@ -145,7 +145,7 @@ func (s *AudioOutputSupervisor) Stop() {
|
||||||
select {
|
select {
|
||||||
case <-s.processDone:
|
case <-s.processDone:
|
||||||
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component stopped gracefully")
|
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component stopped gracefully")
|
||||||
case <-time.After(GetConfig().OutputSupervisorTimeout):
|
case <-time.After(Config.OutputSupervisorTimeout):
|
||||||
s.logger.Warn().Str("component", AudioOutputSupervisorComponent).Msg("component did not stop gracefully, forcing termination")
|
s.logger.Warn().Str("component", AudioOutputSupervisorComponent).Msg("component did not stop gracefully, forcing termination")
|
||||||
s.forceKillProcess("audio output server")
|
s.forceKillProcess("audio output server")
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ func (s *AudioOutputSupervisor) supervisionLoop() {
|
||||||
// Configure supervision parameters
|
// Configure supervision parameters
|
||||||
config := SupervisionConfig{
|
config := SupervisionConfig{
|
||||||
ProcessType: "audio output server",
|
ProcessType: "audio output server",
|
||||||
Timeout: GetConfig().OutputSupervisorTimeout,
|
Timeout: Config.OutputSupervisorTimeout,
|
||||||
EnableRestart: true,
|
EnableRestart: true,
|
||||||
MaxRestartAttempts: getMaxRestartAttempts(),
|
MaxRestartAttempts: getMaxRestartAttempts(),
|
||||||
RestartWindow: getRestartWindow(),
|
RestartWindow: getRestartWindow(),
|
||||||
|
|
|
@ -39,7 +39,7 @@ var (
|
||||||
|
|
||||||
// MaxAudioFrameSize is now retrieved from centralized config
|
// MaxAudioFrameSize is now retrieved from centralized config
|
||||||
func GetMaxAudioFrameSize() int {
|
func GetMaxAudioFrameSize() int {
|
||||||
return GetConfig().MaxAudioFrameSize
|
return Config.MaxAudioFrameSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioQuality represents different audio quality presets
|
// AudioQuality represents different audio quality presets
|
||||||
|
@ -74,17 +74,17 @@ type AudioMetrics struct {
|
||||||
var (
|
var (
|
||||||
currentConfig = AudioConfig{
|
currentConfig = AudioConfig{
|
||||||
Quality: AudioQualityMedium,
|
Quality: AudioQualityMedium,
|
||||||
Bitrate: GetConfig().AudioQualityMediumOutputBitrate,
|
Bitrate: Config.AudioQualityMediumOutputBitrate,
|
||||||
SampleRate: GetConfig().SampleRate,
|
SampleRate: Config.SampleRate,
|
||||||
Channels: GetConfig().Channels,
|
Channels: Config.Channels,
|
||||||
FrameSize: GetConfig().AudioQualityMediumFrameSize,
|
FrameSize: Config.AudioQualityMediumFrameSize,
|
||||||
}
|
}
|
||||||
currentMicrophoneConfig = AudioConfig{
|
currentMicrophoneConfig = AudioConfig{
|
||||||
Quality: AudioQualityMedium,
|
Quality: AudioQualityMedium,
|
||||||
Bitrate: GetConfig().AudioQualityMediumInputBitrate,
|
Bitrate: Config.AudioQualityMediumInputBitrate,
|
||||||
SampleRate: GetConfig().SampleRate,
|
SampleRate: Config.SampleRate,
|
||||||
Channels: 1,
|
Channels: 1,
|
||||||
FrameSize: GetConfig().AudioQualityMediumFrameSize,
|
FrameSize: Config.AudioQualityMediumFrameSize,
|
||||||
}
|
}
|
||||||
metrics AudioMetrics
|
metrics AudioMetrics
|
||||||
)
|
)
|
||||||
|
@ -96,24 +96,24 @@ var qualityPresets = map[AudioQuality]struct {
|
||||||
frameSize time.Duration
|
frameSize time.Duration
|
||||||
}{
|
}{
|
||||||
AudioQualityLow: {
|
AudioQualityLow: {
|
||||||
outputBitrate: GetConfig().AudioQualityLowOutputBitrate, inputBitrate: GetConfig().AudioQualityLowInputBitrate,
|
outputBitrate: Config.AudioQualityLowOutputBitrate, inputBitrate: Config.AudioQualityLowInputBitrate,
|
||||||
sampleRate: GetConfig().AudioQualityLowSampleRate, channels: GetConfig().AudioQualityLowChannels,
|
sampleRate: Config.AudioQualityLowSampleRate, channels: Config.AudioQualityLowChannels,
|
||||||
frameSize: GetConfig().AudioQualityLowFrameSize,
|
frameSize: Config.AudioQualityLowFrameSize,
|
||||||
},
|
},
|
||||||
AudioQualityMedium: {
|
AudioQualityMedium: {
|
||||||
outputBitrate: GetConfig().AudioQualityMediumOutputBitrate, inputBitrate: GetConfig().AudioQualityMediumInputBitrate,
|
outputBitrate: Config.AudioQualityMediumOutputBitrate, inputBitrate: Config.AudioQualityMediumInputBitrate,
|
||||||
sampleRate: GetConfig().AudioQualityMediumSampleRate, channels: GetConfig().AudioQualityMediumChannels,
|
sampleRate: Config.AudioQualityMediumSampleRate, channels: Config.AudioQualityMediumChannels,
|
||||||
frameSize: GetConfig().AudioQualityMediumFrameSize,
|
frameSize: Config.AudioQualityMediumFrameSize,
|
||||||
},
|
},
|
||||||
AudioQualityHigh: {
|
AudioQualityHigh: {
|
||||||
outputBitrate: GetConfig().AudioQualityHighOutputBitrate, inputBitrate: GetConfig().AudioQualityHighInputBitrate,
|
outputBitrate: Config.AudioQualityHighOutputBitrate, inputBitrate: Config.AudioQualityHighInputBitrate,
|
||||||
sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityHighChannels,
|
sampleRate: Config.SampleRate, channels: Config.AudioQualityHighChannels,
|
||||||
frameSize: GetConfig().AudioQualityHighFrameSize,
|
frameSize: Config.AudioQualityHighFrameSize,
|
||||||
},
|
},
|
||||||
AudioQualityUltra: {
|
AudioQualityUltra: {
|
||||||
outputBitrate: GetConfig().AudioQualityUltraOutputBitrate, inputBitrate: GetConfig().AudioQualityUltraInputBitrate,
|
outputBitrate: Config.AudioQualityUltraOutputBitrate, inputBitrate: Config.AudioQualityUltraInputBitrate,
|
||||||
sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityUltraChannels,
|
sampleRate: Config.SampleRate, channels: Config.AudioQualityUltraChannels,
|
||||||
frameSize: GetConfig().AudioQualityUltraFrameSize,
|
frameSize: Config.AudioQualityUltraFrameSize,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
|
||||||
Bitrate: preset.inputBitrate,
|
Bitrate: preset.inputBitrate,
|
||||||
SampleRate: func() int {
|
SampleRate: func() int {
|
||||||
if quality == AudioQualityLow {
|
if quality == AudioQualityLow {
|
||||||
return GetConfig().AudioQualityMicLowSampleRate
|
return Config.AudioQualityMicLowSampleRate
|
||||||
}
|
}
|
||||||
return preset.sampleRate
|
return preset.sampleRate
|
||||||
}(),
|
}(),
|
||||||
|
@ -172,36 +172,36 @@ func SetAudioQuality(quality AudioQuality) {
|
||||||
var complexity, vbr, signalType, bandwidth, dtx int
|
var complexity, vbr, signalType, bandwidth, dtx int
|
||||||
switch quality {
|
switch quality {
|
||||||
case AudioQualityLow:
|
case AudioQualityLow:
|
||||||
complexity = GetConfig().AudioQualityLowOpusComplexity
|
complexity = Config.AudioQualityLowOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityLowOpusVBR
|
vbr = Config.AudioQualityLowOpusVBR
|
||||||
signalType = GetConfig().AudioQualityLowOpusSignalType
|
signalType = Config.AudioQualityLowOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityLowOpusBandwidth
|
bandwidth = Config.AudioQualityLowOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityLowOpusDTX
|
dtx = Config.AudioQualityLowOpusDTX
|
||||||
case AudioQualityMedium:
|
case AudioQualityMedium:
|
||||||
complexity = GetConfig().AudioQualityMediumOpusComplexity
|
complexity = Config.AudioQualityMediumOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityMediumOpusVBR
|
vbr = Config.AudioQualityMediumOpusVBR
|
||||||
signalType = GetConfig().AudioQualityMediumOpusSignalType
|
signalType = Config.AudioQualityMediumOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
|
bandwidth = Config.AudioQualityMediumOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityMediumOpusDTX
|
dtx = Config.AudioQualityMediumOpusDTX
|
||||||
case AudioQualityHigh:
|
case AudioQualityHigh:
|
||||||
complexity = GetConfig().AudioQualityHighOpusComplexity
|
complexity = Config.AudioQualityHighOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityHighOpusVBR
|
vbr = Config.AudioQualityHighOpusVBR
|
||||||
signalType = GetConfig().AudioQualityHighOpusSignalType
|
signalType = Config.AudioQualityHighOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityHighOpusBandwidth
|
bandwidth = Config.AudioQualityHighOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityHighOpusDTX
|
dtx = Config.AudioQualityHighOpusDTX
|
||||||
case AudioQualityUltra:
|
case AudioQualityUltra:
|
||||||
complexity = GetConfig().AudioQualityUltraOpusComplexity
|
complexity = Config.AudioQualityUltraOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityUltraOpusVBR
|
vbr = Config.AudioQualityUltraOpusVBR
|
||||||
signalType = GetConfig().AudioQualityUltraOpusSignalType
|
signalType = Config.AudioQualityUltraOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityUltraOpusBandwidth
|
bandwidth = Config.AudioQualityUltraOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityUltraOpusDTX
|
dtx = Config.AudioQualityUltraOpusDTX
|
||||||
default:
|
default:
|
||||||
// Use medium quality as fallback
|
// Use medium quality as fallback
|
||||||
complexity = GetConfig().AudioQualityMediumOpusComplexity
|
complexity = Config.AudioQualityMediumOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityMediumOpusVBR
|
vbr = Config.AudioQualityMediumOpusVBR
|
||||||
signalType = GetConfig().AudioQualityMediumOpusSignalType
|
signalType = Config.AudioQualityMediumOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
|
bandwidth = Config.AudioQualityMediumOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityMediumOpusDTX
|
dtx = Config.AudioQualityMediumOpusDTX
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart audio output subprocess with new OPUS configuration
|
// Restart audio output subprocess with new OPUS configuration
|
||||||
|
@ -256,7 +256,7 @@ func SetAudioQuality(quality AudioQuality) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to dynamic update if supervisor is not available
|
// Fallback to dynamic update if supervisor is not available
|
||||||
vbrConstraint := GetConfig().CGOOpusVBRConstraint
|
vbrConstraint := Config.CGOOpusVBRConstraint
|
||||||
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
|
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")
|
logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
|
||||||
}
|
}
|
||||||
|
@ -287,36 +287,36 @@ func SetMicrophoneQuality(quality AudioQuality) {
|
||||||
var complexity, vbr, signalType, bandwidth, dtx int
|
var complexity, vbr, signalType, bandwidth, dtx int
|
||||||
switch quality {
|
switch quality {
|
||||||
case AudioQualityLow:
|
case AudioQualityLow:
|
||||||
complexity = GetConfig().AudioQualityLowOpusComplexity
|
complexity = Config.AudioQualityLowOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityLowOpusVBR
|
vbr = Config.AudioQualityLowOpusVBR
|
||||||
signalType = GetConfig().AudioQualityLowOpusSignalType
|
signalType = Config.AudioQualityLowOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityLowOpusBandwidth
|
bandwidth = Config.AudioQualityLowOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityLowOpusDTX
|
dtx = Config.AudioQualityLowOpusDTX
|
||||||
case AudioQualityMedium:
|
case AudioQualityMedium:
|
||||||
complexity = GetConfig().AudioQualityMediumOpusComplexity
|
complexity = Config.AudioQualityMediumOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityMediumOpusVBR
|
vbr = Config.AudioQualityMediumOpusVBR
|
||||||
signalType = GetConfig().AudioQualityMediumOpusSignalType
|
signalType = Config.AudioQualityMediumOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
|
bandwidth = Config.AudioQualityMediumOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityMediumOpusDTX
|
dtx = Config.AudioQualityMediumOpusDTX
|
||||||
case AudioQualityHigh:
|
case AudioQualityHigh:
|
||||||
complexity = GetConfig().AudioQualityHighOpusComplexity
|
complexity = Config.AudioQualityHighOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityHighOpusVBR
|
vbr = Config.AudioQualityHighOpusVBR
|
||||||
signalType = GetConfig().AudioQualityHighOpusSignalType
|
signalType = Config.AudioQualityHighOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityHighOpusBandwidth
|
bandwidth = Config.AudioQualityHighOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityHighOpusDTX
|
dtx = Config.AudioQualityHighOpusDTX
|
||||||
case AudioQualityUltra:
|
case AudioQualityUltra:
|
||||||
complexity = GetConfig().AudioQualityUltraOpusComplexity
|
complexity = Config.AudioQualityUltraOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityUltraOpusVBR
|
vbr = Config.AudioQualityUltraOpusVBR
|
||||||
signalType = GetConfig().AudioQualityUltraOpusSignalType
|
signalType = Config.AudioQualityUltraOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityUltraOpusBandwidth
|
bandwidth = Config.AudioQualityUltraOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityUltraOpusDTX
|
dtx = Config.AudioQualityUltraOpusDTX
|
||||||
default:
|
default:
|
||||||
// Use medium quality as fallback
|
// Use medium quality as fallback
|
||||||
complexity = GetConfig().AudioQualityMediumOpusComplexity
|
complexity = Config.AudioQualityMediumOpusComplexity
|
||||||
vbr = GetConfig().AudioQualityMediumOpusVBR
|
vbr = Config.AudioQualityMediumOpusVBR
|
||||||
signalType = GetConfig().AudioQualityMediumOpusSignalType
|
signalType = Config.AudioQualityMediumOpusSignalType
|
||||||
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
|
bandwidth = Config.AudioQualityMediumOpusBandwidth
|
||||||
dtx = GetConfig().AudioQualityMediumOpusDTX
|
dtx = Config.AudioQualityMediumOpusDTX
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update audio input subprocess configuration dynamically without restart
|
// Update audio input subprocess configuration dynamically without restart
|
||||||
|
|
|
@ -18,8 +18,8 @@ type SocketBufferConfig struct {
|
||||||
// DefaultSocketBufferConfig returns the default socket buffer configuration
|
// DefaultSocketBufferConfig returns the default socket buffer configuration
|
||||||
func DefaultSocketBufferConfig() SocketBufferConfig {
|
func DefaultSocketBufferConfig() SocketBufferConfig {
|
||||||
return SocketBufferConfig{
|
return SocketBufferConfig{
|
||||||
SendBufferSize: GetConfig().SocketOptimalBuffer,
|
SendBufferSize: Config.SocketOptimalBuffer,
|
||||||
RecvBufferSize: GetConfig().SocketOptimalBuffer,
|
RecvBufferSize: Config.SocketOptimalBuffer,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,8 @@ func DefaultSocketBufferConfig() SocketBufferConfig {
|
||||||
// HighLoadSocketBufferConfig returns configuration for high-load scenarios
|
// HighLoadSocketBufferConfig returns configuration for high-load scenarios
|
||||||
func HighLoadSocketBufferConfig() SocketBufferConfig {
|
func HighLoadSocketBufferConfig() SocketBufferConfig {
|
||||||
return SocketBufferConfig{
|
return SocketBufferConfig{
|
||||||
SendBufferSize: GetConfig().SocketMaxBuffer,
|
SendBufferSize: Config.SocketMaxBuffer,
|
||||||
RecvBufferSize: GetConfig().SocketMaxBuffer,
|
RecvBufferSize: Config.SocketMaxBuffer,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,8 +123,8 @@ func ValidateSocketBufferConfig(config SocketBufferConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
minBuffer := GetConfig().SocketMinBuffer
|
minBuffer := Config.SocketMinBuffer
|
||||||
maxBuffer := GetConfig().SocketMaxBuffer
|
maxBuffer := Config.SocketMaxBuffer
|
||||||
|
|
||||||
if config.SendBufferSize < minBuffer {
|
if config.SendBufferSize < minBuffer {
|
||||||
return fmt.Errorf("send buffer size validation failed: got %d bytes, minimum required %d bytes (configured range: %d-%d)",
|
return fmt.Errorf("send buffer size validation failed: got %d bytes, minimum required %d bytes (configured range: %d-%d)",
|
||||||
|
|
|
@ -366,17 +366,17 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
||||||
// Validate buffer size parameter
|
// Validate buffer size parameter
|
||||||
if err := ValidateBufferSize(bufferSize); err != nil {
|
if err := ValidateBufferSize(bufferSize); err != nil {
|
||||||
// Use default value on validation error
|
// Use default value on validation error
|
||||||
bufferSize = GetConfig().AudioFramePoolSize
|
bufferSize = Config.AudioFramePoolSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced preallocation strategy based on buffer size and system capacity
|
// Enhanced preallocation strategy based on buffer size and system capacity
|
||||||
var preallocSize int
|
var preallocSize int
|
||||||
if bufferSize <= GetConfig().AudioFramePoolSize {
|
if bufferSize <= Config.AudioFramePoolSize {
|
||||||
// For smaller pools, use enhanced preallocation (40% instead of 20%)
|
// For smaller pools, use enhanced preallocation (40% instead of 20%)
|
||||||
preallocSize = GetConfig().PreallocPercentage * 2
|
preallocSize = Config.PreallocPercentage * 2
|
||||||
} else {
|
} else {
|
||||||
// For larger pools, use standard enhanced preallocation (30% instead of 10%)
|
// For larger pools, use standard enhanced preallocation (30% instead of 10%)
|
||||||
preallocSize = (GetConfig().PreallocPercentage * 3) / 2
|
preallocSize = (Config.PreallocPercentage * 3) / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure minimum preallocation for better performance
|
// Ensure minimum preallocation for better performance
|
||||||
|
@ -594,9 +594,9 @@ func (p *AudioBufferPool) Put(buf []byte) {
|
||||||
// Enhanced global buffer pools for different audio frame types with improved sizing
|
// Enhanced global buffer pools for different audio frame types with improved sizing
|
||||||
var (
|
var (
|
||||||
// Main audio frame pool with enhanced capacity
|
// Main audio frame pool with enhanced capacity
|
||||||
audioFramePool = NewAudioBufferPool(GetConfig().AudioFramePoolSize)
|
audioFramePool = NewAudioBufferPool(Config.AudioFramePoolSize)
|
||||||
// Control message pool with enhanced capacity for better throughput
|
// Control message pool with enhanced capacity for better throughput
|
||||||
audioControlPool = NewAudioBufferPool(512) // Increased from GetConfig().OutputHeaderSize to 512 for better control message handling
|
audioControlPool = NewAudioBufferPool(512) // Increased from Config.OutputHeaderSize to 512 for better control message handling
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAudioFrameBuffer() []byte {
|
func GetAudioFrameBuffer() []byte {
|
||||||
|
@ -628,7 +628,7 @@ func (p *AudioBufferPool) GetPoolStats() AudioBufferPoolDetailedStats {
|
||||||
|
|
||||||
var hitRate float64
|
var hitRate float64
|
||||||
if totalRequests > 0 {
|
if totalRequests > 0 {
|
||||||
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
|
hitRate = float64(hitCount) / float64(totalRequests) * Config.PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
return AudioBufferPoolDetailedStats{
|
return AudioBufferPoolDetailedStats{
|
||||||
|
|
|
@ -21,12 +21,12 @@ func getEnvInt(key string, defaultValue int) int {
|
||||||
// with fallback to default config values
|
// with fallback to default config values
|
||||||
func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
||||||
// Read configuration from environment variables with config defaults
|
// Read configuration from environment variables with config defaults
|
||||||
bitrate = getEnvInt("JETKVM_OPUS_BITRATE", GetConfig().CGOOpusBitrate)
|
bitrate = getEnvInt("JETKVM_OPUS_BITRATE", Config.CGOOpusBitrate)
|
||||||
complexity = getEnvInt("JETKVM_OPUS_COMPLEXITY", GetConfig().CGOOpusComplexity)
|
complexity = getEnvInt("JETKVM_OPUS_COMPLEXITY", Config.CGOOpusComplexity)
|
||||||
vbr = getEnvInt("JETKVM_OPUS_VBR", GetConfig().CGOOpusVBR)
|
vbr = getEnvInt("JETKVM_OPUS_VBR", Config.CGOOpusVBR)
|
||||||
signalType = getEnvInt("JETKVM_OPUS_SIGNAL_TYPE", GetConfig().CGOOpusSignalType)
|
signalType = getEnvInt("JETKVM_OPUS_SIGNAL_TYPE", Config.CGOOpusSignalType)
|
||||||
bandwidth = getEnvInt("JETKVM_OPUS_BANDWIDTH", GetConfig().CGOOpusBandwidth)
|
bandwidth = getEnvInt("JETKVM_OPUS_BANDWIDTH", Config.CGOOpusBandwidth)
|
||||||
dtx = getEnvInt("JETKVM_OPUS_DTX", GetConfig().CGOOpusDTX)
|
dtx = getEnvInt("JETKVM_OPUS_DTX", Config.CGOOpusDTX)
|
||||||
|
|
||||||
return bitrate, complexity, vbr, signalType, bandwidth, dtx
|
return bitrate, complexity, vbr, signalType, bandwidth, dtx
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int
|
||||||
// applyOpusConfig applies OPUS configuration to the global config
|
// applyOpusConfig applies OPUS configuration to the global config
|
||||||
// with optional logging for the specified component
|
// with optional logging for the specified component
|
||||||
func applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int, component string, enableLogging bool) {
|
func applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int, component string, enableLogging bool) {
|
||||||
config := GetConfig()
|
config := Config
|
||||||
config.CGOOpusBitrate = bitrate
|
config.CGOOpusBitrate = bitrate
|
||||||
config.CGOOpusComplexity = complexity
|
config.CGOOpusComplexity = complexity
|
||||||
config.CGOOpusVBR = vbr
|
config.CGOOpusVBR = vbr
|
||||||
|
|
|
@ -134,7 +134,7 @@ func (r *AudioRelay) relayLoop() {
|
||||||
defer r.wg.Done()
|
defer r.wg.Done()
|
||||||
r.logger.Debug().Msg("Audio relay loop started")
|
r.logger.Debug().Msg("Audio relay loop started")
|
||||||
|
|
||||||
var maxConsecutiveErrors = GetConfig().MaxConsecutiveErrors
|
var maxConsecutiveErrors = Config.MaxConsecutiveErrors
|
||||||
consecutiveErrors := 0
|
consecutiveErrors := 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -153,7 +153,7 @@ func (r *AudioRelay) relayLoop() {
|
||||||
r.logger.Error().Int("consecutive_errors", consecutiveErrors).Int("max_errors", maxConsecutiveErrors).Msg("too many consecutive read errors, stopping audio relay")
|
r.logger.Error().Int("consecutive_errors", consecutiveErrors).Int("max_errors", maxConsecutiveErrors).Msg("too many consecutive read errors, stopping audio relay")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.Sleep(GetConfig().ShortSleepDuration)
|
time.Sleep(Config.ShortSleepDuration)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -224,7 +224,7 @@ func (aeb *AudioEventBroadcaster) sendToSubscriber(subscriber *AudioEventSubscri
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(subscriber.ctx, time.Duration(GetConfig().EventTimeoutSeconds)*time.Second)
|
ctx, cancel := context.WithTimeout(subscriber.ctx, time.Duration(Config.EventTimeoutSeconds)*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err := wsjson.Write(ctx, subscriber.conn, event)
|
err := wsjson.Write(ctx, subscriber.conn, event)
|
||||||
|
|
|
@ -98,8 +98,8 @@ type ZeroCopyFramePool struct {
|
||||||
// NewZeroCopyFramePool creates a new zero-copy frame pool
|
// NewZeroCopyFramePool creates a new zero-copy frame pool
|
||||||
func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
|
func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
|
||||||
// Pre-allocate frames for immediate availability
|
// Pre-allocate frames for immediate availability
|
||||||
preallocSizeBytes := GetConfig().PreallocSize
|
preallocSizeBytes := Config.PreallocSize
|
||||||
maxPoolSize := GetConfig().MaxPoolSize // Limit total pool size
|
maxPoolSize := Config.MaxPoolSize // Limit total pool size
|
||||||
|
|
||||||
// Calculate number of frames based on memory budget, not frame count
|
// Calculate number of frames based on memory budget, not frame count
|
||||||
preallocFrameCount := preallocSizeBytes / maxFrameSize
|
preallocFrameCount := preallocSizeBytes / maxFrameSize
|
||||||
|
@ -327,7 +327,7 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats {
|
||||||
|
|
||||||
var hitRate float64
|
var hitRate float64
|
||||||
if totalRequests > 0 {
|
if totalRequests > 0 {
|
||||||
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
|
hitRate = float64(hitCount) / float64(totalRequests) * Config.PercentageMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
return ZeroCopyFramePoolStats{
|
return ZeroCopyFramePoolStats{
|
||||||
|
|
5
main.go
5
main.go
|
@ -58,7 +58,7 @@ func startAudioSubprocess() error {
|
||||||
audio.SetAudioInputSupervisor(audioInputSupervisor)
|
audio.SetAudioInputSupervisor(audioInputSupervisor)
|
||||||
|
|
||||||
// Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106)
|
// Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106)
|
||||||
config := audio.GetConfig()
|
config := audio.Config
|
||||||
audioInputSupervisor.SetOpusConfig(
|
audioInputSupervisor.SetOpusConfig(
|
||||||
config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
|
config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
|
||||||
config.AudioQualityLowOpusComplexity,
|
config.AudioQualityLowOpusComplexity,
|
||||||
|
@ -81,7 +81,8 @@ func startAudioSubprocess() error {
|
||||||
// This prevents "no client connected" errors during quality changes
|
// This prevents "no client connected" errors during quality changes
|
||||||
go func() {
|
go func() {
|
||||||
// Give the audio output server time to initialize and start listening
|
// Give the audio output server time to initialize and start listening
|
||||||
time.Sleep(500 * time.Millisecond)
|
// Increased delay to reduce frame drops during connection establishment
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
// Start audio relay system for main process
|
// Start audio relay system for main process
|
||||||
// If there's an active WebRTC session, use its audio track
|
// If there's an active WebRTC session, use its audio track
|
||||||
|
|
Loading…
Reference in New Issue