| // 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 <algorithm> |
| #include <cstddef> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/mac/mac_logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/trace_event/trace_event.h" |
| #include "media/audio/mac/audio_manager_mac.h" |
| #include "media/base/audio_pull_fifo.h" |
| #include "media/base/audio_timestamp_helper.h" |
| |
| namespace media { |
| |
| // Mapping from Chrome's channel layout to CoreAudio layout. This must match the |
| // layout of the Channels enum in |channel_layout.h| |
| static const AudioChannelLabel kCoreAudioChannelMapping[] = { |
| kAudioChannelLabel_Left, |
| kAudioChannelLabel_Right, |
| kAudioChannelLabel_Center, |
| kAudioChannelLabel_LFEScreen, |
| kAudioChannelLabel_LeftSurround, |
| kAudioChannelLabel_RightSurround, |
| kAudioChannelLabel_LeftCenter, |
| kAudioChannelLabel_RightCenter, |
| kAudioChannelLabel_CenterSurround, |
| kAudioChannelLabel_LeftSurroundDirect, |
| kAudioChannelLabel_RightSurroundDirect, |
| }; |
| static_assert(0 == LEFT && 1 == RIGHT && 2 == CENTER && 3 == LFE && |
| 4 == BACK_LEFT && |
| 5 == BACK_RIGHT && |
| 6 == LEFT_OF_CENTER && |
| 7 == RIGHT_OF_CENTER && |
| 8 == BACK_CENTER && |
| 9 == SIDE_LEFT && |
| 10 == SIDE_RIGHT && |
| 10 == CHANNELS_MAX, |
| "Channel positions must match CoreAudio channel order."); |
| |
| static void WrapBufferList(AudioBufferList* buffer_list, |
| AudioBus* bus, |
| int frames) { |
| const int channels = bus->channels(); |
| const int buffer_list_channels = buffer_list->mNumberBuffers; |
| CHECK_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); |
| } |
| |
| // 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|. |
| static bool SetStreamFormat(int channels, |
| int sample_rate, |
| AudioUnit audio_unit, |
| AudioStreamBasicDescription* format) { |
| format->mSampleRate = 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; |
| |
| // 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 |
| return AudioUnitSetProperty(audio_unit, kAudioUnitProperty_StreamFormat, |
| kAudioUnitScope_Input, AUElement::OUTPUT, format, |
| sizeof(*format)) == noErr; |
| } |
| |
| // Converts |channel_layout| into CoreAudio format and sets up the AUHAL with |
| // our layout information so it knows how to remap the channels. |
| static void SetAudioChannelLayout(int channels, |
| ChannelLayout channel_layout, |
| AudioUnit audio_unit) { |
| DCHECK(audio_unit); |
| DCHECK_GT(channels, 0); |
| DCHECK_GT(channel_layout, CHANNEL_LAYOUT_UNSUPPORTED); |
| |
| // AudioChannelLayout is structure ending in a variable length array, so we |
| // can't directly allocate one. Instead compute the size and and allocate one |
| // inside of a byte array. |
| // |
| // Code modeled after example from Apple documentation here: |
| // https://developer.apple.com/library/content/qa/qa1627/_index.html |
| const size_t layout_size = |
| offsetof(AudioChannelLayout, mChannelDescriptions[channels]); |
| std::unique_ptr<uint8_t[]> layout_storage(new uint8_t[layout_size]); |
| memset(layout_storage.get(), 0, layout_size); |
| AudioChannelLayout* coreaudio_layout = |
| reinterpret_cast<AudioChannelLayout*>(layout_storage.get()); |
| |
| coreaudio_layout->mNumberChannelDescriptions = channels; |
| coreaudio_layout->mChannelLayoutTag = |
| kAudioChannelLayoutTag_UseChannelDescriptions; |
| AudioChannelDescription* descriptions = |
| coreaudio_layout->mChannelDescriptions; |
| |
| if (channel_layout == CHANNEL_LAYOUT_DISCRETE) { |
| // For the discrete case just assume common input mappings; once we run out |
| // of known channels mark them as unknown. |
| for (int ch = 0; ch < channels; ++ch) { |
| descriptions[ch].mChannelLabel = ch > CHANNELS_MAX |
| ? kAudioChannelLabel_Unknown |
| : kCoreAudioChannelMapping[ch]; |
| descriptions[ch].mChannelFlags = kAudioChannelFlags_AllOff; |
| } |
| } else if (channel_layout == CHANNEL_LAYOUT_MONO) { |
| // CoreAudio has a special label for mono. |
| DCHECK_EQ(channels, 1); |
| descriptions[0].mChannelLabel = kAudioChannelLabel_Mono; |
| descriptions[0].mChannelFlags = kAudioChannelFlags_AllOff; |
| } else { |
| for (int ch = 0; ch <= CHANNELS_MAX; ++ch) { |
| const int order = ChannelOrder(channel_layout, static_cast<Channels>(ch)); |
| if (order == -1) |
| continue; |
| descriptions[order].mChannelLabel = kCoreAudioChannelMapping[ch]; |
| descriptions[order].mChannelFlags = kAudioChannelFlags_AllOff; |
| } |
| } |
| |
| OSStatus result = AudioUnitSetProperty( |
| audio_unit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Input, |
| AUElement::OUTPUT, coreaudio_layout, layout_size); |
| if (result != noErr) { |
| OSSTATUS_DLOG(ERROR, result) |
| << "Failed to set audio channel layout. Using default layout."; |
| } |
| } |
| |
| AUHALStream::AUHALStream(AudioManagerMac* manager, |
| const AudioParameters& params, |
| AudioDeviceID device, |
| const AudioManager::LogCallback& log_callback) |
| : manager_(manager), |
| params_(params), |
| number_of_frames_(params_.frames_per_buffer()), |
| number_of_frames_requested_(0), |
| source_(NULL), |
| device_(device), |
| volume_(1), |
| stopped_(true), |
| current_lost_frames_(0), |
| last_sample_time_(0.0), |
| last_number_of_frames_(0), |
| total_lost_frames_(0), |
| largest_glitch_frames_(0), |
| glitches_detected_(0), |
| log_callback_(log_callback) { |
| // We must have a manager. |
| DCHECK(manager_); |
| DCHECK(params_.IsValid()); |
| DCHECK_NE(device, kAudioObjectUnknown); |
| } |
| |
| AUHALStream::~AUHALStream() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| CHECK(!audio_unit_); |
| |
| base::AutoLock al(lock_); |
| ReportAndResetStats(); |
| } |
| |
| bool AUHALStream::Open() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(!output_bus_); |
| DCHECK(!audio_unit_); |
| |
| // The output bus will wrap the AudioBufferList given to us in |
| // the Render() callback. |
| output_bus_ = AudioBus::CreateWrapper(params_.channels()); |
| |
| bool configured = ConfigureAUHAL(); |
| if (configured) { |
| DCHECK(audio_unit_); |
| DCHECK(audio_unit_->is_valid()); |
| hardware_latency_ = AudioManagerMac::GetHardwareLatency( |
| audio_unit_->audio_unit(), device_, kAudioDevicePropertyScopeOutput, |
| params_.sample_rate()); |
| } |
| |
| return configured; |
| } |
| |
| void AUHALStream::Close() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| if (audio_unit_) { |
| Stop(); |
| |
| // Clear the render callback to try and prevent any callbacks from coming |
| // in after we've called stop. https://crbug.com/737527. |
| AURenderCallbackStruct callback = {0}; |
| auto result = AudioUnitSetProperty( |
| audio_unit_->audio_unit(), kAudioUnitProperty_SetRenderCallback, |
| kAudioUnitScope_Input, AUElement::OUTPUT, &callback, sizeof(callback)); |
| OSSTATUS_DLOG_IF(ERROR, result != noErr, result) |
| << "Failed to clear input callback."; |
| } |
| |
| audio_unit_.reset(); |
| // Inform the audio manager that we have been closed. This will cause our |
| // destruction. Also include the device ID as a signal to the audio manager |
| // that it should try to increase the native I/O buffer size after the stream |
| // has been closed. |
| manager_->ReleaseOutputStreamUsingRealDevice(this, device_); |
| } |
| |
| void AUHALStream::Start(AudioSourceCallback* callback) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(callback); |
| if (!audio_unit_) { |
| DLOG(ERROR) << "Open() has not been called successfully"; |
| return; |
| } |
| |
| if (!stopped_) { |
| base::AutoLock al(lock_); |
| CHECK_EQ(source_, callback); |
| return; |
| } |
| |
| // Check if we should defer Start() for http://crbug.com/160920. |
| if (manager_->ShouldDeferStreamStart()) { |
| // Use a cancellable closure so that if Stop() is called before Start() |
| // actually runs, we can cancel the pending start. |
| deferred_start_cb_.Reset( |
| base::BindOnce(&AUHALStream::Start, base::Unretained(this), callback)); |
| manager_->GetTaskRunner()->PostDelayedTask( |
| FROM_HERE, deferred_start_cb_.callback(), |
| base::Seconds(AudioManagerMac::kStartDelayInSecsForPowerEvents)); |
| return; |
| } |
| |
| stopped_ = false; |
| |
| { |
| base::AutoLock al(lock_); |
| audio_fifo_.reset(); |
| source_ = callback; |
| } |
| |
| OSStatus result = AudioOutputUnitStart(audio_unit_->audio_unit()); |
| if (result == noErr) |
| return; |
| |
| Stop(); |
| OSSTATUS_DLOG(ERROR, result) << "AudioOutputUnitStart() failed."; |
| callback->OnError(AudioSourceCallback::ErrorType::kUnknown); |
| } |
| |
| // This stream is always used with sub second buffer sizes, where it's |
| // sufficient to simply always flush upon Start(). |
| void AUHALStream::Flush() {} |
| |
| void AUHALStream::Stop() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| deferred_start_cb_.Cancel(); |
| if (stopped_) |
| return; |
| |
| OSStatus result = AudioOutputUnitStop(audio_unit_->audio_unit()); |
| OSSTATUS_DLOG_IF(ERROR, result != noErr, result) |
| << "AudioOutputUnitStop() failed."; |
| |
| { |
| base::AutoLock al(lock_); |
| if (result != noErr) |
| source_->OnError(AudioSourceCallback::ErrorType::kUnknown); |
| |
| ReportAndResetStats(); |
| source_ = nullptr; |
| } |
| |
| stopped_ = true; |
| } |
| |
| void AUHALStream::SetVolume(double volume) { |
| volume_ = static_cast<float>(volume); |
| } |
| |
| 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* data) { |
| TRACE_EVENT2("audio", "AUHALStream::Render", "input buffer size", |
| number_of_frames_, "output buffer size", number_of_frames); |
| |
| base::AutoLock al(lock_); |
| |
| // There's no documentation on what we should return here, but if we're here |
| // something is wrong so just return an AudioUnit error that looks reasonable. |
| if (!source_) |
| return kAudioUnitErr_Uninitialized; |
| |
| UpdatePlayoutTimestamp(output_time_stamp); |
| |
| // If the stream parameters change for any reason, we need to insert a FIFO |
| // since the OnMoreData() pipeline can't handle frame size changes. |
| if (number_of_frames != number_of_frames_) { |
| // Create a FIFO on the fly to handle any discrepancies in callback rates. |
| if (!audio_fifo_) { |
| // TODO(grunell): We'll only care about the first buffer size change, |
| // any further changes will be ignored. It would be nice to have all |
| // changes reflected in UMA stats. |
| number_of_frames_requested_ = number_of_frames; |
| DVLOG(1) << "Audio frame size changed from " << number_of_frames_ |
| << " to " << number_of_frames << " adding FIFO to compensate."; |
| audio_fifo_ = std::make_unique<AudioPullFifo>( |
| params_.channels(), number_of_frames_, |
| base::BindRepeating(&AUHALStream::ProvideInput, |
| base::Unretained(this))); |
| } |
| } |
| |
| // Make |output_bus_| wrap the output AudioBufferList. |
| WrapBufferList(data, output_bus_.get(), number_of_frames); |
| |
| current_playout_time_ = GetPlayoutTime(output_time_stamp); |
| |
| if (audio_fifo_) |
| audio_fifo_->Consume(output_bus_.get(), output_bus_->frames()); |
| else |
| ProvideInput(0, output_bus_.get()); |
| |
| last_number_of_frames_ = number_of_frames; |
| |
| return noErr; |
| } |
| |
| void AUHALStream::ProvideInput(int frame_delay, AudioBus* dest) { |
| lock_.AssertAcquired(); |
| DCHECK(source_); |
| |
| const base::TimeTicks playout_time = |
| current_playout_time_ + |
| AudioTimestampHelper::FramesToTime(frame_delay, params_.sample_rate()); |
| const base::TimeTicks now = base::TimeTicks::Now(); |
| const base::TimeDelta delay = playout_time - now; |
| |
| // Supply the input data and render the output data. |
| source_->OnMoreData(delay, now, current_lost_frames_, dest); |
| dest->Scale(volume_); |
| current_lost_frames_ = 0; |
| } |
| |
| // 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); |
| } |
| |
| base::TimeTicks AUHALStream::GetPlayoutTime( |
| const AudioTimeStamp* output_time_stamp) { |
| // A platform bug has been observed where the platform sometimes reports that |
| // the next frames will be output at an invalid time or a time in the past. |
| // Because the target playout time cannot be invalid or in the past, return |
| // "now" in these cases. |
| if ((output_time_stamp->mFlags & kAudioTimeStampHostTimeValid) == 0) |
| return base::TimeTicks::Now(); |
| |
| return std::max(base::TimeTicks::FromMachAbsoluteTime( |
| output_time_stamp->mHostTime), |
| base::TimeTicks::Now()) + |
| hardware_latency_; |
| } |
| |
| void AUHALStream::UpdatePlayoutTimestamp(const AudioTimeStamp* timestamp) { |
| lock_.AssertAcquired(); |
| |
| if ((timestamp->mFlags & kAudioTimeStampSampleTimeValid) == 0) |
| return; |
| |
| if (last_sample_time_) { |
| DCHECK_NE(0U, last_number_of_frames_); |
| UInt32 diff = |
| static_cast<UInt32>(timestamp->mSampleTime - last_sample_time_); |
| if (diff != last_number_of_frames_) { |
| DCHECK_GT(diff, last_number_of_frames_); |
| // We're being asked to render samples post what we expected. Update the |
| // glitch count etc and keep a record of the largest glitch. |
| auto lost_frames = diff - last_number_of_frames_; |
| total_lost_frames_ += lost_frames; |
| current_lost_frames_ += lost_frames; |
| if (lost_frames > largest_glitch_frames_) |
| largest_glitch_frames_ = lost_frames; |
| ++glitches_detected_; |
| } |
| } |
| |
| // Store the last sample time for use next time we get called back. |
| last_sample_time_ = timestamp->mSampleTime; |
| } |
| |
| void AUHALStream::ReportAndResetStats() { |
| lock_.AssertAcquired(); |
| |
| if (!last_sample_time_) |
| return; // No stats gathered to report. |
| |
| // A value of 0 indicates that we got the buffer size we asked for. |
| UMA_HISTOGRAM_COUNTS_1M("Media.Audio.Render.FramesRequested", |
| number_of_frames_requested_); |
| // Even if there aren't any glitches, we want to record it to get a feel for |
| // how often we get no glitches vs the alternative. |
| UMA_HISTOGRAM_CUSTOM_COUNTS("Media.Audio.Render.Glitches", glitches_detected_, |
| 1, 999999, 100); |
| |
| auto lost_frames_ms = (total_lost_frames_ * 1000) / params_.sample_rate(); |
| |
| std::string log_message = base::StringPrintf( |
| "AU out: Total glitches=%d. Total frames lost=%d (%d ms).", |
| glitches_detected_, total_lost_frames_, lost_frames_ms); |
| |
| if (!log_callback_.is_null()) |
| log_callback_.Run(log_message); |
| |
| if (glitches_detected_ != 0) { |
| UMA_HISTOGRAM_COUNTS_1M("Media.Audio.Render.LostFramesInMs", |
| lost_frames_ms); |
| auto largest_glitch_ms = |
| (largest_glitch_frames_ * 1000) / params_.sample_rate(); |
| UMA_HISTOGRAM_COUNTS_1M("Media.Audio.Render.LargestGlitchMs", |
| largest_glitch_ms); |
| DLOG(WARNING) << log_message; |
| } |
| |
| number_of_frames_requested_ = 0; |
| glitches_detected_ = 0; |
| last_sample_time_ = 0; |
| last_number_of_frames_ = 0; |
| total_lost_frames_ = 0; |
| largest_glitch_frames_ = 0; |
| } |
| |
| bool AUHALStream::ConfigureAUHAL() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| std::unique_ptr<ScopedAudioUnit> local_audio_unit( |
| new ScopedAudioUnit(device_, AUElement::OUTPUT)); |
| if (!local_audio_unit->is_valid()) |
| return false; |
| |
| if (!SetStreamFormat(params_.channels(), params_.sample_rate(), |
| local_audio_unit->audio_unit(), &output_format_)) { |
| return false; |
| } |
| |
| bool size_was_changed = false; |
| size_t io_buffer_frame_size = 0; |
| if (!manager_->MaybeChangeBufferSize(device_, local_audio_unit->audio_unit(), |
| 0, number_of_frames_, &size_was_changed, |
| &io_buffer_frame_size)) { |
| return false; |
| } |
| |
| // Setup callback. |
| AURenderCallbackStruct callback; |
| callback.inputProc = InputProc; |
| callback.inputProcRefCon = this; |
| OSStatus result = AudioUnitSetProperty( |
| local_audio_unit->audio_unit(), kAudioUnitProperty_SetRenderCallback, |
| kAudioUnitScope_Input, AUElement::OUTPUT, &callback, sizeof(callback)); |
| if (result != noErr) |
| return false; |
| |
| SetAudioChannelLayout(params_.channels(), params_.channel_layout(), |
| local_audio_unit->audio_unit()); |
| |
| result = AudioUnitInitialize(local_audio_unit->audio_unit()); |
| if (result != noErr) { |
| OSSTATUS_DLOG(ERROR, result) << "AudioUnitInitialize() failed."; |
| return false; |
| } |
| |
| audio_unit_ = std::move(local_audio_unit); |
| return true; |
| } |
| |
| } // namespace media |