From 0022599b039e06d30da9a805c01768805e85e721 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 18 Nov 2025 13:38:06 +0200 Subject: [PATCH] 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. --- audio.go | 11 +- internal/audio/c/audio.c | 296 +++++++++++++------ internal/audio/cgo_source.go | 2 +- internal/audio/source.go | 8 +- internal/usbgadget/config.go | 4 +- ui/localization/messages/en.json | 3 + ui/src/routes/devices.$id.settings.audio.tsx | 19 +- 7 files changed, 241 insertions(+), 102 deletions(-) diff --git a/audio.go b/audio.go index c02238cc..b51225ac 100644 --- a/audio.go +++ b/audio.go @@ -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,8 +328,10 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) { } if err := (*source).WriteMessage(0, opusData); err != nil { - audioLogger.Warn().Err(err).Msg("failed to write audio message") - (*source).Disconnect() + if inputSource.Load() == source { + audioLogger.Warn().Err(err).Msg("failed to write audio message") + (*source).Disconnect() + } } inputSourceMutex.Unlock() diff --git a/internal/audio/c/audio.c b/internal/audio/c/audio.c index 10508629..3408d595 100644 --- a/internal/audio/c/audio.c +++ b/internal/audio/c/audio.c @@ -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,13 +346,27 @@ int jetkvm_audio_capture_init() { return 0; } - if (encoder) { - opus_encoder_destroy(encoder); - encoder = NULL; - } - if (pcm_capture_handle) { - snd_pcm_close(pcm_capture_handle); - pcm_capture_handle = NULL; + 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; + } + if (pcm_capture_handle) { + snd_pcm_close(pcm_capture_handle); + pcm_capture_handle = NULL; + } + + pthread_mutex_unlock(&capture_mutex); } err = safe_alsa_open(&pcm_capture_handle, alsa_capture_device, SND_PCM_STREAM_CAPTURE); @@ -358,31 +374,34 @@ int jetkvm_audio_capture_init() { 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); + err = snd_pcm_prepare(handle); if (err < 0) { - snd_pcm_drop(pcm_capture_handle); - err = snd_pcm_prepare(pcm_capture_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,13 +605,27 @@ int jetkvm_audio_playback_init() { return 0; } - if (decoder) { - opus_decoder_destroy(decoder); - decoder = NULL; - } - if (pcm_playback_handle) { - snd_pcm_close(pcm_playback_handle); - pcm_playback_handle = NULL; + 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; + } + if (pcm_playback_handle) { + snd_pcm_close(pcm_playback_handle); + pcm_playback_handle = NULL; + } + + pthread_mutex_unlock(&playback_mutex); } err = safe_alsa_open(&pcm_playback_handle, alsa_playback_device, SND_PCM_STREAM_PLAYBACK); @@ -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; diff --git a/internal/audio/cgo_source.go b/internal/audio/cgo_source.go index 81f9b70b..99aaf071 100644 --- a/internal/audio/cgo_source.go +++ b/internal/audio/cgo_source.go @@ -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), diff --git a/internal/audio/source.go b/internal/audio/source.go index b490b31d..30356ffa 100644 --- a/internal/audio/source.go +++ b/internal/audio/source.go @@ -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, } } diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index 97464a4a..2e7a980e 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -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 diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 78513a26..b206496e 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -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", diff --git a/ui/src/routes/devices.$id.settings.audio.tsx b/ui/src/routes/devices.$id.settings.audio.tsx index ae1e1fe7..de6d55d5 100644 --- a/ui/src/routes/devices.$id.settings.audio.tsx +++ b/ui/src/routes/devices.$id.settings.audio.tsx @@ -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%" }, ]}