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 10 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.
This commit is contained in:
1 parent
382e3e74e8
commit
68b1d10da5
2 files changed
+53
-72
No files matched your search
+2
-2
@@ -97,7 +97,6 @@ struct frame_info {
|
||||
double pts;
|
||||
double duration; // PTS difference to next frame
|
||||
double approx_duration; // possibly fixed/smoothed out duration
|
||||
double av_diff; // A/V diff at time of scheduling
|
||||
int num_vsyncs; // scheduled vsyncs, if using display-sync
|
||||
};
|
||||
|
||||
@@ -345,7 +344,8 @@ typedef struct MPContext {
|
||||
// update_playback_speed() updates them from the other fields.
|
||||
double audio_speed, video_speed;
|
||||
bool display_sync_active;
|
||||
int display_sync_drift_dir;
|
||||
double audio_drift_compensation;
|
||||
double avd_filtered;
|
||||
// Timing error (in seconds) due to rounding on vsync boundaries
|
||||
double display_sync_error;
|
||||
// Number of mistimed frames.
|
||||
|
||||
+51
-70
@@ -118,7 +118,8 @@ void reset_video_state(struct MPContext *mpctx)
|
||||
mpctx->last_av_difference = 0;
|
||||
mpctx->mistimed_frames_total = 0;
|
||||
mpctx->drop_message_shown = 0;
|
||||
mpctx->display_sync_drift_dir = 0;
|
||||
mpctx->audio_drift_compensation = 0;
|
||||
mpctx->avd_filtered = 0;
|
||||
mpctx->display_sync_error = 0;
|
||||
mpctx->display_sync_active = 0;
|
||||
|
||||
@@ -721,32 +722,19 @@ static bool using_spdif_passthrough(struct MPContext *mpctx)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compute the relative audio speed difference by taking A/V dsync into account.
|
||||
static double compute_audio_drift(struct MPContext *mpctx, double vsync)
|
||||
{
|
||||
// Least-squares linear regression, using relative real time for x, and
|
||||
// audio desync for y. Assume speed didn't change for the frames we're
|
||||
// looking at for simplicity. This also should actually use the realtime
|
||||
// (minus paused time) for x, but use vsync scheduling points instead.
|
||||
if (mpctx->num_past_frames <= 10)
|
||||
return NAN;
|
||||
int num = mpctx->num_past_frames - 1;
|
||||
double sum_x = 0, sum_y = 0, sum_xy = 0, sum_xx = 0;
|
||||
double x = 0;
|
||||
for (int n = 0; n < num; n++) {
|
||||
struct frame_info *frame = &mpctx->past_frames[n + 1];
|
||||
if (frame->num_vsyncs < 0)
|
||||
return NAN;
|
||||
double y = frame->av_diff;
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_xy += x * y;
|
||||
sum_xx += x * x;
|
||||
x -= frame->num_vsyncs * vsync;
|
||||
}
|
||||
return (sum_x * sum_y - num * sum_xy) / (sum_x * sum_x - num * sum_xx);
|
||||
}
|
||||
|
||||
// Audio drift compensation for display-sync. Tunes the audio-speed scale
|
||||
// factor (speed_factor_a) relative to video so that A/V difference decays
|
||||
// exponentially toward zero.
|
||||
//
|
||||
// The raw per-frame av_diff is dominated by vsync-rounding jitter
|
||||
// (±1 vsync). Acting on it directly would modulate audio pitch at the
|
||||
// jitter frequency. A low-pass filter on av_diff removes the jitter, so
|
||||
// comp converges to the value that cancels the true underlying drift and
|
||||
// then stops moving.
|
||||
//
|
||||
// From d(av_diff)/dt = other * (comp - 1) with other = playback_speed *
|
||||
// speed_factor_v, the comp that makes av_diff_lp decay with time
|
||||
// constant AVD_RECOVERY_TIME is (1 - av_diff_lp / (τ · other)).
|
||||
static void adjust_audio_drift_compensation(struct MPContext *mpctx, double vsync)
|
||||
{
|
||||
struct MPOpts *opts = mpctx->opts;
|
||||
@@ -756,54 +744,47 @@ static void adjust_audio_drift_compensation(struct MPContext *mpctx, double vsyn
|
||||
mpctx->audio_status != STATUS_PLAYING)
|
||||
{
|
||||
mpctx->speed_factor_a = mpctx->speed_factor_v;
|
||||
mpctx->audio_drift_compensation = 0;
|
||||
mpctx->avd_filtered = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to smooth out audio timing drifts. This can happen if either
|
||||
// video isn't playing at expected speed, or audio is not playing at
|
||||
// the requested speed. Both are unavoidable.
|
||||
// The audio desync is made up of 2 parts: 1. drift due to rounding
|
||||
// errors and imperfect information, and 2. an offset, due to
|
||||
// unaligned audio/video start, or disruptive events halting audio
|
||||
// or video for a small time.
|
||||
// Instead of trying to be clever, just apply an awfully dumb drift
|
||||
// compensation with a constant factor, which does what we want. In
|
||||
// theory we could calculate the exact drift compensation needed,
|
||||
// but it likely would be wrong anyway, and we'd run into the same
|
||||
// issues again, except with more complex code.
|
||||
// 1 means drifts to positive, -1 means drifts to negative
|
||||
double max_drift = vsync / 2;
|
||||
double av_diff = mpctx->last_av_difference;
|
||||
int new = mpctx->display_sync_drift_dir;
|
||||
if (av_diff * -mpctx->display_sync_drift_dir >= 0)
|
||||
new = 0;
|
||||
if (fabs(av_diff) > max_drift)
|
||||
new = av_diff >= 0 ? 1 : -1;
|
||||
// Low-pass time constant on av_diff. Long enough to suppress
|
||||
// per-frame vsync-rounding jitter, short enough not to lag the
|
||||
// recovery loop noticeably.
|
||||
const double AVD_FILTER_TIME = 1.0;
|
||||
// Target exponential decay time of av_diff_lp toward zero.
|
||||
const double AVD_RECOVERY_TIME = 10.0;
|
||||
// Per-frame ceiling on comp motion, expressed as a fraction of the
|
||||
// full compensation budget. Adaptive slew (|p_delta|) normally
|
||||
// governs; this cap only bites for large post-seek kicks, spreading
|
||||
// the ramp to full correction over ~SLEW_RAMP_FRAMES frames so the
|
||||
// audio pipeline can absorb it without artifacts.
|
||||
const double SLEW_RAMP_FRAMES = 10.0;
|
||||
|
||||
bool change = mpctx->display_sync_drift_dir != new;
|
||||
if (new || change) {
|
||||
if (change)
|
||||
MP_VERBOSE(mpctx, "Change display sync audio drift: %d\n", new);
|
||||
mpctx->display_sync_drift_dir = new;
|
||||
double max_correct = opts->sync_max_audio_change / 100;
|
||||
double other = opts->playback_speed * mpctx->speed_factor_v;
|
||||
double comp = 1.0 + mpctx->audio_drift_compensation;
|
||||
|
||||
double max_correct = opts->sync_max_audio_change / 100;
|
||||
double audio_factor = 1 + max_correct * -mpctx->display_sync_drift_dir;
|
||||
double alpha = vsync / (AVD_FILTER_TIME + vsync);
|
||||
mpctx->avd_filtered += alpha * (mpctx->last_av_difference -
|
||||
mpctx->avd_filtered);
|
||||
double avd_lp = mpctx->avd_filtered;
|
||||
|
||||
if (new == 0) {
|
||||
// If we're resetting, actually try to be clever and pick a speed
|
||||
// which compensates the general drift we're getting.
|
||||
double drift = compute_audio_drift(mpctx, vsync);
|
||||
if (isnormal(drift)) {
|
||||
// other = will be multiplied with audio_factor for final speed
|
||||
double other = mpctx->opts->playback_speed * mpctx->speed_factor_v;
|
||||
audio_factor = (mpctx->audio_speed - drift) / other;
|
||||
MP_VERBOSE(mpctx, "Compensation factor: %f\n", audio_factor);
|
||||
}
|
||||
}
|
||||
double p_delta = MPCLAMP(-avd_lp / (AVD_RECOVERY_TIME * other),
|
||||
-max_correct, max_correct);
|
||||
// Adaptive slew: fast response for large excursions, gentle motion
|
||||
// near equilibrium.
|
||||
double slew = MPMIN(fabs(p_delta), max_correct / SLEW_RAMP_FRAMES);
|
||||
|
||||
audio_factor = MPCLAMP(audio_factor, 1 - max_correct, 1 + max_correct);
|
||||
mpctx->speed_factor_a = audio_factor * mpctx->speed_factor_v;
|
||||
}
|
||||
double target_comp = MPCLAMP(1.0 + p_delta,
|
||||
1 - max_correct, 1 + max_correct);
|
||||
double new_comp = comp + MPCLAMP(target_comp - comp, -slew, +slew);
|
||||
|
||||
mpctx->audio_drift_compensation = new_comp - 1.0;
|
||||
mpctx->speed_factor_a = new_comp * mpctx->speed_factor_v;
|
||||
|
||||
MP_STATS(mpctx, "value %f adrift", avd_lp);
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
if (!mpctx->display_sync_active) {
|
||||
mpctx->display_sync_error = 0.0;
|
||||
mpctx->display_sync_drift_dir = 0;
|
||||
mpctx->audio_drift_compensation = 0.0;
|
||||
mpctx->avd_filtered = 0;
|
||||
}
|
||||
|
||||
mpctx->display_sync_active = false;
|
||||
@@ -909,7 +891,6 @@ static void handle_display_sync_frame(struct MPContext *mpctx,
|
||||
update_av_diff(mpctx, time_left * opts->playback_speed);
|
||||
|
||||
mpctx->past_frames[0].num_vsyncs = num_vsyncs;
|
||||
mpctx->past_frames[0].av_diff = mpctx->last_av_difference;
|
||||
|
||||
if (resample || mode == VS_DISP_ADROP || mode == VS_DISP_TEMPO) {
|
||||
adjust_audio_drift_compensation(mpctx, vsync);
|
||||
|
||||
Reference in new issue
Block a user