Fix audio channel separation and improve quality defaults

- Separate capture_channels (stereo HDMI) from playback_channels (mono mic)
  to prevent initialization conflicts that were breaking stereo output
- Optimize defaults for LAN use: 192kbps bitrate, complexity 8, 0% packet
  loss compensation, DTX disabled (eliminates static and improves clarity)
- Add comprehensive race condition protection in C audio layer with handle
  validity checks and mutex-protected cleanup operations
- Enable USB audio volume control and configure microphone as mono
- Add centralized AUDIO_DEFAULTS constant in UI with localized labels
- Add missing time import to fix compilation

This resolves audio quality issues and crash scenarios when switching
between HDMI and USB audio sources.
This commit is contained in:
Alex P 2025-11-18 13:38:06 +02:00
parent 2f622df28d
commit 0022599b03
7 changed files with 241 additions and 102 deletions

View File

@ -4,6 +4,7 @@ import (
"io"
"sync"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/audio"
"github.com/jetkvm/kvm/internal/logging"
@ -159,7 +160,9 @@ func stopInputAudio() {
inRelay.Stop()
}
if inSource != nil {
inputSourceMutex.Lock()
(*inSource).Disconnect()
inputSourceMutex.Unlock()
}
}
@ -249,6 +252,8 @@ func SetAudioOutputSource(source string) error {
stopOutputAudio()
config.AudioOutputSource = source
time.Sleep(100 * time.Millisecond)
if err := startAudio(); err != nil {
audioLogger.Error().Err(err).Str("source", source).Msg("Failed to start audio output after source change")
}
@ -323,9 +328,11 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) {
}
if err := (*source).WriteMessage(0, opusData); err != nil {
if inputSource.Load() == source {
audioLogger.Warn().Err(err).Msg("failed to write audio message")
(*source).Disconnect()
}
}
inputSourceMutex.Unlock()
}

View File

@ -47,13 +47,14 @@ static const char *alsa_playback_device = NULL;
static OpusEncoder *encoder = NULL;
static OpusDecoder *decoder = NULL;
// Audio format (S16_LE @ 48kHz stereo)
// Audio format (S16_LE @ 48kHz)
static uint32_t sample_rate = 48000;
static uint8_t channels = 2;
static uint8_t capture_channels = 2; // OUTPUT: HDMI stereo → client
static uint8_t playback_channels = 1; // INPUT: Client mono mic → device
static uint16_t frame_size = 960; // 20ms frames at 48kHz
static uint32_t opus_bitrate = 128000;
static uint8_t opus_complexity = 5;
static uint32_t opus_bitrate = 192000;
static uint8_t opus_complexity = 8;
static uint16_t max_packet_size = 1500;
#define OPUS_VBR 1
@ -62,10 +63,10 @@ static uint16_t max_packet_size = 1500;
#define OPUS_BANDWIDTH 1104
#define OPUS_LSB_DEPTH 16
static uint8_t opus_dtx_enabled = 1;
static uint8_t opus_dtx_enabled = 0;
static uint8_t opus_fec_enabled = 1;
static uint8_t opus_packet_loss_perc = 20;
static uint8_t buffer_period_count = 12;
static uint8_t opus_packet_loss_perc = 0;
static uint8_t buffer_period_count = 24;
static uint32_t sleep_microseconds = 1000;
static uint32_t sleep_milliseconds = 1;
@ -103,7 +104,7 @@ void update_audio_constants(uint32_t bitrate, uint8_t complexity,
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;
capture_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;
@ -120,7 +121,7 @@ void update_audio_decoder_constants(uint32_t sr, uint8_t ch, uint16_t fs, uint16
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;
playback_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;
@ -228,14 +229,15 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream
}
/**
* Configure ALSA device (S16_LE @ variable rate stereo with optimized buffering)
* Configure ALSA device (S16_LE @ variable rate with optimized buffering)
* @param handle ALSA PCM handle
* @param device_name Device name for logging
* @param num_channels Number of channels (1=mono, 2=stereo)
* @param actual_rate_out Pointer to store the actual rate the device was configured to use
* @param actual_frame_size_out Pointer to store the actual frame size (samples per channel)
* @return 0 on success, negative error code on failure
*/
static int configure_alsa_device(snd_pcm_t *handle, const char *device_name,
static int configure_alsa_device(snd_pcm_t *handle, const char *device_name, uint8_t num_channels,
unsigned int *actual_rate_out, uint16_t *actual_frame_size_out) {
snd_pcm_hw_params_t *params;
snd_pcm_sw_params_t *sw_params;
@ -255,7 +257,7 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name,
err = snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
if (err < 0) return err;
err = snd_pcm_hw_params_set_channels(handle, params, channels);
err = snd_pcm_hw_params_set_channels(handle, params, num_channels);
if (err < 0) return err;
unsigned int requested_rate = sample_rate;
@ -271,7 +273,7 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name,
fflush(stderr);
actual_frame_size = (actual_rate * 20) / 1000;
if (channels == 2) {
if (num_channels == 2) {
if (actual_frame_size < 480) actual_frame_size = 480;
if (actual_frame_size > 2880) actual_frame_size = 2880;
}
@ -344,6 +346,17 @@ int jetkvm_audio_capture_init() {
return 0;
}
if (encoder != NULL || pcm_capture_handle != NULL) {
capture_initialized = 0;
capture_stop_requested = 1;
__sync_synchronize();
if (pcm_capture_handle) {
snd_pcm_drop(pcm_capture_handle);
}
pthread_mutex_lock(&capture_mutex);
if (encoder) {
opus_encoder_destroy(encoder);
encoder = NULL;
@ -353,36 +366,42 @@ int jetkvm_audio_capture_init() {
pcm_capture_handle = NULL;
}
pthread_mutex_unlock(&capture_mutex);
}
err = safe_alsa_open(&pcm_capture_handle, alsa_capture_device, SND_PCM_STREAM_CAPTURE);
if (err < 0) {
fprintf(stderr, "Failed to open ALSA capture device %s: %s\n",
alsa_capture_device, snd_strerror(err));
fflush(stderr);
capture_stop_requested = 0;
capture_initializing = 0;
return -1;
}
unsigned int actual_rate = 0;
uint16_t actual_frame_size = 0;
err = configure_alsa_device(pcm_capture_handle, "capture", &actual_rate, &actual_frame_size);
err = configure_alsa_device(pcm_capture_handle, "capture", capture_channels, &actual_rate, &actual_frame_size);
if (err < 0) {
snd_pcm_close(pcm_capture_handle);
pcm_capture_handle = NULL;
capture_stop_requested = 0;
capture_initializing = 0;
return -2;
}
fprintf(stderr, "INFO: capture: Initializing Opus encoder at %u Hz, %u channels, frame size %u\n",
actual_rate, channels, actual_frame_size);
actual_rate, capture_channels, actual_frame_size);
fflush(stderr);
int opus_err = 0;
encoder = opus_encoder_create(actual_rate, channels, OPUS_APPLICATION_AUDIO, &opus_err);
encoder = opus_encoder_create(actual_rate, capture_channels, OPUS_APPLICATION_AUDIO, &opus_err);
if (!encoder || opus_err != OPUS_OK) {
if (pcm_capture_handle) {
snd_pcm_close(pcm_capture_handle);
pcm_capture_handle = NULL;
}
capture_stop_requested = 0;
capture_initializing = 0;
return -3;
}
@ -400,6 +419,7 @@ int jetkvm_audio_capture_init() {
opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(opus_packet_loss_perc));
capture_initialized = 1;
capture_stop_requested = 0;
capture_initializing = 0;
return 0;
}
@ -441,46 +461,69 @@ retry_read:
return -1;
}
pcm_rc = snd_pcm_readi(pcm_capture_handle, pcm_buffer, frame_size);
snd_pcm_t *handle = pcm_capture_handle;
if (!handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
pcm_rc = snd_pcm_readi(handle, pcm_buffer, frame_size);
if (handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
if (__builtin_expect(pcm_rc < 0, 0)) {
if (pcm_rc == -EPIPE) {
recovery_attempts++;
if (recovery_attempts > max_recovery_attempts) {
if (recovery_attempts > max_recovery_attempts || handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
err = snd_pcm_prepare(pcm_capture_handle);
if (err < 0) {
snd_pcm_drop(pcm_capture_handle);
err = snd_pcm_prepare(pcm_capture_handle);
err = snd_pcm_prepare(handle);
if (err < 0) {
if (handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
snd_pcm_drop(handle);
err = snd_pcm_prepare(handle);
if (err < 0 || handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
}
goto retry_read;
} else if (pcm_rc == -EAGAIN) {
snd_pcm_wait(pcm_capture_handle, sleep_milliseconds);
if (handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
snd_pcm_wait(handle, sleep_milliseconds);
goto retry_read;
} else if (pcm_rc == -ESTRPIPE) {
recovery_attempts++;
if (recovery_attempts > max_recovery_attempts) {
if (recovery_attempts > max_recovery_attempts || handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
uint8_t resume_attempts = 0;
while ((err = snd_pcm_resume(pcm_capture_handle)) == -EAGAIN && resume_attempts < 10) {
if (capture_stop_requested) {
while ((err = snd_pcm_resume(handle)) == -EAGAIN && resume_attempts < 10) {
if (capture_stop_requested || handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
snd_pcm_wait(pcm_capture_handle, sleep_milliseconds);
snd_pcm_wait(handle, sleep_milliseconds);
resume_attempts++;
}
if (err < 0) {
err = snd_pcm_prepare(pcm_capture_handle);
if (err < 0) {
if (handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
err = snd_pcm_prepare(handle);
if (err < 0 || handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
@ -492,10 +535,14 @@ retry_read:
return -1;
} else if (pcm_rc == -EIO) {
recovery_attempts++;
if (recovery_attempts <= max_recovery_attempts) {
snd_pcm_drop(pcm_capture_handle);
err = snd_pcm_prepare(pcm_capture_handle);
if (err >= 0) {
if (recovery_attempts <= max_recovery_attempts && handle == pcm_capture_handle) {
snd_pcm_drop(handle);
if (handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
err = snd_pcm_prepare(handle);
if (err >= 0 && handle == pcm_capture_handle) {
goto retry_read;
}
}
@ -505,8 +552,8 @@ retry_read:
recovery_attempts++;
if (recovery_attempts <= 1 && pcm_rc == -EINTR) {
goto retry_read;
} else if (recovery_attempts <= 1 && pcm_rc == -EBUSY) {
snd_pcm_wait(pcm_capture_handle, 1); // Wait 1ms for device
} else if (recovery_attempts <= 1 && pcm_rc == -EBUSY && handle == pcm_capture_handle) {
snd_pcm_wait(handle, 1); // Wait 1ms for device
goto retry_read;
}
pthread_mutex_unlock(&capture_mutex);
@ -516,11 +563,23 @@ retry_read:
// Zero-pad if we got a short read
if (__builtin_expect(pcm_rc < frame_size, 0)) {
uint32_t remaining_samples = (frame_size - pcm_rc) * channels;
simd_clear_samples_s16(&pcm_buffer[pcm_rc * channels], remaining_samples);
uint32_t remaining_samples = (frame_size - pcm_rc) * capture_channels;
simd_clear_samples_s16(&pcm_buffer[pcm_rc * capture_channels], remaining_samples);
}
OpusEncoder *enc = encoder;
if (!enc || enc != encoder) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
nb_bytes = opus_encode(enc, pcm_buffer, frame_size, out, max_packet_size);
if (enc != encoder) {
pthread_mutex_unlock(&capture_mutex);
return -1;
}
nb_bytes = opus_encode(encoder, pcm_buffer, frame_size, out, max_packet_size);
pthread_mutex_unlock(&capture_mutex);
return nb_bytes;
}
@ -546,6 +605,17 @@ int jetkvm_audio_playback_init() {
return 0;
}
if (decoder != NULL || pcm_playback_handle != NULL) {
playback_initialized = 0;
playback_stop_requested = 1;
__sync_synchronize();
if (pcm_playback_handle) {
snd_pcm_drop(pcm_playback_handle);
}
pthread_mutex_lock(&playback_mutex);
if (decoder) {
opus_decoder_destroy(decoder);
decoder = NULL;
@ -555,6 +625,9 @@ int jetkvm_audio_playback_init() {
pcm_playback_handle = NULL;
}
pthread_mutex_unlock(&playback_mutex);
}
err = safe_alsa_open(&pcm_playback_handle, alsa_playback_device, SND_PCM_STREAM_PLAYBACK);
if (err < 0) {
fprintf(stderr, "Failed to open ALSA playback device %s: %s\n",
@ -562,6 +635,7 @@ int jetkvm_audio_playback_init() {
fflush(stderr);
err = safe_alsa_open(&pcm_playback_handle, "default", SND_PCM_STREAM_PLAYBACK);
if (err < 0) {
playback_stop_requested = 0;
playback_initializing = 0;
return -1;
}
@ -569,28 +643,31 @@ int jetkvm_audio_playback_init() {
unsigned int actual_rate = 0;
uint16_t actual_frame_size = 0;
err = configure_alsa_device(pcm_playback_handle, "playback", &actual_rate, &actual_frame_size);
err = configure_alsa_device(pcm_playback_handle, "playback", playback_channels, &actual_rate, &actual_frame_size);
if (err < 0) {
snd_pcm_close(pcm_playback_handle);
pcm_playback_handle = NULL;
playback_stop_requested = 0;
playback_initializing = 0;
return -1;
}
fprintf(stderr, "INFO: playback: Initializing Opus decoder at %u Hz, %u channels, frame size %u\n",
actual_rate, channels, actual_frame_size);
actual_rate, playback_channels, actual_frame_size);
fflush(stderr);
int opus_err = 0;
decoder = opus_decoder_create(actual_rate, channels, &opus_err);
decoder = opus_decoder_create(actual_rate, playback_channels, &opus_err);
if (!decoder || opus_err != OPUS_OK) {
snd_pcm_close(pcm_playback_handle);
pcm_playback_handle = NULL;
playback_stop_requested = 0;
playback_initializing = 0;
return -2;
}
playback_initialized = 1;
playback_stop_requested = 0;
playback_initializing = 0;
return 0;
}
@ -632,16 +709,38 @@ __attribute__((hot)) int jetkvm_audio_decode_write(void * __restrict__ opus_buf,
}
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Processing Opus packet - size=%d bytes\n", opus_size);
OpusDecoder *dec = decoder;
if (!dec || dec != decoder) {
pthread_mutex_unlock(&playback_mutex);
return -1;
}
// Decode Opus packet to PCM (FEC automatically applied if embedded in packet)
// decode_fec=0 means normal decode (FEC data is used automatically when present)
pcm_frames = opus_decode(decoder, in, opus_size, pcm_buffer, frame_size, 0);
pcm_frames = opus_decode(dec, in, opus_size, pcm_buffer, frame_size, 0);
if (dec != decoder) {
pthread_mutex_unlock(&playback_mutex);
return -1;
}
if (__builtin_expect(pcm_frames < 0, 0)) {
// Decode failed - attempt packet loss concealment using FEC from previous packet
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus decode failed with error %d, attempting packet loss concealment\n", pcm_frames);
if (!dec || dec != decoder) {
pthread_mutex_unlock(&playback_mutex);
return -1;
}
// decode_fec=1 means use FEC data from the NEXT packet to reconstruct THIS lost packet
pcm_frames = opus_decode(decoder, NULL, 0, pcm_buffer, frame_size, 1);
pcm_frames = opus_decode(dec, NULL, 0, pcm_buffer, frame_size, 1);
if (dec != decoder) {
pthread_mutex_unlock(&playback_mutex);
return -1;
}
if (pcm_frames < 0) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Packet loss concealment also failed with error %d\n", pcm_frames);
pthread_mutex_unlock(&playback_mutex);
@ -657,25 +756,40 @@ retry_write:
return -1;
}
pcm_rc = snd_pcm_writei(pcm_playback_handle, pcm_buffer, pcm_frames);
snd_pcm_t *handle = pcm_playback_handle;
if (!handle) {
pthread_mutex_unlock(&playback_mutex);
return -1;
}
pcm_rc = snd_pcm_writei(handle, pcm_buffer, pcm_frames);
if (handle != pcm_playback_handle) {
pthread_mutex_unlock(&playback_mutex);
return -1;
}
if (__builtin_expect(pcm_rc < 0, 0)) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: ALSA write failed with error %d (%s), attempt %d/%d\n",
pcm_rc, snd_strerror(pcm_rc), recovery_attempts + 1, max_recovery_attempts);
if (pcm_rc == -EPIPE) {
recovery_attempts++;
if (recovery_attempts > max_recovery_attempts) {
if (recovery_attempts > max_recovery_attempts || handle != pcm_playback_handle) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun recovery failed after %d attempts\n", max_recovery_attempts);
pthread_mutex_unlock(&playback_mutex);
return -2;
}
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun detected, attempting recovery (attempt %d)\n", recovery_attempts);
err = snd_pcm_prepare(pcm_playback_handle);
err = snd_pcm_prepare(handle);
if (err < 0) {
if (handle != pcm_playback_handle) {
pthread_mutex_unlock(&playback_mutex);
return -2;
}
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: snd_pcm_prepare failed (%s), trying drop+prepare\n", snd_strerror(err));
snd_pcm_drop(pcm_playback_handle);
err = snd_pcm_prepare(pcm_playback_handle);
if (err < 0) {
snd_pcm_drop(handle);
err = snd_pcm_prepare(handle);
if (err < 0 || handle != pcm_playback_handle) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: drop+prepare recovery failed (%s)\n", snd_strerror(err));
pthread_mutex_unlock(&playback_mutex);
return -2;
@ -685,25 +799,29 @@ retry_write:
goto retry_write;
} else if (pcm_rc == -ESTRPIPE) {
recovery_attempts++;
if (recovery_attempts > max_recovery_attempts) {
if (recovery_attempts > max_recovery_attempts || handle != pcm_playback_handle) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspend recovery failed after %d attempts\n", max_recovery_attempts);
pthread_mutex_unlock(&playback_mutex);
return -2;
}
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspended, attempting resume (attempt %d)\n", recovery_attempts);
uint8_t resume_attempts = 0;
while ((err = snd_pcm_resume(pcm_playback_handle)) == -EAGAIN && resume_attempts < 10) {
if (playback_stop_requested) {
while ((err = snd_pcm_resume(handle)) == -EAGAIN && resume_attempts < 10) {
if (playback_stop_requested || handle != pcm_playback_handle) {
pthread_mutex_unlock(&playback_mutex);
return -1;
}
snd_pcm_wait(pcm_playback_handle, sleep_milliseconds);
snd_pcm_wait(handle, sleep_milliseconds);
resume_attempts++;
}
if (err < 0) {
if (handle != pcm_playback_handle) {
pthread_mutex_unlock(&playback_mutex);
return -2;
}
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device resume failed (%s), trying prepare fallback\n", snd_strerror(err));
err = snd_pcm_prepare(pcm_playback_handle);
if (err < 0) {
err = snd_pcm_prepare(handle);
if (err < 0 || handle != pcm_playback_handle) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Prepare fallback failed (%s)\n", snd_strerror(err));
pthread_mutex_unlock(&playback_mutex);
return -2;
@ -718,11 +836,15 @@ retry_write:
return -2;
} else if (pcm_rc == -EIO) {
recovery_attempts++;
if (recovery_attempts <= max_recovery_attempts) {
if (recovery_attempts <= max_recovery_attempts && handle == pcm_playback_handle) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error detected, attempting recovery\n");
snd_pcm_drop(pcm_playback_handle);
err = snd_pcm_prepare(pcm_playback_handle);
if (err >= 0) {
snd_pcm_drop(handle);
if (handle != pcm_playback_handle) {
pthread_mutex_unlock(&playback_mutex);
return -2;
}
err = snd_pcm_prepare(handle);
if (err >= 0 && handle == pcm_playback_handle) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error recovery successful, retrying write\n");
goto retry_write;
}
@ -732,9 +854,9 @@ retry_write:
return -2;
} else if (pcm_rc == -EAGAIN) {
recovery_attempts++;
if (recovery_attempts <= max_recovery_attempts) {
if (recovery_attempts <= max_recovery_attempts && handle == pcm_playback_handle) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device not ready (EAGAIN), waiting and retrying\n");
snd_pcm_wait(pcm_playback_handle, 1); // Wait 1ms
snd_pcm_wait(handle, 1); // Wait 1ms
goto retry_write;
}
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device not ready recovery failed after %d attempts\n", max_recovery_attempts);
@ -742,9 +864,9 @@ retry_write:
return -2;
} else {
recovery_attempts++;
if (recovery_attempts <= 1 && (pcm_rc == -EINTR || pcm_rc == -EBUSY)) {
if (recovery_attempts <= 1 && (pcm_rc == -EINTR || pcm_rc == -EBUSY) && handle == pcm_playback_handle) {
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Transient error %d (%s), retrying once\n", pcm_rc, snd_strerror(pcm_rc));
snd_pcm_wait(pcm_playback_handle, 1); // Wait 1ms
snd_pcm_wait(handle, 1); // Wait 1ms
goto retry_write;
}
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Unrecoverable error %d (%s)\n", pcm_rc, snd_strerror(pcm_rc));
@ -772,15 +894,15 @@ void jetkvm_audio_playback_close() {
return;
}
// Drop PCM stream BEFORE acquiring mutex to interrupt any blocking writes
// snd_pcm_drop() is thread-safe and will cause snd_pcm_writei() to return immediately
struct timespec short_delay = { .tv_sec = 0, .tv_nsec = 5000000 };
nanosleep(&short_delay, NULL);
pthread_mutex_lock(&playback_mutex);
if (pcm_playback_handle) {
snd_pcm_drop(pcm_playback_handle);
}
// Now acquire mutex for cleanup
pthread_mutex_lock(&playback_mutex);
if (decoder) {
opus_decoder_destroy(decoder);
decoder = NULL;
@ -808,15 +930,15 @@ void jetkvm_audio_capture_close() {
return;
}
// Drop PCM stream BEFORE acquiring mutex to interrupt any blocking reads
// snd_pcm_drop() is thread-safe and will cause snd_pcm_readi() to return immediately
struct timespec short_delay = { .tv_sec = 0, .tv_nsec = 5000000 };
nanosleep(&short_delay, NULL);
pthread_mutex_lock(&capture_mutex);
if (pcm_capture_handle) {
snd_pcm_drop(pcm_capture_handle);
}
// Now acquire mutex for cleanup
pthread_mutex_lock(&capture_mutex);
if (pcm_capture_handle) {
snd_pcm_close(pcm_capture_handle);
pcm_capture_handle = NULL;

View File

@ -122,7 +122,7 @@ func (c *CgoSource) Connect() error {
C.update_audio_decoder_constants(
C.uint(c.config.SampleRate),
C.uchar(2),
C.uchar(1),
C.ushort(960),
C.ushort(1500),
C.uint(1000),

View File

@ -16,13 +16,13 @@ type AudioConfig struct {
func DefaultAudioConfig() AudioConfig {
return AudioConfig{
Bitrate: 128,
Complexity: 5,
Bitrate: 192,
Complexity: 8,
BufferPeriods: 12,
DTXEnabled: true,
DTXEnabled: false,
FECEnabled: true,
SampleRate: 48000,
PacketLossPerc: 20,
PacketLossPerc: 0,
}
}

View File

@ -69,8 +69,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
"p_chmask": "3", // Playback: stereo (2 channels)
"p_srate": "48000", // Playback: 48kHz sample rate
"p_ssize": "2", // Playback: 16-bit (2 bytes)
"p_volume_present": "0", // Playback: no volume control
"c_chmask": "3", // Capture: stereo (2 channels)
"p_volume_present": "1", // Playback: enable volume control
"c_chmask": "1", // Capture: mono (1 channel)
"c_srate": "48000", // Capture: 48kHz sample rate
"c_ssize": "2", // Capture: 16-bit (2 bytes)
"c_volume_present": "0", // Capture: no volume control

View File

@ -93,6 +93,9 @@
"audio_settings_config_updated": "Audio configuration updated",
"audio_settings_apply_button": "Apply Settings",
"audio_settings_applied": "Audio settings applied",
"audio_settings_default_suffix": " (default)",
"audio_settings_default_lan_suffix": " (default - LAN)",
"audio_settings_no_compensation_suffix": " (no compensation)",
"action_bar_extension": "Extension",
"action_bar_fullscreen": "Fullscreen",
"action_bar_settings": "Settings",

View File

@ -20,6 +20,13 @@ interface AudioConfigResult {
packet_loss_perc: number;
}
// Backend default values - single source of truth
const AUDIO_DEFAULTS = {
bitrate: 192,
complexity: 8,
packetLossPerc: 0,
} as const;
export default function SettingsAudioRoute() {
const { send } = useJsonRpc();
const {
@ -205,7 +212,7 @@ export default function SettingsAudioRoute() {
{ value: "96", label: "96 kbps" },
{ value: "128", label: "128 kbps" },
{ value: "160", label: "160 kbps" },
{ value: "192", label: "192 kbps" },
{ value: "192", label: `192 kbps${192 === AUDIO_DEFAULTS.bitrate ? m.audio_settings_default_suffix() : ''}` },
{ value: "256", label: "256 kbps" },
]}
onChange={(e) =>
@ -231,9 +238,9 @@ export default function SettingsAudioRoute() {
options={[
{ value: "0", label: "0 (fastest)" },
{ value: "2", label: "2" },
{ value: "5", label: "5 (balanced)" },
{ value: "8", label: "8" },
{ value: "10", label: "10 (best)" },
{ value: "5", label: "5" },
{ value: "8", label: `8${8 === AUDIO_DEFAULTS.complexity ? m.audio_settings_default_suffix() : ''}` },
{ value: "10", label: "10 (best quality)" },
]}
onChange={(e) =>
handleAudioConfigChange(
@ -337,11 +344,11 @@ export default function SettingsAudioRoute() {
size="SM"
value={String(audioPacketLossPerc)}
options={[
{ value: "0", label: "0% (no compensation)" },
{ value: "0", label: `0%${0 === AUDIO_DEFAULTS.packetLossPerc ? m.audio_settings_default_lan_suffix() : m.audio_settings_no_compensation_suffix()}` },
{ value: "5", label: "5%" },
{ value: "10", label: "10%" },
{ value: "15", label: "15%" },
{ value: "20", label: "20% (default)" },
{ value: "20", label: "20%" },
{ value: "25", label: "25%" },
{ value: "30", label: "30%" },
]}