refactor(audio): centralize configuration constants in audio module

Replace hardcoded values with centralized config constants for better maintainability and flexibility. This includes sleep durations, buffer sizes, thresholds, and various audio processing parameters.

The changes affect multiple components including buffer pools, latency monitoring, IPC, and audio processing. This refactoring makes it easier to adjust parameters without modifying individual files.

Key changes:
- Replace hardcoded sleep durations with config values
- Centralize buffer sizes and pool configurations
- Move thresholds and limits to config
- Update audio quality presets to use config values
This commit is contained in:
Alex P 2025-08-25 18:08:12 +00:00
parent 7ec583ed6a
commit 35a666ed31
24 changed files with 1372 additions and 245 deletions

View File

@ -42,20 +42,20 @@ func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig {
DefaultBufferSize: 6, // Default 6 frames (increased for better stability)
// CPU thresholds optimized for single-core ARM Cortex A7 under load
LowCPUThreshold: 20.0, // Below 20% CPU
HighCPUThreshold: 60.0, // Above 60% CPU (lowered to be more responsive)
LowCPUThreshold: GetConfig().LowCPUThreshold * 100, // Below 20% CPU
HighCPUThreshold: GetConfig().HighCPUThreshold * 100, // Above 60% CPU (lowered to be more responsive)
// Memory thresholds for 256MB total RAM
LowMemoryThreshold: 35.0, // Below 35% memory usage
HighMemoryThreshold: 75.0, // Above 75% memory usage (lowered for earlier response)
LowMemoryThreshold: GetConfig().LowMemoryThreshold * 100, // Below 35% memory usage
HighMemoryThreshold: GetConfig().HighMemoryThreshold * 100, // Above 75% memory usage (lowered for earlier response)
// Latency targets
TargetLatency: 20 * time.Millisecond, // Target 20ms latency
MaxLatency: 50 * time.Millisecond, // Max acceptable 50ms
TargetLatency: GetConfig().TargetLatency, // Target 20ms latency
MaxLatency: GetConfig().MaxLatencyTarget, // Max acceptable latency
// Adaptation settings
AdaptationInterval: 500 * time.Millisecond, // Check every 500ms
SmoothingFactor: 0.3, // Moderate responsiveness
AdaptationInterval: GetConfig().BufferUpdateInterval, // Check every 500ms
SmoothingFactor: GetConfig().SmoothingFactor, // Moderate responsiveness
}
}
@ -133,7 +133,7 @@ func (abm *AdaptiveBufferManager) UpdateLatency(latency time.Duration) {
atomic.StoreInt64(&abm.averageLatency, newLatency)
} else {
// Exponential moving average: 70% historical, 30% current
newAvg := int64(float64(currentAvg)*0.7 + float64(newLatency)*0.3)
newAvg := int64(float64(currentAvg)*GetConfig().HistoricalWeight + float64(newLatency)*GetConfig().CurrentWeight)
atomic.StoreInt64(&abm.averageLatency, newAvg)
}
}
@ -195,7 +195,7 @@ func (abm *AdaptiveBufferManager) adaptBufferSizes() {
latencyFactor := abm.calculateLatencyFactor(currentLatency)
// Combine factors with weights (CPU has highest priority for KVM coexistence)
combinedFactor := 0.5*cpuFactor + 0.3*memoryFactor + 0.2*latencyFactor
combinedFactor := GetConfig().CPUMemoryWeight*cpuFactor + GetConfig().MemoryWeight*memoryFactor + GetConfig().LatencyWeight*latencyFactor
// Apply adaptation with smoothing
currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize))
@ -306,8 +306,8 @@ func (abm *AdaptiveBufferManager) GetStats() map[string]interface{} {
"input_buffer_size": abm.GetInputBufferSize(),
"output_buffer_size": abm.GetOutputBufferSize(),
"average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6,
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / 100,
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / 100,
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / GetConfig().PercentageMultiplier,
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / GetConfig().PercentageMultiplier,
"adaptation_count": atomic.LoadInt64(&abm.adaptationCount),
"last_adaptation": lastAdaptation,
}

View File

@ -42,9 +42,9 @@ type OptimizerConfig struct {
func DefaultOptimizerConfig() OptimizerConfig {
return OptimizerConfig{
MaxOptimizationLevel: 8,
CooldownPeriod: 30 * time.Second,
Aggressiveness: 0.7,
RollbackThreshold: 300 * time.Millisecond,
CooldownPeriod: GetConfig().CooldownPeriod,
Aggressiveness: GetConfig().OptimizerAggressiveness,
RollbackThreshold: GetConfig().RollbackThreshold,
StabilityPeriod: 10 * time.Second,
}
}
@ -109,7 +109,7 @@ func (ao *AdaptiveOptimizer) handleLatencyOptimization(metrics LatencyMetrics) e
// calculateTargetOptimizationLevel determines the appropriate optimization level
func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMetrics) int64 {
// Base calculation on current latency vs target
latencyRatio := float64(metrics.Current) / float64(50*time.Millisecond) // 50ms target
latencyRatio := float64(metrics.Current) / float64(GetConfig().LatencyTarget) // 50ms target
// Adjust based on trend
switch metrics.Trend {
@ -125,7 +125,7 @@ func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMet
latencyRatio *= ao.config.Aggressiveness
// Convert to optimization level
targetLevel := int64(latencyRatio * 2) // Scale to 0-10 range
targetLevel := int64(latencyRatio * GetConfig().LatencyScalingFactor) // Scale to 0-10 range
if targetLevel > int64(ao.config.MaxOptimizationLevel) {
targetLevel = int64(ao.config.MaxOptimizationLevel)
}

View File

@ -47,17 +47,17 @@ type AudioMetrics struct {
var (
currentConfig = AudioConfig{
Quality: AudioQualityMedium,
Bitrate: 64,
Bitrate: GetConfig().AudioQualityMediumOutputBitrate,
SampleRate: GetConfig().SampleRate,
Channels: GetConfig().Channels,
FrameSize: 20 * time.Millisecond,
FrameSize: GetConfig().AudioQualityMediumFrameSize,
}
currentMicrophoneConfig = AudioConfig{
Quality: AudioQualityMedium,
Bitrate: 32,
Bitrate: GetConfig().AudioQualityMediumInputBitrate,
SampleRate: GetConfig().SampleRate,
Channels: 1,
FrameSize: 20 * time.Millisecond,
FrameSize: GetConfig().AudioQualityMediumFrameSize,
}
metrics AudioMetrics
)
@ -69,24 +69,24 @@ var qualityPresets = map[AudioQuality]struct {
frameSize time.Duration
}{
AudioQualityLow: {
outputBitrate: 32, inputBitrate: 16,
sampleRate: 22050, channels: 1,
frameSize: 40 * time.Millisecond,
outputBitrate: GetConfig().AudioQualityLowOutputBitrate, inputBitrate: GetConfig().AudioQualityLowInputBitrate,
sampleRate: GetConfig().AudioQualityLowSampleRate, channels: GetConfig().AudioQualityLowChannels,
frameSize: GetConfig().AudioQualityLowFrameSize,
},
AudioQualityMedium: {
outputBitrate: 64, inputBitrate: 32,
sampleRate: 44100, channels: 2,
frameSize: 20 * time.Millisecond,
outputBitrate: GetConfig().AudioQualityMediumOutputBitrate, inputBitrate: GetConfig().AudioQualityMediumInputBitrate,
sampleRate: GetConfig().AudioQualityMediumSampleRate, channels: GetConfig().AudioQualityMediumChannels,
frameSize: GetConfig().AudioQualityMediumFrameSize,
},
AudioQualityHigh: {
outputBitrate: 128, inputBitrate: 64,
sampleRate: GetConfig().SampleRate, channels: GetConfig().Channels,
frameSize: 20 * time.Millisecond,
outputBitrate: GetConfig().AudioQualityHighOutputBitrate, inputBitrate: GetConfig().AudioQualityHighInputBitrate,
sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityHighChannels,
frameSize: GetConfig().AudioQualityHighFrameSize,
},
AudioQualityUltra: {
outputBitrate: 192, inputBitrate: 96,
sampleRate: GetConfig().SampleRate, channels: GetConfig().Channels,
frameSize: 10 * time.Millisecond,
outputBitrate: GetConfig().AudioQualityUltraOutputBitrate, inputBitrate: GetConfig().AudioQualityUltraInputBitrate,
sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityUltraChannels,
frameSize: GetConfig().AudioQualityUltraFrameSize,
},
}
@ -114,7 +114,7 @@ func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
Bitrate: preset.inputBitrate,
SampleRate: func() int {
if quality == AudioQualityLow {
return 16000
return GetConfig().AudioQualityMicLowSampleRate
}
return preset.sampleRate
}(),

View File

@ -72,7 +72,7 @@ func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAu
readQueue: make(chan batchReadRequest, batchSize*2),
readBufPool: &sync.Pool{
New: func() interface{} {
return make([]byte, 1500) // Max audio frame size
return make([]byte, GetConfig().AudioFramePoolSize) // Max audio frame size
},
},
}
@ -110,7 +110,7 @@ func (bap *BatchAudioProcessor) Stop() {
bap.cancel()
// Wait for processing to complete
time.Sleep(bap.batchDuration + 10*time.Millisecond)
time.Sleep(bap.batchDuration + GetConfig().BatchProcessingDelay)
bap.logger.Info().Msg("batch audio processor stopped")
}
@ -134,7 +134,7 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
select {
case bap.readQueue <- request:
// Successfully queued
case <-time.After(5 * time.Millisecond):
case <-time.After(GetConfig().ShortTimeout):
// Queue is full or blocked, fallback to single operation
atomic.AddInt64(&bap.stats.SingleReads, 1)
atomic.AddInt64(&bap.stats.SingleFrames, 1)
@ -145,7 +145,7 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
select {
case result := <-resultChan:
return result.length, result.err
case <-time.After(50 * time.Millisecond):
case <-time.After(GetConfig().MediumTimeout):
// Timeout, fallback to single operation
atomic.AddInt64(&bap.stats.SingleReads, 1)
atomic.AddInt64(&bap.stats.SingleFrames, 1)

View File

@ -23,7 +23,7 @@ type AudioBufferPool struct {
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
// Pre-allocate 20% of max pool size for immediate availability
preallocSize := 20
preallocSize := GetConfig().PreallocPercentage
preallocated := make([]*[]byte, 0, preallocSize)
// Pre-allocate buffers to reduce initial allocation overhead
@ -34,7 +34,7 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
return &AudioBufferPool{
bufferSize: bufferSize,
maxPoolSize: 100, // Limit pool size to prevent excessive memory usage
maxPoolSize: GetConfig().MaxPoolSize, // Limit pool size to prevent excessive memory usage
preallocated: preallocated,
preallocSize: preallocSize,
pool: sync.Pool{
@ -111,8 +111,8 @@ func (p *AudioBufferPool) Put(buf []byte) {
}
var (
audioFramePool = NewAudioBufferPool(1500)
audioControlPool = NewAudioBufferPool(64)
audioFramePool = NewAudioBufferPool(GetConfig().AudioFramePoolSize)
audioControlPool = NewAudioBufferPool(GetConfig().OutputHeaderSize)
)
func GetAudioFrameBuffer() []byte {
@ -144,7 +144,7 @@ func (p *AudioBufferPool) GetPoolStats() AudioBufferPoolDetailedStats {
var hitRate float64
if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * 100
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
}
return AudioBufferPoolDetailedStats{

View File

@ -22,18 +22,37 @@ static snd_pcm_t *pcm_handle = NULL;
static snd_pcm_t *pcm_playback_handle = NULL;
static OpusEncoder *encoder = NULL;
static OpusDecoder *decoder = NULL;
// Optimized Opus encoder settings for ARM Cortex-A7
static int opus_bitrate = 96000; // Increased for better quality
static int opus_complexity = 3; // Reduced for ARM performance
static int opus_vbr = 1; // Variable bitrate enabled
static int opus_vbr_constraint = 1; // Constrained VBR for consistent latency
static int opus_signal_type = OPUS_SIGNAL_MUSIC; // Optimized for general audio
static int opus_bandwidth = OPUS_BANDWIDTH_FULLBAND; // Full bandwidth
static int opus_dtx = 0; // Disable DTX for real-time audio
static int sample_rate = 48000;
static int channels = 2;
static int frame_size = 960; // 20ms for 48kHz
static int max_packet_size = 1500;
// Opus encoder settings - initialized from Go configuration
static int opus_bitrate = 96000; // Will be set from GetConfig().CGOOpusBitrate
static int opus_complexity = 3; // Will be set from GetConfig().CGOOpusComplexity
static int opus_vbr = 1; // Will be set from GetConfig().CGOOpusVBR
static int opus_vbr_constraint = 1; // Will be set from GetConfig().CGOOpusVBRConstraint
static int opus_signal_type = 3; // Will be set from GetConfig().CGOOpusSignalType
static int opus_bandwidth = 1105; // Will be set from GetConfig().CGOOpusBandwidth
static int opus_dtx = 0; // Will be set from GetConfig().CGOOpusDTX
static int sample_rate = 48000; // Will be set from GetConfig().CGOSampleRate
static int channels = 2; // Will be set from GetConfig().CGOChannels
static int frame_size = 960; // Will be set from GetConfig().CGOFrameSize
static int max_packet_size = 1500; // Will be set from GetConfig().CGOMaxPacketSize
static int sleep_microseconds = 50000; // Will be set from GetConfig().CGOSleepMicroseconds
// Function to update constants from Go configuration
void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint,
int signal_type, int bandwidth, int dtx, int sr, int ch,
int fs, int max_pkt, int sleep_us) {
opus_bitrate = bitrate;
opus_complexity = complexity;
opus_vbr = vbr;
opus_vbr_constraint = vbr_constraint;
opus_signal_type = signal_type;
opus_bandwidth = bandwidth;
opus_dtx = dtx;
sample_rate = sr;
channels = ch;
frame_size = fs;
max_packet_size = max_pkt;
sleep_microseconds = sleep_us;
}
// State tracking to prevent race conditions during rapid start/stop
static volatile int capture_initializing = 0;
@ -56,7 +75,7 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream
if (err == -EBUSY && attempts > 0) {
// Device busy, wait and retry
usleep(50000); // 50ms
usleep(sleep_microseconds); // 50ms
continue;
}
break;
@ -415,8 +434,25 @@ var (
)
func cgoAudioInit() error {
ret := C.jetkvm_audio_init()
if ret != 0 {
// Update C constants from Go configuration
config := GetConfig()
C.update_audio_constants(
C.int(config.CGOOpusBitrate),
C.int(config.CGOOpusComplexity),
C.int(config.CGOOpusVBR),
C.int(config.CGOOpusVBRConstraint),
C.int(config.CGOOpusSignalType),
C.int(config.CGOOpusBandwidth),
C.int(config.CGOOpusDTX),
C.int(config.CGOSampleRate),
C.int(config.CGOChannels),
C.int(config.CGOFrameSize),
C.int(config.CGOMaxPacketSize),
C.int(config.CGOSleepMicroseconds),
)
result := C.jetkvm_audio_init()
if result != 0 {
return errAudioInitFailed
}
return nil
@ -427,7 +463,7 @@ func cgoAudioClose() {
}
func cgoAudioReadEncode(buf []byte) (int, error) {
if len(buf) < 1276 {
if len(buf) < GetConfig().MinReadEncodeBuffer {
return 0, errBufferTooSmall
}
@ -461,7 +497,7 @@ func cgoAudioDecodeWrite(buf []byte) (int, error) {
if buf == nil {
return 0, errNilBuffer
}
if len(buf) > 4096 {
if len(buf) > GetConfig().MaxDecodeWriteBuffer {
return 0, errBufferTooLarge
}

File diff suppressed because it is too large Load Diff

View File

@ -16,14 +16,19 @@ import (
"github.com/jetkvm/kvm/internal/logging"
)
const (
inputMagicNumber uint32 = 0x4A4B4D49 // "JKMI" (JetKVM Microphone Input)
var (
inputMagicNumber uint32 = GetConfig().InputMagicNumber // "JKMI" (JetKVM Microphone Input)
inputSocketName = "audio_input.sock"
maxFrameSize = 4096 // Maximum Opus frame size
writeTimeout = 15 * time.Millisecond // Non-blocking write timeout (increased for high load)
maxDroppedFrames = 100 // Maximum consecutive dropped frames before reconnect
)
const (
headerSize = 17 // Fixed header size: 4+1+4+8 bytes
messagePoolSize = 256 // Pre-allocated message pool size
)
var (
maxFrameSize = GetConfig().MaxFrameSize // Maximum Opus frame size
messagePoolSize = GetConfig().MessagePoolSize // Pre-allocated message pool size
)
// InputMessageType represents the type of IPC message
@ -79,9 +84,9 @@ var messagePoolInitOnce sync.Once
func initializeMessagePool() {
messagePoolInitOnce.Do(func() {
// Pre-allocate 30% of pool size for immediate availability
preallocSize := messagePoolSize * 30 / 100
preallocSize := messagePoolSize * GetConfig().InputPreallocPercentage / 100
globalMessagePool.preallocSize = preallocSize
globalMessagePool.maxPoolSize = messagePoolSize * 2 // Allow growth up to 2x
globalMessagePool.maxPoolSize = messagePoolSize * GetConfig().PoolGrowthMultiplier // Allow growth up to 2x
globalMessagePool.preallocated = make([]*OptimizedIPCMessage, 0, preallocSize)
// Pre-allocate messages to reduce initial allocation overhead
@ -315,7 +320,7 @@ func (ais *AudioInputServer) handleConnection(conn net.Conn) {
if ais.conn == nil {
return
}
time.Sleep(100 * time.Millisecond)
time.Sleep(GetConfig().DefaultSleepDuration)
}
}
}
@ -345,7 +350,7 @@ func (ais *AudioInputServer) readMessage(conn net.Conn) (*InputIPCMessage, error
}
// Validate message length
if msg.Length > maxFrameSize {
if msg.Length > uint32(maxFrameSize) {
return nil, fmt.Errorf("message too large: %d bytes", msg.Length)
}
@ -711,7 +716,7 @@ func (aic *AudioInputClient) GetDropRate() float64 {
if total == 0 {
return 0.0
}
return float64(dropped) / float64(total) * 100.0
return float64(dropped) / float64(total) * GetConfig().PercentageMultiplier
}
// ResetStats resets frame statistics
@ -820,11 +825,11 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
}()
defer ais.wg.Done()
ticker := time.NewTicker(100 * time.Millisecond)
ticker := time.NewTicker(GetConfig().DefaultTickerInterval)
defer ticker.Stop()
// Buffer size update ticker (less frequent)
bufferUpdateTicker := time.NewTicker(500 * time.Millisecond)
bufferUpdateTicker := time.NewTicker(GetConfig().BufferUpdateInterval)
defer bufferUpdateTicker.Stop()
for {
@ -917,7 +922,7 @@ func (mp *MessagePool) GetMessagePoolStats() MessagePoolStats {
var hitRate float64
if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * 100
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
}
// Calculate channel pool size

View File

@ -41,13 +41,13 @@ func (aim *AudioInputIPCManager) Start() error {
}
config := InputIPCConfig{
SampleRate: 48000,
Channels: 2,
FrameSize: 960,
SampleRate: GetConfig().InputIPCSampleRate,
Channels: GetConfig().InputIPCChannels,
FrameSize: GetConfig().InputIPCFrameSize,
}
// Wait for subprocess readiness
time.Sleep(200 * time.Millisecond)
time.Sleep(GetConfig().LongSleepDuration)
err = aim.supervisor.SendConfig(config)
if err != nil {

View File

@ -64,7 +64,7 @@ func RunAudioInputServer() error {
server.Stop()
// Give some time for cleanup
time.Sleep(100 * time.Millisecond)
time.Sleep(GetConfig().DefaultSleepDuration)
logger.Info().Msg("Audio input server subprocess stopped")
return nil

View File

@ -128,7 +128,7 @@ func (ais *AudioInputSupervisor) Stop() {
select {
case <-done:
ais.logger.Info().Msg("Audio input server subprocess stopped gracefully")
case <-time.After(5 * time.Second):
case <-time.After(GetConfig().InputSupervisorTimeout):
// Force kill if graceful shutdown failed
ais.logger.Warn().Msg("Audio input server subprocess did not stop gracefully, force killing")
err := ais.cmd.Process.Kill()
@ -220,7 +220,7 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
// connectClient attempts to connect the client to the server
func (ais *AudioInputSupervisor) connectClient() {
// Wait briefly for the server to start (reduced from 500ms)
time.Sleep(100 * time.Millisecond)
time.Sleep(GetConfig().DefaultSleepDuration)
err := ais.client.Connect()
if err != nil {

View File

@ -16,16 +16,14 @@ import (
"github.com/rs/zerolog"
)
const (
outputMagicNumber uint32 = 0x4A4B4F55 // "JKOU" (JetKVM Output)
var (
outputMagicNumber uint32 = GetConfig().OutputMagicNumber // "JKOU" (JetKVM Output)
outputSocketName = "audio_output.sock"
outputMaxFrameSize = 4096 // Maximum Opus frame size
outputWriteTimeout = 10 * time.Millisecond // Non-blocking write timeout (increased for high load)
outputMaxDroppedFrames = 50 // Maximum consecutive dropped frames
outputHeaderSize = 17 // Fixed header size: 4+1+4+8 bytes
outputMessagePoolSize = 128 // Pre-allocated message pool size
)
// Output IPC constants are now centralized in config_constants.go
// outputMaxFrameSize, outputWriteTimeout, outputMaxDroppedFrames, outputHeaderSize, outputMessagePoolSize
// OutputMessageType represents the type of IPC message
type OutputMessageType uint8
@ -48,7 +46,7 @@ type OutputIPCMessage struct {
// OutputOptimizedMessage represents a pre-allocated message for zero-allocation operations
type OutputOptimizedMessage struct {
header [outputHeaderSize]byte // Pre-allocated header buffer
header [17]byte // Pre-allocated header buffer (using constant value since array size must be compile-time constant)
data []byte // Reusable data buffer
}
@ -66,7 +64,7 @@ func NewOutputMessagePool(size int) *OutputMessagePool {
// Pre-allocate messages
for i := 0; i < size; i++ {
msg := &OutputOptimizedMessage{
data: make([]byte, outputMaxFrameSize),
data: make([]byte, GetConfig().OutputMaxFrameSize),
}
pool.pool <- msg
}
@ -82,7 +80,7 @@ func (p *OutputMessagePool) Get() *OutputOptimizedMessage {
default:
// Pool exhausted, create new message
return &OutputOptimizedMessage{
data: make([]byte, outputMaxFrameSize),
data: make([]byte, GetConfig().OutputMaxFrameSize),
}
}
}
@ -98,7 +96,7 @@ func (p *OutputMessagePool) Put(msg *OutputOptimizedMessage) {
}
// Global message pool for output IPC
var globalOutputMessagePool = NewOutputMessagePool(outputMessagePoolSize)
var globalOutputMessagePool = NewOutputMessagePool(GetConfig().OutputMessagePoolSize)
type AudioServer struct {
// Atomic fields must be first for proper alignment on ARM
@ -135,7 +133,7 @@ func NewAudioServer() (*AudioServer, error) {
}
// Initialize with adaptive buffer size (start with 500 frames)
initialBufferSize := int64(500)
initialBufferSize := int64(GetConfig().InitialBufferFrames)
// Initialize latency monitoring
latencyConfig := DefaultLatencyConfig()
@ -284,8 +282,8 @@ func (s *AudioServer) Close() error {
}
func (s *AudioServer) SendFrame(frame []byte) error {
if len(frame) > outputMaxFrameSize {
return fmt.Errorf("frame size %d exceeds maximum %d", len(frame), outputMaxFrameSize)
if len(frame) > GetConfig().OutputMaxFrameSize {
return fmt.Errorf("frame size %d exceeds maximum %d", len(frame), GetConfig().OutputMaxFrameSize)
}
start := time.Now()
@ -340,7 +338,7 @@ func (s *AudioServer) sendFrameToClient(frame []byte) error {
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(start.UnixNano()))
// Use non-blocking write with timeout
ctx, cancel := context.WithTimeout(context.Background(), outputWriteTimeout)
ctx, cancel := context.WithTimeout(context.Background(), GetConfig().OutputWriteTimeout)
defer cancel()
// Create a channel to signal write completion
@ -492,8 +490,8 @@ func (c *AudioClient) ReceiveFrame() ([]byte, error) {
}
size := binary.LittleEndian.Uint32(optMsg.header[5:9])
if size > outputMaxFrameSize {
return nil, fmt.Errorf("frame size %d exceeds maximum %d", size, outputMaxFrameSize)
if int(size) > GetConfig().OutputMaxFrameSize {
return nil, fmt.Errorf("frame size %d exceeds maximum %d", size, GetConfig().OutputMaxFrameSize)
}
// Read frame data

View File

@ -83,10 +83,10 @@ const (
func DefaultLatencyConfig() LatencyConfig {
return LatencyConfig{
TargetLatency: 50 * time.Millisecond,
MaxLatency: 200 * time.Millisecond,
MaxLatency: GetConfig().MaxLatencyThreshold,
OptimizationInterval: 5 * time.Second,
HistorySize: 100,
JitterThreshold: 20 * time.Millisecond,
HistorySize: GetConfig().LatencyHistorySize,
JitterThreshold: GetConfig().JitterThreshold,
AdaptiveThreshold: 0.8, // Trigger optimization when 80% above target
}
}

View File

@ -171,8 +171,8 @@ func LogMemoryMetrics() {
metrics := CollectMemoryMetrics()
logger.Info().
Uint64("heap_alloc_mb", metrics.RuntimeStats.HeapAlloc/1024/1024).
Uint64("heap_sys_mb", metrics.RuntimeStats.HeapSys/1024/1024).
Uint64("heap_alloc_mb", metrics.RuntimeStats.HeapAlloc/uint64(GetConfig().BytesToMBDivisor)).
Uint64("heap_sys_mb", metrics.RuntimeStats.HeapSys/uint64(GetConfig().BytesToMBDivisor)).
Uint64("heap_objects", metrics.RuntimeStats.HeapObjects).
Uint32("num_gc", metrics.RuntimeStats.NumGC).
Float64("gc_cpu_fraction", metrics.RuntimeStats.GCCPUFraction).

View File

@ -451,7 +451,7 @@ func GetLastMetricsUpdate() time.Time {
// StartMetricsUpdater starts a goroutine that periodically updates Prometheus metrics
func StartMetricsUpdater() {
go func() {
ticker := time.NewTicker(5 * time.Second) // Update every 5 seconds
ticker := time.NewTicker(GetConfig().StatsUpdateInterval) // Update every 5 seconds
defer ticker.Stop()
for range ticker.C {

View File

@ -105,7 +105,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
}
if atomic.CompareAndSwapInt32(&micContentionInitialized, 0, 1) {
manager := NewMicrophoneContentionManager(200 * time.Millisecond)
manager := NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
atomic.StorePointer(&globalMicContentionManager, unsafe.Pointer(manager))
return manager
}
@ -115,7 +115,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
return (*MicrophoneContentionManager)(ptr)
}
return NewMicrophoneContentionManager(200 * time.Millisecond)
return NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
}
func TryMicrophoneOperation() OperationResult {

View File

@ -64,7 +64,7 @@ func RunAudioOutputServer() error {
StopNonBlockingAudioStreaming()
// Give some time for cleanup
time.Sleep(100 * time.Millisecond)
time.Sleep(GetConfig().DefaultSleepDuration)
logger.Info().Msg("Audio output server subprocess stopped")
return nil

View File

@ -62,7 +62,7 @@ func NewOutputStreamer() (*OutputStreamer, error) {
ctx: ctx,
cancel: cancel,
batchSize: initialBatchSize, // Use adaptive batch size
processingChan: make(chan []byte, 500), // Large buffer for smooth processing
processingChan: make(chan []byte, GetConfig().ChannelBufferSize), // Large buffer for smooth processing
statsInterval: 5 * time.Second, // Statistics every 5 seconds
lastStatsTime: time.Now().UnixNano(),
}, nil
@ -127,7 +127,7 @@ func (s *OutputStreamer) streamLoop() {
defer ticker.Stop()
// Batch size update ticker
batchUpdateTicker := time.NewTicker(500 * time.Millisecond)
batchUpdateTicker := time.NewTicker(GetConfig().BufferUpdateInterval)
defer batchUpdateTicker.Stop()
for {
@ -233,7 +233,7 @@ func (s *OutputStreamer) reportStatistics() {
processingTime := atomic.LoadInt64(&s.processingTime)
if processed > 0 {
dropRate := float64(dropped) / float64(processed+dropped) * 100
dropRate := float64(dropped) / float64(processed+dropped) * GetConfig().PercentageMultiplier
avgProcessingTime := time.Duration(processingTime)
getOutputStreamingLogger().Info().Int64("processed", processed).Int64("dropped", dropped).Float64("drop_rate", dropRate).Dur("avg_processing", avgProcessingTime).Msg("Output Audio Stats")
@ -270,7 +270,7 @@ func (s *OutputStreamer) GetDetailedStats() map[string]interface{} {
}
if processed+dropped > 0 {
stats["drop_rate_percent"] = float64(dropped) / float64(processed+dropped) * 100
stats["drop_rate_percent"] = float64(dropped) / float64(processed+dropped) * GetConfig().PercentageMultiplier
}
// Add client statistics
@ -343,7 +343,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
RecordFrameReceived(n)
}
// Small delay to prevent busy waiting
time.Sleep(10 * time.Millisecond)
time.Sleep(GetConfig().ShortSleepDuration)
}
}
}()
@ -364,6 +364,6 @@ func StopAudioOutputStreaming() {
// Wait for streaming to stop
for atomic.LoadInt32(&outputStreamingRunning) == 1 {
time.Sleep(10 * time.Millisecond)
time.Sleep(GetConfig().ShortSleepDuration)
}
}

View File

@ -16,23 +16,17 @@ type SchedParam struct {
Priority int32
}
// Priority levels for audio processing
const (
// SCHED_FIFO priorities (1-99, higher = more priority)
AudioHighPriority = 80 // High priority for critical audio processing
AudioMediumPriority = 60 // Medium priority for regular audio processing
AudioLowPriority = 40 // Low priority for background audio tasks
// getPriorityConstants returns priority levels from centralized config
func getPriorityConstants() (audioHigh, audioMedium, audioLow, normal int) {
config := GetConfig()
return config.AudioHighPriority, config.AudioMediumPriority, config.AudioLowPriority, config.NormalPriority
}
// SCHED_NORMAL is the default (priority 0)
NormalPriority = 0
)
// Scheduling policies
const (
SCHED_NORMAL = 0
SCHED_FIFO = 1
SCHED_RR = 2
)
// getSchedulingPolicies returns scheduling policies from centralized config
func getSchedulingPolicies() (schedNormal, schedFIFO, schedRR int) {
config := GetConfig()
return config.SchedNormal, config.SchedFIFO, config.SchedRR
}
// PriorityScheduler manages thread priorities for audio processing
type PriorityScheduler struct {
@ -73,7 +67,8 @@ func (ps *PriorityScheduler) SetThreadPriority(priority int, policy int) error {
if errno != 0 {
// If we can't set real-time priority, try nice value instead
if policy != SCHED_NORMAL {
schedNormal, _, _ := getSchedulingPolicies()
if policy != schedNormal {
ps.logger.Warn().Int("errno", int(errno)).Msg("Failed to set real-time priority, falling back to nice")
return ps.setNicePriority(priority)
}
@ -89,11 +84,11 @@ func (ps *PriorityScheduler) setNicePriority(rtPriority int) error {
// Convert real-time priority to nice value (inverse relationship)
// RT priority 80 -> nice -10, RT priority 40 -> nice 0
niceValue := (40 - rtPriority) / 4
if niceValue < -20 {
niceValue = -20
if niceValue < GetConfig().MinNiceValue {
niceValue = GetConfig().MinNiceValue
}
if niceValue > 19 {
niceValue = 19
if niceValue > GetConfig().MaxNiceValue {
niceValue = GetConfig().MaxNiceValue
}
err := syscall.Setpriority(syscall.PRIO_PROCESS, 0, niceValue)
@ -108,22 +103,30 @@ func (ps *PriorityScheduler) setNicePriority(rtPriority int) error {
// SetAudioProcessingPriority sets high priority for audio processing threads
func (ps *PriorityScheduler) SetAudioProcessingPriority() error {
return ps.SetThreadPriority(AudioHighPriority, SCHED_FIFO)
audioHigh, _, _, _ := getPriorityConstants()
_, schedFIFO, _ := getSchedulingPolicies()
return ps.SetThreadPriority(audioHigh, schedFIFO)
}
// SetAudioIOPriority sets medium priority for audio I/O threads
func (ps *PriorityScheduler) SetAudioIOPriority() error {
return ps.SetThreadPriority(AudioMediumPriority, SCHED_FIFO)
_, audioMedium, _, _ := getPriorityConstants()
_, schedFIFO, _ := getSchedulingPolicies()
return ps.SetThreadPriority(audioMedium, schedFIFO)
}
// SetAudioBackgroundPriority sets low priority for background audio tasks
func (ps *PriorityScheduler) SetAudioBackgroundPriority() error {
return ps.SetThreadPriority(AudioLowPriority, SCHED_FIFO)
_, _, audioLow, _ := getPriorityConstants()
_, schedFIFO, _ := getSchedulingPolicies()
return ps.SetThreadPriority(audioLow, schedFIFO)
}
// ResetPriority resets thread to normal scheduling
func (ps *PriorityScheduler) ResetPriority() error {
return ps.SetThreadPriority(NormalPriority, SCHED_NORMAL)
_, _, _, normal := getPriorityConstants()
schedNormal, _, _ := getSchedulingPolicies()
return ps.SetThreadPriority(normal, schedNormal)
}
// Disable disables priority scheduling (useful for testing or fallback)

View File

@ -13,26 +13,29 @@ import (
"github.com/rs/zerolog"
)
// Constants for process monitoring
const (
// Variables for process monitoring (using configuration)
var (
// System constants
pageSize = 4096
maxCPUPercent = 100.0
minCPUPercent = 0.01
defaultClockTicks = 250.0 // Common for embedded ARM systems
defaultMemoryGB = 8
maxCPUPercent = GetConfig().MaxCPUPercent
minCPUPercent = GetConfig().MinCPUPercent
defaultClockTicks = GetConfig().DefaultClockTicks
defaultMemoryGB = GetConfig().DefaultMemoryGB
// Monitoring thresholds
maxWarmupSamples = 3
warmupCPUSamples = 2
logThrottleInterval = 10
maxWarmupSamples = GetConfig().MaxWarmupSamples
warmupCPUSamples = GetConfig().WarmupCPUSamples
// Channel buffer size
metricsChannelBuffer = 100
metricsChannelBuffer = GetConfig().MetricsChannelBuffer
// Clock tick detection ranges
minValidClockTicks = 50
maxValidClockTicks = 1000
minValidClockTicks = float64(GetConfig().MinValidClockTicks)
maxValidClockTicks = float64(GetConfig().MaxValidClockTicks)
)
// Variables for process monitoring
var (
pageSize = GetConfig().PageSize
)
// ProcessMetrics represents CPU and memory usage metrics for a process
@ -217,7 +220,7 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
vsize, _ := strconv.ParseInt(fields[22], 10, 64)
rss, _ := strconv.ParseInt(fields[23], 10, 64)
metric.MemoryRSS = rss * pageSize
metric.MemoryRSS = rss * int64(pageSize)
metric.MemoryVMS = vsize
// Calculate CPU percentage
@ -230,7 +233,7 @@ func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessM
// Calculate memory percentage (RSS / total system memory)
if totalMem := pm.getTotalMemory(); totalMem > 0 {
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * 100.0
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * GetConfig().PercentageMultiplier
}
// Update state for next calculation
@ -261,7 +264,7 @@ func (pm *ProcessMonitor) calculateCPUPercent(totalCPUTime int64, state *process
// Convert from clock ticks to seconds using actual system clock ticks
clockTicks := pm.getClockTicks()
cpuSeconds := cpuDelta / clockTicks
cpuPercent := (cpuSeconds / timeDelta) * 100.0
cpuPercent := (cpuSeconds / timeDelta) * GetConfig().PercentageMultiplier
// Apply bounds
if cpuPercent > maxCPUPercent {
@ -341,7 +344,7 @@ func (pm *ProcessMonitor) getTotalMemory() int64 {
pm.memoryOnce.Do(func() {
file, err := os.Open("/proc/meminfo")
if err != nil {
pm.totalMemory = defaultMemoryGB * 1024 * 1024 * 1024
pm.totalMemory = int64(defaultMemoryGB) * 1024 * 1024 * 1024
return
}
defer file.Close()
@ -360,7 +363,7 @@ func (pm *ProcessMonitor) getTotalMemory() int64 {
break
}
}
pm.totalMemory = defaultMemoryGB * 1024 * 1024 * 1024 // Fallback
pm.totalMemory = int64(defaultMemoryGB) * 1024 * 1024 * 1024 // Fallback
})
return pm.totalMemory
}

View File

@ -151,7 +151,7 @@ func (r *AudioRelay) relayLoop() {
r.logger.Error().Msg("Too many consecutive errors, stopping relay")
return
}
time.Sleep(10 * time.Millisecond)
time.Sleep(GetConfig().ShortSleepDuration)
continue
}

View File

@ -6,12 +6,7 @@ import (
"syscall"
)
const (
// Socket buffer sizes optimized for JetKVM's audio workload
OptimalSocketBuffer = 128 * 1024 // 128KB (32 frames @ 4KB each)
MaxSocketBuffer = 256 * 1024 // 256KB for high-load scenarios
MinSocketBuffer = 32 * 1024 // 32KB minimum for basic functionality
)
// Socket buffer sizes are now centralized in config_constants.go
// SocketBufferConfig holds socket buffer configuration
type SocketBufferConfig struct {
@ -23,8 +18,8 @@ type SocketBufferConfig struct {
// DefaultSocketBufferConfig returns the default socket buffer configuration
func DefaultSocketBufferConfig() SocketBufferConfig {
return SocketBufferConfig{
SendBufferSize: OptimalSocketBuffer,
RecvBufferSize: OptimalSocketBuffer,
SendBufferSize: GetConfig().SocketOptimalBuffer,
RecvBufferSize: GetConfig().SocketOptimalBuffer,
Enabled: true,
}
}
@ -32,8 +27,8 @@ func DefaultSocketBufferConfig() SocketBufferConfig {
// HighLoadSocketBufferConfig returns configuration for high-load scenarios
func HighLoadSocketBufferConfig() SocketBufferConfig {
return SocketBufferConfig{
SendBufferSize: MaxSocketBuffer,
RecvBufferSize: MaxSocketBuffer,
SendBufferSize: GetConfig().SocketMaxBuffer,
RecvBufferSize: GetConfig().SocketMaxBuffer,
Enabled: true,
}
}
@ -112,20 +107,20 @@ func ValidateSocketBufferConfig(config SocketBufferConfig) error {
return nil
}
if config.SendBufferSize < MinSocketBuffer {
return fmt.Errorf("send buffer size %d is below minimum %d", config.SendBufferSize, MinSocketBuffer)
if config.SendBufferSize < GetConfig().SocketMinBuffer {
return fmt.Errorf("send buffer size %d is below minimum %d", config.SendBufferSize, GetConfig().SocketMinBuffer)
}
if config.RecvBufferSize < MinSocketBuffer {
return fmt.Errorf("receive buffer size %d is below minimum %d", config.RecvBufferSize, MinSocketBuffer)
if config.RecvBufferSize < GetConfig().SocketMinBuffer {
return fmt.Errorf("receive buffer size %d is below minimum %d", config.RecvBufferSize, GetConfig().SocketMinBuffer)
}
if config.SendBufferSize > MaxSocketBuffer {
return fmt.Errorf("send buffer size %d exceeds maximum %d", config.SendBufferSize, MaxSocketBuffer)
if config.SendBufferSize > GetConfig().SocketMaxBuffer {
return fmt.Errorf("send buffer size %d exceeds maximum %d", config.SendBufferSize, GetConfig().SocketMaxBuffer)
}
if config.RecvBufferSize > MaxSocketBuffer {
return fmt.Errorf("receive buffer size %d exceeds maximum %d", config.RecvBufferSize, MaxSocketBuffer)
if config.RecvBufferSize > GetConfig().SocketMaxBuffer {
return fmt.Errorf("receive buffer size %d exceeds maximum %d", config.RecvBufferSize, GetConfig().SocketMaxBuffer)
}
return nil

View File

@ -131,7 +131,7 @@ func (s *AudioServerSupervisor) Stop() error {
select {
case <-s.processDone:
s.logger.Info().Msg("audio server process stopped gracefully")
case <-time.After(10 * time.Second):
case <-time.After(GetConfig().SupervisorTimeout):
s.logger.Warn().Msg("audio server process did not stop gracefully, forcing termination")
s.forceKillProcess()
}
@ -365,7 +365,7 @@ func (s *AudioServerSupervisor) terminateProcess() {
select {
case <-done:
s.logger.Info().Int("pid", pid).Msg("audio server process terminated gracefully")
case <-time.After(5 * time.Second):
case <-time.After(GetConfig().InputSupervisorTimeout):
s.logger.Warn().Int("pid", pid).Msg("process did not terminate gracefully, sending SIGKILL")
s.forceKillProcess()
}

View File

@ -234,7 +234,7 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats {
var hitRate float64
if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * 100
hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
}
return ZeroCopyFramePoolStats{