diff options
author | miu@chromium.org <miu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-19 20:55:56 +0000 |
---|---|---|
committer | miu@chromium.org <miu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-19 20:55:56 +0000 |
commit | e2cdbb87ea16864fec2814f6562d3ec64fa4a9a4 (patch) | |
tree | d395b3169477c9cd375272741e29f07cf1ad3fd9 /media/audio | |
parent | b8145ed316d8626baa7d4a9fd83dd4e39868bc67 (diff) | |
download | chromium_src-e2cdbb87ea16864fec2814f6562d3ec64fa4a9a4.zip chromium_src-e2cdbb87ea16864fec2814f6562d3ec64fa4a9a4.tar.gz chromium_src-e2cdbb87ea16864fec2814f6562d3ec64fa4a9a4.tar.bz2 |
Silence detection for audio output UI favicon indicator.
Added a ProbablyContainsSilence() utility method to media::AudioBus, and then added silence detection logic to media::AudioOutputController. While the detection logic will run on the "native audio thread," it should use an inconsequential amount of CPU time. AudioOutputController will invoke a new EventHandler::OnAudible() callback no more often than every 50 ms, and only when transitioning to/from periods of silence.
Also, to avoid O(N) look-ups in AudioRendererHost for each call to any AudioOutputController::EventHandler method, the interface was simplified such that each stream/controller gets its own EventHandler instance. Changed AudioRendererHost::AudioEntry to become the class that implements the EventHandler interface. Lots of related clean-ups throughout audio_renderer_host.cc to deflate the code.
BUG=178343
TEST=media_unittests, content_unittests, and confirmation by running chrome manually
TBR=avi
Review URL: https://chromiumcodereview.appspot.com/12426002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@189095 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media/audio')
-rw-r--r-- | media/audio/audio_output_controller.cc | 38 | ||||
-rw-r--r-- | media/audio/audio_output_controller.h | 25 | ||||
-rw-r--r-- | media/audio/audio_output_controller_unittest.cc | 35 | ||||
-rw-r--r-- | media/audio/audio_silence_detector.cc | 134 | ||||
-rw-r--r-- | media/audio/audio_silence_detector.h | 112 | ||||
-rw-r--r-- | media/audio/audio_silence_detector_unittest.cc | 227 |
6 files changed, 542 insertions, 29 deletions
diff --git a/media/audio/audio_output_controller.cc b/media/audio/audio_output_controller.cc index 9120706..d162cc2 100644 --- a/media/audio/audio_output_controller.cc +++ b/media/audio/audio_output_controller.cc @@ -10,6 +10,7 @@ #include "base/threading/platform_thread.h" #include "base/time.h" #include "build/build_config.h" +#include "media/audio/audio_silence_detector.h" #include "media/audio/audio_util.h" #include "media/audio/shared_memory_util.h" @@ -18,6 +19,17 @@ using base::TimeDelta; namespace media { +// Amount of contiguous time where all audio is silent before considering the +// stream to have transitioned and EventHandler::OnAudible() should be called. +static const int kQuestionableSilencePeriodMillis = 50; + +// Sample value range below which audio is considered indistinguishably silent. +// +// TODO(miu): This value should be specified in dbFS units rather than full +// scale. See TODO in audio_silence_detector.h. +static const float kIndistinguishableSilenceThreshold = + 1.0f / 4096.0f; // Note: This is approximately -72 dbFS. + // Polling-related constants. const int AudioOutputController::kPollNumAttempts = 3; const int AudioOutputController::kPollPauseInMilliseconds = 3; @@ -109,7 +121,7 @@ void AudioOutputController::DoCreate(bool is_for_device_change) { state_ = kError; // TODO(hclam): Define error types. - handler_->OnError(this, 0); + handler_->OnError(0); return; } @@ -118,7 +130,7 @@ void AudioOutputController::DoCreate(bool is_for_device_change) { state_ = kError; // TODO(hclam): Define error types. - handler_->OnError(this, 0); + handler_->OnError(0); return; } @@ -135,7 +147,7 @@ void AudioOutputController::DoCreate(bool is_for_device_change) { // And then report we have been created if we haven't done so already. if (!is_for_device_change) - handler_->OnCreated(this); + handler_->OnCreated(); } void AudioOutputController::DoPlay() { @@ -185,12 +197,20 @@ void AudioOutputController::StartStream() { DCHECK(message_loop_->BelongsToCurrentThread()); state_ = kPlaying; + silence_detector_.reset(new AudioSilenceDetector( + params_.sample_rate(), + TimeDelta::FromMilliseconds(kQuestionableSilencePeriodMillis), + kIndistinguishableSilenceThreshold)); + // We start the AudioOutputStream lazily. AllowEntryToOnMoreIOData(); stream_->Start(this); - // Tell the event handler that we are now playing. - handler_->OnPlaying(this); + // Tell the event handler that we are now playing, and also start the silence + // detection notifications. + handler_->OnPlaying(); + silence_detector_->Start( + base::Bind(&EventHandler::OnAudible, base::Unretained(handler_))); } void AudioOutputController::StopStream() { @@ -203,6 +223,8 @@ void AudioOutputController::StopStream() { } else if (state_ == kPlaying) { stream_->Stop(); DisallowEntryToOnMoreIOData(); + silence_detector_->Stop(true); + silence_detector_.reset(); state_ = kPaused; } } @@ -218,7 +240,7 @@ void AudioOutputController::DoPause() { // Send a special pause mark to the low-latency audio thread. sync_reader_->UpdatePendingBytes(kPauseMark); - handler_->OnPaused(this); + handler_->OnPaused(); } void AudioOutputController::DoFlush() { @@ -259,7 +281,7 @@ void AudioOutputController::DoSetVolume(double volume) { void AudioOutputController::DoReportError(int code) { DCHECK(message_loop_->BelongsToCurrentThread()); if (state_ != kClosed) - handler_->OnError(this, code); + handler_->OnError(code); } int AudioOutputController::OnMoreData(AudioBus* dest, @@ -288,6 +310,8 @@ int AudioOutputController::OnMoreIOData(AudioBus* source, sync_reader_->UpdatePendingBytes( buffers_state.total_bytes() + frames * params_.GetBytesPerFrame()); + silence_detector_->Scan(dest, frames); + AllowEntryToOnMoreIOData(); return frames; } diff --git a/media/audio/audio_output_controller.h b/media/audio/audio_output_controller.h index e8fae22..cfb685f 100644 --- a/media/audio/audio_output_controller.h +++ b/media/audio/audio_output_controller.h @@ -8,7 +8,9 @@ #include "base/atomic_ref_count.h" #include "base/callback.h" #include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" #include "base/memory/weak_ptr.h" +#include "base/timer.h" #include "media/audio/audio_buffers_state.h" #include "media/audio/audio_io.h" #include "media/audio/audio_manager.h" @@ -57,6 +59,8 @@ class MessageLoop; namespace media { +class AudioSilenceDetector; + class MEDIA_EXPORT AudioOutputController : public base::RefCountedThreadSafe<AudioOutputController>, public AudioOutputStream::AudioSourceCallback, @@ -67,12 +71,12 @@ class MEDIA_EXPORT AudioOutputController // following methods are called on the audio manager thread. class MEDIA_EXPORT EventHandler { public: - virtual void OnCreated(AudioOutputController* controller) = 0; - virtual void OnPlaying(AudioOutputController* controller) = 0; - virtual void OnPaused(AudioOutputController* controller) = 0; - virtual void OnError(AudioOutputController* controller, int error_code) = 0; - virtual void OnDeviceChange(AudioOutputController* controller, - int new_buffer_size, int new_sample_rate) = 0; + virtual void OnCreated() = 0; + virtual void OnPlaying() = 0; + virtual void OnAudible(bool is_audible) = 0; + virtual void OnPaused() = 0; + virtual void OnError(int error_code) = 0; + virtual void OnDeviceChange(int new_buffer_size, int new_sample_rate) = 0; protected: virtual ~EventHandler() {} @@ -193,6 +197,10 @@ class MEDIA_EXPORT AudioOutputController void DoStartDiverting(AudioOutputStream* to_stream); void DoStopDiverting(); + // Called at regular intervals during playback to check for a change in + // silence and call EventHandler::OnAudible() when state changes occur. + void MaybeInvokeAudibleCallback(); + // Helper methods that start/stop physical stream. void StartStream(); void StopStream(); @@ -245,6 +253,11 @@ class MEDIA_EXPORT AudioOutputController // (when starting-up a stream). base::WeakPtrFactory<AudioOutputController> weak_this_; + // Scans audio samples from OnMoreIOData() as input and causes + // EventHandler::OnAudbile() to be called whenever a transition to a period of + // silence or non-silence is detected. + scoped_ptr<AudioSilenceDetector> silence_detector_; + DISALLOW_COPY_AND_ASSIGN(AudioOutputController); }; diff --git a/media/audio/audio_output_controller_unittest.cc b/media/audio/audio_output_controller_unittest.cc index 944d661..699810f 100644 --- a/media/audio/audio_output_controller_unittest.cc +++ b/media/audio/audio_output_controller_unittest.cc @@ -38,13 +38,13 @@ class MockAudioOutputControllerEventHandler public: MockAudioOutputControllerEventHandler() {} - MOCK_METHOD1(OnCreated, void(AudioOutputController* controller)); - MOCK_METHOD1(OnPlaying, void(AudioOutputController* controller)); - MOCK_METHOD1(OnPaused, void(AudioOutputController* controller)); - MOCK_METHOD2(OnError, void(AudioOutputController* controller, - int error_code)); - MOCK_METHOD3(OnDeviceChange, void(AudioOutputController* controller, - int new_buffer_size, int new_sample_rate)); + MOCK_METHOD0(OnCreated, void()); + MOCK_METHOD0(OnPlaying, void()); + MOCK_METHOD1(OnAudible, void(bool is_audible)); + MOCK_METHOD0(OnPaused, void()); + MOCK_METHOD1(OnError, void(int error_code)); + MOCK_METHOD2(OnDeviceChange, void(int new_buffer_size, int new_sample_rate)); + private: DISALLOW_COPY_AND_ASSIGN(MockAudioOutputControllerEventHandler); }; @@ -117,7 +117,7 @@ class AudioOutputControllerTest : public testing::Test { kSampleRate, kBitsPerSample, samples_per_packet); if (params_.IsValid()) { - EXPECT_CALL(mock_event_handler_, OnCreated(NotNull())) + EXPECT_CALL(mock_event_handler_, OnCreated()) .WillOnce(SignalEvent(&create_event_)); } @@ -131,14 +131,17 @@ class AudioOutputControllerTest : public testing::Test { } void Play() { - // Expect the event handler to receive one OnPlaying() call. - EXPECT_CALL(mock_event_handler_, OnPlaying(NotNull())) + // Expect the event handler to receive one OnPlaying() call and one or more + // OnAudible() calls. + EXPECT_CALL(mock_event_handler_, OnPlaying()) .WillOnce(SignalEvent(&play_event_)); + EXPECT_CALL(mock_event_handler_, OnAudible(_)) + .Times(AtLeast(1)); // During playback, the mock pretends to provide audio data rendered and // sent from the render process. EXPECT_CALL(mock_sync_reader_, UpdatePendingBytes(_)) - .Times(AtLeast(2)); + .Times(AtLeast(1)); EXPECT_CALL(mock_sync_reader_, Read(_, _)) .WillRepeatedly(DoAll(PopulateBuffer(), SignalEvent(&read_event_), @@ -151,7 +154,7 @@ class AudioOutputControllerTest : public testing::Test { void Pause() { // Expect the event handler to receive one OnPaused() call. - EXPECT_CALL(mock_event_handler_, OnPaused(NotNull())) + EXPECT_CALL(mock_event_handler_, OnPaused()) .WillOnce(SignalEvent(&pause_event_)); controller_->Pause(); @@ -160,9 +163,9 @@ class AudioOutputControllerTest : public testing::Test { void ChangeDevice() { // Expect the event handler to receive one OnPaying() call and no OnPaused() // call. - EXPECT_CALL(mock_event_handler_, OnPlaying(NotNull())) + EXPECT_CALL(mock_event_handler_, OnPlaying()) .WillOnce(SignalEvent(&play_event_)); - EXPECT_CALL(mock_event_handler_, OnPaused(NotNull())) + EXPECT_CALL(mock_event_handler_, OnPaused()) .Times(0); // Simulate a device change event to AudioOutputController from the @@ -176,7 +179,7 @@ class AudioOutputControllerTest : public testing::Test { if (was_playing) { // Expect the handler to receive one OnPlaying() call as a result of the // stream switching. - EXPECT_CALL(mock_event_handler_, OnPlaying(NotNull())) + EXPECT_CALL(mock_event_handler_, OnPlaying()) .WillOnce(SignalEvent(&play_event_)); } @@ -208,7 +211,7 @@ class AudioOutputControllerTest : public testing::Test { if (was_playing) { // Expect the handler to receive one OnPlaying() call as a result of the // stream switching back. - EXPECT_CALL(mock_event_handler_, OnPlaying(NotNull())) + EXPECT_CALL(mock_event_handler_, OnPlaying()) .WillOnce(SignalEvent(&play_event_)); } diff --git a/media/audio/audio_silence_detector.cc b/media/audio/audio_silence_detector.cc new file mode 100644 index 0000000..4f4bc06 --- /dev/null +++ b/media/audio/audio_silence_detector.cc @@ -0,0 +1,134 @@ +// 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/float_util.h" +#include "base/time.h" +#include "media/base/audio_bus.h" + +using base::AtomicRefCountDec; +using base::AtomicRefCountInc; +using base::AtomicRefCountIsOne; +using base::AtomicRefCountIsZero; + +namespace media { + +AudioSilenceDetector::AudioSilenceDetector( + int sample_rate, + const base::TimeDelta& questionable_silence_period, + float indistinguishable_silence_threshold) + : polling_period_(questionable_silence_period), + frames_before_observing_silence_( + sample_rate * questionable_silence_period.InSecondsF()), + silence_threshold_(indistinguishable_silence_threshold), + frames_silent_so_far_(frames_before_observing_silence_), + observing_silence_(1), + was_audible_(false) { +} + +AudioSilenceDetector::~AudioSilenceDetector() { + DCHECK(thread_checker_.CalledOnValidThread()); + // Note: If active, ~RepeatingTimer() will StopAndAbandon(). +} + +void AudioSilenceDetector::Start(const AudibleCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(notify_is_audible_.is_null()); + DCHECK(!callback.is_null()); + + notify_is_audible_ = callback; + was_audible_ = AtomicRefCountIsZero(&observing_silence_); + notify_is_audible_.Run(was_audible_); + poll_timer_.Start( + FROM_HERE, polling_period_, + this, &AudioSilenceDetector::MaybeInvokeAudibleCallback); +} + +void AudioSilenceDetector::Stop(bool notify_ending_in_silence) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(!notify_is_audible_.is_null()); + + poll_timer_.Stop(); + if (notify_ending_in_silence) + notify_is_audible_.Run(false); + notify_is_audible_.Reset(); +} + +void AudioSilenceDetector::Scan(const AudioBus* buffer, int frames) { + // Determine whether the frames just read are probably silence. If enough + // frames of silence have been observed, flip the |observing_silence_| + // boolean, which will be read by another thread. + if (ProbablyContainsSilence(buffer, frames)) { + // Note: Prevent indefinite incrementing of |frames_silent_so_far_|, to + // avoid eventual integer overflow. + if (frames_silent_so_far_ < frames_before_observing_silence_) { + frames_silent_so_far_ += frames; + if (frames_silent_so_far_ >= frames_before_observing_silence_) { + DCHECK(AtomicRefCountIsZero(&observing_silence_)); + AtomicRefCountInc(&observing_silence_); + } + } + } else { + if (frames_silent_so_far_ >= frames_before_observing_silence_) { + DCHECK(AtomicRefCountIsOne(&observing_silence_)); + AtomicRefCountDec(&observing_silence_); + } + frames_silent_so_far_ = 0; + } +} + +void AudioSilenceDetector::MaybeInvokeAudibleCallback() { + DCHECK(thread_checker_.CalledOnValidThread()); + + const bool is_now_audible = AtomicRefCountIsZero(&observing_silence_); + if (was_audible_ && !is_now_audible) + notify_is_audible_.Run(was_audible_ = false); + else if (!was_audible_ && is_now_audible) + notify_is_audible_.Run(was_audible_ = true); +} + +bool AudioSilenceDetector::ProbablyContainsSilence(const AudioBus* buffer, + int num_frames) { + if (!buffer) + return true; + DCHECK_LE(num_frames, buffer->frames()); + if (buffer->frames() <= 0) + return true; + + // Scan the data in each channel. If any one channel contains sound whose + // range of values exceeds |silence_threshold_|, return false immediately. + for (int i = 0; i < buffer->channels(); ++i) { + // Examine the 1st, 2nd (+1), 4th (+2), 7th (+3), 11th (+4), etc. samples, + // checking whether |silence_threshold_| has been breached each time. For + // typical AudioBus sizes, this algorithm will in the worst case examine + // fewer than 10% of the samples. + // + // Note that there *is* a heavy bias in sampling at the beginning of the + // channels, but that doesn't matter. The buffer sizes are simply too + // small. For example, it is commonplace to use 128-sample buffers, which + // represents ~3 ms of audio at 44.1 kHz; and this means that frequencies + // below ~350 Hz will span more than one buffer to make a full cycle. In + // all, the algorithm here is meant to be dirt-simple math that isn't + // susceptible to periodic bias within a single buffer. + const float* p = buffer->channel(i); + const float* const end_of_samples = p + num_frames; + int skip = 1; + float min_value = *p; + float max_value = *p; + for (p += skip; p < end_of_samples; ++skip, p += skip) { + DCHECK(base::IsFinite(*p)); + if (*p < min_value) + min_value = *p; + else if (max_value < *p) + max_value = *p; + if ((max_value - min_value) > silence_threshold_) + return false; + } + } + + return true; +} + +} // namespace media diff --git a/media/audio/audio_silence_detector.h b/media/audio/audio_silence_detector.h new file mode 100644 index 0000000..b6c52a7 --- /dev/null +++ b/media/audio/audio_silence_detector.h @@ -0,0 +1,112 @@ +// 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. + +#ifndef MEDIA_AUDIO_AUDIO_SILENCE_DETECTOR_H_ +#define MEDIA_AUDIO_AUDIO_SILENCE_DETECTOR_H_ + +#include "base/atomic_ref_count.h" +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/thread_checker.h" +#include "base/timer.h" +#include "media/base/media_export.h" + +// An audio silence detector. It is periodically provided an AudioBus by the +// native audio thread, where simple logic determines whether the audio samples +// in each channel in the buffer represent silence. If a long-enough period of +// contiguous silence is observed in all channels, a notification callback is +// run on the thread that constructed AudioSilenceDetector. +// +// Note that extreme care has been taken to make the +// AudioSilenceDetector::Scan() method safe to be called on the native audio +// thread. The code acquires no locks, nor engages in any operation that could +// result in an undetermined/unbounded amount of run-time. Comments in +// audio_silence_detector.cc elaborate further on the silence detection +// algorithm. + +namespace base { +class TimeDelta; +} + +namespace media { + +class AudioBus; + +class MEDIA_EXPORT AudioSilenceDetector { + public: + typedef base::Callback<void(bool)> AudibleCallback; + + // Tunable parameters: |questionable_silence_period| is the amount of time + // where audio must remain silent before triggerring a callback. + // |indistinguishable_silence_threshold| is the value range below which audio + // is considered silent, in full-scale units. + // + // TODO(miu): |indistinguishable_silence_threshold| should be specified in + // dbFS units rather than full-scale. We need a dbFS data type for + // media/audio first. + AudioSilenceDetector(int sample_rate, + const base::TimeDelta& questionable_silence_period, + float indistinguishable_silence_threshold); + + ~AudioSilenceDetector(); + + // Start detecting silence, notifying via the given callback. + void Start(const AudibleCallback& notify_is_audible); + + // Stop detecting silence. If |notify_ending_in_silence| is true, a final + // notify_is_audible(false) call will be made here. + void Stop(bool notify_ending_in_silence); + + // Scan more |frames| of audio data from |buffer|. This is usually called + // within the native audio thread's "more data" callback. + void Scan(const AudioBus* buffer, int frames); + + private: + // Called by |poll_timer_| at regular intervals to determine whether to invoke + // the callback due to a silence state change. + void MaybeInvokeAudibleCallback(); + + // Returns true if the first |num_frames| frames in all channels in |buffer| + // probably contain silence. A simple heuristic is used to quickly examine a + // subset of the samples in each channel, hence the name of this method. + // "Silence" means that the range of the sample values examined does not + // exceed |silence_threshold_|. + bool ProbablyContainsSilence(const AudioBus* buffer, int num_frames); + + // Time between polls for changes in state. + const base::TimeDelta polling_period_; + + // Number of frames of contiguous silence to be observed before setting + // |observing_silence_| to false. + const int frames_before_observing_silence_; + + // Threshold below which audio should be considered indistinguishably silent. + const float silence_threshold_; + + // Number of frames of contiguous silence observed thus far on the native + // audio thread. + int frames_silent_so_far_; + + // Boolean state (0 or 1) set by the native audio thread. This is polled + // regularly by the thread that invokes the callback. + base::AtomicRefCount observing_silence_; + + // Callback for notifying of a detected transition to silence or non-silence. + AudibleCallback notify_is_audible_; + + // Last reported audible state, used for de-duping callback invocations. + bool was_audible_; + + // Fires regularly, calling MaybeInvokeAudibleCallback(). + base::RepeatingTimer<AudioSilenceDetector> poll_timer_; + + // Constructor, destructor and most methods must be called on the same thread. + base::ThreadChecker thread_checker_; + + DISALLOW_COPY_AND_ASSIGN(AudioSilenceDetector); +}; + +} // namespace media + +#endif // MEDIA_AUDIO_AUDIO_SILENCE_DETECTOR_H_ diff --git a/media/audio/audio_silence_detector_unittest.cc b/media/audio/audio_silence_detector_unittest.cc new file mode 100644 index 0000000..52e6958 --- /dev/null +++ b/media/audio/audio_silence_detector_unittest.cc @@ -0,0 +1,227 @@ +// 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.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<TestScenario> { + 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<AudioBus> CreatePopulatedBuffer( + const float* data, int num_channels, int num_frames) { + scoped_ptr<AudioBus> 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<base::Thread> audio_manager_thread_; + scoped_refptr<base::MessageLoopProxy> audio_message_loop_; + + base::WaitableEvent notification_received_; + + scoped_ptr<AudioSilenceDetector> 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<AudioBus> 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 |