}
void snap_by(int64_t offset) {
+ if (in_easing) {
+ // Easing will normally aim for a snap at the very end,
+ // so don't disturb it by jittering during the ease.
+ return;
+ }
origin.in_pts += offset;
}
void change_master_speed(double new_master_speed, Instant now);
+ float in_master_speed(float speed) const {
+ return (!in_easing && fabs(master_speed - speed) < 1e-6);
+ }
+
// Instead of changing the speed instantly, change it over the course of
// about 200 ms. This is a simple linear ramp; I tried various forms of
// Bézier curves for more elegant/dramatic changing, but it seemed linear
// looked just as good in practical video.
- void start_easing(double new_master_speed, Instant now);
+ void start_easing(double new_master_speed, int64_t length_out_pts, Instant now);
+
+ int64_t find_easing_length(double master_speed_target, int64_t length_out_pts, const vector<FrameOnDisk> &frames, Instant now);
private:
// Find out how far we are into the easing curve (0..1).
bool in_easing = false;
int64_t ease_started_pts = 0;
double master_speed_ease_target;
- static constexpr int64_t ease_length_out_pts = TIMEBASE / 5; // 200 ms.
+ int64_t ease_length_out_pts = 0;
};
TimelineTracker::Instant TimelineTracker::advance_to_frame(int64_t frameno)
origin = now;
}
-void TimelineTracker::start_easing(double new_master_speed, Instant now)
+void TimelineTracker::start_easing(double new_master_speed, int64_t length_out_pts, Instant now)
{
if (in_easing) {
// Apply whatever we managed to complete of the previous easing.
in_easing = true;
ease_started_pts = now.out_pts;
master_speed_ease_target = new_master_speed;
+ ease_length_out_pts = length_out_pts;
}
double TimelineTracker::find_ease_t(double out_pts) const
return val;
}
+int64_t TimelineTracker::find_easing_length(double master_speed_target, int64_t desired_length_out_pts, const vector<FrameOnDisk> &frames, Instant now)
+{
+ // Find out what frame we would have hit (approximately) with the given ease length.
+ double in_pts_length = 0.5 * (master_speed_target + master_speed) * desired_length_out_pts * clip->speed;
+ const int input_frame_num = distance(
+ frames.begin(),
+ find_first_frame_at_or_after(frames, lrint(now.in_pts + in_pts_length)));
+
+ // Round length_out_pts to the nearest amount of whole frames.
+ const double frame_length = TIMEBASE / global_flags.output_framerate;
+ const int length_out_frames = lrint(desired_length_out_pts / frame_length);
+
+ // Time the easing so that we aim at 200 ms (or whatever length_out_pts
+ // was), but adjust it so that we hit exactly on a frame. Unless we are
+ // somehow unlucky and run in the middle of a bad fade, this should
+ // lock us nicely into a cadence where we hit original frames (of course
+ // assuming the new speed is a reasonable ratio).
+ //
+ // Assume for a moment that we are easing into a slowdown, and that
+ // we're slightly too late to hit the frame we want to. This means that
+ // we can shorten the ease a bit; this chops some of the total integrated
+ // velocity and arrive at the frame a bit sooner. Solve for the time
+ // we want to shorten the ease by (let's call it x, where the original
+ // length of the ease is called len) such that we hit exactly the in
+ // pts at the right time:
+ //
+ // 0.5 * (mst + ms) * (len - x) * cs + mst * x * cs = desired_len_in_pts
+ //
+ // gives
+ //
+ // x = (2 * desired_len_in_pts / cs - (mst + ms) * len) / (mst - ms)
+ //
+ // Conveniently, this holds even if we are too early; a negative x
+ // (surprisingly!) gives a lenghtening such that we don't hit the desired
+ // frame, but hit one slightly later. (x larger than len means that
+ // it's impossible to hit the desired frame, even if we dropped the ease
+ // altogether and just changed speeds instantly.) We also have sign invariance,
+ // so that these properties hold even if we are speeding up, not slowing
+ // down. Together, these two properties mean that we can cast a fairly
+ // wide net, trying various input and output frames and seeing which ones
+ // can be matched up with a minimal change to easing time. (This lets us
+ // e.g. end the ease close to the midpoint between two endpoint frames
+ // even if we don't know the frame rate, or deal fairly robustly with
+ // dropped input frames.) Many of these will give us the same answer,
+ // but that's fine, because the ease length is the only output.
+ int64_t best_length_out_pts = TIMEBASE * 10; // Infinite.
+ for (int output_frame_offset = -2; output_frame_offset <= 2; ++output_frame_offset) {
+ int64_t aim_length_out_pts = lrint((length_out_frames + output_frame_offset) * frame_length);
+ if (aim_length_out_pts < 0) {
+ continue;
+ }
+
+ for (int input_frame_offset = -2; input_frame_offset <= 2; ++input_frame_offset) {
+ if (input_frame_num + input_frame_offset < 0 ||
+ input_frame_num + input_frame_offset >= int(frames.size())) {
+ continue;
+ }
+ const int64_t in_pts = frames[input_frame_num + input_frame_offset].pts;
+ double shorten_by_out_pts = (2.0 * (in_pts - now.in_pts) / clip->speed - (master_speed_target + master_speed) * aim_length_out_pts) / (master_speed_target - master_speed);
+ int64_t length_out_pts = lrint(aim_length_out_pts - shorten_by_out_pts);
+
+ if (length_out_pts >= 0 &&
+ abs(length_out_pts - desired_length_out_pts) < abs(best_length_out_pts - desired_length_out_pts)) {
+ best_length_out_pts = length_out_pts;
+ }
+ }
+ }
+
+ // If we need more than two seconds of easing, we give up --
+ // this can happen if we're e.g. going from 101% to 100%.
+ // If so, it would be better to let other mechanisms, such as the switch
+ // to the next clip, deal with getting us back into sync.
+ if (best_length_out_pts > TIMEBASE * 2) {
+ return desired_length_out_pts;
+ } else {
+ return best_length_out_pts;
+ }
+}
+
} // namespace
void Player::play_playlist_once()
next_frame_start = instant.wallclock_time;
float new_master_speed = change_master_speed.exchange(0.0f / 0.0f);
- if (!std::isnan(new_master_speed)) {
- timeline.start_easing(new_master_speed, instant);
+ if (!std::isnan(new_master_speed) && !timeline.in_master_speed(new_master_speed)) {
+ int64_t ease_length_out_pts = TIMEBASE / 5; // 200 ms.
+ int64_t recommended_pts_length = timeline.find_easing_length(new_master_speed, ease_length_out_pts, frames[clip->stream_idx], instant);
+ timeline.start_easing(new_master_speed, recommended_pts_length, instant);
}
if (should_skip_to_next.exchange(false)) { // Test and clear.