/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef MOZILLA_AUDIO_DRIFT_CORRECTION_H_ #define MOZILLA_AUDIO_DRIFT_CORRECTION_H_ #include "DynamicResampler.h" #include "mozilla/Preferences.h" namespace mozilla { extern LazyLogModule gMediaTrackGraphLog; /** * ClockDrift calculates the diverge of the source clock from the nominal * (provided) rate compared to the target clock, which is considered the master * clock. In the case of different sampling rates, it is assumed that resampling * will take place so the returned correction is estimated after the resampling. * That means that resampling is taken into account in the calculations but it * does appear in the correction. The correction must be applied to the top of * the resampling. * * It works by measuring the incoming, the outgoing frames, and the amount of * buffered data and estimates the correction needed. The correction logic has * been created with two things in mind. First, not to run out of frames because * that means the audio will glitch. Second, not to change the correction very * often because this will result in a change in the resampling ratio. The * resampler recreates its internal memory when the ratio changes which has a * performance impact. * * The pref `media.clock drift.buffering` can be used to configure the desired * internal buffering. Right now it is at 50ms. But it can be increased if there * are audio quality problems. */ class ClockDrift final { public: /** * Provide the nominal source and the target sample rate. */ ClockDrift(uint32_t aSourceRate, uint32_t aTargetRate, uint32_t aDesiredBuffering) : mSourceRate(aSourceRate), mTargetRate(aTargetRate), mDesiredBuffering(aDesiredBuffering) {} /** * The correction in the form of a ratio. A correction of 0.98 means that the * target is 2% slower compared to the source or 1.03 which means that the * target is 3% faster than the source. */ float GetCorrection() { return mCorrection; } /** * Update the available source frames, target frames, and the current * buffer, in every iteration. If the conditions are met a new correction is * calculated. A new correction is calculated in the following cases: * 1. Every mAdjustmentIntervalMs milliseconds (1000ms). * 2. Every time we run low on buffered frames (less than 20ms). * In addition to that, the correction is clamped to 10% to avoid sound * distortion so the result will be in [0.9, 1.1]. */ void UpdateClock(uint32_t aSourceFrames, uint32_t aTargetFrames, uint32_t aBufferedFrames, uint32_t aRemainingFrames) { if (mSourceClock >= mSourceRate / 10 || mTargetClock >= mTargetRate / 10) { // Only update the correction if 100ms has passed since last update. if (aBufferedFrames < mDesiredBuffering * 4 / 10 /*40%*/ || aRemainingFrames < mDesiredBuffering * 4 / 10 /*40%*/) { // We are getting close to the lower or upper bound of the internal // buffer. Steer clear. CalculateCorrection(0.9, aBufferedFrames, aRemainingFrames); } else if ((mTargetClock * 1000 / mTargetRate) >= mAdjustmentIntervalMs || (mSourceClock * 1000 / mSourceRate) >= mAdjustmentIntervalMs) { // The adjustment interval has passed on one side. Recalculate. CalculateCorrection(0.6, aBufferedFrames, aRemainingFrames); } } mTargetClock += aTargetFrames; mSourceClock += aSourceFrames; } private: /** * aCalculationWeight is a percentage [0, 1] with which the calculated * correction will be weighted. The existing correction will be weighted with * 1 - aCalculationWeight. This gives some inertia to the speed at which the * correction changes, for smoother changes. */ void CalculateCorrection(float aCalculationWeight, uint32_t aBufferedFrames, uint32_t aRemainingFrames) { // We want to maintain the desired buffer uint32_t bufferedFramesDiff = aBufferedFrames - mDesiredBuffering; uint32_t resampledSourceClock = std::max(1u, mSourceClock + bufferedFramesDiff); if (mTargetRate != mSourceRate) { resampledSourceClock *= static_cast(mTargetRate) / mSourceRate; } MOZ_LOG(gMediaTrackGraphLog, LogLevel::Verbose, ("ClockDrift %p Calculated correction %.3f (with weight: %.1f -> " "%.3f) (buffer: %u, desired: %u, remaining: %u)", this, static_cast(mTargetClock) / resampledSourceClock, aCalculationWeight, (1 - aCalculationWeight) * mCorrection + aCalculationWeight * mTargetClock / resampledSourceClock, aBufferedFrames, mDesiredBuffering, aRemainingFrames)); mCorrection = (1 - aCalculationWeight) * mCorrection + aCalculationWeight * mTargetClock / resampledSourceClock; // Clamp to range [0.9, 1.1] to avoid distortion mCorrection = std::min(std::max(mCorrection, 0.9f), 1.1f); // Reset the counters to prepare for the next period. mTargetClock = 0; mSourceClock = 0; } public: const uint32_t mSourceRate; const uint32_t mTargetRate; const uint32_t mAdjustmentIntervalMs = 1000; const uint32_t mDesiredBuffering; private: float mCorrection = 1.0; uint32_t mSourceClock = 0; uint32_t mTargetClock = 0; }; /** * Correct the drift between two independent clocks, the source, and the target * clock. The target clock is the master clock so the correction syncs the drift * of the source clock to the target. The nominal sampling rates of source and * target must be provided. If the source and the target operate in different * sample rate the drift correction will be performed on the top of resampling * from the source rate to the target rate. * * It works with AudioSegment in order to be able to be used from the * MediaTrackGraph/MediaTrack. The audio buffers are pre-allocated so there is * no new allocation takes place during operation. The preallocation capacity is * 100ms for input and 100ms for output. The class consists of ClockDrift and * AudioResampler check there for more details. * * The class is not thread-safe. The construction can happen in any thread but * the member method must be used in a single thread that can be different than * the construction thread. Appropriate for being used in the high priority * audio thread. */ class AudioDriftCorrection final { public: AudioDriftCorrection(uint32_t aSourceRate, uint32_t aTargetRate) : mDesiredBuffering( std::max(5, Preferences::GetInt("media.clockdrift.buffering", 50)) * aSourceRate / 1000), mTargetRate(aTargetRate), mClockDrift(aSourceRate, aTargetRate, mDesiredBuffering), mResampler(aSourceRate, aTargetRate, mDesiredBuffering) {} /** * The source audio frames and request the number of target audio frames must * be provided. The duration of the source and the output is considered as the * source clock and the target clock. The input is buffered internally so some * latency exists. The returned AudioSegment must be cleaned up because the * internal buffer will be reused after 100ms. If the drift correction (and * possible resampling) is not possible due to lack of input data an empty * AudioSegment will be returned. Not thread-safe. */ AudioSegment RequestFrames(const AudioSegment& aInput, uint32_t aOutputFrames) { // Very important to go first since the Dynamic will get the sample format // from the chunk. if (aInput.GetDuration()) { // Always go through the resampler because the clock might shift later. mResampler.AppendInput(aInput); } mClockDrift.UpdateClock(aInput.GetDuration(), aOutputFrames, mResampler.InputReadableFrames(), mResampler.InputWritableFrames()); TrackRate receivingRate = mTargetRate * mClockDrift.GetCorrection(); // Update resampler's rate if there is a new correction. mResampler.UpdateOutRate(receivingRate); // If it does not have enough frames the result will be an empty segment. AudioSegment output = mResampler.Resample(aOutputFrames); if (output.IsEmpty()) { NS_WARNING("Got nothing from the resampler"); output.AppendNullData(aOutputFrames); } return output; } // Only accessible from the same thread that is driving RequestFrames(). uint32_t CurrentBuffering() const { return mResampler.InputReadableFrames(); } const uint32_t mDesiredBuffering; const uint32_t mTargetRate; private: ClockDrift mClockDrift; AudioResampler mResampler; }; }; // namespace mozilla #endif /* MOZILLA_AUDIO_DRIFT_CORRECTION_H_ */