mirror of https://github.com/jetkvm/kvm.git
Compare commits
7 Commits
0d4176cf98
...
950ca2bd99
| Author | SHA1 | Date |
|---|---|---|
|
|
950ca2bd99 | |
|
|
dfbf9249b9 | |
|
|
f51f6da2de | |
|
|
fd7608384a | |
|
|
6adcc26ff2 | |
|
|
858859e317 | |
|
|
9c0aff4489 |
|
|
@ -13,6 +13,7 @@ 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,3 +70,17 @@ 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,3 +1,6 @@
|
||||||
|
//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,
|
||||||
|
|
@ -167,7 +170,7 @@ func SetAudioQuality(quality AudioQuality) {
|
||||||
if config, exists := presets[quality]; exists {
|
if config, exists := presets[quality]; exists {
|
||||||
currentConfig = config
|
currentConfig = config
|
||||||
|
|
||||||
// Update CGO OPUS encoder parameters based on quality
|
// Get 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:
|
||||||
|
|
@ -203,11 +206,27 @@ func SetAudioQuality(quality AudioQuality) {
|
||||||
dtx = GetConfig().AudioQualityMediumOpusDTX
|
dtx = GetConfig().AudioQualityMediumOpusDTX
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamically update CGO OPUS encoder parameters
|
// Restart audio output subprocess with new OPUS configuration
|
||||||
// Use current VBR constraint setting from config
|
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
|
||||||
vbrConstraint := GetConfig().CGOOpusVBRConstraint
|
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
||||||
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
|
logger.Info().Int("quality", int(quality)).Msg("restarting audio output subprocess with new quality settings")
|
||||||
logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
|
|
||||||
|
// 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
|
||||||
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +249,81 @@ 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,8 +6,6 @@ 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
|
||||||
|
|
@ -18,6 +16,9 @@ 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 {
|
||||||
|
|
@ -38,6 +39,41 @@ 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)
|
||||||
|
|
@ -57,9 +93,7 @@ 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 {
|
||||||
// Log validation error and use default value
|
// Use default value on validation error
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,6 +133,9 @@ 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,6 +709,12 @@ 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,35 +1554,36 @@ func DefaultAudioConfig() *AudioConfigConstants {
|
||||||
FrameSize: 960,
|
FrameSize: 960,
|
||||||
MaxPacketSize: 4000,
|
MaxPacketSize: 4000,
|
||||||
|
|
||||||
// Audio Quality Bitrates
|
// Audio Quality Bitrates - Optimized for RV1106 SoC and KVM layer compatibility
|
||||||
|
// Reduced bitrates to minimize CPU load and prevent mouse lag
|
||||||
AudioQualityLowOutputBitrate: 32,
|
AudioQualityLowOutputBitrate: 32,
|
||||||
AudioQualityLowInputBitrate: 16,
|
AudioQualityLowInputBitrate: 16,
|
||||||
AudioQualityMediumOutputBitrate: 64,
|
AudioQualityMediumOutputBitrate: 48,
|
||||||
AudioQualityMediumInputBitrate: 32,
|
AudioQualityMediumInputBitrate: 24,
|
||||||
|
|
||||||
// AudioQualityHighOutputBitrate defines bitrate for high-quality output.
|
// AudioQualityHighOutputBitrate defines bitrate for high-quality output.
|
||||||
// Used in: Professional applications requiring excellent audio fidelity
|
// Used in: Professional applications requiring good audio fidelity on RV1106
|
||||||
// Impact: Provides excellent quality but increases bandwidth usage.
|
// Impact: Balanced quality optimized for single-core ARM performance.
|
||||||
// Default 128kbps matches professional Opus encoding standards.
|
// Reduced to 64kbps for RV1106 compatibility and minimal CPU overhead.
|
||||||
AudioQualityHighOutputBitrate: 128,
|
AudioQualityHighOutputBitrate: 64,
|
||||||
|
|
||||||
// AudioQualityHighInputBitrate defines bitrate for high-quality input.
|
// AudioQualityHighInputBitrate defines bitrate for high-quality input.
|
||||||
// Used in: High-quality microphone input for professional use
|
// Used in: High-quality microphone input optimized for RV1106
|
||||||
// Impact: Ensures clear voice reproduction for professional scenarios.
|
// Impact: Clear voice reproduction without overwhelming single-core CPU.
|
||||||
// Default 64kbps provides excellent voice quality.
|
// Reduced to 32kbps for optimal RV1106 performance without lag.
|
||||||
AudioQualityHighInputBitrate: 64,
|
AudioQualityHighInputBitrate: 32,
|
||||||
|
|
||||||
// AudioQualityUltraOutputBitrate defines bitrate for ultra-quality output.
|
// AudioQualityUltraOutputBitrate defines bitrate for ultra-quality output.
|
||||||
// Used in: Audiophile-grade reproduction and high-bandwidth connections
|
// Used in: Maximum quality while ensuring RV1106 stability
|
||||||
// Impact: Maximum quality but requires significant bandwidth.
|
// Impact: Best possible quality without interfering with KVM operations.
|
||||||
// Default 192kbps suitable for high-bandwidth, quality-critical scenarios.
|
// Reduced to 96kbps for RV1106 maximum performance without mouse lag.
|
||||||
AudioQualityUltraOutputBitrate: 192,
|
AudioQualityUltraOutputBitrate: 96,
|
||||||
|
|
||||||
// AudioQualityUltraInputBitrate defines bitrate for ultra-quality input.
|
// AudioQualityUltraInputBitrate defines bitrate for ultra-quality input.
|
||||||
// Used in: Professional microphone input requiring maximum quality
|
// Used in: Premium microphone input optimized for RV1106 constraints
|
||||||
// Impact: Provides audiophile-grade voice quality with high bandwidth.
|
// Impact: Excellent voice quality within RV1106 processing limits.
|
||||||
// Default 96kbps ensures maximum voice reproduction quality.
|
// Reduced to 48kbps for stable RV1106 operation without lag.
|
||||||
AudioQualityUltraInputBitrate: 96,
|
AudioQualityUltraInputBitrate: 48,
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -1590,15 +1591,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 11kHz while minimizing processing load.
|
// Impact: Captures frequencies up to 24kHz while maintaining efficiency.
|
||||||
// Default 22.05kHz sufficient for speech and basic audio.
|
// Default 48kHz provides better quality while maintaining compatibility.
|
||||||
AudioQualityLowSampleRate: 22050,
|
AudioQualityLowSampleRate: 48000,
|
||||||
|
|
||||||
// AudioQualityMediumSampleRate defines sampling frequency for medium-quality audio.
|
// AudioQualityMediumSampleRate defines sampling frequency for medium-quality audio.
|
||||||
// Used in: Standard audio scenarios requiring CD-quality reproduction
|
// Used in: Standard audio scenarios requiring high-quality reproduction
|
||||||
// Impact: Captures full audible range up to 22kHz with balanced processing.
|
// Impact: Captures full audible range up to 24kHz with excellent processing.
|
||||||
// Default 44.1kHz provides CD-quality standard with excellent balance.
|
// Default 48kHz provides professional standard with optimal balance.
|
||||||
AudioQualityMediumSampleRate: 44100,
|
AudioQualityMediumSampleRate: 48000,
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -1611,80 +1612,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: Bandwidth-constrained scenarios prioritizing efficiency
|
// Used in: RV1106 efficiency-prioritized scenarios
|
||||||
// Impact: Reduces processing overhead with acceptable latency increase.
|
// Impact: Balanced frame size for quality and efficiency.
|
||||||
// Default 40ms provides efficiency for constrained scenarios.
|
// Reduced to 20ms for better responsiveness and reduced audio saccades.
|
||||||
AudioQualityLowFrameSize: 40 * time.Millisecond,
|
AudioQualityLowFrameSize: 20 * time.Millisecond,
|
||||||
|
|
||||||
// AudioQualityMediumFrameSize defines frame duration for medium-quality audio.
|
// AudioQualityMediumFrameSize defines frame duration for medium-quality audio.
|
||||||
// Used in: Standard real-time audio applications
|
// Used in: Balanced RV1106 real-time audio applications
|
||||||
// Impact: Provides good balance of latency and processing efficiency.
|
// Impact: Balances latency and processing efficiency for RV1106.
|
||||||
// Default 20ms standard for real-time audio applications.
|
// Optimized to 20ms for RV1106 balanced performance.
|
||||||
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: High-quality audio scenarios with balanced requirements
|
// Used in: RV1106 high-quality scenarios with performance constraints
|
||||||
// Impact: Maintains good latency while ensuring quality processing.
|
// Impact: Maintains acceptable latency while reducing RV1106 CPU load.
|
||||||
// Default 20ms provides optimal balance for high-quality scenarios.
|
// Optimized to 20ms for RV1106 high-quality balance.
|
||||||
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: Applications requiring immediate audio feedback
|
// Used in: Maximum RV1106 performance without KVM interference
|
||||||
// Impact: Minimizes latency for ultra-responsive audio processing.
|
// Impact: Balances quality and processing efficiency for RV1106 stability.
|
||||||
// Default 10ms ensures minimal latency for immediate feedback.
|
// Optimized to 20ms for RV1106 maximum stable performance.
|
||||||
AudioQualityUltraFrameSize: 10 * time.Millisecond,
|
AudioQualityUltraFrameSize: 20 * time.Millisecond,
|
||||||
|
|
||||||
// Audio Quality Channels - Channel configuration for different quality levels
|
// Audio Quality Channels - Optimized for RV1106 processing efficiency
|
||||||
// Used in: Audio processing pipeline for channel handling and bandwidth control
|
// Used in: Audio processing pipeline optimized for single-core ARM performance
|
||||||
AudioQualityLowChannels: 1,
|
AudioQualityLowChannels: 1, // Mono for minimal RV1106 processing
|
||||||
AudioQualityMediumChannels: 2,
|
AudioQualityMediumChannels: 2, // Stereo for balanced RV1106 performance
|
||||||
AudioQualityHighChannels: 2,
|
AudioQualityHighChannels: 2, // Stereo for RV1106 high-quality scenarios
|
||||||
AudioQualityUltraChannels: 2,
|
AudioQualityUltraChannels: 2, // Stereo for maximum RV1106 performance
|
||||||
|
|
||||||
// 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 bandwidth conservation
|
// Low Quality OPUS Parameters - Optimized for RV1106 minimal CPU usage
|
||||||
AudioQualityLowOpusComplexity: 1, // Low complexity for minimal CPU usage
|
AudioQualityLowOpusComplexity: 0, // Minimum complexity to reduce CPU load
|
||||||
AudioQualityLowOpusVBR: 0, // CBR for predictable bandwidth
|
AudioQualityLowOpusVBR: 1, // VBR for better quality at same bitrate
|
||||||
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE
|
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for lower complexity
|
||||||
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND
|
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND for efficiency
|
||||||
AudioQualityLowOpusDTX: 1, // Enable DTX for silence suppression
|
AudioQualityLowOpusDTX: 1, // Enable DTX to reduce processing when silent
|
||||||
|
|
||||||
// Medium Quality OPUS Parameters - Balanced performance and quality
|
// Medium Quality OPUS Parameters - Balanced for RV1106 performance
|
||||||
AudioQualityMediumOpusComplexity: 5, // Medium complexity for balanced performance
|
AudioQualityMediumOpusComplexity: 1, // Very low complexity for RV1106 stability
|
||||||
AudioQualityMediumOpusVBR: 1, // VBR for better quality
|
AudioQualityMediumOpusVBR: 1, // VBR for optimal quality
|
||||||
AudioQualityMediumOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
AudioQualityMediumOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for efficiency
|
||||||
AudioQualityMediumOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND
|
AudioQualityMediumOpusBandwidth: 1102, // OPUS_BANDWIDTH_MEDIUMBAND for balance
|
||||||
AudioQualityMediumOpusDTX: 0, // Disable DTX for consistent quality
|
AudioQualityMediumOpusDTX: 1, // Enable DTX for CPU savings
|
||||||
|
|
||||||
// High Quality OPUS Parameters - High quality with good performance
|
// High Quality OPUS Parameters - Optimized for RV1106 high performance
|
||||||
AudioQualityHighOpusComplexity: 8, // High complexity for better quality
|
AudioQualityHighOpusComplexity: 2, // Low complexity for RV1106 limits
|
||||||
AudioQualityHighOpusVBR: 1, // VBR for optimal quality
|
AudioQualityHighOpusVBR: 1, // VBR for optimal quality
|
||||||
AudioQualityHighOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
AudioQualityHighOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
||||||
AudioQualityHighOpusBandwidth: 1104, // OPUS_BANDWIDTH_SUPERWIDEBAND
|
AudioQualityHighOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for good range
|
||||||
AudioQualityHighOpusDTX: 0, // Disable DTX for consistent quality
|
AudioQualityHighOpusDTX: 0, // Disable DTX for consistent quality
|
||||||
|
|
||||||
// Ultra Quality OPUS Parameters - Maximum quality settings
|
// Ultra Quality OPUS Parameters - Maximum RV1106 performance without KVM interference
|
||||||
AudioQualityUltraOpusComplexity: 10, // Maximum complexity for best quality
|
AudioQualityUltraOpusComplexity: 3, // Moderate complexity for RV1106 stability
|
||||||
AudioQualityUltraOpusVBR: 1, // VBR for optimal quality
|
AudioQualityUltraOpusVBR: 1, // VBR for optimal quality
|
||||||
AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
||||||
AudioQualityUltraOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
|
AudioQualityUltraOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for stability
|
||||||
AudioQualityUltraOpusDTX: 0, // Disable DTX for maximum quality
|
AudioQualityUltraOpusDTX: 0, // Disable DTX for maximum quality
|
||||||
|
|
||||||
// CGO Audio Constants
|
// CGO Audio Constants - Optimized for RV1106 native audio processing
|
||||||
CGOOpusBitrate: 96000,
|
CGOOpusBitrate: 64000, // Reduced for RV1106 efficiency
|
||||||
CGOOpusComplexity: 3,
|
CGOOpusComplexity: 2, // Minimal complexity for RV1106
|
||||||
CGOOpusVBR: 1,
|
CGOOpusVBR: 1,
|
||||||
CGOOpusVBRConstraint: 1,
|
CGOOpusVBRConstraint: 1,
|
||||||
CGOOpusSignalType: 3, // OPUS_SIGNAL_MUSIC
|
CGOOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
|
||||||
CGOOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
|
CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for RV1106
|
||||||
CGOOpusDTX: 0,
|
CGOOpusDTX: 0,
|
||||||
CGOSampleRate: 48000,
|
CGOSampleRate: 48000,
|
||||||
CGOChannels: 2,
|
CGOChannels: 2,
|
||||||
CGOFrameSize: 960,
|
CGOFrameSize: 960,
|
||||||
CGOMaxPacketSize: 1500,
|
CGOMaxPacketSize: 1200, // Reduced for RV1106 memory efficiency
|
||||||
|
|
||||||
// Input IPC Constants
|
// Input IPC Constants
|
||||||
// InputIPCSampleRate defines sample rate for input IPC operations.
|
// InputIPCSampleRate defines sample rate for input IPC operations.
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,30 @@ func (aim *AudioInputManager) LogPerformanceStats() {
|
||||||
Msg("Audio input performance metrics")
|
Msg("Audio input performance metrics")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: IsRunning() is inherited from BaseAudioManager
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// 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,6 +36,7 @@ type InputMessageType uint8
|
||||||
const (
|
const (
|
||||||
InputMessageTypeOpusFrame InputMessageType = iota
|
InputMessageTypeOpusFrame InputMessageType = iota
|
||||||
InputMessageTypeConfig
|
InputMessageTypeConfig
|
||||||
|
InputMessageTypeOpusConfig
|
||||||
InputMessageTypeStop
|
InputMessageTypeStop
|
||||||
InputMessageTypeHeartbeat
|
InputMessageTypeHeartbeat
|
||||||
InputMessageTypeAck
|
InputMessageTypeAck
|
||||||
|
|
@ -203,6 +204,19 @@ 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)
|
||||||
|
|
@ -462,6 +476,8 @@ 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:
|
||||||
|
|
@ -507,6 +523,50 @@ 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()
|
||||||
|
|
@ -725,6 +785,44 @@ 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,21 +1,70 @@
|
||||||
|
//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()
|
||||||
|
|
||||||
|
|
@ -23,13 +72,16 @@ func RunAudioInputServer() error {
|
||||||
StartAdaptiveBuffering()
|
StartAdaptiveBuffering()
|
||||||
defer StopAdaptiveBuffering()
|
defer StopAdaptiveBuffering()
|
||||||
|
|
||||||
// Initialize CGO audio system
|
// Initialize CGO audio playback (optional for input server)
|
||||||
|
// This is used for audio loopback/monitoring features
|
||||||
err := CGOAudioPlaybackInit()
|
err := CGOAudioPlaybackInit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("failed to initialize CGO audio playback")
|
logger.Warn().Err(err).Msg("failed to initialize CGO audio playback - audio monitoring disabled")
|
||||||
return err
|
// Continue without playback - input functionality doesn't require it
|
||||||
|
} 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,9 +1,15 @@
|
||||||
|
//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"
|
||||||
)
|
)
|
||||||
|
|
@ -12,6 +18,9 @@ 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
|
||||||
|
|
@ -22,13 +31,43 @@ 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() {
|
||||||
return fmt.Errorf("audio input supervisor already running with PID %d", ais.cmd.Process.Pid)
|
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
|
// Create context for subprocess management
|
||||||
|
|
@ -40,11 +79,16 @@ 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, "--audio-input-server")
|
cmd := exec.CommandContext(ais.ctx, execPath, args...)
|
||||||
cmd.Env = append(os.Environ(),
|
|
||||||
"JETKVM_AUDIO_INPUT_IPC=true", // Enable IPC mode
|
// 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
|
||||||
|
|
||||||
// Set process group to allow clean termination
|
// Set process group to allow clean termination
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
|
@ -62,7 +106,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).Msg("Audio input server subprocess started")
|
ais.logger.Info().Int("pid", cmd.Process.Pid).Strs("args", args).Strs("opus_env", ais.opusEnv).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")
|
||||||
|
|
@ -97,7 +141,8 @@ 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 {
|
||||||
ais.logger.Info().Int("pid", ais.cmd.Process.Pid).Msg("Stopping audio input server subprocess")
|
pid := ais.cmd.Process.Pid
|
||||||
|
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)
|
||||||
|
|
@ -107,19 +152,49 @@ 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() {
|
||||||
done <- ais.cmd.Wait()
|
waitErr = ais.cmd.Wait()
|
||||||
|
done <- waitErr
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
ais.logger.Info().Msg("Audio input server subprocess stopped gracefully")
|
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):
|
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")
|
||||||
err := ais.cmd.Process.Kill()
|
// Use a more robust approach to check if process is still alive
|
||||||
if err != nil {
|
if ais.cmd != nil && ais.cmd.Process != nil {
|
||||||
ais.logger.Error().Err(err).Msg("Failed to kill audio input server subprocess")
|
// 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
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +253,7 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
|
||||||
ais.client.Disconnect()
|
ais.client.Disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as not running
|
// Mark as not running first to prevent race conditions
|
||||||
ais.setRunning(false)
|
ais.setRunning(false)
|
||||||
ais.cmd = nil
|
ais.cmd = nil
|
||||||
|
|
||||||
|
|
@ -238,3 +313,72 @@ 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,18 +4,69 @@ 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,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -311,10 +312,18 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
return ErrAudioAlreadyRunning
|
return ErrAudioAlreadyRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize CGO audio capture
|
// Initialize CGO audio capture with retry logic
|
||||||
if err := CGOAudioInit(); err != nil {
|
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 {
|
||||||
atomic.StoreInt32(&outputStreamingRunning, 0)
|
atomic.StoreInt32(&outputStreamingRunning, 0)
|
||||||
return err
|
return fmt.Errorf("failed to initialize audio after 3 attempts: %w", initErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
@ -341,7 +350,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
// Capture audio frame with enhanced error handling
|
// Capture audio frame with enhanced error handling and initialization checking
|
||||||
n, err := CGOAudioReadEncode(buffer)
|
n, err := CGOAudioReadEncode(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
consecutiveErrors++
|
consecutiveErrors++
|
||||||
|
|
@ -350,6 +359,13 @@ 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,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -42,6 +43,9 @@ 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)
|
||||||
|
|
@ -72,6 +76,23 @@ 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) {
|
||||||
|
|
@ -223,18 +244,24 @@ 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, "--audio-output-server")
|
s.cmd = exec.CommandContext(s.ctx, execPath, args...)
|
||||||
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).Msg("audio server process started")
|
s.logger.Info().Int("pid", s.processPID).Strs("args", args).Strs("opus_env", s.opusEnv).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,6 +44,25 @@ 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