Compare commits

...

7 Commits

Author SHA1 Message Date
Alex P 950ca2bd99 fix(audio): improve process termination handling in input supervisor
Add more robust process state checking and error handling during audio input server shutdown. Use signal 0 to verify process existence before killing and handle various edge cases. Also improve logging to better track shutdown outcomes.
2025-09-01 11:08:13 +00:00
Alex P dfbf9249b9 fix(audio): improve audio initialization and process termination handling
- Add retry logic for CGO audio initialization
- Enhance process termination checks in input supervisor
- Skip initialization check in cgo_audio to avoid compilation issues
- Add proper error handling for audio system initialization failures
2025-09-01 10:24:26 +00:00
Alex P f51f6da2de fix(audio): improve logging for Opus config and subprocess status
Add detailed logging when sending Opus configuration to audio input subprocess
Include supervisor running status in log when subprocess is not connected
2025-09-01 08:07:53 +00:00
Alex P fd7608384a feat(audio): implement dynamic Opus config updates and optimize audio params
Add support for dynamic Opus encoder configuration updates without requiring subprocess restart. This allows quality changes to be applied immediately while maintaining audio stream continuity.

Optimize audio quality parameters to reduce CPU load and prevent mouse lag on RV1106 devices. Lower bitrates and complexity while adjusting signal types and bandwidths for better performance.

Add build tags for CGO requirements and improve audio input supervisor behavior to check for existing processes before starting new ones.
2025-09-01 08:02:43 +00:00
Alex P 6adcc26ff2 feat(audio): add goroutine cache cleanup and process reuse
Implement periodic cleanup of stale goroutine buffer caches to prevent memory leaks
Add ability to detect and reuse existing audio input server processes
2025-08-29 17:05:37 +00:00
Alex P 858859e317 perf(audio): optimize audio config for RV1106 SoC compatibility
Adjust bitrates, frame sizes, and OPUS parameters to balance quality and performance on RV1106. Reduce channel count for low quality to minimize CPU load. Update CGO constants for better memory efficiency.
2025-08-28 22:25:11 +00:00
Alex P 9c0aff4489 feat(audio): implement audio input supervisor and opus config management
add audio input supervisor with opus configuration support
update audio quality presets and configuration handling
restructure audio subprocess management with environment variables
2025-08-28 22:02:22 +00:00
14 changed files with 689 additions and 106 deletions

View File

@ -13,6 +13,7 @@ func main() {
versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit")
audioServerPtr := flag.Bool("audio-output-server", false, "Run as audio server subprocess")
audioInputServerPtr := flag.Bool("audio-input-server", false, "Run as audio input server subprocess")
flag.Parse()
if *versionPtr || *versionJsonPtr {

View File

@ -8,8 +8,8 @@ import (
)
var (
// Global audio output supervisor instance
globalOutputSupervisor unsafe.Pointer // *AudioOutputSupervisor
globalInputSupervisor unsafe.Pointer // *AudioInputSupervisor
)
// isAudioServerProcess detects if we're running as the audio server subprocess
@ -70,3 +70,17 @@ func GetAudioOutputSupervisor() *AudioOutputSupervisor {
}
return (*AudioOutputSupervisor)(ptr)
}
// SetAudioInputSupervisor sets the global audio input supervisor
func SetAudioInputSupervisor(supervisor *AudioInputSupervisor) {
atomic.StorePointer(&globalInputSupervisor, unsafe.Pointer(supervisor))
}
// GetAudioInputSupervisor returns the global audio input supervisor
func GetAudioInputSupervisor() *AudioInputSupervisor {
ptr := atomic.LoadPointer(&globalInputSupervisor)
if ptr == nil {
return nil
}
return (*AudioInputSupervisor)(ptr)
}

View File

@ -1,3 +1,6 @@
//go:build cgo
// +build cgo
// Package audio provides real-time audio processing for JetKVM with low-latency streaming.
//
// Key components: output/input pipelines with Opus codec, adaptive buffer management,
@ -167,7 +170,7 @@ func SetAudioQuality(quality AudioQuality) {
if config, exists := presets[quality]; exists {
currentConfig = config
// Update CGO OPUS encoder parameters based on quality
// Get OPUS encoder parameters based on quality
var complexity, vbr, signalType, bandwidth, dtx int
switch quality {
case AudioQualityLow:
@ -203,11 +206,27 @@ func SetAudioQuality(quality AudioQuality) {
dtx = GetConfig().AudioQualityMediumOpusDTX
}
// Dynamically update CGO OPUS encoder parameters
// Use current VBR constraint setting from config
vbrConstraint := GetConfig().CGOOpusVBRConstraint
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
// Restart audio output subprocess with new OPUS configuration
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Info().Int("quality", int(quality)).Msg("restarting audio output subprocess with new quality settings")
// Set new OPUS configuration
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
// Stop current subprocess
supervisor.Stop()
// Start subprocess with new configuration
if err := supervisor.Start(); err != nil {
logger.Error().Err(err).Msg("failed to restart audio output subprocess")
}
} else {
// Fallback to dynamic update if supervisor is not available
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()
if config, exists := presets[quality]; exists {
currentMicrophoneConfig = config
// Get OPUS parameters for the selected quality
var complexity, vbr, signalType, bandwidth, dtx int
switch quality {
case AudioQualityLow:
complexity = GetConfig().AudioQualityLowOpusComplexity
vbr = GetConfig().AudioQualityLowOpusVBR
signalType = GetConfig().AudioQualityLowOpusSignalType
bandwidth = GetConfig().AudioQualityLowOpusBandwidth
dtx = GetConfig().AudioQualityLowOpusDTX
case AudioQualityMedium:
complexity = GetConfig().AudioQualityMediumOpusComplexity
vbr = GetConfig().AudioQualityMediumOpusVBR
signalType = GetConfig().AudioQualityMediumOpusSignalType
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
dtx = GetConfig().AudioQualityMediumOpusDTX
case AudioQualityHigh:
complexity = GetConfig().AudioQualityHighOpusComplexity
vbr = GetConfig().AudioQualityHighOpusVBR
signalType = GetConfig().AudioQualityHighOpusSignalType
bandwidth = GetConfig().AudioQualityHighOpusBandwidth
dtx = GetConfig().AudioQualityHighOpusDTX
case AudioQualityUltra:
complexity = GetConfig().AudioQualityUltraOpusComplexity
vbr = GetConfig().AudioQualityUltraOpusVBR
signalType = GetConfig().AudioQualityUltraOpusSignalType
bandwidth = GetConfig().AudioQualityUltraOpusBandwidth
dtx = GetConfig().AudioQualityUltraOpusDTX
default:
// Use medium quality as fallback
complexity = GetConfig().AudioQualityMediumOpusComplexity
vbr = GetConfig().AudioQualityMediumOpusVBR
signalType = GetConfig().AudioQualityMediumOpusSignalType
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
dtx = GetConfig().AudioQualityMediumOpusDTX
}
// Update audio input subprocess configuration dynamically without restart
if supervisor := GetAudioInputSupervisor(); supervisor != nil {
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Info().Int("quality", int(quality)).Msg("updating audio input subprocess quality settings dynamically")
// Set new OPUS configuration for future restarts
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
// Send dynamic configuration update to running subprocess
if supervisor.IsConnected() {
// Convert AudioConfig to InputIPCOpusConfig with complete Opus parameters
opusConfig := InputIPCOpusConfig{
SampleRate: config.SampleRate,
Channels: config.Channels,
FrameSize: int(config.FrameSize.Milliseconds() * int64(config.SampleRate) / 1000), // Convert ms to samples
Bitrate: config.Bitrate * 1000, // Convert kbps to bps
Complexity: complexity,
VBR: vbr,
SignalType: signalType,
Bandwidth: bandwidth,
DTX: dtx,
}
logger.Info().Interface("opusConfig", opusConfig).Msg("sending Opus configuration to audio input subprocess")
if err := supervisor.SendOpusConfig(opusConfig); err != nil {
logger.Warn().Err(err).Msg("failed to send dynamic Opus config update, subprocess may need restart")
// Fallback to restart if dynamic update fails
supervisor.Stop()
if err := supervisor.Start(); err != nil {
logger.Error().Err(err).Msg("failed to restart audio input subprocess after config update failure")
}
} else {
logger.Info().Msg("audio input quality updated dynamically with complete Opus configuration")
}
} else {
logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio input subprocess not connected, configuration will apply on next start")
}
}
}
}

View File

@ -6,8 +6,6 @@ import (
"sync/atomic"
"time"
"unsafe"
"github.com/jetkvm/kvm/internal/logging"
)
// Lock-free buffer cache for per-goroutine optimization
@ -18,6 +16,9 @@ type lockFreeBufferCache struct {
// Per-goroutine buffer cache using goroutine-local storage
var goroutineBufferCache = make(map[int64]*lockFreeBufferCache)
var goroutineCacheMutex sync.RWMutex
var lastCleanupTime int64 // Unix timestamp of last cleanup
const maxCacheSize = 1000 // Maximum number of goroutine caches
const cleanupInterval = 300 // Cleanup interval in seconds (5 minutes)
// getGoroutineID extracts goroutine ID from runtime stack for cache key
func getGoroutineID() int64 {
@ -38,6 +39,41 @@ func getGoroutineID() int64 {
return 0
}
// cleanupGoroutineCache removes stale entries from the goroutine cache
func cleanupGoroutineCache() {
now := time.Now().Unix()
lastCleanup := atomic.LoadInt64(&lastCleanupTime)
// Only cleanup if enough time has passed
if now-lastCleanup < cleanupInterval {
return
}
// Try to acquire cleanup lock atomically
if !atomic.CompareAndSwapInt64(&lastCleanupTime, lastCleanup, now) {
return // Another goroutine is already cleaning up
}
goroutineCacheMutex.Lock()
defer goroutineCacheMutex.Unlock()
// If cache is too large, remove oldest entries (simple FIFO)
if len(goroutineBufferCache) > maxCacheSize {
// Remove half of the entries to avoid frequent cleanups
toRemove := len(goroutineBufferCache) - maxCacheSize/2
count := 0
for gid := range goroutineBufferCache {
delete(goroutineBufferCache, gid)
count++
if count >= toRemove {
break
}
}
// Log cleanup for debugging (removed logging dependency)
_ = count // Avoid unused variable warning
}
}
type AudioBufferPool struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
currentSize int64 // Current pool size (atomic)
@ -57,9 +93,7 @@ type AudioBufferPool struct {
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
// Validate buffer size parameter
if err := ValidateBufferSize(bufferSize); err != nil {
// 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")
// Use default value on validation error
bufferSize = GetConfig().AudioFramePoolSize
}
@ -99,6 +133,9 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
}
func (p *AudioBufferPool) Get() []byte {
// Trigger periodic cleanup of goroutine cache
cleanupGoroutineCache()
start := time.Now()
wasHit := false
defer func() {

View File

@ -709,6 +709,12 @@ func cgoAudioReadEncode(buf []byte) (int, error) {
return 0, newBufferTooSmallError(len(buf), minRequired)
}
// Skip initialization check for now to avoid CGO compilation issues
// TODO: Add proper initialization state checking
// Note: The C code already has comprehensive state tracking with capture_initialized,
// capture_initializing, playback_initialized, and playback_initializing flags.
// When CGO environment is properly configured, this should check C.capture_initialized.
n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0]))
if n < 0 {
return 0, newAudioReadEncodeError(int(n))

View File

@ -1554,35 +1554,36 @@ func DefaultAudioConfig() *AudioConfigConstants {
FrameSize: 960,
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,
AudioQualityLowInputBitrate: 16,
AudioQualityMediumOutputBitrate: 64,
AudioQualityMediumInputBitrate: 32,
AudioQualityMediumOutputBitrate: 48,
AudioQualityMediumInputBitrate: 24,
// AudioQualityHighOutputBitrate defines bitrate for high-quality output.
// Used in: Professional applications requiring excellent audio fidelity
// Impact: Provides excellent quality but increases bandwidth usage.
// Default 128kbps matches professional Opus encoding standards.
AudioQualityHighOutputBitrate: 128,
// Used in: Professional applications requiring good audio fidelity on RV1106
// Impact: Balanced quality optimized for single-core ARM performance.
// Reduced to 64kbps for RV1106 compatibility and minimal CPU overhead.
AudioQualityHighOutputBitrate: 64,
// AudioQualityHighInputBitrate defines bitrate for high-quality input.
// Used in: High-quality microphone input for professional use
// Impact: Ensures clear voice reproduction for professional scenarios.
// Default 64kbps provides excellent voice quality.
AudioQualityHighInputBitrate: 64,
// Used in: High-quality microphone input optimized for RV1106
// Impact: Clear voice reproduction without overwhelming single-core CPU.
// Reduced to 32kbps for optimal RV1106 performance without lag.
AudioQualityHighInputBitrate: 32,
// AudioQualityUltraOutputBitrate defines bitrate for ultra-quality output.
// Used in: Audiophile-grade reproduction and high-bandwidth connections
// Impact: Maximum quality but requires significant bandwidth.
// Default 192kbps suitable for high-bandwidth, quality-critical scenarios.
AudioQualityUltraOutputBitrate: 192,
// Used in: Maximum quality while ensuring RV1106 stability
// Impact: Best possible quality without interfering with KVM operations.
// Reduced to 96kbps for RV1106 maximum performance without mouse lag.
AudioQualityUltraOutputBitrate: 96,
// AudioQualityUltraInputBitrate defines bitrate for ultra-quality input.
// Used in: Professional microphone input requiring maximum quality
// Impact: Provides audiophile-grade voice quality with high bandwidth.
// Default 96kbps ensures maximum voice reproduction quality.
AudioQualityUltraInputBitrate: 96,
// Used in: Premium microphone input optimized for RV1106 constraints
// Impact: Excellent voice quality within RV1106 processing limits.
// Reduced to 48kbps for stable RV1106 operation without lag.
AudioQualityUltraInputBitrate: 48,
// Audio Quality Sample Rates - Sampling frequencies for different quality levels
// Used in: Audio capture, processing, and format negotiation
@ -1590,15 +1591,15 @@ func DefaultAudioConfig() *AudioConfigConstants {
// AudioQualityLowSampleRate defines sampling frequency for low-quality audio.
// Used in: Bandwidth-constrained scenarios and basic audio requirements
// Impact: Captures frequencies up to 11kHz while minimizing processing load.
// Default 22.05kHz sufficient for speech and basic audio.
AudioQualityLowSampleRate: 22050,
// Impact: Captures frequencies up to 24kHz while maintaining efficiency.
// Default 48kHz provides better quality while maintaining compatibility.
AudioQualityLowSampleRate: 48000,
// AudioQualityMediumSampleRate defines sampling frequency for medium-quality audio.
// Used in: Standard audio scenarios requiring CD-quality reproduction
// Impact: Captures full audible range up to 22kHz with balanced processing.
// Default 44.1kHz provides CD-quality standard with excellent balance.
AudioQualityMediumSampleRate: 44100,
// Used in: Standard audio scenarios requiring high-quality reproduction
// Impact: Captures full audible range up to 24kHz with excellent processing.
// Default 48kHz provides professional standard with optimal balance.
AudioQualityMediumSampleRate: 48000,
// AudioQualityMicLowSampleRate defines sampling frequency for low-quality microphone.
// Used in: Voice/microphone input in bandwidth-constrained scenarios
@ -1611,80 +1612,80 @@ func DefaultAudioConfig() *AudioConfigConstants {
// Impact: Controls latency vs processing efficiency trade-offs
// AudioQualityLowFrameSize defines frame duration for low-quality audio.
// Used in: Bandwidth-constrained scenarios prioritizing efficiency
// Impact: Reduces processing overhead with acceptable latency increase.
// Default 40ms provides efficiency for constrained scenarios.
AudioQualityLowFrameSize: 40 * time.Millisecond,
// Used in: RV1106 efficiency-prioritized scenarios
// Impact: Balanced frame size for quality and efficiency.
// Reduced to 20ms for better responsiveness and reduced audio saccades.
AudioQualityLowFrameSize: 20 * time.Millisecond,
// AudioQualityMediumFrameSize defines frame duration for medium-quality audio.
// Used in: Standard real-time audio applications
// Impact: Provides good balance of latency and processing efficiency.
// Default 20ms standard for real-time audio applications.
// Used in: Balanced RV1106 real-time audio applications
// Impact: Balances latency and processing efficiency for RV1106.
// Optimized to 20ms for RV1106 balanced performance.
AudioQualityMediumFrameSize: 20 * time.Millisecond,
// AudioQualityHighFrameSize defines frame duration for high-quality audio.
// Used in: High-quality audio scenarios with balanced requirements
// Impact: Maintains good latency while ensuring quality processing.
// Default 20ms provides optimal balance for high-quality scenarios.
// Used in: RV1106 high-quality scenarios with performance constraints
// Impact: Maintains acceptable latency while reducing RV1106 CPU load.
// Optimized to 20ms for RV1106 high-quality balance.
AudioQualityHighFrameSize: 20 * time.Millisecond,
// AudioQualityUltraFrameSize defines frame duration for ultra-quality audio.
// Used in: Applications requiring immediate audio feedback
// Impact: Minimizes latency for ultra-responsive audio processing.
// Default 10ms ensures minimal latency for immediate feedback.
AudioQualityUltraFrameSize: 10 * time.Millisecond,
// Used in: Maximum RV1106 performance without KVM interference
// Impact: Balances quality and processing efficiency for RV1106 stability.
// Optimized to 20ms for RV1106 maximum stable performance.
AudioQualityUltraFrameSize: 20 * time.Millisecond,
// Audio Quality Channels - Channel configuration for different quality levels
// Used in: Audio processing pipeline for channel handling and bandwidth control
AudioQualityLowChannels: 1,
AudioQualityMediumChannels: 2,
AudioQualityHighChannels: 2,
AudioQualityUltraChannels: 2,
// Audio Quality Channels - Optimized for RV1106 processing efficiency
// Used in: Audio processing pipeline optimized for single-core ARM performance
AudioQualityLowChannels: 1, // Mono for minimal RV1106 processing
AudioQualityMediumChannels: 2, // Stereo for balanced RV1106 performance
AudioQualityHighChannels: 2, // Stereo for RV1106 high-quality scenarios
AudioQualityUltraChannels: 2, // Stereo for maximum RV1106 performance
// Audio Quality OPUS Encoder Parameters - Quality-specific encoder settings
// Used in: Dynamic OPUS encoder configuration based on quality presets
// Impact: Controls encoding complexity, VBR, signal type, bandwidth, and DTX
// Low Quality OPUS Parameters - Optimized for bandwidth conservation
AudioQualityLowOpusComplexity: 1, // Low complexity for minimal CPU usage
AudioQualityLowOpusVBR: 0, // CBR for predictable bandwidth
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND
AudioQualityLowOpusDTX: 1, // Enable DTX for silence suppression
// Low Quality OPUS Parameters - Optimized for RV1106 minimal CPU usage
AudioQualityLowOpusComplexity: 0, // Minimum complexity to reduce CPU load
AudioQualityLowOpusVBR: 1, // VBR for better quality at same bitrate
AudioQualityLowOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for lower complexity
AudioQualityLowOpusBandwidth: 1101, // OPUS_BANDWIDTH_NARROWBAND for efficiency
AudioQualityLowOpusDTX: 1, // Enable DTX to reduce processing when silent
// Medium Quality OPUS Parameters - Balanced performance and quality
AudioQualityMediumOpusComplexity: 5, // Medium complexity for balanced performance
AudioQualityMediumOpusVBR: 1, // VBR for better quality
AudioQualityMediumOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityMediumOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND
AudioQualityMediumOpusDTX: 0, // Disable DTX for consistent quality
// Medium Quality OPUS Parameters - Balanced for RV1106 performance
AudioQualityMediumOpusComplexity: 1, // Very low complexity for RV1106 stability
AudioQualityMediumOpusVBR: 1, // VBR for optimal quality
AudioQualityMediumOpusSignalType: 3001, // OPUS_SIGNAL_VOICE for efficiency
AudioQualityMediumOpusBandwidth: 1102, // OPUS_BANDWIDTH_MEDIUMBAND for balance
AudioQualityMediumOpusDTX: 1, // Enable DTX for CPU savings
// High Quality OPUS Parameters - High quality with good performance
AudioQualityHighOpusComplexity: 8, // High complexity for better quality
// High Quality OPUS Parameters - Optimized for RV1106 high performance
AudioQualityHighOpusComplexity: 2, // Low complexity for RV1106 limits
AudioQualityHighOpusVBR: 1, // VBR for optimal quality
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
// Ultra Quality OPUS Parameters - Maximum quality settings
AudioQualityUltraOpusComplexity: 10, // Maximum complexity for best quality
// Ultra Quality OPUS Parameters - Maximum RV1106 performance without KVM interference
AudioQualityUltraOpusComplexity: 3, // Moderate complexity for RV1106 stability
AudioQualityUltraOpusVBR: 1, // VBR for optimal quality
AudioQualityUltraOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
AudioQualityUltraOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
AudioQualityUltraOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for stability
AudioQualityUltraOpusDTX: 0, // Disable DTX for maximum quality
// CGO Audio Constants
CGOOpusBitrate: 96000,
CGOOpusComplexity: 3,
// CGO Audio Constants - Optimized for RV1106 native audio processing
CGOOpusBitrate: 64000, // Reduced for RV1106 efficiency
CGOOpusComplexity: 2, // Minimal complexity for RV1106
CGOOpusVBR: 1,
CGOOpusVBRConstraint: 1,
CGOOpusSignalType: 3, // OPUS_SIGNAL_MUSIC
CGOOpusBandwidth: 1105, // OPUS_BANDWIDTH_FULLBAND
CGOOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC
CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND for RV1106
CGOOpusDTX: 0,
CGOSampleRate: 48000,
CGOChannels: 2,
CGOFrameSize: 960,
CGOMaxPacketSize: 1500,
CGOMaxPacketSize: 1200, // Reduced for RV1106 memory efficiency
// Input IPC Constants
// InputIPCSampleRate defines sample rate for input IPC operations.

View File

@ -208,7 +208,30 @@ func (aim *AudioInputManager) LogPerformanceStats() {
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
// This checks both that it's running and that the IPC connection is established

View File

@ -36,6 +36,7 @@ type InputMessageType uint8
const (
InputMessageTypeOpusFrame InputMessageType = iota
InputMessageTypeConfig
InputMessageTypeOpusConfig
InputMessageTypeStop
InputMessageTypeHeartbeat
InputMessageTypeAck
@ -203,6 +204,19 @@ type InputIPCConfig struct {
FrameSize int
}
// InputIPCOpusConfig contains complete Opus encoder configuration
type InputIPCOpusConfig struct {
SampleRate int
Channels int
FrameSize int
Bitrate int
Complexity int
VBR int
SignalType int
Bandwidth int
DTX int
}
// AudioInputServer handles IPC communication for audio input processing
type AudioInputServer struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
@ -462,6 +476,8 @@ func (ais *AudioInputServer) processMessage(msg *InputIPCMessage) error {
return ais.processOpusFrame(msg.Data)
case InputMessageTypeConfig:
return ais.processConfig(msg.Data)
case InputMessageTypeOpusConfig:
return ais.processOpusConfig(msg.Data)
case InputMessageTypeStop:
return fmt.Errorf("stop message received")
case InputMessageTypeHeartbeat:
@ -507,6 +523,50 @@ func (ais *AudioInputServer) processConfig(data []byte) error {
return ais.sendAck()
}
// processOpusConfig processes a complete Opus encoder configuration update
func (ais *AudioInputServer) processOpusConfig(data []byte) error {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputServerComponent).Logger()
// Validate configuration data size (9 * int32 = 36 bytes)
if len(data) != 36 {
return fmt.Errorf("invalid Opus configuration data size: expected 36 bytes, got %d", len(data))
}
// Deserialize Opus configuration
config := InputIPCOpusConfig{
SampleRate: int(binary.LittleEndian.Uint32(data[0:4])),
Channels: int(binary.LittleEndian.Uint32(data[4:8])),
FrameSize: int(binary.LittleEndian.Uint32(data[8:12])),
Bitrate: int(binary.LittleEndian.Uint32(data[12:16])),
Complexity: int(binary.LittleEndian.Uint32(data[16:20])),
VBR: int(binary.LittleEndian.Uint32(data[20:24])),
SignalType: int(binary.LittleEndian.Uint32(data[24:28])),
Bandwidth: int(binary.LittleEndian.Uint32(data[28:32])),
DTX: int(binary.LittleEndian.Uint32(data[32:36])),
}
logger.Info().Interface("config", config).Msg("applying dynamic Opus encoder configuration")
// Apply the Opus encoder configuration dynamically
err := CGOUpdateOpusEncoderParams(
config.Bitrate,
config.Complexity,
config.VBR,
0, // VBR constraint - using default
config.SignalType,
config.Bandwidth,
config.DTX,
)
if err != nil {
logger.Error().Err(err).Msg("failed to apply Opus encoder configuration")
return fmt.Errorf("failed to apply Opus configuration: %w", err)
}
logger.Info().Msg("Opus encoder configuration applied successfully")
return ais.sendAck()
}
// sendAck sends an acknowledgment message
func (ais *AudioInputServer) sendAck() error {
ais.mtx.Lock()
@ -725,6 +785,44 @@ func (aic *AudioInputClient) SendConfig(config InputIPCConfig) error {
return aic.writeMessage(msg)
}
// SendOpusConfig sends a complete Opus encoder configuration update to the audio input server
func (aic *AudioInputClient) SendOpusConfig(config InputIPCOpusConfig) error {
aic.mtx.Lock()
defer aic.mtx.Unlock()
if !aic.running || aic.conn == nil {
return fmt.Errorf("not connected to audio input server")
}
// Validate configuration parameters
if config.SampleRate <= 0 || config.Channels <= 0 || config.FrameSize <= 0 || config.Bitrate <= 0 {
return fmt.Errorf("invalid Opus configuration: SampleRate=%d, Channels=%d, FrameSize=%d, Bitrate=%d",
config.SampleRate, config.Channels, config.FrameSize, config.Bitrate)
}
// Serialize Opus configuration (9 * int32 = 36 bytes)
data := make([]byte, 36)
binary.LittleEndian.PutUint32(data[0:4], uint32(config.SampleRate))
binary.LittleEndian.PutUint32(data[4:8], uint32(config.Channels))
binary.LittleEndian.PutUint32(data[8:12], uint32(config.FrameSize))
binary.LittleEndian.PutUint32(data[12:16], uint32(config.Bitrate))
binary.LittleEndian.PutUint32(data[16:20], uint32(config.Complexity))
binary.LittleEndian.PutUint32(data[20:24], uint32(config.VBR))
binary.LittleEndian.PutUint32(data[24:28], uint32(config.SignalType))
binary.LittleEndian.PutUint32(data[28:32], uint32(config.Bandwidth))
binary.LittleEndian.PutUint32(data[32:36], uint32(config.DTX))
msg := &InputIPCMessage{
Magic: inputMagicNumber,
Type: InputMessageTypeOpusConfig,
Length: uint32(len(data)),
Timestamp: time.Now().UnixNano(),
Data: data,
}
return aic.writeMessage(msg)
}
// SendHeartbeat sends a heartbeat message
func (aic *AudioInputClient) SendHeartbeat() error {
aic.mtx.Lock()

View File

@ -1,21 +1,70 @@
//go:build cgo
// +build cgo
package audio
/*
#cgo pkg-config: alsa
#cgo LDFLAGS: -lopus
*/
import "C"
import (
"context"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/jetkvm/kvm/internal/logging"
)
// getEnvInt reads an integer from environment variable with a default value
func getEnvIntInput(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
// parseOpusConfigInput reads OPUS configuration from environment variables
// with fallback to default config values for input server
func parseOpusConfigInput() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
// Read configuration from environment variables with config defaults
bitrate = getEnvIntInput("JETKVM_OPUS_BITRATE", GetConfig().CGOOpusBitrate)
complexity = getEnvIntInput("JETKVM_OPUS_COMPLEXITY", GetConfig().CGOOpusComplexity)
vbr = getEnvIntInput("JETKVM_OPUS_VBR", GetConfig().CGOOpusVBR)
signalType = getEnvIntInput("JETKVM_OPUS_SIGNAL_TYPE", GetConfig().CGOOpusSignalType)
bandwidth = getEnvIntInput("JETKVM_OPUS_BANDWIDTH", GetConfig().CGOOpusBandwidth)
dtx = getEnvIntInput("JETKVM_OPUS_DTX", GetConfig().CGOOpusDTX)
return bitrate, complexity, vbr, signalType, bandwidth, dtx
}
// applyOpusConfigInput applies OPUS configuration to the global config for input server
func applyOpusConfigInput(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
config := GetConfig()
config.CGOOpusBitrate = bitrate
config.CGOOpusComplexity = complexity
config.CGOOpusVBR = vbr
config.CGOOpusSignalType = signalType
config.CGOOpusBandwidth = bandwidth
config.CGOOpusDTX = dtx
}
// RunAudioInputServer runs the audio input server subprocess
// This should be called from main() when the subprocess is detected
func RunAudioInputServer() error {
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
logger.Debug().Msg("audio input server subprocess starting")
// Parse OPUS configuration from environment variables
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfigInput()
applyOpusConfigInput(bitrate, complexity, vbr, signalType, bandwidth, dtx)
// Initialize validation cache for optimal performance
InitValidationCache()
@ -23,13 +72,16 @@ func RunAudioInputServer() error {
StartAdaptiveBuffering()
defer StopAdaptiveBuffering()
// Initialize CGO audio system
// Initialize CGO audio playback (optional for input server)
// This is used for audio loopback/monitoring features
err := CGOAudioPlaybackInit()
if err != nil {
logger.Error().Err(err).Msg("failed to initialize CGO audio playback")
return err
logger.Warn().Err(err).Msg("failed to initialize CGO audio playback - audio monitoring disabled")
// Continue without playback - input functionality doesn't require it
} else {
defer CGOAudioPlaybackClose()
logger.Debug().Msg("CGO audio playback initialized successfully")
}
defer CGOAudioPlaybackClose()
// Create and start the IPC server
server, err := NewAudioInputServer()

View File

@ -1,9 +1,15 @@
//go:build cgo
// +build cgo
package audio
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
)
@ -12,6 +18,9 @@ import (
type AudioInputSupervisor struct {
*BaseSupervisor
client *AudioInputClient
// Environment variables for OPUS configuration
opusEnv []string
}
// 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
func (ais *AudioInputSupervisor) Start() error {
ais.mutex.Lock()
defer ais.mutex.Unlock()
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
@ -40,11 +79,16 @@ func (ais *AudioInputSupervisor) Start() error {
return fmt.Errorf("failed to get executable path: %w", err)
}
// Build command arguments (only subprocess flag)
args := []string{"--audio-input-server"}
// Create command for audio input server subprocess
cmd := exec.CommandContext(ais.ctx, execPath, "--audio-input-server")
cmd.Env = append(os.Environ(),
"JETKVM_AUDIO_INPUT_IPC=true", // Enable IPC mode
)
cmd := exec.CommandContext(ais.ctx, execPath, args...)
// Set environment variables for IPC and OPUS configuration
env := append(os.Environ(), "JETKVM_AUDIO_INPUT_IPC=true") // Enable IPC mode
env = append(env, ais.opusEnv...) // Add OPUS configuration
cmd.Env = env
// Set process group to allow clean termination
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)
}
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
ais.processMonitor.AddProcess(cmd.Process.Pid, "audio-input-server")
@ -97,7 +141,8 @@ func (ais *AudioInputSupervisor) Stop() {
// Try graceful termination first
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
err := ais.cmd.Process.Signal(syscall.SIGTERM)
@ -107,19 +152,49 @@ func (ais *AudioInputSupervisor) Stop() {
// Wait for graceful shutdown with timeout
done := make(chan error, 1)
var waitErr error
go func() {
done <- ais.cmd.Wait()
waitErr = ais.cmd.Wait()
done <- waitErr
}()
select {
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):
// Force kill if graceful shutdown failed
ais.logger.Warn().Msg("Audio input server subprocess did not stop gracefully, force killing")
err := ais.cmd.Process.Kill()
if err != nil {
ais.logger.Error().Err(err).Msg("Failed to kill audio input server subprocess")
// Use a more robust approach to check if process is still alive
if ais.cmd != nil && ais.cmd.Process != nil {
// Try to send signal 0 to check if process exists
if err := ais.cmd.Process.Signal(syscall.Signal(0)); err == nil {
// Process is still alive, force kill it
if killErr := ais.cmd.Process.Kill(); killErr != nil {
// Only log error if it's not "process already finished"
if !strings.Contains(killErr.Error(), "process already finished") {
ais.logger.Error().Err(killErr).Msg("Failed to kill audio input server subprocess")
} else {
ais.logger.Debug().Msg("Audio input server subprocess already finished during kill attempt")
}
} else {
ais.logger.Info().Msg("Audio input server subprocess force killed successfully")
}
} else {
ais.logger.Debug().Msg("Audio input server subprocess already finished")
}
// Wait a bit for the kill to take effect and collect the exit status
go func() {
select {
case <-done:
// Process finished
case <-time.After(1 * time.Second):
// Give up waiting
}
}()
}
}
}
@ -178,7 +253,7 @@ func (ais *AudioInputSupervisor) monitorSubprocess() {
ais.client.Disconnect()
}
// Mark as not running
// Mark as not running first to prevent race conditions
ais.setRunning(false)
ais.cmd = nil
@ -238,3 +313,72 @@ func (ais *AudioInputSupervisor) SendConfig(config InputIPCConfig) error {
return ais.client.SendConfig(config)
}
// SendOpusConfig sends a complete Opus encoder configuration to the audio input server
func (ais *AudioInputSupervisor) SendOpusConfig(config InputIPCOpusConfig) error {
if ais.client == nil {
return fmt.Errorf("client not initialized")
}
if !ais.client.IsConnected() {
return fmt.Errorf("client not connected")
}
return ais.client.SendOpusConfig(config)
}
// findExistingAudioInputProcess checks if there's already an audio input server process running
func (ais *AudioInputSupervisor) findExistingAudioInputProcess() (int, error) {
// Get current executable path
execPath, err := os.Executable()
if err != nil {
return 0, fmt.Errorf("failed to get executable path: %w", err)
}
execName := filepath.Base(execPath)
// Use ps to find processes with our executable name and audio-input-server argument
cmd := exec.Command("ps", "aux")
output, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("failed to run ps command: %w", err)
}
// Parse ps output to find audio input server processes
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, execName) && strings.Contains(line, "--audio-input-server") {
// Extract PID from ps output (second column)
fields := strings.Fields(line)
if len(fields) >= 2 {
if pid, err := strconv.Atoi(fields[1]); err == nil {
// Verify the process is still running and accessible
if ais.isProcessRunning(pid) {
return pid, nil
}
}
}
}
}
return 0, fmt.Errorf("no existing audio input server process found")
}
// isProcessRunning checks if a process with the given PID is still running
func (ais *AudioInputSupervisor) isProcessRunning(pid int) bool {
// Try to send signal 0 to check if process exists
process, err := os.FindProcess(pid)
if err != nil {
return false
}
err = process.Signal(syscall.Signal(0))
return err == nil
}
// HasExistingProcess checks if there's already an audio input server process running
// This is a public wrapper around findExistingAudioInputProcess for external access
func (ais *AudioInputSupervisor) HasExistingProcess() (int, bool) {
pid, err := ais.findExistingAudioInputProcess()
return pid, err == nil
}

View File

@ -4,18 +4,69 @@ import (
"context"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/jetkvm/kvm/internal/logging"
)
// getEnvInt reads an integer from environment variable with a default value
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
// parseOpusConfig reads OPUS configuration from environment variables
// with fallback to default config values
func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
// Read configuration from environment variables with config defaults
bitrate = getEnvInt("JETKVM_OPUS_BITRATE", GetConfig().CGOOpusBitrate)
complexity = getEnvInt("JETKVM_OPUS_COMPLEXITY", GetConfig().CGOOpusComplexity)
vbr = getEnvInt("JETKVM_OPUS_VBR", GetConfig().CGOOpusVBR)
signalType = getEnvInt("JETKVM_OPUS_SIGNAL_TYPE", GetConfig().CGOOpusSignalType)
bandwidth = getEnvInt("JETKVM_OPUS_BANDWIDTH", GetConfig().CGOOpusBandwidth)
dtx = getEnvInt("JETKVM_OPUS_DTX", GetConfig().CGOOpusDTX)
return bitrate, complexity, vbr, signalType, bandwidth, dtx
}
// applyOpusConfig applies OPUS configuration to the global config
func applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
config := GetConfig()
config.CGOOpusBitrate = bitrate
config.CGOOpusComplexity = complexity
config.CGOOpusVBR = vbr
config.CGOOpusSignalType = signalType
config.CGOOpusBandwidth = bandwidth
config.CGOOpusDTX = dtx
logger.Info().
Int("bitrate", bitrate).
Int("complexity", complexity).
Int("vbr", vbr).
Int("signal_type", signalType).
Int("bandwidth", bandwidth).
Int("dtx", dtx).
Msg("applied OPUS configuration")
}
// RunAudioOutputServer runs the audio output server subprocess
// This should be called from main() when the subprocess is detected
func RunAudioOutputServer() error {
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
logger.Debug().Msg("audio output server subprocess starting")
// Parse OPUS configuration from environment variables
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfig()
applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx)
// Initialize validation cache for optimal performance
InitValidationCache()

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
@ -311,10 +312,18 @@ func StartAudioOutputStreaming(send func([]byte)) error {
return ErrAudioAlreadyRunning
}
// Initialize CGO audio capture
if err := CGOAudioInit(); err != nil {
// Initialize CGO audio capture with retry logic
var initErr error
for attempt := 0; attempt < 3; attempt++ {
if initErr = CGOAudioInit(); initErr == nil {
break
}
getOutputStreamingLogger().Warn().Err(initErr).Int("attempt", attempt+1).Msg("Audio initialization failed, retrying")
time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)
}
if initErr != nil {
atomic.StoreInt32(&outputStreamingRunning, 0)
return err
return fmt.Errorf("failed to initialize audio after 3 attempts: %w", initErr)
}
ctx, cancel := context.WithCancel(context.Background())
@ -341,7 +350,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
case <-ctx.Done():
return
default:
// Capture audio frame with enhanced error handling
// Capture audio frame with enhanced error handling and initialization checking
n, err := CGOAudioReadEncode(buffer)
if err != nil {
consecutiveErrors++
@ -350,6 +359,13 @@ func StartAudioOutputStreaming(send func([]byte)) error {
Int("consecutive_errors", consecutiveErrors).
Msg("Failed to read/encode audio")
// Check if this is an initialization error (C error code -1)
if strings.Contains(err.Error(), "C error code -1") {
getOutputStreamingLogger().Error().Msg("Audio system not initialized properly, forcing reinitialization")
// Force immediate reinitialization for init errors
consecutiveErrors = maxConsecutiveErrors
}
// Implement progressive backoff for consecutive errors
if consecutiveErrors >= maxConsecutiveErrors {
getOutputStreamingLogger().Error().

View File

@ -7,6 +7,7 @@ import (
"fmt"
"os"
"os/exec"
"strconv"
"sync/atomic"
"syscall"
"time"
@ -42,6 +43,9 @@ type AudioOutputSupervisor struct {
stopChanClosed bool // Track if stopChan is closed
processDoneClosed bool // Track if processDone is closed
// Environment variables for OPUS configuration
opusEnv []string
// Callbacks
onProcessStart func(pid int)
onProcessExit func(pid int, exitCode int, crashed bool)
@ -72,6 +76,23 @@ func (s *AudioOutputSupervisor) SetCallbacks(
s.onRestart = onRestart
}
// SetOpusConfig sets OPUS configuration parameters as environment variables
// for the audio output subprocess
func (s *AudioOutputSupervisor) SetOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
s.mutex.Lock()
defer s.mutex.Unlock()
// Store OPUS parameters as environment variables
s.opusEnv = []string{
"JETKVM_OPUS_BITRATE=" + strconv.Itoa(bitrate),
"JETKVM_OPUS_COMPLEXITY=" + strconv.Itoa(complexity),
"JETKVM_OPUS_VBR=" + strconv.Itoa(vbr),
"JETKVM_OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType),
"JETKVM_OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth),
"JETKVM_OPUS_DTX=" + strconv.Itoa(dtx),
}
}
// Start begins supervising the audio output server process
func (s *AudioOutputSupervisor) Start() error {
if !atomic.CompareAndSwapInt32(&s.running, 0, 1) {
@ -223,18 +244,24 @@ func (s *AudioOutputSupervisor) startProcess() error {
s.mutex.Lock()
defer s.mutex.Unlock()
// Build command arguments (only subprocess flag)
args := []string{"--audio-output-server"}
// Create new command
s.cmd = exec.CommandContext(s.ctx, execPath, "--audio-output-server")
s.cmd = exec.CommandContext(s.ctx, execPath, args...)
s.cmd.Stdout = os.Stdout
s.cmd.Stderr = os.Stderr
// Set environment variables for OPUS configuration
s.cmd.Env = append(os.Environ(), s.opusEnv...)
// Start the process
if err := s.cmd.Start(); err != nil {
return fmt.Errorf("failed to start audio output server process: %w", err)
}
s.processPID = s.cmd.Process.Pid
s.logger.Info().Int("pid", s.processPID).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
s.processMonitor.AddProcess(s.processPID, "audio-output-server")

19
main.go
View File

@ -44,6 +44,25 @@ func startAudioSubprocess() error {
// Set the global supervisor for access from audio package
audio.SetAudioOutputSupervisor(audioSupervisor)
// Create and register audio input supervisor (but don't start it)
// Audio input will be started on-demand through the UI
audioInputSupervisor := audio.NewAudioInputSupervisor()
audio.SetAudioInputSupervisor(audioInputSupervisor)
// Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106)
config := audio.GetConfig()
audioInputSupervisor.SetOpusConfig(
config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
config.AudioQualityLowOpusComplexity,
config.AudioQualityLowOpusVBR,
config.AudioQualityLowOpusSignalType,
config.AudioQualityLowOpusBandwidth,
config.AudioQualityLowOpusDTX,
)
// Note: Audio input supervisor is NOT started here - it will be started on-demand
// when the user activates microphone input through the UI
// Set up callbacks for process lifecycle events
audioSupervisor.SetCallbacks(
// onProcessStart