diff --git a/audio.go b/audio.go index 00b8c764..c031492e 100644 --- a/audio.go +++ b/audio.go @@ -39,7 +39,23 @@ func initAudio() { audioInitialized = true } -// startAudio starts audio sources and relays (skips already running ones) +func getAudioConfig() audio.AudioConfig { + ensureConfigLoaded() + cfg := audio.DefaultAudioConfig() + if config.AudioBitrate >= 64 && config.AudioBitrate <= 256 { + cfg.Bitrate = uint16(config.AudioBitrate) + } + if config.AudioComplexity >= 0 && config.AudioComplexity <= 10 { + cfg.Complexity = uint8(config.AudioComplexity) + } + cfg.DTXEnabled = config.AudioDTXEnabled + cfg.FECEnabled = config.AudioFECEnabled + if config.AudioBufferPeriods >= 2 && config.AudioBufferPeriods <= 24 { + cfg.BufferPeriods = uint8(config.AudioBufferPeriods) + } + return cfg +} + func startAudio() error { audioMutex.Lock() defer audioMutex.Unlock() @@ -49,30 +65,32 @@ func startAudio() error { return nil } - // Start output audio if not running, enabled, and we have a track if outputSource == nil && audioOutputEnabled.Load() && currentAudioTrack != nil { ensureConfigLoaded() - alsaDevice := "hw:1,0" // USB audio (default) + alsaDevice := "hw:1,0" if config.AudioOutputSource == "hdmi" { - alsaDevice = "hw:0,0" // HDMI audio + alsaDevice = "hw:0,0" } - outputSource = audio.NewCgoOutputSource(alsaDevice) + source := audio.NewCgoOutputSource(alsaDevice) + source.SetConfig(getAudioConfig()) + outputSource = source outputRelay = audio.NewOutputRelay(outputSource, currentAudioTrack) if err := outputRelay.Start(); err != nil { audioLogger.Error().Err(err).Msg("Failed to start audio output relay") } } - // Start input audio if not running, USB audio enabled, and input enabled ensureConfigLoaded() if inputSource.Load() == nil && audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio { - alsaPlaybackDevice := "hw:1,0" // USB speakers + alsaPlaybackDevice := "hw:1,0" - var source audio.AudioSource = audio.NewCgoInputSource(alsaPlaybackDevice) - inputSource.Store(&source) + source := audio.NewCgoInputSource(alsaPlaybackDevice) + source.SetConfig(getAudioConfig()) + var audioSource audio.AudioSource = source + inputSource.Store(&audioSource) - inputRelay = audio.NewInputRelay(source) + inputRelay = audio.NewInputRelay(audioSource) if err := inputRelay.Start(); err != nil { audioLogger.Error().Err(err).Msg("Failed to start input relay") } @@ -158,11 +176,12 @@ func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) { audioMutex.Lock() if currentAudioTrack != nil && audioOutputEnabled.Load() { ensureConfigLoaded() - alsaDevice := "hw:1,0" // USB audio (default) + alsaDevice := "hw:1,0" if config.AudioOutputSource == "hdmi" { - alsaDevice = "hw:0,0" // HDMI audio + alsaDevice = "hw:0,0" } newSource := audio.NewCgoOutputSource(alsaDevice) + newSource.SetConfig(getAudioConfig()) newRelay := audio.NewOutputRelay(newSource, currentAudioTrack) outputSource = newSource outputRelay = newRelay @@ -187,10 +206,9 @@ func setPendingInputTrack(track *webrtc.TrackRemote) { go handleInputTrackForSession(track) } -// SetAudioOutputEnabled enables or disables audio output func SetAudioOutputEnabled(enabled bool) error { if audioOutputEnabled.Swap(enabled) == enabled { - return nil // Already in desired state + return nil } if enabled { @@ -204,10 +222,9 @@ func SetAudioOutputEnabled(enabled bool) error { return nil } -// SetAudioInputEnabled enables or disables audio input func SetAudioInputEnabled(enabled bool) error { if audioInputEnabled.Swap(enabled) == enabled { - return nil // Already in desired state + return nil } if enabled { @@ -221,7 +238,6 @@ func SetAudioInputEnabled(enabled bool) error { return nil } -// SetAudioOutputSource switches between HDMI and USB audio sources func SetAudioOutputSource(source string) error { if source != "hdmi" && source != "usb" { return nil @@ -237,12 +253,13 @@ func SetAudioOutputSource(source string) error { stopOutputAudio() if audioOutputEnabled.Load() && activeConnections.Load() > 0 && currentAudioTrack != nil { - alsaDevice := "hw:1,0" // USB + alsaDevice := "hw:1,0" if source == "hdmi" { - alsaDevice = "hw:0,0" // HDMI + alsaDevice = "hw:0,0" } newSource := audio.NewCgoOutputSource(alsaDevice) + newSource.SetConfig(getAudioConfig()) newRelay := audio.NewOutputRelay(newSource, currentAudioTrack) audioMutex.Lock() @@ -258,54 +275,79 @@ func SetAudioOutputSource(source string) error { return SaveConfig() } -// handleInputTrackForSession runs for the entire WebRTC session lifetime -// It continuously reads from the track and sends to whatever relay is currently active +func RestartAudioOutput() { + audioMutex.Lock() + hasActiveOutput := outputSource != nil && currentAudioTrack != nil && audioOutputEnabled.Load() + audioMutex.Unlock() + + if !hasActiveOutput { + return + } + + audioLogger.Info().Msg("Restarting audio output") + + stopOutputAudio() + + ensureConfigLoaded() + alsaDevice := "hw:1,0" + if config.AudioOutputSource == "hdmi" { + alsaDevice = "hw:0,0" + } + + newSource := audio.NewCgoOutputSource(alsaDevice) + newSource.SetConfig(getAudioConfig()) + newRelay := audio.NewOutputRelay(newSource, currentAudioTrack) + + audioMutex.Lock() + outputSource = newSource + outputRelay = newRelay + audioMutex.Unlock() + + if err := newRelay.Start(); err != nil { + audioLogger.Error().Err(err).Msg("Failed to restart audio output") + } +} + func handleInputTrackForSession(track *webrtc.TrackRemote) { myTrackID := track.ID() audioLogger.Debug(). Str("codec", track.Codec().MimeType). Str("track_id", myTrackID). - Msg("starting session-lifetime track handler") + Msg("starting input track handler") for { - // Check if we've been superseded by a new track currentTrackID := currentInputTrack.Load() if currentTrackID != nil && *currentTrackID != myTrackID { audioLogger.Debug(). Str("my_track_id", myTrackID). Str("current_track_id", *currentTrackID). - Msg("audio track handler exiting - superseded by new track") + Msg("input track handler exiting - superseded") return } - // Read RTP packet (must always read to keep track alive) rtpPacket, _, err := track.ReadRTP() if err != nil { if err == io.EOF { - audioLogger.Debug().Str("track_id", myTrackID).Msg("audio track ended") + audioLogger.Debug().Str("track_id", myTrackID).Msg("input track ended") return } audioLogger.Warn().Err(err).Str("track_id", myTrackID).Msg("failed to read RTP packet") continue } - // Extract Opus payload opusData := rtpPacket.Payload if len(opusData) == 0 { continue } - // Only send if input is enabled if !audioInputEnabled.Load() { - continue // Drop frame but keep reading + continue } - // Lock-free source access (hot path optimization) source := inputSource.Load() - if source == nil { - continue // No relay, drop frame but keep reading + continue } inputSourceMutex.Lock() diff --git a/config.go b/config.go index 792d8605..a155c7ee 100644 --- a/config.go +++ b/config.go @@ -110,6 +110,11 @@ type Config struct { AudioInputAutoEnable bool `json:"audio_input_auto_enable"` AudioOutputEnabled bool `json:"audio_output_enabled"` AudioOutputSource string `json:"audio_output_source"` // "hdmi" or "usb" + AudioBitrate int `json:"audio_bitrate"` // kbps (64-256) + AudioComplexity int `json:"audio_complexity"` // 0-10 + AudioDTXEnabled bool `json:"audio_dtx_enabled"` + AudioFECEnabled bool `json:"audio_fec_enabled"` + AudioBufferPeriods int `json:"audio_buffer_periods"` // 2-24 } func (c *Config) GetDisplayRotation() uint16 { @@ -186,6 +191,11 @@ func getDefaultConfig() Config { AudioInputAutoEnable: false, AudioOutputEnabled: true, AudioOutputSource: "usb", + AudioBitrate: 128, + AudioComplexity: 5, + AudioDTXEnabled: true, + AudioFECEnabled: true, + AudioBufferPeriods: 12, } } @@ -256,6 +266,15 @@ func LoadConfig() { loadedConfig.JigglerConfig = getDefaultConfig().JigglerConfig } + if loadedConfig.AudioBitrate == 0 { + defaults := getDefaultConfig() + loadedConfig.AudioBitrate = defaults.AudioBitrate + loadedConfig.AudioComplexity = defaults.AudioComplexity + loadedConfig.AudioDTXEnabled = defaults.AudioDTXEnabled + loadedConfig.AudioFECEnabled = defaults.AudioFECEnabled + loadedConfig.AudioBufferPeriods = defaults.AudioBufferPeriods + } + // fixup old keyboard layout value if loadedConfig.KeyboardLayout == "en_US" { loadedConfig.KeyboardLayout = "en-US" diff --git a/internal/audio/c/audio.c b/internal/audio/c/audio.c index a8a5a9e9..276fb251 100644 --- a/internal/audio/c/audio.c +++ b/internal/audio/c/audio.c @@ -2,7 +2,6 @@ * JetKVM Audio Processing Module * * Bidirectional audio processing optimized for ARM NEON SIMD: -* TODO: Remove USB Gadget audio once new system image release is made available * - OUTPUT PATH: TC358743 HDMI or USB Gadget audio → Client speakers * Pipeline: ALSA hw:0,0 or hw:1,0 capture → Opus encode (128kbps, FEC enabled) * @@ -56,18 +55,20 @@ static uint8_t channels = 2; static uint16_t frame_size = 960; // 20ms frames at 48kHz static uint32_t opus_bitrate = 128000; -static uint8_t opus_complexity = 5; // Higher complexity for better quality +static uint8_t opus_complexity = 5; static uint16_t max_packet_size = 1500; -// Opus encoder constants (hardcoded for production) -#define OPUS_VBR 1 // VBR enabled -#define OPUS_VBR_CONSTRAINT 1 // Constrained VBR (prevents bitrate starvation at low volumes) -#define OPUS_SIGNAL_TYPE 3002 // OPUS_SIGNAL_MUSIC (better transient handling) -#define OPUS_BANDWIDTH 1104 // OPUS_BANDWIDTH_SUPERWIDEBAND (16kHz) -#define OPUS_DTX 1 // DTX enabled (bandwidth optimization) -#define OPUS_LSB_DEPTH 16 // 16-bit depth +#define OPUS_VBR 1 +#define OPUS_VBR_CONSTRAINT 1 +#define OPUS_SIGNAL_TYPE 3002 +#define OPUS_BANDWIDTH 1104 +#define OPUS_LSB_DEPTH 16 + +static uint8_t opus_dtx_enabled = 1; +static uint8_t opus_fec_enabled = 1; +static uint8_t opus_packet_loss_perc = 20; +static uint8_t buffer_period_count = 12; -// ALSA retry configuration static uint32_t sleep_microseconds = 1000; static uint32_t sleep_milliseconds = 1; static uint8_t max_attempts_global = 5; @@ -86,43 +87,45 @@ int jetkvm_audio_decode_write(void *opus_buf, int opus_size); void update_audio_constants(uint32_t bitrate, uint8_t complexity, uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt, - uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff); + uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff, + uint8_t dtx_enabled, uint8_t fec_enabled, uint8_t buf_periods); void update_audio_decoder_constants(uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt, - uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff); + uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff, + uint8_t buf_periods); int update_opus_encoder_params(uint32_t bitrate, uint8_t complexity); -/** - * Sync encoder configuration from Go to C - */ void update_audio_constants(uint32_t bitrate, uint8_t complexity, uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt, - uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff) { - opus_bitrate = bitrate; - opus_complexity = complexity; - sample_rate = sr; - channels = ch; - frame_size = fs; - max_packet_size = max_pkt; - sleep_microseconds = sleep_us; - sleep_milliseconds = sleep_us / 1000; // Precompute for snd_pcm_wait - max_attempts_global = max_attempts; - max_backoff_us_global = max_backoff; + uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff, + uint8_t dtx_enabled, uint8_t fec_enabled, uint8_t buf_periods) { + opus_bitrate = (bitrate >= 64000 && bitrate <= 256000) ? bitrate : 128000; + opus_complexity = (complexity <= 10) ? complexity : 5; + sample_rate = sr > 0 ? sr : 48000; + channels = (ch == 1 || ch == 2) ? ch : 2; + frame_size = fs > 0 ? fs : 960; + max_packet_size = max_pkt > 0 ? max_pkt : 1500; + sleep_microseconds = sleep_us > 0 ? sleep_us : 1000; + sleep_milliseconds = sleep_microseconds / 1000; + max_attempts_global = max_attempts > 0 ? max_attempts : 5; + max_backoff_us_global = max_backoff > 0 ? max_backoff : 500000; + opus_dtx_enabled = dtx_enabled ? 1 : 0; + opus_fec_enabled = fec_enabled ? 1 : 0; + buffer_period_count = (buf_periods >= 2 && buf_periods <= 24) ? buf_periods : 12; } -/** - * Sync decoder configuration from Go to C (no encoder-only params) - */ void update_audio_decoder_constants(uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt, - uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff) { - sample_rate = sr; - channels = ch; - frame_size = fs; - max_packet_size = max_pkt; - sleep_microseconds = sleep_us; - sleep_milliseconds = sleep_us / 1000; // Precompute for snd_pcm_wait - max_attempts_global = max_attempts; - max_backoff_us_global = max_backoff; + uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff, + uint8_t buf_periods) { + sample_rate = sr > 0 ? sr : 48000; + channels = (ch == 1 || ch == 2) ? ch : 2; + frame_size = fs > 0 ? fs : 960; + max_packet_size = max_pkt > 0 ? max_pkt : 1500; + sleep_microseconds = sleep_us > 0 ? sleep_us : 1000; + sleep_milliseconds = sleep_microseconds / 1000; + max_attempts_global = max_attempts > 0 ? max_attempts : 5; + max_backoff_us_global = max_backoff > 0 ? max_backoff : 500000; + buffer_period_count = (buf_periods >= 2 && buf_periods <= 24) ? buf_periods : 12; } /** @@ -233,18 +236,17 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream attempt++; - // Exponential backoff with bit shift (faster than multiplication) if (err == -EBUSY || err == -EAGAIN) { precise_sleep_us(backoff_us); - backoff_us = (backoff_us << 1 < max_backoff_us_global) ? (backoff_us << 1) : max_backoff_us_global; + backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000; } else if (err == -ENODEV || err == -ENOENT) { - precise_sleep_us(backoff_us << 1); - backoff_us = (backoff_us << 1 < max_backoff_us_global) ? (backoff_us << 1) : max_backoff_us_global; + precise_sleep_us(backoff_us); + backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000; } else if (err == -EPERM || err == -EACCES) { precise_sleep_us(backoff_us >> 1); } else { precise_sleep_us(backoff_us); - backoff_us = (backoff_us << 1 < max_backoff_us_global) ? (backoff_us << 1) : max_backoff_us_global; + backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000; } } return err; @@ -285,13 +287,13 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) { if (err < 0) return err; } - snd_pcm_uframes_t period_size = frame_size; // Optimized: use full frame as period + snd_pcm_uframes_t period_size = frame_size; if (period_size < 64) period_size = 64; err = snd_pcm_hw_params_set_period_size_near(handle, params, &period_size, 0); if (err < 0) return err; - snd_pcm_uframes_t buffer_size = period_size * 12; // 12 periods = 240ms buffer for better jitter tolerance + snd_pcm_uframes_t buffer_size = period_size * buffer_period_count; err = snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffer_size); if (err < 0) return err; @@ -378,11 +380,11 @@ int jetkvm_audio_capture_init() { opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(OPUS_VBR_CONSTRAINT)); opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_TYPE)); opus_encoder_ctl(encoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH)); - opus_encoder_ctl(encoder, OPUS_SET_DTX(OPUS_DTX)); + opus_encoder_ctl(encoder, OPUS_SET_DTX(opus_dtx_enabled)); opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(OPUS_LSB_DEPTH)); - opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(1)); - opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(20)); + opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(opus_fec_enabled)); + opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(opus_packet_loss_perc)); capture_initialized = 1; capture_initializing = 0; diff --git a/internal/audio/cgo_source.go b/internal/audio/cgo_source.go index d985e507..fc4b80b4 100644 --- a/internal/audio/cgo_source.go +++ b/internal/audio/cgo_source.go @@ -21,21 +21,20 @@ import ( ) const ( - ipcMaxFrameSize = 1024 // Max Opus frame size: 128kbps @ 20ms = ~600 bytes + ipcMaxFrameSize = 1024 ) -// CgoSource implements AudioSource via direct CGO calls to C audio functions (in-process) type CgoSource struct { - direction string // "output" or "input" + direction string alsaDevice string initialized bool connected bool mu sync.Mutex logger zerolog.Logger - opusBuf []byte // Reusable buffer for Opus packets + opusBuf []byte + config AudioConfig } -// NewCgoOutputSource creates a new CGO audio source for output (HDMI/USB → browser) func NewCgoOutputSource(alsaDevice string) *CgoSource { logger := logging.GetDefaultLogger().With().Str("component", "audio-output-cgo").Logger() @@ -44,10 +43,10 @@ func NewCgoOutputSource(alsaDevice string) *CgoSource { alsaDevice: alsaDevice, logger: logger, opusBuf: make([]byte, ipcMaxFrameSize), + config: DefaultAudioConfig(), } } -// NewCgoInputSource creates a new CGO audio source for input (browser → USB speakers) func NewCgoInputSource(alsaDevice string) *CgoSource { logger := logging.GetDefaultLogger().With().Str("component", "audio-input-cgo").Logger() @@ -56,10 +55,16 @@ func NewCgoInputSource(alsaDevice string) *CgoSource { alsaDevice: alsaDevice, logger: logger, opusBuf: make([]byte, ipcMaxFrameSize), + config: DefaultAudioConfig(), } } -// Connect initializes the C audio subsystem +func (c *CgoSource) SetConfig(cfg AudioConfig) { + c.mu.Lock() + defer c.mu.Unlock() + c.config = cfg +} + func (c *CgoSource) Connect() error { c.mu.Lock() defer c.mu.Unlock() @@ -68,46 +73,61 @@ func (c *CgoSource) Connect() error { return nil } - // Set ALSA device via environment for C code to read via init_alsa_devices_from_env() if c.direction == "output" { - // Set capture device for output path via environment variable os.Setenv("ALSA_CAPTURE_DEVICE", c.alsaDevice) - // Initialize constants + dtx := C.uchar(0) + if c.config.DTXEnabled { + dtx = C.uchar(1) + } + fec := C.uchar(0) + if c.config.FECEnabled { + fec = C.uchar(1) + } + + c.logger.Debug(). + Uint16("bitrate_kbps", c.config.Bitrate). + Uint8("complexity", c.config.Complexity). + Bool("dtx", c.config.DTXEnabled). + Bool("fec", c.config.FECEnabled). + Uint8("buffer_periods", c.config.BufferPeriods). + Str("alsa_device", c.alsaDevice). + Msg("Initializing audio capture") + C.update_audio_constants( - C.uint(128000), // bitrate - C.uchar(5), // complexity - C.uint(48000), // sample_rate - C.uchar(2), // channels - C.ushort(960), // frame_size - C.ushort(1500), // max_packet_size - C.uint(1000), // sleep_us - C.uchar(5), // max_attempts - C.uint(500000), // max_backoff_us + C.uint(uint32(c.config.Bitrate)*1000), + C.uchar(c.config.Complexity), + C.uint(48000), + C.uchar(2), + C.ushort(960), + C.ushort(1500), + C.uint(1000), + C.uchar(5), + C.uint(500000), + dtx, + fec, + C.uchar(c.config.BufferPeriods), ) - // Initialize capture (HDMI/USB → browser) rc := C.jetkvm_audio_capture_init() if rc != 0 { c.logger.Error().Int("rc", int(rc)).Msg("Failed to initialize audio capture") return fmt.Errorf("jetkvm_audio_capture_init failed: %d", rc) } } else { - // Set playback device for input path via environment variable os.Setenv("ALSA_PLAYBACK_DEVICE", c.alsaDevice) - // Initialize decoder constants C.update_audio_decoder_constants( - C.uint(48000), // sample_rate - C.uchar(2), // channels - C.ushort(960), // frame_size - C.ushort(1500), // max_packet_size - C.uint(1000), // sleep_us - C.uchar(5), // max_attempts - C.uint(500000), // max_backoff_us + C.uint(48000), + C.uchar(2), + C.ushort(960), + C.ushort(1500), + C.uint(1000), + C.uchar(5), + C.uint(500000), + C.uchar(c.config.BufferPeriods), ) - // Initialize playback (browser → USB speakers) rc := C.jetkvm_audio_playback_init() if rc != 0 { c.logger.Error().Int("rc", int(rc)).Msg("Failed to initialize audio playback") @@ -120,7 +140,6 @@ func (c *CgoSource) Connect() error { return nil } -// Disconnect closes the C audio subsystem func (c *CgoSource) Disconnect() { c.mu.Lock() defer c.mu.Unlock() @@ -138,17 +157,12 @@ func (c *CgoSource) Disconnect() { c.connected = false } -// IsConnected returns true if currently connected func (c *CgoSource) IsConnected() bool { c.mu.Lock() defer c.mu.Unlock() return c.connected } -// ReadMessage reads the next audio frame from C audio subsystem -// For output path: reads HDMI/USB audio and encodes to Opus -// For input path: not used (input uses WriteMessage instead) -// Returns message type (0 = Opus), payload data, and error func (c *CgoSource) ReadMessage() (uint8, []byte, error) { c.mu.Lock() defer c.mu.Unlock() @@ -161,25 +175,18 @@ func (c *CgoSource) ReadMessage() (uint8, []byte, error) { return 0, nil, fmt.Errorf("ReadMessage only supported for output direction") } - // Call C function to read HDMI/USB audio and encode to Opus - // Returns Opus packet size (>0) or error (<0) opusSize := C.jetkvm_audio_read_encode(unsafe.Pointer(&c.opusBuf[0])) - if opusSize < 0 { return 0, nil, fmt.Errorf("jetkvm_audio_read_encode failed: %d", opusSize) } if opusSize == 0 { - // No data available (silence/DTX) return 0, nil, nil } - // Return slice of opusBuf - caller must use immediately return ipcMsgTypeOpus, c.opusBuf[:opusSize], nil } -// WriteMessage writes an Opus packet to the C audio subsystem for playback -// Only used for input path (browser → USB speakers) func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error { c.mu.Lock() defer c.mu.Unlock() @@ -193,7 +200,6 @@ func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error { } if msgType != ipcMsgTypeOpus { - // Ignore non-Opus messages return nil } @@ -201,9 +207,7 @@ func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error { return nil } - // Call C function to decode Opus and write to USB speakers rc := C.jetkvm_audio_decode_write(unsafe.Pointer(&payload[0]), C.int(len(payload))) - if rc < 0 { return fmt.Errorf("jetkvm_audio_decode_write failed: %d", rc) } diff --git a/internal/audio/cgo_source_stub.go b/internal/audio/cgo_source_stub.go index f7ccfac6..3658877d 100644 --- a/internal/audio/cgo_source_stub.go +++ b/internal/audio/cgo_source_stub.go @@ -33,3 +33,7 @@ func (c *CgoSource) ReadMessage() (uint8, []byte, error) { func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error { panic("audio CGO source not supported on this platform") } + +func (c *CgoSource) SetConfig(cfg AudioConfig) { + panic("audio CGO source not supported on this platform") +} diff --git a/internal/audio/relay.go b/internal/audio/relay.go index 77ec648e..e836482d 100644 --- a/internal/audio/relay.go +++ b/internal/audio/relay.go @@ -12,7 +12,6 @@ import ( "github.com/rs/zerolog" ) -// OutputRelay forwards audio from AudioSource (CGO) to WebRTC (browser) type OutputRelay struct { source AudioSource audioTrack *webrtc.TrackLocalStaticSample @@ -23,12 +22,10 @@ type OutputRelay struct { sample media.Sample stopped chan struct{} - // Stats (Uint32: overflows after 2.7 years @ 50fps, faster atomics on 32-bit ARM) framesRelayed atomic.Uint32 framesDropped atomic.Uint32 } -// NewOutputRelay creates a relay for output audio (device → browser) func NewOutputRelay(source AudioSource, audioTrack *webrtc.TrackLocalStaticSample) *OutputRelay { ctx, cancel := context.WithCancel(context.Background()) logger := logging.GetDefaultLogger().With().Str("component", "audio-output-relay").Logger() @@ -46,7 +43,6 @@ func NewOutputRelay(source AudioSource, audioTrack *webrtc.TrackLocalStaticSampl } } -// Start begins relaying audio frames func (r *OutputRelay) Start() error { if r.running.Swap(true) { return fmt.Errorf("output relay already running") @@ -57,7 +53,6 @@ func (r *OutputRelay) Start() error { return nil } -// Stop stops the relay and waits for goroutine to exit func (r *OutputRelay) Stop() { if !r.running.Swap(false) { return @@ -72,14 +67,12 @@ func (r *OutputRelay) Stop() { Msg("output relay stopped") } -// relayLoop continuously reads from audio source and writes to WebRTC func (r *OutputRelay) relayLoop() { defer close(r.stopped) const reconnectDelay = 1 * time.Second for r.running.Load() { - // Ensure connected if !r.source.IsConnected() { if err := r.source.Connect(); err != nil { r.logger.Debug().Err(err).Msg("failed to connect, will retry") @@ -88,10 +81,8 @@ func (r *OutputRelay) relayLoop() { } } - // Read message from audio source msgType, payload, err := r.source.ReadMessage() if err != nil { - // Connection error - reconnect if r.running.Load() { r.logger.Warn().Err(err).Msg("read error, reconnecting") r.source.Disconnect() @@ -100,11 +91,8 @@ func (r *OutputRelay) relayLoop() { continue } - // Handle message if msgType == ipcMsgTypeOpus && len(payload) > 0 { - // Reuse sample struct (zero-allocation hot path) r.sample.Data = payload - if err := r.audioTrack.WriteSample(r.sample); err != nil { r.framesDropped.Add(1) r.logger.Warn().Err(err).Msg("failed to write sample to WebRTC") @@ -115,7 +103,6 @@ func (r *OutputRelay) relayLoop() { } } -// InputRelay forwards audio from WebRTC (browser microphone) to AudioSource (USB audio) type InputRelay struct { source AudioSource ctx context.Context @@ -124,7 +111,6 @@ type InputRelay struct { running atomic.Bool } -// NewInputRelay creates a relay for input audio (browser → device) func NewInputRelay(source AudioSource) *InputRelay { ctx, cancel := context.WithCancel(context.Background()) logger := logging.GetDefaultLogger().With().Str("component", "audio-input-relay").Logger() @@ -137,7 +123,6 @@ func NewInputRelay(source AudioSource) *InputRelay { } } -// Start begins relaying audio frames func (r *InputRelay) Start() error { if r.running.Swap(true) { return fmt.Errorf("input relay already running") @@ -147,7 +132,6 @@ func (r *InputRelay) Start() error { return nil } -// Stop stops the relay func (r *InputRelay) Stop() { if !r.running.Swap(false) { return diff --git a/internal/audio/source.go b/internal/audio/source.go index c7393a04..9f7e431c 100644 --- a/internal/audio/source.go +++ b/internal/audio/source.go @@ -1,28 +1,32 @@ package audio -// IPC message types const ( - ipcMsgTypeOpus = 0 // Message type for Opus audio data + ipcMsgTypeOpus = 0 ) -// AudioSource provides audio frames via CGO (in-process) C audio functions -type AudioSource interface { - // ReadMessage reads the next audio message - // Returns message type, payload data, and error - // Blocks until data is available or error occurs - // Used for output path (device → browser) - ReadMessage() (msgType uint8, payload []byte, err error) - - // WriteMessage writes an audio message - // Used for input path (browser → device) - WriteMessage(msgType uint8, payload []byte) error - - // IsConnected returns true if the source is connected and ready - IsConnected() bool - - // Connect initializes the C audio subsystem - Connect() error - - // Disconnect closes the connection and releases resources - Disconnect() +type AudioConfig struct { + Bitrate uint16 + Complexity uint8 + BufferPeriods uint8 + DTXEnabled bool + FECEnabled bool +} + +func DefaultAudioConfig() AudioConfig { + return AudioConfig{ + Bitrate: 128, + Complexity: 5, + BufferPeriods: 12, + DTXEnabled: true, + FECEnabled: true, + } +} + +type AudioSource interface { + ReadMessage() (msgType uint8, payload []byte, err error) + WriteMessage(msgType uint8, payload []byte) error + IsConnected() bool + Connect() error + Disconnect() + SetConfig(cfg AudioConfig) } diff --git a/jsonrpc.go b/jsonrpc.go index bfc1c2bf..8b771bb5 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -1000,6 +1000,60 @@ func rpcSetAudioOutputSource(source string) error { return SetAudioOutputSource(source) } +type AudioConfigResponse struct { + Bitrate int `json:"bitrate"` + Complexity int `json:"complexity"` + DTXEnabled bool `json:"dtx_enabled"` + FECEnabled bool `json:"fec_enabled"` + BufferPeriods int `json:"buffer_periods"` +} + +func rpcGetAudioConfig() (AudioConfigResponse, error) { + ensureConfigLoaded() + bitrate := config.AudioBitrate + if bitrate < 64 || bitrate > 256 { + bitrate = 128 + } + bufferPeriods := config.AudioBufferPeriods + if bufferPeriods < 2 || bufferPeriods > 24 { + bufferPeriods = 12 + } + return AudioConfigResponse{ + Bitrate: bitrate, + Complexity: config.AudioComplexity, + DTXEnabled: config.AudioDTXEnabled, + FECEnabled: config.AudioFECEnabled, + BufferPeriods: bufferPeriods, + }, nil +} + +func rpcSetAudioConfig(bitrate int, complexity int, dtxEnabled bool, fecEnabled bool, bufferPeriods int) error { + ensureConfigLoaded() + + if bitrate < 64 || bitrate > 256 { + return fmt.Errorf("bitrate must be between 64 and 256 kbps") + } + if complexity < 0 || complexity > 10 { + return fmt.Errorf("complexity must be between 0 and 10") + } + if bufferPeriods < 2 || bufferPeriods > 24 { + return fmt.Errorf("buffer periods must be between 2 and 24") + } + + config.AudioBitrate = bitrate + config.AudioComplexity = complexity + config.AudioDTXEnabled = dtxEnabled + config.AudioFECEnabled = fecEnabled + config.AudioBufferPeriods = bufferPeriods + + return SaveConfig() +} + +func rpcRestartAudioOutput() error { + RestartAudioOutput() + return nil +} + func rpcGetAudioInputAutoEnable() (bool, error) { ensureConfigLoaded() return config.AudioInputAutoEnable, nil @@ -1340,6 +1394,9 @@ var rpcHandlers = map[string]RPCHandler{ "getAudioOutputSource": {Func: rpcGetAudioOutputSource}, "setAudioOutputSource": {Func: rpcSetAudioOutputSource, Params: []string{"source"}}, "refreshHdmiConnection": {Func: rpcRefreshHdmiConnection}, + "getAudioConfig": {Func: rpcGetAudioConfig}, + "setAudioConfig": {Func: rpcSetAudioConfig, Params: []string{"bitrate", "complexity", "dtxEnabled", "fecEnabled", "bufferPeriods"}}, + "restartAudioOutput": {Func: rpcRestartAudioOutput}, "getAudioInputAutoEnable": {Func: rpcGetAudioInputAutoEnable}, "setAudioInputAutoEnable": {Func: rpcSetAudioInputAutoEnable, Params: []string{"enabled"}}, "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index 505a68f8..ab627274 100644 --- a/ui/localization/messages/da.json +++ b/ui/localization/messages/da.json @@ -86,6 +86,19 @@ "audio_settings_usb_label": "USB", "audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk", "audio_settings_auto_enable_microphone_description": "Aktiver automatisk browsermikrofon ved tilslutning (ellers skal du aktivere det manuelt ved hver session)", + "audio_settings_bitrate_title": "Opus Bitrate", + "audio_settings_bitrate_description": "Lydkodningsbitrate (højere = bedre kvalitet, mere båndbredde)", + "audio_settings_complexity_title": "Opus Kompleksitet", + "audio_settings_complexity_description": "Encoder-kompleksitet (0-10, højere = bedre kvalitet, mere CPU)", + "audio_settings_dtx_title": "DTX (Diskontinuerlig Transmission)", + "audio_settings_dtx_description": "Spar båndbredde under stilhed", + "audio_settings_fec_title": "FEC (Fremadrettet Fejlkorrektion)", + "audio_settings_fec_description": "Forbedre lydkvaliteten på tabende forbindelser", + "audio_settings_buffer_title": "Bufferperioder", + "audio_settings_buffer_description": "ALSA bufferstørrelse (højere = mere stabil, mere latens)", + "audio_settings_config_updated": "Lydkonfiguration opdateret", + "audio_settings_apply_button": "Anvend indstillinger", + "audio_settings_applied": "Lydindstillinger anvendt", "action_bar_extension": "Udvidelse", "action_bar_fullscreen": "Fuldskærm", "action_bar_settings": "Indstillinger", diff --git a/ui/localization/messages/de.json b/ui/localization/messages/de.json index cf5b50c9..05ffb769 100644 --- a/ui/localization/messages/de.json +++ b/ui/localization/messages/de.json @@ -86,6 +86,19 @@ "audio_settings_usb_label": "USB", "audio_settings_auto_enable_microphone_title": "Mikrofon automatisch aktivieren", "audio_settings_auto_enable_microphone_description": "Browser-Mikrofon beim Verbinden automatisch aktivieren (andernfalls müssen Sie es in jeder Sitzung manuell aktivieren)", + "audio_settings_bitrate_title": "Opus Bitrate", + "audio_settings_bitrate_description": "Audio-Codierungsbitrate (höher = bessere Qualität, mehr Bandbreite)", + "audio_settings_complexity_title": "Opus Komplexität", + "audio_settings_complexity_description": "Encoder-Komplexität (0-10, höher = bessere Qualität, mehr CPU)", + "audio_settings_dtx_title": "DTX (Discontinuous Transmission)", + "audio_settings_dtx_description": "Bandbreite während Stille sparen", + "audio_settings_fec_title": "FEC (Forward Error Correction)", + "audio_settings_fec_description": "Audioqualität bei verlustbehafteten Verbindungen verbessern", + "audio_settings_buffer_title": "Pufferperioden", + "audio_settings_buffer_description": "ALSA-Puffergröße (höher = stabiler, mehr Latenz)", + "audio_settings_config_updated": "Audiokonfiguration aktualisiert", + "audio_settings_apply_button": "Einstellungen anwenden", + "audio_settings_applied": "Audioeinstellungen angewendet", "action_bar_extension": "Erweiterung", "action_bar_fullscreen": "Vollbild", "action_bar_settings": "Einstellungen", diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index cdf541c2..8b4daca4 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -86,6 +86,19 @@ "audio_settings_usb_label": "USB", "audio_settings_auto_enable_microphone_title": "Auto-enable Microphone", "audio_settings_auto_enable_microphone_description": "Automatically enable browser microphone when connecting (otherwise you must manually enable each session)", + "audio_settings_bitrate_title": "Opus Bitrate", + "audio_settings_bitrate_description": "Audio encoding bitrate (higher = better quality, more bandwidth)", + "audio_settings_complexity_title": "Opus Complexity", + "audio_settings_complexity_description": "Encoder complexity (0-10, higher = better quality, more CPU)", + "audio_settings_dtx_title": "DTX (Discontinuous Transmission)", + "audio_settings_dtx_description": "Save bandwidth during silence", + "audio_settings_fec_title": "FEC (Forward Error Correction)", + "audio_settings_fec_description": "Improve audio quality on lossy connections", + "audio_settings_buffer_title": "Buffer Periods", + "audio_settings_buffer_description": "ALSA buffer size (higher = more stable, more latency)", + "audio_settings_config_updated": "Audio configuration updated", + "audio_settings_apply_button": "Apply Settings", + "audio_settings_applied": "Audio settings applied", "action_bar_extension": "Extension", "action_bar_fullscreen": "Fullscreen", "action_bar_settings": "Settings", diff --git a/ui/localization/messages/es.json b/ui/localization/messages/es.json index 3f9609e7..bc66d543 100644 --- a/ui/localization/messages/es.json +++ b/ui/localization/messages/es.json @@ -86,6 +86,19 @@ "audio_settings_usb_label": "USB", "audio_settings_auto_enable_microphone_title": "Habilitar micrófono automáticamente", "audio_settings_auto_enable_microphone_description": "Habilitar automáticamente el micrófono del navegador al conectar (de lo contrario, debe habilitarlo manualmente en cada sesión)", + "audio_settings_bitrate_title": "Bitrate Opus", + "audio_settings_bitrate_description": "Tasa de bits de codificación de audio (mayor = mejor calidad, más ancho de banda)", + "audio_settings_complexity_title": "Complejidad Opus", + "audio_settings_complexity_description": "Complejidad del codificador (0-10, mayor = mejor calidad, más CPU)", + "audio_settings_dtx_title": "DTX (Transmisión Discontinua)", + "audio_settings_dtx_description": "Ahorrar ancho de banda durante el silencio", + "audio_settings_fec_title": "FEC (Corrección de Errores)", + "audio_settings_fec_description": "Mejorar la calidad de audio en conexiones con pérdida", + "audio_settings_buffer_title": "Períodos de Buffer", + "audio_settings_buffer_description": "Tamaño del buffer ALSA (mayor = más estable, más latencia)", + "audio_settings_config_updated": "Configuración de audio actualizada", + "audio_settings_apply_button": "Aplicar configuración", + "audio_settings_applied": "Configuración de audio aplicada", "action_bar_extension": "Extensión", "action_bar_fullscreen": "Pantalla completa", "action_bar_settings": "Ajustes", diff --git a/ui/localization/messages/fr.json b/ui/localization/messages/fr.json index d54d4fe7..764a1f8c 100644 --- a/ui/localization/messages/fr.json +++ b/ui/localization/messages/fr.json @@ -86,6 +86,19 @@ "audio_settings_usb_label": "USB", "audio_settings_auto_enable_microphone_title": "Activer automatiquement le microphone", "audio_settings_auto_enable_microphone_description": "Activer automatiquement le microphone du navigateur lors de la connexion (sinon vous devez l'activer manuellement à chaque session)", + "audio_settings_bitrate_title": "Débit Opus", + "audio_settings_bitrate_description": "Débit d'encodage audio (plus élevé = meilleure qualité, plus de bande passante)", + "audio_settings_complexity_title": "Complexité Opus", + "audio_settings_complexity_description": "Complexité de l'encodeur (0-10, plus élevé = meilleure qualité, plus de CPU)", + "audio_settings_dtx_title": "DTX (Transmission Discontinue)", + "audio_settings_dtx_description": "Économiser la bande passante pendant le silence", + "audio_settings_fec_title": "FEC (Correction d'Erreur)", + "audio_settings_fec_description": "Améliorer la qualité audio sur les connexions avec perte", + "audio_settings_buffer_title": "Périodes de Tampon", + "audio_settings_buffer_description": "Taille du tampon ALSA (plus élevé = plus stable, plus de latence)", + "audio_settings_config_updated": "Configuration audio mise à jour", + "audio_settings_apply_button": "Appliquer les paramètres", + "audio_settings_applied": "Paramètres audio appliqués", "action_bar_extension": "Extension", "action_bar_fullscreen": "Plein écran", "action_bar_settings": "Paramètres", diff --git a/ui/localization/messages/it.json b/ui/localization/messages/it.json index 48b0f24e..e2bf11c4 100644 --- a/ui/localization/messages/it.json +++ b/ui/localization/messages/it.json @@ -86,6 +86,19 @@ "audio_settings_usb_label": "USB", "audio_settings_auto_enable_microphone_title": "Abilita automaticamente il microfono", "audio_settings_auto_enable_microphone_description": "Abilita automaticamente il microfono del browser durante la connessione (altrimenti devi abilitarlo manualmente ad ogni sessione)", + "audio_settings_bitrate_title": "Bitrate Opus", + "audio_settings_bitrate_description": "Bitrate di codifica audio (più alto = migliore qualità, più banda)", + "audio_settings_complexity_title": "Complessità Opus", + "audio_settings_complexity_description": "Complessità dell'encoder (0-10, più alto = migliore qualità, più CPU)", + "audio_settings_dtx_title": "DTX (Trasmissione Discontinua)", + "audio_settings_dtx_description": "Risparmia banda durante il silenzio", + "audio_settings_fec_title": "FEC (Correzione Errori)", + "audio_settings_fec_description": "Migliora la qualità audio su connessioni con perdita", + "audio_settings_buffer_title": "Periodi Buffer", + "audio_settings_buffer_description": "Dimensione buffer ALSA (più alto = più stabile, più latenza)", + "audio_settings_config_updated": "Configurazione audio aggiornata", + "audio_settings_apply_button": "Applica impostazioni", + "audio_settings_applied": "Impostazioni audio applicate", "action_bar_extension": "Estensione", "action_bar_fullscreen": "A schermo intero", "action_bar_settings": "Impostazioni", diff --git a/ui/localization/messages/nb.json b/ui/localization/messages/nb.json index db39b02b..f482d853 100644 --- a/ui/localization/messages/nb.json +++ b/ui/localization/messages/nb.json @@ -86,6 +86,19 @@ "audio_settings_usb_label": "USB", "audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk", "audio_settings_auto_enable_microphone_description": "Aktiver automatisk nettlesermikrofon ved tilkobling (ellers må du aktivere det manuelt hver økt)", + "audio_settings_bitrate_title": "Opus Bitrate", + "audio_settings_bitrate_description": "Lydkodingsbitrate (høyere = bedre kvalitet, mer båndbredde)", + "audio_settings_complexity_title": "Opus Kompleksitet", + "audio_settings_complexity_description": "Encoder-kompleksitet (0-10, høyere = bedre kvalitet, mer CPU)", + "audio_settings_dtx_title": "DTX (Diskontinuerlig Overføring)", + "audio_settings_dtx_description": "Spar båndbredde under stillhet", + "audio_settings_fec_title": "FEC (Fremadrettet Feilkorreksjon)", + "audio_settings_fec_description": "Forbedre lydkvaliteten på tapende tilkoblinger", + "audio_settings_buffer_title": "Bufferperioder", + "audio_settings_buffer_description": "ALSA bufferstørrelse (høyere = mer stabil, mer latens)", + "audio_settings_config_updated": "Lydkonfigurasjon oppdatert", + "audio_settings_apply_button": "Bruk innstillinger", + "audio_settings_applied": "Lydinnstillinger brukt", "action_bar_extension": "Forlengelse", "action_bar_fullscreen": "Fullskjerm", "action_bar_settings": "Innstillinger", diff --git a/ui/localization/messages/sv.json b/ui/localization/messages/sv.json index 52cbd4ae..d6bf3435 100644 --- a/ui/localization/messages/sv.json +++ b/ui/localization/messages/sv.json @@ -86,6 +86,19 @@ "audio_settings_usb_label": "USB", "audio_settings_auto_enable_microphone_title": "Aktivera mikrofon automatiskt", "audio_settings_auto_enable_microphone_description": "Aktivera automatiskt webbläsarmikrofon vid anslutning (annars måste du aktivera den manuellt varje session)", + "audio_settings_bitrate_title": "Opus Bitrate", + "audio_settings_bitrate_description": "Ljudkodningsbitrate (högre = bättre kvalitet, mer bandbredd)", + "audio_settings_complexity_title": "Opus Komplexitet", + "audio_settings_complexity_description": "Encoder-komplexitet (0-10, högre = bättre kvalitet, mer CPU)", + "audio_settings_dtx_title": "DTX (Diskontinuerlig Överföring)", + "audio_settings_dtx_description": "Spara bandbredd under tystnad", + "audio_settings_fec_title": "FEC (Framåtriktad Felkorrigering)", + "audio_settings_fec_description": "Förbättra ljudkvaliteten på förlustdrabbade anslutningar", + "audio_settings_buffer_title": "Bufferperioder", + "audio_settings_buffer_description": "ALSA bufferstorlek (högre = mer stabil, mer latens)", + "audio_settings_config_updated": "Ljudkonfiguration uppdaterad", + "audio_settings_apply_button": "Tillämpa inställningar", + "audio_settings_applied": "Ljudinställningar tillämpade", "action_bar_extension": "Förlängning", "action_bar_fullscreen": "Helskärm", "action_bar_settings": "Inställningar", diff --git a/ui/localization/messages/zh.json b/ui/localization/messages/zh.json index 4cf45279..7424fd26 100644 --- a/ui/localization/messages/zh.json +++ b/ui/localization/messages/zh.json @@ -86,6 +86,19 @@ "audio_settings_usb_label": "USB", "audio_settings_auto_enable_microphone_title": "自动启用麦克风", "audio_settings_auto_enable_microphone_description": "连接时自动启用浏览器麦克风(否则您必须在每次会话中手动启用)", + "audio_settings_bitrate_title": "Opus 比特率", + "audio_settings_bitrate_description": "音频编码比特率(越高 = 质量越好,带宽越大)", + "audio_settings_complexity_title": "Opus 复杂度", + "audio_settings_complexity_description": "编码器复杂度(0-10,越高 = 质量越好,CPU 使用越多)", + "audio_settings_dtx_title": "DTX(不连续传输)", + "audio_settings_dtx_description": "在静音时节省带宽", + "audio_settings_fec_title": "FEC(前向纠错)", + "audio_settings_fec_description": "改善有损连接上的音频质量", + "audio_settings_buffer_title": "缓冲周期", + "audio_settings_buffer_description": "ALSA 缓冲大小(越高 = 越稳定,延迟越高)", + "audio_settings_config_updated": "音频配置已更新", + "audio_settings_apply_button": "应用设置", + "audio_settings_applied": "音频设置已应用", "action_bar_extension": "扩展", "action_bar_fullscreen": "全屏", "action_bar_settings": "设置", diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 649405b4..e28713f5 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -392,6 +392,18 @@ export interface SettingsState { audioInputAutoEnable: boolean; setAudioInputAutoEnable: (enabled: boolean) => void; + // Audio codec settings + audioBitrate: number; + setAudioBitrate: (value: number) => void; + audioComplexity: number; + setAudioComplexity: (value: number) => void; + audioDTXEnabled: boolean; + setAudioDTXEnabled: (enabled: boolean) => void; + audioFECEnabled: boolean; + setAudioFECEnabled: (enabled: boolean) => void; + audioBufferPeriods: number; + setAudioBufferPeriods: (value: number) => void; + resetMicrophoneState: () => void; } @@ -450,6 +462,17 @@ export const useSettingsStore = create( audioInputAutoEnable: false, setAudioInputAutoEnable: (enabled: boolean) => set({ audioInputAutoEnable: enabled }), + audioBitrate: 128, + setAudioBitrate: (value: number) => set({ audioBitrate: value }), + audioComplexity: 5, + setAudioComplexity: (value: number) => set({ audioComplexity: value }), + audioDTXEnabled: true, + setAudioDTXEnabled: (enabled: boolean) => set({ audioDTXEnabled: enabled }), + audioFECEnabled: true, + setAudioFECEnabled: (enabled: boolean) => set({ audioFECEnabled: enabled }), + audioBufferPeriods: 12, + setAudioBufferPeriods: (value: number) => set({ audioBufferPeriods: value }), + resetMicrophoneState: () => set({ microphoneEnabled: false }), }), { diff --git a/ui/src/routes/devices.$id.settings.audio.tsx b/ui/src/routes/devices.$id.settings.audio.tsx index 2ceaafc2..8d03c8a9 100644 --- a/ui/src/routes/devices.$id.settings.audio.tsx +++ b/ui/src/routes/devices.$id.settings.audio.tsx @@ -10,9 +10,34 @@ import { m } from "@localizations/messages.js"; import notifications from "../notifications"; +interface AudioConfigResult { + bitrate: number; + complexity: number; + dtx_enabled: boolean; + fec_enabled: boolean; + buffer_periods: number; +} + export default function SettingsAudioRoute() { const { send } = useJsonRpc(); - const { setAudioOutputEnabled, setAudioInputAutoEnable, setAudioOutputSource, audioOutputEnabled, audioInputAutoEnable, audioOutputSource } = useSettingsStore(); + const { + setAudioOutputEnabled, + setAudioInputAutoEnable, + setAudioOutputSource, + audioOutputEnabled, + audioInputAutoEnable, + audioOutputSource, + audioBitrate, + setAudioBitrate, + audioComplexity, + setAudioComplexity, + audioDTXEnabled, + setAudioDTXEnabled, + audioFECEnabled, + setAudioFECEnabled, + audioBufferPeriods, + setAudioBufferPeriods, + } = useSettingsStore(); useEffect(() => { send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => { @@ -29,7 +54,17 @@ export default function SettingsAudioRoute() { if ("error" in resp) return; setAudioOutputSource(resp.result as string); }); - }, [send, setAudioOutputEnabled, setAudioInputAutoEnable, setAudioOutputSource]); + + send("getAudioConfig", {}, (resp: JsonRpcResponse) => { + if ("error" in resp) return; + const config = resp.result as AudioConfigResult; + setAudioBitrate(config.bitrate); + setAudioComplexity(config.complexity); + setAudioDTXEnabled(config.dtx_enabled); + setAudioFECEnabled(config.fec_enabled); + setAudioBufferPeriods(config.buffer_periods); + }); + }, [send, setAudioOutputEnabled, setAudioInputAutoEnable, setAudioOutputSource, setAudioBitrate, setAudioComplexity, setAudioDTXEnabled, setAudioFECEnabled, setAudioBufferPeriods]); const handleAudioOutputEnabledChange = (enabled: boolean) => { send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => { @@ -71,6 +106,41 @@ export default function SettingsAudioRoute() { }); }; + const handleAudioConfigChange = ( + bitrate: number, + complexity: number, + dtxEnabled: boolean, + fecEnabled: boolean, + bufferPeriods: number + ) => { + send( + "setAudioConfig", + { bitrate, complexity, dtxEnabled, fecEnabled, bufferPeriods }, + (resp: JsonRpcResponse) => { + if ("error" in resp) { + notifications.error(String(resp.error.data || m.unknown_error())); + return; + } + setAudioBitrate(bitrate); + setAudioComplexity(complexity); + setAudioDTXEnabled(dtxEnabled); + setAudioFECEnabled(fecEnabled); + setAudioBufferPeriods(bufferPeriods); + notifications.success(m.audio_settings_config_updated()); + } + ); + }; + + const handleRestartAudio = () => { + send("restartAudioOutput", {}, (resp: JsonRpcResponse) => { + if ("error" in resp) { + notifications.error(String(resp.error.data || m.unknown_error())); + return; + } + notifications.success(m.audio_settings_applied()); + }); + }; + return (