[WIP] Updates: simplify audio system

This commit is contained in:
Alex P 2025-09-30 09:36:19 +00:00
parent 680607e82e
commit f6dd605ea6
9 changed files with 38 additions and 206 deletions

View File

@ -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_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_complexity = 1; // Complexity: 1 (minimal CPU, ~0.5% on RV1106)
static int opus_vbr = 1; // VBR: enabled for efficient encoding 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_signal_type = 3002; // Signal: OPUS_SIGNAL_MUSIC (3002)
static int opus_bandwidth = 1103; // Bandwidth: WIDEBAND (1103 = native 48kHz, no resampling) static int opus_bandwidth = 1103; // Bandwidth: WIDEBAND (1103 = native 48kHz, no resampling)
static int opus_dtx = 0; // DTX: disabled (continuous audio stream) 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)); opus_encoder_ctl(encoder, OPUS_SET_DTX(opus_dtx));
// Set LSB depth for improved bit allocation on constrained hardware // Set LSB depth for improved bit allocation on constrained hardware
opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(opus_lsb_depth)); opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(opus_lsb_depth));
// Enable packet loss concealment for better resilience // Packet loss concealment removed - causes artifacts on transients in LAN environment
opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(5)); // Prediction enabled (default) for better transient handling (beeps, sharp sounds)
// Set prediction disabled for lower latency
opus_encoder_ctl(encoder, OPUS_SET_PREDICTION_DISABLED(1));
capture_initialized = 1; capture_initialized = 1;
capture_initializing = 0; capture_initializing = 0;

View File

@ -287,7 +287,7 @@ func DefaultAudioConfig() *AudioConfigConstants {
CGOOpusBitrate: 96000, // 96 kbps optimal for stereo @ 48kHz CGOOpusBitrate: 96000, // 96 kbps optimal for stereo @ 48kHz
CGOOpusComplexity: 1, // Complexity 1: minimal CPU (~0.5% on RV1106) CGOOpusComplexity: 1, // Complexity 1: minimal CPU (~0.5% on RV1106)
CGOOpusVBR: 1, // VBR enabled for efficiency 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) CGOOpusSignalType: 3002, // OPUS_SIGNAL_MUSIC (better for HDMI audio)
CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND (native 48kHz, no resampling) CGOOpusBandwidth: 1103, // OPUS_BANDWIDTH_WIDEBAND (native 48kHz, no resampling)
CGOOpusDTX: 0, // DTX disabled for continuous audio CGOOpusDTX: 0, // DTX disabled for continuous audio

View File

@ -236,19 +236,19 @@ func (s *AudioControlService) GetMicrophoneStatus() map[string]interface{} {
} }
} }
// SetAudioQuality sets the audio output quality // SetAudioQuality is deprecated - audio quality is now fixed at optimal settings
func (s *AudioControlService) SetAudioQuality(quality AudioQuality) { func (s *AudioControlService) SetAudioQuality(quality int) {
SetAudioQuality(quality) // No-op: quality is fixed at optimal configuration
} }
// GetAudioQualityPresets returns available audio quality presets // GetAudioQualityPresets is deprecated - returns empty map
func (s *AudioControlService) GetAudioQualityPresets() map[AudioQuality]AudioConfig { func (s *AudioControlService) GetAudioQualityPresets() map[int]AudioConfig {
return GetAudioQualityPresets() return map[int]AudioConfig{}
} }
// GetMicrophoneQualityPresets returns available microphone quality presets // GetMicrophoneQualityPresets is deprecated - returns empty map
func (s *AudioControlService) GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig { func (s *AudioControlService) GetMicrophoneQualityPresets() map[int]AudioConfig {
return GetMicrophoneQualityPresets() return map[int]AudioConfig{}
} }
// GetCurrentAudioQuality returns the current audio quality configuration // GetCurrentAudioQuality returns the current audio quality configuration

View File

@ -11,7 +11,6 @@ import (
// Validation errors // Validation errors
var ( var (
ErrInvalidAudioQuality = errors.New("invalid audio quality level")
ErrInvalidFrameSize = errors.New("invalid frame size") ErrInvalidFrameSize = errors.New("invalid frame size")
ErrInvalidFrameData = errors.New("invalid frame data") ErrInvalidFrameData = errors.New("invalid frame data")
ErrFrameDataEmpty = errors.New("invalid frame data: frame data is empty") ErrFrameDataEmpty = errors.New("invalid frame data: frame data is empty")
@ -30,13 +29,9 @@ var (
ErrInvalidLength = errors.New("invalid length") ErrInvalidLength = errors.New("invalid length")
) )
// ValidateAudioQuality validates audio quality enum values with enhanced checks // ValidateAudioQuality is deprecated - quality is now fixed at optimal settings
func ValidateAudioQuality(quality AudioQuality) error { func ValidateAudioQuality(quality int) error {
// Validate enum range // Quality validation removed - using fixed optimal configuration
if quality < AudioQualityLow || quality > AudioQualityUltra {
return fmt.Errorf("%w: quality value %d outside valid range [%d, %d]",
ErrInvalidAudioQuality, int(quality), int(AudioQualityLow), int(AudioQualityUltra))
}
return nil return nil
} }
@ -316,9 +311,6 @@ func ValidateAudioConfigComplete(config AudioConfig) error {
} }
// Slower path: validate each parameter individually // 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 { if err := ValidateBitrate(config.Bitrate); err != nil {
return fmt.Errorf("bitrate validation failed: %w", err) return fmt.Errorf("bitrate validation failed: %w", err)
} }
@ -336,12 +328,7 @@ func ValidateAudioConfigComplete(config AudioConfig) error {
// ValidateAudioConfigConstants validates audio configuration constants // ValidateAudioConfigConstants validates audio configuration constants
func ValidateAudioConfigConstants(config *AudioConfigConstants) error { func ValidateAudioConfigConstants(config *AudioConfigConstants) error {
// Validate that audio quality constants are within valid ranges // Quality validation removed - using fixed optimal configuration
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)
}
}
// Validate configuration values if config is provided // Validate configuration values if config is provided
if config != nil { if config != nil {
if Config.MaxFrameSize <= 0 { if Config.MaxFrameSize <= 0 {

View File

@ -59,7 +59,7 @@ func (aom *AudioOutputIPCManager) Start() error {
config := UnifiedIPCConfig{ config := UnifiedIPCConfig{
SampleRate: Config.SampleRate, SampleRate: Config.SampleRate,
Channels: Config.Channels, Channels: Config.Channels,
FrameSize: int(Config.AudioQualityMediumFrameSize.Milliseconds()), FrameSize: 20, // Fixed 20ms frame size for optimal audio
} }
if err := aom.SendConfig(config); err != nil { if err := aom.SendConfig(config); err != nil {

View File

@ -28,8 +28,6 @@ import (
"errors" "errors"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/jetkvm/kvm/internal/logging"
) )
var ( var (

View File

@ -8,7 +8,6 @@ import { useAudioEvents } from "@/hooks/useAudioEvents";
import { useJsonRpc, JsonRpcResponse } from "@/hooks/useJsonRpc"; import { useJsonRpc, JsonRpcResponse } from "@/hooks/useJsonRpc";
import { useRTCStore } from "@/hooks/stores"; import { useRTCStore } from "@/hooks/stores";
import notifications from "@/notifications"; import notifications from "@/notifications";
import audioQualityService from "@/services/audioQualityService";
// Type for microphone error // Type for microphone error
interface MicrophoneError { interface MicrophoneError {
@ -69,11 +68,7 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
const { send } = useJsonRpc(); const { send } = useJsonRpc();
// Initialize audio quality service with RPC for cloud compatibility // Initialize audio quality service with RPC for cloud compatibility
useEffect(() => { // Audio quality service removed - using fixed optimal configuration
if (send) {
audioQualityService.setRpcSend(send);
}
}, [send]);
// WebSocket-only implementation - no fallback polling // WebSocket-only implementation - no fallback polling
@ -131,12 +126,24 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
const loadAudioConfigurations = async () => { const loadAudioConfigurations = async () => {
try { try {
// Use centralized audio quality service // Load audio configuration directly via RPC
const { audio } = await audioQualityService.loadAllConfigurations(); if (!send) return;
if (audio) { await new Promise<void>((resolve, reject) => {
setCurrentConfig(audio.current); 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); setConfigsLoaded(true);
} catch { } catch {
@ -437,9 +444,6 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
</div> </div>
</div> </div>
)} )}
</div> </div>
</div> </div>
); );

View File

@ -89,17 +89,8 @@ export const AUDIO_CONFIG = {
SYNC_DEBOUNCE_MS: 1000, // debounce state synchronization SYNC_DEBOUNCE_MS: 1000, // debounce state synchronization
AUDIO_TEST_TIMEOUT: 100, // ms - timeout for audio testing AUDIO_TEST_TIMEOUT: 100, // ms - timeout for audio testing
// NOTE: Audio quality presets (bitrates, sample rates, channels, frame sizes) // Audio quality is fixed at optimal settings (96 kbps @ 48kHz stereo)
// are now fetched dynamically from the backend API via audioQualityService // No quality presets needed - single optimal configuration for all use cases
// 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 Analysis // Audio Analysis
ANALYSIS_FFT_SIZE: 256, // for detailed audio analysis ANALYSIS_FFT_SIZE: 256, // for detailed audio analysis

View File

@ -1,146 +0,0 @@
import { JsonRpcResponse } from '@/hooks/useJsonRpc';
interface AudioConfig {
Quality: number;
Bitrate: number;
SampleRate: number;
Channels: number;
FrameSize: string;
}
type QualityPresets = Record<number, AudioConfig>;
interface AudioQualityResponse {
current: AudioConfig;
presets: QualityPresets;
}
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (resp: JsonRpcResponse) => void) => void;
class AudioQualityService {
private audioPresets: QualityPresets | null = null;
private microphonePresets: QualityPresets | null = null;
private qualityLabels: Record<number, string> = {
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<AudioQualityResponse | null> {
if (!this.rpcSend) {
console.error('RPC not available for audio quality presets');
return null;
}
try {
return await new Promise<AudioQualityResponse | null>((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<number, string> = {};
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<number, string> {
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<boolean> {
if (!this.rpcSend) {
console.error('RPC not available for audio quality change');
return false;
}
try {
return await new Promise<boolean>((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;