// Copyright 2017 The Chromium Authors
// 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/mixer_output_stream_alsa.h"
#include <algorithm>
#include <limits>
#include <string>
#include "base/command_line.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "chromecast/base/chromecast_switches.h"
#include "chromecast/media/cma/backend/alsa/alsa_wrapper.h"
#include "media/base/audio_sample_types.h"
#include "media/base/media_switches.h"
#define RETURN_FALSE_ON_ERROR(snd_func, ...) \
do { \
int a_err = alsa_->snd_func(__VA_ARGS__); \
if (a_err < 0) { \
LOG(ERROR) << #snd_func " error: " << alsa_->StrError(a_err); \
return false; \
} \
} while (0)
#define RETURN_ERROR_CODE(snd_func, ...) \
do { \
int a_err = alsa_->snd_func(__VA_ARGS__); \
if (a_err < 0) { \
LOG(ERROR) << #snd_func " error: " << alsa_->StrError(a_err); \
return a_err; \
} \
} while (0)
#define CHECK_PCM_INITIALIZED() \
if (!pcm_ || !pcm_hw_params_) { \
LOG(WARNING) << __FUNCTION__ << "() called after failed initialization"; \
return false; \
}
namespace chromecast {
namespace media {
namespace {
template <class TargetSampleTypeTraits>
void ToFixedPoint(const float* input,
int frames,
typename TargetSampleTypeTraits::ValueType* dest_buffer) {
for (int f = 0; f < frames; ++f) {
dest_buffer[f] = TargetSampleTypeTraits::FromFloat(input[f]);
}
}
void ToFixedPoint(const float* input,
int frames,
int bytes_per_sample,
uint8_t* dest_buffer) {
switch (bytes_per_sample) {
case 1:
ToFixedPoint<::media::UnsignedInt8SampleTypeTraits>(
input, frames, reinterpret_cast<uint8_t*>(dest_buffer));
break;
case 2:
ToFixedPoint<::media::SignedInt16SampleTypeTraits>(
input, frames, reinterpret_cast<int16_t*>(dest_buffer));
break;
case 4:
ToFixedPoint<::media::SignedInt32SampleTypeTraits>(
input, frames, reinterpret_cast<int32_t*>(dest_buffer));
break;
default:
NOTREACHED_IN_MIGRATION()
<< "Unsupported bytes per sample encountered: " << bytes_per_sample;
}
}
constexpr int64_t kNoTimestamp = std::numeric_limits<int64_t>::min();
constexpr char kOutputDeviceDefaultName[] = "default";
constexpr bool kPcmRecoverIsSilent = false;
constexpr int kDefaultOutputBufferSizeFrames = 1024;
// 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
constexpr int kSupportedSampleRates[] =
{ 8000, 11025, 12000,
16000, 22050, 24000,
32000, 44100, 48000,
64000, 88200, 96000};
// clang-format on
// 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.
constexpr unsigned int kFallbackSampleRate = 48000;
constexpr unsigned int kFallbackSampleRateHiRes = 96000;
// 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.
constexpr int* kAlsaDirDontCare = nullptr;
// The snd_pcm_resume function can return EAGAIN error code, so call should be
// retried. Below constants define retries params.
constexpr int kRestoreAfterSuspensionAttempts = 10;
constexpr base::TimeDelta kRestoreAfterSuspensionAttemptDelay =
base::Milliseconds(20);
// These sample formats will be tried in order. 32 bit samples is ideal, but
// some devices do not support 32 bit samples.
constexpr snd_pcm_format_t kPreferredSampleFormats[] = {
SND_PCM_FORMAT_FLOAT, SND_PCM_FORMAT_S32, SND_PCM_FORMAT_S16};
int64_t TimespecToMicroseconds(struct timespec time) {
return static_cast<int64_t>(time.tv_sec) *
base::Time::kMicrosecondsPerSecond +
time.tv_nsec / 1000;
}
} // namespace
// static
std::unique_ptr<MixerOutputStream> MixerOutputStream::Create() {
return std::make_unique<MixerOutputStreamAlsa>();
}
MixerOutputStreamAlsa::MixerOutputStreamAlsa() {
DefineAlsaParameters();
}
MixerOutputStreamAlsa::~MixerOutputStreamAlsa() {
Stop();
}
void MixerOutputStreamAlsa::SetAlsaWrapperForTest(
std::unique_ptr<AlsaWrapper> alsa) {
DCHECK(!alsa_);
alsa_ = std::move(alsa);
}
bool MixerOutputStreamAlsa::Start(int sample_rate, int channels) {
if (!alsa_) {
alsa_ = std::make_unique<AlsaWrapper>();
}
num_output_channels_ = channels;
// Open PCM devices when Start() is called for the first time.
if (!pcm_) {
std::string device_name = kOutputDeviceDefaultName;
if (base::CommandLine::InitializedForCurrentProcess() &&
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kAlsaOutputDevice)) {
device_name = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kAlsaOutputDevice);
}
RETURN_FALSE_ON_ERROR(PcmOpen, &pcm_, 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(sample_rate);
if (err < 0) {
LOG(ERROR) << "Error setting ALSA playback parameters: "
<< alsa_->StrError(err);
return false;
}
RETURN_FALSE_ON_ERROR(PcmPrepare, pcm_);
RETURN_FALSE_ON_ERROR(PcmStatusMalloc, &pcm_status_);
rendering_delay_.timestamp_microseconds = kNoTimestamp;
rendering_delay_.delay_microseconds = 0;
first_write_ = true;
return true;
}
int MixerOutputStreamAlsa::GetNumChannels() {
return num_output_channels_;
}
int MixerOutputStreamAlsa::GetSampleRate() {
return sample_rate_;
}
MediaPipelineBackend::AudioDecoder::RenderingDelay
MixerOutputStreamAlsa::GetRenderingDelay() {
return rendering_delay_;
}
int MixerOutputStreamAlsa::OptimalWriteFramesCount() {
CHECK_PCM_INITIALIZED();
return alsa_period_size_;
}
bool MixerOutputStreamAlsa::Write(const float* data,
int data_size,
bool* out_playback_interrupted) {
CHECK_PCM_INITIALIZED();
*out_playback_interrupted = false;
int frames = data_size / num_output_channels_;
ssize_t bytes_per_sample = alsa_->PcmFormatSize(pcm_format_, 1);
const uint8_t* output_data;
if (pcm_format_ == SND_PCM_FORMAT_FLOAT) {
output_data = reinterpret_cast<const uint8_t*>(data);
} else {
// Resize interleaved if necessary.
size_t output_data_size = data_size * bytes_per_sample;
if (output_buffer_.size() < output_data_size) {
output_buffer_.resize(output_data_size);
}
ToFixedPoint(data, data_size, bytes_per_sample, output_buffer_.data());
output_data = output_buffer_.data();
}
// 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_FALSE_ON_ERROR(PcmPrepare, pcm_);
}
int frames_left = frames;
while (frames_left) {
int frames_or_error;
while ((frames_or_error =
alsa_->PcmWritei(pcm_, output_data, frames_left)) < 0) {
if (!first_write_) {
*out_playback_interrupted = true;
}
if (frames_or_error == -EBADFD &&
MaybeRecoverDeviceFromSuspendedState()) {
// Write data again, if recovered.
continue;
}
RETURN_FALSE_ON_ERROR(PcmRecover, pcm_, frames_or_error,
kPcmRecoverIsSilent);
}
frames_left -= frames_or_error;
DCHECK_GE(frames_left, 0);
output_data += frames_or_error * num_output_channels_ * bytes_per_sample;
}
first_write_ = false;
UpdateRenderingDelay();
return true;
}
void MixerOutputStreamAlsa::Stop() {
if (alsa_) {
alsa_->PcmStatusFree(pcm_status_);
alsa_->PcmHwParamsFree(pcm_hw_params_);
}
pcm_status_ = nullptr;
pcm_hw_params_ = nullptr;
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);
}
}
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;
}
int MixerOutputStreamAlsa::SetAlsaPlaybackParams(int requested_sample_rate) {
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_,
num_output_channels_);
// 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 new_sample_rate = DetermineOutputRate(requested_sample_rate);
RETURN_ERROR_CODE(PcmHwParamsSetRateNear, pcm_, pcm_hw_params_,
&new_sample_rate, kAlsaDirDontCare);
if (requested_sample_rate != static_cast<int>(new_sample_rate)) {
LOG(WARNING) << "Requested sample rate (" << requested_sample_rate
<< " Hz) does not match the actual sample rate ("
<< new_sample_rate
<< " Hz). This may lead to lower audio quality.";
}
LOG(INFO) << "Sample rate changed from " << sample_rate_ << " to "
<< new_sample_rate;
sample_rate_ = static_cast<int>(new_sample_rate);
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.";
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;
}
void MixerOutputStreamAlsa::DefineAlsaParameters() {
// Get the ALSA output configuration from the command line.
if (base::CommandLine::InitializedForCurrentProcess()) {
alsa_buffer_size_ = GetSwitchValueNonNegativeInt(
switches::kAlsaOutputBufferSize, kDefaultOutputBufferSizeFrames);
alsa_period_size_ = GetSwitchValueNonNegativeInt(
switches::kAlsaOutputPeriodSize, alsa_buffer_size_ / 2);
} else {
alsa_buffer_size_ = kDefaultOutputBufferSizeFrames;
alsa_period_size_ = alsa_buffer_size_ / 2;
}
if (alsa_period_size_ >= alsa_buffer_size_) {
LOG(DFATAL) << "ALSA period size must be smaller than the buffer size";
alsa_period_size_ = alsa_buffer_size_ / 2;
}
LOG(INFO) << "ALSA buffer = " << alsa_buffer_size_
<< ", period = " << alsa_period_size_;
if (base::CommandLine::InitializedForCurrentProcess()) {
alsa_start_threshold_ = GetSwitchValueNonNegativeInt(
switches::kAlsaOutputStartThreshold,
(alsa_buffer_size_ / alsa_period_size_) * alsa_period_size_);
} else {
alsa_start_threshold_ =
(alsa_buffer_size_ / alsa_period_size_) * alsa_period_size_;
}
if (alsa_start_threshold_ > alsa_buffer_size_) {
LOG(DFATAL) << "ALSA start threshold must be no larger than "
<< "the buffer size";
alsa_start_threshold_ =
(alsa_buffer_size_ / alsa_period_size_) * alsa_period_size_;
}
// By default, allow the transfer when at least period_size samples can be
// processed.
if (base::CommandLine::InitializedForCurrentProcess()) {
alsa_avail_min_ = GetSwitchValueNonNegativeInt(
switches::kAlsaOutputAvailMin, alsa_period_size_);
} else {
alsa_avail_min_ = alsa_period_size_;
}
if (alsa_avail_min_ > alsa_buffer_size_) {
LOG(DFATAL) << "ALSA avail min must be no larger than the buffer size";
alsa_avail_min_ = alsa_period_size_;
}
}
int MixerOutputStreamAlsa::DetermineOutputRate(int requested_sample_rate) {
unsigned int unsigned_output_sample_rate = requested_sample_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 + std::size(kSupportedSampleRates);
auto* nearest_sample_rate =
std::min_element(kSupportedSampleRates, kSupportedSampleRatesEnd,
[requested_sample_rate](int r1, int r2) -> bool {
return abs(requested_sample_rate - r1) <
abs(requested_sample_rate - 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_sample_rate;
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_sample_rate = 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_sample_rate;
}
void MixerOutputStreamAlsa::UpdateRenderingDelay() {
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);
if (status_timestamp.tv_sec == 0 && status_timestamp.tv_nsec == 0) {
// ALSA didn't actually give us a timestamp.
rendering_delay_.timestamp_microseconds = kNoTimestamp;
rendering_delay_.delay_microseconds = 0;
return;
}
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 /
sample_rate_;
}
bool MixerOutputStreamAlsa::MaybeRecoverDeviceFromSuspendedState() {
if (alsa_->PcmState(pcm_) != SND_PCM_STATE_SUSPENDED) {
LOG(WARNING) << "Alsa output is not suspended";
return false;
}
if (alsa_->PcmHwParamsCanResume(pcm_hw_params_)) {
LOG(INFO) << "Trying to resume output";
for (int attempt = 0; attempt < kRestoreAfterSuspensionAttempts;
++attempt) {
int err = alsa_->PcmResume(pcm_);
if (err == 0) {
LOG(INFO) << "ALSA output is resumed from suspended state";
return true;
}
if (err != -EAGAIN) {
// If PcmResume failed or device doesn't support resume, try to use
// PcmPrepare.
err = alsa_->PcmPrepare(pcm_);
LOG_IF(INFO, err == 0)
<< "ALSA output is recovered from suspended state";
return err == 0;
}
base::PlatformThread::Sleep(kRestoreAfterSuspensionAttemptDelay);
}
}
return false;
}
} // namespace media
} // namespace chromecast