mirror of https://github.com/jetkvm/kvm.git
Compare commits
No commits in common. "950ca2bd9966ae69ebe0463b1c4ba2d22f73b5ec" and "0d4176cf98748a18481b5893c282980d30864836" have entirely different histories.
950ca2bd99
...
0d4176cf98
|
|
@ -13,7 +13,6 @@ func main() {
|
|||
versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit")
|
||||
audioServerPtr := flag.Bool("audio-output-server", false, "Run as audio server subprocess")
|
||||
audioInputServerPtr := flag.Bool("audio-input-server", false, "Run as audio input server subprocess")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *versionPtr || *versionJsonPtr {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// Global audio output supervisor instance
|
||||
globalOutputSupervisor unsafe.Pointer // *AudioOutputSupervisor
|
||||
globalInputSupervisor unsafe.Pointer // *AudioInputSupervisor
|
||||
)
|
||||
|
||||
// isAudioServerProcess detects if we're running as the audio server subprocess
|
||||
|
|
@ -70,17 +70,3 @@ func GetAudioOutputSupervisor() *AudioOutputSupervisor {
|
|||
}
|
||||
return (*AudioOutputSupervisor)(ptr)
|
||||
}
|
||||
|
||||
// SetAudioInputSupervisor sets the global audio input supervisor
|
||||
func SetAudioInputSupervisor(supervisor *AudioInputSupervisor) {
|
||||
atomic.StorePointer(&globalInputSupervisor, unsafe.Pointer(supervisor))
|
||||
}
|
||||
|
||||
// GetAudioInputSupervisor returns the global audio input supervisor
|
||||
func GetAudioInputSupervisor() *AudioInputSupervisor {
|
||||
ptr := atomic.LoadPointer(&globalInputSupervisor)
|
||||
if ptr == nil {
|
||||
return nil
|
||||
}
|
||||
return (*AudioInputSupervisor)(ptr)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
// Package audio provides real-time audio processing for JetKVM with low-latency streaming.
|
||||
//
|
||||
// Key components: output/input pipelines with Opus codec, adaptive buffer management,
|
||||
|
|
@ -170,7 +167,7 @@ func SetAudioQuality(quality AudioQuality) {
|
|||
if config, exists := presets[quality]; exists {
|
||||
currentConfig = config
|
||||
|
||||
// Get OPUS encoder parameters based on quality
|
||||
// Update CGO OPUS encoder parameters based on quality
|
||||
var complexity, vbr, signalType, bandwidth, dtx int
|
||||
switch quality {
|
||||
case AudioQualityLow:
|
||||
|
|
@ -206,29 +203,13 @@ func SetAudioQuality(quality AudioQuality) {
|
|||
dtx = GetConfig().AudioQualityMediumOpusDTX
|
||||
}
|
||||
|
||||
// Restart audio output subprocess with new OPUS configuration
|
||||
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
|
||||
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
||||
logger.Info().Int("quality", int(quality)).Msg("restarting audio output subprocess with new quality settings")
|
||||
|
||||
// Set new OPUS configuration
|
||||
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
|
||||
|
||||
// Stop current subprocess
|
||||
supervisor.Stop()
|
||||
|
||||
// Start subprocess with new configuration
|
||||
if err := supervisor.Start(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to restart audio output subprocess")
|
||||
}
|
||||
} else {
|
||||
// Fallback to dynamic update if supervisor is not available
|
||||
// Dynamically update CGO OPUS encoder parameters
|
||||
// Use current VBR constraint setting from config
|
||||
vbrConstraint := GetConfig().CGOOpusVBRConstraint
|
||||
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
|
||||
logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetAudioConfig returns the current audio configuration
|
||||
|
|
@ -249,81 +230,6 @@ func SetMicrophoneQuality(quality AudioQuality) {
|
|||
presets := GetMicrophoneQualityPresets()
|
||||
if config, exists := presets[quality]; exists {
|
||||
currentMicrophoneConfig = config
|
||||
|
||||
// Get OPUS parameters for the selected quality
|
||||
var complexity, vbr, signalType, bandwidth, dtx int
|
||||
switch quality {
|
||||
case AudioQualityLow:
|
||||
complexity = GetConfig().AudioQualityLowOpusComplexity
|
||||
vbr = GetConfig().AudioQualityLowOpusVBR
|
||||
signalType = GetConfig().AudioQualityLowOpusSignalType
|
||||
bandwidth = GetConfig().AudioQualityLowOpusBandwidth
|
||||
dtx = GetConfig().AudioQualityLowOpusDTX
|
||||
case AudioQualityMedium:
|
||||
complexity = GetConfig().AudioQualityMediumOpusComplexity
|
||||
vbr = GetConfig().AudioQualityMediumOpusVBR
|
||||
signalType = GetConfig().AudioQualityMediumOpusSignalType
|
||||
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
|
||||
dtx = GetConfig().AudioQualityMediumOpusDTX
|
||||
case AudioQualityHigh:
|
||||
complexity = GetConfig().AudioQualityHighOpusComplexity
|
||||
vbr = GetConfig().AudioQualityHighOpusVBR
|
||||
signalType = GetConfig().AudioQualityHighOpusSignalType
|
||||
bandwidth = GetConfig().AudioQualityHighOpusBandwidth
|
||||
dtx = GetConfig().AudioQualityHighOpusDTX
|
||||
case AudioQualityUltra:
|
||||
complexity = GetConfig().AudioQualityUltraOpusComplexity
|
||||
vbr = GetConfig().AudioQualityUltraOpusVBR
|
||||
signalType = GetConfig().AudioQualityUltraOpusSignalType
|
||||
bandwidth = GetConfig().AudioQualityUltraOpusBandwidth
|
||||
dtx = GetConfig().AudioQualityUltraOpusDTX
|
||||
default:
|
||||
// Use medium quality as fallback
|
||||
complexity = GetConfig().AudioQualityMediumOpusComplexity
|
||||
vbr = GetConfig().AudioQualityMediumOpusVBR
|
||||
signalType = GetConfig().AudioQualityMediumOpusSignalType
|
||||
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
|
||||
dtx = GetConfig().AudioQualityMediumOpusDTX
|
||||
}
|
||||
|
||||
// Update audio input subprocess configuration dynamically without restart
|
||||
if supervisor := GetAudioInputSupervisor(); supervisor != nil {
|
||||
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
||||
logger.Info().Int("quality", int(quality)).Msg("updating audio input subprocess quality settings dynamically")
|
||||
|
||||
// Set new OPUS configuration for future restarts
|
||||
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
|
||||
|
||||
// Send dynamic configuration update to running subprocess
|
||||
if supervisor.IsConnected() {
|
||||
// Convert AudioConfig to InputIPCOpusConfig with complete Opus parameters
|
||||
opusConfig := InputIPCOpusConfig{
|
||||
SampleRate: config.SampleRate,
|
||||
Channels: config.Channels,
|
||||
FrameSize: int(config.FrameSize.Milliseconds() * int64(config.SampleRate) / 1000), // Convert ms to samples
|
||||
Bitrate: config.Bitrate * 1000, // Convert kbps to bps
|
||||
Complexity: complexity,
|
||||
VBR: vbr,
|
||||
SignalType: signalType,
|
||||
Bandwidth: bandwidth,
|
||||
DTX: dtx,
|
||||
}
|
||||
|
||||
logger.Info().Interface("opusConfig", opusConfig).Msg("sending Opus configuration to audio input subprocess")
|
||||
if err := supervisor.SendOpusConfig(opusConfig); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to send dynamic Opus config update, subprocess may need restart")
|
||||
// Fallback to restart if dynamic update fails
|
||||
supervisor.Stop()
|
||||
if err := supervisor.Start(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to restart audio input subprocess after config update failure")
|
||||
}
|
||||
} else {
|
||||
logger.Info().Msg("audio input quality updated dynamically with complete Opus configuration")
|
||||
}
|
||||
} else {
|
||||
logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio input subprocess not connected, configuration will apply on next start")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
)
|
||||
|
||||
// Lock-free buffer cache for per-goroutine optimization
|
||||
|
|
@ -16,9 +18,6 @@ type lockFreeBufferCache struct {
|
|||
// Per-goroutine buffer cache using goroutine-local storage
|
||||
var goroutineBufferCache = make(map[int64]*lockFreeBufferCache)
|
||||
var goroutineCacheMutex sync.RWMutex
|
||||
var lastCleanupTime int64 // Unix timestamp of last cleanup
|
||||
const maxCacheSize = 1000 // Maximum number of goroutine caches
|
||||
const cleanupInterval = 300 // Cleanup interval in seconds (5 minutes)
|
||||
|
||||
// getGoroutineID extracts goroutine ID from runtime stack for cache key
|
||||
func getGoroutineID() int64 {
|
||||
|
|
@ -39,41 +38,6 @@ func getGoroutineID() int64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
// cleanupGoroutineCache removes stale entries from the goroutine cache
|
||||
func cleanupGoroutineCache() {
|
||||
now := time.Now().Unix()
|
||||
lastCleanup := atomic.LoadInt64(&lastCleanupTime)
|
||||
|
||||
// Only cleanup if enough time has passed
|
||||
if now-lastCleanup < cleanupInterval {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to acquire cleanup lock atomically
|
||||
if !atomic.CompareAndSwapInt64(&lastCleanupTime, lastCleanup, now) {
|
||||
return // Another goroutine is already cleaning up
|
||||
}
|
||||
|
||||
goroutineCacheMutex.Lock()
|
||||
defer goroutineCacheMutex.Unlock()
|
||||
|
||||
// If cache is too large, remove oldest entries (simple FIFO)
|
||||
if len(goroutineBufferCache) > maxCacheSize {
|
||||
// Remove half of the entries to avoid frequent cleanups
|
||||
toRemove := len(goroutineBufferCache) - maxCacheSize/2
|
||||
count := 0
|
||||
for gid := range goroutineBufferCache {
|
||||
delete(goroutineBufferCache, gid)
|
||||
count++
|
||||
if count >= toRemove {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Log cleanup for debugging (removed logging dependency)
|
||||
_ = count // Avoid unused variable warning
|
||||
}
|
||||
}
|
||||
|
||||
type AudioBufferPool struct {
|
||||
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
|
||||
currentSize int64 // Current pool size (atomic)
|
||||
|
|
@ -93,7 +57,9 @@ type AudioBufferPool struct {
|
|||
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
||||
// Validate buffer size parameter
|
||||
if err := ValidateBufferSize(bufferSize); err != nil {
|
||||
// Use default value on validation error
|
||||
// Log validation error and use default value
|
||||
logger := logging.GetDefaultLogger().With().Str("component", "AudioBufferPool").Logger()
|
||||
logger.Warn().Err(err).Int("bufferSize", bufferSize).Msg("invalid buffer size, using default")
|
||||
bufferSize = GetConfig().AudioFramePoolSize
|
||||
}
|
||||
|
||||
|
|
@ -133,9 +99,6 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
|||
}
|
||||
|
||||
func (p *AudioBufferPool) Get() []byte {
|
||||
// Trigger periodic cleanup of goroutine cache
|
||||
cleanupGoroutineCache()
|
||||
|
||||
start := time.Now()
|
||||
wasHit := false
|
||||
defer func() {
|
||||
|
|
|
|||
|
|
@ -709,12 +709,6 @@ func cgoAudioReadEncode(buf []byte) (int, error) {
|
|||
return 0, newBufferTooSmallError(len(buf), minRequired)
|
||||
}
|
||||
|
||||
// Skip initialization check for now to avoid CGO compilation issues
|
||||
// TODO: Add proper initialization state checking
|
||||
// Note: The C code already has comprehensive state tracking with capture_initialized,
|
||||
// capture_initializing, playback_initialized, and playback_initializing flags.
|
||||
// When CGO environment is properly configured, this should check C.capture_initialized.
|
||||
|
||||
n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0]))
|
||||
if n < 0 {
|
||||
return 0, newAudioReadEncodeError(int(n))
|
||||
|
|
|
|||
|
|
@ -1554,36 +1554,35 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
|||
FrameSize: 960,
|
||||
MaxPacketSize: 4000,
|
||||
|
||||
// Audio Quality Bitrates - Optimized for RV1106 SoC and KVM layer compatibility
|
||||
// Reduced bitrates to minimize CPU load and prevent mouse lag
|
||||
// Audio Quality Bitrates
|
||||
AudioQualityLowOutputBitrate: 32,
|
||||
AudioQualityLowInputBitrate: 16,
|
||||
AudioQualityMediumOutputBitrate: 48,
|
||||
AudioQualityMediumInputBitrate: 24,
|
||||
AudioQualityMediumOutputBitrate: 64,
|
||||
AudioQualityMediumInputBitrate: 32,
|
||||
|
||||
// AudioQualityHighOutputBitrate defines bitrate for high-quality output.
|
||||
// Used in: Professional applications requiring good audio fidelity on RV1106
|
||||
// Impact: Balanced quality optimized for single-core ARM performance.
|
||||
// Reduced to 64kbps for RV1106 compatibility and minimal CPU overhead.
|
||||
AudioQualityHighOutputBitrate: 64,
|
||||
// Used in: Professional applications requiring excellent audio fidelity
|
||||
// Impact: Provides excellent quality but increases bandwidth usage.
|
||||
// Default 128kbps matches professional Opus encoding standards.
|
||||
AudioQualityHighOutputBitrate: 128,
|
||||
|
||||
// AudioQualityHighInputBitrate defines bitrate for high-quality input.
|
||||
// Used in: High-quality microphone input optimized for RV1106
|
||||
// Impact: Clear voice reproduction without overwhelming single-core CPU.
|
||||
// Reduced to 32kbps for optimal RV1106 performance without lag.
|
||||
AudioQualityHighInputBitrate: 32,
|
||||
// Used in: High-quality microphone input for professional use
|
||||
// Impact: Ensures clear voice reproduction for professional scenarios.
|
||||
// Default 64kbps provides excellent voice quality.
|
||||
AudioQualityHighInputBitrate: 64,
|
||||
|
||||
// AudioQualityUltraOutputBitrate defines bitrate for ultra-quality output.
|
||||
// Used in: Maximum quality while ensuring RV1106 stability
|
||||
// Impact: Best possible quality without interfering with KVM operations.
|
||||
// Reduced to 96kbps for RV1106 maximum performance without mouse lag.
|
||||
AudioQualityUltraOutputBitrate: 96,
|
||||
// Used in: Audiophile-grade reproduction and high-bandwidth connections
|
||||
// Impact: Maximum quality but requires significant bandwidth.
|
||||
// Default 192kbps suitable for high-bandwidth, quality-critical scenarios.
|
||||
AudioQualityUltraOutputBitrate: 192,
|
||||
|
||||
// AudioQualityUltraInputBitrate defines bitrate for ultra-quality input.
|
||||
// Used in: Premium microphone input optimized for RV1106 constraints
|
||||
// Impact: Excellent voice quality within RV1106 processing limits.
|
||||
// Reduced to 48kbps for stable RV1106 operation without lag.
|
||||
AudioQualityUltraInputBitrate: 48,
|
||||
// Used in: Professional microphone input requiring maximum quality
|
||||
// Impact: Provides audiophile-grade voice quality with high bandwidth.
|
||||
// Default 96kbps ensures maximum voice reproduction quality.
|
||||
AudioQualityUltraInputBitrate: 96,
|
||||
|
||||
// Audio Quality Sample Rates - Sampling frequencies for different quality levels
|
||||
// Used in: Audio capture, processing, and format negotiation
|
||||
|
|
@ -1591,15 +1590,15 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
|||
|
||||
// AudioQualityLowSampleRate defines sampling frequency for low-quality audio.
|
||||
// Used in: Bandwidth-constrained scenarios and basic audio requirements
|
||||
// Impact: Captures frequencies up to 24kHz while maintaining efficiency.
|
||||
// Default 48kHz provides better quality while maintaining compatibility.
|
||||
AudioQualityLowSampleRate: 48000,
|
||||
// Impact: Captures frequencies up to 11kHz while minimizing processing load.
|
||||
// Default 22.05kHz sufficient for speech and basic audio.
|
||||
AudioQualityLowSampleRate: 22050,
|
||||
|
||||
// AudioQualityMediumSampleRate defines sampling frequency for medium-quality audio.
|
||||
// Used in: Standard audio scenarios requiring high-quality reproduction
|
||||
// Impact: Captures full audible range up to 24kHz with excellent processing.
|
||||
// Default 48kHz provides professional standard with optimal balance.
|
||||
AudioQualityMediumSampleRate: 48000,
|
||||
// Used in: Standard audio scenarios requiring CD-quality reproduction
|
||||
// Impact: Captures full audible range up to 22kHz with balanced processing.
|
||||
// Default 44.1kHz provides CD-quality standard with excellent balance.
|
||||
AudioQualityMediumSampleRate: 44100,
|
||||
|
||||
// AudioQualityMicLowSampleRate defines sampling frequency for low-quality microphone.
|
||||
// Used in: Voice/microphone input in bandwidth-constrained scenarios
|
||||
|
|
@ -1612,80 +1611,80 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
|||
// Impact: Controls latency vs processing efficiency trade-offs
|
||||
|
||||
// AudioQualityLowFrameSize defines frame duration for low-quality audio.
|
||||
// Used in: RV1106 efficiency-prioritized scenarios
|
||||
// Impact: Balanced frame size for quality and efficiency.
|
||||
// Reduced to 20ms for better responsiveness and reduced audio saccades.
|
||||
AudioQualityLowFrameSize: 20 * time.Millisecond,
|
||||
// Used in: Bandwidth-constrained scenarios prioritizing efficiency
|
||||
// Impact: Reduces processing overhead with acceptable latency increase.
|
||||
// Default 40ms provides efficiency for constrained scenarios.
|
||||
AudioQualityLowFrameSize: 40 * time.Millisecond,
|
||||
|
||||
// AudioQualityMediumFrameSize defines frame duration for medium-quality audio.
|
||||
// Used in: Balanced RV1106 real-time audio applications
|
||||
// Impact: Balances latency and processing efficiency for RV1106.
|
||||
// Optimized to 20ms for RV1106 balanced performance.
|
||||
// Used in: Standard real-time audio applications
|
||||
// Impact: Provides good balance of latency and processing efficiency.
|
||||
// Default 20ms standard for real-time audio applications.
|
||||
AudioQualityMediumFrameSize: 20 * time.Millisecond,
|
||||
|
||||
// AudioQualityHighFrameSize defines frame duration for high-quality audio.
|
||||
// Used in: RV1106 high-quality scenarios with performance constraints
|
||||
// Impact: Maintains acceptable latency while reducing RV1106 CPU load.
|
||||
// Optimized to 20ms for RV1106 high-quality balance.
|
||||
// Used in: High-quality audio scenarios with balanced requirements
|
||||
// Impact: Maintains good latency while ensuring quality processing.
|
||||
// Default 20ms provides optimal balance for high-quality scenarios.
|
||||
AudioQualityHighFrameSize: 20 * time.Millisecond,
|
||||
|
||||
// AudioQualityUltraFrameSize defines frame duration for ultra-quality audio.
|
||||
// Used in: Maximum RV1106 performance without KVM interference
|
||||
// Impact: Balances quality and processing efficiency for RV1106 stability.
|
||||
// Optimized to 20ms for RV1106 maximum stable performance.
|
||||
AudioQualityUltraFrameSize: 20 * time.Millisecond,
|
||||
// Used in: Applications requiring immediate audio feedback
|
||||
// Impact: Minimizes latency for ultra-responsive audio processing.
|
||||
// Default 10ms ensures minimal latency for immediate feedback.
|
||||
AudioQualityUltraFrameSize: 10 * time.Millisecond,
|
||||
|
||||
// Audio Quality Channels - Optimized for RV1106 processing efficiency
|
||||
// Used in: Audio processing pipeline optimized for single-core ARM performance
|
||||
AudioQualityLowChannels: 1, // Mono for minimal RV1106 processing
|
||||
AudioQualityMediumChannels: 2, // Stereo for balanced RV1106 performance
|
||||
AudioQualityHighChannels: 2, // Stereo for RV1106 high-quality scenarios
|
||||
AudioQualityUltraChannels: 2, // Stereo for maximum RV1106 performance
|
||||
// Audio Quality Channels - Channel configuration for different quality levels
|
||||
// Used in: Audio processing pipeline for channel handling and bandwidth control
|
||||
AudioQualityLowChannels: 1,
|
||||
AudioQualityMediumChannels: 2,
|
||||
AudioQualityHighChannels: 2,
|
||||
AudioQualityUltraChannels: 2,
|
||||
|
||||
// Audio Quality OPUS Encoder Parameters - Quality-specific encoder settings
|
||||
// Used in: Dynamic OPUS encoder configuration based on quality presets
|
||||
// Impact: Controls encoding complexity, VBR, signal type, bandwidth, and DTX
|
||||
|
||||
// Low Quality OPUS Parameters - Optimized for RV1106 minimal CPU usage
|
||||
AudioQualityLowOpusComplexity: 0, // Minimum complexity to reduce CPU load
|
||||
AudioQualityLowOpusVBR: 1, // VBR for better quality at same bitrate
|
||||
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for lower complexity
|
||||
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND for efficiency
|
||||
AudioQualityLowOpusDTX: 1, // Enable DTX to reduce processing when silent
|
||||
// Low Quality OPUS Parameters - Optimized for bandwidth conservation
|
||||
AudioQualityLowOpusComplexity: 1, // Low complexity for minimal CPU usage
|
||||
AudioQualityLowOpusVBR: 0, // CBR for predictable bandwidth
|
||||
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE
|
||||
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND
|
||||
AudioQualityLowOpusDTX: 1, // Enable DTX for silence suppression
|
||||
|
||||
// Medium Quality OPUS Parameters - Balanced for RV1106 performance
|
||||
AudioQualityMediumOpusComplexity: 1, // Very low complexity for RV1106 stability
|
||||
AudioQualityMediumOpusVBR: 1, // VBR for optimal quality
|
||||
AudioQualityMediumOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for efficiency
|
||||
AudioQualityMediumOpusBandwidth: 1102, // OPUS_BANDWIDTH_MEDIUMBAND for balance
|
||||
AudioQualityMediumOpusDTX: 1, // Enable DTX for CPU savings
|
||||
// Medium Quality OPUS Parameters - Balanced performance and quality
|
||||
AudioQualityMediumOpusComplexity: 5, // Medium complexity for balanced performance
|
||||
AudioQualityMediumOpusVBR: 1, // VBR for better quality
|
||||
AudioQualityMediumOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
||||
AudioQualityMediumOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND
|
||||
AudioQualityMediumOpusDTX: 0, // Disable DTX for consistent quality
|
||||
|
||||
// High Quality OPUS Parameters - Optimized for RV1106 high performance
|
||||
AudioQualityHighOpusComplexity: 2, // Low complexity for RV1106 limits
|
||||
// High Quality OPUS Parameters - High quality with good performance
|
||||
AudioQualityHighOpusComplexity: 8, // High complexity for better quality
|
||||
AudioQualityHighOpusVBR: 1, // VBR for optimal quality
|
||||
AudioQualityHighOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
||||
AudioQualityHighOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for good range
|
||||
AudioQualityHighOpusBandwidth: 1104, // OPUS_BANDWIDTH_SUPERWIDEBAND
|
||||
AudioQualityHighOpusDTX: 0, // Disable DTX for consistent quality
|
||||
|
||||
// Ultra Quality OPUS Parameters - Maximum RV1106 performance without KVM interference
|
||||
AudioQualityUltraOpusComplexity: 3, // Moderate complexity for RV1106 stability
|
||||
// Ultra Quality OPUS Parameters - Maximum quality settings
|
||||
AudioQualityUltraOpusComplexity: 10, // Maximum complexity for best quality
|
||||
AudioQualityUltraOpusVBR: 1, // VBR for optimal quality
|
||||
AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
||||
AudioQualityUltraOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for stability
|
||||
AudioQualityUltraOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
|
||||
AudioQualityUltraOpusDTX: 0, // Disable DTX for maximum quality
|
||||
|
||||
// CGO Audio Constants - Optimized for RV1106 native audio processing
|
||||
CGOOpusBitrate: 64000, // Reduced for RV1106 efficiency
|
||||
CGOOpusComplexity: 2, // Minimal complexity for RV1106
|
||||
// CGO Audio Constants
|
||||
CGOOpusBitrate: 96000,
|
||||
CGOOpusComplexity: 3,
|
||||
CGOOpusVBR: 1,
|
||||
CGOOpusVBRConstraint: 1,
|
||||
CGOOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
||||
CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for RV1106
|
||||
CGOOpusSignalType: 3, // OPUS_SIGNAL_MUSIC
|
||||
CGOOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
|
||||
CGOOpusDTX: 0,
|
||||
CGOSampleRate: 48000,
|
||||
CGOChannels: 2,
|
||||
CGOFrameSize: 960,
|
||||
CGOMaxPacketSize: 1200, // Reduced for RV1106 memory efficiency
|
||||
CGOMaxPacketSize: 1500,
|
||||
|
||||
// Input IPC Constants
|
||||
// InputIPCSampleRate defines sample rate for input IPC operations.
|
||||
|
|
|
|||
|
|
@ -208,30 +208,7 @@ func (aim *AudioInputManager) LogPerformanceStats() {
|
|||
Msg("Audio input performance metrics")
|
||||
}
|
||||
|
||||
// IsRunning returns whether the audio input manager is running
|
||||
// This checks both the internal state and existing system processes
|
||||
func (aim *AudioInputManager) IsRunning() bool {
|
||||
// First check internal state
|
||||
if aim.BaseAudioManager.IsRunning() {
|
||||
return true
|
||||
}
|
||||
|
||||
// If internal state says not running, check for existing system processes
|
||||
// This prevents duplicate subprocess creation when a process already exists
|
||||
if aim.ipcManager != nil {
|
||||
supervisor := aim.ipcManager.GetSupervisor()
|
||||
if supervisor != nil {
|
||||
if existingPID, exists := supervisor.HasExistingProcess(); exists {
|
||||
aim.logger.Info().Int("existing_pid", existingPID).Msg("Found existing audio input server process")
|
||||
// Update internal state to reflect reality
|
||||
aim.setRunning(true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
// Note: IsRunning() is inherited from BaseAudioManager
|
||||
|
||||
// IsReady returns whether the audio input manager is ready to receive frames
|
||||
// This checks both that it's running and that the IPC connection is established
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ type InputMessageType uint8
|
|||
const (
|
||||
InputMessageTypeOpusFrame InputMessageType = iota
|
||||
InputMessageTypeConfig
|
||||
InputMessageTypeOpusConfig
|
||||
InputMessageTypeStop
|
||||
InputMessageTypeHeartbeat
|
||||
InputMessageTypeAck
|
||||
|
|
@ -204,19 +203,6 @@ type InputIPCConfig struct {
|
|||
FrameSize int
|
||||
}
|
||||
|
||||
// InputIPCOpusConfig contains complete Opus encoder configuration
|
||||
type InputIPCOpusConfig struct {
|
||||
SampleRate int
|
||||
Channels int
|
||||
FrameSize int
|
||||
Bitrate int
|
||||
Complexity int
|
||||
VBR int
|
||||
SignalType int
|
||||
Bandwidth int
|
||||
DTX int
|
||||
}
|
||||
|
||||
// AudioInputServer handles IPC communication for audio input processing
|
||||
type AudioInputServer struct {
|
||||
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
|
||||
|
|
@ -476,8 +462,6 @@ func (ais *AudioInputServer) processMessage(msg *InputIPCMessage) error {
|
|||
return ais.processOpusFrame(msg.Data)
|
||||
case InputMessageTypeConfig:
|
||||
return ais.processConfig(msg.Data)
|
||||
case InputMessageTypeOpusConfig:
|
||||
return ais.processOpusConfig(msg.Data)
|
||||
case InputMessageTypeStop:
|
||||
return fmt.Errorf("stop message received")
|
||||
case InputMessageTypeHeartbeat:
|
||||
|
|
@ -523,50 +507,6 @@ func (ais *AudioInputServer) processConfig(data []byte) error {
|
|||
return ais.sendAck()
|
||||
}
|
||||
|
||||
// processOpusConfig processes a complete Opus encoder configuration update
|
||||
func (ais *AudioInputServer) processOpusConfig(data []byte) error {
|
||||
logger := logging.GetDefaultLogger().With().Str("component", AudioInputServerComponent).Logger()
|
||||
|
||||
// Validate configuration data size (9 * int32 = 36 bytes)
|
||||
if len(data) != 36 {
|
||||
return fmt.Errorf("invalid Opus configuration data size: expected 36 bytes, got %d", len(data))
|
||||
}
|
||||
|
||||
// Deserialize Opus configuration
|
||||
config := InputIPCOpusConfig{
|
||||
SampleRate: int(binary.LittleEndian.Uint32(data[0:4])),
|
||||
Channels: int(binary.LittleEndian.Uint32(data[4:8])),
|
||||
FrameSize: int(binary.LittleEndian.Uint32(data[8:12])),
|
||||
Bitrate: int(binary.LittleEndian.Uint32(data[12:16])),
|
||||
Complexity: int(binary.LittleEndian.Uint32(data[16:20])),
|
||||
VBR: int(binary.LittleEndian.Uint32(data[20:24])),
|
||||
SignalType: int(binary.LittleEndian.Uint32(data[24:28])),
|
||||
Bandwidth: int(binary.LittleEndian.Uint32(data[28:32])),
|
||||
DTX: int(binary.LittleEndian.Uint32(data[32:36])),
|
||||
}
|
||||
|
||||
logger.Info().Interface("config", config).Msg("applying dynamic Opus encoder configuration")
|
||||
|
||||
// Apply the Opus encoder configuration dynamically
|
||||
err := CGOUpdateOpusEncoderParams(
|
||||
config.Bitrate,
|
||||
config.Complexity,
|
||||
config.VBR,
|
||||
0, // VBR constraint - using default
|
||||
config.SignalType,
|
||||
config.Bandwidth,
|
||||
config.DTX,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to apply Opus encoder configuration")
|
||||
return fmt.Errorf("failed to apply Opus configuration: %w", err)
|
||||
}
|
||||
|
||||
logger.Info().Msg("Opus encoder configuration applied successfully")
|
||||
return ais.sendAck()
|
||||
}
|
||||
|
||||
// sendAck sends an acknowledgment message
|
||||
func (ais *AudioInputServer) sendAck() error {
|
||||
ais.mtx.Lock()
|
||||
|
|
@ -785,44 +725,6 @@ func (aic *AudioInputClient) SendConfig(config InputIPCConfig) error {
|
|||
return aic.writeMessage(msg)
|
||||
}
|
||||
|
||||
// SendOpusConfig sends a complete Opus encoder configuration update to the audio input server
|
||||
func (aic *AudioInputClient) SendOpusConfig(config InputIPCOpusConfig) error {
|
||||
aic.mtx.Lock()
|
||||
defer aic.mtx.Unlock()
|
||||
|
||||
if !aic.running || aic.conn == nil {
|
||||
return fmt.Errorf("not connected to audio input server")
|
||||
}
|
||||
|
||||
// Validate configuration parameters
|
||||
if config.SampleRate <= 0 || config.Channels <= 0 || config.FrameSize <= 0 || config.Bitrate <= 0 {
|
||||
return fmt.Errorf("invalid Opus configuration: SampleRate=%d, Channels=%d, FrameSize=%d, Bitrate=%d",
|
||||
config.SampleRate, config.Channels, config.FrameSize, config.Bitrate)
|
||||
}
|
||||
|
||||
// Serialize Opus configuration (9 * int32 = 36 bytes)
|
||||
data := make([]byte, 36)
|
||||
binary.LittleEndian.PutUint32(data[0:4], uint32(config.SampleRate))
|
||||
binary.LittleEndian.PutUint32(data[4:8], uint32(config.Channels))
|
||||
binary.LittleEndian.PutUint32(data[8:12], uint32(config.FrameSize))
|
||||
binary.LittleEndian.PutUint32(data[12:16], uint32(config.Bitrate))
|
||||
binary.LittleEndian.PutUint32(data[16:20], uint32(config.Complexity))
|
||||
binary.LittleEndian.PutUint32(data[20:24], uint32(config.VBR))
|
||||
binary.LittleEndian.PutUint32(data[24:28], uint32(config.SignalType))
|
||||
binary.LittleEndian.PutUint32(data[28:32], uint32(config.Bandwidth))
|
||||
binary.LittleEndian.PutUint32(data[32:36], uint32(config.DTX))
|
||||
|
||||
msg := &InputIPCMessage{
|
||||
Magic: inputMagicNumber,
|
||||
Type: InputMessageTypeOpusConfig,
|
||||
Length: uint32(len(data)),
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Data: data,
|
||||
}
|
||||
|
||||
return aic.writeMessage(msg)
|
||||
}
|
||||
|
||||
// SendHeartbeat sends a heartbeat message
|
||||
func (aic *AudioInputClient) SendHeartbeat() error {
|
||||
aic.mtx.Lock()
|
||||
|
|
|
|||
|
|
@ -1,70 +1,21 @@
|
|||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
package audio
|
||||
|
||||
/*
|
||||
#cgo pkg-config: alsa
|
||||
#cgo LDFLAGS: -lopus
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
)
|
||||
|
||||
// getEnvInt reads an integer from environment variable with a default value
|
||||
func getEnvIntInput(key string, defaultValue int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// parseOpusConfigInput reads OPUS configuration from environment variables
|
||||
// with fallback to default config values for input server
|
||||
func parseOpusConfigInput() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
||||
// Read configuration from environment variables with config defaults
|
||||
bitrate = getEnvIntInput("JETKVM_OPUS_BITRATE", GetConfig().CGOOpusBitrate)
|
||||
complexity = getEnvIntInput("JETKVM_OPUS_COMPLEXITY", GetConfig().CGOOpusComplexity)
|
||||
vbr = getEnvIntInput("JETKVM_OPUS_VBR", GetConfig().CGOOpusVBR)
|
||||
signalType = getEnvIntInput("JETKVM_OPUS_SIGNAL_TYPE", GetConfig().CGOOpusSignalType)
|
||||
bandwidth = getEnvIntInput("JETKVM_OPUS_BANDWIDTH", GetConfig().CGOOpusBandwidth)
|
||||
dtx = getEnvIntInput("JETKVM_OPUS_DTX", GetConfig().CGOOpusDTX)
|
||||
|
||||
return bitrate, complexity, vbr, signalType, bandwidth, dtx
|
||||
}
|
||||
|
||||
// applyOpusConfigInput applies OPUS configuration to the global config for input server
|
||||
func applyOpusConfigInput(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
||||
config := GetConfig()
|
||||
config.CGOOpusBitrate = bitrate
|
||||
config.CGOOpusComplexity = complexity
|
||||
config.CGOOpusVBR = vbr
|
||||
config.CGOOpusSignalType = signalType
|
||||
config.CGOOpusBandwidth = bandwidth
|
||||
config.CGOOpusDTX = dtx
|
||||
}
|
||||
|
||||
// RunAudioInputServer runs the audio input server subprocess
|
||||
// This should be called from main() when the subprocess is detected
|
||||
func RunAudioInputServer() error {
|
||||
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
|
||||
logger.Debug().Msg("audio input server subprocess starting")
|
||||
|
||||
// Parse OPUS configuration from environment variables
|
||||
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfigInput()
|
||||
applyOpusConfigInput(bitrate, complexity, vbr, signalType, bandwidth, dtx)
|
||||
|
||||
// Initialize validation cache for optimal performance
|
||||
InitValidationCache()
|
||||
|
||||
|
|
@ -72,16 +23,13 @@ func RunAudioInputServer() error {
|
|||
StartAdaptiveBuffering()
|
||||
defer StopAdaptiveBuffering()
|
||||
|
||||
// Initialize CGO audio playback (optional for input server)
|
||||
// This is used for audio loopback/monitoring features
|
||||
// Initialize CGO audio system
|
||||
err := CGOAudioPlaybackInit()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to initialize CGO audio playback - audio monitoring disabled")
|
||||
// Continue without playback - input functionality doesn't require it
|
||||
} else {
|
||||
defer CGOAudioPlaybackClose()
|
||||
logger.Debug().Msg("CGO audio playback initialized successfully")
|
||||
logger.Error().Err(err).Msg("failed to initialize CGO audio playback")
|
||||
return err
|
||||
}
|
||||
defer CGOAudioPlaybackClose()
|
||||
|
||||
// Create and start the IPC server
|
||||
server, err := NewAudioInputServer()
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -18,9 +12,6 @@ import (
|
|||
type AudioInputSupervisor struct {
|
||||
*BaseSupervisor
|
||||
client *AudioInputClient
|
||||
|
||||
// Environment variables for OPUS configuration
|
||||
opusEnv []string
|
||||
}
|
||||
|
||||
// NewAudioInputSupervisor creates a new audio input supervisor
|
||||
|
|
@ -31,44 +22,14 @@ func NewAudioInputSupervisor() *AudioInputSupervisor {
|
|||
}
|
||||
}
|
||||
|
||||
// SetOpusConfig sets OPUS configuration parameters as environment variables
|
||||
// for the audio input subprocess
|
||||
func (ais *AudioInputSupervisor) SetOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
||||
ais.mutex.Lock()
|
||||
defer ais.mutex.Unlock()
|
||||
|
||||
// Store OPUS parameters as environment variables
|
||||
ais.opusEnv = []string{
|
||||
"JETKVM_OPUS_BITRATE=" + strconv.Itoa(bitrate),
|
||||
"JETKVM_OPUS_COMPLEXITY=" + strconv.Itoa(complexity),
|
||||
"JETKVM_OPUS_VBR=" + strconv.Itoa(vbr),
|
||||
"JETKVM_OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType),
|
||||
"JETKVM_OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth),
|
||||
"JETKVM_OPUS_DTX=" + strconv.Itoa(dtx),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the audio input server subprocess
|
||||
func (ais *AudioInputSupervisor) Start() error {
|
||||
ais.mutex.Lock()
|
||||
defer ais.mutex.Unlock()
|
||||
|
||||
if ais.IsRunning() {
|
||||
if ais.cmd != nil && ais.cmd.Process != nil {
|
||||
return fmt.Errorf("audio input supervisor already running with PID %d", ais.cmd.Process.Pid)
|
||||
}
|
||||
return fmt.Errorf("audio input supervisor already running")
|
||||
}
|
||||
|
||||
// Check for existing audio input server process
|
||||
if existingPID, err := ais.findExistingAudioInputProcess(); err == nil {
|
||||
ais.logger.Info().Int("existing_pid", existingPID).Msg("Found existing audio input server process, connecting to it")
|
||||
|
||||
// Try to connect to the existing process
|
||||
ais.setRunning(true)
|
||||
go ais.connectClient()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create context for subprocess management
|
||||
ais.createContext()
|
||||
|
|
@ -79,16 +40,11 @@ func (ais *AudioInputSupervisor) Start() error {
|
|||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Build command arguments (only subprocess flag)
|
||||
args := []string{"--audio-input-server"}
|
||||
|
||||
// Create command for audio input server subprocess
|
||||
cmd := exec.CommandContext(ais.ctx, execPath, args...)
|
||||
|
||||
// Set environment variables for IPC and OPUS configuration
|
||||
env := append(os.Environ(), "JETKVM_AUDIO_INPUT_IPC=true") // Enable IPC mode
|
||||
env = append(env, ais.opusEnv...) // Add OPUS configuration
|
||||
cmd.Env = env
|
||||
cmd := exec.CommandContext(ais.ctx, execPath, "--audio-input-server")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"JETKVM_AUDIO_INPUT_IPC=true", // Enable IPC mode
|
||||
)
|
||||
|
||||
// Set process group to allow clean termination
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
|
|
@ -106,7 +62,7 @@ func (ais *AudioInputSupervisor) Start() error {
|
|||
return fmt.Errorf("failed to start audio input server process: %w", err)
|
||||
}
|
||||
|
||||
ais.logger.Info().Int("pid", cmd.Process.Pid).Strs("args", args).Strs("opus_env", ais.opusEnv).Msg("Audio input server subprocess started")
|
||||
ais.logger.Info().Int("pid", cmd.Process.Pid).Msg("Audio input server subprocess started")
|
||||
|
||||
// Add process to monitoring
|
||||
ais.processMonitor.AddProcess(cmd.Process.Pid, "audio-input-server")
|
||||
|
|
@ -141,8 +97,7 @@ func (ais *AudioInputSupervisor) Stop() {
|
|||
|
||||
// Try graceful termination first
|
||||
if ais.cmd != nil && ais.cmd.Process != nil {
|
||||
pid := ais.cmd.Process.Pid
|
||||
ais.logger.Info().Int("pid", pid).Msg("Stopping audio input server subprocess")
|
||||
ais.logger.Info().Int("pid", ais.cmd.Process.Pid).Msg("Stopping audio input server subprocess")
|
||||
|
||||
// Send SIGTERM
|
||||
err := ais.cmd.Process.Signal(syscall.SIGTERM)
|
||||
|
|
@ -152,49 +107,19 @@ func (ais *AudioInputSupervisor) Stop() {
|
|||
|
||||
// Wait for graceful shutdown with timeout
|
||||
done := make(chan error, 1)
|
||||
var waitErr error
|
||||
go func() {
|
||||
waitErr = ais.cmd.Wait()
|
||||
done <- waitErr
|
||||
done <- ais.cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
if waitErr != nil {
|
||||
ais.logger.Info().Err(waitErr).Msg("Audio input server subprocess stopped with error")
|
||||
} else {
|
||||
ais.logger.Info().Msg("Audio input server subprocess stopped gracefully")
|
||||
}
|
||||
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")
|
||||
// Use a more robust approach to check if process is still alive
|
||||
if ais.cmd != nil && ais.cmd.Process != nil {
|
||||
// Try to send signal 0 to check if process exists
|
||||
if err := ais.cmd.Process.Signal(syscall.Signal(0)); err == nil {
|
||||
// Process is still alive, force kill it
|
||||
if killErr := ais.cmd.Process.Kill(); killErr != nil {
|
||||
// Only log error if it's not "process already finished"
|
||||
if !strings.Contains(killErr.Error(), "process already finished") {
|
||||
ais.logger.Error().Err(killErr).Msg("Failed to kill audio input server subprocess")
|
||||
} else {
|
||||
ais.logger.Debug().Msg("Audio input server subprocess already finished during kill attempt")
|
||||
}
|
||||
} else {
|
||||
ais.logger.Info().Msg("Audio input server subprocess force killed successfully")
|
||||
}
|
||||
} else {
|
||||
ais.logger.Debug().Msg("Audio input server subprocess already finished")
|
||||
}
|
||||
// Wait a bit for the kill to take effect and collect the exit status
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
// Process finished
|
||||
case <-time.After(1 * time.Second):
|
||||
// Give up waiting
|
||||
}
|
||||
}()
|
||||
err := ais.cmd.Process.Kill()
|
||||
if err != nil {
|
||||
ais.logger.Error().Err(err).Msg("Failed to kill audio input server subprocess")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -253,7 +178,7 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
|
|||
ais.client.Disconnect()
|
||||
}
|
||||
|
||||
// Mark as not running first to prevent race conditions
|
||||
// Mark as not running
|
||||
ais.setRunning(false)
|
||||
ais.cmd = nil
|
||||
|
||||
|
|
@ -313,72 +238,3 @@ func (ais *AudioInputSupervisor) SendConfig(config InputIPCConfig) error {
|
|||
|
||||
return ais.client.SendConfig(config)
|
||||
}
|
||||
|
||||
// SendOpusConfig sends a complete Opus encoder configuration to the audio input server
|
||||
func (ais *AudioInputSupervisor) SendOpusConfig(config InputIPCOpusConfig) error {
|
||||
if ais.client == nil {
|
||||
return fmt.Errorf("client not initialized")
|
||||
}
|
||||
|
||||
if !ais.client.IsConnected() {
|
||||
return fmt.Errorf("client not connected")
|
||||
}
|
||||
|
||||
return ais.client.SendOpusConfig(config)
|
||||
}
|
||||
|
||||
// findExistingAudioInputProcess checks if there's already an audio input server process running
|
||||
func (ais *AudioInputSupervisor) findExistingAudioInputProcess() (int, error) {
|
||||
// Get current executable path
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
execName := filepath.Base(execPath)
|
||||
|
||||
// Use ps to find processes with our executable name and audio-input-server argument
|
||||
cmd := exec.Command("ps", "aux")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to run ps command: %w", err)
|
||||
}
|
||||
|
||||
// Parse ps output to find audio input server processes
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, execName) && strings.Contains(line, "--audio-input-server") {
|
||||
// Extract PID from ps output (second column)
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
if pid, err := strconv.Atoi(fields[1]); err == nil {
|
||||
// Verify the process is still running and accessible
|
||||
if ais.isProcessRunning(pid) {
|
||||
return pid, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no existing audio input server process found")
|
||||
}
|
||||
|
||||
// isProcessRunning checks if a process with the given PID is still running
|
||||
func (ais *AudioInputSupervisor) isProcessRunning(pid int) bool {
|
||||
// Try to send signal 0 to check if process exists
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
err = process.Signal(syscall.Signal(0))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// HasExistingProcess checks if there's already an audio input server process running
|
||||
// This is a public wrapper around findExistingAudioInputProcess for external access
|
||||
func (ais *AudioInputSupervisor) HasExistingProcess() (int, bool) {
|
||||
pid, err := ais.findExistingAudioInputProcess()
|
||||
return pid, err == nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,69 +4,18 @@ import (
|
|||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
)
|
||||
|
||||
// getEnvInt reads an integer from environment variable with a default value
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// parseOpusConfig reads OPUS configuration from environment variables
|
||||
// with fallback to default config values
|
||||
func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
||||
// Read configuration from environment variables with config defaults
|
||||
bitrate = getEnvInt("JETKVM_OPUS_BITRATE", GetConfig().CGOOpusBitrate)
|
||||
complexity = getEnvInt("JETKVM_OPUS_COMPLEXITY", GetConfig().CGOOpusComplexity)
|
||||
vbr = getEnvInt("JETKVM_OPUS_VBR", GetConfig().CGOOpusVBR)
|
||||
signalType = getEnvInt("JETKVM_OPUS_SIGNAL_TYPE", GetConfig().CGOOpusSignalType)
|
||||
bandwidth = getEnvInt("JETKVM_OPUS_BANDWIDTH", GetConfig().CGOOpusBandwidth)
|
||||
dtx = getEnvInt("JETKVM_OPUS_DTX", GetConfig().CGOOpusDTX)
|
||||
|
||||
return bitrate, complexity, vbr, signalType, bandwidth, dtx
|
||||
}
|
||||
|
||||
// applyOpusConfig applies OPUS configuration to the global config
|
||||
func applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
||||
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
|
||||
|
||||
config := GetConfig()
|
||||
config.CGOOpusBitrate = bitrate
|
||||
config.CGOOpusComplexity = complexity
|
||||
config.CGOOpusVBR = vbr
|
||||
config.CGOOpusSignalType = signalType
|
||||
config.CGOOpusBandwidth = bandwidth
|
||||
config.CGOOpusDTX = dtx
|
||||
|
||||
logger.Info().
|
||||
Int("bitrate", bitrate).
|
||||
Int("complexity", complexity).
|
||||
Int("vbr", vbr).
|
||||
Int("signal_type", signalType).
|
||||
Int("bandwidth", bandwidth).
|
||||
Int("dtx", dtx).
|
||||
Msg("applied OPUS configuration")
|
||||
}
|
||||
|
||||
// RunAudioOutputServer runs the audio output server subprocess
|
||||
// This should be called from main() when the subprocess is detected
|
||||
func RunAudioOutputServer() error {
|
||||
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
|
||||
logger.Debug().Msg("audio output server subprocess starting")
|
||||
|
||||
// Parse OPUS configuration from environment variables
|
||||
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfig()
|
||||
applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx)
|
||||
|
||||
// Initialize validation cache for optimal performance
|
||||
InitValidationCache()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
|
@ -312,18 +311,10 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
|||
return ErrAudioAlreadyRunning
|
||||
}
|
||||
|
||||
// Initialize CGO audio capture with retry logic
|
||||
var initErr error
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
if initErr = CGOAudioInit(); initErr == nil {
|
||||
break
|
||||
}
|
||||
getOutputStreamingLogger().Warn().Err(initErr).Int("attempt", attempt+1).Msg("Audio initialization failed, retrying")
|
||||
time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)
|
||||
}
|
||||
if initErr != nil {
|
||||
// Initialize CGO audio capture
|
||||
if err := CGOAudioInit(); err != nil {
|
||||
atomic.StoreInt32(&outputStreamingRunning, 0)
|
||||
return fmt.Errorf("failed to initialize audio after 3 attempts: %w", initErr)
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
|
@ -350,7 +341,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
|||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
// Capture audio frame with enhanced error handling and initialization checking
|
||||
// Capture audio frame with enhanced error handling
|
||||
n, err := CGOAudioReadEncode(buffer)
|
||||
if err != nil {
|
||||
consecutiveErrors++
|
||||
|
|
@ -359,13 +350,6 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
|||
Int("consecutive_errors", consecutiveErrors).
|
||||
Msg("Failed to read/encode audio")
|
||||
|
||||
// Check if this is an initialization error (C error code -1)
|
||||
if strings.Contains(err.Error(), "C error code -1") {
|
||||
getOutputStreamingLogger().Error().Msg("Audio system not initialized properly, forcing reinitialization")
|
||||
// Force immediate reinitialization for init errors
|
||||
consecutiveErrors = maxConsecutiveErrors
|
||||
}
|
||||
|
||||
// Implement progressive backoff for consecutive errors
|
||||
if consecutiveErrors >= maxConsecutiveErrors {
|
||||
getOutputStreamingLogger().Error().
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
|
@ -43,9 +42,6 @@ type AudioOutputSupervisor struct {
|
|||
stopChanClosed bool // Track if stopChan is closed
|
||||
processDoneClosed bool // Track if processDone is closed
|
||||
|
||||
// Environment variables for OPUS configuration
|
||||
opusEnv []string
|
||||
|
||||
// Callbacks
|
||||
onProcessStart func(pid int)
|
||||
onProcessExit func(pid int, exitCode int, crashed bool)
|
||||
|
|
@ -76,23 +72,6 @@ func (s *AudioOutputSupervisor) SetCallbacks(
|
|||
s.onRestart = onRestart
|
||||
}
|
||||
|
||||
// SetOpusConfig sets OPUS configuration parameters as environment variables
|
||||
// for the audio output subprocess
|
||||
func (s *AudioOutputSupervisor) SetOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
// Store OPUS parameters as environment variables
|
||||
s.opusEnv = []string{
|
||||
"JETKVM_OPUS_BITRATE=" + strconv.Itoa(bitrate),
|
||||
"JETKVM_OPUS_COMPLEXITY=" + strconv.Itoa(complexity),
|
||||
"JETKVM_OPUS_VBR=" + strconv.Itoa(vbr),
|
||||
"JETKVM_OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType),
|
||||
"JETKVM_OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth),
|
||||
"JETKVM_OPUS_DTX=" + strconv.Itoa(dtx),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins supervising the audio output server process
|
||||
func (s *AudioOutputSupervisor) Start() error {
|
||||
if !atomic.CompareAndSwapInt32(&s.running, 0, 1) {
|
||||
|
|
@ -244,24 +223,18 @@ func (s *AudioOutputSupervisor) startProcess() error {
|
|||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
// Build command arguments (only subprocess flag)
|
||||
args := []string{"--audio-output-server"}
|
||||
|
||||
// Create new command
|
||||
s.cmd = exec.CommandContext(s.ctx, execPath, args...)
|
||||
s.cmd = exec.CommandContext(s.ctx, execPath, "--audio-output-server")
|
||||
s.cmd.Stdout = os.Stdout
|
||||
s.cmd.Stderr = os.Stderr
|
||||
|
||||
// Set environment variables for OPUS configuration
|
||||
s.cmd.Env = append(os.Environ(), s.opusEnv...)
|
||||
|
||||
// Start the process
|
||||
if err := s.cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start audio output server process: %w", err)
|
||||
}
|
||||
|
||||
s.processPID = s.cmd.Process.Pid
|
||||
s.logger.Info().Int("pid", s.processPID).Strs("args", args).Strs("opus_env", s.opusEnv).Msg("audio server process started")
|
||||
s.logger.Info().Int("pid", s.processPID).Msg("audio server process started")
|
||||
|
||||
// Add process to monitoring
|
||||
s.processMonitor.AddProcess(s.processPID, "audio-output-server")
|
||||
|
|
|
|||
19
main.go
19
main.go
|
|
@ -44,25 +44,6 @@ func startAudioSubprocess() error {
|
|||
// Set the global supervisor for access from audio package
|
||||
audio.SetAudioOutputSupervisor(audioSupervisor)
|
||||
|
||||
// Create and register audio input supervisor (but don't start it)
|
||||
// Audio input will be started on-demand through the UI
|
||||
audioInputSupervisor := audio.NewAudioInputSupervisor()
|
||||
audio.SetAudioInputSupervisor(audioInputSupervisor)
|
||||
|
||||
// Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106)
|
||||
config := audio.GetConfig()
|
||||
audioInputSupervisor.SetOpusConfig(
|
||||
config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
|
||||
config.AudioQualityLowOpusComplexity,
|
||||
config.AudioQualityLowOpusVBR,
|
||||
config.AudioQualityLowOpusSignalType,
|
||||
config.AudioQualityLowOpusBandwidth,
|
||||
config.AudioQualityLowOpusDTX,
|
||||
)
|
||||
|
||||
// Note: Audio input supervisor is NOT started here - it will be started on-demand
|
||||
// when the user activates microphone input through the UI
|
||||
|
||||
// Set up callbacks for process lifecycle events
|
||||
audioSupervisor.SetCallbacks(
|
||||
// onProcessStart
|
||||
|
|
|
|||
Loading…
Reference in New Issue