summaryrefslogtreecommitdiffstats
path: root/dom/media/gtest/TestAudioCallbackDriver.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/media/gtest/TestAudioCallbackDriver.cpp
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/gtest/TestAudioCallbackDriver.cpp')
-rw-r--r--dom/media/gtest/TestAudioCallbackDriver.cpp477
1 files changed, 477 insertions, 0 deletions
diff --git a/dom/media/gtest/TestAudioCallbackDriver.cpp b/dom/media/gtest/TestAudioCallbackDriver.cpp
new file mode 100644
index 0000000000..050395fa44
--- /dev/null
+++ b/dom/media/gtest/TestAudioCallbackDriver.cpp
@@ -0,0 +1,477 @@
+/* -*- 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/. */
+
+#include <tuple>
+
+#include "CubebUtils.h"
+#include "GraphDriver.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+
+#include "MediaTrackGraphImpl.h"
+#include "mozilla/gtest/WaitFor.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+#include "MockCubeb.h"
+
+using namespace mozilla;
+using IterationResult = GraphInterface::IterationResult;
+using ::testing::_;
+using ::testing::AnyNumber;
+using ::testing::NiceMock;
+
+class MockGraphInterface : public GraphInterface {
+ NS_DECL_THREADSAFE_ISUPPORTS
+ explicit MockGraphInterface(TrackRate aSampleRate)
+ : mSampleRate(aSampleRate) {}
+ MOCK_METHOD0(NotifyInputStopped, void());
+ MOCK_METHOD5(NotifyInputData, void(const AudioDataValue*, size_t, TrackRate,
+ uint32_t, uint32_t));
+ MOCK_METHOD0(DeviceChanged, void());
+#ifdef DEBUG
+ MOCK_CONST_METHOD1(InDriverIteration, bool(const GraphDriver*));
+#endif
+ /* OneIteration cannot be mocked because IterationResult is non-memmovable and
+ * cannot be passed as a parameter, which GMock does internally. */
+ IterationResult OneIteration(GraphTime aStateComputedTime, GraphTime,
+ MixerCallbackReceiver* aMixerReceiver) {
+ GraphDriver* driver = mCurrentDriver;
+ if (aMixerReceiver) {
+ mMixer.StartMixing();
+ mMixer.Mix(nullptr, driver->AsAudioCallbackDriver()->OutputChannelCount(),
+ aStateComputedTime - mStateComputedTime, mSampleRate);
+ aMixerReceiver->MixerCallback(mMixer.MixedChunk(), mSampleRate);
+ }
+ if (aStateComputedTime != mStateComputedTime) {
+ mFramesIteratedEvent.Notify(aStateComputedTime - mStateComputedTime);
+ ++mIterationCount;
+ }
+ mStateComputedTime = aStateComputedTime;
+ if (!mKeepProcessing) {
+ return IterationResult::CreateStop(
+ NS_NewRunnableFunction(__func__, [] {}));
+ }
+ if (auto guard = mNextDriver.Lock(); guard->isSome()) {
+ auto tup = guard->extract();
+ const auto& [driver, switchedRunnable] = tup;
+ return IterationResult::CreateSwitchDriver(driver, switchedRunnable);
+ }
+ if (mEnsureNextIteration) {
+ driver->EnsureNextIteration();
+ }
+ return IterationResult::CreateStillProcessing();
+ }
+ void SetEnsureNextIteration(bool aEnsure) { mEnsureNextIteration = aEnsure; }
+
+ size_t IterationCount() const { return mIterationCount; }
+
+ GraphTime StateComputedTime() const { return mStateComputedTime; }
+ void SetCurrentDriver(GraphDriver* aDriver) { mCurrentDriver = aDriver; }
+
+ void StopIterating() { mKeepProcessing = false; }
+
+ void SwitchTo(RefPtr<GraphDriver> aDriver,
+ RefPtr<Runnable> aSwitchedRunnable = NS_NewRunnableFunction(
+ "DefaultNoopSwitchedRunnable", [] {})) {
+ auto guard = mNextDriver.Lock();
+ MOZ_ASSERT(guard->isNothing());
+ *guard =
+ Some(std::make_tuple(std::move(aDriver), std::move(aSwitchedRunnable)));
+ }
+ const TrackRate mSampleRate;
+
+ MediaEventSource<uint32_t>& FramesIteratedEvent() {
+ return mFramesIteratedEvent;
+ }
+
+ protected:
+ Atomic<size_t> mIterationCount{0};
+ Atomic<GraphTime> mStateComputedTime{0};
+ Atomic<GraphDriver*> mCurrentDriver{nullptr};
+ Atomic<bool> mEnsureNextIteration{false};
+ Atomic<bool> mKeepProcessing{true};
+ DataMutex<Maybe<std::tuple<RefPtr<GraphDriver>, RefPtr<Runnable>>>>
+ mNextDriver{"MockGraphInterface::mNextDriver"};
+ RefPtr<Runnable> mNextDriverSwitchedRunnable;
+ MediaEventProducer<uint32_t> mFramesIteratedEvent;
+ AudioMixer mMixer;
+ virtual ~MockGraphInterface() = default;
+};
+
+NS_IMPL_ISUPPORTS0(MockGraphInterface)
+
+TEST(TestAudioCallbackDriver, StartStop)
+MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
+ const TrackRate rate = 44100;
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ RefPtr<AudioCallbackDriver> driver;
+ auto graph = MakeRefPtr<NiceMock<MockGraphInterface>>(rate);
+ EXPECT_CALL(*graph, NotifyInputStopped).Times(0);
+
+ driver = MakeRefPtr<AudioCallbackDriver>(graph, nullptr, rate, 2, 0, nullptr,
+ nullptr, AudioInputType::Unknown);
+ EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running";
+ EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started";
+
+ graph->SetCurrentDriver(driver);
+ driver->Start();
+ // Allow some time to "play" audio.
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+ EXPECT_TRUE(driver->ThreadRunning()) << "Verify thread is running";
+ EXPECT_TRUE(driver->IsStarted()) << "Verify thread is started";
+
+ // This will block untill all events have been executed.
+ MOZ_KnownLive(driver)->Shutdown();
+ EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running";
+ EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started";
+}
+
+void TestSlowStart(const TrackRate aRate) MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
+ std::cerr << "TestSlowStart with rate " << aRate << std::endl;
+
+ MockCubeb* cubeb = new MockCubeb();
+ cubeb->SetStreamStartFreezeEnabled(true);
+ auto unforcer = WaitFor(cubeb->ForceAudioThread()).unwrap();
+ Unused << unforcer;
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ RefPtr<AudioCallbackDriver> driver;
+ auto graph = MakeRefPtr<NiceMock<MockGraphInterface>>(aRate);
+ EXPECT_CALL(*graph, NotifyInputStopped).Times(0);
+
+ nsIThread* mainThread = NS_GetCurrentThread();
+ Maybe<int64_t> audioStart;
+ Maybe<uint32_t> alreadyBuffered;
+ int64_t inputFrameCount = 0;
+ int64_t processedFrameCount = -1;
+ ON_CALL(*graph, NotifyInputData)
+ .WillByDefault([&](const AudioDataValue*, size_t aFrames, TrackRate,
+ uint32_t, uint32_t aAlreadyBuffered) {
+ if (!audioStart) {
+ audioStart = Some(graph->StateComputedTime());
+ alreadyBuffered = Some(aAlreadyBuffered);
+ mainThread->Dispatch(NS_NewRunnableFunction(__func__, [&] {
+ // Start processedFrameCount now, ignoring frames processed while
+ // waiting for the fallback driver to stop.
+ processedFrameCount = 0;
+ }));
+ }
+ EXPECT_NEAR(inputFrameCount,
+ static_cast<int64_t>(graph->StateComputedTime() -
+ *audioStart + *alreadyBuffered),
+ WEBAUDIO_BLOCK_SIZE)
+ << "Input should be behind state time, due to the delayed start. "
+ "stateComputedTime="
+ << graph->StateComputedTime() << ", audioStartTime=" << *audioStart
+ << ", alreadyBuffered=" << *alreadyBuffered;
+ inputFrameCount += aFrames;
+ });
+
+ driver = MakeRefPtr<AudioCallbackDriver>(graph, nullptr, aRate, 2, 2, nullptr,
+ (void*)1, AudioInputType::Voice);
+ EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running";
+ EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started";
+
+ graph->SetCurrentDriver(driver);
+ graph->SetEnsureNextIteration(true);
+
+ driver->Start();
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ cubeb->SetStreamStartFreezeEnabled(false);
+
+ const size_t fallbackIterations = 3;
+ WaitUntil(graph->FramesIteratedEvent(), [&](uint32_t aFrames) {
+ const GraphTime tenMillis = aRate / 100;
+ // An iteration is always rounded upwards to the next full block.
+ const GraphTime tenMillisIteration =
+ MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(tenMillis);
+ // The iteration may be smaller because up to an extra block may have been
+ // processed and buffered.
+ const GraphTime tenMillisMinIteration =
+ tenMillisIteration - WEBAUDIO_BLOCK_SIZE;
+ // An iteration must be at least one audio block.
+ const GraphTime minIteration =
+ std::max<GraphTime>(WEBAUDIO_BLOCK_SIZE, tenMillisMinIteration);
+ EXPECT_GE(aFrames, minIteration)
+ << "Fallback driver iteration >= 10ms, modulo an audio block";
+ EXPECT_LT(aFrames, static_cast<size_t>(aRate))
+ << "Fallback driver iteration <1s (sanity)";
+ return graph->IterationCount() >= fallbackIterations;
+ });
+
+ MediaEventListener processedListener =
+ stream->FramesProcessedEvent().Connect(mainThread, [&](uint32_t aFrames) {
+ if (processedFrameCount >= 0) {
+ processedFrameCount += aFrames;
+ }
+ });
+ stream->Thaw();
+
+ SpinEventLoopUntil(
+ "processed at least 100ms of audio data from stream callback"_ns,
+ [&] { return processedFrameCount >= aRate / 10; });
+
+ // This will block until all events have been queued.
+ MOZ_KnownLive(driver)->Shutdown();
+ // Process processListener events.
+ NS_ProcessPendingEvents(mainThread);
+ processedListener.Disconnect();
+
+ EXPECT_EQ(inputFrameCount, processedFrameCount);
+ EXPECT_NEAR(graph->StateComputedTime() - *audioStart,
+ inputFrameCount + *alreadyBuffered, WEBAUDIO_BLOCK_SIZE)
+ << "Graph progresses while audio driver runs. stateComputedTime="
+ << graph->StateComputedTime() << ", inputFrameCount=" << inputFrameCount;
+}
+
+TEST(TestAudioCallbackDriver, SlowStart)
+MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
+ TestSlowStart(1000); // 10ms = 10 <<< 128 samples
+ TestSlowStart(8000); // 10ms = 80 < 128 samples
+ TestSlowStart(44100); // 10ms = 441 > 128 samples
+}
+
+#ifdef DEBUG
+template <typename T>
+class MOZ_STACK_CLASS AutoSetter {
+ std::atomic<T>& mVal;
+ T mNew;
+ T mOld;
+
+ public:
+ explicit AutoSetter(std::atomic<T>& aVal, T aNew)
+ : mVal(aVal), mNew(aNew), mOld(mVal.exchange(aNew)) {}
+ ~AutoSetter() {
+ DebugOnly<T> oldNew = mVal.exchange(mOld);
+ MOZ_ASSERT(oldNew == mNew);
+ }
+};
+#endif
+
+TEST(TestAudioCallbackDriver, SlowDeviceChange)
+MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ constexpr TrackRate rate = 48000;
+ MockCubeb* cubeb = new MockCubeb(MockCubeb::RunningMode::Manual);
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ auto graph = MakeRefPtr<MockGraphInterface>(rate);
+ auto driver = MakeRefPtr<AudioCallbackDriver>(
+ graph, nullptr, rate, 2, 1, nullptr, (void*)1, AudioInputType::Voice);
+ EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running";
+ EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started";
+
+#ifdef DEBUG
+ std::atomic<std::thread::id> threadInDriverIteration((std::thread::id()));
+ EXPECT_CALL(*graph, InDriverIteration(driver.get())).WillRepeatedly([&] {
+ return std::this_thread::get_id() == threadInDriverIteration;
+ });
+#endif
+ constexpr size_t ignoredFrameCount = 1337;
+ EXPECT_CALL(*graph, NotifyInputData(_, 0, rate, 1, _)).Times(AnyNumber());
+ EXPECT_CALL(*graph, NotifyInputData(_, ignoredFrameCount, _, _, _)).Times(0);
+ EXPECT_CALL(*graph, DeviceChanged);
+
+ graph->SetCurrentDriver(driver);
+ graph->SetEnsureNextIteration(true);
+ // This starts the fallback driver.
+ driver->Start();
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+
+ // Wait for the audio driver to have started the stream before running data
+ // callbacks. driver->Start() does a dispatch to the cubeb operation thread
+ // and starts the stream there.
+ nsCOMPtr<nsIEventTarget> cubebOpThread = CUBEB_TASK_THREAD;
+ MOZ_ALWAYS_SUCCEEDS(SyncRunnable::DispatchToThread(
+ cubebOpThread, NS_NewRunnableFunction(__func__, [] {})));
+
+ // This makes the fallback driver stop on its next callback.
+ EXPECT_EQ(stream->ManualDataCallback(0),
+ MockCubebStream::KeepProcessing::Yes);
+ {
+#ifdef DEBUG
+ AutoSetter as(threadInDriverIteration, std::this_thread::get_id());
+#endif
+ while (driver->OnFallback()) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+ }
+
+ const TimeStamp wallClockStart = TimeStamp::Now();
+ const GraphTime graphClockStart = graph->StateComputedTime();
+ const size_t iterationCountStart = graph->IterationCount();
+
+ // Flag that the stream should force a devicechange event.
+ stream->NotifyDeviceChangedNow();
+
+ // The audio driver should now have switched on the fallback driver again.
+ {
+#ifdef DEBUG
+ AutoSetter as(threadInDriverIteration, std::this_thread::get_id());
+#endif
+ EXPECT_TRUE(driver->OnFallback());
+ }
+
+ // Make sure that the audio driver can handle (and ignore) data callbacks for
+ // a little while after the devicechange callback. Cubeb does not provide
+ // ordering guarantees here.
+ auto start = TimeStamp::Now();
+ while (start + TimeDuration::FromMilliseconds(5) > TimeStamp::Now()) {
+ EXPECT_EQ(stream->ManualDataCallback(ignoredFrameCount),
+ MockCubebStream::KeepProcessing::Yes);
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+
+ // Let the fallback driver start and spin for one second.
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+
+ // Tell the fallback driver to hand over to the audio driver which has
+ // finished changing devices.
+ EXPECT_EQ(stream->ManualDataCallback(0),
+ MockCubebStream::KeepProcessing::Yes);
+
+ // Wait for the fallback to stop.
+ {
+#ifdef DEBUG
+ AutoSetter as(threadInDriverIteration, std::this_thread::get_id());
+#endif
+ while (driver->OnFallback()) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+ }
+
+ TimeStamp wallClockEnd = TimeStamp::Now();
+ GraphTime graphClockEnd = graph->StateComputedTime();
+ size_t iterationCountEnd = graph->IterationCount();
+
+ auto wallClockDuration =
+ media::TimeUnit::FromTimeDuration(wallClockEnd - wallClockStart);
+ auto graphClockDuration =
+ media::TimeUnit(CheckedInt64(graphClockEnd) - graphClockStart, rate);
+
+ // Check that the time while we switched devices was accounted for by the
+ // fallback driver.
+ EXPECT_NEAR(
+ wallClockDuration.ToSeconds(), graphClockDuration.ToSeconds(),
+#ifdef XP_MACOSX
+ // SystemClockDriver on macOS in CI is underrunning, i.e. the driver
+ // thread when waiting for the next iteration waits too long. Therefore
+ // the graph clock is unable to keep up with wall clock.
+ wallClockDuration.ToSeconds() * 0.8
+#else
+ 0.1
+#endif
+ );
+ // Check that each fallback driver was of reasonable cadence. It's a thread
+ // that tries to run a task every 10ms. Check that the average callback
+ // interval i falls in 8ms ≤ i ≤ 40ms.
+ auto fallbackCadence =
+ graphClockDuration /
+ static_cast<int64_t>(iterationCountEnd - iterationCountStart);
+ EXPECT_LE(8, fallbackCadence.ToMilliseconds());
+ EXPECT_LE(fallbackCadence.ToMilliseconds(), 40.0);
+
+ // This will block until all events have been queued.
+ MOZ_KnownLive(driver)->Shutdown();
+ // Drain the event queue.
+ NS_ProcessPendingEvents(nullptr);
+}
+
+TEST(TestAudioCallbackDriver, DeviceChangeAfterStop)
+MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ constexpr TrackRate rate = 48000;
+ MockCubeb* cubeb = new MockCubeb(MockCubeb::RunningMode::Manual);
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ auto graph = MakeRefPtr<MockGraphInterface>(rate);
+ auto driver = MakeRefPtr<AudioCallbackDriver>(
+ graph, nullptr, rate, 2, 1, nullptr, (void*)1, AudioInputType::Voice);
+ EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running";
+ EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started";
+
+ auto newDriver = MakeRefPtr<AudioCallbackDriver>(
+ graph, nullptr, rate, 2, 1, nullptr, (void*)1, AudioInputType::Voice);
+ EXPECT_FALSE(newDriver->ThreadRunning()) << "Verify thread is not running";
+ EXPECT_FALSE(newDriver->IsStarted()) << "Verify thread is not started";
+
+#ifdef DEBUG
+ std::atomic<std::thread::id> threadInDriverIteration(
+ (std::this_thread::get_id()));
+ EXPECT_CALL(*graph, InDriverIteration(_)).WillRepeatedly([&] {
+ return std::this_thread::get_id() == threadInDriverIteration;
+ });
+#endif
+ EXPECT_CALL(*graph, NotifyInputData(_, 0, rate, 1, _)).Times(AnyNumber());
+ EXPECT_CALL(*graph, DeviceChanged);
+
+ graph->SetCurrentDriver(driver);
+ graph->SetEnsureNextIteration(true);
+ // This starts the fallback driver.
+ driver->Start();
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+
+ // Wait for the audio driver to have started or the DeviceChanged event will
+ // be ignored. driver->Start() does a dispatch to the cubeb operation thread
+ // and starts the stream there.
+ nsCOMPtr<nsIEventTarget> cubebOpThread = CUBEB_TASK_THREAD;
+ MOZ_ALWAYS_SUCCEEDS(SyncRunnable::DispatchToThread(
+ cubebOpThread, NS_NewRunnableFunction(__func__, [] {})));
+
+#ifdef DEBUG
+ AutoSetter as(threadInDriverIteration, std::this_thread::get_id());
+#endif
+
+ // This marks the audio driver as running.
+ EXPECT_EQ(stream->ManualDataCallback(0),
+ MockCubebStream::KeepProcessing::Yes);
+
+ // If a fallback driver callback happens between the audio callback above, and
+ // the SwitchTo below, the audio driver will perform the switch instead of the
+ // fallback since the fallback will have stopped. This test may therefore
+ // intermittently take different code paths.
+
+ // Stop the fallback driver by switching audio driver in the graph.
+ {
+ Monitor mon(__func__);
+ MonitorAutoLock lock(mon);
+ bool switched = false;
+ graph->SwitchTo(newDriver, NS_NewRunnableFunction(__func__, [&] {
+ MonitorAutoLock lock(mon);
+ switched = true;
+ lock.Notify();
+ }));
+ while (!switched) {
+ lock.Wait();
+ }
+ }
+
+ {
+#ifdef DEBUG
+ AutoSetter as(threadInDriverIteration, std::thread::id());
+#endif
+ // After stopping the fallback driver, but before newDriver has stopped the
+ // old audio driver, fire a DeviceChanged event to ensure it is handled
+ // properly.
+ AudioCallbackDriver::DeviceChangedCallback_s(driver);
+ }
+
+ graph->StopIterating();
+ newDriver->EnsureNextIteration();
+ while (newDriver->OnFallback()) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+
+ // This will block until all events have been queued.
+ MOZ_KnownLive(driver)->Shutdown();
+ MOZ_KnownLive(newDriver)->Shutdown();
+ // Drain the event queue.
+ NS_ProcessPendingEvents(nullptr);
+}