| // Copyright (c) 2012 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 <stdint.h> |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/environment.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/run_loop.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/threading/platform_thread.h" |
| #include "build/build_config.h" |
| #include "media/audio/audio_device_description.h" |
| #include "media/audio/audio_device_info_accessor_for_tests.h" |
| #include "media/audio/audio_io.h" |
| #include "media/audio/audio_manager_base.h" |
| #include "media/audio/audio_unittest_util.h" |
| #include "media/audio/mac/audio_low_latency_input_mac.h" |
| #include "media/audio/test_audio_thread.h" |
| #include "media/base/seekable_buffer.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using ::testing::_; |
| using ::testing::AnyNumber; |
| using ::testing::AtLeast; |
| using ::testing::Ge; |
| using ::testing::NotNull; |
| |
| namespace media { |
| |
| ACTION_P4(CheckCountAndPostQuitTask, count, limit, task_runner, closure) { |
| if (++*count >= limit) { |
| task_runner->PostTask(FROM_HERE, closure); |
| } |
| } |
| |
| class MockAudioInputCallback : public AudioInputStream::AudioInputCallback { |
| public: |
| MOCK_METHOD3(OnData, |
| void(const AudioBus* src, |
| base::TimeTicks capture_time, |
| double volume)); |
| MOCK_METHOD0(OnError, void()); |
| }; |
| |
| // This audio sink implementation should be used for manual tests only since |
| // the recorded data is stored on a raw binary data file. |
| // The last test (WriteToFileAudioSink) - which is disabled by default - |
| // can use this audio sink to store the captured data on a file for offline |
| // analysis. |
| class WriteToFileAudioSink : public AudioInputStream::AudioInputCallback { |
| public: |
| // Allocate space for ~10 seconds of data @ 48kHz in stereo: |
| // 2 bytes per sample, 2 channels, 10ms @ 48kHz, 10 seconds <=> 1920000 bytes. |
| static const int kMaxBufferSize = 2 * 2 * 480 * 100 * 10; |
| |
| explicit WriteToFileAudioSink(const char* file_name) |
| : buffer_(0, kMaxBufferSize), |
| file_(fopen(file_name, "wb")), |
| bytes_to_write_(0) { |
| } |
| |
| ~WriteToFileAudioSink() override { |
| int bytes_written = 0; |
| while (bytes_written < bytes_to_write_) { |
| const uint8_t* chunk; |
| int chunk_size; |
| |
| // Stop writing if no more data is available. |
| if (!buffer_.GetCurrentChunk(&chunk, &chunk_size)) |
| break; |
| |
| // Write recorded data chunk to the file and prepare for next chunk. |
| fwrite(chunk, 1, chunk_size, file_); |
| buffer_.Seek(chunk_size); |
| bytes_written += chunk_size; |
| } |
| fclose(file_); |
| } |
| |
| // AudioInputStream::AudioInputCallback implementation. |
| void OnData(const AudioBus* src, |
| base::TimeTicks capture_time, |
| double volume) override { |
| const int num_samples = src->frames() * src->channels(); |
| std::unique_ptr<int16_t> interleaved(new int16_t[num_samples]); |
| src->ToInterleaved<SignedInt16SampleTypeTraits>(src->frames(), |
| interleaved.get()); |
| |
| // Store data data in a temporary buffer to avoid making blocking |
| // fwrite() calls in the audio callback. The complete buffer will be |
| // written to file in the destructor. |
| const int bytes_per_sample = sizeof(*interleaved); |
| const int size = bytes_per_sample * num_samples; |
| if (buffer_.Append((const uint8_t*)interleaved.get(), size)) { |
| bytes_to_write_ += size; |
| } |
| } |
| |
| void OnError() override {} |
| |
| private: |
| media::SeekableBuffer buffer_; |
| FILE* file_; |
| int bytes_to_write_; |
| }; |
| |
| class MacAudioInputTest : public testing::Test { |
| protected: |
| MacAudioInputTest() |
| : task_environment_( |
| base::test::SingleThreadTaskEnvironment::MainThreadType::UI), |
| audio_manager_(AudioManager::CreateForTesting( |
| std::make_unique<TestAudioThread>())) { |
| // Wait for the AudioManager to finish any initialization on the audio loop. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| ~MacAudioInputTest() override { audio_manager_->Shutdown(); } |
| |
| bool InputDevicesAvailable() { |
| #if defined(OS_MAC) && defined(ARCH_CPU_ARM64) |
| // TODO(crbug.com/1128458): macOS on ARM64 says it has devices, but won't |
| // let any of them be opened or listed. |
| return false; |
| #else |
| return AudioDeviceInfoAccessorForTests(audio_manager_.get()) |
| .HasAudioInputDevices(); |
| #endif |
| } |
| |
| // Convenience method which creates a default AudioInputStream object using |
| // a 10ms frame size and a sample rate which is set to the hardware sample |
| // rate. |
| AudioInputStream* CreateDefaultAudioInputStream() { |
| int fs = static_cast<int>(AUAudioInputStream::HardwareSampleRate()); |
| int samples_per_packet = fs / 100; |
| AudioInputStream* ais = audio_manager_->MakeAudioInputStream( |
| AudioParameters(AudioParameters::AUDIO_PCM_LOW_LATENCY, |
| CHANNEL_LAYOUT_STEREO, fs, samples_per_packet), |
| AudioDeviceDescription::kDefaultDeviceId, |
| base::BindRepeating(&MacAudioInputTest::OnLogMessage, |
| base::Unretained(this))); |
| EXPECT_TRUE(ais); |
| return ais; |
| } |
| |
| // Convenience method which creates an AudioInputStream object with a |
| // specified channel layout. |
| AudioInputStream* CreateAudioInputStream(ChannelLayout channel_layout) { |
| int fs = static_cast<int>(AUAudioInputStream::HardwareSampleRate()); |
| int samples_per_packet = fs / 100; |
| AudioInputStream* ais = audio_manager_->MakeAudioInputStream( |
| AudioParameters(AudioParameters::AUDIO_PCM_LOW_LATENCY, channel_layout, |
| fs, samples_per_packet), |
| AudioDeviceDescription::kDefaultDeviceId, |
| base::BindRepeating(&MacAudioInputTest::OnLogMessage, |
| base::Unretained(this))); |
| EXPECT_TRUE(ais); |
| return ais; |
| } |
| |
| void OnLogMessage(const std::string& message) { log_message_ = message; } |
| |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| std::unique_ptr<AudioManager> audio_manager_; |
| std::string log_message_; |
| }; |
| |
| // Test Create(), Close(). |
| TEST_F(MacAudioInputTest, AUAudioInputStreamCreateAndClose) { |
| ABORT_AUDIO_TEST_IF_NOT(InputDevicesAvailable()); |
| AudioInputStream* ais = CreateDefaultAudioInputStream(); |
| ais->Close(); |
| } |
| |
| // Test Open(), Close(). |
| TEST_F(MacAudioInputTest, AUAudioInputStreamOpenAndClose) { |
| ABORT_AUDIO_TEST_IF_NOT(InputDevicesAvailable()); |
| AudioInputStream* ais = CreateDefaultAudioInputStream(); |
| EXPECT_EQ(ais->Open(), AudioInputStream::OpenOutcome::kSuccess); |
| ais->Close(); |
| } |
| |
| // Test Open(), Start(), Close(). |
| TEST_F(MacAudioInputTest, AUAudioInputStreamOpenStartAndClose) { |
| ABORT_AUDIO_TEST_IF_NOT(InputDevicesAvailable()); |
| AudioInputStream* ais = CreateDefaultAudioInputStream(); |
| EXPECT_EQ(ais->Open(), AudioInputStream::OpenOutcome::kSuccess); |
| MockAudioInputCallback sink; |
| ais->Start(&sink); |
| ais->Close(); |
| } |
| |
| // Test Open(), Start(), Stop(), Close(). |
| TEST_F(MacAudioInputTest, AUAudioInputStreamOpenStartStopAndClose) { |
| ABORT_AUDIO_TEST_IF_NOT(InputDevicesAvailable()); |
| AudioInputStream* ais = CreateDefaultAudioInputStream(); |
| EXPECT_EQ(ais->Open(), AudioInputStream::OpenOutcome::kSuccess); |
| MockAudioInputCallback sink; |
| ais->Start(&sink); |
| ais->Stop(); |
| ais->Close(); |
| } |
| |
| // Verify that recording starts and stops correctly in mono using mocked sink. |
| TEST_F(MacAudioInputTest, AUAudioInputStreamVerifyMonoRecording) { |
| ABORT_AUDIO_TEST_IF_NOT(InputDevicesAvailable()); |
| |
| int count = 0; |
| |
| // Create an audio input stream which records in mono. |
| AudioInputStream* ais = CreateAudioInputStream(CHANNEL_LAYOUT_MONO); |
| EXPECT_EQ(ais->Open(), AudioInputStream::OpenOutcome::kSuccess); |
| |
| MockAudioInputCallback sink; |
| |
| // We use 10ms packets and will run the test until ten packets are received. |
| // All should contain valid packets of the same size and a valid delay |
| // estimate. |
| base::RunLoop run_loop; |
| EXPECT_CALL(sink, OnData(NotNull(), _, _)) |
| .Times(AtLeast(10)) |
| .WillRepeatedly(CheckCountAndPostQuitTask( |
| &count, 10, task_environment_.GetMainThreadTaskRunner(), |
| run_loop.QuitClosure())); |
| ais->Start(&sink); |
| run_loop.Run(); |
| ais->Stop(); |
| ais->Close(); |
| |
| EXPECT_FALSE(log_message_.empty()); |
| } |
| |
| // Verify that recording starts and stops correctly in mono using mocked sink. |
| TEST_F(MacAudioInputTest, AUAudioInputStreamVerifyStereoRecording) { |
| ABORT_AUDIO_TEST_IF_NOT(InputDevicesAvailable()); |
| |
| int count = 0; |
| |
| // Create an audio input stream which records in stereo. |
| AudioInputStream* ais = CreateAudioInputStream(CHANNEL_LAYOUT_STEREO); |
| EXPECT_EQ(ais->Open(), AudioInputStream::OpenOutcome::kSuccess); |
| |
| MockAudioInputCallback sink; |
| |
| // We use 10ms packets and will run the test until ten packets are received. |
| // All should contain valid packets of the same size and a valid delay |
| // estimate. |
| // TODO(henrika): http://crbug.com/154352 forced us to run the capture side |
| // using a native buffer size of 128 audio frames and combine it with a FIFO |
| // to match the requested size by the client. This change might also have |
| // modified the delay estimates since the existing Ge(bytes_per_packet) for |
| // parameter #4 does no longer pass. I am removing this restriction here to |
| // ensure that we can land the patch but will revisit this test again when |
| // more analysis of the delay estimates are done. |
| base::RunLoop run_loop; |
| EXPECT_CALL(sink, OnData(NotNull(), _, _)) |
| .Times(AtLeast(10)) |
| .WillRepeatedly(CheckCountAndPostQuitTask( |
| &count, 10, task_environment_.GetMainThreadTaskRunner(), |
| run_loop.QuitClosure())); |
| ais->Start(&sink); |
| run_loop.Run(); |
| ais->Stop(); |
| ais->Close(); |
| |
| EXPECT_FALSE(log_message_.empty()); |
| } |
| |
| // This test is intended for manual tests and should only be enabled |
| // when it is required to store the captured data on a local file. |
| // By default, GTest will print out YOU HAVE 1 DISABLED TEST. |
| // To include disabled tests in test execution, just invoke the test program |
| // with --gtest_also_run_disabled_tests or set the GTEST_ALSO_RUN_DISABLED_TESTS |
| // environment variable to a value greater than 0. |
| TEST_F(MacAudioInputTest, DISABLED_AUAudioInputStreamRecordToFile) { |
| ABORT_AUDIO_TEST_IF_NOT(InputDevicesAvailable()); |
| const char* file_name = "out_stereo_10sec.pcm"; |
| |
| int fs = static_cast<int>(AUAudioInputStream::HardwareSampleRate()); |
| AudioInputStream* ais = CreateDefaultAudioInputStream(); |
| EXPECT_EQ(ais->Open(), AudioInputStream::OpenOutcome::kSuccess); |
| |
| fprintf(stderr, " File name : %s\n", file_name); |
| fprintf(stderr, " Sample rate: %d\n", fs); |
| WriteToFileAudioSink file_sink(file_name); |
| fprintf(stderr, " >> Speak into the mic while recording...\n"); |
| ais->Start(&file_sink); |
| base::PlatformThread::Sleep(TestTimeouts::action_timeout()); |
| ais->Stop(); |
| fprintf(stderr, " >> Recording has stopped.\n"); |
| ais->Close(); |
| } |
| |
| TEST(MacAudioInputUpmixerTest, Upmix16bit) { |
| constexpr int kNumFrames = 512; |
| constexpr int kBytesPerSample = sizeof(int16_t); |
| int16_t mono[kNumFrames]; |
| int16_t stereo[kNumFrames * 2]; |
| |
| // Fill the mono buffer and the first half of the stereo buffer with data |
| for (int i = 0; i != kNumFrames; ++i) { |
| mono[i] = i; |
| stereo[i] = i; |
| } |
| |
| AudioBuffer audio_buffer; |
| audio_buffer.mNumberChannels = 2; |
| audio_buffer.mDataByteSize = kNumFrames * kBytesPerSample * 2; |
| audio_buffer.mData = stereo; |
| AUAudioInputStream::UpmixMonoToStereoInPlace(&audio_buffer, kBytesPerSample); |
| |
| // Assert that the samples have been distributed properly |
| for (int i = 0; i != kNumFrames; ++i) { |
| ASSERT_EQ(mono[i], stereo[i * 2]); |
| ASSERT_EQ(mono[i], stereo[i * 2 + 1]); |
| } |
| } |
| |
| TEST(MacAudioInputUpmixerTest, Upmix32bit) { |
| constexpr int kNumFrames = 512; |
| constexpr int kBytesPerSample = sizeof(int32_t); |
| int32_t mono[kNumFrames]; |
| int32_t stereo[kNumFrames * 2]; |
| |
| // Fill the mono buffer and the first half of the stereo buffer with data |
| for (int i = 0; i != kNumFrames; ++i) { |
| mono[i] = i; |
| stereo[i] = i; |
| } |
| |
| AudioBuffer audio_buffer; |
| audio_buffer.mNumberChannels = 2; |
| audio_buffer.mDataByteSize = kNumFrames * kBytesPerSample * 2; |
| audio_buffer.mData = stereo; |
| AUAudioInputStream::UpmixMonoToStereoInPlace(&audio_buffer, kBytesPerSample); |
| |
| // Assert that the samples have been distributed properly |
| for (int i = 0; i != kNumFrames; ++i) { |
| ASSERT_EQ(mono[i], stereo[i * 2]); |
| ASSERT_EQ(mono[i], stereo[i * 2 + 1]); |
| } |
| } |
| |
| } // namespace media |