diff --git a/internal/audio/c/audio.c b/internal/audio/c/audio.c index cc1ce769..d9e0b0aa 100644 --- a/internal/audio/c/audio.c +++ b/internal/audio/c/audio.c @@ -62,7 +62,7 @@ static int frame_size = 960; // Frames per Opus packet static int opus_bitrate = 96000; // Bitrate: 96 kbps (optimal for stereo @ 48kHz) static int opus_complexity = 1; // Complexity: 1 (minimal CPU, ~0.5% on RV1106) static int opus_vbr = 1; // VBR: enabled for efficient encoding -static int opus_vbr_constraint = 1; // Constrained VBR: predictable bitrate +static int opus_vbr_constraint = 0; // Unconstrained VBR: allows bitrate spikes for transients (beeps/sharp sounds) static int opus_signal_type = 3002; // Signal: OPUS_SIGNAL_MUSIC (3002) static int opus_bandwidth = 1103; // Bandwidth: WIDEBAND (1103 = native 48kHz, no resampling) static int opus_dtx = 0; // DTX: disabled (continuous audio stream) @@ -745,10 +745,8 @@ int jetkvm_audio_capture_init() { opus_encoder_ctl(encoder, OPUS_SET_DTX(opus_dtx)); // Set LSB depth for improved bit allocation on constrained hardware opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(opus_lsb_depth)); - // Enable packet loss concealment for better resilience - opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(5)); - // Set prediction disabled for lower latency - opus_encoder_ctl(encoder, OPUS_SET_PREDICTION_DISABLED(1)); + // Packet loss concealment removed - causes artifacts on transients in LAN environment + // Prediction enabled (default) for better transient handling (beeps, sharp sounds) capture_initialized = 1; capture_initializing = 0; diff --git a/internal/audio/core_config_constants.go b/internal/audio/core_config_constants.go index 2ef27167..ea737dd2 100644 --- a/internal/audio/core_config_constants.go +++ b/internal/audio/core_config_constants.go @@ -287,7 +287,7 @@ func DefaultAudioConfig() *AudioConfigConstants { CGOOpusBitrate: 96000, // 96 kbps optimal for stereo @ 48kHz CGOOpusComplexity: 1, // Complexity 1: minimal CPU (~0.5% on RV1106) CGOOpusVBR: 1, // VBR enabled for efficiency - CGOOpusVBRConstraint: 1, // Constrained VBR for predictable bitrate + CGOOpusVBRConstraint: 0, // Unconstrained VBR: allows bitrate spikes for transients (beeps/sharp sounds) CGOOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC (better for HDMI audio) CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND (native 48kHz, no resampling) CGOOpusDTX: 0, // DTX disabled for continuous audio diff --git a/internal/audio/core_handlers.go b/internal/audio/core_handlers.go index 501ad1f7..d38256d2 100644 --- a/internal/audio/core_handlers.go +++ b/internal/audio/core_handlers.go @@ -236,19 +236,19 @@ func (s *AudioControlService) GetMicrophoneStatus() map[string]interface{} { } } -// SetAudioQuality sets the audio output quality -func (s *AudioControlService) SetAudioQuality(quality AudioQuality) { - SetAudioQuality(quality) +// SetAudioQuality is deprecated - audio quality is now fixed at optimal settings +func (s *AudioControlService) SetAudioQuality(quality int) { + // No-op: quality is fixed at optimal configuration } -// GetAudioQualityPresets returns available audio quality presets -func (s *AudioControlService) GetAudioQualityPresets() map[AudioQuality]AudioConfig { - return GetAudioQualityPresets() +// GetAudioQualityPresets is deprecated - returns empty map +func (s *AudioControlService) GetAudioQualityPresets() map[int]AudioConfig { + return map[int]AudioConfig{} } -// GetMicrophoneQualityPresets returns available microphone quality presets -func (s *AudioControlService) GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig { - return GetMicrophoneQualityPresets() +// GetMicrophoneQualityPresets is deprecated - returns empty map +func (s *AudioControlService) GetMicrophoneQualityPresets() map[int]AudioConfig { + return map[int]AudioConfig{} } // GetCurrentAudioQuality returns the current audio quality configuration diff --git a/internal/audio/core_validation.go b/internal/audio/core_validation.go index 9aff34a0..3fa296cc 100644 --- a/internal/audio/core_validation.go +++ b/internal/audio/core_validation.go @@ -11,7 +11,6 @@ import ( // Validation errors var ( - ErrInvalidAudioQuality = errors.New("invalid audio quality level") ErrInvalidFrameSize = errors.New("invalid frame size") ErrInvalidFrameData = errors.New("invalid frame data") ErrFrameDataEmpty = errors.New("invalid frame data: frame data is empty") @@ -30,13 +29,9 @@ var ( ErrInvalidLength = errors.New("invalid length") ) -// ValidateAudioQuality validates audio quality enum values with enhanced checks -func ValidateAudioQuality(quality AudioQuality) error { - // Validate enum range - if quality < AudioQualityLow || quality > AudioQualityUltra { - return fmt.Errorf("%w: quality value %d outside valid range [%d, %d]", - ErrInvalidAudioQuality, int(quality), int(AudioQualityLow), int(AudioQualityUltra)) - } +// ValidateAudioQuality is deprecated - quality is now fixed at optimal settings +func ValidateAudioQuality(quality int) error { + // Quality validation removed - using fixed optimal configuration return nil } @@ -316,9 +311,6 @@ func ValidateAudioConfigComplete(config AudioConfig) error { } // Slower path: validate each parameter individually - if err := ValidateAudioQuality(config.Quality); err != nil { - return fmt.Errorf("quality validation failed: %w", err) - } if err := ValidateBitrate(config.Bitrate); err != nil { return fmt.Errorf("bitrate validation failed: %w", err) } @@ -336,12 +328,7 @@ func ValidateAudioConfigComplete(config AudioConfig) error { // ValidateAudioConfigConstants validates audio configuration constants func ValidateAudioConfigConstants(config *AudioConfigConstants) error { - // Validate that audio quality constants are within valid ranges - for _, quality := range []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} { - if err := ValidateAudioQuality(quality); err != nil { - return fmt.Errorf("invalid audio quality constant %v: %w", quality, err) - } - } + // Quality validation removed - using fixed optimal configuration // Validate configuration values if config is provided if config != nil { if Config.MaxFrameSize <= 0 { diff --git a/internal/audio/mgmt_output_ipc_manager.go b/internal/audio/mgmt_output_ipc_manager.go index bb80f61d..3d8dfac5 100644 --- a/internal/audio/mgmt_output_ipc_manager.go +++ b/internal/audio/mgmt_output_ipc_manager.go @@ -59,7 +59,7 @@ func (aom *AudioOutputIPCManager) Start() error { config := UnifiedIPCConfig{ SampleRate: Config.SampleRate, Channels: Config.Channels, - FrameSize: int(Config.AudioQualityMediumFrameSize.Milliseconds()), + FrameSize: 20, // Fixed 20ms frame size for optimal audio } if err := aom.SendConfig(config); err != nil { diff --git a/internal/audio/quality_presets.go b/internal/audio/quality_presets.go index 52f7e768..25cf603f 100644 --- a/internal/audio/quality_presets.go +++ b/internal/audio/quality_presets.go @@ -28,8 +28,6 @@ import ( "errors" "sync/atomic" "time" - - "github.com/jetkvm/kvm/internal/logging" ) var ( diff --git a/ui/src/components/popovers/AudioControlPopover.tsx b/ui/src/components/popovers/AudioControlPopover.tsx index e8dae58a..70422c9d 100644 --- a/ui/src/components/popovers/AudioControlPopover.tsx +++ b/ui/src/components/popovers/AudioControlPopover.tsx @@ -8,7 +8,6 @@ import { useAudioEvents } from "@/hooks/useAudioEvents"; import { useJsonRpc, JsonRpcResponse } from "@/hooks/useJsonRpc"; import { useRTCStore } from "@/hooks/stores"; import notifications from "@/notifications"; -import audioQualityService from "@/services/audioQualityService"; // Type for microphone error interface MicrophoneError { @@ -69,11 +68,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP const { send } = useJsonRpc(); // Initialize audio quality service with RPC for cloud compatibility - useEffect(() => { - if (send) { - audioQualityService.setRpcSend(send); - } - }, [send]); + // Audio quality service removed - using fixed optimal configuration // WebSocket-only implementation - no fallback polling @@ -131,12 +126,24 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP const loadAudioConfigurations = async () => { try { - // Use centralized audio quality service - const { audio } = await audioQualityService.loadAllConfigurations(); + // Load audio configuration directly via RPC + if (!send) return; - if (audio) { - setCurrentConfig(audio.current); - } + await new Promise((resolve, reject) => { + send("audioStatus", {}, (resp: JsonRpcResponse) => { + if ("error" in resp) { + reject(new Error(resp.error.message)); + } else if ("result" in resp && resp.result) { + const result = resp.result as any; + if (result.config) { + setCurrentConfig(result.config); + } + resolve(); + } else { + resolve(); + } + }); + }); setConfigsLoaded(true); } catch { @@ -437,9 +444,6 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP )} - - - ); diff --git a/ui/src/config/constants.ts b/ui/src/config/constants.ts index da0da3a0..d9e3d10c 100644 --- a/ui/src/config/constants.ts +++ b/ui/src/config/constants.ts @@ -89,17 +89,8 @@ export const AUDIO_CONFIG = { SYNC_DEBOUNCE_MS: 1000, // debounce state synchronization AUDIO_TEST_TIMEOUT: 100, // ms - timeout for audio testing - // NOTE: Audio quality presets (bitrates, sample rates, channels, frame sizes) - // are now fetched dynamically from the backend API via audioQualityService - // to eliminate duplication with backend config_constants.go - - // Default Quality Labels - will be updated dynamically by audioQualityService - DEFAULT_QUALITY_LABELS: { - 0: "Low", - 1: "Medium", - 2: "High", - 3: "Ultra", - } as const, + // Audio quality is fixed at optimal settings (96 kbps @ 48kHz stereo) + // No quality presets needed - single optimal configuration for all use cases // Audio Analysis ANALYSIS_FFT_SIZE: 256, // for detailed audio analysis diff --git a/ui/src/services/audioQualityService.ts b/ui/src/services/audioQualityService.ts deleted file mode 100644 index d2454c62..00000000 --- a/ui/src/services/audioQualityService.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { JsonRpcResponse } from '@/hooks/useJsonRpc'; - -interface AudioConfig { - Quality: number; - Bitrate: number; - SampleRate: number; - Channels: number; - FrameSize: string; -} - -type QualityPresets = Record; - -interface AudioQualityResponse { - current: AudioConfig; - presets: QualityPresets; -} - -type RpcSendFunction = (method: string, params: Record, callback: (resp: JsonRpcResponse) => void) => void; - -class AudioQualityService { - private audioPresets: QualityPresets | null = null; - private microphonePresets: QualityPresets | null = null; - private qualityLabels: Record = { - 0: 'Low', - 1: 'Medium', - 2: 'High', - 3: 'Ultra' - }; - private rpcSend: RpcSendFunction | null = null; - - /** - * Set RPC send function for cloud compatibility - */ - setRpcSend(rpcSend: RpcSendFunction): void { - this.rpcSend = rpcSend; - } - - /** - * Fetch audio quality presets using RPC (cloud-compatible) - */ - async fetchAudioQualityPresets(): Promise { - if (!this.rpcSend) { - console.error('RPC not available for audio quality presets'); - return null; - } - - try { - return await new Promise((resolve) => { - this.rpcSend!("audioQualityPresets", {}, (resp: JsonRpcResponse) => { - if ("error" in resp) { - console.error('RPC audio quality presets failed:', resp.error); - resolve(null); - } else if ("result" in resp) { - const data = resp.result as AudioQualityResponse; - this.audioPresets = data.presets; - this.updateQualityLabels(data.presets); - resolve(data); - } else { - resolve(null); - } - }); - }); - } catch (error) { - console.error('Failed to fetch audio quality presets:', error); - return null; - } - } - - /** - * Update quality labels with actual bitrates from presets - */ - private updateQualityLabels(presets: QualityPresets): void { - const newQualityLabels: Record = {}; - Object.entries(presets).forEach(([qualityNum, preset]) => { - const quality = parseInt(qualityNum); - const qualityNames = ['Low', 'Medium', 'High', 'Ultra']; - const name = qualityNames[quality] || `Quality ${quality}`; - newQualityLabels[quality] = `${name} (${preset.Bitrate}kbps)`; - }); - this.qualityLabels = newQualityLabels; - } - - /** - * Get quality labels with bitrates - */ - getQualityLabels(): Record { - return this.qualityLabels; - } - - /** - * Get cached audio presets - */ - getAudioPresets(): QualityPresets | null { - return this.audioPresets; - } - - /** - * Get cached microphone presets - */ - getMicrophonePresets(): QualityPresets | null { - return this.microphonePresets; - } - - /** - * Set audio quality using RPC (cloud-compatible) - */ - async setAudioQuality(quality: number): Promise { - if (!this.rpcSend) { - console.error('RPC not available for audio quality change'); - return false; - } - - try { - return await new Promise((resolve) => { - this.rpcSend!("audioQuality", { quality }, (resp: JsonRpcResponse) => { - if ("error" in resp) { - console.error('RPC audio quality change failed:', resp.error); - resolve(false); - } else { - resolve(true); - } - }); - }); - } catch (error) { - console.error('Failed to set audio quality:', error); - return false; - } - } - - /** - * Load both audio and microphone configurations - */ - async loadAllConfigurations(): Promise<{ - audio: AudioQualityResponse | null; - }> { - const [audio ] = await Promise.all([ - this.fetchAudioQualityPresets(), - ]); - - return { audio }; - } -} - -// Export a singleton instance -export const audioQualityService = new AudioQualityService(); -export default audioQualityService; \ No newline at end of file