Add runtime configurable audio parameters with UI controls

- Add config fields for bitrate, complexity, DTX, FEC, buffer periods
- Add RPC methods for get/set audio config and restart
- Add UI settings page with controls for all audio parameters
- Add Apply Settings button to restart audio with new config
- Add config migration for backwards compatibility
- Add translations for all 9 languages
- Clean up redundant comments and optimize log levels
This commit is contained in:
Alex P 2025-11-17 21:51:08 +02:00
parent e79c6f730e
commit 922a7158e7
19 changed files with 616 additions and 166 deletions

106
audio.go
View File

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

View File

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

View File

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

View File

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

View File

@ -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")
}

View File

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

View File

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

View File

@ -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"}},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "设置",

View File

@ -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 }),
}),
{

View File

@ -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 (
<div className="space-y-4">
<SettingsPageHeader
@ -112,6 +182,130 @@ export default function SettingsAudioRoute() {
onChange={(e) => handleAudioInputAutoEnableChange(e.target.checked)}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_bitrate_title()}
description={m.audio_settings_bitrate_description()}
>
<SelectMenuBasic
size="SM"
value={String(audioBitrate)}
options={[
{ value: "64", label: "64 kbps" },
{ value: "96", label: "96 kbps" },
{ value: "128", label: "128 kbps" },
{ value: "160", label: "160 kbps" },
{ value: "192", label: "192 kbps" },
{ value: "256", label: "256 kbps" },
]}
onChange={(e) =>
handleAudioConfigChange(
parseInt(e.target.value),
audioComplexity,
audioDTXEnabled,
audioFECEnabled,
audioBufferPeriods
)
}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_complexity_title()}
description={m.audio_settings_complexity_description()}
>
<SelectMenuBasic
size="SM"
value={String(audioComplexity)}
options={[
{ value: "0", label: "0 (fastest)" },
{ value: "2", label: "2" },
{ value: "5", label: "5 (balanced)" },
{ value: "8", label: "8" },
{ value: "10", label: "10 (best)" },
]}
onChange={(e) =>
handleAudioConfigChange(
audioBitrate,
parseInt(e.target.value),
audioDTXEnabled,
audioFECEnabled,
audioBufferPeriods
)
}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_dtx_title()}
description={m.audio_settings_dtx_description()}
>
<Checkbox
checked={audioDTXEnabled}
onChange={(e) =>
handleAudioConfigChange(
audioBitrate,
audioComplexity,
e.target.checked,
audioFECEnabled,
audioBufferPeriods
)
}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_fec_title()}
description={m.audio_settings_fec_description()}
>
<Checkbox
checked={audioFECEnabled}
onChange={(e) =>
handleAudioConfigChange(
audioBitrate,
audioComplexity,
audioDTXEnabled,
e.target.checked,
audioBufferPeriods
)
}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_buffer_title()}
description={m.audio_settings_buffer_description()}
>
<SelectMenuBasic
size="SM"
value={String(audioBufferPeriods)}
options={[
{ value: "4", label: "4 (80ms)" },
{ value: "8", label: "8 (160ms)" },
{ value: "12", label: "12 (240ms)" },
{ value: "16", label: "16 (320ms)" },
{ value: "24", label: "24 (480ms)" },
]}
onChange={(e) =>
handleAudioConfigChange(
audioBitrate,
audioComplexity,
audioDTXEnabled,
audioFECEnabled,
parseInt(e.target.value)
)
}
/>
</SettingsItem>
<div className="pt-4">
<button
onClick={handleRestartAudio}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
{m.audio_settings_apply_button()}
</button>
</div>
</div>
</div>
);