// Copyright (c) 2013 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 "media/audio/audio_silence_detector.h" #include "base/bind.h" #include "base/bind_helpers.h" #include "base/message_loop.h" #include "base/synchronization/waitable_event.h" #include "base/threading/thread.h" #include "base/time/time.h" #include "media/base/audio_bus.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" using ::testing::_; using ::testing::InvokeWithoutArgs; using ::testing::TestWithParam; using ::testing::Values; namespace media { static const int kSampleRate = 48000; static const int kFramesPerBuffer = 128; namespace { class MockObserver { public: MOCK_METHOD1(OnAudible, void(bool)); }; struct TestScenario { const float* data; int num_channels; int num_frames; float value_range; TestScenario(const float* d, int c, int f, float v) : data(d), num_channels(c), num_frames(f), value_range(v) {} }; } // namespace class AudioSilenceDetectorTest : public TestWithParam { public: AudioSilenceDetectorTest() : audio_manager_thread_(new base::Thread("AudioThread")), notification_received_(false, false) { audio_manager_thread_->Start(); audio_message_loop_ = audio_manager_thread_->message_loop_proxy(); } virtual ~AudioSilenceDetectorTest() { SyncWithAudioThread(); } AudioSilenceDetector* silence_detector() const { return silence_detector_.get(); } void StartSilenceDetector(float threshold, MockObserver* observer) { audio_message_loop_->PostTask( FROM_HERE, base::Bind(&AudioSilenceDetectorTest::StartDetectorOnAudioThread, base::Unretained(this), threshold, observer)); SyncWithAudioThread(); } void StopSilenceDetector() { audio_message_loop_->PostTask( FROM_HERE, base::Bind(&AudioSilenceDetectorTest::StopDetectorOnAudioThread, base::Unretained(this))); SyncWithAudioThread(); } // Creates an AudioBus, sized and populated with kFramesPerBuffer frames of // data. The given test |data| is repeated to fill the buffer. scoped_ptr CreatePopulatedBuffer( const float* data, int num_channels, int num_frames) { scoped_ptr bus = AudioBus::Create(num_channels, kFramesPerBuffer); for (int ch = 0; ch < num_channels; ++ch) { for (int frames = 0; frames < kFramesPerBuffer; frames += num_frames) { const int num_to_copy = std::min(num_frames, kFramesPerBuffer - frames); memcpy(bus->channel(ch) + frames, data + num_frames * ch, sizeof(float) * num_to_copy); } } return bus.Pass(); } void SignalNotificationReceived() { notification_received_.Signal(); } void WaitForNotificationReceived() { notification_received_.Wait(); } // Post a task on the audio thread and block until it is executed. This // provides a barrier: All previous tasks pending on the audio thread have // completed before this method returns. void SyncWithAudioThread() { base::WaitableEvent done(false, false); audio_message_loop_->PostTask( FROM_HERE, base::Bind(&base::WaitableEvent::Signal, base::Unretained(&done))); done.Wait(); } private: void StartDetectorOnAudioThread(float threshold, MockObserver* observer) { const AudioSilenceDetector::AudibleCallback notify_is_audible = base::Bind(&MockObserver::OnAudible, base::Unretained(observer)); silence_detector_.reset(new AudioSilenceDetector( kSampleRate, base::TimeDelta::FromMilliseconds(1), threshold)); silence_detector_->Start(notify_is_audible); } void StopDetectorOnAudioThread() { silence_detector_->Stop(true); silence_detector_.reset(); } scoped_ptr audio_manager_thread_; scoped_refptr audio_message_loop_; base::WaitableEvent notification_received_; scoped_ptr silence_detector_; DISALLOW_COPY_AND_ASSIGN(AudioSilenceDetectorTest); }; TEST_P(AudioSilenceDetectorTest, TriggersAtVariousThresholds) { static const float kThresholdsToTry[] = { 0.0f, 0.125f, 0.25f, 0.5f, 0.75f, 1.0f }; const TestScenario& scenario = GetParam(); for (size_t i = 0; i < arraysize(kThresholdsToTry); ++i) { MockObserver observer; // Start the detector. This should cause a single callback to indicate // we're starting out in silence. EXPECT_CALL(observer, OnAudible(false)) .WillOnce(InvokeWithoutArgs( this, &AudioSilenceDetectorTest::SignalNotificationReceived)) .RetiresOnSaturation(); StartSilenceDetector(kThresholdsToTry[i], &observer); WaitForNotificationReceived(); // Send more data to the silence detector. For some test scenarios, the // sound data will trigger a callback. const bool expect_a_callback = (kThresholdsToTry[i] < scenario.value_range); if (expect_a_callback) { EXPECT_CALL(observer, OnAudible(true)) .WillOnce(InvokeWithoutArgs( this, &AudioSilenceDetectorTest::SignalNotificationReceived)) .RetiresOnSaturation(); } scoped_ptr bus = CreatePopulatedBuffer( scenario.data, scenario.num_channels, scenario.num_frames); silence_detector()->Scan(bus.get(), bus->frames()); if (expect_a_callback) WaitForNotificationReceived(); // Stop the detector. This should cause another callback to indicate we're // ending in silence. EXPECT_CALL(observer, OnAudible(false)) .WillOnce(InvokeWithoutArgs( this, &AudioSilenceDetectorTest::SignalNotificationReceived)) .RetiresOnSaturation(); StopSilenceDetector(); WaitForNotificationReceived(); SyncWithAudioThread(); } } static const float kAllZeros[] = { // left channel 0.0f, // right channel 0.0f }; static const float kAllOnes[] = { // left channel 1.0f, // right channel 1.0f }; static const float kSilentLeftChannel[] = { // left channel 0.5f, 0.5f, 0.5f, 0.5f, // right channel 0.0f, 0.25f, 0.0f, 0.0f }; static const float kSilentRightChannel[] = { // left channel 1.0f, 1.0f, 0.75f, 0.5f, 1.0f, // right channel 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; static const float kAtDifferentVolumesAndBias[] = { // left channel 1.0f, 0.9f, 0.8f, 0.7f, 0.6f, 0.5f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, // right channel 0.0f, 0.2f, 0.4f, 0.6f, 0.4f, 0.2f, 0.0f, 0.2f, 0.4f, 0.6f, 0.4f, 0.2f }; INSTANTIATE_TEST_CASE_P( Scenarios, AudioSilenceDetectorTest, Values( TestScenario(kAllZeros, 2, 1, 0.0f), TestScenario(kAllOnes, 2, 1, 0.0f), TestScenario(kSilentLeftChannel, 2, 4, 0.25f), TestScenario(kSilentRightChannel, 2, 5, 0.5f), TestScenario(kAtDifferentVolumesAndBias, 2, 12, 0.6f))); } // namespace media