From 2efa40a34ea93c1163cd61640b83608f2918a8b0 Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Tue, 4 Aug 2026 17:40:13 +0300 Subject: [PATCH] audio: phase_vocoder: re-anchor synthesis phase on speed change When the user changes the speed control the component preserves the polar analysis state across the reset so that the interpolation can continue smoothly, but the synthesis phase accumulator output_phase was left drifting. Each output IFFT in stft_do_fft_ifft() advances output_phase by an interpolated one-analysis-hop delta of the form (1 - frac) * angle_delta_prev + frac * angle_delta, where frac is the current interpolation position between two consecutive analysis frames. That delta is added irrespective of the current speed, so at non-unity speed output_phase runs faster than the true polar angle. On top of that, the first post-reset IFFT does not consume a new input FFT yet still applies its phase interpolation step (with frac = 0, so the added term equals angle_delta_prev), which adds one extra angle_delta_prev per reset. After a repeated excursion (for example 1.0 -> 0.5 -> 1.0 with several intermediate steps) both effects accumulate as a random per-bin phase offset that persists after returning to unity speed. Perceptually this smears transients and dulls the sound even though the audio is being processed at speed 1.0 again. The steady-state invariant at speed 1.0 is that after each IFFT's phase interpolation step output_phase equals polar_prev.angle. To preserve this invariant across a speed change, re-anchor output_phase to polar_prev.angle minus angle_delta_prev in reset_for_new_speed(). The first post-reset IFFT then lands output_phase exactly on polar_prev.angle, and all subsequent IFFTs advance normally with no cumulative offset. Existing NULL and channel-count guards keep the init-time call from touching buffers before they are allocated. Signed-off-by: Seppo Ingalsuo --- .../phase_vocoder/phase_vocoder_common.c | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/audio/phase_vocoder/phase_vocoder_common.c b/src/audio/phase_vocoder/phase_vocoder_common.c index d21f4e9e90e5..e31bc0239819 100644 --- a/src/audio/phase_vocoder/phase_vocoder_common.c +++ b/src/audio/phase_vocoder/phase_vocoder_common.c @@ -215,6 +215,12 @@ static int32_t unwrap_angle_q27(int32_t angle) void phase_vocoder_reset_for_new_speed(struct phase_vocoder_comp_data *cd) { struct phase_vocoder_state *state = &cd->state; + struct phase_vocoder_polar *polar = &state->polar; + struct phase_vocoder_fft *fft = &state->fft; + struct ipolar32 *polar_prev_ch; + int32_t *angle_delta_prev_ch; + int32_t *output_phase_ch; + int ch, i; state->speed = cd->speed_ctrl; @@ -232,6 +238,27 @@ void phase_vocoder_reset_for_new_speed(struct phase_vocoder_comp_data *cd) */ state->num_input_fft = 1; state->num_output_ifft = 0; + + /* Re-anchor synthesis phase so the first post-reset IFFT lands + * output_phase back on polar_prev.angle (the steady-state invariant). + * Between speed changes output_phase advances by one analysis-hop + * delta per output IFFT regardless of speed, so at speed != 1 it + * drifts away from the true polar angle; and each reset also adds + * one extra angle_delta_prev on the first no-consume IFFT. If not done, + * both accumulate across interactive speed changes as a per-bin + * phase offset that smears transients and dulls the sound. + */ + for (ch = 0; ch < cd->process_channels; ch++) { + polar_prev_ch = polar->polar_prev[ch]; + angle_delta_prev_ch = polar->angle_delta_prev[ch]; + output_phase_ch = polar->output_phase[ch]; + if (!polar_prev_ch || !angle_delta_prev_ch || !output_phase_ch) + continue; + + for (i = 0; i < fft->half_fft_size; i++) + output_phase_ch[i] = + unwrap_angle_q27(polar_prev_ch[i].angle - angle_delta_prev_ch[i]); + } } static void copy_polar_angles(int32_t *angle_delta_ch, struct ipolar32 *polar_data_ch,