// Copyright 2022 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 "starboard/shared/uwp/wasapi_audio_sink.h" | |
#include <mfapi.h> | |
#include <utility> | |
namespace starboard { | |
namespace shared { | |
namespace uwp { | |
namespace { | |
#define CHECK_HRESULT_OK(hr) \ | |
do { \ | |
SB_DCHECK(hr == S_OK) << "WASAPI audio sink error, error code: " \ | |
<< std::hex << hr; \ | |
} while (false) | |
void SetPassthroughWaveFormat(WAVEFORMATEXTENSIBLE* wfext, | |
SbMediaAudioCodec audio_codec, | |
int channels, | |
int sample_rate) { | |
SB_DCHECK(audio_codec == kSbMediaAudioCodecAc3 || | |
audio_codec == kSbMediaAudioCodecEac3); | |
wfext->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; | |
wfext->Format.nChannels = kIec60958Channels; | |
wfext->Format.wBitsPerSample = kIec60958BitsPerSample; | |
wfext->Format.nSamplesPerSec = sample_rate; | |
wfext->Format.nBlockAlign = kIec60958BlockAlign; | |
wfext->Format.nAvgBytesPerSec = | |
wfext->Format.nSamplesPerSec * wfext->Format.nBlockAlign; | |
wfext->Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); | |
wfext->Samples.wValidBitsPerSample = wfext->Format.wBitsPerSample; | |
wfext->dwChannelMask = KSAUDIO_SPEAKER_DIRECTOUT; | |
wfext->SubFormat = audio_codec == kSbMediaAudioCodecAc3 | |
? MFAudioFormat_Dolby_AC3_SPDIF | |
: KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS; | |
} | |
} // namespace | |
WASAPIAudioSink::WASAPIAudioSink() | |
: thread_checker_(starboard::ThreadChecker::kSetThreadIdOnFirstCheck) { | |
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> device_enumerator; | |
HRESULT hr = CoCreateInstance( | |
CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, | |
reinterpret_cast<void**>(device_enumerator.GetAddressOf())); | |
CHECK_HRESULT_OK(hr); | |
hr = device_enumerator->GetDefaultAudioEndpoint(eRender, eConsole, | |
device_.GetAddressOf()); | |
CHECK_HRESULT_OK(hr); | |
} | |
bool WASAPIAudioSink::Initialize(int channels, | |
int sample_rate, | |
SbMediaAudioCodec audio_codec) { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
SB_DCHECK(audio_codec == kSbMediaAudioCodecAc3 || | |
audio_codec == kSbMediaAudioCodecEac3); | |
HRESULT hr; | |
WAVEFORMATEXTENSIBLE wave_format; | |
SetPassthroughWaveFormat(&wave_format, audio_codec, channels, sample_rate); | |
hr = | |
device_->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, NULL, | |
reinterpret_cast<void**>(audio_client_.GetAddressOf())); | |
if (hr != S_OK) { | |
SB_LOG(ERROR) << "Failed to activate audio client, error code: " << std::hex | |
<< hr; | |
return false; | |
} | |
// 300 milliseconds, as REFERENCE_TIME is in units of 100 nanoseconds. | |
REFERENCE_TIME buffer_duration = 300 * 10000; | |
hr = | |
audio_client_->Initialize(AUDCLNT_SHAREMODE_EXCLUSIVE, 0, buffer_duration, | |
buffer_duration, &wave_format.Format, NULL); | |
if (hr != S_OK) { | |
SB_LOG(ERROR) << "Failed to initialize audio client, error code: " | |
<< std::hex << hr; | |
return false; | |
} | |
hr = device_->Activate( | |
IID_IAudioEndpointVolume, CLSCTX_ALL, NULL, | |
reinterpret_cast<void**>(audio_endpoint_volume_.GetAddressOf())); | |
if (hr != S_OK) { | |
SB_LOG(ERROR) << "Failed to initialize volume handler, error code: " | |
<< std::hex << hr; | |
return false; | |
} | |
hr = audio_client_->GetService( | |
IID_IAudioRenderClient, | |
reinterpret_cast<void**>(render_client_.GetAddressOf())); | |
if (hr != S_OK) { | |
SB_LOG(ERROR) << "Failed to initialize render client, error code: " | |
<< std::hex << hr; | |
return false; | |
} | |
hr = audio_client_->GetService( | |
IID_IAudioClock, reinterpret_cast<void**>(audio_clock_.GetAddressOf())); | |
if (hr != S_OK) { | |
SB_LOG(ERROR) << "Failed to initialize audio clock, error code: " | |
<< std::hex << hr; | |
return false; | |
} | |
audio_clock_->GetFrequency(&audio_clock_frequency_); | |
SB_DCHECK(audio_clock_frequency_ > 0); | |
audio_client_->GetBufferSize(&client_buffer_size_in_frames_); | |
frames_per_audio_buffer_ = audio_codec == kSbMediaAudioCodecAc3 | |
? kAc3BufferSizeInFrames | |
: kEac3BufferSizeInFrames; | |
SB_DCHECK(client_buffer_size_in_frames_ >= frames_per_audio_buffer_); | |
return true; | |
} | |
bool WASAPIAudioSink::WriteBuffer(scoped_refptr<DecodedAudio> decoded_audio) { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
SB_DCHECK(decoded_audio->size_in_bytes() == kAc3BufferSize || | |
decoded_audio->size_in_bytes() == kEac3BufferSize); | |
SB_DCHECK(decoded_audio); | |
if (!job_thread_) { | |
job_thread_.reset(new JobThread("wasapi_audio_sink")); | |
} | |
ScopedLock lock(output_frames_mutex_); | |
bool queued_decoded_audio = false; | |
if (pending_decoded_audios_.size() < kMaxDecodedAudios) { | |
pending_decoded_audios_.push(std::move(decoded_audio)); | |
queued_decoded_audio = true; | |
} | |
job_thread_->job_queue()->Schedule( | |
std::bind(&WASAPIAudioSink::OutputFrames, this)); | |
return queued_decoded_audio; | |
} | |
void WASAPIAudioSink::Reset() { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
job_thread_.reset(); | |
was_playing_ = false; | |
ScopedLock decoded_audios_lock(output_frames_mutex_); | |
pending_decoded_audios_ = std::queue<scoped_refptr<DecodedAudio>>(); | |
audio_client_->Stop(); | |
ScopedLock audio_clock_lock(audio_clock_mutex_); | |
audio_client_->Reset(); | |
} | |
void WASAPIAudioSink::Pause() { | |
paused_.store(true); | |
if (job_thread_) { | |
job_thread_->job_queue()->Schedule( | |
std::bind(&WASAPIAudioSink::UpdatePlaybackState, this)); | |
} | |
} | |
void WASAPIAudioSink::Play() { | |
paused_.store(false); | |
if (job_thread_) { | |
job_thread_->job_queue()->Schedule( | |
std::bind(&WASAPIAudioSink::UpdatePlaybackState, this)); | |
} | |
} | |
void WASAPIAudioSink::SetVolume(double volume) { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
if (volume > 1.0 || volume < 0.0) { | |
SB_LOG(WARNING) << "volume " << volume << " is not between 0.0 and 1.0"; | |
volume = volume > 1.0 ? 1.0 : 0.0; | |
} | |
volume_.store(volume); | |
if (job_thread_) { | |
job_thread_->job_queue()->Schedule( | |
std::bind(&WASAPIAudioSink::UpdatePlaybackState, this)); | |
} | |
} | |
void WASAPIAudioSink::SetPlaybackRate(double playback_rate) { | |
SB_DCHECK(thread_checker_.CalledOnValidThread()); | |
SB_DCHECK(playback_rate == 0.0 || playback_rate == 1.0) | |
<< "Playback rate " << playback_rate | |
<< " is unsupported by WASAPIAudioSink."; | |
playback_rate_.store(playback_rate); | |
if (job_thread_) { | |
job_thread_->job_queue()->Schedule( | |
std::bind(&WASAPIAudioSink::UpdatePlaybackState, this)); | |
} | |
} | |
double WASAPIAudioSink::GetCurrentPlaybackTime(uint64_t* updated_at) { | |
SB_DCHECK(audio_clock_); | |
ScopedLock lock(audio_clock_mutex_); | |
uint64_t pos; | |
HRESULT hr = audio_clock_->GetPosition(&pos, updated_at); | |
if (hr != S_OK) { | |
SB_LOG(INFO) << "WASAPI audio clock error, error code: " << std::hex << hr; | |
return -1; | |
} | |
return (static_cast<double>(pos) / | |
static_cast<double>(audio_clock_frequency_)) * | |
kSbTimeSecond; | |
} | |
void WASAPIAudioSink::OutputFrames() { | |
SB_DCHECK(job_thread_->job_queue()->BelongsToCurrentThread()); | |
ScopedLock lock(output_frames_mutex_); | |
if (pending_decoded_audios_.empty()) { | |
return; | |
} | |
int frames_copied = 0; | |
uint32_t frames_in_client_buffer = 0; | |
SbTime output_frames_job_delay = 5 * kSbTimeMillisecond; | |
HRESULT hr = audio_client_->GetCurrentPadding(&frames_in_client_buffer); | |
CHECK_HRESULT_OK(hr); | |
int frames_available = static_cast<int>(client_buffer_size_in_frames_) - | |
static_cast<int>(frames_in_client_buffer); | |
if (frames_available >= frames_per_audio_buffer_) { | |
BYTE* client_buffer = nullptr; | |
hr = render_client_->GetBuffer(frames_available, &client_buffer); | |
CHECK_HRESULT_OK(hr); | |
int client_buffer_offset = 0; | |
while (frames_available >= frames_per_audio_buffer_ && | |
!pending_decoded_audios_.empty()) { | |
scoped_refptr<DecodedAudio> decoded_audio = | |
pending_decoded_audios_.front(); | |
SB_DCHECK(decoded_audio); | |
memcpy(client_buffer + client_buffer_offset, | |
reinterpret_cast<BYTE*>(decoded_audio->data()), | |
decoded_audio->size_in_bytes()); | |
frames_copied += frames_per_audio_buffer_; | |
client_buffer_offset += decoded_audio->size_in_bytes(); | |
pending_decoded_audios_.pop(); | |
frames_available -= frames_per_audio_buffer_; | |
} | |
hr = render_client_->ReleaseBuffer(frames_copied, 0 /* dwFlags */); | |
CHECK_HRESULT_OK(hr); | |
output_frames_job_delay = 0; | |
} | |
if (!pending_decoded_audios_.empty()) { | |
job_thread_->job_queue()->Schedule( | |
std::bind(&WASAPIAudioSink::OutputFrames, this), | |
output_frames_job_delay); | |
} | |
} | |
void WASAPIAudioSink::UpdatePlaybackState() { | |
SB_DCHECK(job_thread_->job_queue()->BelongsToCurrentThread()); | |
bool is_playing = playing(); | |
HRESULT hr; | |
if (is_playing != was_playing_) { | |
if (is_playing) { | |
hr = audio_client_->Start(); | |
CHECK_HRESULT_OK(hr); | |
} else { | |
hr = audio_client_->Stop(); | |
CHECK_HRESULT_OK(hr); | |
} | |
was_playing_ = is_playing; | |
} | |
double volume = volume_.load(); | |
if (current_volume_ != volume) { | |
hr = audio_endpoint_volume_->SetMasterVolumeLevelScalar(volume, NULL); | |
CHECK_HRESULT_OK(hr); | |
current_volume_ = volume; | |
} | |
} | |
} // namespace uwp | |
} // namespace shared | |
} // namespace starboard |