/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* 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 https://mozilla.org/MPL/2.0/. */ #include "AudioInputSource.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include "MockCubeb.h" #include "mozilla/Result.h" #include "mozilla/gtest/WaitFor.h" #include "nsContentUtils.h" using namespace mozilla; using testing::ContainerEq; // Short-hand for DispatchToCurrentThread with a function. #define DispatchFunction(f) \ NS_DispatchToCurrentThread(NS_NewRunnableFunction(__func__, f)) // Short-hand for draining the current threads event queue, i.e. processing // those runnables dispatched per above. #define ProcessEventQueue() \ while (NS_ProcessNextEvent(nullptr, false)) { \ } class MockEventListener : public AudioInputSource::EventListener { public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MockEventListener, override); MOCK_METHOD1(AudioDeviceChanged, void(AudioInputSource::Id)); MOCK_METHOD2(AudioStateCallback, void(AudioInputSource::Id, AudioInputSource::EventListener::State)); private: ~MockEventListener() = default; }; TEST(TestAudioInputSource, StartAndStop) { MockCubeb* cubeb = new MockCubeb(); CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); const AudioInputSource::Id sourceId = 1; const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; const uint32_t channels = 2; const PrincipalHandle testPrincipal = MakePrincipalHandle(nsContentUtils::GetSystemPrincipal()); const TrackRate sourceRate = 44100; const TrackRate targetRate = 48000; auto listener = MakeRefPtr(); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Started)) .Times(2); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Stopped)) .Times(4); RefPtr ais = MakeRefPtr( std::move(listener), sourceId, deviceId, channels, true, testPrincipal, sourceRate, targetRate); ASSERT_TRUE(ais); // Make sure start and stop works. { DispatchFunction([&] { ais->Init(); ais->Start(); }); RefPtr stream = WaitFor(cubeb->StreamInitEvent()); EXPECT_TRUE(stream->mHasInput); EXPECT_FALSE(stream->mHasOutput); EXPECT_EQ(stream->GetInputDeviceID(), deviceId); EXPECT_EQ(stream->InputChannels(), channels); EXPECT_EQ(stream->SampleRate(), static_cast(sourceRate)); Unused << WaitFor(stream->FramesProcessedEvent()); DispatchFunction([&] { ais->Stop(); }); Unused << WaitFor(cubeb->StreamDestroyEvent()); } // Make sure restart is ok. { DispatchFunction([&] { ais->Init(); ais->Start(); }); RefPtr stream = WaitFor(cubeb->StreamInitEvent()); EXPECT_TRUE(stream->mHasInput); EXPECT_FALSE(stream->mHasOutput); EXPECT_EQ(stream->GetInputDeviceID(), deviceId); EXPECT_EQ(stream->InputChannels(), channels); EXPECT_EQ(stream->SampleRate(), static_cast(sourceRate)); Unused << WaitFor(stream->FramesProcessedEvent()); DispatchFunction([&] { ais->Stop(); }); Unused << WaitFor(cubeb->StreamDestroyEvent()); } ais = nullptr; // Drop the SharedThreadPool here. } TEST(TestAudioInputSource, DataOutputBeforeStartAndAfterStop) { MockCubeb* cubeb = new MockCubeb(); CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); const AudioInputSource::Id sourceId = 1; const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; const uint32_t channels = 2; const PrincipalHandle testPrincipal = MakePrincipalHandle(nsContentUtils::GetSystemPrincipal()); const TrackRate sourceRate = 44100; const TrackRate targetRate = 48000; const TrackTime requestFrames = 2 * WEBAUDIO_BLOCK_SIZE; auto listener = MakeRefPtr(); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Started)); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Stopped)) .Times(2); RefPtr ais = MakeRefPtr( std::move(listener), sourceId, deviceId, channels, true, testPrincipal, sourceRate, targetRate); ASSERT_TRUE(ais); // It's ok to call GetAudioSegment before starting { AudioSegment data = ais->GetAudioSegment(requestFrames, AudioInputSource::Consumer::Same); EXPECT_EQ(data.GetDuration(), requestFrames); EXPECT_TRUE(data.IsNull()); } DispatchFunction([&] { ais->Init(); ais->Start(); }); RefPtr stream = WaitFor(cubeb->StreamInitEvent()); EXPECT_TRUE(stream->mHasInput); EXPECT_FALSE(stream->mHasOutput); EXPECT_EQ(stream->InputChannels(), channels); stream->SetInputRecordingEnabled(true); Unused << WaitFor(stream->FramesProcessedEvent()); DispatchFunction([&] { ais->Stop(); }); Unused << WaitFor(cubeb->StreamDestroyEvent()); // Check the data output { nsTArray record = stream->TakeRecordedInput(); size_t frames = record.Length() / channels; AudioSegment deinterleaved; deinterleaved.AppendFromInterleavedBuffer(record.Elements(), frames, channels, testPrincipal); AudioDriftCorrection driftCorrector(sourceRate, targetRate, testPrincipal); AudioSegment expectedSegment = driftCorrector.RequestFrames( deinterleaved, static_cast(requestFrames)); CopyableTArray expected; size_t expectedSamples = expectedSegment.WriteToInterleavedBuffer(expected, channels); AudioSegment actualSegment = ais->GetAudioSegment(requestFrames, AudioInputSource::Consumer::Same); EXPECT_EQ(actualSegment.GetDuration(), requestFrames); CopyableTArray actual; size_t actualSamples = actualSegment.WriteToInterleavedBuffer(actual, channels); EXPECT_EQ(actualSamples, expectedSamples); EXPECT_EQ(actualSamples / channels, static_cast(requestFrames)); EXPECT_THAT(actual, ContainerEq(expected)); } ais = nullptr; // Drop the SharedThreadPool here. } TEST(TestAudioInputSource, ErrorCallback) { MockCubeb* cubeb = new MockCubeb(); CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); const AudioInputSource::Id sourceId = 1; const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; const uint32_t channels = 2; const PrincipalHandle testPrincipal = MakePrincipalHandle(nsContentUtils::GetSystemPrincipal()); const TrackRate sourceRate = 44100; const TrackRate targetRate = 48000; auto listener = MakeRefPtr(); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Started)); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Error)); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Stopped)) .Times(2); RefPtr ais = MakeRefPtr( std::move(listener), sourceId, deviceId, channels, true, testPrincipal, sourceRate, targetRate); ASSERT_TRUE(ais); DispatchFunction([&] { ais->Init(); ais->Start(); }); RefPtr stream = WaitFor(cubeb->StreamInitEvent()); EXPECT_TRUE(stream->mHasInput); EXPECT_FALSE(stream->mHasOutput); EXPECT_EQ(stream->InputChannels(), channels); Unused << WaitFor(stream->FramesProcessedEvent()); DispatchFunction([&] { stream->ForceError(); }); WaitFor(stream->ErrorForcedEvent()); DispatchFunction([&] { ais->Stop(); }); Unused << WaitFor(cubeb->StreamDestroyEvent()); ais = nullptr; // Drop the SharedThreadPool here. } TEST(TestAudioInputSource, DeviceChangedCallback) { MockCubeb* cubeb = new MockCubeb(); CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); const AudioInputSource::Id sourceId = 1; const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; const uint32_t channels = 2; const PrincipalHandle testPrincipal = MakePrincipalHandle(nsContentUtils::GetSystemPrincipal()); const TrackRate sourceRate = 44100; const TrackRate targetRate = 48000; auto listener = MakeRefPtr(); EXPECT_CALL(*listener, AudioDeviceChanged(sourceId)); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Started)); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Stopped)) .Times(2); RefPtr ais = MakeRefPtr( std::move(listener), sourceId, deviceId, channels, true, testPrincipal, sourceRate, targetRate); ASSERT_TRUE(ais); DispatchFunction([&] { ais->Init(); ais->Start(); }); RefPtr stream = WaitFor(cubeb->StreamInitEvent()); EXPECT_TRUE(stream->mHasInput); EXPECT_FALSE(stream->mHasOutput); EXPECT_EQ(stream->InputChannels(), channels); Unused << WaitFor(stream->FramesProcessedEvent()); DispatchFunction([&] { stream->ForceDeviceChanged(); }); WaitFor(stream->DeviceChangeForcedEvent()); DispatchFunction([&] { ais->Stop(); }); Unused << WaitFor(cubeb->StreamDestroyEvent()); ais = nullptr; // Drop the SharedThreadPool here. } TEST(TestAudioInputSource, InputProcessing) { MockCubeb* cubeb = new MockCubeb(); CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); const AudioInputSource::Id sourceId = 1; const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; const uint32_t channels = 2; const PrincipalHandle testPrincipal = MakePrincipalHandle(nsContentUtils::GetSystemPrincipal()); const TrackRate sourceRate = 44100; const TrackRate targetRate = 48000; using ProcessingPromise = AudioInputSource::SetRequestedProcessingParamsPromise; auto listener = MakeRefPtr(); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Started)) .Times(0); EXPECT_CALL(*listener, AudioStateCallback( sourceId, AudioInputSource::EventListener::State::Stopped)) .Times(10); RefPtr ais = MakeRefPtr( std::move(listener), sourceId, deviceId, channels, true, testPrincipal, sourceRate, targetRate); const auto test = [&](cubeb_input_processing_params aRequested, const Result& aExpected) { RefPtr p; DispatchFunction([&] { ais->Init(); p = ais->SetRequestedProcessingParams(aRequested); }); ProcessEventQueue(); EXPECT_EQ(WaitFor(p), aExpected); DispatchFunction([&] { ais->Stop(); }); Unused << WaitFor(cubeb->StreamDestroyEvent()); }; // Not supported by backend. cubeb->SetSupportedInputProcessingParams(CUBEB_INPUT_PROCESSING_PARAM_NONE, CUBEB_ERROR_NOT_SUPPORTED); test(CUBEB_INPUT_PROCESSING_PARAM_NONE, Err(CUBEB_ERROR_NOT_SUPPORTED)); // Not supported by params. cubeb->SetSupportedInputProcessingParams(CUBEB_INPUT_PROCESSING_PARAM_NONE, CUBEB_OK); test(CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION, CUBEB_INPUT_PROCESSING_PARAM_NONE); constexpr cubeb_input_processing_params allParams = CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION | CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION | CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL | CUBEB_INPUT_PROCESSING_PARAM_VOICE_ISOLATION; // Successful all. cubeb->SetSupportedInputProcessingParams(allParams, CUBEB_OK); test(allParams, allParams); // Successful partial. test(CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION, CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION); // Not supported by stream. // Note this also tests that AudioInputSource resets its configured params // state from the previous successful test. constexpr int propagatedError = 99; cubeb->SetInputProcessingApplyRv(propagatedError); test(CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION, Err(propagatedError)); ais = nullptr; // Drop the SharedThreadPool here. }