#include "media/audio/linux/alsa_output.h"
#include <algorithm>
#include "base/bind.h"
#include "base/debug/trace_event.h"
#include "base/logging.h"
#include "base/message_loop.h"
#include "base/stl_util.h"
#include "base/time.h"
#include "media/audio/audio_util.h"
#include "media/audio/linux/alsa_util.h"
#include "media/audio/linux/alsa_wrapper.h"
#include "media/audio/linux/audio_manager_linux.h"
#include "media/base/data_buffer.h"
#include "media/base/seekable_buffer.h"
namespace media {
static const uint32 kNoDataSleepMilliseconds = 10;
static const uint32 kMinIntervalBetweenOnMoreDataCallsInMs = 5;
static const uint32 kSleepErrorMilliseconds = 10;
#if defined(NDEBUG)
static const int kPcmRecoverIsSilent = 1;
#else
static const int kPcmRecoverIsSilent = 0;
#endif
static const int kAlsaMaxSampleRate = 48000;
static const char* GuessSpecificDeviceName(uint32 channels) {
switch (channels) {
case 8:
return "surround71";
case 7:
return "surround70";
case 6:
return "surround51";
case 5:
return "surround50";
case 4:
return "surround40";
default:
return NULL;
}
}
std::ostream& operator<<(std::ostream& os,
AlsaPcmOutputStream::InternalState state) {
switch (state) {
case AlsaPcmOutputStream::kInError:
os << "kInError";
break;
case AlsaPcmOutputStream::kCreated:
os << "kCreated";
break;
case AlsaPcmOutputStream::kIsOpened:
os << "kIsOpened";
break;
case AlsaPcmOutputStream::kIsPlaying:
os << "kIsPlaying";
break;
case AlsaPcmOutputStream::kIsStopped:
os << "kIsStopped";
break;
case AlsaPcmOutputStream::kIsClosed:
os << "kIsClosed";
break;
};
return os;
}
const char AlsaPcmOutputStream::kDefaultDevice[] = "default";
const char AlsaPcmOutputStream::kAutoSelectDevice[] = "";
const char AlsaPcmOutputStream::kPlugPrefix[] = "plug:";
const uint32 AlsaPcmOutputStream::kMinLatencyMicros = 40 * 1000;
AlsaPcmOutputStream::AlsaPcmOutputStream(const std::string& device_name,
const AudioParameters& params,
AlsaWrapper* wrapper,
AudioManagerLinux* manager)
: requested_device_name_(device_name),
pcm_format_(alsa_util::BitsToFormat(params.bits_per_sample())),
channels_(params.channels()),
sample_rate_(params.sample_rate()),
bytes_per_sample_(params.bits_per_sample() / 8),
bytes_per_frame_(channels_ * params.bits_per_sample() / 8),
should_downmix_(false),
packet_size_(params.GetBytesPerBuffer()),
micros_per_packet_(FramesToMicros(
params.frames_per_buffer(), sample_rate_)),
latency_micros_(std::max(AlsaPcmOutputStream::kMinLatencyMicros,
micros_per_packet_ * 2)),
bytes_per_output_frame_(bytes_per_frame_),
alsa_buffer_frames_(0),
stop_stream_(false),
wrapper_(wrapper),
manager_(manager),
message_loop_(MessageLoop::current()),
playback_handle_(NULL),
frames_per_packet_(packet_size_ / bytes_per_frame_),
ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)),
state_(kCreated),
volume_(1.0f),
source_callback_(NULL),
audio_bus_(AudioBus::Create(params)) {
DCHECK(manager_->GetMessageLoop()->BelongsToCurrentThread());
DCHECK_EQ(audio_bus_->frames() * bytes_per_frame_, packet_size_);
if (params.sample_rate() > kAlsaMaxSampleRate ||
params.sample_rate() <= 0) {
LOG(WARNING) << "Unsupported audio frequency.";
TransitionTo(kInError);
}
if (AudioParameters::AUDIO_PCM_LINEAR != params.format() &&
AudioParameters::AUDIO_PCM_LOW_LATENCY != params.format()) {
LOG(WARNING) << "Unsupported audio format";
TransitionTo(kInError);
}
if (pcm_format_ == SND_PCM_FORMAT_UNKNOWN) {
LOG(WARNING) << "Unsupported bits per sample: " << params.bits_per_sample();
TransitionTo(kInError);
}
}
AlsaPcmOutputStream::~AlsaPcmOutputStream() {
InternalState current_state = state();
DCHECK(current_state == kCreated ||
current_state == kIsClosed ||
current_state == kInError);
DCHECK(!playback_handle_);
}
bool AlsaPcmOutputStream::Open() {
DCHECK(IsOnAudioThread());
if (state() == kInError)
return false;
if (!CanTransitionTo(kIsOpened)) {
NOTREACHED() << "Invalid state: " << state();
return false;
}
TransitionTo(kIsOpened);
if (requested_device_name_ == kAutoSelectDevice) {
playback_handle_ = AutoSelectDevice(latency_micros_);
if (playback_handle_)
DVLOG(1) << "Auto-selected device: " << device_name_;
} else {
device_name_ = requested_device_name_;
playback_handle_ = alsa_util::OpenPlaybackDevice(
wrapper_, device_name_.c_str(), channels_, sample_rate_,
pcm_format_, latency_micros_);
}
if (playback_handle_ == NULL) {
stop_stream_ = true;
TransitionTo(kInError);
return false;
} else {
bytes_per_output_frame_ = should_downmix_ ? 2 * bytes_per_sample_ :
bytes_per_frame_;
uint32 output_packet_size = frames_per_packet_ * bytes_per_output_frame_;
buffer_.reset(new media::SeekableBuffer(0, output_packet_size));
snd_pcm_uframes_t buffer_size;
snd_pcm_uframes_t period_size;
int error = wrapper_->PcmGetParams(playback_handle_, &buffer_size,
&period_size);
if (error < 0) {
LOG(ERROR) << "Failed to get playback buffer size from ALSA: "
<< wrapper_->StrError(error);
alsa_buffer_frames_ = frames_per_packet_ * 2;
} else {
alsa_buffer_frames_ = buffer_size;
}
}
return true;
}
void AlsaPcmOutputStream::Close() {
DCHECK(IsOnAudioThread());
if (state() != kIsClosed)
TransitionTo(kIsClosed);
if (playback_handle_) {
if (alsa_util::CloseDevice(wrapper_, playback_handle_) < 0) {
LOG(WARNING) << "Unable to close audio device. Leaking handle.";
}
playback_handle_ = NULL;
buffer_.reset();
stop_stream_ = true;
}
weak_factory_.InvalidateWeakPtrs();
manager_->ReleaseOutputStream(this);
}
void AlsaPcmOutputStream::Start(AudioSourceCallback* callback) {
DCHECK(IsOnAudioThread());
CHECK(callback);
if (stop_stream_)
return;
set_source_callback(callback);
if (TransitionTo(kIsPlaying) == kIsPlaying) {
buffer_->Clear();
int error = wrapper_->PcmDrop(playback_handle_);
if (error < 0 && error != -EAGAIN) {
LOG(ERROR) << "Failure clearing playback device ("
<< wrapper_->PcmName(playback_handle_) << "): "
<< wrapper_->StrError(error);
stop_stream_ = true;
} else {
error = wrapper_->PcmPrepare(playback_handle_);
if (error < 0 && error != -EAGAIN) {
LOG(ERROR) << "Failure preparing stream ("
<< wrapper_->PcmName(playback_handle_) << "): "
<< wrapper_->StrError(error);
stop_stream_ = true;
}
}
if (!stop_stream_)
WriteTask();
}
}
void AlsaPcmOutputStream::Stop() {
DCHECK(IsOnAudioThread());
set_source_callback(NULL);
TransitionTo(kIsStopped);
}
void AlsaPcmOutputStream::SetVolume(double volume) {
DCHECK(IsOnAudioThread());
volume_ = static_cast<float>(volume);
}
void AlsaPcmOutputStream::GetVolume(double* volume) {
DCHECK(IsOnAudioThread());
*volume = volume_;
}
void AlsaPcmOutputStream::BufferPacket(bool* source_exhausted) {
DCHECK(IsOnAudioThread());
if (stop_stream_) {
buffer_->Clear();
*source_exhausted = true;
return;
}
*source_exhausted = false;
if (!buffer_->forward_bytes()) {
uint32 buffer_delay = buffer_->forward_bytes() * bytes_per_frame_ /
bytes_per_output_frame_;
uint32 hardware_delay = GetCurrentDelay() * bytes_per_frame_;
scoped_refptr<media::DataBuffer> packet =
new media::DataBuffer(packet_size_);
int frames_filled = RunDataCallback(
audio_bus_.get(), AudioBuffersState(buffer_delay, hardware_delay));
size_t packet_size = frames_filled * bytes_per_frame_;
DCHECK_LE(packet_size, packet_size_);
audio_bus_->ToInterleaved(
frames_filled, bytes_per_sample_, packet->GetWritableData());
last_fill_time_ = base::Time::Now();
if (should_downmix_) {
if (media::FoldChannels(packet->GetWritableData(),
packet_size,
channels_,
bytes_per_sample_,
volume_)) {
packet_size = packet_size / bytes_per_frame_ * bytes_per_output_frame_;
} else {
LOG(ERROR) << "Folding failed";
}
} else {
media::AdjustVolume(packet->GetWritableData(),
packet_size,
channels_,
bytes_per_sample_,
volume_);
}
if (packet_size > 0) {
packet->SetDataSize(packet_size);
buffer_->Append(packet);
} else {
*source_exhausted = true;
}
}
}
void AlsaPcmOutputStream::WritePacket() {
DCHECK(IsOnAudioThread());
if (stop_stream_) {
buffer_->Clear();
return;
}
if (state() != kIsPlaying)
return;
CHECK_EQ(buffer_->forward_bytes() % bytes_per_output_frame_, 0u);
const uint8* buffer_data;
int buffer_size;
if (buffer_->GetCurrentChunk(&buffer_data, &buffer_size)) {
buffer_size = buffer_size - (buffer_size % bytes_per_output_frame_);
snd_pcm_sframes_t frames = std::min(
static_cast<snd_pcm_sframes_t>(buffer_size / bytes_per_output_frame_),
GetAvailableFrames());
snd_pcm_sframes_t frames_written =
wrapper_->PcmWritei(playback_handle_, buffer_data, frames);
if (frames_written < 0) {
frames_written = wrapper_->PcmRecover(playback_handle_,
frames_written,
kPcmRecoverIsSilent);
if (frames_written < 0) {
if (frames_written != -EAGAIN) {
LOG(ERROR) << "Failed to write to pcm device: "
<< wrapper_->StrError(frames_written);
RunErrorCallback(frames_written);
stop_stream_ = true;
}
}
} else {
DCHECK_EQ(frames_written, frames);
buffer_->Seek(frames_written * bytes_per_output_frame_);
}
} else {
if (playback_handle_ &&
(wrapper_->PcmState(playback_handle_) == SND_PCM_STATE_PREPARED) &&
GetCurrentDelay() > 0) {
wrapper_->PcmStart(playback_handle_);
}
}
}
void AlsaPcmOutputStream::WriteTask() {
DCHECK(IsOnAudioThread());
if (stop_stream_)
return;
if (state() == kIsStopped)
return;
bool source_exhausted;
BufferPacket(&source_exhausted);
WritePacket();
ScheduleNextWrite(source_exhausted);
}
void AlsaPcmOutputStream::ScheduleNextWrite(bool source_exhausted) {
DCHECK(IsOnAudioThread());
if (stop_stream_)
return;
const uint32 kTargetFramesAvailable = alsa_buffer_frames_ / 2;
uint32 available_frames = GetAvailableFrames();
uint32 frames_in_buffer = buffer_->forward_bytes() / bytes_per_output_frame_;
uint32 next_fill_time_ms =
FramesToMillis(frames_per_packet_ / 2, sample_rate_);
if (frames_in_buffer && available_frames) {
if (frames_in_buffer <= available_frames)
next_fill_time_ms = 0;
} else {
if (available_frames < kTargetFramesAvailable) {
uint32 frames_until_empty_enough =
kTargetFramesAvailable - available_frames;
next_fill_time_ms =
FramesToMillis(frames_until_empty_enough, sample_rate_);
if (next_fill_time_ms > kSleepErrorMilliseconds)
next_fill_time_ms -= kSleepErrorMilliseconds;
else
next_fill_time_ms = 0;
} else {
next_fill_time_ms = 0;
}
base::TimeDelta delay = base::Time::Now() - last_fill_time_;
if (delay.InMilliseconds() < kMinIntervalBetweenOnMoreDataCallsInMs &&
next_fill_time_ms < kMinIntervalBetweenOnMoreDataCallsInMs)
next_fill_time_ms = kMinIntervalBetweenOnMoreDataCallsInMs;
}
if (source_exhausted)
next_fill_time_ms = std::max(next_fill_time_ms, kNoDataSleepMilliseconds);
if (state() == kIsPlaying) {
message_loop_->PostDelayedTask(
FROM_HERE,
base::Bind(&AlsaPcmOutputStream::WriteTask,
weak_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(next_fill_time_ms));
}
}
uint32 AlsaPcmOutputStream::FramesToMicros(uint32 frames,
uint32 sample_rate) {
return frames * base::Time::kMicrosecondsPerSecond / sample_rate;
}
uint32 AlsaPcmOutputStream::FramesToMillis(uint32 frames,
uint32 sample_rate) {
return frames * base::Time::kMillisecondsPerSecond / sample_rate;
}
std::string AlsaPcmOutputStream::FindDeviceForChannels(uint32 channels) {
static const int kGetAllDevices = -1;
static const char kPcmInterfaceName[] = "pcm";
static const char kIoHintName[] = "IOID";
static const char kNameHintName[] = "NAME";
const char* wanted_device = GuessSpecificDeviceName(channels);
if (!wanted_device)
return "";
std::string guessed_device;
void** hints = NULL;
int error = wrapper_->DeviceNameHint(kGetAllDevices,
kPcmInterfaceName,
&hints);
if (error == 0) {
for (void** hint_iter = hints; *hint_iter != NULL; hint_iter++) {
scoped_ptr_malloc<char> io(
wrapper_->DeviceNameGetHint(*hint_iter, kIoHintName));
if (io != NULL && strcmp(io.get(), "Input") == 0)
continue;
scoped_ptr_malloc<char> name(
wrapper_->DeviceNameGetHint(*hint_iter, kNameHintName));
if (strncmp(wanted_device, name.get(), strlen(wanted_device)) == 0) {
guessed_device = name.get();
break;
}
}
wrapper_->DeviceNameFreeHint(hints);
hints = NULL;
} else {
LOG(ERROR) << "Unable to get hints for devices: "
<< wrapper_->StrError(error);
}
return guessed_device;
}
snd_pcm_sframes_t AlsaPcmOutputStream::GetCurrentDelay() {
snd_pcm_sframes_t delay = -1;
snd_pcm_state_t pcm_state = wrapper_->PcmState(playback_handle_);
if (pcm_state != SND_PCM_STATE_XRUN &&
pcm_state != SND_PCM_STATE_PREPARED) {
int error = wrapper_->PcmDelay(playback_handle_, &delay);
if (error < 0) {
delay = -1;
error = wrapper_->PcmRecover(playback_handle_,
error,
kPcmRecoverIsSilent);
if (error < 0) {
LOG(ERROR) << "Failed querying delay: " << wrapper_->StrError(error);
}
}
}
if (delay < 0 || static_cast<snd_pcm_uframes_t>(delay) > alsa_buffer_frames_)
delay = alsa_buffer_frames_ - GetAvailableFrames();
return delay;
}
snd_pcm_sframes_t AlsaPcmOutputStream::GetAvailableFrames() {
DCHECK(IsOnAudioThread());
if (stop_stream_)
return 0;
snd_pcm_sframes_t available_frames =
wrapper_->PcmAvailUpdate(playback_handle_);
if (available_frames < 0) {
available_frames = wrapper_->PcmRecover(playback_handle_,
available_frames,
kPcmRecoverIsSilent);
}
if (available_frames < 0) {
LOG(ERROR) << "Failed querying available frames. Assuming 0: "
<< wrapper_->StrError(available_frames);
return 0;
}
if (static_cast<uint32>(available_frames) > alsa_buffer_frames_) {
LOG(ERROR) << "ALSA returned " << available_frames << " of "
<< alsa_buffer_frames_ << " frames available.";
return alsa_buffer_frames_;
}
return available_frames;
}
snd_pcm_t* AlsaPcmOutputStream::AutoSelectDevice(unsigned int latency) {
snd_pcm_t* handle = NULL;
device_name_ = FindDeviceForChannels(channels_);
if (!device_name_.empty()) {
if ((handle = alsa_util::OpenPlaybackDevice(wrapper_, device_name_.c_str(),
channels_, sample_rate_,
pcm_format_,
latency)) != NULL) {
return handle;
}
device_name_ = kPlugPrefix + device_name_;
if ((handle = alsa_util::OpenPlaybackDevice(wrapper_, device_name_.c_str(),
channels_, sample_rate_,
pcm_format_,
latency)) != NULL) {
return handle;
}
}
uint32 default_channels = channels_;
if (default_channels > 2 && default_channels <= 8) {
should_downmix_ = true;
default_channels = 2;
}
device_name_ = kDefaultDevice;
if ((handle = alsa_util::OpenPlaybackDevice(
wrapper_, device_name_.c_str(), default_channels, sample_rate_,
pcm_format_, latency)) != NULL) {
return handle;
}
device_name_ = kPlugPrefix + device_name_;
if ((handle = alsa_util::OpenPlaybackDevice(
wrapper_, device_name_.c_str(), default_channels, sample_rate_,
pcm_format_, latency)) != NULL) {
return handle;
}
device_name_.clear();
return NULL;
}
bool AlsaPcmOutputStream::CanTransitionTo(InternalState to) {
switch (state_) {
case kCreated:
return to == kIsOpened || to == kIsClosed || to == kInError;
case kIsOpened:
return to == kIsPlaying || to == kIsStopped ||
to == kIsClosed || to == kInError;
case kIsPlaying:
return to == kIsPlaying || to == kIsStopped ||
to == kIsClosed || to == kInError;
case kIsStopped:
return to == kIsPlaying || to == kIsStopped ||
to == kIsClosed || to == kInError;
case kInError:
return to == kIsClosed || to == kInError;
case kIsClosed:
default:
return false;
}
}
AlsaPcmOutputStream::InternalState
AlsaPcmOutputStream::TransitionTo(InternalState to) {
DCHECK(IsOnAudioThread());
if (!CanTransitionTo(to)) {
NOTREACHED() << "Cannot transition from: " << state_ << " to: " << to;
state_ = kInError;
} else {
state_ = to;
}
return state_;
}
AlsaPcmOutputStream::InternalState AlsaPcmOutputStream::state() {
return state_;
}
bool AlsaPcmOutputStream::IsOnAudioThread() const {
return message_loop_ && message_loop_ == MessageLoop::current();
}
int AlsaPcmOutputStream::RunDataCallback(AudioBus* audio_bus,
AudioBuffersState buffers_state) {
TRACE_EVENT0("audio", "AlsaPcmOutputStream::RunDataCallback");
if (source_callback_)
return source_callback_->OnMoreData(audio_bus, buffers_state);
return 0;
}
void AlsaPcmOutputStream::RunErrorCallback(int code) {
if (source_callback_)
source_callback_->OnError(this, code);
}
void AlsaPcmOutputStream::set_source_callback(AudioSourceCallback* callback) {
DCHECK(IsOnAudioThread());
source_callback_ = callback;
}
}