| /* |
| * 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 ¤t, 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 ¤t, 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 |