| // Copyright 2017 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/win32/audio_sink.h" |
| |
| #include <basetyps.h> |
| #include <wrl.h> |
| #include <xaudio2.h> |
| |
| #include <limits> |
| #include <sstream> |
| #include <string> |
| |
| #include "starboard/configuration.h" |
| #include "starboard/log.h" |
| #include "starboard/mutex.h" |
| #include "starboard/thread.h" |
| #include "starboard/time.h" |
| |
| using Microsoft::WRL::ComPtr; |
| |
| namespace { |
| // Fails an SB_DCHECK if an HRESULT is not S_OK |
| void CHECK_HRESULT_OK(HRESULT hr) { |
| SB_DCHECK(SUCCEEDED(hr)) << std::hex << hr; |
| } |
| |
| const int kMaxBuffersSubmittedPerLoop = 2; |
| |
| std::string GenerateThreadName() { |
| static int s_count = 0; |
| std::stringstream ss; |
| ss << "AudioOut_" << s_count++; |
| return ss.str(); |
| } |
| } // namespace. |
| |
| namespace starboard { |
| namespace shared { |
| namespace win32 { |
| |
| class XAudioAudioSink : public SbAudioSinkPrivate { |
| public: |
| XAudioAudioSink(Type* type, |
| const WAVEFORMATEX& wfx, |
| SbAudioSinkFrameBuffers frame_buffers, |
| int frame_buffers_size_in_frames, |
| SbAudioSinkUpdateSourceStatusFunc update_source_status_func, |
| SbAudioSinkConsumeFramesFunc consume_frame_func, |
| void* context); |
| ~XAudioAudioSink() SB_OVERRIDE; |
| |
| bool IsType(Type* type) SB_OVERRIDE { return type_ == type; } |
| void SetPlaybackRate(double playback_rate) SB_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; |
| } |
| |
| private: |
| static void* ThreadEntryPoint(void* context); |
| void AudioThreadFunc(); |
| void SubmitSourceBuffer(int offset_in_frames, int count_frames); |
| |
| XAudioAudioSinkType* type_; |
| SbAudioSinkUpdateSourceStatusFunc update_source_status_func_; |
| SbAudioSinkConsumeFramesFunc consume_frame_func_; |
| void* context_; |
| |
| SbThread audio_out_thread_; |
| |
| SbAudioSinkFrameBuffers frame_buffers_; |
| int frame_buffers_size_in_frames_; |
| WAVEFORMATEX wfx_; |
| |
| // Note: despite some documentation to the contrary, it appears |
| // that IXAudio2SourceVoice cannot be a ComPtr. |
| IXAudio2SourceVoice* source_voice_; |
| |
| // mutex_ protects only destroying_ and playback_rate_. |
| // Everything else is immutable |
| // after the constructor. |
| ::starboard::Mutex mutex_; |
| bool destroying_; |
| double playback_rate_; |
| }; |
| |
| XAudioAudioSink::XAudioAudioSink( |
| Type* type, |
| const WAVEFORMATEX& wfx, |
| SbAudioSinkFrameBuffers frame_buffers, |
| int frame_buffers_size_in_frames, |
| SbAudioSinkUpdateSourceStatusFunc update_source_status_func, |
| SbAudioSinkConsumeFramesFunc consume_frame_func, |
| void* context) |
| : type_(static_cast<XAudioAudioSinkType*>(type)), |
| update_source_status_func_(update_source_status_func), |
| consume_frame_func_(consume_frame_func), |
| context_(context), |
| audio_out_thread_(kSbThreadInvalid), |
| frame_buffers_(frame_buffers), |
| frame_buffers_size_in_frames_(frame_buffers_size_in_frames), |
| wfx_(wfx), |
| destroying_(false), |
| playback_rate_(1.0) { |
| // TODO: Check MaxFrequencyRatio |
| CHECK_HRESULT_OK( |
| type_->x_audio2_->CreateSourceVoice(&source_voice_, &wfx, 0, |
| /*MaxFrequencyRatio = */ 1.0)); |
| |
| CHECK_HRESULT_OK(source_voice_->Stop(0)); |
| |
| std::string thread_name = GenerateThreadName(); |
| |
| audio_out_thread_ = SbThreadCreate( |
| 0, kSbThreadPriorityRealTime, kSbThreadNoAffinity, true, |
| thread_name.c_str(), &XAudioAudioSink::ThreadEntryPoint, this); |
| SB_DCHECK(SbThreadIsValid(audio_out_thread_)); |
| } |
| |
| XAudioAudioSink::~XAudioAudioSink() { |
| { |
| ScopedLock lock(mutex_); |
| destroying_ = true; |
| } |
| SbThreadJoin(audio_out_thread_, nullptr); |
| source_voice_->DestroyVoice(); |
| } |
| |
| // static |
| void* XAudioAudioSink::ThreadEntryPoint(void* context) { |
| SB_DCHECK(context); |
| XAudioAudioSink* sink = static_cast<XAudioAudioSink*>(context); |
| sink->AudioThreadFunc(); |
| |
| return nullptr; |
| } |
| |
| 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)); |
| } |
| |
| void XAudioAudioSink::AudioThreadFunc() { |
| const int kMaxFramesToConsumePerRequest = 1024; |
| |
| int submitted_frames = 0; |
| uint64_t samples_played = 0; |
| int queued_buffers = 0; |
| bool was_playing = false; // The player starts out playing by default. |
| for (;;) { |
| { |
| ScopedLock lock(mutex_); |
| if (destroying_) { |
| break; |
| } |
| } |
| int frames_in_buffer, offset_in_frames; |
| bool is_playing, is_eos_reached; |
| bool is_playback_rate_zero; |
| { |
| ScopedLock lock(mutex_); |
| is_playback_rate_zero = playback_rate_ == 0.0; |
| } |
| 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) { |
| SbThreadSleep(kSbTimeMillisecond * 5); |
| continue; |
| } |
| 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; |
| |
| SbThreadSleep(kSbTimeMillisecond); |
| |
| 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_frame_func_(consumed_frames_int, context_); |
| submitted_frames -= consumed_frames_int; |
| samples_played = voice_state.SamplesPlayed; |
| queued_buffers = voice_state.BuffersQueued; |
| } |
| } |
| |
| namespace { |
| |
| WORD SampleTypeToFormatTag(SbMediaAudioSampleType type) { |
| switch (type) { |
| case kSbMediaAudioSampleTypeInt16: |
| return WAVE_FORMAT_PCM; |
| case kSbMediaAudioSampleTypeFloat32: |
| return WAVE_FORMAT_IEEE_FLOAT; |
| default: |
| SB_NOTREACHED(); |
| return 0; |
| } |
| } |
| |
| WORD SampleTypeToBitsPerSample(SbMediaAudioSampleType type) { |
| switch (type) { |
| case kSbMediaAudioSampleTypeInt16: |
| return 16; |
| case kSbMediaAudioSampleTypeFloat32: |
| return 32; |
| default: |
| SB_NOTREACHED(); |
| return 0; |
| } |
| } |
| |
| } // namespace |
| |
| XAudioAudioSinkType::XAudioAudioSinkType() { |
| CHECK_HRESULT_OK(XAudio2Create(&x_audio2_, 0, XAUDIO2_DEFAULT_PROCESSOR)); |
| CHECK_HRESULT_OK(x_audio2_->CreateMasteringVoice(&mastering_voice_)); |
| } |
| |
| 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, |
| SbAudioSinkConsumeFramesFunc consume_frames_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; |
| |
| return new XAudioAudioSink( |
| this, wfx, frame_buffers, frame_buffers_size_in_frames, |
| update_source_status_func, consume_frames_func, context); |
| } |
| |
| } // 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; |
| } |