]> git.sesse.net Git - nageru/commitdiff
Whenever the speed changes, ease into it over the next 200 ms.
authorSteinar H. Gunderson <steinar+nageru@gunderson.no>
Wed, 11 Mar 2020 20:30:15 +0000 (21:30 +0100)
committerSteinar H. Gunderson <steinar+nageru@gunderson.no>
Sat, 14 Mar 2020 22:44:17 +0000 (23:44 +0100)
This causes a slight delay compared to the operator's wishes,
but that should hardly be visible, and it allows for somewhat
better behavior when we get very abrupt changes from the controller
(or the lock button suddenly is pressed) -- it's essentially
more of an interpolation. Even more importantly, it will allow us
to make a little trick to increase performance in the next patch
that would be somewhat more jerky without it.

futatabi/player.cpp

index d168a9dfc051b76339fccbabc7c38dc2acf0ac12..982091d88595f32ee20c512f561c624c95bcac35 100644 (file)
@@ -125,6 +125,7 @@ public:
        TimelineTracker(double master_speed, int64_t out_pts_origin)
                : master_speed(master_speed), last_out_pts(out_pts_origin) {
                origin.out_pts = out_pts_origin;
+               master_speed_ease_target = master_speed;  // Keeps GCC happy.
        }
 
        void new_clip(steady_clock::time_point wallclock_origin, const Clip *clip, int64_t start_pts_offset)
@@ -141,6 +142,8 @@ public:
 
        int64_t get_in_pts_origin() const { return origin.in_pts; }
        bool playing_at_normal_speed() const {
+               if (in_easing) return false;
+
                const double effective_speed = clip->speed * master_speed;
                return effective_speed >= 0.999 && effective_speed <= 1.001;
        }
@@ -151,25 +154,56 @@ public:
 
        void change_master_speed(double new_master_speed, Instant now);
 
+       // 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);
+
 private:
+       // Find out how far we are into the easing curve (0..1).
+       // We use this to adjust the input pts.
+       double find_ease_t(double out_pts) const;
+       double easing_out_pts_adjustment(double out_pts) const;
+
        double master_speed;
        const Clip *clip = nullptr;
        Instant origin;
        int64_t last_out_pts;
+
+       // If easing between new and old master speeds.
+       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.
 };
 
 TimelineTracker::Instant TimelineTracker::advance_to_frame(int64_t frameno)
 {
        Instant ret;
        double in_pts_double = origin.in_pts + TIMEBASE * clip->speed * (frameno - origin.frameno) * master_speed / global_flags.output_framerate;
-       ret.in_pts = lrint(in_pts_double);
        double out_pts_double = origin.out_pts + TIMEBASE * (frameno - origin.frameno) / global_flags.output_framerate;
+
+       if (in_easing) {
+               double in_pts_adjustment = easing_out_pts_adjustment(out_pts_double) * clip->speed;
+               in_pts_double += in_pts_adjustment;
+       }
+
+       ret.in_pts = lrint(in_pts_double);
        ret.out_pts = lrint(out_pts_double);
        ret.wallclock_time = origin.wallclock_time + microseconds(lrint((out_pts_double - origin.out_pts) * 1e6 / TIMEBASE));
        ret.frameno = frameno;
 
        last_out_pts = ret.out_pts;
 
+       if (in_easing && ret.out_pts >= ease_started_pts + ease_length_out_pts) {
+               // We have ended easing. Add what we need for the entire easing period,
+               // then _actually_ change the speed as we go back into normal mode.
+               origin.out_pts += easing_out_pts_adjustment(out_pts_double);
+               change_master_speed(master_speed_ease_target, ret);
+               in_easing = false;
+       }
+
        return ret;
 }
 
@@ -182,6 +216,35 @@ void TimelineTracker::change_master_speed(double new_master_speed, Instant now)
        origin = now;
 }
 
+void TimelineTracker::start_easing(double new_master_speed, Instant now)
+{
+       if (in_easing) {
+               // Apply whatever we managed to complete of the previous easing.
+               origin.out_pts += easing_out_pts_adjustment(now.out_pts);
+               double reached_speed = master_speed + (master_speed_ease_target - master_speed) * find_ease_t(now.out_pts);
+               change_master_speed(reached_speed, now);
+       }
+       in_easing = true;
+       ease_started_pts = now.out_pts;
+       master_speed_ease_target = new_master_speed;
+}
+
+double TimelineTracker::find_ease_t(double out_pts) const
+{
+       return (out_pts - ease_started_pts) / double(ease_length_out_pts);
+}
+
+double TimelineTracker::easing_out_pts_adjustment(double out_pts) const
+{
+       double t = find_ease_t(out_pts);
+       double area_factor = (master_speed_ease_target - master_speed) * ease_length_out_pts;
+       double val = 0.5 * min(t, 1.0) * min(t, 1.0) * area_factor;
+       if (t > 1.0) {
+               val += area_factor * (t - 1.0);
+       }
+       return val;
+}
+
 }  // namespace
 
 void Player::play_playlist_once()
@@ -272,7 +335,7 @@ void Player::play_playlist_once()
 
                        float new_master_speed = change_master_speed.exchange(0.0f / 0.0f);
                        if (!std::isnan(new_master_speed)) {
-                               timeline.change_master_speed(new_master_speed, instant);
+                               timeline.start_easing(new_master_speed, instant);
                        }
 
                        if (should_skip_to_next.exchange(false)) {  // Test and clear.