blob: 57d38cbe128cb8e641b8c91cb96da2b726ee1a8b [file] [log] [blame]
// Copyright 2017 The Cobalt Authors. 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 <basetyps.h>
#include <wrl.h>
#include <xaudio2.h>
#include <algorithm>
#include <limits>
#include <list>
#include "starboard/atomic.h"
#include "starboard/common/condition_variable.h"
#include "starboard/common/log.h"
#include "starboard/common/mutex.h"
#include "starboard/configuration.h"
#include "starboard/shared/starboard/audio_sink/audio_sink_internal.h"
#include "starboard/shared/starboard/player/job_thread.h"
#include "starboard/shared/starboard/thread_checker.h"
#include "starboard/thread.h"
#include "starboard/time.h"
namespace starboard {
namespace shared {
namespace win32 {
namespace {
using Microsoft::WRL::ComPtr;
const int kMaxBuffersSubmittedPerLoop = 2;
// Fails an SB_DCHECK if an HRESULT is not S_OK
void CHECK_HRESULT_OK(HRESULT hr) {
SB_DCHECK(SUCCEEDED(hr)) << std::hex << hr;
}
WORD SampleTypeToFormatTag(SbMediaAudioSampleType type) {
switch (type) {
#if SB_HAS_QUIRK(SUPPORT_INT16_AUDIO_SAMPLES)
case kSbMediaAudioSampleTypeInt16:
return WAVE_FORMAT_PCM;
#endif // SB_HAS_QUIRK(SUPPORT_INT16_AUDIO_SAMPLES)
case kSbMediaAudioSampleTypeFloat32:
return WAVE_FORMAT_IEEE_FLOAT;
default:
SB_NOTREACHED();
return 0;
}
}
WORD SampleTypeToBitsPerSample(SbMediaAudioSampleType type) {
switch (type) {
#if SB_HAS_QUIRK(SUPPORT_INT16_AUDIO_SAMPLES)
case kSbMediaAudioSampleTypeInt16:
return 16;
#endif // SB_HAS_QUIRK(SUPPORT_INT16_AUDIO_SAMPLES)
case kSbMediaAudioSampleTypeFloat32:
return 32;
default:
SB_NOTREACHED();
return 0;
}
}
class XAudioAudioSinkType;
class XAudioAudioSink : public SbAudioSinkPrivate {
public:
XAudioAudioSink(XAudioAudioSinkType* type,
IXAudio2SourceVoice* source_voice,
const WAVEFORMATEX& wfx,
SbAudioSinkFrameBuffers frame_buffers,
int frame_buffers_size_in_frames,
SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
ConsumeFramesFunc consume_frames_func,
void* context);
~XAudioAudioSink() override{};
void SetSourceVoice(IXAudio2SourceVoice* source_voice) {
source_voice_ = source_voice;
samples_played_ = 0;
submitted_frames_ = 0;
if (source_voice_) {
CHECK_HRESULT_OK(source_voice_->Start(0));
SbAtomicRelease_Store(&stop_callbacks_, 0);
}
}
bool IsType(Type* type) override;
void SetPlaybackRate(double playback_rate) override {
SB_DCHECK(playback_rate >= 0.0);
if (playback_rate != 0.0 && playback_rate != 1.0) {
SB_NOTIMPLEMENTED() << "TODO: Only playback rates of 0.0 and 1.0 are "
"currently supported.";
playback_rate = (playback_rate > 0.0) ? 1.0 : 0.0;
}
ScopedLock lock(mutex_);
playback_rate_ = playback_rate;
}
void SetVolume(double volume) override {
ScopedLock lock(mutex_);
volume_ = volume;
}
void Process();
void StopCallbacks() {
SbAtomicBarrier_Increment(&stop_callbacks_, 1);
// Make sure that any call to Process() returns so we know that
// no future callbacks will be invoked.
process_mutex_.Acquire();
process_mutex_.Release();
// This must happen on a non-XAudio callback thread.
if (source_voice_) {
source_voice_->DestroyVoice();
}
}
const WAVEFORMATEX& GetWaveFormatEx() const { return wfx_; }
private:
bool AreCallbacksStopped() const {
return SbAtomicAcquire_Load(&stop_callbacks_) != 0;
}
void SubmitSourceBuffer(int offset_in_frames, int count_frames);
// If true, this instance's source_voice_ has been destroyed and
// future Process() calls should return immediately.
SbAtomic32 stop_callbacks_;
XAudioAudioSinkType* const type_;
const SbAudioSinkUpdateSourceStatusFunc update_source_status_func_;
const ConsumeFramesFunc consume_frames_func_;
void* const context_;
SbAudioSinkFrameBuffers frame_buffers_;
const int frame_buffers_size_in_frames_;
const WAVEFORMATEX wfx_;
// Note: despite some documentation to the contrary, it appears
// that IXAudio2SourceVoice cannot be a ComPtr.
IXAudio2SourceVoice* source_voice_;
// |process_mutex_| is held during Process. Others may rapidly
// acquire/release to ensure they wait until the current Process() ends.
Mutex process_mutex_;
// |mutex_| protects |playback_rate_| and |volume_|.
Mutex mutex_;
double playback_rate_;
double volume_;
// The following variables are only used inside Process(). To keep it in the
// class simply to allow them to be kept between Process() calls.
int submitted_frames_;
int samples_played_;
int queued_buffers_;
bool was_playing_;
double current_volume_;
};
class XAudioAudioSinkType : public SbAudioSinkPrivate::Type,
private IXAudio2EngineCallback {
public:
XAudioAudioSinkType();
ComPtr<IXAudio2> XAudioCreate();
SbAudioSink Create(
int channels,
int sampling_frequency_hz,
SbMediaAudioSampleType audio_sample_type,
SbMediaAudioFrameStorageType audio_frame_storage_type,
SbAudioSinkFrameBuffers frame_buffers,
int frame_buffers_size_in_frames,
SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
SbAudioSinkPrivate::ConsumeFramesFunc consume_frames_func,
SbAudioSinkPrivate::ErrorFunc error_func,
void* context);
bool IsValid(SbAudioSink audio_sink) override {
return audio_sink != kSbAudioSinkInvalid && audio_sink->IsType(this);
}
void Destroy(SbAudioSink audio_sink) override;
private:
void RestartAudioDevice();
void TryAcquireSinkVoices();
// IXAudio2EngineCallback methods
// This function will be called periodically with an interval of ~10ms.
void OnProcessingPassStart() override;
void OnProcessingPassEnd() override {}
void OnCriticalError(HRESULT hr) {
SB_LOG(INFO) << "OnCriticalError() called with code " << hr;
SB_DCHECK(thread_checker_.CalledOnValidThread());
// The thread id of callbacks will be changed after OnCriticalError() call.
thread_checker_.Detach();
if (sink_shutdown_in_progress_) {
return;
}
sink_shutdown_in_progress_ = true;
x_audio2_->UnregisterForCallbacks(this);
for (XAudioAudioSink* sink : audio_sinks_on_xaudio_callbacks_) {
sink->StopCallbacks();
}
restart_audio_thread_.job_queue()->Schedule(
std::bind(&XAudioAudioSinkType::RestartAudioDevice, this));
}
void ProcessSinksToAdd() {
if (!audio_sinks_to_add_.empty()) {
audio_sinks_on_xaudio_callbacks_.insert(
audio_sinks_on_xaudio_callbacks_.end(), audio_sinks_to_add_.begin(),
audio_sinks_to_add_.end());
audio_sinks_to_add_.clear();
}
}
void ProcessSinksToDelete() {
if (!audio_sinks_to_delete_.empty()) {
for (auto sink : audio_sinks_to_delete_) {
audio_sinks_on_xaudio_callbacks_.erase(
std::find(audio_sinks_on_xaudio_callbacks_.begin(),
audio_sinks_on_xaudio_callbacks_.end(), sink));
delete sink;
}
audio_sinks_to_delete_.clear();
}
}
ComPtr<IXAudio2> x_audio2_;
Mutex x_audio2_mutex_;
IXAudio2MasteringVoice* mastering_voice_ = nullptr;
// This mutex protects |audio_sinks_to_add_| and |audio_sinks_to_delete_|.
Mutex mutex_;
std::list<XAudioAudioSink*> audio_sinks_to_add_;
std::list<SbAudioSink> audio_sinks_to_delete_;
// This must only be accessed from the OnProcessingPassStart callback
std::list<XAudioAudioSink*> audio_sinks_on_xaudio_callbacks_;
starboard::ThreadChecker thread_checker_;
starboard::player::JobThread restart_audio_thread_;
std::list<XAudioAudioSink*> audio_sinks_to_restart_;
bool sink_shutdown_in_progress_;
};
XAudioAudioSink::XAudioAudioSink(
XAudioAudioSinkType* type,
IXAudio2SourceVoice* source_voice,
const WAVEFORMATEX& wfx,
SbAudioSinkFrameBuffers frame_buffers,
int frame_buffers_size_in_frames,
SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
ConsumeFramesFunc consume_frames_func,
void* context)
: stop_callbacks_(0),
type_(type),
source_voice_(source_voice),
update_source_status_func_(update_source_status_func),
consume_frames_func_(consume_frames_func),
context_(context),
frame_buffers_(frame_buffers),
frame_buffers_size_in_frames_(frame_buffers_size_in_frames),
wfx_(wfx),
playback_rate_(1.0),
volume_(1.0),
submitted_frames_(0),
samples_played_(0),
queued_buffers_(0),
was_playing_(false),
current_volume_(1.0) {
CHECK_HRESULT_OK(source_voice_->Stop(0));
}
bool XAudioAudioSink::IsType(Type* type) {
return type_ == type;
}
void XAudioAudioSink::Process() {
ScopedLock process_lock(process_mutex_);
if (AreCallbacksStopped()) {
// We must not continue in this case, since |source_voice_| has been
// destroyed.
return;
}
int frames_in_buffer, offset_in_frames;
bool is_playing, is_eos_reached;
bool is_playback_rate_zero = false;
bool should_set_volume = false;
// This function is run on the XAudio thread and shouldn't be blocked.
if (mutex_.AcquireTry()) {
is_playback_rate_zero = playback_rate_ == 0.0;
should_set_volume = current_volume_ != volume_;
current_volume_ = volume_;
mutex_.Release();
}
if (should_set_volume) {
CHECK_HRESULT_OK(source_voice_->SetVolume(current_volume_));
}
update_source_status_func_(&frames_in_buffer, &offset_in_frames, &is_playing,
&is_eos_reached, context_);
if (is_playback_rate_zero) {
is_playing = false;
}
if (is_playing != was_playing_) {
if (is_playing) {
CHECK_HRESULT_OK(source_voice_->Start(0));
} else {
CHECK_HRESULT_OK(source_voice_->Stop(0));
}
}
was_playing_ = is_playing;
// TODO: make sure that frames_in_buffer is large enough
// that it exceeds the voice state pool interval
if (!is_playing || frames_in_buffer == 0 || is_playback_rate_zero) {
return;
}
int unsubmitted_frames = frames_in_buffer - submitted_frames_;
int unsubmitted_start =
(offset_in_frames + submitted_frames_) % frame_buffers_size_in_frames_;
if (unsubmitted_frames == 0 || queued_buffers_ + kMaxBuffersSubmittedPerLoop >
XAUDIO2_MAX_QUEUED_BUFFERS) {
// submit nothing
} else if (unsubmitted_start + unsubmitted_frames <=
frame_buffers_size_in_frames_) {
SubmitSourceBuffer(unsubmitted_start, unsubmitted_frames);
} else {
int count_tail_frames = frame_buffers_size_in_frames_ - unsubmitted_start;
// Note since we can submit up to two source buffers at a time,
// kMaxBuffersSubmittedPerLoop = 2.
SubmitSourceBuffer(unsubmitted_start, count_tail_frames);
SubmitSourceBuffer(0, unsubmitted_frames - count_tail_frames);
}
submitted_frames_ = frames_in_buffer;
XAUDIO2_VOICE_STATE voice_state;
source_voice_->GetState(&voice_state);
int64_t consumed_frames = voice_state.SamplesPlayed - samples_played_;
SB_DCHECK(consumed_frames >= 0);
SB_DCHECK(consumed_frames <= std::numeric_limits<int>::max());
int consumed_frames_int = static_cast<int>(consumed_frames);
consume_frames_func_(consumed_frames_int, SbTimeGetMonotonicNow(), context_);
submitted_frames_ -= consumed_frames_int;
samples_played_ = voice_state.SamplesPlayed;
queued_buffers_ = voice_state.BuffersQueued;
}
void XAudioAudioSink::SubmitSourceBuffer(int offset_in_frames,
int count_frames) {
XAUDIO2_BUFFER audio_buffer_info;
audio_buffer_info.Flags = 0;
audio_buffer_info.AudioBytes = wfx_.nChannels *
frame_buffers_size_in_frames_ *
(wfx_.wBitsPerSample / 8);
audio_buffer_info.pAudioData = static_cast<const BYTE*>(frame_buffers_[0]);
audio_buffer_info.PlayBegin = offset_in_frames;
audio_buffer_info.PlayLength = count_frames;
audio_buffer_info.LoopBegin = 0;
audio_buffer_info.LoopLength = 0;
audio_buffer_info.LoopCount = 0;
audio_buffer_info.pContext = nullptr;
CHECK_HRESULT_OK(source_voice_->SubmitSourceBuffer(&audio_buffer_info));
}
XAudioAudioSinkType::XAudioAudioSinkType()
: restart_audio_thread_("RestartAudioDevice"),
sink_shutdown_in_progress_(false),
thread_checker_(starboard::ThreadChecker::kSetThreadIdOnFirstCheck) {
x_audio2_ = XAudioCreate();
HRESULT hr = x_audio2_->CreateMasteringVoice(&mastering_voice_);
SB_LOG_IF(WARNING, FAILED(hr)) << "Audio failed to CreateMasteringVoice(), "
"sound will be disabled.";
}
ComPtr<IXAudio2> XAudioAudioSinkType::XAudioCreate() {
ComPtr<IXAudio2> x_audio2;
HRESULT hr = XAudio2Create(&x_audio2, 0, XAUDIO2_DEFAULT_PROCESSOR);
if (FAILED(hr)) {
return nullptr;
}
#if !defined(COBALT_BUILD_TYPE_GOLD)
XAUDIO2_DEBUG_CONFIGURATION debug_config = {};
debug_config.TraceMask = XAUDIO2_LOG_ERRORS | XAUDIO2_LOG_WARNINGS |
XAUDIO2_LOG_INFO | XAUDIO2_LOG_DETAIL |
XAUDIO2_LOG_TIMING | XAUDIO2_LOG_LOCKS;
debug_config.LogThreadID = TRUE;
debug_config.LogFileline = TRUE;
debug_config.LogFunctionName = TRUE;
debug_config.LogTiming = TRUE;
x_audio2->SetDebugConfiguration(&debug_config, NULL);
#endif // !defined(COBALT_BUILD_TYPE_GOLD)
x_audio2->RegisterForCallbacks(this);
return x_audio2;
}
SbAudioSink XAudioAudioSinkType::Create(
int channels,
int sampling_frequency_hz,
SbMediaAudioSampleType audio_sample_type,
SbMediaAudioFrameStorageType audio_frame_storage_type,
SbAudioSinkFrameBuffers frame_buffers,
int frame_buffers_size_in_frames,
SbAudioSinkUpdateSourceStatusFunc update_source_status_func,
SbAudioSinkPrivate::ConsumeFramesFunc consume_frames_func,
SbAudioSinkPrivate::ErrorFunc error_func,
void* context) {
SB_DCHECK(audio_frame_storage_type ==
kSbMediaAudioFrameStorageTypeInterleaved);
WAVEFORMATEX wfx;
wfx.wFormatTag = SampleTypeToFormatTag(audio_sample_type);
wfx.nChannels = static_cast<WORD>(channels);
wfx.nSamplesPerSec = sampling_frequency_hz;
wfx.nAvgBytesPerSec = channels *
SampleTypeToBitsPerSample(audio_sample_type) *
sampling_frequency_hz / 8;
wfx.wBitsPerSample = SampleTypeToBitsPerSample(audio_sample_type);
wfx.nBlockAlign = static_cast<WORD>((channels * wfx.wBitsPerSample) / 8);
wfx.cbSize = 0;
IXAudio2SourceVoice* source_voice = nullptr;
HRESULT hr = E_FAIL;
{
ScopedLock lock(x_audio2_mutex_);
if (!x_audio2_) {
x_audio2_ = XAudioCreate();
if (!x_audio2_) {
SB_DLOG(WARNING) << "Audio failed to XAudioCreate(), "
"sound will be disabled.";
return nullptr;
}
hr = x_audio2_->CreateMasteringVoice(&mastering_voice_);
if (FAILED(hr)) {
SB_DLOG(WARNING) << "Audio failed to CreateMasteringVoice(), "
"sound will be disabled.";
return nullptr;
}
}
hr = x_audio2_->CreateSourceVoice(&source_voice, &wfx,
XAUDIO2_VOICE_NOPITCH, 1.f);
}
if (FAILED(hr)) {
SB_DLOG(WARNING) << "Could not create source voice, error code: " << hr;
return nullptr;
}
XAudioAudioSink* audio_sink = new XAudioAudioSink(
this, source_voice, wfx, frame_buffers, frame_buffers_size_in_frames,
update_source_status_func, consume_frames_func, context);
ScopedLock lock(mutex_);
audio_sinks_to_add_.push_back(audio_sink);
return audio_sink;
}
void XAudioAudioSinkType::Destroy(SbAudioSink audio_sink) {
if (audio_sink == kSbAudioSinkInvalid) {
return;
}
if (!IsValid(audio_sink)) {
SB_LOG(WARNING) << "audio_sink is invalid.";
return;
}
// Previous versions of this code waited for the next OnProcessingPassStart()
// call to occur before returning. However, various circumstances could
// cause that never to happen, especially during UWP suspend.
// Instead, we return immediately, ensuring no SbAudioSink callbacks occur
// and postpone the delete itself until the next OnProcessingPassStart()
static_cast<XAudioAudioSink*>(audio_sink)->StopCallbacks();
ScopedLock lock(mutex_);
auto it = std::find(audio_sinks_to_restart_.begin(),
audio_sinks_to_restart_.end(), audio_sink);
if (it == audio_sinks_to_restart_.end()) {
audio_sinks_to_delete_.push_back(audio_sink);
} else {
audio_sinks_to_restart_.erase(it);
delete audio_sink;
}
}
void XAudioAudioSinkType::RestartAudioDevice() {
{
ScopedLock lock(mutex_);
audio_sinks_to_restart_.insert(audio_sinks_to_restart_.end(),
audio_sinks_on_xaudio_callbacks_.begin(),
audio_sinks_on_xaudio_callbacks_.end());
audio_sinks_on_xaudio_callbacks_.clear();
audio_sinks_to_restart_.insert(audio_sinks_to_restart_.end(),
audio_sinks_to_add_.begin(),
audio_sinks_to_add_.end());
audio_sinks_to_add_.clear();
ProcessSinksToDelete();
for (auto sink : audio_sinks_to_restart_) {
sink->SetSourceVoice(nullptr);
}
}
if (mastering_voice_) {
mastering_voice_->DestroyVoice();
mastering_voice_ = nullptr;
}
{
ScopedLock lock(x_audio2_mutex_);
x_audio2_.Reset();
}
sink_shutdown_in_progress_ = false;
TryAcquireSinkVoices();
}
void XAudioAudioSinkType::TryAcquireSinkVoices() {
HRESULT hr = E_FAIL;
ScopedLock x_audio2_mutex_lock(x_audio2_mutex_);
if (!x_audio2_) {
hr = XAudio2Create(&x_audio2_, 0, XAUDIO2_DEFAULT_PROCESSOR);
if (FAILED(hr)) {
restart_audio_thread_.job_queue()->Schedule(
std::bind(&XAudioAudioSinkType::TryAcquireSinkVoices, this));
return;
}
x_audio2_->RegisterForCallbacks(this);
}
if (!mastering_voice_) {
hr = x_audio2_->CreateMasteringVoice(&mastering_voice_);
if (FAILED(hr)) {
restart_audio_thread_.job_queue()->Schedule(
std::bind(&XAudioAudioSinkType::TryAcquireSinkVoices, this));
return;
}
}
std::list<XAudioAudioSink*> audio_sinks_failed;
ScopedLock mutex_lock(mutex_);
for (auto sink : audio_sinks_to_restart_) {
IXAudio2SourceVoice* source_voice = nullptr;
hr = x_audio2_->CreateSourceVoice(&source_voice, &sink->GetWaveFormatEx(),
XAUDIO2_VOICE_NOPITCH, 1.f);
if (FAILED(hr)) {
audio_sinks_failed.push_back(sink);
continue;
}
sink->SetSourceVoice(source_voice);
audio_sinks_to_add_.push_back(sink);
}
audio_sinks_to_restart_ = audio_sinks_failed;
if (audio_sinks_to_restart_.empty()) {
return;
}
restart_audio_thread_.job_queue()->Schedule(
std::bind(&XAudioAudioSinkType::TryAcquireSinkVoices, this));
}
void XAudioAudioSinkType::OnProcessingPassStart() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
if (mutex_.AcquireTry()) {
ProcessSinksToAdd();
ProcessSinksToDelete();
mutex_.Release();
}
for (XAudioAudioSink* sink : audio_sinks_on_xaudio_callbacks_) {
sink->Process();
}
}
} // namespace
} // namespace win32
} // namespace shared
} // namespace starboard
namespace {
SbAudioSinkPrivate::Type* audio_sink_;
} // namespace
// static
void SbAudioSinkPrivate::PlatformInitialize() {
SB_DCHECK(!audio_sink_);
audio_sink_ = new starboard::shared::win32::XAudioAudioSinkType();
SetPrimaryType(audio_sink_);
EnableFallbackToStub();
}
// static
void SbAudioSinkPrivate::PlatformTearDown() {
SB_DCHECK(audio_sink_ == GetPrimaryType());
SetPrimaryType(nullptr);
delete audio_sink_;
audio_sink_ = nullptr;
}