diff options
author | mukai@chromium.org <mukai@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-31 21:36:46 +0000 |
---|---|---|
committer | mukai@chromium.org <mukai@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-31 21:36:46 +0000 |
commit | 87f7ab4c5f2f608b1abbab78fd9d524c51e79d83 (patch) | |
tree | f2deaab641d079e96e1c752ad98eb0c72f6a59ce /chrome/browser/notifications | |
parent | 914816dfb279c4b9f07235f26ff511a3598ffe8f (diff) | |
download | chromium_src-87f7ab4c5f2f608b1abbab78fd9d524c51e79d83.zip chromium_src-87f7ab4c5f2f608b1abbab78fd9d524c51e79d83.tar.gz chromium_src-87f7ab4c5f2f608b1abbab78fd9d524c51e79d83.tar.bz2 |
Introduces NotificationAudioManager.
This CL does the following things:
- allow to access media::AudioManager from chrome/browser
- .wav format parser
- NotificationAudioManager class, which manages playing sound by a resource id (representing wav file format data)
BUG=164248
TEST=with crrev.com/12663010, a notification will cause a sound
Review URL: https://chromiumcodereview.appspot.com/12287003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@203482 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/notifications')
3 files changed, 578 insertions, 0 deletions
diff --git a/chrome/browser/notifications/notification_audio_controller.cc b/chrome/browser/notifications/notification_audio_controller.cc new file mode 100644 index 0000000..213f9c4 --- /dev/null +++ b/chrome/browser/notifications/notification_audio_controller.cc @@ -0,0 +1,383 @@ +// Copyright 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 "chrome/browser/notifications/notification_audio_controller.h" + +#include <cstring> + +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/cancelable_callback.h" +#include "base/location.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/sys_byteorder.h" +#include "base/task_runner_util.h" +#include "media/audio/audio_io.h" +#include "media/audio/audio_manager.h" +#include "media/audio/audio_parameters.h" +#include "media/base/audio_bus.h" +#include "media/base/channel_layout.h" + +namespace { + +// The size of the header of a wav file. The header consists of 'RIFF', 4 bytes +// of total data length, and 'WAVE'. +const size_t kWavFileHeaderSize = 12; + +// The size of a chunk header in wav file format. A chunk header consists of a +// tag ('fmt ' or 'data') and 4 bytes of chunk length. +const size_t kChunkHeaderSize = 8; + +// The minimum size of 'fmt' chunk. +const size_t kFmtChunkMinimumSize = 16; + +// The offsets of 'fmt' fields. +const size_t kAudioFormatOffset = 0; +const size_t kChunnelOffset = 2; +const size_t kSampleRateOffset = 4; +const size_t kBitsPerSampleOffset = 14; + +// Some constants for audio format. +const int kAudioFormatPCM = 1; + +// The frame-per-buffer parameter for wav data format reader. Since wav format +// doesn't have this parameter in its header, just choose some value. +const int kFramesPerBuffer = 4096; + +/////////////////////////////////////////////////////////////////////////// +// WavAudioHandler +// +// This class provides the input from wav file format. +// See https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ +class WavAudioHandler { + public: + explicit WavAudioHandler(const base::StringPiece& data); + virtual ~WavAudioHandler(); + + // Returns true when it plays to the end of the track. + bool AtEnd(); + + // Copies the audio data to |dest|. + void CopyTo(media::AudioBus* dest); + + uint16 num_channels() const { return num_channels_; } + uint32 sample_rate() const { return sample_rate_; } + uint16 bits_per_sample() const { return bits_per_sample_; } + + private: + // Parses a chunk of wav format data. Returns the length of the chunk. + int ParseSubChunk(const base::StringPiece& data); + + // Parses the 'fmt' section chunk and stores |params_|. + void ParseFmtChunk(const base::StringPiece& data); + + // Parses the 'data' section chunk and stores |data_|. + void ParseDataChunk(const base::StringPiece& data); + + // Reads an integer from |data| with |offset|. + template<typename T> T ReadInt(const base::StringPiece& data, size_t offset); + + base::StringPiece data_; + size_t cursor_; + uint16 num_channels_; + uint32 sample_rate_; + uint16 bits_per_sample_; + + DISALLOW_COPY_AND_ASSIGN(WavAudioHandler); +}; + +WavAudioHandler::WavAudioHandler(const base::StringPiece& data) + : cursor_(0) { + if (data.size() < kWavFileHeaderSize) + return; + + if (!data.starts_with("RIFF") || ::memcmp(data.data() + 8, "WAVE", 4) != 0) + return; + + uint32 total_length = + std::min(ReadInt<uint32>(data, 4), static_cast<uint32>(data.size())); + + uint32 offset = kWavFileHeaderSize; + while (offset < total_length) + offset += ParseSubChunk(data.substr(offset)); +} + +WavAudioHandler::~WavAudioHandler() { +} + +bool WavAudioHandler::AtEnd() { + return data_.size() <= cursor_; +} + +void WavAudioHandler::CopyTo(media::AudioBus* dest) { + DCHECK_EQ(dest->channels(), num_channels_); + if (cursor_ >= data_.size()) { + dest->Zero(); + return; + } + + const int bytes_per_sample = bits_per_sample_ / 8; + const int bytes_per_frame = num_channels_ * bytes_per_sample; + const int remaining_frames = (data_.size() - cursor_) / bytes_per_frame; + const int frames = std::min(dest->frames(), remaining_frames); + dest->FromInterleaved(data_.data() + cursor_, frames, bytes_per_sample); + cursor_ += frames * bytes_per_frame; + dest->ZeroFramesPartial(frames, dest->frames() - frames); +} + +int WavAudioHandler::ParseSubChunk(const base::StringPiece& data) { + if (data.size() < kChunkHeaderSize) + return data.size(); + uint32 chunk_length = ReadInt<uint32>(data, 4); + + if (data.starts_with("fmt ")) + ParseFmtChunk(data.substr(kChunkHeaderSize, chunk_length)); + else if (data.starts_with("data")) + ParseDataChunk(data.substr(kChunkHeaderSize, chunk_length)); + else + DLOG(WARNING) << "Unknown data chunk: " << data.substr(0, 4); + return chunk_length + kChunkHeaderSize; +} + +void WavAudioHandler::ParseFmtChunk(const base::StringPiece& data) { + if (data.size() < kFmtChunkMinimumSize) { + DLOG(ERROR) << "data size " << data.size() << " is too short."; + return; + } + + DCHECK_EQ(ReadInt<uint16>(data, kAudioFormatOffset), kAudioFormatPCM); + + num_channels_ = ReadInt<uint16>(data, kChunnelOffset); + sample_rate_ = ReadInt<uint32>(data, kSampleRateOffset); + bits_per_sample_ = ReadInt<uint16>(data, kBitsPerSampleOffset); +} + +void WavAudioHandler::ParseDataChunk(const base::StringPiece& data) { + data_ = data; +} + +template<typename T> T WavAudioHandler::ReadInt(const base::StringPiece& data, + size_t offset) { + DCHECK_LE(offset + sizeof(T), data.size()); + T result; + ::memcpy(&result, data.data() + offset, sizeof(T)); +#if !defined(ARCH_CPU_LITTLE_ENDIAN) + result = base::ByteSwap(result); +#endif + + return result; +} + +} // namespace + +/////////////////////////////////////////////////////////////////////////// +// NotificationAudioController::AudioHandler +// +// This class connects a sound for a notification to the audio manager. It +// will be released by its owner when the sound ends. +class NotificationAudioController::AudioHandler + : public media::AudioOutputStream::AudioSourceCallback { + public: + typedef base::Callback<void(AudioHandler*)> OnFinishedCB; + + AudioHandler(const OnFinishedCB& on_finished_callback, + const std::string& notification_id, + const Profile* profile, + const base::StringPiece& data); + virtual ~AudioHandler(); + + const std::string& notification_id() const { return notification_id_; } + const Profile* profile() const { return profile_; } + + // Start playing the sound. Returns true when it's successfully scheduled. + bool StartPlayingSound(); + + // Stops the playing sound request. + void StopPlayingSound(); + + private: + // media::AudioOutputStream::AudioSourceCallback overrides: + virtual int OnMoreData(media::AudioBus* dest, + media::AudioBuffersState state) OVERRIDE; + virtual int OnMoreIOData(media::AudioBus* source, + media::AudioBus* dest, + media::AudioBuffersState state) OVERRIDE; + virtual void OnError(media::AudioOutputStream* stream) OVERRIDE; + + OnFinishedCB on_finished_callback_; + base::CancelableClosure stop_playing_sound_; + std::string notification_id_; + const Profile* profile_; + + media::AudioManager* audio_manager_; + WavAudioHandler wav_audio_; + media::AudioOutputStream* stream_; + + DISALLOW_COPY_AND_ASSIGN(AudioHandler); +}; + +NotificationAudioController::AudioHandler::AudioHandler( + const OnFinishedCB& on_finished_callback, + const std::string& notification_id, + const Profile* profile, + const base::StringPiece& data) + : on_finished_callback_(on_finished_callback), + notification_id_(notification_id), + profile_(profile), + audio_manager_(media::AudioManager::Get()), + wav_audio_(data) { +} + +NotificationAudioController::AudioHandler::~AudioHandler() { +} + +bool NotificationAudioController::AudioHandler::StartPlayingSound() { + DCHECK(audio_manager_->GetMessageLoop()->BelongsToCurrentThread()); + + media::AudioParameters params = media::AudioParameters( + media::AudioParameters::AUDIO_PCM_LOW_LATENCY, + media::GuessChannelLayout(wav_audio_.num_channels()), + wav_audio_.num_channels(), + wav_audio_.sample_rate(), + wav_audio_.bits_per_sample(), + kFramesPerBuffer); + + if (!params.IsValid()) + return false; + + stream_ = audio_manager_->MakeAudioOutputStreamProxy(params); + if (!stream_->Open()) { + DLOG(ERROR) << "Failed to open the output stream"; + stream_->Close(); + return false; + } + + stop_playing_sound_.Reset(base::Bind( + &AudioHandler::StopPlayingSound, base::Unretained(this))); + stream_->Start(this); + return true; +} + +// Stops actually the audio output stream. +void NotificationAudioController::AudioHandler::StopPlayingSound() { + DCHECK(audio_manager_->GetMessageLoop()->BelongsToCurrentThread()); + stop_playing_sound_.Cancel(); + + // Close deletes |stream| too. + stream_->Stop(); + stream_->Close(); + on_finished_callback_.Run(this); +} + +int NotificationAudioController::AudioHandler::OnMoreData( + media::AudioBus* dest, + media::AudioBuffersState state) { + wav_audio_.CopyTo(dest); + if (wav_audio_.AtEnd()) { + audio_manager_->GetMessageLoop()->PostTask( + FROM_HERE, stop_playing_sound_.callback()); + } + return dest->frames(); +} + +int NotificationAudioController::AudioHandler::OnMoreIOData( + media::AudioBus* source, + media::AudioBus* dest, + media::AudioBuffersState state) { + return OnMoreData(dest, state); +} + +void NotificationAudioController::AudioHandler::OnError( + media::AudioOutputStream* stream) { +} + +/////////////////////////////////////////////////////////////////////////// +// NotificationAudioController +// + +NotificationAudioController::NotificationAudioController() + : message_loop_(media::AudioManager::Get()->GetMessageLoop()) { +} + +NotificationAudioController::~NotificationAudioController() { + DCHECK(message_loop_->BelongsToCurrentThread()); +} + +void NotificationAudioController::RequestPlayNotificationSound( + const std::string& notification_id, + const Profile* profile, + const base::StringPiece& data) { + message_loop_->PostTask( + FROM_HERE, + base::Bind( + &NotificationAudioController::PlayNotificationSound, + base::Unretained(this), + notification_id, + base::Unretained(profile), + data)); +} + +void NotificationAudioController::RequestShutdown() { + message_loop_->PostTask( + FROM_HERE, + base::Bind( + &NotificationAudioController::Shutdown, base::Unretained(this))); +} + +void NotificationAudioController::OnNotificationSoundFinished( + AudioHandler* audio_handler) { + DCHECK(message_loop_->BelongsToCurrentThread()); + std::vector<AudioHandler*>::iterator it = + std::find(audio_handlers_.begin(), audio_handlers_.end(), audio_handler); + DCHECK(it != audio_handlers_.end()); + audio_handlers_.erase(it); +} + +void NotificationAudioController::GetAudioHandlersSizeForTest( + const base::Callback<void(size_t)>& on_get) { + base::PostTaskAndReplyWithResult( + message_loop_.get(), + FROM_HERE, + base::Bind(&NotificationAudioController::GetAudioHandlersSizeCallback, + base::Unretained(this)), + on_get); +} + +void NotificationAudioController::PlayNotificationSound( + const std::string& notification_id, + const Profile* profile, + const base::StringPiece& data) { + DCHECK(message_loop_->BelongsToCurrentThread()); + + for (size_t i = 0; i < audio_handlers_.size(); ++i) { + if (audio_handlers_[i]->notification_id() == notification_id && + audio_handlers_[i]->profile() == profile) { + audio_handlers_[i]->StopPlayingSound(); + break; + } + } + + scoped_ptr<AudioHandler> new_handler(new AudioHandler( + base::Bind(&NotificationAudioController::OnNotificationSoundFinished, + base::Unretained(this)), + notification_id, + profile, + data)); + if (new_handler->StartPlayingSound()) + audio_handlers_.push_back(new_handler.release()); +} + +void NotificationAudioController::Shutdown() { + DCHECK(message_loop_->BelongsToCurrentThread()); + + while (!audio_handlers_.empty()) + audio_handlers_[0]->StopPlayingSound(); + + delete this; +} + +size_t NotificationAudioController::GetAudioHandlersSizeCallback() { + DCHECK(message_loop_->BelongsToCurrentThread()); + return audio_handlers_.size(); +} diff --git a/chrome/browser/notifications/notification_audio_controller.h b/chrome/browser/notifications/notification_audio_controller.h new file mode 100644 index 0000000..fb370b3 --- /dev/null +++ b/chrome/browser/notifications/notification_audio_controller.h @@ -0,0 +1,73 @@ +// Copyright 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 CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_AUDIO_CONTROLLER_H_ +#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_AUDIO_CONTROLLER_H_ + +#include <string> + +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_vector.h" +#include "base/string_piece.h" + +namespace base { +class MessageLoopProxy; +} + +class Profile; + +// This class controls the sound playing for notifications. Note that this class +// belongs to the audio thread and self-owned. +class NotificationAudioController { + public: + NotificationAudioController(); + + // Requests the audio thread to play a sound for a notification. |data| + // specifies the raw audio data in wav file format. |notification_id| + // and |profile| represents the id of the notification for the sound. + // When this method is called during playing sound for the specified + // |notification_id|, it will stop playing the sound and start the sound for + // the newly specified |data|. + void RequestPlayNotificationSound( + const std::string& notification_id, + const Profile* profile, + const base::StringPiece& data); + + // Request shutdown process in the audio thread. Delete itself when all + // processes have finished. + void RequestShutdown(); + + private: + class AudioHandler; + friend class NotificationAudioControllerTest; + + ~NotificationAudioController(); + + // Request the current number of audio handlers to the audio thread. |on_get| + // will be called with the result when finished. + void GetAudioHandlersSizeForTest(const base::Callback<void(size_t)>& on_get); + + // Actually start playing the notification sound in the audio thread. + void PlayNotificationSound( + const std::string& notification_id, + const Profile* profile, + const base::StringPiece& data); + + // Removes all instances in |audio_handlers_|. + void Shutdown(); + + // Gets the current size of |audio_handlers_| in the audio thread. + size_t GetAudioHandlersSizeCallback(); + + // Called when the sound for |audio_handler| has finished. + void OnNotificationSoundFinished(AudioHandler* audio_handler); + + scoped_refptr<base::MessageLoopProxy> message_loop_; + ScopedVector<AudioHandler> audio_handlers_; + + DISALLOW_COPY_AND_ASSIGN(NotificationAudioController); +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_AUDIO_CONTROLLER_H_ diff --git a/chrome/browser/notifications/notification_audio_controller_unittest.cc b/chrome/browser/notifications/notification_audio_controller_unittest.cc new file mode 100644 index 0000000..2126722 --- /dev/null +++ b/chrome/browser/notifications/notification_audio_controller_unittest.cc @@ -0,0 +1,122 @@ +// Copyright 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 "chrome/browser/notifications/notification_audio_controller.h" + +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "base/run_loop.h" +#include "chrome/test/base/testing_profile.h" +#include "media/audio/audio_manager.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// Sample audio data +const char kTestAudioData[] = "RIFF\x26\x00\x00\x00WAVEfmt \x10\x00\x00\x00" + "\x01\x00\x02\x00\x80\xbb\x00\x00\x00\x77\x01\x00\x02\x00\x10\x00" + "data\x04\x00\x00\x00\x01\x00\x01\x00"; +const char kInvalidAudioData[] = "invalid audio data"; +const char kDummyNotificationId[] = "dummy_id"; +const char kDummyNotificationId2[] = "dummy_id2"; + +void CopyResultAndQuit( + base::RunLoop* run_loop, size_t *result_ptr, size_t result) { + *result_ptr = result; + run_loop->Quit(); +} + +} // namespace + +class NotificationAudioControllerTest : public testing::Test { + public: + NotificationAudioControllerTest() { + audio_manager_.reset(media::AudioManager::Create()); + notification_audio_controller_ = new NotificationAudioController(); + } + + virtual ~NotificationAudioControllerTest() { + notification_audio_controller_->RequestShutdown(); + audio_manager_.reset(); + } + + protected: + NotificationAudioController* notification_audio_controller() { + return notification_audio_controller_; + } + Profile* profile() { return &profile_; } + + size_t GetHandlersSize() { + base::RunLoop run_loop; + size_t result = 0; + notification_audio_controller_->GetAudioHandlersSizeForTest( + base::Bind(&CopyResultAndQuit, &run_loop, &result)); + run_loop.Run(); + return result; + } + + private: + base::MessageLoopForUI message_loop_; + TestingProfile profile_; + NotificationAudioController* notification_audio_controller_; + scoped_ptr<media::AudioManager> audio_manager_; + + DISALLOW_COPY_AND_ASSIGN(NotificationAudioControllerTest); +}; + +TEST_F(NotificationAudioControllerTest, PlaySound) { + notification_audio_controller()->RequestPlayNotificationSound( + kDummyNotificationId, + profile(), + base::StringPiece(kTestAudioData, arraysize(kTestAudioData))); + // Still playing the sound, not empty yet. + EXPECT_EQ(1u, GetHandlersSize()); +} + +TEST_F(NotificationAudioControllerTest, PlayInvalidSound) { + notification_audio_controller()->RequestPlayNotificationSound( + kDummyNotificationId, + profile(), + base::StringPiece(kInvalidAudioData, arraysize(kInvalidAudioData))); + EXPECT_EQ(0u, GetHandlersSize()); +} + +TEST_F(NotificationAudioControllerTest, PlayTwoSounds) { + notification_audio_controller()->RequestPlayNotificationSound( + kDummyNotificationId, + profile(), + base::StringPiece(kTestAudioData, arraysize(kTestAudioData))); + notification_audio_controller()->RequestPlayNotificationSound( + kDummyNotificationId2, + profile(), + base::StringPiece(kTestAudioData, arraysize(kTestAudioData))); + EXPECT_EQ(2u, GetHandlersSize()); +} + +TEST_F(NotificationAudioControllerTest, PlaySoundTwice) { + notification_audio_controller()->RequestPlayNotificationSound( + kDummyNotificationId, + profile(), + base::StringPiece(kTestAudioData, arraysize(kTestAudioData))); + notification_audio_controller()->RequestPlayNotificationSound( + kDummyNotificationId, + profile(), + base::StringPiece(kTestAudioData, arraysize(kTestAudioData))); + EXPECT_EQ(1u, GetHandlersSize()); +} + +TEST_F(NotificationAudioControllerTest, MultiProfiles) { + TestingProfile profile2; + + notification_audio_controller()->RequestPlayNotificationSound( + kDummyNotificationId, + profile(), + base::StringPiece(kTestAudioData, arraysize(kTestAudioData))); + notification_audio_controller()->RequestPlayNotificationSound( + kDummyNotificationId, + &profile2, + base::StringPiece(kTestAudioData, arraysize(kTestAudioData))); + EXPECT_EQ(2u, GetHandlersSize()); +} |