kvm/internal/audio/core_validation.go
Alex P 1d1658db15 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
2025-09-08 17:30:49 +00:00

526 lines
17 KiB
Go

//go:build cgo || arm
// +build cgo arm
package audio
import (
"errors"
"fmt"
"time"
)
// Validation errors
var (
ErrInvalidAudioQuality = errors.New("invalid audio quality level")
ErrInvalidFrameSize = errors.New("invalid frame size")
ErrInvalidFrameData = errors.New("invalid frame data")
ErrFrameDataEmpty = errors.New("invalid frame data: frame data is empty")
ErrFrameDataTooLarge = errors.New("invalid frame data: exceeds maximum")
ErrInvalidBufferSize = errors.New("invalid buffer size")
ErrInvalidLatency = errors.New("invalid latency value")
ErrInvalidConfiguration = errors.New("invalid configuration")
ErrInvalidSocketConfig = errors.New("invalid socket configuration")
ErrInvalidMetricsInterval = errors.New("invalid metrics interval")
ErrInvalidSampleRate = errors.New("invalid sample rate")
ErrInvalidChannels = errors.New("invalid channels")
ErrInvalidBitrate = errors.New("invalid bitrate")
ErrInvalidFrameDuration = errors.New("invalid frame duration")
ErrInvalidOffset = errors.New("invalid offset")
ErrInvalidLength = errors.New("invalid length")
)
// ValidateAudioQuality validates audio quality enum values with enhanced checks
func ValidateAudioQuality(quality AudioQuality) error {
// Validate enum range
if quality < AudioQualityLow || quality > AudioQualityUltra {
return fmt.Errorf("%w: quality value %d outside valid range [%d, %d]",
ErrInvalidAudioQuality, int(quality), int(AudioQualityLow), int(AudioQualityUltra))
}
return nil
}
// ValidateZeroCopyFrame validates zero-copy audio frame
// Optimized to use cached max frame size
func ValidateZeroCopyFrame(frame *ZeroCopyAudioFrame) error {
if frame == nil {
return ErrInvalidFrameData
}
data := frame.Data()
if len(data) == 0 {
return ErrInvalidFrameData
}
// Fast path: use cached max frame size
maxFrameSize := cachedMaxFrameSize
if maxFrameSize == 0 {
// Fallback: get from cache
cache := Config
maxFrameSize = cache.MaxAudioFrameSize
if maxFrameSize == 0 {
// Last resort: use default
maxFrameSize = cache.MaxAudioFrameSize
}
// Cache globally for next calls
cachedMaxFrameSize = maxFrameSize
}
if len(data) > maxFrameSize {
return ErrInvalidFrameSize
}
return nil
}
// ValidateBufferSize validates buffer size parameters with enhanced boundary checks
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateBufferSize(size int) error {
if size <= 0 {
return fmt.Errorf("%w: buffer size %d must be positive", ErrInvalidBufferSize, size)
}
// Fast path: Check against cached max frame size
cache := Config
maxFrameSize := cache.MaxAudioFrameSize
// Most common case: validating a buffer that's sized for audio frames
if maxFrameSize > 0 && size <= maxFrameSize {
return nil
}
// Slower path: full validation against SocketMaxBuffer
config := Config
// Use SocketMaxBuffer as the upper limit for general buffer validation
// This allows for socket buffers while still preventing extremely large allocations
if size > config.SocketMaxBuffer {
return fmt.Errorf("%w: buffer size %d exceeds maximum %d",
ErrInvalidBufferSize, size, config.SocketMaxBuffer)
}
return nil
}
// ValidateLatency validates latency duration values with reasonable bounds
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateLatency(latency time.Duration) error {
if latency < 0 {
return fmt.Errorf("%w: latency %v cannot be negative", ErrInvalidLatency, latency)
}
// Fast path: check against cached max latency
cache := Config
maxLatency := time.Duration(cache.MaxLatency)
// If we have a valid cached value, use it
if maxLatency > 0 {
minLatency := time.Millisecond // Minimum reasonable latency
if latency > 0 && latency < minLatency {
return fmt.Errorf("%w: latency %v below minimum %v",
ErrInvalidLatency, latency, minLatency)
}
if latency > maxLatency {
return fmt.Errorf("%w: latency %v exceeds maximum %v",
ErrInvalidLatency, latency, maxLatency)
}
return nil
}
// Slower path: full validation with GetConfig()
config := Config
minLatency := time.Millisecond // Minimum reasonable latency
if latency > 0 && latency < minLatency {
return fmt.Errorf("%w: latency %v below minimum %v",
ErrInvalidLatency, latency, minLatency)
}
if latency > config.MaxLatency {
return fmt.Errorf("%w: latency %v exceeds maximum %v",
ErrInvalidLatency, latency, config.MaxLatency)
}
return nil
}
// ValidateMetricsInterval validates metrics update interval
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateMetricsInterval(interval time.Duration) error {
// Fast path: check against cached values
cache := Config
minInterval := time.Duration(cache.MinMetricsUpdateInterval)
maxInterval := time.Duration(cache.MaxMetricsUpdateInterval)
// If we have valid cached values, use them
if minInterval > 0 && maxInterval > 0 {
if interval < minInterval {
return fmt.Errorf("%w: interval %v below minimum %v",
ErrInvalidMetricsInterval, interval, minInterval)
}
if interval > maxInterval {
return fmt.Errorf("%w: interval %v exceeds maximum %v",
ErrInvalidMetricsInterval, interval, maxInterval)
}
return nil
}
// Slower path: full validation with GetConfig()
config := Config
minInterval = config.MinMetricsUpdateInterval
maxInterval = config.MaxMetricsUpdateInterval
if interval < minInterval {
return ErrInvalidMetricsInterval
}
if interval > maxInterval {
return ErrInvalidMetricsInterval
}
return nil
}
// ValidateAdaptiveBufferConfig validates adaptive buffer configuration
func ValidateAdaptiveBufferConfig(minSize, maxSize, defaultSize int) error {
if minSize <= 0 || maxSize <= 0 || defaultSize <= 0 {
return ErrInvalidBufferSize
}
if minSize >= maxSize {
return ErrInvalidBufferSize
}
if defaultSize < minSize || defaultSize > maxSize {
return ErrInvalidBufferSize
}
// Validate against global limits
maxBuffer := Config.SocketMaxBuffer
if maxSize > maxBuffer {
return ErrInvalidBufferSize
}
return nil
}
// ValidateInputIPCConfig validates input IPC configuration
func ValidateInputIPCConfig(sampleRate, channels, frameSize int) error {
// Use config values
config := Config
minSampleRate := config.MinSampleRate
maxSampleRate := config.MaxSampleRate
maxChannels := config.MaxChannels
if sampleRate < minSampleRate || sampleRate > maxSampleRate {
return ErrInvalidSampleRate
}
if channels < 1 || channels > maxChannels {
return ErrInvalidChannels
}
if frameSize <= 0 {
return ErrInvalidFrameSize
}
return nil
}
// ValidateOutputIPCConfig validates output IPC configuration
func ValidateOutputIPCConfig(sampleRate, channels, frameSize int) error {
// Use config values
config := Config
minSampleRate := config.MinSampleRate
maxSampleRate := config.MaxSampleRate
maxChannels := config.MaxChannels
if sampleRate < minSampleRate || sampleRate > maxSampleRate {
return ErrInvalidSampleRate
}
if channels < 1 || channels > maxChannels {
return ErrInvalidChannels
}
if frameSize <= 0 {
return ErrInvalidFrameSize
}
return nil
}
// ValidateLatencyConfig validates latency monitor configuration
func ValidateLatencyConfig(config LatencyConfig) error {
if err := ValidateLatency(config.TargetLatency); err != nil {
return err
}
if err := ValidateLatency(config.MaxLatency); err != nil {
return err
}
if config.TargetLatency >= config.MaxLatency {
return ErrInvalidLatency
}
if err := ValidateMetricsInterval(config.OptimizationInterval); err != nil {
return err
}
if config.HistorySize <= 0 {
return ErrInvalidBufferSize
}
if config.JitterThreshold < 0 {
return ErrInvalidLatency
}
if config.AdaptiveThreshold < 0 || config.AdaptiveThreshold > 1.0 {
return ErrInvalidConfiguration
}
return nil
}
// ValidateSampleRate validates audio sample rate values
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateSampleRate(sampleRate int) error {
if sampleRate <= 0 {
return fmt.Errorf("%w: sample rate %d must be positive", ErrInvalidSampleRate, sampleRate)
}
// Fast path: Check against cached sample rate first
cache := Config
cachedRate := cache.SampleRate
// Most common case: validating against the current sample rate
if sampleRate == cachedRate {
return nil
}
// Slower path: check against all valid rates
config := Config
validRates := config.ValidSampleRates
for _, rate := range validRates {
if sampleRate == rate {
return nil
}
}
return fmt.Errorf("%w: sample rate %d not in supported rates %v",
ErrInvalidSampleRate, sampleRate, validRates)
}
// ValidateChannelCount validates audio channel count
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateChannelCount(channels int) error {
if channels <= 0 {
return fmt.Errorf("%w: channel count %d must be positive", ErrInvalidChannels, channels)
}
// Fast path: Check against cached channels first
cache := Config
cachedChannels := cache.Channels
// Most common case: validating against the current channel count
if channels == cachedChannels {
return nil
}
// Fast path: Check against cached max channels
cachedMaxChannels := cache.MaxChannels
if cachedMaxChannels > 0 && channels <= cachedMaxChannels {
return nil
}
// Slow path: Use current config values
updatedMaxChannels := cache.MaxChannels
if channels > updatedMaxChannels {
return fmt.Errorf("%w: channel count %d exceeds maximum %d",
ErrInvalidChannels, channels, updatedMaxChannels)
}
return nil
}
// ValidateBitrate validates audio bitrate values (expects kbps)
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateBitrate(bitrate int) error {
if bitrate <= 0 {
return fmt.Errorf("%w: bitrate %d must be positive", ErrInvalidBitrate, bitrate)
}
// Fast path: Check against cached bitrate values
cache := Config
minBitrate := cache.MinOpusBitrate
maxBitrate := cache.MaxOpusBitrate
// If we have valid cached values, use them
if minBitrate > 0 && maxBitrate > 0 {
// Convert kbps to bps for comparison with config limits
bitrateInBps := bitrate * 1000
if bitrateInBps < minBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, minBitrate)
}
if bitrateInBps > maxBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) exceeds maximum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, maxBitrate)
}
return nil
}
// Slower path: full validation with GetConfig()
config := Config
// Convert kbps to bps for comparison with config limits
bitrateInBps := bitrate * 1000
if bitrateInBps < config.MinOpusBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, config.MinOpusBitrate)
}
if bitrateInBps > config.MaxOpusBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) exceeds maximum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, config.MaxOpusBitrate)
}
return nil
}
// ValidateFrameDuration validates frame duration values
// Optimized to use AudioConfigCache for frequently accessed values
func ValidateFrameDuration(duration time.Duration) error {
if duration <= 0 {
return fmt.Errorf("%w: frame duration %v must be positive", ErrInvalidFrameDuration, duration)
}
// Fast path: Check against cached frame size first
cache := Config
// Convert frameSize (samples) to duration for comparison
cachedFrameSize := cache.FrameSize
cachedSampleRate := cache.SampleRate
// Only do this calculation if we have valid cached values
if cachedFrameSize > 0 && cachedSampleRate > 0 {
cachedDuration := time.Duration(cachedFrameSize) * time.Second / time.Duration(cachedSampleRate)
// Most common case: validating against the current frame duration
if duration == cachedDuration {
return nil
}
}
// Fast path: Check against cached min/max frame duration
cachedMinDuration := time.Duration(cache.MinFrameDuration)
cachedMaxDuration := time.Duration(cache.MaxFrameDuration)
if cachedMinDuration > 0 && cachedMaxDuration > 0 {
if duration < cachedMinDuration {
return fmt.Errorf("%w: frame duration %v below minimum %v",
ErrInvalidFrameDuration, duration, cachedMinDuration)
}
if duration > cachedMaxDuration {
return fmt.Errorf("%w: frame duration %v exceeds maximum %v",
ErrInvalidFrameDuration, duration, cachedMaxDuration)
}
return nil
}
// Slow path: Use current config values
updatedMinDuration := time.Duration(cache.MinFrameDuration)
updatedMaxDuration := time.Duration(cache.MaxFrameDuration)
if duration < updatedMinDuration {
return fmt.Errorf("%w: frame duration %v below minimum %v",
ErrInvalidFrameDuration, duration, updatedMinDuration)
}
if duration > updatedMaxDuration {
return fmt.Errorf("%w: frame duration %v exceeds maximum %v",
ErrInvalidFrameDuration, duration, updatedMaxDuration)
}
return nil
}
// ValidateAudioConfigComplete performs comprehensive audio configuration validation
// Uses optimized validation functions that leverage AudioConfigCache
func ValidateAudioConfigComplete(config AudioConfig) error {
// Fast path: Check if all values match the current cached configuration
cache := Config
cachedSampleRate := cache.SampleRate
cachedChannels := cache.Channels
cachedBitrate := cache.OpusBitrate / 1000 // Convert from bps to kbps
cachedFrameSize := cache.FrameSize
// Only do this calculation if we have valid cached values
if cachedSampleRate > 0 && cachedChannels > 0 && cachedBitrate > 0 && cachedFrameSize > 0 {
cachedDuration := time.Duration(cachedFrameSize) * time.Second / time.Duration(cachedSampleRate)
// Most common case: validating the current configuration
if config.SampleRate == cachedSampleRate &&
config.Channels == cachedChannels &&
config.Bitrate == cachedBitrate &&
config.FrameSize == cachedDuration {
return nil
}
}
// Slower path: validate each parameter individually
if err := ValidateAudioQuality(config.Quality); err != nil {
return fmt.Errorf("quality validation failed: %w", err)
}
if err := ValidateBitrate(config.Bitrate); err != nil {
return fmt.Errorf("bitrate validation failed: %w", err)
}
if err := ValidateSampleRate(config.SampleRate); err != nil {
return fmt.Errorf("sample rate validation failed: %w", err)
}
if err := ValidateChannelCount(config.Channels); err != nil {
return fmt.Errorf("channel count validation failed: %w", err)
}
if err := ValidateFrameDuration(config.FrameSize); err != nil {
return fmt.Errorf("frame duration validation failed: %w", err)
}
return nil
}
// ValidateAudioConfigConstants validates audio configuration constants
func ValidateAudioConfigConstants(config *AudioConfigConstants) error {
// Validate that audio quality constants are within valid ranges
for _, quality := range []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} {
if err := ValidateAudioQuality(quality); err != nil {
return fmt.Errorf("invalid audio quality constant %v: %w", quality, err)
}
}
// Validate configuration values if config is provided
if config != nil {
if config.MaxFrameSize <= 0 {
return fmt.Errorf("invalid MaxFrameSize: %d", config.MaxFrameSize)
}
if config.SampleRate <= 0 {
return fmt.Errorf("invalid SampleRate: %d", config.SampleRate)
}
}
return nil
}
// Global variable for backward compatibility
var cachedMaxFrameSize int
// InitValidationCache initializes cached validation values with actual config
func InitValidationCache() {
// Initialize the global cache variable for backward compatibility
config := Config
cachedMaxFrameSize = config.MaxAudioFrameSize
// Initialize the global audio config cache
cachedMaxFrameSize = Config.MaxAudioFrameSize
}
// ValidateAudioFrame validates audio frame data with cached max size for performance
//
//go:inline
func ValidateAudioFrame(data []byte) error {
// Fast path: check length against cached max size in single operation
dataLen := len(data)
if dataLen == 0 {
return ErrFrameDataEmpty
}
// Use global cached value for fastest access - updated during initialization
maxSize := cachedMaxFrameSize
if maxSize == 0 {
// Fallback: get from cache only if global cache not initialized
cache := Config
maxSize = cache.MaxAudioFrameSize
if maxSize == 0 {
// Last resort: get fresh value
maxSize = cache.MaxAudioFrameSize
}
// Cache the value globally for next calls
cachedMaxFrameSize = maxSize
}
// Single comparison for validation
if dataLen > maxSize {
return ErrFrameDataTooLarge
}
return nil
}
// WrapWithMetadata wraps error with metadata for enhanced validation context
func WrapWithMetadata(err error, component, operation string, metadata map[string]interface{}) error {
if err == nil {
return nil
}
return fmt.Errorf("%s.%s: %w (metadata: %+v)", component, operation, err, metadata)
}