mirror of https://github.com/jetkvm/kvm.git
[WIP] Updates: simplify audio system
This commit is contained in:
parent
680607e82e
commit
f6dd605ea6
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -28,8 +28,6 @@ import (
|
|||
"errors"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue