| // 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 "base/basictypes.h" |
| #include "base/environment.h" |
| #include "base/file_util.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/message_loop.h" |
| #include "base/path_service.h" |
| #include "base/synchronization/lock.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/time.h" |
| #include "build/build_config.h" |
| #include "media/audio/audio_io.h" |
| #include "media/audio/audio_manager_base.h" |
| #include "media/audio/audio_util.h" |
| #include "media/base/seekable_buffer.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #if defined(OS_LINUX) || defined(OS_OPENBSD) |
| #include "media/audio/linux/audio_manager_linux.h" |
| #elif defined(OS_MACOSX) |
| #include "media/audio/mac/audio_manager_mac.h" |
| #elif defined(OS_WIN) |
| #include "base/win/scoped_com_initializer.h" |
| #include "media/audio/win/audio_manager_win.h" |
| #include "media/audio/win/core_audio_util_win.h" |
| #elif defined(OS_ANDROID) |
| #include "media/audio/android/audio_manager_android.h" |
| #endif |
| |
| namespace media { |
| |
| #if defined(OS_LINUX) || defined(OS_OPENBSD) |
| typedef AudioManagerLinux AudioManagerAnyPlatform; |
| #elif defined(OS_MACOSX) |
| typedef AudioManagerMac AudioManagerAnyPlatform; |
| #elif defined(OS_WIN) |
| typedef AudioManagerWin AudioManagerAnyPlatform; |
| #elif defined(OS_ANDROID) |
| typedef AudioManagerAndroid AudioManagerAnyPlatform; |
| #endif |
| |
| // Limits the number of delay measurements we can store in an array and |
| // then write to file at end of the WASAPIAudioInputOutputFullDuplex test. |
| static const size_t kMaxDelayMeasurements = 1000; |
| |
| // Name of the output text file. The output file will be stored in the |
| // directory containing media_unittests.exe. |
| // Example: \src\build\Debug\audio_delay_values_ms.txt. |
| // See comments for the WASAPIAudioInputOutputFullDuplex test for more details |
| // about the file format. |
| static const char* kDelayValuesFileName = "audio_delay_values_ms.txt"; |
| |
| // Contains delay values which are reported during the full-duplex test. |
| // Total delay = |buffer_delay_ms| + |input_delay_ms| + |output_delay_ms|. |
| struct AudioDelayState { |
| AudioDelayState() |
| : delta_time_ms(0), |
| buffer_delay_ms(0), |
| input_delay_ms(0), |
| output_delay_ms(0) { |
| } |
| |
| // Time in milliseconds since last delay report. Typical value is ~10 [ms]. |
| int delta_time_ms; |
| |
| // Size of internal sync buffer. Typical value is ~0 [ms]. |
| int buffer_delay_ms; |
| |
| // Reported capture/input delay. Typical value is ~10 [ms]. |
| int input_delay_ms; |
| |
| // Reported render/output delay. Typical value is ~40 [ms]. |
| int output_delay_ms; |
| }; |
| |
| // This class mocks the platform specific audio manager and overrides |
| // the GetMessageLoop() method to ensure that we can run our tests on |
| // the main thread instead of the audio thread. |
| class MockAudioManager : public AudioManagerAnyPlatform { |
| public: |
| MockAudioManager() {} |
| virtual ~MockAudioManager() {} |
| |
| virtual scoped_refptr<base::MessageLoopProxy> GetMessageLoop() OVERRIDE { |
| return MessageLoop::current()->message_loop_proxy(); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MockAudioManager); |
| }; |
| |
| // Test fixture class. |
| class AudioLowLatencyInputOutputTest : public testing::Test { |
| protected: |
| AudioLowLatencyInputOutputTest() {} |
| |
| virtual ~AudioLowLatencyInputOutputTest() {} |
| |
| AudioManager* audio_manager() { return &mock_audio_manager_; } |
| MessageLoopForUI* message_loop() { return &message_loop_; } |
| |
| // Convenience method which ensures that we are not running on the build |
| // bots and that at least one valid input and output device can be found. |
| bool CanRunAudioTests() { |
| bool input = audio_manager()->HasAudioInputDevices(); |
| bool output = audio_manager()->HasAudioOutputDevices(); |
| LOG_IF(WARNING, !input) << "No input device detected."; |
| LOG_IF(WARNING, !output) << "No output device detected."; |
| return input && output; |
| } |
| |
| private: |
| MessageLoopForUI message_loop_; |
| MockAudioManager mock_audio_manager_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AudioLowLatencyInputOutputTest); |
| }; |
| |
| // This audio source/sink implementation should be used for manual tests |
| // only since delay measurements are stored on an output text file. |
| // All incoming/recorded audio packets are stored in an intermediate media |
| // buffer which the renderer reads from when it needs audio for playout. |
| // The total effect is that recorded audio is played out in loop back using |
| // a sync buffer as temporary storage. |
| class FullDuplexAudioSinkSource |
| : public AudioInputStream::AudioInputCallback, |
| public AudioOutputStream::AudioSourceCallback { |
| public: |
| FullDuplexAudioSinkSource(int sample_rate, |
| int samples_per_packet, |
| int channels) |
| : sample_rate_(sample_rate), |
| samples_per_packet_(samples_per_packet), |
| channels_(channels), |
| input_elements_to_write_(0), |
| output_elements_to_write_(0), |
| previous_write_time_(base::Time::Now()) { |
| // Size in bytes of each audio frame (4 bytes for 16-bit stereo PCM). |
| frame_size_ = (16 / 8) * channels_; |
| |
| // Start with the smallest possible buffer size. It will be increased |
| // dynamically during the test if required. |
| buffer_.reset( |
| new media::SeekableBuffer(0, samples_per_packet_ * frame_size_)); |
| |
| frames_to_ms_ = static_cast<double>(1000.0 / sample_rate_); |
| delay_states_.reset(new AudioDelayState[kMaxDelayMeasurements]); |
| } |
| |
| virtual ~FullDuplexAudioSinkSource() { |
| // Get complete file path to output file in the directory containing |
| // media_unittests.exe. Example: src/build/Debug/audio_delay_values_ms.txt. |
| FilePath file_name; |
| EXPECT_TRUE(PathService::Get(base::DIR_EXE, &file_name)); |
| file_name = file_name.AppendASCII(kDelayValuesFileName); |
| |
| FILE* text_file = file_util::OpenFile(file_name, "wt"); |
| DLOG_IF(ERROR, !text_file) << "Failed to open log file."; |
| LOG(INFO) << ">> Output file " << file_name.value() << " has been created."; |
| |
| // Write the array which contains time-stamps, buffer size and |
| // audio delays values to a text file. |
| size_t elements_written = 0; |
| while (elements_written < |
| std::min(input_elements_to_write_, output_elements_to_write_)) { |
| const AudioDelayState state = delay_states_[elements_written]; |
| fprintf(text_file, "%d %d %d %d\n", |
| state.delta_time_ms, |
| state.buffer_delay_ms, |
| state.input_delay_ms, |
| state.output_delay_ms); |
| ++elements_written; |
| } |
| |
| file_util::CloseFile(text_file); |
| } |
| |
| // AudioInputStream::AudioInputCallback. |
| virtual void OnData(AudioInputStream* stream, |
| const uint8* src, uint32 size, |
| uint32 hardware_delay_bytes, |
| double volume) OVERRIDE { |
| base::AutoLock lock(lock_); |
| |
| // Update three components in the AudioDelayState for this recorded |
| // audio packet. |
| base::Time now_time = base::Time::Now(); |
| int diff = (now_time - previous_write_time_).InMilliseconds(); |
| previous_write_time_ = now_time; |
| if (input_elements_to_write_ < kMaxDelayMeasurements) { |
| delay_states_[input_elements_to_write_].delta_time_ms = diff; |
| delay_states_[input_elements_to_write_].buffer_delay_ms = |
| BytesToMilliseconds(buffer_->forward_bytes()); |
| delay_states_[input_elements_to_write_].input_delay_ms = |
| BytesToMilliseconds(hardware_delay_bytes); |
| ++input_elements_to_write_; |
| } |
| |
| // Store the captured audio packet in a seekable media buffer. |
| if (!buffer_->Append(src, size)) { |
| // An attempt to write outside the buffer limits has been made. |
| // Double the buffer capacity to ensure that we have a buffer large |
| // enough to handle the current sample test scenario. |
| buffer_->set_forward_capacity(2 * buffer_->forward_capacity()); |
| buffer_->Clear(); |
| } |
| } |
| |
| virtual void OnClose(AudioInputStream* stream) OVERRIDE {} |
| virtual void OnError(AudioInputStream* stream, int code) OVERRIDE {} |
| |
| // AudioOutputStream::AudioSourceCallback. |
| virtual int OnMoreData(AudioBus* audio_bus, |
| AudioBuffersState buffers_state) OVERRIDE { |
| base::AutoLock lock(lock_); |
| |
| // Update one component in the AudioDelayState for the packet |
| // which is about to be played out. |
| if (output_elements_to_write_ < kMaxDelayMeasurements) { |
| int output_delay_bytes = buffers_state.hardware_delay_bytes; |
| #if defined(OS_WIN) |
| // Special fix for Windows in combination with Wave where the |
| // pending bytes field of the audio buffer state is used to |
| // report the delay. |
| if (!CoreAudioUtil::IsSupported()) { |
| output_delay_bytes = buffers_state.pending_bytes; |
| } |
| #endif |
| delay_states_[output_elements_to_write_].output_delay_ms = |
| BytesToMilliseconds(output_delay_bytes); |
| ++output_elements_to_write_; |
| } |
| |
| int size; |
| const uint8* source; |
| // Read the data from the seekable media buffer which contains |
| // captured data at the same size and sample rate as the output side. |
| if (buffer_->GetCurrentChunk(&source, &size) && size > 0) { |
| EXPECT_EQ(channels_, audio_bus->channels()); |
| size = std::min(audio_bus->frames() * frame_size_, size); |
| EXPECT_EQ(static_cast<size_t>(size) % sizeof(*audio_bus->channel(0)), 0U); |
| audio_bus->FromInterleaved( |
| source, size / frame_size_, frame_size_ / channels_); |
| buffer_->Seek(size); |
| return size / frame_size_; |
| } |
| |
| return 0; |
| } |
| |
| virtual int OnMoreIOData(AudioBus* source, |
| AudioBus* dest, |
| AudioBuffersState buffers_state) OVERRIDE { |
| NOTREACHED(); |
| return 0; |
| } |
| |
| virtual void OnError(AudioOutputStream* stream, int code) OVERRIDE {} |
| virtual void WaitTillDataReady() OVERRIDE {} |
| |
| protected: |
| // Converts from bytes to milliseconds taking the sample rate and size |
| // of an audio frame into account. |
| int BytesToMilliseconds(uint32 delay_bytes) const { |
| return static_cast<int>((delay_bytes / frame_size_) * frames_to_ms_ + 0.5); |
| } |
| |
| private: |
| base::Lock lock_; |
| scoped_ptr<media::SeekableBuffer> buffer_; |
| int sample_rate_; |
| int samples_per_packet_; |
| int channels_; |
| int frame_size_; |
| double frames_to_ms_; |
| scoped_array<AudioDelayState> delay_states_; |
| size_t input_elements_to_write_; |
| size_t output_elements_to_write_; |
| base::Time previous_write_time_; |
| }; |
| |
| class AudioInputStreamTraits { |
| public: |
| typedef AudioInputStream StreamType; |
| |
| static int HardwareSampleRate() { |
| return static_cast<int>(media::GetAudioInputHardwareSampleRate( |
| AudioManagerBase::kDefaultDeviceId)); |
| } |
| |
| // TODO(henrika): add support for GetAudioInputHardwareBufferSize in media. |
| static int HardwareBufferSize() { |
| return static_cast<int>(media::GetAudioHardwareBufferSize()); |
| } |
| |
| static StreamType* CreateStream(AudioManager* audio_manager, |
| const AudioParameters& params) { |
| return audio_manager->MakeAudioInputStream(params, |
| AudioManagerBase::kDefaultDeviceId); |
| } |
| }; |
| |
| class AudioOutputStreamTraits { |
| public: |
| typedef AudioOutputStream StreamType; |
| |
| static int HardwareSampleRate() { |
| return static_cast<int>(media::GetAudioHardwareSampleRate()); |
| } |
| |
| static int HardwareBufferSize() { |
| return static_cast<int>(media::GetAudioHardwareBufferSize()); |
| } |
| |
| static StreamType* CreateStream(AudioManager* audio_manager, |
| const AudioParameters& params) { |
| return audio_manager->MakeAudioOutputStream(params); |
| } |
| }; |
| |
| // Traits template holding a trait of StreamType. It encapsulates |
| // AudioInputStream and AudioOutputStream stream types. |
| template <typename StreamTraits> |
| class StreamWrapper { |
| public: |
| typedef typename StreamTraits::StreamType StreamType; |
| |
| explicit StreamWrapper(AudioManager* audio_manager) |
| : |
| #if defined(OS_WIN) |
| com_init_(base::win::ScopedCOMInitializer::kMTA), |
| #endif |
| audio_manager_(audio_manager), |
| format_(AudioParameters::AUDIO_PCM_LOW_LATENCY), |
| #if defined(OS_ANDROID) |
| channel_layout_(CHANNEL_LAYOUT_MONO), |
| #else |
| channel_layout_(CHANNEL_LAYOUT_STEREO), |
| #endif |
| bits_per_sample_(16) { |
| // Use the preferred sample rate. |
| sample_rate_ = StreamTraits::HardwareSampleRate(); |
| |
| // Use the preferred buffer size. Note that the input side uses the same |
| // size as the output side in this implementation. |
| samples_per_packet_ = StreamTraits::HardwareBufferSize(); |
| } |
| |
| virtual ~StreamWrapper() {} |
| |
| // Creates an Audio[Input|Output]Stream stream object using default |
| // parameters. |
| StreamType* Create() { |
| return CreateStream(); |
| } |
| |
| int channels() const { |
| return ChannelLayoutToChannelCount(channel_layout_); |
| } |
| int bits_per_sample() const { return bits_per_sample_; } |
| int sample_rate() const { return sample_rate_; } |
| int samples_per_packet() const { return samples_per_packet_; } |
| |
| private: |
| StreamType* CreateStream() { |
| StreamType* stream = StreamTraits::CreateStream(audio_manager_, |
| AudioParameters(format_, channel_layout_, sample_rate_, |
| bits_per_sample_, samples_per_packet_)); |
| EXPECT_TRUE(stream); |
| return stream; |
| } |
| |
| #if defined(OS_WIN) |
| base::win::ScopedCOMInitializer com_init_; |
| #endif |
| |
| AudioManager* audio_manager_; |
| AudioParameters::Format format_; |
| ChannelLayout channel_layout_; |
| int bits_per_sample_; |
| int sample_rate_; |
| int samples_per_packet_; |
| }; |
| |
| typedef StreamWrapper<AudioInputStreamTraits> AudioInputStreamWrapper; |
| typedef StreamWrapper<AudioOutputStreamTraits> AudioOutputStreamWrapper; |
| |
| // This test is intended for manual tests and should only be enabled |
| // when it is required to make a real-time test of audio in full duplex and |
| // at the same time create a text file which contains measured delay values. |
| // The file can later be analyzed off line using e.g. MATLAB. |
| // MATLAB example: |
| // D=load('audio_delay_values_ms.txt'); |
| // x=cumsum(D(:,1)); |
| // plot(x, D(:,2), x, D(:,3), x, D(:,4), x, D(:,2)+D(:,3)+D(:,4)); |
| // axis([0, max(x), 0, max(D(:,2)+D(:,3)+D(:,4))+10]); |
| // legend('buffer delay','input delay','output delay','total delay'); |
| // xlabel('time [msec]') |
| // ylabel('delay [msec]') |
| // title('Full-duplex audio delay measurement'); |
| TEST_F(AudioLowLatencyInputOutputTest, DISABLED_FullDuplexDelayMeasurement) { |
| if (!CanRunAudioTests()) |
| return; |
| |
| AudioInputStreamWrapper aisw(audio_manager()); |
| AudioInputStream* ais = aisw.Create(); |
| EXPECT_TRUE(ais); |
| |
| AudioOutputStreamWrapper aosw(audio_manager()); |
| AudioOutputStream* aos = aosw.Create(); |
| EXPECT_TRUE(aos); |
| |
| // This test only supports identical parameters in both directions. |
| // TODO(henrika): it is possible to cut delay here by using different |
| // buffer sizes for input and output. |
| if (aisw.sample_rate() != aosw.sample_rate() || |
| aisw.samples_per_packet() != aosw.samples_per_packet() || |
| aisw.channels()!= aosw.channels() || |
| aisw.bits_per_sample() != aosw.bits_per_sample()) { |
| LOG(ERROR) << "This test requires symmetric input and output parameters. " |
| "Ensure that sample rate and number of channels are identical in " |
| "both directions"; |
| aos->Close(); |
| ais->Close(); |
| return; |
| } |
| |
| EXPECT_TRUE(ais->Open()); |
| EXPECT_TRUE(aos->Open()); |
| |
| FullDuplexAudioSinkSource full_duplex( |
| aisw.sample_rate(), aisw.samples_per_packet(), aisw.channels()); |
| |
| LOG(INFO) << ">> You should now be able to hear yourself in loopback..."; |
| DLOG(INFO) << " sample_rate : " << aisw.sample_rate(); |
| DLOG(INFO) << " samples_per_packet: " << aisw.samples_per_packet(); |
| DLOG(INFO) << " channels : " << aisw.channels(); |
| |
| ais->Start(&full_duplex); |
| aos->Start(&full_duplex); |
| |
| // Wait for approximately 10 seconds. The user shall hear his own voice |
| // in loop back during this time. At the same time, delay recordings are |
| // performed and stored in the output text file. |
| message_loop()->PostDelayedTask(FROM_HERE, |
| MessageLoop::QuitClosure(), TestTimeouts::action_timeout()); |
| message_loop()->Run(); |
| |
| aos->Stop(); |
| ais->Stop(); |
| |
| // All Close() operations that run on the mocked audio thread, |
| // should be synchronous and not post additional close tasks to |
| // mocked the audio thread. Hence, there is no need to call |
| // message_loop()->RunUntilIdle() after the Close() methods. |
| aos->Close(); |
| ais->Close(); |
| } |
| |
| } // namespace media |