blob: 4c91f520f3f08d1794475d31f233e33ac807533a [file] [log] [blame]
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "starboard/shared/alsa/alsa_audio_sink_type.h"
#include <alsa/asoundlib.h>
#include <algorithm>
#include <vector>
#include "starboard/condition_variable.h"
#include "starboard/memory.h"
#include "starboard/mutex.h"
#include "starboard/shared/alsa/alsa_util.h"
#include "starboard/thread.h"
#include "starboard/time.h"
namespace starboard {
namespace shared {
namespace alsa {
namespace {
using starboard::ScopedLock;
using starboard::ScopedTryLock;
using starboard::shared::alsa::AlsaGetBufferedFrames;
using starboard::shared::alsa::AlsaWriteFrames;
// The maximum number of frames that can be written to ALSA once. It must be a
// power of 2. It is also used as the ALSA polling size. A small number will
// lead to more CPU being used as the callbacks will be called more
// frequently and also it will be more likely to cause underflow but can make
// the audio clock more accurate.
const int kFramesPerRequest = 512;
// When the frames inside ALSA buffer is less than |kMinimumFramesInALSA|, the
// class will try to write more frames. The larger the number is, the less
// likely an underflow will happen but to use a larger number will cause longer
// delays after pause and stop.
const int kMinimumFramesInALSA = 2048;
// The size of the audio buffer ALSA allocates internally. Ideally this value
// should be greater than the sum of the above two constants. Choose a value
// that is too large can waste some memory as the extra buffer is never used.
const int kALSABufferSizeInFrames = 8192;
// Helper function to compute the size of the two valid starboard audio sample
// types.
size_t GetSampleSize(SbMediaAudioSampleType sample_type) {
switch (sample_type) {
case kSbMediaAudioSampleTypeFloat32:
return sizeof(float);
case kSbMediaAudioSampleTypeInt16:
return sizeof(int16_t);
}
SB_NOTREACHED();
return 0u;
}
void* IncrementPointerByBytes(void* pointer, size_t offset) {
return static_cast<void*>(static_cast<uint8_t*>(pointer) + offset);
}
// This class is an ALSA based audio sink with the following features:
// 1. It doesn't cache any data internally and maintains minimum data inside
// the ALSA buffer. It relies on pulling data from its source in high
// frequency to playback audio.
// 2. It never stops the underlying ALSA audio sink once created. When its
// source cannot provide enough data to continue playback, it simply writes
// silence to ALSA.
class AlsaAudioSink : public SbAudioSinkPrivate {
public:
AlsaAudioSink(Type* type,
int channels,
int sampling_frequency_hz,
SbMediaAudioSampleType sample_type,
SbAudioSinkFrameBuffers frame_buffers,
int frames_per_channel,
SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
SbAudioSinkConsumeFramesFunc consume_frame_func,
void* context);
~AlsaAudioSink() SB_OVERRIDE;
bool IsType(Type* type) SB_OVERRIDE { return type_ == type; }
bool is_valid() { return playback_handle_ != NULL; }
private:
static void* ThreadEntryPoint(void* context);
void AudioThreadFunc();
// Write silence to ALSA when there is not enough data in source or when the
// sink is paused.
// Return true to continue to play. Return false when destroying.
bool IdleLoop();
// Keep pulling frames from source until there is no frames to keep playback.
// When the sink is paused or there is no frames in source, it returns true
// so we can continue into the IdleLoop(). It returns false when destroying.
bool PlaybackLoop();
// Helper function to write frames contained in a ring buffer to ALSA.
void WriteFrames(int frames_to_write,
int frames_in_buffer,
int offset_in_frames);
Type* type_;
SbAudioSinkUpdateSourceStatusFunc update_source_status_func_;
SbAudioSinkConsumeFramesFunc consume_frame_func_;
void* context_;
int channels_;
int sampling_frequency_hz_;
SbMediaAudioSampleType sample_type_;
SbThread audio_out_thread_;
starboard::Mutex mutex_;
starboard::ConditionVariable creation_signal_;
SbTime time_to_wait_;
bool destroying_;
void* frame_buffer_;
int frames_per_channel_;
void* silence_frames_;
void* playback_handle_;
};
AlsaAudioSink::AlsaAudioSink(
Type* type,
int channels,
int sampling_frequency_hz,
SbMediaAudioSampleType sample_type,
SbAudioSinkFrameBuffers frame_buffers,
int frames_per_channel,
SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
SbAudioSinkConsumeFramesFunc consume_frame_func,
void* context)
: type_(type),
channels_(channels),
sampling_frequency_hz_(sampling_frequency_hz),
sample_type_(sample_type),
update_source_status_func_(update_source_status_func),
consume_frame_func_(consume_frame_func),
context_(context),
audio_out_thread_(kSbThreadInvalid),
creation_signal_(mutex_),
time_to_wait_(kFramesPerRequest * kSbTimeSecond / sampling_frequency_hz /
2),
destroying_(false),
frame_buffer_(frame_buffers[0]),
frames_per_channel_(frames_per_channel),
silence_frames_(new uint8_t[channels * kFramesPerRequest *
GetSampleSize(sample_type)]),
playback_handle_(NULL) {
SB_DCHECK(update_source_status_func_);
SB_DCHECK(consume_frame_func_);
SB_DCHECK(frame_buffer_);
SB_DCHECK(SbAudioSinkIsAudioSampleTypeSupported(sample_type_));
SbMemorySet(silence_frames_, 0,
channels * kFramesPerRequest * GetSampleSize(sample_type));
ScopedLock lock(mutex_);
audio_out_thread_ =
SbThreadCreate(0, kSbThreadPriorityRealTime, kSbThreadNoAffinity, true,
"alsa_audio_out", &AlsaAudioSink::ThreadEntryPoint, this);
SB_DCHECK(SbThreadIsValid(audio_out_thread_));
creation_signal_.Wait();
}
AlsaAudioSink::~AlsaAudioSink() {
{
ScopedLock lock(mutex_);
destroying_ = true;
}
SbThreadJoin(audio_out_thread_, NULL);
delete[] static_cast<uint8_t*>(silence_frames_);
}
// static
void* AlsaAudioSink::ThreadEntryPoint(void* context) {
SB_DCHECK(context);
AlsaAudioSink* sink = reinterpret_cast<AlsaAudioSink*>(context);
sink->AudioThreadFunc();
return NULL;
}
void AlsaAudioSink::AudioThreadFunc() {
snd_pcm_format_t alsa_sample_type =
sample_type_ == kSbMediaAudioSampleTypeFloat32 ? SND_PCM_FORMAT_FLOAT_LE
: SND_PCM_FORMAT_S16;
playback_handle_ = starboard::shared::alsa::AlsaOpenPlaybackDevice(
channels_, sampling_frequency_hz_, kFramesPerRequest,
kALSABufferSizeInFrames, alsa_sample_type);
creation_signal_.Signal();
if (!playback_handle_) {
return;
}
for (;;) {
if (!IdleLoop()) {
break;
}
if (!PlaybackLoop()) {
break;
}
}
starboard::shared::alsa::AlsaCloseDevice(playback_handle_);
ScopedLock lock(mutex_);
playback_handle_ = NULL;
}
bool AlsaAudioSink::IdleLoop() {
SB_DLOG(INFO) << "alsa::AlsaAudioSink enters idle loop";
bool drain = true;
for (;;) {
{
ScopedLock lock(mutex_);
if (destroying_) {
break;
}
}
int frames_in_buffer, offset_in_frames;
bool is_playing, is_eos_reached;
update_source_status_func_(&frames_in_buffer, &offset_in_frames,
&is_playing, &is_eos_reached, context_);
if (is_playing && frames_in_buffer > 0) {
return true;
}
if (drain) {
drain = false;
AlsaWriteFrames(playback_handle_, silence_frames_, kFramesPerRequest);
AlsaDrain(playback_handle_);
}
SbThreadSleep(time_to_wait_);
}
return false;
}
bool AlsaAudioSink::PlaybackLoop() {
SB_DLOG(INFO) << "alsa::AlsaAudioSink enters playback loop";
for (;;) {
int delayed_frame = AlsaGetBufferedFrames(playback_handle_);
{
ScopedTryLock lock(mutex_);
if (lock.is_locked()) {
if (destroying_) {
break;
}
}
}
if (delayed_frame < kMinimumFramesInALSA) {
int frames_in_buffer, offset_in_frames;
bool is_playing, is_eos_reached;
update_source_status_func_(&frames_in_buffer, &offset_in_frames,
&is_playing, &is_eos_reached, context_);
if (!is_playing || frames_in_buffer == 0) {
return true;
}
WriteFrames(std::min(kFramesPerRequest, frames_in_buffer),
frames_in_buffer, offset_in_frames);
} else {
SbThreadSleep(time_to_wait_);
}
}
return false;
}
void AlsaAudioSink::WriteFrames(int frames_to_write,
int frames_in_buffer,
int offset_in_frames) {
SB_DCHECK(frames_to_write <= frames_in_buffer);
int frames_to_buffer_end = frames_per_channel_ - offset_in_frames;
if (frames_to_write > frames_to_buffer_end) {
int consumed = AlsaWriteFrames(
playback_handle_,
IncrementPointerByBytes(frame_buffer_, offset_in_frames * channels_ *
GetSampleSize(sample_type_)),
frames_to_buffer_end);
consume_frame_func_(consumed, context_);
if (consumed != frames_to_buffer_end) {
return;
}
frames_to_write -= frames_to_buffer_end;
offset_in_frames = 0;
}
int consumed = AlsaWriteFrames(
playback_handle_,
IncrementPointerByBytes(frame_buffer_, offset_in_frames * channels_ *
GetSampleSize(sample_type_)),
frames_to_write);
consume_frame_func_(consumed, context_);
}
} // namespace
SbAudioSink AlsaAudioSinkType::Create(
int channels,
int sampling_frequency_hz,
SbMediaAudioSampleType audio_sample_type,
SbMediaAudioFrameStorageType audio_frame_storage_type,
SbAudioSinkFrameBuffers frame_buffers,
int frames_per_channel,
SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
SbAudioSinkConsumeFramesFunc consume_frames_func,
void* context) {
AlsaAudioSink* audio_sink = new AlsaAudioSink(
this, channels, sampling_frequency_hz, audio_sample_type, frame_buffers,
frames_per_channel, update_source_status_func, consume_frames_func,
context);
if (!audio_sink->is_valid()) {
delete audio_sink;
return kSbAudioSinkInvalid;
}
return audio_sink;
}
} // namespace alsa
} // namespace shared
} // namespace starboard
namespace {
SbAudioSinkPrivate::Type* alsa_audio_sink_type_;
} // namespace
// static
void SbAudioSinkPrivate::PlatformInitialize() {
SB_DCHECK(!alsa_audio_sink_type_);
alsa_audio_sink_type_ = new starboard::shared::alsa::AlsaAudioSinkType;
SetPrimaryType(alsa_audio_sink_type_);
EnableFallbackToStub();
}
// static
void SbAudioSinkPrivate::PlatformTearDown() {
SB_DCHECK(alsa_audio_sink_type_ == GetPrimaryType());
SetPrimaryType(NULL);
delete alsa_audio_sink_type_;
alsa_audio_sink_type_ = NULL;
}