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")
|
versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit")
|
||||||
audioServerPtr := flag.Bool("audio-output-server", false, "Run as audio server subprocess")
|
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")
|
audioInputServerPtr := flag.Bool("audio-input-server", false, "Run as audio input server subprocess")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *versionPtr || *versionJsonPtr {
|
if *versionPtr || *versionJsonPtr {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// Global audio output supervisor instance
|
||||||
globalOutputSupervisor unsafe.Pointer // *AudioOutputSupervisor
|
globalOutputSupervisor unsafe.Pointer // *AudioOutputSupervisor
|
||||||
globalInputSupervisor unsafe.Pointer // *AudioInputSupervisor
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// isAudioServerProcess detects if we're running as the audio server subprocess
|
// isAudioServerProcess detects if we're running as the audio server subprocess
|
||||||
|
|
@ -70,17 +70,3 @@ func GetAudioOutputSupervisor() *AudioOutputSupervisor {
|
||||||
}
|
}
|
||||||
return (*AudioOutputSupervisor)(ptr)
|
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.
|
// Package audio provides real-time audio processing for JetKVM with low-latency streaming.
|
||||||
//
|
//
|
||||||
// Key components: output/input pipelines with Opus codec, adaptive buffer management,
|
// 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 {
|
if config, exists := presets[quality]; exists {
|
||||||
currentConfig = config
|
currentConfig = config
|
||||||
|
|
||||||
// Get OPUS encoder parameters based on quality
|
// Update CGO OPUS encoder parameters based on quality
|
||||||
var complexity, vbr, signalType, bandwidth, dtx int
|
var complexity, vbr, signalType, bandwidth, dtx int
|
||||||
switch quality {
|
switch quality {
|
||||||
case AudioQualityLow:
|
case AudioQualityLow:
|
||||||
|
|
@ -206,29 +203,13 @@ func SetAudioQuality(quality AudioQuality) {
|
||||||
dtx = GetConfig().AudioQualityMediumOpusDTX
|
dtx = GetConfig().AudioQualityMediumOpusDTX
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart audio output subprocess with new OPUS configuration
|
// Dynamically update CGO OPUS encoder parameters
|
||||||
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
|
// Use current VBR constraint setting from config
|
||||||
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
|
|
||||||
vbrConstraint := GetConfig().CGOOpusVBRConstraint
|
vbrConstraint := GetConfig().CGOOpusVBRConstraint
|
||||||
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
|
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
|
||||||
logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
|
logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAudioConfig returns the current audio configuration
|
// GetAudioConfig returns the current audio configuration
|
||||||
|
|
@ -249,81 +230,6 @@ func SetMicrophoneQuality(quality AudioQuality) {
|
||||||
presets := GetMicrophoneQualityPresets()
|
presets := GetMicrophoneQualityPresets()
|
||||||
if config, exists := presets[quality]; exists {
|
if config, exists := presets[quality]; exists {
|
||||||
currentMicrophoneConfig = config
|
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"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Lock-free buffer cache for per-goroutine optimization
|
// Lock-free buffer cache for per-goroutine optimization
|
||||||
|
|
@ -16,9 +18,6 @@ type lockFreeBufferCache struct {
|
||||||
// Per-goroutine buffer cache using goroutine-local storage
|
// Per-goroutine buffer cache using goroutine-local storage
|
||||||
var goroutineBufferCache = make(map[int64]*lockFreeBufferCache)
|
var goroutineBufferCache = make(map[int64]*lockFreeBufferCache)
|
||||||
var goroutineCacheMutex sync.RWMutex
|
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
|
// getGoroutineID extracts goroutine ID from runtime stack for cache key
|
||||||
func getGoroutineID() int64 {
|
func getGoroutineID() int64 {
|
||||||
|
|
@ -39,41 +38,6 @@ func getGoroutineID() int64 {
|
||||||
return 0
|
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 {
|
type AudioBufferPool struct {
|
||||||
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
|
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
|
||||||
currentSize int64 // Current pool size (atomic)
|
currentSize int64 // Current pool size (atomic)
|
||||||
|
|
@ -93,7 +57,9 @@ type AudioBufferPool struct {
|
||||||
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
||||||
// Validate buffer size parameter
|
// Validate buffer size parameter
|
||||||
if err := ValidateBufferSize(bufferSize); err != nil {
|
if err := ValidateBufferSize(bufferSize); err != nil {
|
||||||
// Use default value on validation error
|
// 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
|
bufferSize = GetConfig().AudioFramePoolSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,9 +99,6 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AudioBufferPool) Get() []byte {
|
func (p *AudioBufferPool) Get() []byte {
|
||||||
// Trigger periodic cleanup of goroutine cache
|
|
||||||
cleanupGoroutineCache()
|
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
wasHit := false
|
wasHit := false
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
||||||
|
|
@ -709,12 +709,6 @@ func cgoAudioReadEncode(buf []byte) (int, error) {
|
||||||
return 0, newBufferTooSmallError(len(buf), minRequired)
|
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]))
|
n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0]))
|
||||||
if n < 0 {
|
if n < 0 {
|
||||||
return 0, newAudioReadEncodeError(int(n))
|
return 0, newAudioReadEncodeError(int(n))
|
||||||
|
|
|
||||||
|
|
@ -1554,36 +1554,35 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
FrameSize: 960,
|
FrameSize: 960,
|
||||||
MaxPacketSize: 4000,
|
MaxPacketSize: 4000,
|
||||||
|
|
||||||
// Audio Quality Bitrates - Optimized for RV1106 SoC and KVM layer compatibility
|
// Audio Quality Bitrates
|
||||||
// Reduced bitrates to minimize CPU load and prevent mouse lag
|
|
||||||
AudioQualityLowOutputBitrate: 32,
|
AudioQualityLowOutputBitrate: 32,
|
||||||
AudioQualityLowInputBitrate: 16,
|
AudioQualityLowInputBitrate: 16,
|
||||||
AudioQualityMediumOutputBitrate: 48,
|
AudioQualityMediumOutputBitrate: 64,
|
||||||
AudioQualityMediumInputBitrate: 24,
|
AudioQualityMediumInputBitrate: 32,
|
||||||
|
|
||||||
// AudioQualityHighOutputBitrate defines bitrate for high-quality output.
|
// AudioQualityHighOutputBitrate defines bitrate for high-quality output.
|
||||||
// Used in: Professional applications requiring good audio fidelity on RV1106
|
// Used in: Professional applications requiring excellent audio fidelity
|
||||||
// Impact: Balanced quality optimized for single-core ARM performance.
|
// Impact: Provides excellent quality but increases bandwidth usage.
|
||||||
// Reduced to 64kbps for RV1106 compatibility and minimal CPU overhead.
|
// Default 128kbps matches professional Opus encoding standards.
|
||||||
AudioQualityHighOutputBitrate: 64,
|
AudioQualityHighOutputBitrate: 128,
|
||||||
|
|
||||||
// AudioQualityHighInputBitrate defines bitrate for high-quality input.
|
// AudioQualityHighInputBitrate defines bitrate for high-quality input.
|
||||||
// Used in: High-quality microphone input optimized for RV1106
|
// Used in: High-quality microphone input for professional use
|
||||||
// Impact: Clear voice reproduction without overwhelming single-core CPU.
|
// Impact: Ensures clear voice reproduction for professional scenarios.
|
||||||
// Reduced to 32kbps for optimal RV1106 performance without lag.
|
// Default 64kbps provides excellent voice quality.
|
||||||
AudioQualityHighInputBitrate: 32,
|
AudioQualityHighInputBitrate: 64,
|
||||||
|
|
||||||
// AudioQualityUltraOutputBitrate defines bitrate for ultra-quality output.
|
// AudioQualityUltraOutputBitrate defines bitrate for ultra-quality output.
|
||||||
// Used in: Maximum quality while ensuring RV1106 stability
|
// Used in: Audiophile-grade reproduction and high-bandwidth connections
|
||||||
// Impact: Best possible quality without interfering with KVM operations.
|
// Impact: Maximum quality but requires significant bandwidth.
|
||||||
// Reduced to 96kbps for RV1106 maximum performance without mouse lag.
|
// Default 192kbps suitable for high-bandwidth, quality-critical scenarios.
|
||||||
AudioQualityUltraOutputBitrate: 96,
|
AudioQualityUltraOutputBitrate: 192,
|
||||||
|
|
||||||
// AudioQualityUltraInputBitrate defines bitrate for ultra-quality input.
|
// AudioQualityUltraInputBitrate defines bitrate for ultra-quality input.
|
||||||
// Used in: Premium microphone input optimized for RV1106 constraints
|
// Used in: Professional microphone input requiring maximum quality
|
||||||
// Impact: Excellent voice quality within RV1106 processing limits.
|
// Impact: Provides audiophile-grade voice quality with high bandwidth.
|
||||||
// Reduced to 48kbps for stable RV1106 operation without lag.
|
// Default 96kbps ensures maximum voice reproduction quality.
|
||||||
AudioQualityUltraInputBitrate: 48,
|
AudioQualityUltraInputBitrate: 96,
|
||||||
|
|
||||||
// Audio Quality Sample Rates - Sampling frequencies for different quality levels
|
// Audio Quality Sample Rates - Sampling frequencies for different quality levels
|
||||||
// Used in: Audio capture, processing, and format negotiation
|
// Used in: Audio capture, processing, and format negotiation
|
||||||
|
|
@ -1591,15 +1590,15 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
|
|
||||||
// AudioQualityLowSampleRate defines sampling frequency for low-quality audio.
|
// AudioQualityLowSampleRate defines sampling frequency for low-quality audio.
|
||||||
// Used in: Bandwidth-constrained scenarios and basic audio requirements
|
// Used in: Bandwidth-constrained scenarios and basic audio requirements
|
||||||
// Impact: Captures frequencies up to 24kHz while maintaining efficiency.
|
// Impact: Captures frequencies up to 11kHz while minimizing processing load.
|
||||||
// Default 48kHz provides better quality while maintaining compatibility.
|
// Default 22.05kHz sufficient for speech and basic audio.
|
||||||
AudioQualityLowSampleRate: 48000,
|
AudioQualityLowSampleRate: 22050,
|
||||||
|
|
||||||
// AudioQualityMediumSampleRate defines sampling frequency for medium-quality audio.
|
// AudioQualityMediumSampleRate defines sampling frequency for medium-quality audio.
|
||||||
// Used in: Standard audio scenarios requiring high-quality reproduction
|
// Used in: Standard audio scenarios requiring CD-quality reproduction
|
||||||
// Impact: Captures full audible range up to 24kHz with excellent processing.
|
// Impact: Captures full audible range up to 22kHz with balanced processing.
|
||||||
// Default 48kHz provides professional standard with optimal balance.
|
// Default 44.1kHz provides CD-quality standard with excellent balance.
|
||||||
AudioQualityMediumSampleRate: 48000,
|
AudioQualityMediumSampleRate: 44100,
|
||||||
|
|
||||||
// AudioQualityMicLowSampleRate defines sampling frequency for low-quality microphone.
|
// AudioQualityMicLowSampleRate defines sampling frequency for low-quality microphone.
|
||||||
// Used in: Voice/microphone input in bandwidth-constrained scenarios
|
// Used in: Voice/microphone input in bandwidth-constrained scenarios
|
||||||
|
|
@ -1612,80 +1611,80 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
// Impact: Controls latency vs processing efficiency trade-offs
|
// Impact: Controls latency vs processing efficiency trade-offs
|
||||||
|
|
||||||
// AudioQualityLowFrameSize defines frame duration for low-quality audio.
|
// AudioQualityLowFrameSize defines frame duration for low-quality audio.
|
||||||
// Used in: RV1106 efficiency-prioritized scenarios
|
// Used in: Bandwidth-constrained scenarios prioritizing efficiency
|
||||||
// Impact: Balanced frame size for quality and efficiency.
|
// Impact: Reduces processing overhead with acceptable latency increase.
|
||||||
// Reduced to 20ms for better responsiveness and reduced audio saccades.
|
// Default 40ms provides efficiency for constrained scenarios.
|
||||||
AudioQualityLowFrameSize: 20 * time.Millisecond,
|
AudioQualityLowFrameSize: 40 * time.Millisecond,
|
||||||
|
|
||||||
// AudioQualityMediumFrameSize defines frame duration for medium-quality audio.
|
// AudioQualityMediumFrameSize defines frame duration for medium-quality audio.
|
||||||
// Used in: Balanced RV1106 real-time audio applications
|
// Used in: Standard real-time audio applications
|
||||||
// Impact: Balances latency and processing efficiency for RV1106.
|
// Impact: Provides good balance of latency and processing efficiency.
|
||||||
// Optimized to 20ms for RV1106 balanced performance.
|
// Default 20ms standard for real-time audio applications.
|
||||||
AudioQualityMediumFrameSize: 20 * time.Millisecond,
|
AudioQualityMediumFrameSize: 20 * time.Millisecond,
|
||||||
|
|
||||||
// AudioQualityHighFrameSize defines frame duration for high-quality audio.
|
// AudioQualityHighFrameSize defines frame duration for high-quality audio.
|
||||||
// Used in: RV1106 high-quality scenarios with performance constraints
|
// Used in: High-quality audio scenarios with balanced requirements
|
||||||
// Impact: Maintains acceptable latency while reducing RV1106 CPU load.
|
// Impact: Maintains good latency while ensuring quality processing.
|
||||||
// Optimized to 20ms for RV1106 high-quality balance.
|
// Default 20ms provides optimal balance for high-quality scenarios.
|
||||||
AudioQualityHighFrameSize: 20 * time.Millisecond,
|
AudioQualityHighFrameSize: 20 * time.Millisecond,
|
||||||
|
|
||||||
// AudioQualityUltraFrameSize defines frame duration for ultra-quality audio.
|
// AudioQualityUltraFrameSize defines frame duration for ultra-quality audio.
|
||||||
// Used in: Maximum RV1106 performance without KVM interference
|
// Used in: Applications requiring immediate audio feedback
|
||||||
// Impact: Balances quality and processing efficiency for RV1106 stability.
|
// Impact: Minimizes latency for ultra-responsive audio processing.
|
||||||
// Optimized to 20ms for RV1106 maximum stable performance.
|
// Default 10ms ensures minimal latency for immediate feedback.
|
||||||
AudioQualityUltraFrameSize: 20 * time.Millisecond,
|
AudioQualityUltraFrameSize: 10 * time.Millisecond,
|
||||||
|
|
||||||
// Audio Quality Channels - Optimized for RV1106 processing efficiency
|
// Audio Quality Channels - Channel configuration for different quality levels
|
||||||
// Used in: Audio processing pipeline optimized for single-core ARM performance
|
// Used in: Audio processing pipeline for channel handling and bandwidth control
|
||||||
AudioQualityLowChannels: 1, // Mono for minimal RV1106 processing
|
AudioQualityLowChannels: 1,
|
||||||
AudioQualityMediumChannels: 2, // Stereo for balanced RV1106 performance
|
AudioQualityMediumChannels: 2,
|
||||||
AudioQualityHighChannels: 2, // Stereo for RV1106 high-quality scenarios
|
AudioQualityHighChannels: 2,
|
||||||
AudioQualityUltraChannels: 2, // Stereo for maximum RV1106 performance
|
AudioQualityUltraChannels: 2,
|
||||||
|
|
||||||
// Audio Quality OPUS Encoder Parameters - Quality-specific encoder settings
|
// Audio Quality OPUS Encoder Parameters - Quality-specific encoder settings
|
||||||
// Used in: Dynamic OPUS encoder configuration based on quality presets
|
// Used in: Dynamic OPUS encoder configuration based on quality presets
|
||||||
// Impact: Controls encoding complexity, VBR, signal type, bandwidth, and DTX
|
// Impact: Controls encoding complexity, VBR, signal type, bandwidth, and DTX
|
||||||
|
|
||||||
// Low Quality OPUS Parameters - Optimized for RV1106 minimal CPU usage
|
// Low Quality OPUS Parameters - Optimized for bandwidth conservation
|
||||||
AudioQualityLowOpusComplexity: 0, // Minimum complexity to reduce CPU load
|
AudioQualityLowOpusComplexity: 1, // Low complexity for minimal CPU usage
|
||||||
AudioQualityLowOpusVBR: 1, // VBR for better quality at same bitrate
|
AudioQualityLowOpusVBR: 0, // CBR for predictable bandwidth
|
||||||
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for lower complexity
|
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE
|
||||||
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND for efficiency
|
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND
|
||||||
AudioQualityLowOpusDTX: 1, // Enable DTX to reduce processing when silent
|
AudioQualityLowOpusDTX: 1, // Enable DTX for silence suppression
|
||||||
|
|
||||||
// Medium Quality OPUS Parameters - Balanced for RV1106 performance
|
// Medium Quality OPUS Parameters - Balanced performance and quality
|
||||||
AudioQualityMediumOpusComplexity: 1, // Very low complexity for RV1106 stability
|
AudioQualityMediumOpusComplexity: 5, // Medium complexity for balanced performance
|
||||||
AudioQualityMediumOpusVBR: 1, // VBR for optimal quality
|
AudioQualityMediumOpusVBR: 1, // VBR for better quality
|
||||||
AudioQualityMediumOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for efficiency
|
AudioQualityMediumOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
||||||
AudioQualityMediumOpusBandwidth: 1102, // OPUS_BANDWIDTH_MEDIUMBAND for balance
|
AudioQualityMediumOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND
|
||||||
AudioQualityMediumOpusDTX: 1, // Enable DTX for CPU savings
|
AudioQualityMediumOpusDTX: 0, // Disable DTX for consistent quality
|
||||||
|
|
||||||
// High Quality OPUS Parameters - Optimized for RV1106 high performance
|
// High Quality OPUS Parameters - High quality with good performance
|
||||||
AudioQualityHighOpusComplexity: 2, // Low complexity for RV1106 limits
|
AudioQualityHighOpusComplexity: 8, // High complexity for better quality
|
||||||
AudioQualityHighOpusVBR: 1, // VBR for optimal quality
|
AudioQualityHighOpusVBR: 1, // VBR for optimal quality
|
||||||
AudioQualityHighOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
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
|
AudioQualityHighOpusDTX: 0, // Disable DTX for consistent quality
|
||||||
|
|
||||||
// Ultra Quality OPUS Parameters - Maximum RV1106 performance without KVM interference
|
// Ultra Quality OPUS Parameters - Maximum quality settings
|
||||||
AudioQualityUltraOpusComplexity: 3, // Moderate complexity for RV1106 stability
|
AudioQualityUltraOpusComplexity: 10, // Maximum complexity for best quality
|
||||||
AudioQualityUltraOpusVBR: 1, // VBR for optimal quality
|
AudioQualityUltraOpusVBR: 1, // VBR for optimal quality
|
||||||
AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
||||||
AudioQualityUltraOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for stability
|
AudioQualityUltraOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
|
||||||
AudioQualityUltraOpusDTX: 0, // Disable DTX for maximum quality
|
AudioQualityUltraOpusDTX: 0, // Disable DTX for maximum quality
|
||||||
|
|
||||||
// CGO Audio Constants - Optimized for RV1106 native audio processing
|
// CGO Audio Constants
|
||||||
CGOOpusBitrate: 64000, // Reduced for RV1106 efficiency
|
CGOOpusBitrate: 96000,
|
||||||
CGOOpusComplexity: 2, // Minimal complexity for RV1106
|
CGOOpusComplexity: 3,
|
||||||
CGOOpusVBR: 1,
|
CGOOpusVBR: 1,
|
||||||
CGOOpusVBRConstraint: 1,
|
CGOOpusVBRConstraint: 1,
|
||||||
CGOOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
CGOOpusSignalType: 3, // OPUS_SIGNAL_MUSIC
|
||||||
CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for RV1106
|
CGOOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
|
||||||
CGOOpusDTX: 0,
|
CGOOpusDTX: 0,
|
||||||
CGOSampleRate: 48000,
|
CGOSampleRate: 48000,
|
||||||
CGOChannels: 2,
|
CGOChannels: 2,
|
||||||
CGOFrameSize: 960,
|
CGOFrameSize: 960,
|
||||||
CGOMaxPacketSize: 1200, // Reduced for RV1106 memory efficiency
|
CGOMaxPacketSize: 1500,
|
||||||
|
|
||||||
// Input IPC Constants
|
// Input IPC Constants
|
||||||
// InputIPCSampleRate defines sample rate for input IPC operations.
|
// InputIPCSampleRate defines sample rate for input IPC operations.
|
||||||
|
|
|
||||||
|
|
@ -208,30 +208,7 @@ func (aim *AudioInputManager) LogPerformanceStats() {
|
||||||
Msg("Audio input performance metrics")
|
Msg("Audio input performance metrics")
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRunning returns whether the audio input manager is running
|
// Note: IsRunning() is inherited from BaseAudioManager
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsReady returns whether the audio input manager is ready to receive frames
|
// 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
|
// This checks both that it's running and that the IPC connection is established
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ type InputMessageType uint8
|
||||||
const (
|
const (
|
||||||
InputMessageTypeOpusFrame InputMessageType = iota
|
InputMessageTypeOpusFrame InputMessageType = iota
|
||||||
InputMessageTypeConfig
|
InputMessageTypeConfig
|
||||||
InputMessageTypeOpusConfig
|
|
||||||
InputMessageTypeStop
|
InputMessageTypeStop
|
||||||
InputMessageTypeHeartbeat
|
InputMessageTypeHeartbeat
|
||||||
InputMessageTypeAck
|
InputMessageTypeAck
|
||||||
|
|
@ -204,19 +203,6 @@ type InputIPCConfig struct {
|
||||||
FrameSize int
|
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
|
// AudioInputServer handles IPC communication for audio input processing
|
||||||
type AudioInputServer struct {
|
type AudioInputServer struct {
|
||||||
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
|
// 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)
|
return ais.processOpusFrame(msg.Data)
|
||||||
case InputMessageTypeConfig:
|
case InputMessageTypeConfig:
|
||||||
return ais.processConfig(msg.Data)
|
return ais.processConfig(msg.Data)
|
||||||
case InputMessageTypeOpusConfig:
|
|
||||||
return ais.processOpusConfig(msg.Data)
|
|
||||||
case InputMessageTypeStop:
|
case InputMessageTypeStop:
|
||||||
return fmt.Errorf("stop message received")
|
return fmt.Errorf("stop message received")
|
||||||
case InputMessageTypeHeartbeat:
|
case InputMessageTypeHeartbeat:
|
||||||
|
|
@ -523,50 +507,6 @@ func (ais *AudioInputServer) processConfig(data []byte) error {
|
||||||
return ais.sendAck()
|
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
|
// sendAck sends an acknowledgment message
|
||||||
func (ais *AudioInputServer) sendAck() error {
|
func (ais *AudioInputServer) sendAck() error {
|
||||||
ais.mtx.Lock()
|
ais.mtx.Lock()
|
||||||
|
|
@ -785,44 +725,6 @@ func (aic *AudioInputClient) SendConfig(config InputIPCConfig) error {
|
||||||
return aic.writeMessage(msg)
|
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
|
// SendHeartbeat sends a heartbeat message
|
||||||
func (aic *AudioInputClient) SendHeartbeat() error {
|
func (aic *AudioInputClient) SendHeartbeat() error {
|
||||||
aic.mtx.Lock()
|
aic.mtx.Lock()
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,21 @@
|
||||||
//go:build cgo
|
|
||||||
// +build cgo
|
|
||||||
|
|
||||||
package audio
|
package audio
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo pkg-config: alsa
|
|
||||||
#cgo LDFLAGS: -lopus
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"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
|
// RunAudioInputServer runs the audio input server subprocess
|
||||||
// This should be called from main() when the subprocess is detected
|
// This should be called from main() when the subprocess is detected
|
||||||
func RunAudioInputServer() error {
|
func RunAudioInputServer() error {
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
|
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
|
||||||
logger.Debug().Msg("audio input server subprocess starting")
|
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
|
// Initialize validation cache for optimal performance
|
||||||
InitValidationCache()
|
InitValidationCache()
|
||||||
|
|
||||||
|
|
@ -72,16 +23,13 @@ func RunAudioInputServer() error {
|
||||||
StartAdaptiveBuffering()
|
StartAdaptiveBuffering()
|
||||||
defer StopAdaptiveBuffering()
|
defer StopAdaptiveBuffering()
|
||||||
|
|
||||||
// Initialize CGO audio playback (optional for input server)
|
// Initialize CGO audio system
|
||||||
// This is used for audio loopback/monitoring features
|
|
||||||
err := CGOAudioPlaybackInit()
|
err := CGOAudioPlaybackInit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to initialize CGO audio playback - audio monitoring disabled")
|
logger.Error().Err(err).Msg("failed to initialize CGO audio playback")
|
||||||
// Continue without playback - input functionality doesn't require it
|
return err
|
||||||
} else {
|
|
||||||
defer CGOAudioPlaybackClose()
|
|
||||||
logger.Debug().Msg("CGO audio playback initialized successfully")
|
|
||||||
}
|
}
|
||||||
|
defer CGOAudioPlaybackClose()
|
||||||
|
|
||||||
// Create and start the IPC server
|
// Create and start the IPC server
|
||||||
server, err := NewAudioInputServer()
|
server, err := NewAudioInputServer()
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
//go:build cgo
|
|
||||||
// +build cgo
|
|
||||||
|
|
||||||
package audio
|
package audio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -18,9 +12,6 @@ import (
|
||||||
type AudioInputSupervisor struct {
|
type AudioInputSupervisor struct {
|
||||||
*BaseSupervisor
|
*BaseSupervisor
|
||||||
client *AudioInputClient
|
client *AudioInputClient
|
||||||
|
|
||||||
// Environment variables for OPUS configuration
|
|
||||||
opusEnv []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAudioInputSupervisor creates a new audio input supervisor
|
// 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
|
// Start starts the audio input server subprocess
|
||||||
func (ais *AudioInputSupervisor) Start() error {
|
func (ais *AudioInputSupervisor) Start() error {
|
||||||
ais.mutex.Lock()
|
ais.mutex.Lock()
|
||||||
defer ais.mutex.Unlock()
|
defer ais.mutex.Unlock()
|
||||||
|
|
||||||
if ais.IsRunning() {
|
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 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
|
// Create context for subprocess management
|
||||||
ais.createContext()
|
ais.createContext()
|
||||||
|
|
@ -79,16 +40,11 @@ func (ais *AudioInputSupervisor) Start() error {
|
||||||
return fmt.Errorf("failed to get executable path: %w", err)
|
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
|
// Create command for audio input server subprocess
|
||||||
cmd := exec.CommandContext(ais.ctx, execPath, args...)
|
cmd := exec.CommandContext(ais.ctx, execPath, "--audio-input-server")
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
// Set environment variables for IPC and OPUS configuration
|
"JETKVM_AUDIO_INPUT_IPC=true", // Enable IPC mode
|
||||||
env := append(os.Environ(), "JETKVM_AUDIO_INPUT_IPC=true") // Enable IPC mode
|
)
|
||||||
env = append(env, ais.opusEnv...) // Add OPUS configuration
|
|
||||||
cmd.Env = env
|
|
||||||
|
|
||||||
// Set process group to allow clean termination
|
// Set process group to allow clean termination
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
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)
|
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
|
// Add process to monitoring
|
||||||
ais.processMonitor.AddProcess(cmd.Process.Pid, "audio-input-server")
|
ais.processMonitor.AddProcess(cmd.Process.Pid, "audio-input-server")
|
||||||
|
|
@ -141,8 +97,7 @@ func (ais *AudioInputSupervisor) Stop() {
|
||||||
|
|
||||||
// Try graceful termination first
|
// Try graceful termination first
|
||||||
if ais.cmd != nil && ais.cmd.Process != nil {
|
if ais.cmd != nil && ais.cmd.Process != nil {
|
||||||
pid := ais.cmd.Process.Pid
|
ais.logger.Info().Int("pid", ais.cmd.Process.Pid).Msg("Stopping audio input server subprocess")
|
||||||
ais.logger.Info().Int("pid", pid).Msg("Stopping audio input server subprocess")
|
|
||||||
|
|
||||||
// Send SIGTERM
|
// Send SIGTERM
|
||||||
err := ais.cmd.Process.Signal(syscall.SIGTERM)
|
err := ais.cmd.Process.Signal(syscall.SIGTERM)
|
||||||
|
|
@ -152,49 +107,19 @@ func (ais *AudioInputSupervisor) Stop() {
|
||||||
|
|
||||||
// Wait for graceful shutdown with timeout
|
// Wait for graceful shutdown with timeout
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
var waitErr error
|
|
||||||
go func() {
|
go func() {
|
||||||
waitErr = ais.cmd.Wait()
|
done <- ais.cmd.Wait()
|
||||||
done <- waitErr
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-done:
|
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")
|
ais.logger.Info().Msg("Audio input server subprocess stopped gracefully")
|
||||||
}
|
|
||||||
case <-time.After(GetConfig().InputSupervisorTimeout):
|
case <-time.After(GetConfig().InputSupervisorTimeout):
|
||||||
// Force kill if graceful shutdown failed
|
// Force kill if graceful shutdown failed
|
||||||
ais.logger.Warn().Msg("Audio input server subprocess did not stop gracefully, force killing")
|
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
|
err := ais.cmd.Process.Kill()
|
||||||
if ais.cmd != nil && ais.cmd.Process != nil {
|
if err != nil {
|
||||||
// Try to send signal 0 to check if process exists
|
ais.logger.Error().Err(err).Msg("Failed to kill audio input server subprocess")
|
||||||
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
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +178,7 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
|
||||||
ais.client.Disconnect()
|
ais.client.Disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as not running first to prevent race conditions
|
// Mark as not running
|
||||||
ais.setRunning(false)
|
ais.setRunning(false)
|
||||||
ais.cmd = nil
|
ais.cmd = nil
|
||||||
|
|
||||||
|
|
@ -313,72 +238,3 @@ func (ais *AudioInputSupervisor) SendConfig(config InputIPCConfig) error {
|
||||||
|
|
||||||
return ais.client.SendConfig(config)
|
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"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"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
|
// RunAudioOutputServer runs the audio output server subprocess
|
||||||
// This should be called from main() when the subprocess is detected
|
// This should be called from main() when the subprocess is detected
|
||||||
func RunAudioOutputServer() error {
|
func RunAudioOutputServer() error {
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
|
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
|
||||||
logger.Debug().Msg("audio output server subprocess starting")
|
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
|
// Initialize validation cache for optimal performance
|
||||||
InitValidationCache()
|
InitValidationCache()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -312,18 +311,10 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
return ErrAudioAlreadyRunning
|
return ErrAudioAlreadyRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize CGO audio capture with retry logic
|
// Initialize CGO audio capture
|
||||||
var initErr error
|
if err := CGOAudioInit(); err != nil {
|
||||||
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 {
|
|
||||||
atomic.StoreInt32(&outputStreamingRunning, 0)
|
atomic.StoreInt32(&outputStreamingRunning, 0)
|
||||||
return fmt.Errorf("failed to initialize audio after 3 attempts: %w", initErr)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
@ -350,7 +341,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
// Capture audio frame with enhanced error handling and initialization checking
|
// Capture audio frame with enhanced error handling
|
||||||
n, err := CGOAudioReadEncode(buffer)
|
n, err := CGOAudioReadEncode(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
consecutiveErrors++
|
consecutiveErrors++
|
||||||
|
|
@ -359,13 +350,6 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
Int("consecutive_errors", consecutiveErrors).
|
Int("consecutive_errors", consecutiveErrors).
|
||||||
Msg("Failed to read/encode audio")
|
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
|
// Implement progressive backoff for consecutive errors
|
||||||
if consecutiveErrors >= maxConsecutiveErrors {
|
if consecutiveErrors >= maxConsecutiveErrors {
|
||||||
getOutputStreamingLogger().Error().
|
getOutputStreamingLogger().Error().
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -43,9 +42,6 @@ type AudioOutputSupervisor struct {
|
||||||
stopChanClosed bool // Track if stopChan is closed
|
stopChanClosed bool // Track if stopChan is closed
|
||||||
processDoneClosed bool // Track if processDone is closed
|
processDoneClosed bool // Track if processDone is closed
|
||||||
|
|
||||||
// Environment variables for OPUS configuration
|
|
||||||
opusEnv []string
|
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
onProcessStart func(pid int)
|
onProcessStart func(pid int)
|
||||||
onProcessExit func(pid int, exitCode int, crashed bool)
|
onProcessExit func(pid int, exitCode int, crashed bool)
|
||||||
|
|
@ -76,23 +72,6 @@ func (s *AudioOutputSupervisor) SetCallbacks(
|
||||||
s.onRestart = onRestart
|
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
|
// Start begins supervising the audio output server process
|
||||||
func (s *AudioOutputSupervisor) Start() error {
|
func (s *AudioOutputSupervisor) Start() error {
|
||||||
if !atomic.CompareAndSwapInt32(&s.running, 0, 1) {
|
if !atomic.CompareAndSwapInt32(&s.running, 0, 1) {
|
||||||
|
|
@ -244,24 +223,18 @@ func (s *AudioOutputSupervisor) startProcess() error {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
// Build command arguments (only subprocess flag)
|
|
||||||
args := []string{"--audio-output-server"}
|
|
||||||
|
|
||||||
// Create new command
|
// 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.Stdout = os.Stdout
|
||||||
s.cmd.Stderr = os.Stderr
|
s.cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
// Set environment variables for OPUS configuration
|
|
||||||
s.cmd.Env = append(os.Environ(), s.opusEnv...)
|
|
||||||
|
|
||||||
// Start the process
|
// Start the process
|
||||||
if err := s.cmd.Start(); err != nil {
|
if err := s.cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("failed to start audio output server process: %w", err)
|
return fmt.Errorf("failed to start audio output server process: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.processPID = s.cmd.Process.Pid
|
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
|
// Add process to monitoring
|
||||||
s.processMonitor.AddProcess(s.processPID, "audio-output-server")
|
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
|
// Set the global supervisor for access from audio package
|
||||||
audio.SetAudioOutputSupervisor(audioSupervisor)
|
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
|
// Set up callbacks for process lifecycle events
|
||||||
audioSupervisor.SetCallbacks(
|
audioSupervisor.SetCallbacks(
|
||||||
// onProcessStart
|
// onProcessStart
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue