chromium/chromecast/media/audio/rate_adjuster.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/media/audio/rate_adjuster.h"

#include <algorithm>
#include <cmath>
#include <utility>

#include "base/check.h"

namespace chromecast {
namespace media {

namespace {
constexpr auto kMaxRateChangeInterval = base::Minutes(5);
}  // namespace

RateAdjuster::RateAdjuster(const Config& config,
                           RateChangeCallback change_clock_rate,
                           double current_clock_rate)
    : config_(config),
      change_clock_rate_(std::move(change_clock_rate)),
      linear_error_(config_.linear_regression_window.InMicroseconds()),
      current_clock_rate_(current_clock_rate) {
  DCHECK(change_clock_rate_);
}

RateAdjuster::~RateAdjuster() = default;

void RateAdjuster::Reserve(int count) {
  linear_error_.Reserve(count);
}

void RateAdjuster::Reset() {
  linear_error_.Reset();
  initialized_ = false;
  clock_rate_start_timestamp_ = 0;
  initial_timestamp_ = 0;
  clock_rate_error_base_ = 0.0;
}

void RateAdjuster::AddError(int64_t error, int64_t timestamp) {
  if (!initialized_) {
    clock_rate_start_timestamp_ = timestamp;
    clock_rate_error_base_ = 0.0;
    initial_timestamp_ = timestamp;
    initialized_ = true;
  }

  int64_t x = timestamp - initial_timestamp_;

  // Error is positive if actions are happening too late.
  // We perform |current_clock_rate_| seconds of actions per second of actual
  // time. We want to run a linear regression on how the error is changing over
  // time, if we ignore the effects of any previous clock rate changes. To do
  // this, we correct the error value to what it would have been if we had never
  // adjusted the clock rate.
  // In the last N seconds, if the clock rate was 1.0 we would have performed
  // (1.0 - clock_rate) * N more seconds of actions, so the current action would
  // have occurred that much sooner (reducing its error by that amount). We
  // also need to take into account the previous "expected error" (due to clock
  // rate changes) at the point when we last changed the clock rate. The
  // expected error now is the previous expected error, plus the change due to
  // the clock rate of (1.0 - clock_rate) * N seconds.
  int64_t time_at_current_clock_rate = timestamp - clock_rate_start_timestamp_;
  double correction = clock_rate_error_base_ +
                      (1.0 - current_clock_rate_) * time_at_current_clock_rate;
  int64_t corrected_error = error - correction;
  linear_error_.AddSample(x, corrected_error, 1.0);

  if (time_at_current_clock_rate <
      config_.rate_change_interval.InMicroseconds()) {
    // Don't change clock rate too frequently.
    return;
  }

  int64_t offset;
  double slope;
  double e;
  if (!linear_error_.EstimateY(x, &offset, &e) ||
      !linear_error_.EstimateSlope(&slope, &e)) {
    return;
  }

  // Get the smoothed error (linear regression estimate) at the current time,
  // translated back into actual error.
  int64_t smoothed_error = offset + correction;

  // If slope is positive, a clock rate of 1.0 is too slow (actions are
  // occurring progressively later than desired). We wanted to do slope*N
  // seconds actions during N seconds than would have been done at rate 1.0.
  // Therefore the actual clock rate should be (1.0 + slope).
  // However, we also want to correct for any existing offset. We correct so
  // that the error should reduce to 0 by the next rate change interval;
  // however the rate change is capped to prevent very fast slewing.
  double offset_correction =
      static_cast<double>(smoothed_error) /
      (config_.rate_change_interval.InMicroseconds() * 2);
  if (std::abs(smoothed_error) < config_.max_ignored_current_error) {
    // Offset is small enough that we can ignore it, but still correct a little
    // bit to avoid bouncing in and out of the ignored region.
    offset_correction = offset_correction / 4;
  }
  offset_correction =
      std::clamp(offset_correction, -config_.max_current_error_correction,
                 config_.max_current_error_correction);
  double new_rate = (1.0 + slope) + offset_correction;

  // Only change the clock rate if the difference between the desired rate and
  // the current rate is larger than the minimum change.
  if (std::fabs(new_rate - current_clock_rate_) > config_.min_rate_change ||
      time_at_current_clock_rate > kMaxRateChangeInterval.InMicroseconds()) {
    current_clock_rate_ =
        change_clock_rate_.Run(new_rate, slope, smoothed_error);
    clock_rate_start_timestamp_ = timestamp;
    clock_rate_error_base_ = correction;
  }
}

}  // namespace media
}  // namespace chromecast