blob: b5c7e9d3e1baf84cb771c71e09c53326cb8fd59c [file] [log] [blame] [edit]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/media/audio/lib/clock/clock_synchronizer.h"
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/time.h>
#include <string>
#include "src/media/audio/lib/clock/audio_clock_coefficients.h"
#include "src/media/audio/lib/clock/logging.h"
namespace media_audio {
namespace {
constexpr int32_t kMicroSrcAdjustmentPpmMax = 2500;
// When tuning a ClientAdjustable to a monotonic target, we use proportional clock adjustments
// instead of the normal PID feedback control, because once a ClientAdjustable is aligned with its
// monotonic target, it stays aligned (the whole clock domain drifts together, if at all).
//
// We synchronize clocks as tightly as possible, in all sync modes; the 10-nsec error threshold
// below is the smallest possible threshold. Clock-tuning precision is limited to integer ppms. If
// 10 msec elapse between clock-sync measurements, a minimum rate adjustment (+/- 1ppm) will
// change the position error (relative to monotonic) by 10 nsec. So once position error is less
// than this threshold, we "lock" the client clock to 0 ppm.
//
// Note: this approach might yield acceptable results for synchronizing software clocks to
// non-monotonic targets as well. Further investigation/measurement is needed.
constexpr zx::duration kLockToMonotonicErrorThreshold = zx::nsec(10);
} // namespace
// static
std::shared_ptr<ClockSynchronizer> ClockSynchronizer::Create(std::shared_ptr<Clock> leader,
std::shared_ptr<Clock> follower,
Mode mode) {
FX_CHECK(leader);
FX_CHECK(follower);
// If we will adjust the follower clock's rate, the follower must be an adjustable clock.
if (mode == Mode::WithAdjustments) {
FX_CHECK(follower->adjustable());
}
struct MakePublicCtor : ClockSynchronizer {
MakePublicCtor(std::shared_ptr<Clock> leader, std::shared_ptr<Clock> follower, Mode mode)
: ClockSynchronizer(std::move(leader), std::move(follower), mode,
(mode == Mode::WithAdjustments)
? media::audio::kPidFactorsClockChasesClock
: media::audio::kPidFactorsMicroSrc) {}
};
return std::make_shared<MakePublicCtor>(std::move(leader), std::move(follower), mode);
}
// static
std::shared_ptr<ClockSynchronizer> ClockSynchronizer::SelectModeAndCreate(
std::shared_ptr<Clock> source, std::shared_ptr<Clock> dest) {
FX_CHECK(source);
FX_CHECK(dest);
// For now we only adjust clocks in kExternalDomain (i.e. "client" clocks).
std::shared_ptr<Clock> follower;
if (source->adjustable() && source->domain() == Clock::kExternalDomain) {
return Create(/*leader=*/dest, /*follower=*/source, Mode::WithAdjustments);
}
if (dest->adjustable() && dest->domain() == Clock::kExternalDomain) {
return Create(/*leader=*/source, /*follower=*/dest, Mode::WithAdjustments);
}
// For MicroSRC, always express the adjustment relative to the source.
return Create(/*leader=*/dest, /*follower=*/source, Mode::WithMicroSRC);
}
int32_t ClockSynchronizer::ClampPpm(int32_t ppm) {
if (mode() == Mode::WithMicroSRC) {
return std::clamp(ppm, -kMicroSrcAdjustmentPpmMax, kMicroSrcAdjustmentPpmMax);
} else {
return Clock::ClampZxClockPpm(ppm);
}
}
int32_t ClockSynchronizer::ClampDoubleToPpm(double val) {
return ClampPpm(static_cast<int32_t>(std::round(static_cast<double>(val) * 1e6)));
}
int32_t ClockSynchronizer::follower_adjustment_ppm() const {
return last_adjustment_ppm_ ? *last_adjustment_ppm_ : 0;
}
bool ClockSynchronizer::NeedsSynchronization() const {
FX_CHECK(state_on_reset_) << "must call Reset before Update";
auto& follower_at_reset = state_on_reset_->follower_snapshot;
auto& leader_at_reset = state_on_reset_->leader_snapshot;
// Synchronization not needed if the leader and follower are the same clocks.
if (follower_->koid() == leader_->koid()) {
return false;
}
// Synchronization not needed if the leader and follower have identical rates and
// haven't changed since the last Reset.
if (follower_at_reset.to_clock_mono.rate() == leader_at_reset.to_clock_mono.rate() &&
follower_at_reset.generation == follower_->to_clock_mono_snapshot().generation &&
leader_at_reset.generation == leader_->to_clock_mono_snapshot().generation) {
// Force synchronization if the Reset snapshot was not IdenticalToMonotonic.
//
// TODO(fxbug.dev/87651): This check is not necessary but preserves identical behavior
// with the old code from audio_core. Some unit tests require this check because they
// setup the clocks inconsistently relative to internal Mix parameters (e.g. some
// MixStagePositionTests).
if (!follower_->IdenticalToMonotonicClock() || !leader_->IdenticalToMonotonicClock()) {
return true;
}
return false;
}
// Synchronization may be needed.
return true;
}
void ClockSynchronizer::Reset(zx::time mono_now) {
FX_CHECK(!last_mono_time_ || mono_now > *last_mono_time_)
<< "Reset at time " << mono_now.get() << " is not after the last Update or Reset at time "
<< last_mono_time_->get();
pid_.Start(mono_now);
last_mono_time_ = mono_now;
state_on_reset_ = {
.follower_snapshot = follower_->to_clock_mono_snapshot(),
.leader_snapshot = leader_->to_clock_mono_snapshot(),
};
}
void ClockSynchronizer::Update(zx::time mono_now, zx::duration follower_pos_error) {
FX_CHECK(state_on_reset_) << "must call Reset before Update";
FX_CHECK(mono_now > *last_mono_time_)
<< "Update at time " << mono_now.get() << " is not after the last Update or Reset at time "
<< last_mono_time_->get();
auto adjust_ppm = ComputeNewAdjustPpm(mono_now, follower_pos_error);
if (adjust_ppm) {
LogClockAdjustment(*follower_, last_adjustment_ppm_, *adjust_ppm, follower_pos_error, pid_);
}
// Update the follower's clock rate if it changed.
if (mode() == Mode::WithAdjustments && adjust_ppm) {
if (last_adjustment_ppm_ != adjust_ppm) {
follower_->SetRate(*adjust_ppm);
}
}
last_adjustment_ppm_ = adjust_ppm;
last_mono_time_ = mono_now;
}
std::optional<int32_t> ClockSynchronizer::ComputeNewAdjustPpm(zx::time mono_now,
zx::duration follower_pos_error) {
constexpr bool kEnableFollowerPosErrChecks = false;
if (!NeedsSynchronization()) {
// TODO(fxbug.dev/87651): Enable this check. It currently cannot be enabled because unit tests
// (e.g. mix_stage_unittest.cc) change internal Mix parameters (e.g. info.source_pos_error) in
// ways that are inconsistent with the test's clocks.
if constexpr (kEnableFollowerPosErrChecks) {
FX_CHECK(follower_pos_error == zx::nsec(0))
<< "measured non-zero position error " << follower_pos_error.to_nsecs()
<< "ns when synchronization is not needed";
}
return std::nullopt;
}
// If the leader and follower are in the same clock domain, they have the same rate,
// therefore they must not diverge.
if (leader_->domain() == follower_->domain() && leader_->domain() != Clock::kExternalDomain) {
// TODO(fxbug.dev/87651): Enable this check. See above comment.
if constexpr (kEnableFollowerPosErrChecks) {
FX_CHECK(follower_pos_error == zx::nsec(0))
<< "measured non-zero position error " << follower_pos_error.to_nsecs()
<< "ns from clocks in the same domain, where domain=" << leader_->domain();
}
return std::nullopt;
}
if (mode() == Mode::WithAdjustments && leader_->domain() == Clock::kMonotonicDomain) {
// Converge position proportionally instead of the normal mechanism. Doing this rather than
// allowing the PID to fully settle (and then locking to 0) gets us to tight sync faster.
// See audio_clock_coefficients.h for an explanation of why positive error leads to a positive
// clock rate adjustment.
auto adjust_ppm =
ClampPpm(static_cast<int32_t>(follower_pos_error / kLockToMonotonicErrorThreshold));
// Not using the PID, so reset it.
pid_.Start(mono_now);
return adjust_ppm;
}
// Otherwise, use the PID to compute an adjustment.
pid_.TuneForError(mono_now, static_cast<double>(follower_pos_error.to_nsecs()));
auto adjust_ppm = ClampDoubleToPpm(pid_.Read());
return adjust_ppm;
}
std::string ClockSynchronizer::ToDebugString() const {
auto mono_to_follower_ref = follower_->to_clock_mono().Inverse();
double mono_to_follower_rate = static_cast<double>(mono_to_follower_ref.subject_delta()) /
static_cast<double>(mono_to_follower_ref.reference_delta());
double follower_ppm = 1'000'000.0 * mono_to_follower_rate - 1'000'000.0;
auto mono_to_leader_ref = leader_->to_clock_mono().Inverse();
double mono_to_leader_rate = static_cast<double>(mono_to_leader_ref.subject_delta()) /
static_cast<double>(mono_to_leader_ref.reference_delta());
double leader_ppm = 1'000'000.0 * mono_to_leader_rate - 1'000'000.0;
std::stringstream os;
os << "Mode " << mode() << ". Follower (0x" << follower_.get() << " " << follower_->name() << " "
<< follower_ppm << " ppm). Leader (0x" << leader_.get() << " " << leader_->name() << " "
<< leader_ppm << " ppm). ";
if (last_adjustment_ppm_) {
os << "Adjustment " << *last_adjustment_ppm_ << " ppm.";
} else {
os << "No adjustment yet.";
}
return os.str();
}
} // namespace media_audio