Skip to content

Commit 3e4127f

Browse files
committed
player/video: rewrite display-sync audio drift compensation
The old code had several interacting problems that prevented A/V from reaching a steady state. When the A/V difference was large, it applied the maximum allowed compensation in one step. That was fine for catching up, but the value was never re-evaluated, so audio speed routinely overshot the target. Once the difference crossed zero (or re-entered the acceptable range) it would run a linear regression over recent av_diff samples and snap the audio speed to the estimated drift. The fatal flaw was that those samples already included the controller's own compensation: even when the underlying drift was zero, the controller would "detect" the drift it had just induced and try to cancel it. Between these rare, sparse updates audio speed stayed frozen, so in practice the controller was introducing drift more than it was fixing it. On high fps outputs, where the vsync range is tight, this degenerated into a ping-pong: overshoot, snap back, overshoot again, never settling. The new code replaces the bang-bang direction controller and one-shot regression with a proportional controller driven by a low-pass-filtered av_diff. The LP filter (1 s time constant) rejects per-frame vsync jitter so the compensation no longer chases noise. The target then decays av_diff exponentially toward zero with a 15 s recovery time. Adaptive slew caps per-frame motion to the desired correction magnitude, so large excursions are absorbed quickly while near-equilibrium motion stays below audibility. The result is a continuous, gentle control loop that actually reaches steady state and does so without audible audio-speed modulation.
1 parent 76d556e commit 3e4127f

2 files changed

Lines changed: 57 additions & 76 deletions

File tree

player/core.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ struct frame_info {
9797
double pts;
9898
double duration; // PTS difference to next frame
9999
double approx_duration; // possibly fixed/smoothed out duration
100-
double av_diff; // A/V diff at time of scheduling
101100
int num_vsyncs; // scheduled vsyncs, if using display-sync
102101
};
103102

@@ -345,7 +344,8 @@ typedef struct MPContext {
345344
// update_playback_speed() updates them from the other fields.
346345
double audio_speed, video_speed;
347346
bool display_sync_active;
348-
int display_sync_drift_dir;
347+
double audio_drift_compensation;
348+
double avd_filtered;
349349
// Timing error (in seconds) due to rounding on vsync boundaries
350350
double display_sync_error;
351351
// Number of mistimed frames.

player/video.c

Lines changed: 55 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ void reset_video_state(struct MPContext *mpctx)
118118
mpctx->last_av_difference = 0;
119119
mpctx->mistimed_frames_total = 0;
120120
mpctx->drop_message_shown = 0;
121-
mpctx->display_sync_drift_dir = 0;
121+
mpctx->audio_drift_compensation = 0;
122+
mpctx->avd_filtered = 0;
122123
mpctx->display_sync_error = 0;
123124
mpctx->display_sync_active = 0;
124125

@@ -721,32 +722,19 @@ static bool using_spdif_passthrough(struct MPContext *mpctx)
721722
return false;
722723
}
723724

724-
// Compute the relative audio speed difference by taking A/V dsync into account.
725-
static double compute_audio_drift(struct MPContext *mpctx, double vsync)
726-
{
727-
// Least-squares linear regression, using relative real time for x, and
728-
// audio desync for y. Assume speed didn't change for the frames we're
729-
// looking at for simplicity. This also should actually use the realtime
730-
// (minus paused time) for x, but use vsync scheduling points instead.
731-
if (mpctx->num_past_frames <= 10)
732-
return NAN;
733-
int num = mpctx->num_past_frames - 1;
734-
double sum_x = 0, sum_y = 0, sum_xy = 0, sum_xx = 0;
735-
double x = 0;
736-
for (int n = 0; n < num; n++) {
737-
struct frame_info *frame = &mpctx->past_frames[n + 1];
738-
if (frame->num_vsyncs < 0)
739-
return NAN;
740-
double y = frame->av_diff;
741-
sum_x += x;
742-
sum_y += y;
743-
sum_xy += x * y;
744-
sum_xx += x * x;
745-
x -= frame->num_vsyncs * vsync;
746-
}
747-
return (sum_x * sum_y - num * sum_xy) / (sum_x * sum_x - num * sum_xx);
748-
}
749-
725+
// Audio drift compensation for display-sync. Tunes the audio-speed scale
726+
// factor (speed_factor_a) relative to video so that A/V difference decays
727+
// exponentially toward zero.
728+
//
729+
// The raw per-frame av_diff is dominated by vsync-rounding jitter
730+
// (±1 vsync). Acting on it directly would modulate audio pitch at the
731+
// jitter frequency. A low-pass filter on av_diff removes the jitter, so
732+
// comp converges to the value that cancels the true underlying drift and
733+
// then stops moving.
734+
//
735+
// From d(av_diff)/dt = other * (comp - 1) with other = playback_speed *
736+
// speed_factor_v, the comp that makes av_diff_lp decay with time
737+
// constant AVD_RECOVERY_TIME is (1 - av_diff_lp / (τ · other)).
750738
static void adjust_audio_drift_compensation(struct MPContext *mpctx, double vsync)
751739
{
752740
struct MPOpts *opts = mpctx->opts;
@@ -756,54 +744,47 @@ static void adjust_audio_drift_compensation(struct MPContext *mpctx, double vsyn
756744
mpctx->audio_status != STATUS_PLAYING)
757745
{
758746
mpctx->speed_factor_a = mpctx->speed_factor_v;
747+
mpctx->audio_drift_compensation = 0;
748+
mpctx->avd_filtered = 0;
759749
return;
760750
}
761751

762-
// Try to smooth out audio timing drifts. This can happen if either
763-
// video isn't playing at expected speed, or audio is not playing at
764-
// the requested speed. Both are unavoidable.
765-
// The audio desync is made up of 2 parts: 1. drift due to rounding
766-
// errors and imperfect information, and 2. an offset, due to
767-
// unaligned audio/video start, or disruptive events halting audio
768-
// or video for a small time.
769-
// Instead of trying to be clever, just apply an awfully dumb drift
770-
// compensation with a constant factor, which does what we want. In
771-
// theory we could calculate the exact drift compensation needed,
772-
// but it likely would be wrong anyway, and we'd run into the same
773-
// issues again, except with more complex code.
774-
// 1 means drifts to positive, -1 means drifts to negative
775-
double max_drift = vsync / 2;
776-
double av_diff = mpctx->last_av_difference;
777-
int new = mpctx->display_sync_drift_dir;
778-
if (av_diff * -mpctx->display_sync_drift_dir >= 0)
779-
new = 0;
780-
if (fabs(av_diff) > max_drift)
781-
new = av_diff >= 0 ? 1 : -1;
782-
783-
bool change = mpctx->display_sync_drift_dir != new;
784-
if (new || change) {
785-
if (change)
786-
MP_VERBOSE(mpctx, "Change display sync audio drift: %d\n", new);
787-
mpctx->display_sync_drift_dir = new;
788-
789-
double max_correct = opts->sync_max_audio_change / 100;
790-
double audio_factor = 1 + max_correct * -mpctx->display_sync_drift_dir;
791-
792-
if (new == 0) {
793-
// If we're resetting, actually try to be clever and pick a speed
794-
// which compensates the general drift we're getting.
795-
double drift = compute_audio_drift(mpctx, vsync);
796-
if (isnormal(drift)) {
797-
// other = will be multiplied with audio_factor for final speed
798-
double other = mpctx->opts->playback_speed * mpctx->speed_factor_v;
799-
audio_factor = (mpctx->audio_speed - drift) / other;
800-
MP_VERBOSE(mpctx, "Compensation factor: %f\n", audio_factor);
801-
}
802-
}
803-
804-
audio_factor = MPCLAMP(audio_factor, 1 - max_correct, 1 + max_correct);
805-
mpctx->speed_factor_a = audio_factor * mpctx->speed_factor_v;
806-
}
752+
// Low-pass time constant on av_diff. Long enough to suppress
753+
// per-frame vsync-rounding jitter, short enough not to lag the
754+
// recovery loop noticeably.
755+
const double AVD_FILTER_TIME = 1.0;
756+
// Target exponential decay time of av_diff_lp toward zero.
757+
const double AVD_RECOVERY_TIME = 15.0;
758+
// Per-frame cap on comp motion. The floor is set below audibility so
759+
// comp is essentially still in steady state; the ceiling keeps a big
760+
// av_diff kick from racing across the budget in a single frame, which
761+
// the audio pipeline cannot absorb without artifacts.
762+
const double SLEW_MIN = 1e-5;
763+
const double SLEW_MAX = 1e-3;
764+
765+
double max_correct = opts->sync_max_audio_change / 100;
766+
double other = opts->playback_speed * mpctx->speed_factor_v;
767+
double comp = 1.0 + mpctx->audio_drift_compensation;
768+
769+
double alpha = vsync / (AVD_FILTER_TIME + vsync);
770+
mpctx->avd_filtered += alpha * (mpctx->last_av_difference -
771+
mpctx->avd_filtered);
772+
double avd_lp = mpctx->avd_filtered;
773+
774+
double p_delta = MPCLAMP(-avd_lp / (AVD_RECOVERY_TIME * other),
775+
-max_correct, max_correct);
776+
// Adaptive slew: fast response for large excursions, gentle motion
777+
// near equilibrium.
778+
double slew = MPCLAMP(fabs(p_delta), SLEW_MIN, SLEW_MAX);
779+
780+
double target_comp = MPCLAMP(1.0 + p_delta,
781+
1 - max_correct, 1 + max_correct);
782+
double new_comp = comp + MPCLAMP(target_comp - comp, -slew, +slew);
783+
784+
mpctx->audio_drift_compensation = new_comp - 1.0;
785+
mpctx->speed_factor_a = new_comp * mpctx->speed_factor_v;
786+
787+
MP_STATS(mpctx, "value %f adrift", avd_lp);
807788
}
808789

809790
// Manipulate frame timing for display sync, or do nothing for normal timing.
@@ -816,7 +797,8 @@ static void handle_display_sync_frame(struct MPContext *mpctx,
816797

817798
if (!mpctx->display_sync_active) {
818799
mpctx->display_sync_error = 0.0;
819-
mpctx->display_sync_drift_dir = 0;
800+
mpctx->audio_drift_compensation = 0.0;
801+
mpctx->avd_filtered = 0;
820802
}
821803

822804
mpctx->display_sync_active = false;
@@ -909,7 +891,6 @@ static void handle_display_sync_frame(struct MPContext *mpctx,
909891
update_av_diff(mpctx, time_left * opts->playback_speed);
910892

911893
mpctx->past_frames[0].num_vsyncs = num_vsyncs;
912-
mpctx->past_frames[0].av_diff = mpctx->last_av_difference;
913894

914895
if (resample || mode == VS_DISP_ADROP || mode == VS_DISP_TEMPO) {
915896
adjust_audio_drift_compensation(mpctx, vsync);

0 commit comments

Comments
 (0)