[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_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;

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

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

View File

@ -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<void>((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
</div>
</div>
)}
</div>
</div>
);

View File

@ -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

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;