From 236291a454a55fc3d08f5c97ab2480d39f29d1ed Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 18 Nov 2025 00:01:00 +0200 Subject: [PATCH] Fix: eliminate audio warping by auto-adapting to actual device sample rate Root cause: ALSA was silently using a different sample rate than configured, causing severe pitch/speed distortion (the "cassette player" warping effect). The bug occurred when: - User configured 48 kHz in UI - HDMI source output 44.1 kHz audio - set_rate() failed, set_rate_near() chose 44.1 kHz - Code never checked what rate was actually set - Opus encoder created for 48 kHz but received 44.1 kHz audio - Result: ~9% pitch shift and timing mismatch Fix: - Always use set_rate_near() and check the actual rate returned - Pass detected rate and frame size to Opus encoder/decoder creation - Avoid modifying global state to prevent capture/playback interference - Recalculate frame_size for 20ms at the actual rate - Verify rate after hw_params application - Add detailed logging for rate adaptation This ensures Opus encoder/decoder use the correct rate matching the hardware, regardless of what the HDMI source outputs. --- internal/audio/c/audio.c | 73 ++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/internal/audio/c/audio.c b/internal/audio/c/audio.c index b446cea0..4fc8e9f7 100644 --- a/internal/audio/c/audio.c +++ b/internal/audio/c/audio.c @@ -254,12 +254,15 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream } /** - * Configure ALSA device (S16_LE @ 48kHz stereo with optimized buffering) + * Configure ALSA device (S16_LE @ variable rate stereo with optimized buffering) * @param handle ALSA PCM handle - * @param device_name Unused (for debugging only) + * @param device_name Device name for logging + * @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, + unsigned int *actual_rate_out, uint16_t *actual_frame_size_out) { snd_pcm_hw_params_t *params; snd_pcm_sw_params_t *sw_params; int err; @@ -281,14 +284,30 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) { err = snd_pcm_hw_params_set_channels(handle, params, channels); if (err < 0) return err; - err = snd_pcm_hw_params_set_rate(handle, params, sample_rate, 0); - if (err < 0) { - unsigned int rate = sample_rate; - err = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0); - if (err < 0) return err; + unsigned int requested_rate = sample_rate; + unsigned int actual_rate = sample_rate; + err = snd_pcm_hw_params_set_rate_near(handle, params, &actual_rate, 0); + if (err < 0) return err; + + uint16_t actual_frame_size = frame_size; + if (actual_rate != requested_rate) { + fprintf(stderr, "INFO: %s: Requested sample rate %u Hz, device supports %u Hz (%.1f%% difference)\n", + device_name, requested_rate, actual_rate, ((float)actual_rate - requested_rate) / requested_rate * 100.0f); + fprintf(stderr, "INFO: %s: Adapting to device rate %u Hz to avoid pitch/speed distortion\n", device_name, actual_rate); + fflush(stderr); + + actual_frame_size = (actual_rate * 20) / 1000; + if (channels == 2) { + if (actual_frame_size < 480) actual_frame_size = 480; + if (actual_frame_size > 2880) actual_frame_size = 2880; + } + + fprintf(stderr, "INFO: %s: Adjusted frame size to %u samples (20ms at %u Hz)\n", + device_name, actual_frame_size, actual_rate); + fflush(stderr); } - snd_pcm_uframes_t period_size = frame_size; + snd_pcm_uframes_t period_size = actual_frame_size; if (period_size < 64) period_size = 64; err = snd_pcm_hw_params_set_period_size_near(handle, params, &period_size, 0); @@ -301,6 +320,14 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) { err = snd_pcm_hw_params(handle, params); if (err < 0) return err; + unsigned int verified_rate = 0; + err = snd_pcm_hw_params_get_rate(params, &verified_rate, 0); + if (err < 0 || verified_rate != actual_rate) { + fprintf(stderr, "WARNING: %s: Rate verification failed - expected %u Hz, got %u Hz\n", + device_name, actual_rate, verified_rate); + fflush(stderr); + } + err = snd_pcm_sw_params_current(handle, sw_params); if (err < 0) return err; @@ -313,7 +340,13 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) { err = snd_pcm_sw_params(handle, sw_params); if (err < 0) return err; - return snd_pcm_prepare(handle); + err = snd_pcm_prepare(handle); + if (err < 0) return err; + + if (actual_rate_out) *actual_rate_out = actual_rate; + if (actual_frame_size_out) *actual_frame_size_out = actual_frame_size; + + return 0; } // AUDIO OUTPUT PATH FUNCTIONS (TC358743 HDMI Audio → Client Speakers) @@ -355,7 +388,9 @@ int jetkvm_audio_capture_init() { return -1; } - err = configure_alsa_device(pcm_capture_handle, "capture"); + unsigned int actual_rate = 0; + uint16_t actual_frame_size = 0; + err = configure_alsa_device(pcm_capture_handle, "capture", &actual_rate, &actual_frame_size); if (err < 0) { snd_pcm_close(pcm_capture_handle); pcm_capture_handle = NULL; @@ -363,8 +398,12 @@ int jetkvm_audio_capture_init() { return -2; } + fprintf(stderr, "INFO: capture: Initializing Opus encoder at %u Hz, %u channels, frame size %u\n", + actual_rate, channels, actual_frame_size); + fflush(stderr); + int opus_err = 0; - encoder = opus_encoder_create(sample_rate, channels, OPUS_APPLICATION_AUDIO, &opus_err); + encoder = opus_encoder_create(actual_rate, channels, OPUS_APPLICATION_AUDIO, &opus_err); if (!encoder || opus_err != OPUS_OK) { if (pcm_capture_handle) { snd_pcm_close(pcm_capture_handle); @@ -534,7 +573,9 @@ int jetkvm_audio_playback_init() { } } - err = configure_alsa_device(pcm_playback_handle, "playback"); + unsigned int actual_rate = 0; + uint16_t actual_frame_size = 0; + err = configure_alsa_device(pcm_playback_handle, "playback", &actual_rate, &actual_frame_size); if (err < 0) { snd_pcm_close(pcm_playback_handle); pcm_playback_handle = NULL; @@ -542,8 +583,12 @@ int jetkvm_audio_playback_init() { return -1; } + fprintf(stderr, "INFO: playback: Initializing Opus decoder at %u Hz, %u channels, frame size %u\n", + actual_rate, channels, actual_frame_size); + fflush(stderr); + int opus_err = 0; - decoder = opus_decoder_create(sample_rate, channels, &opus_err); + decoder = opus_decoder_create(actual_rate, channels, &opus_err); if (!decoder || opus_err != OPUS_OK) { snd_pcm_close(pcm_playback_handle); pcm_playback_handle = NULL;