blob: e8a955828a300b5d3991512272e38e24500081ce [file] [log] [blame]
// Copyright 2015 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 "chromecast/media/cma/backend/alsa/stream_mixer_alsa.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <unordered_set>
#include <utility>
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/lazy_instance.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/numerics/saturated_arithmetic.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chromecast/base/chromecast_switches.h"
#include "chromecast/media/base/audio_device_ids.h"
#include "chromecast/media/cma/backend/alsa/alsa_wrapper.h"
#include "chromecast/media/cma/backend/alsa/audio_filter_factory.h"
#include "chromecast/media/cma/backend/alsa/filter_group.h"
#include "chromecast/media/cma/backend/alsa/stream_mixer_alsa_input_impl.h"
#include "media/audio/audio_device_description.h"
#include "media/base/audio_bus.h"
#include "media/base/media_switches.h"
#define RETURN_REPORT_ERROR(snd_func, ...) \
do { \
int err = alsa_->snd_func(__VA_ARGS__); \
if (err < 0) { \
LOG(ERROR) << #snd_func " error: " << alsa_->StrError(err); \
SignalError(); \
return; \
} \
} while (0)
#define RETURN_ERROR_CODE(snd_func, ...) \
do { \
int err = alsa_->snd_func(__VA_ARGS__); \
if (err < 0) { \
LOG(ERROR) << #snd_func " error: " << alsa_->StrError(err); \
return err; \
} \
} while (0)
#define CHECK_PCM_INITIALIZED() \
if (!pcm_ || !pcm_hw_params_) { \
LOG(WARNING) << __FUNCTION__ << "() called after failed initialization"; \
return; \
}
#define RUN_ON_MIXER_THREAD(callback, ...) \
if (!mixer_task_runner_->BelongsToCurrentThread()) { \
POST_TASK_TO_MIXER_THREAD(callback, ##__VA_ARGS__); \
return; \
}
#define POST_TASK_TO_MIXER_THREAD(task, ...) \
mixer_task_runner_->PostTask( \
FROM_HERE, base::Bind(task, base::Unretained(this), ##__VA_ARGS__));
namespace chromecast {
namespace media {
namespace {
const char kOutputDeviceDefaultName[] = "default";
const int kNumOutputChannels = 2;
const int kDefaultOutputBufferSizeFrames = 4096;
const bool kPcmRecoverIsSilent = false;
// The number of frames of silence to write (to prevent underrun) when no inputs
// are present.
const int kPreventUnderrunChunkSize = 512;
const int kDefaultCheckCloseTimeoutMs = 2000;
const int kMaxWriteSizeMs = 20;
// The minimum amount of data that we allow in the ALSA buffer before starting
// to skip inputs with no available data.
const int kMinBufferedDataMs = 8;
// A list of supported sample rates.
// TODO(jyw): move this up into chromecast/public for 1) documentation and
// 2) to help when implementing IsSampleRateSupported()
// clang-format off
const int kSupportedSampleRates[] =
{ 8000, 11025, 12000,
16000, 22050, 24000,
32000, 44100, 48000,
64000, 88200, 96000};
// clang-format on
const int kInvalidSampleRate = 0;
// Arbitrary sample rate in Hz to mix all audio to when a new primary input has
// a sample rate that is not directly supported, and a better fallback sample
// rate cannot be determined. 48000 is the highest supported non-hi-res sample
// rate. 96000 is the highest supported hi-res sample rate.
const unsigned int kFallbackSampleRate = 48000;
const unsigned int kFallbackSampleRateHiRes = 96000;
// Resample all audio below this frequency.
const unsigned int kLowSampleRateCutoff = 32000;
// The snd_pcm_(hw|sw)_params_set_*_near families of functions will report what
// direction they adjusted the requested parameter in, but since we read the
// output param and then log the information, this module doesn't need to get
// the direction explicitly.
static int* kAlsaDirDontCare = nullptr;
// These sample formats will be tried in order. 32 bit samples is ideal, but
// some devices do not support 32 bit samples.
const snd_pcm_format_t kPreferredSampleFormats[] = {SND_PCM_FORMAT_S32,
SND_PCM_FORMAT_S16};
const int64_t kNoTimestamp = std::numeric_limits<int64_t>::min();
int64_t TimespecToMicroseconds(struct timespec time) {
return static_cast<int64_t>(time.tv_sec) *
base::Time::kMicrosecondsPerSecond +
time.tv_nsec / 1000;
}
bool GetSwitchValueAsInt(const std::string& switch_name,
int default_value,
int* value) {
DCHECK(value);
*value = default_value;
if (!base::CommandLine::InitializedForCurrentProcess()) {
LOG(WARNING) << "No CommandLine for current process.";
return false;
}
const base::CommandLine* command_line =
base::CommandLine::ForCurrentProcess();
if (!command_line->HasSwitch(switch_name)) {
return false;
}
int arg_value;
if (!base::StringToInt(command_line->GetSwitchValueASCII(switch_name),
&arg_value)) {
LOG(DFATAL) << "--" << switch_name << " only accepts integers as arguments";
return false;
}
*value = arg_value;
return true;
}
bool GetSwitchValueAsNonNegativeInt(const std::string& switch_name,
int default_value,
int* value) {
DCHECK_GE(default_value, 0) << "--" << switch_name
<< " must have a non-negative default value";
DCHECK(value);
if (!GetSwitchValueAsInt(switch_name, default_value, value)) {
return false;
}
if (*value < 0) {
LOG(DFATAL) << "--" << switch_name << " must have a non-negative value";
*value = default_value;
return false;
}
return true;
}
void VectorAccumulate(const int32_t* source, size_t size, int32_t* dest) {
for (size_t i = 0; i < size; ++i) {
dest[i] = base::SaturatedAddition(source[i], dest[i]);
}
}
class StreamMixerAlsaInstance : public StreamMixerAlsa {
public:
StreamMixerAlsaInstance() {}
~StreamMixerAlsaInstance() override {}
private:
DISALLOW_COPY_AND_ASSIGN(StreamMixerAlsaInstance);
};
base::LazyInstance<StreamMixerAlsaInstance>::DestructorAtExit g_mixer_instance =
LAZY_INSTANCE_INITIALIZER;
} // namespace
// static
bool StreamMixerAlsa::single_threaded_for_test_ = false;
// static
StreamMixerAlsa* StreamMixerAlsa::Get() {
return g_mixer_instance.Pointer();
}
// static
void StreamMixerAlsa::MakeSingleThreadedForTest() {
single_threaded_for_test_ = true;
StreamMixerAlsa::Get()->ResetTaskRunnerForTest();
}
StreamMixerAlsa::StreamMixerAlsa()
: mixer_thread_(new base::Thread("ALSA CMA mixer thread")),
mixer_task_runner_(nullptr),
requested_output_samples_per_second_(kInvalidSampleRate),
output_samples_per_second_(kInvalidSampleRate),
pcm_(nullptr),
pcm_hw_params_(nullptr),
pcm_status_(nullptr),
pcm_format_(SND_PCM_FORMAT_UNKNOWN),
alsa_buffer_size_(0),
alsa_period_explicitly_set(false),
alsa_period_size_(0),
alsa_start_threshold_(0),
alsa_avail_min_(0),
state_(kStateUninitialized),
retry_write_frames_timer_(new base::Timer(false, false)),
check_close_timeout_(kDefaultCheckCloseTimeoutMs),
check_close_timer_(new base::Timer(false, false)) {
if (single_threaded_for_test_) {
mixer_task_runner_ = base::ThreadTaskRunnerHandle::Get();
} else {
base::Thread::Options options;
options.priority = base::ThreadPriority::REALTIME_AUDIO;
mixer_thread_->StartWithOptions(options);
mixer_task_runner_ = mixer_thread_->task_runner();
}
alsa_device_name_ = kOutputDeviceDefaultName;
if (base::CommandLine::InitializedForCurrentProcess() &&
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kAlsaOutputDevice)) {
alsa_device_name_ =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kAlsaOutputDevice);
}
int fixed_samples_per_second;
GetSwitchValueAsNonNegativeInt(switches::kAlsaFixedOutputSampleRate,
kInvalidSampleRate, &fixed_samples_per_second);
if (fixed_samples_per_second != kInvalidSampleRate) {
LOG(INFO) << "Setting fixed sample rate to " << fixed_samples_per_second;
}
fixed_output_samples_per_second_ = fixed_samples_per_second;
low_sample_rate_cutoff_ =
chromecast::GetSwitchValueBoolean(switches::kAlsaEnableUpsampling, false)
? kLowSampleRateCutoff
: 0;
// Create filter groups.
// TODO(bshaya): Switch to filter groups based on AudioContentType.
filter_groups_.push_back(base::MakeUnique<FilterGroup>(
std::unordered_set<std::string>(
{::media::AudioDeviceDescription::kCommunicationsDeviceId}),
AudioFilterFactory::COMMUNICATION_AUDIO_FILTER));
filter_groups_.push_back(base::MakeUnique<FilterGroup>(
std::unordered_set<std::string>({kAlarmAudioDeviceId}),
AudioFilterFactory::ALARM_AUDIO_FILTER));
filter_groups_.push_back(base::MakeUnique<FilterGroup>(
std::unordered_set<std::string>({kTtsAudioDeviceId}),
AudioFilterFactory::TTS_AUDIO_FILTER));
filter_groups_.push_back(base::MakeUnique<FilterGroup>(
std::unordered_set<std::string>(
{::media::AudioDeviceDescription::kDefaultDeviceId,
kEarconAudioDeviceId, ""}),
AudioFilterFactory::MEDIA_AUDIO_FILTER));
DefineAlsaParameters();
}
void StreamMixerAlsa::ResetTaskRunnerForTest() {
mixer_task_runner_ = base::ThreadTaskRunnerHandle::Get();
}
void StreamMixerAlsa::DefineAlsaParameters() {
// Get the ALSA output configuration from the command line.
int buffer_size;
GetSwitchValueAsNonNegativeInt(switches::kAlsaOutputBufferSize,
kDefaultOutputBufferSizeFrames, &buffer_size);
alsa_buffer_size_ = buffer_size;
int period_size;
if (GetSwitchValueAsNonNegativeInt(switches::kAlsaOutputPeriodSize,
alsa_buffer_size_ / 16, &period_size)) {
if (period_size >= buffer_size) {
LOG(DFATAL) << "ALSA period size must be smaller than the buffer size";
period_size = buffer_size / 2;
} else {
alsa_period_explicitly_set = true;
}
}
alsa_period_size_ = period_size;
int start_threshold;
GetSwitchValueAsNonNegativeInt(switches::kAlsaOutputStartThreshold,
(buffer_size / period_size) * period_size,
&start_threshold);
if (start_threshold > buffer_size) {
LOG(DFATAL) << "ALSA start threshold must be no larger than "
<< "the buffer size";
start_threshold = (buffer_size / period_size) * period_size;
}
alsa_start_threshold_ = start_threshold;
// By default, allow the transfer when at least period_size samples can be
// processed.
int avail_min;
GetSwitchValueAsNonNegativeInt(switches::kAlsaOutputAvailMin, period_size,
&avail_min);
if (avail_min > buffer_size) {
LOG(DFATAL) << "ALSA avail min must be no larger than the buffer size";
avail_min = alsa_period_size_;
}
alsa_avail_min_ = avail_min;
// --accept-resource-provider should imply a check close timeout of 0.
int default_close_timeout = chromecast::GetSwitchValueBoolean(
switches::kAcceptResourceProvider, false)
? 0
: kDefaultCheckCloseTimeoutMs;
GetSwitchValueAsInt(switches::kAlsaCheckCloseTimeout, default_close_timeout,
&check_close_timeout_);
}
unsigned int StreamMixerAlsa::DetermineOutputRate(unsigned int requested_rate) {
if (fixed_output_samples_per_second_ != kInvalidSampleRate) {
LOG(INFO) << "Requested output rate is " << requested_rate;
LOG(INFO) << "Cannot change rate since it is fixed to "
<< fixed_output_samples_per_second_;
return fixed_output_samples_per_second_;
}
unsigned int unsigned_output_samples_per_second = requested_rate;
// Try the requested sample rate. If the ALSA driver doesn't know how to deal
// with it, try the nearest supported sample rate instead. Lastly, try some
// common sample rates as a fallback. Note that PcmHwParamsSetRateNear
// doesn't always choose a rate that's actually near the given input sample
// rate when the input sample rate is not supported.
const int* kSupportedSampleRatesEnd =
kSupportedSampleRates + arraysize(kSupportedSampleRates);
auto* nearest_sample_rate =
std::min_element(kSupportedSampleRates, kSupportedSampleRatesEnd,
[this](int r1, int r2) -> bool {
return abs(requested_output_samples_per_second_ - r1) <
abs(requested_output_samples_per_second_ - r2);
});
// Resample audio with sample rates deemed to be too low (i.e. below 32kHz)
// because some common AV receivers don't support optical out at these
// frequencies. See b/26385501
unsigned int first_choice_sample_rate = requested_rate;
if (requested_rate < low_sample_rate_cutoff_) {
first_choice_sample_rate = output_samples_per_second_ != kInvalidSampleRate
? output_samples_per_second_
: kFallbackSampleRate;
}
const unsigned int preferred_sample_rates[] = {
first_choice_sample_rate,
static_cast<unsigned int>(*nearest_sample_rate),
kFallbackSampleRateHiRes,
kFallbackSampleRate};
int err;
for (const auto& sample_rate : preferred_sample_rates) {
err = alsa_->PcmHwParamsTestRate(pcm_, pcm_hw_params_, sample_rate,
0 /* try exact rate */);
if (err == 0) {
unsigned_output_samples_per_second = sample_rate;
break;
}
}
LOG_IF(ERROR, err != 0) << "Even the fallback sample rate isn't supported! "
<< "Have you tried /bin/alsa_api_test on-device?";
return unsigned_output_samples_per_second;
}
int StreamMixerAlsa::SetAlsaPlaybackParams() {
int err = 0;
// Set hardware parameters.
DCHECK(pcm_);
DCHECK(!pcm_hw_params_);
RETURN_ERROR_CODE(PcmHwParamsMalloc, &pcm_hw_params_);
RETURN_ERROR_CODE(PcmHwParamsAny, pcm_, pcm_hw_params_);
RETURN_ERROR_CODE(PcmHwParamsSetAccess, pcm_, pcm_hw_params_,
SND_PCM_ACCESS_RW_INTERLEAVED);
if (pcm_format_ == SND_PCM_FORMAT_UNKNOWN) {
for (const auto& pcm_format : kPreferredSampleFormats) {
err = alsa_->PcmHwParamsTestFormat(pcm_, pcm_hw_params_, pcm_format);
if (err < 0) {
LOG(WARNING) << "PcmHwParamsTestFormat: " << alsa_->StrError(err);
} else {
pcm_format_ = pcm_format;
break;
}
}
if (pcm_format_ == SND_PCM_FORMAT_UNKNOWN) {
LOG(ERROR) << "Could not find a valid PCM format. Running "
<< "/bin/alsa_api_test may be instructive.";
return err;
}
}
RETURN_ERROR_CODE(PcmHwParamsSetFormat, pcm_, pcm_hw_params_, pcm_format_);
RETURN_ERROR_CODE(PcmHwParamsSetChannels, pcm_, pcm_hw_params_,
kNumOutputChannels);
// Set output rate, allow resampling with a warning if the device doesn't
// support the rate natively.
RETURN_ERROR_CODE(PcmHwParamsSetRateResample, pcm_, pcm_hw_params_,
false /* Don't allow resampling. */);
unsigned int requested_rate =
static_cast<unsigned int>(requested_output_samples_per_second_);
unsigned int unsigned_output_samples_per_second =
DetermineOutputRate(requested_rate);
RETURN_ERROR_CODE(PcmHwParamsSetRateNear, pcm_, pcm_hw_params_,
&unsigned_output_samples_per_second, kAlsaDirDontCare);
if (requested_rate != unsigned_output_samples_per_second) {
LOG(WARNING) << "Requested sample rate (" << requested_rate
<< " Hz) does not match the actual sample rate ("
<< unsigned_output_samples_per_second
<< " Hz). This may lead to lower audio quality.";
}
LOG(INFO) << "Sample rate changed from " << output_samples_per_second_
<< " to " << unsigned_output_samples_per_second;
output_samples_per_second_ =
static_cast<int>(unsigned_output_samples_per_second);
snd_pcm_uframes_t requested_buffer_size = alsa_buffer_size_;
RETURN_ERROR_CODE(PcmHwParamsSetBufferSizeNear, pcm_, pcm_hw_params_,
&alsa_buffer_size_);
if (requested_buffer_size != alsa_buffer_size_) {
LOG(WARNING) << "Requested buffer size (" << requested_buffer_size
<< " frames) does not match the actual buffer size ("
<< alsa_buffer_size_
<< " frames). This may lead to an increase in "
"either audio latency or audio underruns.";
// Always try to use the value for period_size that was passed in on the
// command line, if any.
if (!alsa_period_explicitly_set) {
alsa_period_size_ = alsa_buffer_size_ / 16;
} else if (alsa_period_size_ >= alsa_buffer_size_) {
snd_pcm_uframes_t new_period_size = alsa_buffer_size_ / 2;
LOG(DFATAL) << "Configured period size (" << alsa_period_size_
<< ") is >= actual buffer size (" << alsa_buffer_size_
<< "); reducing to " << new_period_size;
alsa_period_size_ = new_period_size;
}
// Scale the start threshold and avail_min based on the new buffer size.
float original_buffer_size = static_cast<float>(requested_buffer_size);
float avail_min_ratio = original_buffer_size / alsa_avail_min_;
alsa_avail_min_ = alsa_buffer_size_ / avail_min_ratio;
float start_threshold_ratio = original_buffer_size / alsa_start_threshold_;
alsa_start_threshold_ = alsa_buffer_size_ / start_threshold_ratio;
}
snd_pcm_uframes_t requested_period_size = alsa_period_size_;
RETURN_ERROR_CODE(PcmHwParamsSetPeriodSizeNear, pcm_, pcm_hw_params_,
&alsa_period_size_, kAlsaDirDontCare);
if (requested_period_size != alsa_period_size_) {
LOG(WARNING) << "Requested period size (" << requested_period_size
<< " frames) does not match the actual period size ("
<< alsa_period_size_
<< " frames). This may lead to an increase in "
"CPU usage or an increase in audio latency.";
}
RETURN_ERROR_CODE(PcmHwParams, pcm_, pcm_hw_params_);
// Set software parameters.
snd_pcm_sw_params_t* swparams;
RETURN_ERROR_CODE(PcmSwParamsMalloc, &swparams);
RETURN_ERROR_CODE(PcmSwParamsCurrent, pcm_, swparams);
RETURN_ERROR_CODE(PcmSwParamsSetStartThreshold, pcm_, swparams,
alsa_start_threshold_);
if (alsa_start_threshold_ > alsa_buffer_size_) {
LOG(ERROR) << "Requested start threshold (" << alsa_start_threshold_
<< " frames) is larger than the buffer size ("
<< alsa_buffer_size_
<< " frames). Audio playback will not start.";
}
RETURN_ERROR_CODE(PcmSwParamsSetAvailMin, pcm_, swparams, alsa_avail_min_);
RETURN_ERROR_CODE(PcmSwParamsSetTstampMode, pcm_, swparams,
SND_PCM_TSTAMP_ENABLE);
RETURN_ERROR_CODE(PcmSwParamsSetTstampType, pcm_, swparams,
kAlsaTstampTypeMonotonicRaw);
err = alsa_->PcmSwParams(pcm_, swparams);
alsa_->PcmSwParamsFree(swparams);
return err;
}
StreamMixerAlsa::~StreamMixerAlsa() {
FinalizeOnMixerThread();
mixer_thread_->Stop();
mixer_task_runner_ = nullptr;
}
void StreamMixerAlsa::FinalizeOnMixerThread() {
RUN_ON_MIXER_THREAD(&StreamMixerAlsa::FinalizeOnMixerThread);
Close();
// Post a task to allow any pending input deletions to run.
POST_TASK_TO_MIXER_THREAD(&StreamMixerAlsa::FinishFinalize);
}
void StreamMixerAlsa::FinishFinalize() {
retry_write_frames_timer_.reset();
check_close_timer_.reset();
inputs_.clear();
ignored_inputs_.clear();
}
void StreamMixerAlsa::Start() {
DCHECK(mixer_task_runner_->BelongsToCurrentThread());
if (!pcm_) {
RETURN_REPORT_ERROR(PcmOpen, &pcm_, alsa_device_name_.c_str(),
SND_PCM_STREAM_PLAYBACK, 0);
LOG(INFO) << "snd_pcm_open: handle=" << pcm_;
}
// Some OEM-developed Cast for Audio devices don't accurately report their
// support for different output formats, so this tries 32-bit output and then
// 16-bit output if that fails.
//
// TODO(cleichner): Replace this with more specific device introspection.
// b/24747205
int err = SetAlsaPlaybackParams();
if (err < 0) {
LOG(WARNING) << "32-bit playback is not supported on this device, falling "
"back to 16-bit playback. This can degrade audio quality.";
pcm_format_ = SND_PCM_FORMAT_S16;
// Free pcm_hw_params_, which is re-allocated in SetAlsaPlaybackParams().
// See b/25572466.
alsa_->PcmHwParamsFree(pcm_hw_params_);
pcm_hw_params_ = nullptr;
int err = SetAlsaPlaybackParams();
if (err < 0) {
LOG(ERROR) << "Error setting ALSA playback parameters: "
<< alsa_->StrError(err);
SignalError();
return;
}
}
// Initialize filters
for (auto&& filter_group : filter_groups_) {
filter_group->Initialize(output_samples_per_second_,
::media::SampleFormat::kSampleFormatS32);
}
RETURN_REPORT_ERROR(PcmPrepare, pcm_);
RETURN_REPORT_ERROR(PcmStatusMalloc, &pcm_status_);
rendering_delay_.timestamp_microseconds = kNoTimestamp;
rendering_delay_.delay_microseconds = 0;
state_ = kStateNormalPlayback;
}
void StreamMixerAlsa::Stop() {
for (auto* observer : loopback_observers_) {
observer->OnLoopbackInterrupted();
}
if (alsa_) {
alsa_->PcmStatusFree(pcm_status_);
alsa_->PcmHwParamsFree(pcm_hw_params_);
}
pcm_status_ = nullptr;
pcm_hw_params_ = nullptr;
state_ = kStateUninitialized;
output_samples_per_second_ = kInvalidSampleRate;
if (!pcm_) {
return;
}
// If |pcm_| is RUNNING, drain all pending data.
if (alsa_->PcmState(pcm_) == SND_PCM_STATE_RUNNING) {
int err = alsa_->PcmDrain(pcm_);
if (err < 0) {
LOG(ERROR) << "snd_pcm_drain error: " << alsa_->StrError(err);
}
} else {
int err = alsa_->PcmDrop(pcm_);
if (err < 0) {
LOG(ERROR) << "snd_pcm_drop error: " << alsa_->StrError(err);
}
}
}
void StreamMixerAlsa::Close() {
Stop();
if (!pcm_) {
return;
}
LOG(INFO) << "snd_pcm_close: handle=" << pcm_;
int err = alsa_->PcmClose(pcm_);
if (err < 0) {
LOG(ERROR) << "snd_pcm_close error, leaking handle: "
<< alsa_->StrError(err);
}
pcm_ = nullptr;
}
void StreamMixerAlsa::SignalError() {
state_ = kStateError;
retry_write_frames_timer_->Stop();
for (auto&& input : inputs_) {
input->SignalError(StreamMixerAlsaInput::MixerError::kInternalError);
ignored_inputs_.push_back(std::move(input));
}
inputs_.clear();
POST_TASK_TO_MIXER_THREAD(&StreamMixerAlsa::Close);
}
void StreamMixerAlsa::SetAlsaWrapperForTest(
std::unique_ptr<AlsaWrapper> alsa_wrapper) {
if (alsa_) {
Close();
}
alsa_ = std::move(alsa_wrapper);
}
void StreamMixerAlsa::WriteFramesForTest() {
RUN_ON_MIXER_THREAD(&StreamMixerAlsa::WriteFramesForTest);
WriteFrames();
}
void StreamMixerAlsa::ClearInputsForTest() {
RUN_ON_MIXER_THREAD(&StreamMixerAlsa::ClearInputsForTest);
inputs_.clear();
}
void StreamMixerAlsa::AddInput(std::unique_ptr<InputQueue> input) {
RUN_ON_MIXER_THREAD(&StreamMixerAlsa::AddInput,
base::Passed(std::move(input)));
if (!alsa_) {
alsa_.reset(new AlsaWrapper());
}
DCHECK(input);
// If the new input is a primary one, we may need to change the output
// sample rate to match its input sample rate.
// We only change the output rate if it is not set to a fixed value.
if (input->primary() &&
fixed_output_samples_per_second_ == kInvalidSampleRate) {
CheckChangeOutputRate(input->input_samples_per_second());
}
check_close_timer_->Stop();
switch (state_) {
case kStateUninitialized:
requested_output_samples_per_second_ = input->input_samples_per_second();
Start();
// Fallthrough intended
case kStateNormalPlayback: {
bool found_filter_group = false;
input->Initialize(rendering_delay_);
for (auto&& filter_group : filter_groups_) {
if (filter_group->CanProcessInput(input.get())) {
found_filter_group = true;
input->set_filter_group(filter_group.get());
break;
}
}
DCHECK(found_filter_group) << "Could not find a filter group for "
<< input->device_id();
inputs_.push_back(std::move(input));
} break;
case kStateError:
input->SignalError(StreamMixerAlsaInput::MixerError::kInternalError);
ignored_inputs_.push_back(std::move(input));
break;
default:
NOTREACHED();
}
}
void StreamMixerAlsa::CheckChangeOutputRate(int input_samples_per_second) {
DCHECK(mixer_task_runner_->BelongsToCurrentThread());
if (!pcm_ ||
input_samples_per_second == requested_output_samples_per_second_ ||
input_samples_per_second == output_samples_per_second_ ||
input_samples_per_second < static_cast<int>(low_sample_rate_cutoff_)) {
return;
}
for (auto&& input : inputs_) {
if (input->primary() && !input->IsDeleting()) {
return;
}
}
// Move all current inputs to the ignored list
for (auto&& input : inputs_) {
LOG(INFO) << "Mixer input " << input.get()
<< " now being ignored due to output sample rate change from "
<< output_samples_per_second_ << " to "
<< input_samples_per_second;
input->SignalError(StreamMixerAlsaInput::MixerError::kInputIgnored);
ignored_inputs_.push_back(std::move(input));
}
inputs_.clear();
requested_output_samples_per_second_ = input_samples_per_second;
// Reset the ALSA params so that the new output sample rate takes effect.
Stop();
Start();
}
void StreamMixerAlsa::RemoveInput(InputQueue* input) {
RUN_ON_MIXER_THREAD(&StreamMixerAlsa::RemoveInput, input);
DCHECK(input);
DCHECK(!input->IsDeleting());
input->PrepareToDelete(
base::Bind(&StreamMixerAlsa::DeleteInputQueue, base::Unretained(this)));
}
void StreamMixerAlsa::DeleteInputQueue(InputQueue* input) {
// Always post a task, in case an input calls this while we are iterating
// through the |inputs_| list.
POST_TASK_TO_MIXER_THREAD(&StreamMixerAlsa::DeleteInputQueueInternal, input);
}
void StreamMixerAlsa::DeleteInputQueueInternal(InputQueue* input) {
DCHECK(input);
DCHECK(mixer_task_runner_->BelongsToCurrentThread());
auto match_input = [input](const std::unique_ptr<InputQueue>& item) {
return item.get() == input;
};
auto it = std::find_if(inputs_.begin(), inputs_.end(), match_input);
if (it == inputs_.end()) {
it = std::find_if(ignored_inputs_.begin(), ignored_inputs_.end(),
match_input);
DCHECK(it != ignored_inputs_.end());
ignored_inputs_.erase(it);
} else {
inputs_.erase(it);
}
if (inputs_.empty()) {
// Never close if timeout is negative
if (check_close_timeout_ >= 0) {
check_close_timer_->Start(
FROM_HERE, base::TimeDelta::FromMilliseconds(check_close_timeout_),
base::Bind(&StreamMixerAlsa::CheckClose, base::Unretained(this)));
}
}
}
void StreamMixerAlsa::CheckClose() {
DCHECK(mixer_task_runner_->BelongsToCurrentThread());
DCHECK(inputs_.empty());
retry_write_frames_timer_->Stop();
Close();
}
void StreamMixerAlsa::OnFramesQueued() {
if (state_ != kStateNormalPlayback) {
return;
}
if (retry_write_frames_timer_->IsRunning()) {
return;
}
retry_write_frames_timer_->Start(
FROM_HERE, base::TimeDelta(),
base::Bind(&StreamMixerAlsa::WriteFrames, base::Unretained(this)));
}
void StreamMixerAlsa::WriteFrames() {
retry_write_frames_timer_->Stop();
if (TryWriteFrames()) {
retry_write_frames_timer_->Start(
FROM_HERE, base::TimeDelta(),
base::Bind(&StreamMixerAlsa::WriteFrames, base::Unretained(this)));
}
}
bool StreamMixerAlsa::TryWriteFrames() {
DCHECK(mixer_task_runner_->BelongsToCurrentThread());
DCHECK_GE(filter_groups_.size(), 1u);
if (state_ != kStateNormalPlayback) {
return false;
}
const int min_frames_in_buffer =
output_samples_per_second_ * kMinBufferedDataMs / 1000;
int chunk_size = output_samples_per_second_ * kMaxWriteSizeMs / 1000;
bool is_silence = true;
for (auto&& filter_group : filter_groups_) {
filter_group->ClearActiveInputs();
}
for (auto&& input : inputs_) {
int read_size = input->MaxReadSize();
if (read_size > 0) {
DCHECK(input->filter_group());
input->filter_group()->AddActiveInput(input.get());
chunk_size = std::min(chunk_size, read_size);
is_silence = false;
} else if (input->primary()) {
if (alsa_->PcmStatus(pcm_, pcm_status_) != 0) {
LOG(ERROR) << "Failed to get status";
return false;
}
int frames_in_buffer =
alsa_buffer_size_ - alsa_->PcmStatusGetAvail(pcm_status_);
if (alsa_->PcmStatusGetState(pcm_status_) == SND_PCM_STATE_XRUN ||
frames_in_buffer < min_frames_in_buffer) {
// If there has been (or soon will be) an underrun, continue without the
// empty primary input stream.
input->OnSkipped();
continue;
}
// A primary input cannot provide any data, so wait until later.
retry_write_frames_timer_->Start(
FROM_HERE, base::TimeDelta::FromMilliseconds(kMinBufferedDataMs / 2),
base::Bind(&StreamMixerAlsa::WriteFrames, base::Unretained(this)));
return false;
} else {
input->OnSkipped();
}
}
if (is_silence) {
// No inputs have any data to provide. Push silence to prevent underrun.
chunk_size = kPreventUnderrunChunkSize;
}
// Mix and filter each group.
std::vector<uint8_t>* interleaved = nullptr;
for (auto&& filter_group : filter_groups_) {
if (filter_group->MixAndFilter(chunk_size)) {
if (!interleaved) {
interleaved = filter_group->GetInterleaved();
} else {
DCHECK_EQ(4, BytesPerOutputFormatSample());
VectorAccumulate(
reinterpret_cast<int32_t*>(filter_group->GetInterleaved()->data()),
chunk_size * kNumOutputChannels,
reinterpret_cast<int32_t*>(interleaved->data()));
}
}
}
if (!interleaved) {
// No group has any data, write empty buffer.
filter_groups_[0]->ClearInterleaved(chunk_size);
interleaved = filter_groups_[0]->GetInterleaved();
}
WriteMixedPcm(interleaved, chunk_size);
return true;
}
size_t StreamMixerAlsa::InterleavedSize(int frames) {
return BytesPerOutputFormatSample() *
static_cast<size_t>(frames * kNumOutputChannels);
}
ssize_t StreamMixerAlsa::BytesPerOutputFormatSample() {
return alsa_->PcmFormatSize(pcm_format_, 1);
}
void StreamMixerAlsa::WriteMixedPcm(std::vector<uint8_t>* interleaved,
int frames) {
DCHECK(mixer_task_runner_->BelongsToCurrentThread());
CHECK_PCM_INITIALIZED();
DCHECK(interleaved);
DCHECK_GE(interleaved->size(), InterleavedSize(frames));
int64_t expected_playback_time;
if (rendering_delay_.timestamp_microseconds == kNoTimestamp) {
expected_playback_time = kNoTimestamp;
} else {
expected_playback_time = rendering_delay_.timestamp_microseconds +
rendering_delay_.delay_microseconds;
}
for (CastMediaShlib::LoopbackAudioObserver* observer : loopback_observers_) {
observer->OnLoopbackAudio(expected_playback_time, kSampleFormatS32,
output_samples_per_second_, kNumOutputChannels,
interleaved->data(), InterleavedSize(frames));
}
// If the PCM has been drained it will be in SND_PCM_STATE_SETUP and need
// to be prepared in order for playback to work.
if (alsa_->PcmState(pcm_) == SND_PCM_STATE_SETUP) {
RETURN_REPORT_ERROR(PcmPrepare, pcm_);
}
int frames_left = frames;
uint8_t* data = interleaved->data();
while (frames_left) {
int frames_or_error;
while ((frames_or_error = alsa_->PcmWritei(pcm_, data, frames_left)) < 0) {
for (auto* observer : loopback_observers_) {
observer->OnLoopbackInterrupted();
}
RETURN_REPORT_ERROR(PcmRecover, pcm_, frames_or_error,
kPcmRecoverIsSilent);
}
frames_left -= frames_or_error;
DCHECK_GE(frames_left, 0);
data += frames_or_error * kNumOutputChannels * BytesPerOutputFormatSample();
}
UpdateRenderingDelay(frames);
for (auto&& input : inputs_)
input->AfterWriteFrames(rendering_delay_);
}
void StreamMixerAlsa::UpdateRenderingDelay(int newly_pushed_frames) {
DCHECK(mixer_task_runner_->BelongsToCurrentThread());
CHECK_PCM_INITIALIZED();
if (alsa_->PcmStatus(pcm_, pcm_status_) != 0 ||
alsa_->PcmStatusGetState(pcm_status_) != SND_PCM_STATE_RUNNING) {
rendering_delay_.timestamp_microseconds = kNoTimestamp;
rendering_delay_.delay_microseconds = 0;
return;
}
snd_htimestamp_t status_timestamp = {};
alsa_->PcmStatusGetHtstamp(pcm_status_, &status_timestamp);
rendering_delay_.timestamp_microseconds =
TimespecToMicroseconds(status_timestamp);
snd_pcm_sframes_t delay_frames = alsa_->PcmStatusGetDelay(pcm_status_);
rendering_delay_.delay_microseconds = static_cast<int64_t>(delay_frames) *
base::Time::kMicrosecondsPerSecond /
output_samples_per_second_;
}
void StreamMixerAlsa::AddLoopbackAudioObserver(
CastMediaShlib::LoopbackAudioObserver* observer) {
RUN_ON_MIXER_THREAD(&StreamMixerAlsa::AddLoopbackAudioObserver, observer);
DCHECK(observer);
DCHECK(std::find(loopback_observers_.begin(), loopback_observers_.end(),
observer) == loopback_observers_.end());
loopback_observers_.push_back(observer);
}
void StreamMixerAlsa::RemoveLoopbackAudioObserver(
CastMediaShlib::LoopbackAudioObserver* observer) {
RUN_ON_MIXER_THREAD(&StreamMixerAlsa::RemoveLoopbackAudioObserver, observer);
DCHECK(std::find(loopback_observers_.begin(), loopback_observers_.end(),
observer) != loopback_observers_.end());
loopback_observers_.erase(std::remove(loopback_observers_.begin(),
loopback_observers_.end(), observer),
loopback_observers_.end());
observer->OnRemoved();
}
} // namespace media
} // namespace chromecast