diff options
author | crogers@google.com <crogers@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-26 08:25:35 +0000 |
---|---|---|
committer | crogers@google.com <crogers@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-26 08:25:35 +0000 |
commit | e8e49bf53386d069ea5097370f881c41a0d703f7 (patch) | |
tree | d93541d62f911cb7a0c48d0554438e41f4706900 /media | |
parent | b78f96a67f2f6da212aa941d0105580ec970d67d (diff) | |
download | chromium_src-e8e49bf53386d069ea5097370f881c41a0d703f7.zip chromium_src-e8e49bf53386d069ea5097370f881c41a0d703f7.tar.gz chromium_src-e8e49bf53386d069ea5097370f881c41a0d703f7.tar.bz2 |
Implement AUHALStream using an AUHAL with configurable device selection.
We currently have three audio output back-ends on OSX:
1) AUAudioOutputStream in audio_low_latency_mac
2) AudioSynchronizedStream in audio_synchronized_mac
3) AudioHardwareUnifiedStream in audio_unified_mac
1 and 3 can be replaced by a single back-end which uses an AUHAL
Even (2) can probably use the AUHAL with an appropriately configured aggregate device.
Along with other improvements and simplifications, this CL has a chance to improve http://crbug.com/158170.
BUG=158170
TEST=audio_auhal_mac_unittest.cc
(also manually verified that repeated device changes did not crash in gdb)
Review URL: https://chromiumcodereview.appspot.com/12611022
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@190593 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media')
-rw-r--r-- | media/audio/mac/audio_auhal_mac.cc | 529 | ||||
-rw-r--r-- | media/audio/mac/audio_auhal_mac.h | 163 | ||||
-rw-r--r-- | media/audio/mac/audio_auhal_mac_unittest.cc | 218 | ||||
-rw-r--r-- | media/audio/mac/audio_device_listener_mac.cc | 14 | ||||
-rw-r--r-- | media/audio/mac/audio_device_listener_mac.h | 4 | ||||
-rw-r--r-- | media/audio/mac/audio_device_listener_mac_unittest.cc | 2 | ||||
-rw-r--r-- | media/audio/mac/audio_manager_mac.cc | 154 | ||||
-rw-r--r-- | media/audio/mac/audio_manager_mac.h | 23 | ||||
-rw-r--r-- | media/media.gyp | 3 |
9 files changed, 1025 insertions, 85 deletions
diff --git a/media/audio/mac/audio_auhal_mac.cc b/media/audio/mac/audio_auhal_mac.cc new file mode 100644 index 0000000..20df6f5 --- /dev/null +++ b/media/audio/mac/audio_auhal_mac.cc @@ -0,0 +1,529 @@ +// 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 "media/audio/mac/audio_auhal_mac.h" + +#include <CoreServices/CoreServices.h> + +#include "base/basictypes.h" +#include "base/command_line.h" +#include "base/debug/trace_event.h" +#include "base/logging.h" +#include "base/mac/mac_logging.h" +#include "media/audio/audio_util.h" +#include "media/audio/mac/audio_manager_mac.h" +#include "media/base/media_switches.h" + +namespace media { + +static std::ostream& operator<<(std::ostream& os, + const AudioStreamBasicDescription& format) { + os << "sample rate : " << format.mSampleRate << std::endl + << "format ID : " << format.mFormatID << std::endl + << "format flags : " << format.mFormatFlags << std::endl + << "bytes per packet : " << format.mBytesPerPacket << std::endl + << "frames per packet : " << format.mFramesPerPacket << std::endl + << "bytes per frame : " << format.mBytesPerFrame << std::endl + << "channels per frame: " << format.mChannelsPerFrame << std::endl + << "bits per channel : " << format.mBitsPerChannel; + return os; +} + +static void ZeroBufferList(AudioBufferList* buffer_list) { + for (size_t i = 0; i < buffer_list->mNumberBuffers; ++i) { + memset(buffer_list->mBuffers[i].mData, + 0, + buffer_list->mBuffers[i].mDataByteSize); + } +} + +static void WrapBufferList(AudioBufferList* buffer_list, + AudioBus* bus, + int frames) { + DCHECK(buffer_list); + DCHECK(bus); + const int channels = bus->channels(); + const int buffer_list_channels = buffer_list->mNumberBuffers; + DCHECK_EQ(channels, buffer_list_channels); + + // Copy pointers from AudioBufferList. + for (int i = 0; i < channels; ++i) { + bus->SetChannelData( + i, static_cast<float*>(buffer_list->mBuffers[i].mData)); + } + + // Finally set the actual length. + bus->set_frames(frames); +} + +AUHALStream::AUHALStream( + AudioManagerMac* manager, + const AudioParameters& params, + AudioDeviceID device) + : manager_(manager), + params_(params), + input_channels_(params_.input_channels()), + output_channels_(params_.channels()), + number_of_frames_(params_.frames_per_buffer()), + source_(NULL), + device_(device), + audio_unit_(0), + volume_(1), + hardware_latency_frames_(0), + stopped_(false), + input_buffer_list_(NULL) { + // We must have a manager. + DCHECK(manager_); + + DVLOG(1) << "Input channels: " << input_channels_; + DVLOG(1) << "Output channels: " << output_channels_; + DVLOG(1) << "Sample rate: " << params_.sample_rate(); + DVLOG(1) << "Buffer size: " << number_of_frames_; +} + +AUHALStream::~AUHALStream() { +} + +bool AUHALStream::Open() { + // Get the total number of input and output channels that the + // hardware supports. + int device_input_channels; + bool got_input_channels = AudioManagerMac::GetDeviceChannels( + device_, + kAudioDevicePropertyScopeInput, + &device_input_channels); + + int device_output_channels; + bool got_output_channels = AudioManagerMac::GetDeviceChannels( + device_, + kAudioDevicePropertyScopeOutput, + &device_output_channels); + + // Sanity check the requested I/O channels. + if (!got_input_channels || + input_channels_ < 0 || input_channels_ > device_input_channels) { + LOG(ERROR) << "AudioDevice does not support requested input channels."; + return false; + } + + if (!got_output_channels || + output_channels_ <= 0 || output_channels_ > device_output_channels) { + LOG(ERROR) << "AudioDevice does not support requested output channels."; + return false; + } + + // The requested sample-rate must match the hardware sample-rate. + int sample_rate = AudioManagerMac::HardwareSampleRateForDevice(device_); + + if (sample_rate != params_.sample_rate()) { + LOG(ERROR) << "Requested sample-rate: " << params_.sample_rate() + << " must match the hardware sample-rate: " << sample_rate; + return false; + } + + CreateIOBusses(); + + bool configured = ConfigureAUHAL(); + if (configured) + hardware_latency_frames_ = GetHardwareLatency(); + + return configured; +} + +void AUHALStream::Close() { + if (input_buffer_list_) { + input_buffer_list_storage_.reset(); + input_buffer_list_ = NULL; + input_bus_.reset(NULL); + output_bus_.reset(NULL); + } + + if (audio_unit_) { + AudioUnitUninitialize(audio_unit_); + AudioComponentInstanceDispose(audio_unit_); + } + + // Inform the audio manager that we have been closed. This will cause our + // destruction. + manager_->ReleaseOutputStream(this); +} + +void AUHALStream::Start(AudioSourceCallback* callback) { + DCHECK(callback); + if (!audio_unit_) { + DLOG(ERROR) << "Open() has not been called successfully"; + return; + } + + stopped_ = false; + { + base::AutoLock auto_lock(source_lock_); + source_ = callback; + } + + AudioOutputUnitStart(audio_unit_); +} + +void AUHALStream::Stop() { + if (stopped_) + return; + + AudioOutputUnitStop(audio_unit_); + + base::AutoLock auto_lock(source_lock_); + source_ = NULL; + stopped_ = true; +} + +void AUHALStream::SetVolume(double volume) { + volume_ = static_cast<float>(volume); + + // TODO(crogers): set volume property +} + +void AUHALStream::GetVolume(double* volume) { + *volume = volume_; +} + +// Pulls on our provider to get rendered audio stream. +// Note to future hackers of this function: Do not add locks which can +// be contended in the middle of stream processing here (starting and stopping +// the stream are ok) because this is running on a real-time thread. +OSStatus AUHALStream::Render( + AudioUnitRenderActionFlags* flags, + const AudioTimeStamp* output_time_stamp, + UInt32 bus_number, + UInt32 number_of_frames, + AudioBufferList* io_data) { + TRACE_EVENT0("audio", "AUHALStream::Render"); + + if (number_of_frames != number_of_frames_) { + // This can happen if we've suddenly changed sample-rates. + // The stream should be stopping very soon. + // + // Unfortunately AUAudioInputStream and AUHALStream share the frame + // size set by kAudioDevicePropertyBufferFrameSize above on a per process + // basis. What this means is that the |number_of_frames| value may be + // larger or smaller than the value set during ConfigureAUHAL(). + // In this case either audio input or audio output will be broken, + // so just output silence. + ZeroBufferList(io_data); + return noErr; + } + + if (input_channels_ > 0 && input_buffer_list_) { + // Get the input data. |input_buffer_list_| is wrapped + // to point to the data allocated in |input_bus_|. + OSStatus result = AudioUnitRender( + audio_unit_, + flags, + output_time_stamp, + 1, + number_of_frames, + input_buffer_list_); + if (result != noErr) + ZeroBufferList(input_buffer_list_); + } + + // Make |output_bus_| wrap the output AudioBufferList. + WrapBufferList(io_data, output_bus_.get(), number_of_frames); + + // Update the playout latency. + double playout_latency_frames = GetPlayoutLatency(output_time_stamp); + + uint32 hardware_pending_bytes = static_cast<uint32> + ((playout_latency_frames + 0.5) * output_format_.mBytesPerFrame); + + { + // Render() shouldn't be called except between AudioOutputUnitStart() and + // AudioOutputUnitStop() calls, but crash reports have shown otherwise: + // http://crbug.com/178765. We use |source_lock_| to prevent races and + // crashes in Render() when |source_| is cleared. + base::AutoLock auto_lock(source_lock_); + if (!source_) { + ZeroBufferList(io_data); + return noErr; + } + + // Supply the input data and render the output data. + source_->OnMoreIOData( + input_bus_.get(), + output_bus_.get(), + AudioBuffersState(0, hardware_pending_bytes)); + } + + return noErr; +} + +// AUHAL callback. +OSStatus AUHALStream::InputProc( + void* user_data, + AudioUnitRenderActionFlags* flags, + const AudioTimeStamp* output_time_stamp, + UInt32 bus_number, + UInt32 number_of_frames, + AudioBufferList* io_data) { + // Dispatch to our class method. + AUHALStream* audio_output = + static_cast<AUHALStream*>(user_data); + if (!audio_output) + return -1; + + return audio_output->Render( + flags, + output_time_stamp, + bus_number, + number_of_frames, + io_data); +} + +double AUHALStream::GetHardwareLatency() { + if (!audio_unit_ || device_ == kAudioObjectUnknown) { + DLOG(WARNING) << "AudioUnit is NULL or device ID is unknown"; + return 0.0; + } + + // Get audio unit latency. + Float64 audio_unit_latency_sec = 0.0; + UInt32 size = sizeof(audio_unit_latency_sec); + OSStatus result = AudioUnitGetProperty( + audio_unit_, + kAudioUnitProperty_Latency, + kAudioUnitScope_Global, + 0, + &audio_unit_latency_sec, + &size); + if (result != noErr) { + OSSTATUS_DLOG(WARNING, result) << "Could not get AudioUnit latency"; + return 0.0; + } + + // Get output audio device latency. + static const AudioObjectPropertyAddress property_address = { + kAudioDevicePropertyLatency, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMaster + }; + + UInt32 device_latency_frames = 0; + size = sizeof(device_latency_frames); + result = AudioObjectGetPropertyData( + device_, + &property_address, + 0, + NULL, + &size, + &device_latency_frames); + if (result != noErr) { + OSSTATUS_DLOG(WARNING, result) << "Could not get audio device latency"; + return 0.0; + } + + return static_cast<double>((audio_unit_latency_sec * + output_format_.mSampleRate) + device_latency_frames); +} + +double AUHALStream::GetPlayoutLatency( + const AudioTimeStamp* output_time_stamp) { + // Ensure mHostTime is valid. + if ((output_time_stamp->mFlags & kAudioTimeStampHostTimeValid) == 0) + return 0; + + // Get the delay between the moment getting the callback and the scheduled + // time stamp that tells when the data is going to be played out. + UInt64 output_time_ns = AudioConvertHostTimeToNanos( + output_time_stamp->mHostTime); + UInt64 now_ns = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()); + + // Prevent overflow leading to huge delay information; occurs regularly on + // the bots, probably less so in the wild. + if (now_ns > output_time_ns) + return 0; + + double delay_frames = static_cast<double> + (1e-9 * (output_time_ns - now_ns) * output_format_.mSampleRate); + + return (delay_frames + hardware_latency_frames_); +} + +void AUHALStream::CreateIOBusses() { + if (input_channels_ > 0) { + // Allocate storage for the AudioBufferList used for the + // input data from the input AudioUnit. + // We allocate enough space for with one AudioBuffer per channel. + size_t buffer_list_size = offsetof(AudioBufferList, mBuffers[0]) + + (sizeof(AudioBuffer) * input_channels_); + input_buffer_list_storage_.reset(new uint8[buffer_list_size]); + + input_buffer_list_ = + reinterpret_cast<AudioBufferList*>(input_buffer_list_storage_.get()); + input_buffer_list_->mNumberBuffers = input_channels_; + + // |input_bus_| allocates the storage for the PCM input data. + input_bus_ = AudioBus::Create(input_channels_, number_of_frames_); + + // Make the AudioBufferList point to the memory in |input_bus_|. + UInt32 buffer_size_bytes = input_bus_->frames() * sizeof(Float32); + for (size_t i = 0; i < input_buffer_list_->mNumberBuffers; ++i) { + input_buffer_list_->mBuffers[i].mNumberChannels = 1; + input_buffer_list_->mBuffers[i].mDataByteSize = buffer_size_bytes; + input_buffer_list_->mBuffers[i].mData = input_bus_->channel(i); + } + } + + // The output bus will wrap the AudioBufferList given to us in + // the Render() callback. + DCHECK_GT(output_channels_, 0); + output_bus_ = AudioBus::CreateWrapper(output_channels_); +} + +bool AUHALStream::EnableIO(bool enable, UInt32 scope) { + // See Apple technote for details about the EnableIO property. + // Note that we use bus 1 for input and bus 0 for output: + // http://developer.apple.com/library/mac/#technotes/tn2091/_index.html + UInt32 enable_IO = enable ? 1 : 0; + OSStatus result = AudioUnitSetProperty( + audio_unit_, + kAudioOutputUnitProperty_EnableIO, + scope, + (scope == kAudioUnitScope_Input) ? 1 : 0, + &enable_IO, + sizeof(enable_IO)); + return (result == noErr); +} + +bool AUHALStream::SetStreamFormat( + AudioStreamBasicDescription* desc, + int channels, + UInt32 scope, + UInt32 element) { + DCHECK(desc); + AudioStreamBasicDescription& format = *desc; + + format.mSampleRate = params_.sample_rate(); + format.mFormatID = kAudioFormatLinearPCM; + format.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | + kLinearPCMFormatFlagIsNonInterleaved; + format.mBytesPerPacket = sizeof(Float32); + format.mFramesPerPacket = 1; + format.mBytesPerFrame = sizeof(Float32); + format.mChannelsPerFrame = channels; + format.mBitsPerChannel = 32; + format.mReserved = 0; + + OSStatus result = AudioUnitSetProperty( + audio_unit_, + kAudioUnitProperty_StreamFormat, + scope, + element, + &format, + sizeof(format)); + return (result == noErr); +} + +bool AUHALStream::ConfigureAUHAL() { + if (device_ == kAudioObjectUnknown || + (input_channels_ == 0 && output_channels_ == 0)) + return false; + + AudioComponentDescription desc = { + kAudioUnitType_Output, + kAudioUnitSubType_HALOutput, + kAudioUnitManufacturer_Apple, + 0, + 0 + }; + AudioComponent comp = AudioComponentFindNext(0, &desc); + if (!comp) + return false; + + OSStatus result = AudioComponentInstanceNew(comp, &audio_unit_); + if (result != noErr) { + OSSTATUS_DLOG(WARNING, result) << "AudioComponentInstanceNew() failed."; + return false; + } + + result = AudioUnitInitialize(audio_unit_); + if (result != noErr) { + OSSTATUS_DLOG(WARNING, result) << "AudioUnitInitialize() failed."; + return false; + } + + // Enable input and output as appropriate. + if (!EnableIO(input_channels_ > 0, kAudioUnitScope_Input)) + return false; + if (!EnableIO(output_channels_ > 0, kAudioUnitScope_Output)) + return false; + + // Set the device to be used with the AUHAL AudioUnit. + result = AudioUnitSetProperty( + audio_unit_, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + 0, + &device_, + sizeof(AudioDeviceID)); + if (result != noErr) + return false; + + // Set stream formats. + // See Apple's tech note for details on the peculiar way that + // inputs and outputs are handled in the AUHAL concerning scope and bus + // (element) numbers: + // http://developer.apple.com/library/mac/#technotes/tn2091/_index.html + + if (input_channels_ > 0) { + if (!SetStreamFormat(&input_format_, + input_channels_, + kAudioUnitScope_Output, + 1)) + return false; + } + + if (output_channels_ > 0) { + if (!SetStreamFormat(&output_format_, + output_channels_, + kAudioUnitScope_Input, + 0)) + return false; + } + + // Set the buffer frame size. + // WARNING: Setting this value changes the frame size for all audio units in + // the current process. It's imperative that the input and output frame sizes + // be the same as the frames_per_buffer() returned by + // GetDefaultOutputStreamParameters(). + // See http://crbug.com/154352 for details. + UInt32 buffer_size = number_of_frames_; + result = AudioUnitSetProperty( + audio_unit_, + kAudioDevicePropertyBufferFrameSize, + kAudioUnitScope_Output, + 0, + &buffer_size, + sizeof(buffer_size)); + if (result != noErr) { + OSSTATUS_DLOG(WARNING, result) + << "AudioUnitSetProperty(kAudioDevicePropertyBufferFrameSize) failed."; + return false; + } + + // Setup callback. + AURenderCallbackStruct callback; + callback.inputProc = InputProc; + callback.inputProcRefCon = this; + result = AudioUnitSetProperty( + audio_unit_, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + 0, + &callback, + sizeof(callback)); + if (result != noErr) + return false; + + return true; +} + +} // namespace media diff --git a/media/audio/mac/audio_auhal_mac.h b/media/audio/mac/audio_auhal_mac.h new file mode 100644 index 0000000..bd88097 --- /dev/null +++ b/media/audio/mac/audio_auhal_mac.h @@ -0,0 +1,163 @@ +// 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. +// +// Implementation notes: +// +// - It is recommended to first acquire the native sample rate of the default +// output device and then use the same rate when creating this object. +// Use AudioManagerMac::HardwareSampleRate() to retrieve the sample rate. +// - Calling Close() also leads to self destruction. +// - The latency consists of two parts: +// 1) Hardware latency, which includes Audio Unit latency, audio device +// latency; +// 2) The delay between the moment getting the callback and the scheduled time +// stamp that tells when the data is going to be played out. +// +#ifndef MEDIA_AUDIO_MAC_AUDIO_AUHAL_MAC_H_ +#define MEDIA_AUDIO_MAC_AUDIO_AUHAL_MAC_H_ + +#include <AudioUnit/AudioUnit.h> +#include <CoreAudio/CoreAudio.h> + +#include "base/compiler_specific.h" +#include "base/synchronization/lock.h" +#include "media/audio/audio_io.h" +#include "media/audio/audio_parameters.h" + +namespace media { + +class AudioManagerMac; + +// Implementation of AudioOuputStream for Mac OS X using the +// AUHAL Audio Unit present in OS 10.4 and later. +// It is useful for low-latency output with optional synchronized +// input. +// +// Overview of operation: +// 1) An object of AUHALStream is created by the AudioManager +// factory: audio_man->MakeAudioStream(). +// 2) Next some thread will call Open(), at that point the underlying +// AUHAL Audio Unit is created and configured to use the |device|. +// 3) Then some thread will call Start(source). +// Then the AUHAL is started which creates its own thread which +// periodically will call the source for more data as buffers are being +// consumed. +// 4) At some point some thread will call Stop(), which we handle by directly +// stopping the default output Audio Unit. +// 6) The same thread that called stop will call Close() where we cleanup +// and notify the audio manager, which likely will destroy this object. + +class AUHALStream : public AudioOutputStream { + public: + // |manager| creates this object. + // |device| is the CoreAudio device to use for the stream. + // It will often be the default output device. + AUHALStream(AudioManagerMac* manager, + const AudioParameters& params, + AudioDeviceID device); + // The dtor is typically called by the AudioManager only and it is usually + // triggered by calling AudioOutputStream::Close(). + virtual ~AUHALStream(); + + // Implementation of AudioOutputStream. + virtual bool Open() OVERRIDE; + virtual void Close() OVERRIDE; + virtual void Start(AudioSourceCallback* callback) OVERRIDE; + virtual void Stop() OVERRIDE; + virtual void SetVolume(double volume) OVERRIDE; + virtual void GetVolume(double* volume) OVERRIDE; + + private: + // AUHAL callback. + static OSStatus InputProc(void* user_data, + AudioUnitRenderActionFlags* flags, + const AudioTimeStamp* time_stamp, + UInt32 bus_number, + UInt32 number_of_frames, + AudioBufferList* io_data); + + OSStatus Render(AudioUnitRenderActionFlags* flags, + const AudioTimeStamp* output_time_stamp, + UInt32 bus_number, + UInt32 number_of_frames, + AudioBufferList* io_data); + + // Helper method to enable input and output. + bool EnableIO(bool enable, UInt32 scope); + + // Sets the stream format on the AUHAL to PCM Float32 non-interleaved + // for the given number of channels on the given scope and element. + // The created stream description will be stored in |desc|. + bool SetStreamFormat(AudioStreamBasicDescription* desc, + int channels, + UInt32 scope, + UInt32 element); + + // Creates the AUHAL, sets its stream format, buffer-size, etc. + bool ConfigureAUHAL(); + + // Creates the input and output busses. + void CreateIOBusses(); + + // Gets the fixed playout device hardware latency and stores it. Returns 0 + // if not available. + double GetHardwareLatency(); + + // Gets the current playout latency value. + double GetPlayoutLatency(const AudioTimeStamp* output_time_stamp); + + // Our creator, the audio manager needs to be notified when we close. + AudioManagerMac* manager_; + + AudioParameters params_; + // For convenience - same as in params_. + int input_channels_; + int output_channels_; + + // Buffer-size. + size_t number_of_frames_; + + // Pointer to the object that will provide the audio samples. + AudioSourceCallback* source_; + + // Protects |source_|. Necessary since Render() calls seem to be in flight + // when |audio_unit_| is supposedly stopped. See http://crbug.com/178765. + base::Lock source_lock_; + + // Holds the stream format details such as bitrate. + AudioStreamBasicDescription input_format_; + AudioStreamBasicDescription output_format_; + + // The audio device to use with the AUHAL. + // We can potentially handle both input and output with this device. + AudioDeviceID device_; + + // The AUHAL Audio Unit which talks to |device_|. + AudioUnit audio_unit_; + + // Volume level from 0 to 1. + float volume_; + + // Fixed playout hardware latency in frames. + double hardware_latency_frames_; + + // The flag used to stop the streaming. + bool stopped_; + + // The input AudioUnit renders its data here. + scoped_array<uint8> input_buffer_list_storage_; + AudioBufferList* input_buffer_list_; + + // Holds the actual data for |input_buffer_list_|. + scoped_ptr<AudioBus> input_bus_; + + // Container for retrieving data from AudioSourceCallback::OnMoreIOData(). + scoped_ptr<AudioBus> output_bus_; + + DISALLOW_COPY_AND_ASSIGN(AUHALStream); +}; + +} // namespace media + +#endif // MEDIA_AUDIO_MAC_AUDIO_AUHAL_MAC_H_ diff --git a/media/audio/mac/audio_auhal_mac_unittest.cc b/media/audio/mac/audio_auhal_mac_unittest.cc new file mode 100644 index 0000000..cab8c28 --- /dev/null +++ b/media/audio/mac/audio_auhal_mac_unittest.cc @@ -0,0 +1,218 @@ +// 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 "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "media/audio/audio_io.h" +#include "media/audio/audio_manager.h" +#include "media/audio/simple_sources.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::DoAll; +using ::testing::Field; +using ::testing::InSequence; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::NotNull; +using ::testing::Return; + +static const int kBitsPerSample = 16; + +// TODO(crogers): Most of these tests can be made platform agnostic. +// http://crbug.com/223242 + +namespace media { + +class MockAudioSourceCallback : public AudioOutputStream::AudioSourceCallback { + public: + MOCK_METHOD2(OnMoreData, int(AudioBus* audio_bus, + AudioBuffersState buffers_state)); + MOCK_METHOD3(OnMoreIOData, int(AudioBus* source, + AudioBus* dest, + AudioBuffersState buffers_state)); + MOCK_METHOD1(OnError, void(AudioOutputStream* stream)); +}; + +// Convenience method which creates a default AudioOutputStream object but +// also allows the user to modify the default settings. +class AudioOutputStreamWrapper { + public: + explicit AudioOutputStreamWrapper() + : audio_man_(AudioManager::Create()), + format_(AudioParameters::AUDIO_PCM_LOW_LATENCY), + bits_per_sample_(kBitsPerSample) { + AudioParameters preferred_params = + audio_man_->GetDefaultOutputStreamParameters(); + channel_layout_ = preferred_params.channel_layout(); + channels_ = preferred_params.channels(); + sample_rate_ = preferred_params.sample_rate(); + samples_per_packet_ = preferred_params.frames_per_buffer(); + } + + ~AudioOutputStreamWrapper() {} + + // Creates AudioOutputStream object using default parameters. + AudioOutputStream* Create() { + return CreateOutputStream(); + } + + // Creates AudioOutputStream object using non-default parameters where the + // frame size is modified. + AudioOutputStream* Create(int samples_per_packet) { + samples_per_packet_ = samples_per_packet; + return CreateOutputStream(); + } + + // Creates AudioOutputStream object using non-default parameters where the + // sample rate is modified. + AudioOutputStream* CreateWithSampleRate(int sample_rate) { + sample_rate_ = sample_rate; + return CreateOutputStream(); + } + + // Creates AudioOutputStream object using non-default parameters where the + // channel layout is modified. + AudioOutputStream* CreateWithLayout(ChannelLayout layout) { + channel_layout_ = layout; + channels_ = ChannelLayoutToChannelCount(layout); + return CreateOutputStream(); + } + + AudioParameters::Format format() const { return format_; } + 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_; } + + bool CanRunAudioTests() { + return audio_man_->HasAudioOutputDevices(); + } + + private: + AudioOutputStream* CreateOutputStream() { + AudioParameters params; + params.Reset(format_, channel_layout_, + channels_, 0, + sample_rate_, bits_per_sample_, + samples_per_packet_); + + AudioOutputStream* aos = audio_man_->MakeAudioOutputStream(params); + EXPECT_TRUE(aos); + return aos; + } + + scoped_ptr<AudioManager> audio_man_; + + AudioParameters::Format format_; + ChannelLayout channel_layout_; + int channels_; + int bits_per_sample_; + int sample_rate_; + int samples_per_packet_; +}; + +// Test that we can get the hardware sample-rate. +TEST(AUHALStreamTest, HardwareSampleRate) { + AudioOutputStreamWrapper aosw; + if (!aosw.CanRunAudioTests()) + return; + + int sample_rate = aosw.sample_rate(); + EXPECT_GE(sample_rate, 16000); + EXPECT_LE(sample_rate, 192000); +} + +// Test Create(), Close() calling sequence. +TEST(AUHALStreamTest, CreateAndClose) { + AudioOutputStreamWrapper aosw; + if (!aosw.CanRunAudioTests()) + return; + + AudioOutputStream* aos = aosw.Create(); + aos->Close(); +} + +// Test Open(), Close() calling sequence. +TEST(AUHALStreamTest, OpenAndClose) { + AudioOutputStreamWrapper aosw; + if (!aosw.CanRunAudioTests()) + return; + + AudioOutputStream* aos = aosw.Create(); + EXPECT_TRUE(aos->Open()); + aos->Close(); +} + +// Test Open(), Start(), Close() calling sequence. +TEST(AUHALStreamTest, OpenStartAndClose) { + AudioOutputStreamWrapper aosw; + if (!aosw.CanRunAudioTests()) + return; + + AudioOutputStream* aos = aosw.Create(); + EXPECT_TRUE(aos->Open()); + MockAudioSourceCallback source; + EXPECT_CALL(source, OnError(aos)) + .Times(0); + aos->Start(&source); + aos->Close(); +} + +// Test Open(), Start(), Stop(), Close() calling sequence. +TEST(AUHALStreamTest, OpenStartStopAndClose) { + AudioOutputStreamWrapper aosw; + if (!aosw.CanRunAudioTests()) + return; + + AudioOutputStream* aos = aosw.Create(); + EXPECT_TRUE(aos->Open()); + MockAudioSourceCallback source; + EXPECT_CALL(source, OnError(aos)) + .Times(0); + aos->Start(&source); + aos->Stop(); + aos->Close(); +} + +// This test produces actual audio for 0.5 seconds on the default audio device +// at the hardware sample-rate (usually 44.1KHz). +// Parameters have been chosen carefully so you should not hear +// pops or noises while the sound is playing. +TEST(AUHALStreamTest, AUHALStreamPlay200HzTone) { + AudioOutputStreamWrapper aosw; + if (!aosw.CanRunAudioTests()) + return; + + AudioOutputStream* aos = aosw.CreateWithLayout(CHANNEL_LAYOUT_MONO); + + EXPECT_TRUE(aos->Open()); + + SineWaveAudioSource source(1, 200.0, aosw.sample_rate()); + aos->Start(&source); + usleep(500000); + + aos->Stop(); + aos->Close(); +} + +// Test that Open() will fail with a sample-rate which isn't the hardware +// sample-rate. +TEST(AUHALStreamTest, AUHALStreamInvalidSampleRate) { + AudioOutputStreamWrapper aosw; + if (!aosw.CanRunAudioTests()) + return; + + int non_default_sample_rate = aosw.sample_rate() == 44100 ? + 48000 : 44100; + AudioOutputStream* aos = aosw.CreateWithSampleRate(non_default_sample_rate); + + EXPECT_FALSE(aos->Open()); + + aos->Close(); +} + +} // namespace media diff --git a/media/audio/mac/audio_device_listener_mac.cc b/media/audio/mac/audio_device_listener_mac.cc index 15de47b..b6eaf78 100644 --- a/media/audio/mac/audio_device_listener_mac.cc +++ b/media/audio/mac/audio_device_listener_mac.cc @@ -105,16 +105,7 @@ OSStatus AudioDeviceListenerMac::OnDefaultDeviceChanged( addresses[i].mScope == kDeviceChangePropertyAddress.mScope && addresses[i].mElement == kDeviceChangePropertyAddress.mElement && context) { - AudioDeviceListenerMac* p_this = - static_cast<AudioDeviceListenerMac*>(context); - // Device changes on Mac are risky, the OSX API is not thread safe, so - // only change devices if we have to. Again, see http://crbug.com/158170. - // TODO(crogers): Remove this once the AUHAL output driver is in. - int sample_rate = AUAudioOutputStream::HardwareSampleRate(); - if (p_this->current_sample_rate_ != sample_rate) { - p_this->current_sample_rate_ = sample_rate; - p_this->listener_cb_.Run(); - } + static_cast<AudioDeviceListenerMac*>(context)->listener_cb_.Run(); break; } } @@ -125,8 +116,7 @@ OSStatus AudioDeviceListenerMac::OnDefaultDeviceChanged( AudioDeviceListenerMac::AudioDeviceListenerMac(const base::Closure& listener_cb) : listener_block_(NULL), add_listener_block_func_(NULL), - remove_listener_block_func_(NULL), - current_sample_rate_(AUAudioOutputStream::HardwareSampleRate()) { + remove_listener_block_func_(NULL) { // Device changes are hard, lets go shopping! Sadly OSX does not handle // property listener callbacks in a thread safe manner. On 10.6 we can set // kAudioHardwarePropertyRunLoop to account for this. On 10.7 this is broken diff --git a/media/audio/mac/audio_device_listener_mac.h b/media/audio/mac/audio_device_listener_mac.h index 24ecf26..d2d5870 100644 --- a/media/audio/mac/audio_device_listener_mac.h +++ b/media/audio/mac/audio_device_listener_mac.h @@ -68,10 +68,6 @@ class MEDIA_EXPORT AudioDeviceListenerMac { // thread. base::ThreadChecker thread_checker_; - // If the sample rate hasn't changed, don't fire a device change. OSX will - // handle the routing under the hood. - int current_sample_rate_; - DISALLOW_COPY_AND_ASSIGN(AudioDeviceListenerMac); }; diff --git a/media/audio/mac/audio_device_listener_mac_unittest.cc b/media/audio/mac/audio_device_listener_mac_unittest.cc index c88c339..1f884bc 100644 --- a/media/audio/mac/audio_device_listener_mac_unittest.cc +++ b/media/audio/mac/audio_device_listener_mac_unittest.cc @@ -63,8 +63,6 @@ class AudioDeviceListenerMacTest : public testing::Test { kAudioObjectPropertyElementMaster } }; - // Force sample rate change so the listener fires. - output_device_listener_->current_sample_rate_++; return noErr == output_device_listener_->OnDefaultDeviceChanged( kAudioObjectSystemObject, 1, addresses, output_device_listener_.get()); } diff --git a/media/audio/mac/audio_manager_mac.cc b/media/audio/mac/audio_manager_mac.cc index 93dbcc9..ff7e289 100644 --- a/media/audio/mac/audio_manager_mac.cc +++ b/media/audio/mac/audio_manager_mac.cc @@ -14,6 +14,7 @@ #include "base/sys_string_conversions.h" #include "media/audio/audio_parameters.h" #include "media/audio/audio_util.h" +#include "media/audio/mac/audio_auhal_mac.h" #include "media/audio/mac/audio_input_mac.h" #include "media/audio/mac/audio_low_latency_input_mac.h" #include "media/audio/mac/audio_low_latency_output_mac.h" @@ -32,6 +33,9 @@ static const int kMaxOutputStreams = 50; // Default buffer size in samples for low-latency input and output streams. static const int kDefaultLowLatencyBufferSize = 128; +// Default sample-rate on most Apple hardware. +static const int kFallbackSampleRate = 44100; + static int ChooseBufferSize(int output_sample_rate) { int buffer_size = kDefaultLowLatencyBufferSize; const int user_buffer_size = GetUserBufferSize(); @@ -69,38 +73,9 @@ static bool HasAudioHardware(AudioObjectPropertySelector selector) { // Returns true if the default input device is the same as // the default output device. -static bool HasUnifiedDefaultIO() { +bool AudioManagerMac::HasUnifiedDefaultIO() { AudioDeviceID input_id, output_id; - - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioHardwarePropertyDefaultInputDevice; - pa.mScope = kAudioObjectPropertyScopeGlobal; - pa.mElement = kAudioObjectPropertyElementMaster; - UInt32 size = sizeof(input_id); - - // Get the default input. - OSStatus result = AudioObjectGetPropertyData( - kAudioObjectSystemObject, - &pa, - 0, - 0, - &size, - &input_id); - - if (result != noErr) - return false; - - // Get the default output. - pa.mSelector = kAudioHardwarePropertyDefaultOutputDevice; - result = AudioObjectGetPropertyData( - kAudioObjectSystemObject, - &pa, - 0, - 0, - &size, - &output_id); - - if (result != noErr) + if (!GetDefaultInputDevice(&input_id) || !GetDefaultOutputDevice(&output_id)) return false; return input_id == output_id; @@ -254,7 +229,11 @@ static AudioDeviceID GetAudioDeviceIdByUId(bool is_input, return audio_device_id; } -AudioManagerMac::AudioManagerMac() { +AudioManagerMac::AudioManagerMac() + : current_sample_rate_(HardwareSampleRate()) { + if (!GetDefaultOutputDevice(¤t_output_device_)) + current_output_device_ = kAudioDeviceUnknown; + SetMaxOutputStreamsAllowed(kMaxOutputStreams); // Task must be posted last to avoid races from handing out "this" to the @@ -281,30 +260,40 @@ bool AudioManagerMac::HasAudioInputDevices() { } // TODO(crogers): There are several places on the OSX specific code which -// could benefit from this helper function. +// could benefit from these helper functions. +bool AudioManagerMac::GetDefaultInputDevice( + AudioDeviceID* device) { + return GetDefaultDevice(device, true); +} + bool AudioManagerMac::GetDefaultOutputDevice( AudioDeviceID* device) { + return GetDefaultDevice(device, false); +} + +bool AudioManagerMac::GetDefaultDevice( + AudioDeviceID* device, bool input) { CHECK(device); // Obtain the current output device selected by the user. - static const AudioObjectPropertyAddress kAddress = { - kAudioHardwarePropertyDefaultOutputDevice, - kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster - }; + AudioObjectPropertyAddress pa; + pa.mSelector = input ? kAudioHardwarePropertyDefaultInputDevice : + kAudioHardwarePropertyDefaultOutputDevice; + pa.mScope = kAudioObjectPropertyScopeGlobal; + pa.mElement = kAudioObjectPropertyElementMaster; UInt32 size = sizeof(*device); OSStatus result = AudioObjectGetPropertyData( kAudioObjectSystemObject, - &kAddress, + &pa, 0, 0, &size, device); if ((result != kAudioHardwareNoError) || (*device == kAudioDeviceUnknown)) { - DLOG(ERROR) << "Error getting default output AudioDevice."; + DLOG(ERROR) << "Error getting default AudioDevice."; return false; } @@ -312,24 +301,21 @@ bool AudioManagerMac::GetDefaultOutputDevice( } bool AudioManagerMac::GetDefaultOutputChannels( - int* channels, int* channels_per_frame) { + int* channels) { AudioDeviceID device; if (!GetDefaultOutputDevice(&device)) return false; return GetDeviceChannels(device, kAudioDevicePropertyScopeOutput, - channels, - channels_per_frame); + channels); } bool AudioManagerMac::GetDeviceChannels( AudioDeviceID device, AudioObjectPropertyScope scope, - int* channels, - int* channels_per_frame) { + int* channels) { CHECK(channels); - CHECK(channels_per_frame); // Get stream configuration. AudioObjectPropertyAddress pa; @@ -358,19 +344,53 @@ bool AudioManagerMac::GetDeviceChannels( return false; // Determine number of input channels. - *channels_per_frame = buffer_list.mNumberBuffers > 0 ? + int channels_per_frame = buffer_list.mNumberBuffers > 0 ? buffer_list.mBuffers[0].mNumberChannels : 0; - if (*channels_per_frame == 1 && buffer_list.mNumberBuffers > 1) { + if (channels_per_frame == 1 && buffer_list.mNumberBuffers > 1) { // Non-interleaved. *channels = buffer_list.mNumberBuffers; } else { // Interleaved. - *channels = *channels_per_frame; + *channels = channels_per_frame; } return true; } +int AudioManagerMac::HardwareSampleRateForDevice(AudioDeviceID device_id) { + Float64 nominal_sample_rate; + UInt32 info_size = sizeof(nominal_sample_rate); + + static const AudioObjectPropertyAddress kNominalSampleRateAddress = { + kAudioDevicePropertyNominalSampleRate, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + OSStatus result = AudioObjectGetPropertyData( + device_id, + &kNominalSampleRateAddress, + 0, + 0, + &info_size, + &nominal_sample_rate); + if (result != noErr) { + OSSTATUS_DLOG(WARNING, result) + << "Could not get default sample rate for device: " << device_id; + return 0; + } + + return static_cast<int>(nominal_sample_rate); +} + +int AudioManagerMac::HardwareSampleRate() { + // Determine the default output device's sample-rate. + AudioDeviceID device_id = kAudioObjectUnknown; + if (!GetDefaultOutputDevice(&device_id)) + return kFallbackSampleRate; + + return HardwareSampleRateForDevice(device_id); +} + void AudioManagerMac::GetAudioInputDeviceNames( media::AudioDeviceNames* device_names) { GetAudioDeviceInfo(true, device_names); @@ -412,6 +432,9 @@ AudioOutputStream* AudioManagerMac::MakeLowLatencyOutputStream( if (HasUnifiedDefaultIO()) return new AudioHardwareUnifiedStream(this, params); + // TODO(crogers): use aggregate devices along with AUHALStream + // to get better performance for built-in hardware. + // kAudioDeviceUnknown translates to "use default" here. return new AudioSynchronizedStream(this, params, @@ -419,7 +442,9 @@ AudioOutputStream* AudioManagerMac::MakeLowLatencyOutputStream( kAudioDeviceUnknown); } - return new AUAudioOutputStream(this, params); + AudioDeviceID device = kAudioObjectUnknown; + GetDefaultOutputDevice(&device); + return new AUHALStream(this, params, device); } AudioInputStream* AudioManagerMac::MakeLinearInputStream( @@ -444,9 +469,7 @@ AudioInputStream* AudioManagerMac::MakeLowLatencyInputStream( AudioParameters AudioManagerMac::GetPreferredOutputStreamParameters( const AudioParameters& input_params) { int hardware_channels = 2; - int hardware_channels_per_frame = 1; - if (!GetDefaultOutputChannels(&hardware_channels, - &hardware_channels_per_frame)) { + if (!GetDefaultOutputChannels(&hardware_channels)) { // Fallback to stereo. hardware_channels = 2; } @@ -485,8 +508,10 @@ AudioParameters AudioManagerMac::GetPreferredOutputStreamParameters( void AudioManagerMac::CreateDeviceListener() { DCHECK(GetMessageLoop()->BelongsToCurrentThread()); - output_device_listener_.reset(new AudioDeviceListenerMac(base::Bind( - &AudioManagerMac::DelayedDeviceChange, base::Unretained(this)))); + output_device_listener_.reset(new AudioDeviceListenerMac(BindToLoop( + GetMessageLoop(), base::Bind( + &AudioManagerMac::HandleDeviceChanges, + base::Unretained(this))))); } void AudioManagerMac::DestroyDeviceListener() { @@ -494,13 +519,18 @@ void AudioManagerMac::DestroyDeviceListener() { output_device_listener_.reset(); } -void AudioManagerMac::DelayedDeviceChange() { - // TODO(dalecurtis): This is ridiculous, but we need to delay device changes - // to workaround threading issues with OSX property listener callbacks. See - // http://crbug.com/158170 - GetMessageLoop()->PostDelayedTask(FROM_HERE, base::Bind( - &AudioManagerMac::NotifyAllOutputDeviceChangeListeners, - base::Unretained(this)), base::TimeDelta::FromSeconds(2)); +void AudioManagerMac::HandleDeviceChanges() { + int new_sample_rate = HardwareSampleRate(); + AudioDeviceID new_output_device; + GetDefaultOutputDevice(&new_output_device); + + if (current_sample_rate_ == new_sample_rate && + current_output_device_ == new_output_device) + return; + + current_sample_rate_ = new_sample_rate; + current_output_device_ = new_output_device; + NotifyAllOutputDeviceChangeListeners(); } AudioManager* CreateAudioManager() { diff --git a/media/audio/mac/audio_manager_mac.h b/media/audio/mac/audio_manager_mac.h index 6ebac06..8a93215 100644 --- a/media/audio/mac/audio_manager_mac.h +++ b/media/audio/mac/audio_manager_mac.h @@ -5,6 +5,9 @@ #ifndef MEDIA_AUDIO_MAC_AUDIO_MANAGER_MAC_H_ #define MEDIA_AUDIO_MAC_AUDIO_MANAGER_MAC_H_ +#include <CoreAudio/AudioHardware.h> +#include <string> + #include "base/basictypes.h" #include "base/compiler_specific.h" #include "base/message_loop_proxy.h" @@ -38,15 +41,18 @@ class MEDIA_EXPORT AudioManagerMac : public AudioManagerBase { virtual AudioInputStream* MakeLowLatencyInputStream( const AudioParameters& params, const std::string& device_id) OVERRIDE; + static bool GetDefaultInputDevice(AudioDeviceID* device); static bool GetDefaultOutputDevice(AudioDeviceID* device); + static bool GetDefaultDevice(AudioDeviceID* device, bool input); - static bool GetDefaultOutputChannels(int* channels, - int* channels_per_frame); + static bool GetDefaultOutputChannels(int* channels); static bool GetDeviceChannels(AudioDeviceID device, AudioObjectPropertyScope scope, - int* channels, - int* channels_per_frame); + int* channels); + + static int HardwareSampleRateForDevice(AudioDeviceID device_id); + static int HardwareSampleRate(); protected: virtual ~AudioManagerMac(); @@ -55,13 +61,20 @@ class MEDIA_EXPORT AudioManagerMac : public AudioManagerBase { const AudioParameters& input_params) OVERRIDE; private: + bool HasUnifiedDefaultIO(); + // Helper methods for constructing AudioDeviceListenerMac on the audio thread. void CreateDeviceListener(); void DestroyDeviceListener(); - void DelayedDeviceChange(); + void HandleDeviceChanges(); scoped_ptr<AudioDeviceListenerMac> output_device_listener_; + // Track the output sample-rate and the default output device + // so we can intelligently handle device notifications only when necessary. + int current_sample_rate_; + AudioDeviceID current_output_device_; + DISALLOW_COPY_AND_ASSIGN(AudioManagerMac); }; diff --git a/media/media.gyp b/media/media.gyp index 97ef02b..705102e 100644 --- a/media/media.gyp +++ b/media/media.gyp @@ -130,6 +130,8 @@ 'audio/linux/alsa_wrapper.h', 'audio/linux/audio_manager_linux.cc', 'audio/linux/audio_manager_linux.h', + 'audio/mac/audio_auhal_mac.cc', + 'audio/mac/audio_auhal_mac.h', 'audio/mac/audio_device_listener_mac.cc', 'audio/mac/audio_device_listener_mac.h', 'audio/mac/audio_input_mac.cc', @@ -864,6 +866,7 @@ 'audio/fake_audio_consumer_unittest.cc', 'audio/ios/audio_manager_ios_unittest.cc', 'audio/linux/alsa_output_unittest.cc', + 'audio/mac/audio_auhal_mac_unittest.cc', 'audio/mac/audio_device_listener_mac_unittest.cc', 'audio/mac/audio_low_latency_input_mac_unittest.cc', 'audio/simple_sources_unittest.cc', |