blob: 0405498e54967d3a04ff26f484e605b10093b02d [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// 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/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/mac/mac_logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/stringprintf.h"
#include "base/trace_event/trace_event.h"
#include "media/audio/mac/core_audio_util_mac.h"
#include "media/base/audio_pull_fifo.h"
#include "media/base/audio_timestamp_helper.h"
namespace media {
namespace {
// 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_RearSurroundLeft,
kAudioChannelLabel_RearSurroundRight,
kAudioChannelLabel_LeftCenter,
kAudioChannelLabel_RightCenter,
kAudioChannelLabel_CenterSurround,
kAudioChannelLabel_LeftSurround,
kAudioChannelLabel_RightSurround
};
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.");
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|.
bool SetStreamFormat(int channels,
int sample_rate,
AudioUnit audio_unit,
AudioStreamBasicDescription* format) {
format->mSampleRate = sample_rate;
format->mFormatID = kAudioFormatLinearPCM;
format->mFormatFlags = AudioFormatFlags{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.
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.";
}
}
void ReportFramesRequestedUma(int number_of_frames_requested) {
// A value of 0 indicates that we got the buffer size we asked for.
base::UmaHistogramCounts1M("Media.Audio.Render.FramesRequested",
number_of_frames_requested);
}
} // namespace
AUHALStream::AUHALStream(AudioIOStreamClient* client,
const AudioParameters& params,
AudioDeviceID device,
const AudioManager::LogCallback& log_callback)
: client_(client),
params_(params),
source_(nullptr),
device_(device),
volume_(1),
stopped_(true),
last_sample_time_(0.0),
last_number_of_frames_(0),
glitch_reporter_(SystemGlitchReporter::StreamType::kRender),
log_callback_(log_callback) {
// We must have a manager.
DVLOG(1) << __FUNCTION__ << " this " << this << " params "
<< params.AsHumanReadableString();
DCHECK(client_);
DCHECK(params_.IsValid());
#if BUILDFLAG(IS_MAC)
DCHECK_NE(device, kAudioObjectUnknown);
#endif
}
AUHALStream::~AUHALStream() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
CHECK(!audio_unit_);
}
bool AUHALStream::Open() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
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_ = core_audio_mac::GetHardwareLatency(
audio_unit_->audio_unit(), device_, kAudioObjectPropertyScopeOutput,
params_.sample_rate());
}
DVLOG(1) << __FUNCTION__ << " this " << this << " received hardware latency "
<< hardware_latency_;
return configured;
}
void AUHALStream::Close() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DVLOG(1) << __FUNCTION__ << " this " << this;
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.
client_->ReleaseOutputStreamUsingRealDevice(this, device_);
}
void AUHALStream::Start(AudioSourceCallback* callback) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
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;
}
DVLOG(1) << __FUNCTION__ << " this " << this;
#if BUILDFLAG(IS_MAC)
// Check if we should defer Start() for http://crbug.com/160920.
base::TimeDelta defer_start = client_->GetDeferStreamStartTimeout();
if (!defer_start.is_zero()) {
// 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));
client_->GetTaskRunner()->PostDelayedTask(
FROM_HERE, deferred_start_cb_.callback(), defer_start);
return;
}
#endif
stopped_ = false;
{
base::AutoLock al(lock_);
DCHECK(!audio_fifo_);
source_ = callback;
}
#if BUILDFLAG(IS_MAC)
peak_detector_ = std::make_unique<AmplitudePeakDetector>(base::BindRepeating(
&AudioIOStreamClient::StopAmplitudePeakTrace, base::Unretained(client_)));
#endif
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_CALLED_ON_VALID_THREAD(thread_checker_);
deferred_start_cb_.Cancel();
if (stopped_)
return;
DVLOG(1) << __FUNCTION__ << " this " << this;
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);
source_ = nullptr;
if (last_sample_time_) { // Report stats if the stream has been active.
if (!audio_fifo_) // Unexpected buffer size has never been requested.
ReportFramesRequestedUma(0);
SystemGlitchReporter::Stats stats =
glitch_reporter_.GetLongTermStatsAndReset();
std::string log_message = base::StringPrintf(
"AU out: (num_glitches_detected=[%d], cumulative_audio_lost=[%llu "
"ms], "
"largest_glitch=[%llu ms])",
stats.glitches_detected, stats.total_glitch_duration.InMilliseconds(),
stats.largest_glitch_duration.InMilliseconds());
if (!log_callback_.is_null())
log_callback_.Run(log_message);
if (stats.glitches_detected > 0) {
DLOG(WARNING) << log_message;
}
}
last_sample_time_ = 0;
last_number_of_frames_ = 0;
audio_fifo_.reset();
}
#if BUILDFLAG(IS_MAC)
peak_detector_.reset();
#endif
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",
params_.frames_per_buffer(), "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 != static_cast<UInt32>(params_.frames_per_buffer())) {
// Create a FIFO on the fly to handle any discrepancies in callback rates.
if (!audio_fifo_) {
DVLOG(1) << __FUNCTION__ << " this " << this
<< "Audio frame size changed from "
<< params_.frames_per_buffer() << " to " << number_of_frames
<< " adding FIFO to compensate.";
audio_fifo_ = std::make_unique<AudioPullFifo>(
params_.channels(), params_.frames_per_buffer(),
base::BindRepeating(&AUHALStream::ProvideInput,
base::Unretained(this)));
// Report it only once the first time the change happens.
ReportFramesRequestedUma(number_of_frames);
} else if (last_number_of_frames_ != number_of_frames) {
DVLOG(3) << __FUNCTION__ << " this " << this
<< "Audio frame size changed from " << last_number_of_frames_
<< " to " << number_of_frames << " FIFO already exists.";
}
}
// 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());
#if BUILDFLAG(IS_MAC)
peak_detector_->FindPeak(output_bus_.get());
#endif
last_number_of_frames_ = number_of_frames;
return noErr;
}
void AUHALStream::ProvideInput(int frame_delay, AudioBus* dest) {
TRACE_EVENT1("audio", "AUHALStream::ProvideInput", "frames", dest->frames());
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, glitch_info_accumulator_.GetAndReset(), dest);
dest->Scale(volume_);
}
// 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 sample_time_diff =
static_cast<UInt32>(timestamp->mSampleTime - last_sample_time_);
DCHECK_GE(sample_time_diff, last_number_of_frames_);
UInt32 lost_frames = sample_time_diff - last_number_of_frames_;
base::TimeDelta lost_audio_duration =
AudioTimestampHelper::FramesToTime(lost_frames, params_.sample_rate());
glitch_reporter_.UpdateStats(lost_audio_duration);
if (!lost_audio_duration.is_zero()) {
glitch_info_accumulator_.Add(
AudioGlitchInfo{.duration = lost_audio_duration, .count = 1});
}
}
// Store the last sample time for use next time we get called back.
last_sample_time_ = timestamp->mSampleTime;
}
bool AUHALStream::ConfigureAUHAL() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
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;
}
if (!client_->MaybeChangeBufferSize(device_, local_audio_unit->audio_unit(),
0, params_.frames_per_buffer())) {
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