// Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "services/audio/loopback_stream.h" #include #include #include #include #include "base/bind.h" #include "base/containers/unique_ptr_adapters.h" #include "base/memory/scoped_refptr.h" #include "base/test/task_environment.h" #include "base/unguessable_token.h" #include "media/base/audio_parameters.h" #include "media/base/audio_timestamp_helper.h" #include "media/base/channel_layout.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/receiver.h" #include "mojo/public/cpp/bindings/remote.h" #include "services/audio/loopback_coordinator.h" #include "services/audio/loopback_group_member.h" #include "services/audio/test/fake_consumer.h" #include "services/audio/test/fake_loopback_group_member.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" using testing::_; using testing::Mock; using testing::NiceMock; using testing::StrictMock; namespace audio { namespace { // Volume settings for the FakeLoopbackGroupMember (source) and LoopbackStream. constexpr double kSnoopVolume = 0.25; constexpr double kLoopbackVolume = 0.5; // Piano key frequencies. constexpr double kMiddleAFreq = 440; constexpr double kMiddleCFreq = 261.626; // Audio buffer duration. constexpr base::TimeDelta kBufferDuration = base::TimeDelta::FromMilliseconds(10); // Local audio output delay. constexpr base::TimeDelta kDelayUntilOutput = base::TimeDelta::FromMilliseconds(20); // The amount of audio signal to record each time PumpAudioAndTakeNewRecording() // is called. constexpr base::TimeDelta kTestRecordingDuration = base::TimeDelta::FromMilliseconds(250); const media::AudioParameters& GetLoopbackStreamParams() { // 48 kHz, 2-channel audio, with 10 ms buffers. static const media::AudioParameters params( media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::CHANNEL_LAYOUT_STEREO, 48000, 480); return params; } class MockClientAndObserver : public media::mojom::AudioInputStreamClient, public media::mojom::AudioInputStreamObserver { public: MockClientAndObserver() = default; ~MockClientAndObserver() override = default; void Bind(mojo::PendingReceiver client_receiver, mojo::PendingReceiver observer_receiver) { client_receiver_.Bind(std::move(client_receiver)); observer_receiver_.Bind(std::move(observer_receiver)); } void CloseClientBinding() { client_receiver_.reset(); } void CloseObserverBinding() { observer_receiver_.reset(); } MOCK_METHOD0(OnError, void()); MOCK_METHOD0(DidStartRecording, void()); void OnMutedStateChanged(bool) override { NOTREACHED(); } private: mojo::Receiver client_receiver_{this}; mojo::Receiver observer_receiver_{ this}; }; // Subclass of FakeConsumer that adapts the SyncWriter interface to allow the // tests to record and analyze the audio data from the LoopbackStream. class FakeSyncWriter : public FakeConsumer, public InputController::SyncWriter { public: FakeSyncWriter(int channels, int sample_rate) : FakeConsumer(channels, sample_rate) {} ~FakeSyncWriter() override = default; void Clear() { FakeConsumer::Clear(); last_capture_time_ = base::TimeTicks(); } // media::AudioInputController::SyncWriter implementation. void Write(const media::AudioBus* data, double volume, bool key_pressed, base::TimeTicks capture_time) final { FakeConsumer::Consume(*data); // Capture times should be monotonically increasing. if (!last_capture_time_.is_null()) { CHECK_LT(last_capture_time_, capture_time); } last_capture_time_ = capture_time; } void Close() final {} base::TimeTicks last_capture_time_; }; class LoopbackStreamTest : public testing::Test { public: LoopbackStreamTest() : group_id_(base::UnguessableToken::Create()) {} ~LoopbackStreamTest() override = default; void TearDown() override { stream_ = nullptr; for (const auto& source : sources_) { coordinator_.UnregisterMember(group_id_, source.get()); } sources_.clear(); task_environment_.FastForwardUntilNoTasksRemain(); } MockClientAndObserver* client() { return &client_; } LoopbackStream* stream() { return stream_.get(); } FakeSyncWriter* consumer() { return consumer_; } void RunMojoTasks() { task_environment_.RunUntilIdle(); } FakeLoopbackGroupMember* AddSource(int channels, int sample_rate) { sources_.emplace_back(std::make_unique( media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::GuessChannelLayout(channels), sample_rate, (sample_rate * kBufferDuration).InSeconds()))); coordinator_.RegisterMember(group_id_, sources_.back().get()); return sources_.back().get(); } void RemoveSource(FakeLoopbackGroupMember* source) { const auto it = std::find_if(sources_.begin(), sources_.end(), base::MatchesUniquePtr(source)); if (it != sources_.end()) { coordinator_.UnregisterMember(group_id_, source); sources_.erase(it); } } void CreateLoopbackStream() { CHECK(!stream_); mojo::PendingRemote client; mojo::PendingRemote observer; client_.Bind(client.InitWithNewPipeAndPassReceiver(), observer.InitWithNewPipeAndPassReceiver()); stream_ = std::make_unique( base::BindOnce([](media::mojom::ReadOnlyAudioDataPipePtr pipe) { EXPECT_TRUE(pipe->shared_memory.IsValid()); EXPECT_TRUE(pipe->socket.is_valid()); }), base::BindOnce([](LoopbackStreamTest* self, LoopbackStream* stream) { self->stream_ = nullptr; }, this), task_environment_.GetMainThreadTaskRunner(), remote_input_stream_.BindNewPipeAndPassReceiver(), std::move(client), std::move(observer), GetLoopbackStreamParams(), // The following argument is the |shared_memory_count|, which does not // matter because the SyncWriter will be overridden with FakeSyncWriter // below. 1, &coordinator_, group_id_); // Override the clock used by the LoopbackStream so that everything is // single-threaded and synchronized with the driving code in these tests. stream_->set_clock_for_testing(task_environment_.GetMockTickClock()); // Redirect the output of the LoopbackStream to a FakeSyncWriter. // LoopbackStream takes ownership of the FakeSyncWriter. auto consumer = std::make_unique( GetLoopbackStreamParams().channels(), GetLoopbackStreamParams().sample_rate()); CHECK(!consumer_); consumer_ = consumer.get(); stream_->set_sync_writer_for_testing(std::move(consumer)); // Set the volume for the LoopbackStream. remote_input_stream_->SetVolume(kLoopbackVolume); // Allow all pending mojo tasks for all of the above to run and propagate // state. RunMojoTasks(); ASSERT_TRUE(remote_input_stream_); } void StartLoopbackRecording() { ASSERT_EQ(0, consumer_->GetRecordedFrameCount()); remote_input_stream_->Record(); RunMojoTasks(); } void SetLoopbackVolume(double volume) { remote_input_stream_->SetVolume(volume); RunMojoTasks(); } void PumpAudioAndTakeNewRecording() { consumer_->Clear(); const int min_frames_to_record = media::AudioTimestampHelper::TimeToFrames( kTestRecordingDuration, GetLoopbackStreamParams().sample_rate()); do { // Render audio meant for local output at some point in the near // future. const base::TimeTicks output_timestamp = task_environment_.NowTicks() + kDelayUntilOutput; for (const auto& source : sources_) { source->RenderMoreAudio(output_timestamp); } // Move the task runner forward, which will cause the FlowNetwork's // delayed tasks to run, which will generate output for the consumer. task_environment_.FastForwardBy(kBufferDuration); } while (consumer_->GetRecordedFrameCount() < min_frames_to_record); } void CloseInputStreamPtr() { remote_input_stream_.reset(); RunMojoTasks(); } private: base::test::TaskEnvironment task_environment_{ base::test::TaskEnvironment::TimeSource::MOCK_TIME}; LoopbackCoordinator coordinator_; const base::UnguessableToken group_id_; std::vector> sources_; NiceMock client_; std::unique_ptr stream_; FakeSyncWriter* consumer_ = nullptr; // Owned by |stream_|. mojo::Remote remote_input_stream_; DISALLOW_COPY_AND_ASSIGN(LoopbackStreamTest); }; TEST_F(LoopbackStreamTest, ShutsDownStreamWhenInterfacePtrIsClosed) { CreateLoopbackStream(); EXPECT_CALL(*client(), DidStartRecording()); StartLoopbackRecording(); PumpAudioAndTakeNewRecording(); EXPECT_CALL(*client(), OnError()); CloseInputStreamPtr(); EXPECT_FALSE(stream()); Mock::VerifyAndClearExpectations(client()); } TEST_F(LoopbackStreamTest, ShutsDownStreamWhenClientBindingIsClosed) { CreateLoopbackStream(); EXPECT_CALL(*client(), DidStartRecording()); StartLoopbackRecording(); PumpAudioAndTakeNewRecording(); // Note: Expect no call to client::OnError() because it is the client binding // that is being closed and causing the error. EXPECT_CALL(*client(), OnError()).Times(0); client()->CloseClientBinding(); RunMojoTasks(); EXPECT_FALSE(stream()); Mock::VerifyAndClearExpectations(client()); } TEST_F(LoopbackStreamTest, ShutsDownStreamWhenObserverBindingIsClosed) { CreateLoopbackStream(); EXPECT_CALL(*client(), DidStartRecording()); StartLoopbackRecording(); PumpAudioAndTakeNewRecording(); EXPECT_CALL(*client(), OnError()); client()->CloseObserverBinding(); RunMojoTasks(); EXPECT_FALSE(stream()); Mock::VerifyAndClearExpectations(client()); } TEST_F(LoopbackStreamTest, ProducesSilenceWhenNoMembersArePresent) { CreateLoopbackStream(); EXPECT_CALL(*client(), DidStartRecording()); StartLoopbackRecording(); PumpAudioAndTakeNewRecording(); for (int ch = 0; ch < GetLoopbackStreamParams().channels(); ++ch) { SCOPED_TRACE(testing::Message() << "ch=" << ch); EXPECT_TRUE(consumer()->IsSilent(ch)); } } // Syntatic sugar to confirm a tone exists and its amplitude matches // expectations. #define EXPECT_TONE(ch, frequency, expected_amplitude) \ { \ SCOPED_TRACE(testing::Message() << "ch=" << ch); \ const double amplitude = consumer()->ComputeAmplitudeAt( \ ch, frequency, consumer()->GetRecordedFrameCount()); \ VLOG(1) << "For ch=" << ch << ", amplitude at frequency=" << frequency \ << " is " << amplitude; \ EXPECT_NEAR(expected_amplitude, amplitude, 0.01); \ } TEST_F(LoopbackStreamTest, ProducesAudioFromASingleSource) { FakeLoopbackGroupMember* const source = AddSource(1, 48000); // Monaural, 48 kHz. source->SetChannelTone(0, kMiddleAFreq); source->SetVolume(kSnoopVolume); CreateLoopbackStream(); EXPECT_CALL(*client(), DidStartRecording()); StartLoopbackRecording(); PumpAudioAndTakeNewRecording(); // Expect to have recorded middle-A in all of the loopback stream's channels. for (int ch = 0; ch < GetLoopbackStreamParams().channels(); ++ch) { EXPECT_TONE(ch, kMiddleAFreq, kSnoopVolume * kLoopbackVolume); } } TEST_F(LoopbackStreamTest, ProducesAudioFromTwoSources) { // Start the first source (of a middle-A note) before creating the loopback // stream. const int channels = GetLoopbackStreamParams().channels(); FakeLoopbackGroupMember* const source1 = AddSource(channels, 48000); source1->SetChannelTone(0, kMiddleAFreq); source1->SetVolume(kSnoopVolume); CreateLoopbackStream(); EXPECT_CALL(*client(), DidStartRecording()); StartLoopbackRecording(); PumpAudioAndTakeNewRecording(); // Start the second source (of a middle-C note) while the loopback stream is // running. The second source has a different sample rate than the first. FakeLoopbackGroupMember* const source2 = AddSource(channels, 44100); source2->SetChannelTone(1, kMiddleCFreq); source2->SetVolume(kSnoopVolume); PumpAudioAndTakeNewRecording(); // Expect to have recorded both middle-A and middle-C in all of the loopback // stream's channels. EXPECT_TONE(0, kMiddleAFreq, kSnoopVolume * kLoopbackVolume); EXPECT_TONE(1, kMiddleCFreq, kSnoopVolume * kLoopbackVolume); // Switch the channels containig the tone in both sources, and expect to see // the tones have switched channels in the loopback output. source1->SetChannelTone(0, 0.0); source1->SetChannelTone(1, kMiddleAFreq); source2->SetChannelTone(0, kMiddleCFreq); source2->SetChannelTone(1, 0.0); PumpAudioAndTakeNewRecording(); EXPECT_TONE(1, kMiddleAFreq, kSnoopVolume * kLoopbackVolume); EXPECT_TONE(0, kMiddleCFreq, kSnoopVolume * kLoopbackVolume); } TEST_F(LoopbackStreamTest, AudioChangesVolume) { FakeLoopbackGroupMember* const source = AddSource(1, 48000); // Monaural, 48 kHz. source->SetChannelTone(0, kMiddleAFreq); source->SetVolume(kSnoopVolume); CreateLoopbackStream(); StartLoopbackRecording(); PumpAudioAndTakeNewRecording(); // Record and check the amplitude at the default volume settings. double expected_amplitude = kSnoopVolume * kLoopbackVolume; for (int ch = 0; ch < GetLoopbackStreamParams().channels(); ++ch) { EXPECT_TONE(ch, kMiddleAFreq, expected_amplitude); } // Double the volume of the source and expect the output to have also doubled. source->SetVolume(kSnoopVolume * 2); PumpAudioAndTakeNewRecording(); expected_amplitude *= 2; for (int ch = 0; ch < GetLoopbackStreamParams().channels(); ++ch) { EXPECT_TONE(ch, kMiddleAFreq, expected_amplitude); } // Drop the LoopbackStream volume by 1/3 and expect the output to also have // dropped by 1/3. SetLoopbackVolume(kLoopbackVolume / 3); PumpAudioAndTakeNewRecording(); expected_amplitude /= 3; for (int ch = 0; ch < GetLoopbackStreamParams().channels(); ++ch) { EXPECT_TONE(ch, kMiddleAFreq, expected_amplitude); } } } // namespace } // namespace audio