blob: ed27cacc394609490b039c49777243e1425838e0 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef ANDROID_AUDIO_UTILS_TIMESTAMP_VERIFIER_H
#define ANDROID_AUDIO_UTILS_TIMESTAMP_VERIFIER_H
#include <audio_utils/clock.h>
#include <audio_utils/Statistics.h>
namespace android {
/** Verifies that a sequence of timestamps (a frame count, time pair)
* is consistent based on sample rate.
*
* F is the type of frame counts (for example int64_t)
* T is the type of time in Ns (for example int64_t ns)
*
* All methods except for toString() are safe to call from a SCHED_FIFO thread.
*/
template <typename F /* frame count */, typename T /* time units */>
class TimestampVerifier {
public:
explicit constexpr TimestampVerifier(
double alphaJitter = kDefaultAlphaJitter,
double alphaEstimator = kDefaultAlphaEstimator)
: mJitterMs{alphaJitter}
, mTimestampEstimator{alphaEstimator}
, mCorrectedJitterMs{alphaJitter}
{ }
// construct from static arrays.
template <size_t N>
constexpr TimestampVerifier(const F (&frames)[N], const T (&timeNs)[N], uint32_t sampleRate) {
for (size_t i = 0; i < N; ++i) {
add(frames[i], timeNs[i], sampleRate);
}
}
/** adds a timestamp, represented by a (frames, timeNs) pair and the
* sample rate to the TimestampVerifier.
*
* The frames and timeNs should be monotonic increasing
* (from the previous discontinuity, or the start of adding).
*
* A sample rate change triggers a discontinuity automatically.
*/
constexpr void add(F frames, T timeNs, uint32_t sampleRate) {
// We consider negative time as "not ready".
// TODO: do we need to consider an explicit epoch start time?
if (timeNs < 0) {
++mNotReady;
return;
}
// Reject timestamp if identical to last
if (mLastTimestamp.mFrames == frames && mLastTimestamp.mTimeNs == timeNs
&& mSampleRate == sampleRate) {
return;
}
if (mDiscontinuity || mSampleRate != sampleRate) {
// ALOGD("disc:%d frames:%lld timeNs:%lld",
// mDiscontinuity, (long long)frames, (long long)timeNs);
switch (mDiscontinuityMode) {
case DISCONTINUITY_MODE_CONTINUOUS:
break;
case DISCONTINUITY_MODE_ZERO:
// frame position reset to 0 on discontinuity - detect this.
if (mLastTimestamp.mFrames
> kDiscontinuityZeroStartThresholdMs * sampleRate / MILLIS_PER_SECOND
&& frames >= mLastTimestamp.mFrames) {
return;
}
break;
default:
assert(false); // never here.
break;
}
mDiscontinuity = false;
mFirstTimestamp = {frames, timeNs};
mLastTimestamp = mFirstTimestamp;
mLastCorrectedTimestamp = mFirstTimestamp;
mSampleRate = sampleRate;
} else {
assert(sampleRate != 0);
const FrameTime timestamp{frames, timeNs};
if (mCold && (timestamp.mTimeNs == mLastTimestamp.mTimeNs
|| computeRatio(timestamp, mLastTimestamp, sampleRate)
< kMinimumSpeedToStartVerification)) {
// cold is when the timestamp may take some time to start advancing at normal rate.
++mColds;
mFirstTimestamp = timestamp;
mLastCorrectedTimestamp = mFirstTimestamp;
// ALOGD("colds:%lld frames:%lld timeNs:%lld",
// (long long)mColds, (long long)frames, (long long)timeNs);
} else {
const double jitterMs = computeJitterMs(timestamp, mLastTimestamp, sampleRate);
mJitterMs.add(jitterMs);
// ALOGD("frames:%lld timeNs:%lld jitterMs:%lf",
// (long long)frames, (long long)timeNs, jitterMs);
// Handle timestamp estimation
if (mCold) {
mCold = false;
mTimestampEstimator.reset();
mTimestampEstimator.add(
{(double)mFirstTimestamp.mTimeNs * 1e-9, (double)mFirstTimestamp.mFrames});
mFirstCorrectedTimestamp = mFirstTimestamp;
mLastCorrectedTimestamp = mFirstCorrectedTimestamp;
}
mTimestampEstimator.add({(double)timeNs * 1e-9, (double)frames});
// Find the corrected timestamp, a posteriori estimate.
FrameTime correctedTimestamp = timestamp;
// The estimator is valid after 2 timestamps; we choose correlation
// of kEstimatorR2Lock to signal locked.
if (mTimestampEstimator.getN() > 2
&& mTimestampEstimator.getR2() >= kEstimatorR2Lock) {
#if 1
// We choose frame correction over time correction.
// TODO: analyze preference.
// figure out frames based on time.
const F newFrames = mTimestampEstimator.getYFromX((double)timeNs * 1e-9);
// prevent retrograde correction.
correctedTimestamp.mFrames = std::max(
newFrames, mLastCorrectedTimestamp.mFrames);
#else
// figure out time based on frames
const T newTimeNs = mTimestampEstimator.getXFromY((double)frames) * 1e9;
// prevent retrograde correction.
correctedTimestamp.mTimeNs = std::max(
newTimeNs, mLastCorrectedTimestamp.mTimeNs);
#endif
}
// Compute the jitter if the corrected timestamp is used.
const double correctedJitterMs = computeJitterMs(
correctedTimestamp, mLastCorrectedTimestamp, sampleRate);
mCorrectedJitterMs.add(correctedJitterMs);
mLastCorrectedTimestamp = correctedTimestamp;
}
mLastTimestamp = timestamp;
}
++mTimestamps;
}
// How a discontinuity affects frame position.
enum DiscontinuityMode : int32_t {
DISCONTINUITY_MODE_CONTINUOUS, // frame position is unaffected.
DISCONTINUITY_MODE_ZERO, // frame position resets to zero.
};
/** registers a discontinuity.
*
* The next timestamp added does not participate in any statistics with the last
* timestamp, but rather anchors following timestamp sequence verification.
*
* Consecutive discontinuities are treated as one for the purposes of counting.
*/
constexpr void discontinuity(DiscontinuityMode mode) {
assert(mode == DISCONTINUITY_MODE_CONTINUOUS || mode == DISCONTINUITY_MODE_ZERO);
// If there is a pending ZERO discontinuity, do not override with CONTINUOUS
if (mode == DISCONTINUITY_MODE_CONTINUOUS && mDiscontinuityMode == DISCONTINUITY_MODE_ZERO
&& mDiscontinuity) {
return;
}
if (mode != mDiscontinuityMode || !mDiscontinuity) {
// ALOGD("discontinuity");
mDiscontinuity = true;
mDiscontinuityMode = mode;
mCold = true;
++mDiscontinuities;
}
}
/** registers an error.
*
* The timestamp sequence is still assumed continuous after error. Use discontinuity()
* if it is not.
*/
constexpr void error() {
++mErrors;
}
constexpr DiscontinuityMode getDiscontinuityMode() const {
return mDiscontinuityMode;
}
/** returns a string with relevant statistics.
*
* Should not be called from a SCHED_FIFO thread since it uses std::string.
*/
std::string toString() const {
std::stringstream ss;
ss << "n=" << mTimestamps; // number of timestamps added with valid times.
ss << " disc=" << mDiscontinuities; // discontinuities encountered (dups ignored).
ss << " cold=" << mColds; // timestamps not progressing after discontinuity.
ss << " nRdy=" << mNotReady; // timestamps not ready (time negative).
ss << " err=" << mErrors; // errors encountered.
if (mSampleRate != 0) { // ratio of time-by-frames / time
ss << " rate=" << computeRatio( // (since last discontinuity).
mLastTimestamp, mFirstTimestamp, mSampleRate);
}
ss << " jitterMs(" << mJitterMs.toString() << ")"; // timestamp jitter statistics.
double a, b, r2; // sample rate is the slope b.
estimateSampleRate(a, b, r2);
// r2 is correlation coefficient (where 1 is best and 0 is worst),
// so for better printed resolution, we print from 1 - r2.
ss << " localSR(" << b << ", " << (1. - r2) << ")";
ss << " correctedJitterMs(" << mCorrectedJitterMs.toString() << ")";
return ss.str();
}
// general counters
constexpr int64_t getN() const { return mTimestamps; }
constexpr int64_t getDiscontinuities() const { return mDiscontinuities; }
constexpr int64_t getNotReady() const { return mNotReady; }
constexpr int64_t getColds() const { return mColds; }
constexpr int64_t getErrors() const { return mErrors; }
constexpr const audio_utils::Statistics<double> & getJitterMs() const {
return mJitterMs;
}
// estimate local sample rate (dframes / dtime) which is the slope b from:
// y = a + bx
constexpr void estimateSampleRate(double &a, double &b, double &r2) const {
mTimestampEstimator.computeYLine(a, b, r2);
}
// timestamp anchor info
using FrameTime = struct { F mFrames; T mTimeNs; }; // a "constexpr" pair
constexpr FrameTime getFirstTimestamp() const { return mFirstTimestamp; }
constexpr FrameTime getLastTimestamp() const { return mLastTimestamp; }
constexpr uint32_t getSampleRate() const { return mSampleRate; }
constexpr FrameTime getLastCorrectedTimestamp() const { return mLastCorrectedTimestamp; }
// Inf+-, NaN is possible only if sampleRate is 0 (should not happen)
static constexpr double computeJitterMs(
const FrameTime &current, const FrameTime &last, uint32_t sampleRate) {
const auto diff = sub(current, last);
const double frameDifferenceNs = diff.first * 1e9 / sampleRate;
const double jitterNs = diff.second - frameDifferenceNs; // actual - expected
return jitterNs * 1e-6;
}
private:
// our statistics have exponentially weighted history.
// the defaults are here.
static constexpr double kDefaultAlphaJitter = 0.999;
static constexpr double kDefaultAlphaEstimator = 0.99;
static constexpr double kEstimatorR2Lock = 0.95;
// general counters
int64_t mTimestamps = 0;
int64_t mDiscontinuities = 0;
int64_t mNotReady = 0;
int64_t mColds = 0;
int64_t mErrors = 0;
audio_utils::Statistics<double> mJitterMs{kDefaultAlphaJitter};
// timestamp anchor info
bool mDiscontinuity = true;
bool mCold = true;
FrameTime mFirstTimestamp{};
FrameTime mLastTimestamp{};
uint32_t mSampleRate = 0;
// timestamp estimation and correction
audio_utils::LinearLeastSquaresFit<double> mTimestampEstimator{kDefaultAlphaEstimator};
FrameTime mFirstCorrectedTimestamp{};
FrameTime mLastCorrectedTimestamp{};
audio_utils::Statistics<double> mCorrectedJitterMs{kDefaultAlphaJitter};
// configuration
DiscontinuityMode mDiscontinuityMode = DISCONTINUITY_MODE_CONTINUOUS;
static constexpr double kMinimumSpeedToStartVerification = 0.1;
// Number of ms so small that initial jitter is OK for DISCONTINUITY_MODE_ZERO.
static constexpr int64_t kDiscontinuityZeroStartThresholdMs = 5;
// sub returns the signed type of the difference between left and right.
// This is only important if F or T are unsigned int types.
__attribute__((no_sanitize("integer")))
static constexpr auto sub(const FrameTime &left, const FrameTime &right) {
return std::make_pair<
typename std::make_signed<F>::type, typename std::make_signed<T>::type>(
left.mFrames - right.mFrames, left.mTimeNs - right.mTimeNs);
}
// Inf+-, Nan possible depending on differences between current and last.
static constexpr double computeRatio(
const FrameTime &current, const FrameTime &last, uint32_t sampleRate) {
const auto diff = sub(current, last);
const double frameDifferenceNs = diff.first * 1e9 / sampleRate;
return frameDifferenceNs / diff.second;
}
};
} // namespace android
#endif // !ANDROID_AUDIO_UTILS_TIMESTAMP_VERIFIER_H