diff --git a/CMakeLists.txt b/CMakeLists.txt index 4647a9147..01e89b529 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -497,7 +497,7 @@ set_property(SOURCE src/subtitles_provider_csri.cpp PROPERTY INCLUDE_DIRECTORIES if(MSVC) target_link_libraries (Aegisub dsound) add_definitions("-DWITH_DIRECTSOUND") - target_sources(Aegisub PRIVATE src/audio_player_dsound.cpp src/audio_player_dsound2.cpp) + target_sources(Aegisub PRIVATE src/audio_player_dsound.cpp src/audio_player_dsound2.cpp src/audio_player_xaudio2.cpp) endif(MSVC) find_package(FFMS2) diff --git a/meson.build b/meson.build index 616c86053..5527cfbe8 100644 --- a/meson.build +++ b/meson.build @@ -237,20 +237,37 @@ if host_machine.system() == 'windows' and get_option('avisynth').enabled() deps_inc += avs.include_directories('AviSynth-Headers') endif -if host_machine.system() == 'windows' and not get_option('directsound').disabled() - dsound_dep = cc.find_library('dsound', required: get_option('directsound')) - winmm_dep = cc.find_library('winmm', required: get_option('directsound')) - ole32_dep = cc.find_library('ole32', required: get_option('directsound')) - have_dsound_h = cc.has_header('dsound.h') - if not have_dsound_h and get_option('directsound').enabled() - error('DirectSound enabled but dsound.h not found') +if host_machine.system() == 'windows' + + if not get_option('directsound').disabled() + dsound_dep = cc.find_library('dsound', required: get_option('directsound')) + winmm_dep = cc.find_library('winmm', required: get_option('directsound')) + ole32_dep = cc.find_library('ole32', required: get_option('directsound')) + have_dsound_h = cc.has_header('dsound.h') + if not have_dsound_h and get_option('directsound').enabled() + error('DirectSound enabled but dsound.h not found') + endif + + dxguid_dep = cc.find_library('dxguid', required: true) + if dsound_dep.found() and winmm_dep.found() and ole32_dep.found() and dxguid_dep.found() and have_dsound_h + deps += [dsound_dep, winmm_dep, ole32_dep, dxguid_dep] + conf.set('WITH_DIRECTSOUND', 1) + dep_avail += 'DirectSound' + endif endif - dxguid_dep = cc.find_library('dxguid', required: true) - if dsound_dep.found() and winmm_dep.found() and ole32_dep.found() and dxguid_dep.found() and have_dsound_h - deps += [dsound_dep, winmm_dep, ole32_dep, dxguid_dep] - conf.set('WITH_DIRECTSOUND', 1) - dep_avail += 'DirectSound' + if not get_option('xaudio2').disabled() + have_xaudio_h = cc.has_header('xaudio2.h') + xaudio2_dep = cc.find_library('xaudio2', required: true) + if have_xaudio_h + deps += [xaudio2_dep] + conf.set('WITH_XAUDIO2', 1) + dep_avail += 'XAudio2' + endif + + if not have_dsound_h and get_option('xaudio2').enabled() + error('xaudio2 enabled but xaudio2.h not found') + endif endif endif diff --git a/meson_options.txt b/meson_options.txt index 3bc0461cd..97a183f22 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,7 +3,8 @@ option('openal', type: 'feature', description: 'OpenAL audio output') option('libpulse', type: 'feature', description: 'PulseAudio audio output') option('portaudio', type: 'feature', description: 'PortAudio audio output') option('directsound', type: 'feature', description: 'DirectSound audio output') -option('default_audio_output', type: 'combo', choices: ['auto', 'ALSA', 'OpenAL', 'PulseAudio', 'PortAudio', 'DirectSound'], description: 'Default audio output') +option('xaudio2', type: 'feature', description: 'XAudio2 audio output') +option('default_audio_output', type: 'combo', choices: ['auto', 'ALSA', 'OpenAL', 'PulseAudio', 'PortAudio', 'DirectSound', 'XAudio2'], description: 'Default audio output') option('ffms2', type: 'feature', description: 'FFMS2 video source') option('avisynth', type: 'feature', description: 'AviSynth video source') diff --git a/src/audio_player.cpp b/src/audio_player.cpp index f5a8327ca..c46ba0a40 100644 --- a/src/audio_player.cpp +++ b/src/audio_player.cpp @@ -43,6 +43,7 @@ std::unique_ptr CreateAlsaPlayer(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreateDirectSoundPlayer(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreateDirectSound2Player(agi::AudioProvider *providers, wxWindow *window); +std::unique_ptr CreateXAudio2Player(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreateOpenALPlayer(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreatePortAudioPlayer(agi::AudioProvider *providers, wxWindow *window); std::unique_ptr CreatePulseAudioPlayer(agi::AudioProvider *providers, wxWindow *window); @@ -63,6 +64,9 @@ namespace { {"DirectSound-old", CreateDirectSoundPlayer, false}, {"DirectSound", CreateDirectSound2Player, false}, #endif +#ifdef WITH_XAUDIO2 + {"Xaudio2", CreateXAudio2Player, false}, +#endif #ifdef WITH_OPENAL {"OpenAL", CreateOpenALPlayer, false}, #endif diff --git a/src/audio_player_xaudio2.cpp b/src/audio_player_xaudio2.cpp new file mode 100644 index 000000000..a7cbf0f28 --- /dev/null +++ b/src/audio_player_xaudio2.cpp @@ -0,0 +1,694 @@ +// Copyright (c) 2019, Qirui Wang +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the Aegisub Group nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Aegisub Project http://www.aegisub.org/ + +#ifdef WITH_XAUDIO2 +#include "include/aegisub/audio_player.h" + +#include "options.h" + +#include +#include +#include +#include + +#ifndef XAUDIO2_REDIST +#include +#else +#include +#endif + +namespace { +class XAudio2Thread; + +/// @class XAudio2Player +/// @brief XAudio2-based audio player +/// +/// The core design idea is to have a playback thread that performs all playback operations, and use the player object as a proxy to send commands to the playback thread. +class XAudio2Player final : public AudioPlayer { + /// The playback thread + std::unique_ptr thread; + + /// Desired length in milliseconds to write ahead of the playback cursor + int WantedLatency; + + /// Multiplier for WantedLatency to get total buffer length + int BufferLength; + + /// @brief Tell whether playback thread is alive + /// @return True if there is a playback thread and it's ready + bool IsThreadAlive(); + +public: + /// @brief Constructor + XAudio2Player(agi::AudioProvider* provider); + /// @brief Destructor + ~XAudio2Player() = default; + + /// @brief Start playback + /// @param start First audio frame to play + /// @param count Number of audio frames to play + void Play(int64_t start, int64_t count); + + /// @brief Stop audio playback + /// @param timerToo Whether to also stop the playback update timer + void Stop(); + + /// @brief Tell whether playback is active + /// @return True if audio is playing back + bool IsPlaying(); + + /// @brief Get playback end position + /// @return Audio frame index + /// + /// Returns 0 if playback is stopped or there is no playback thread + int64_t GetEndPosition(); + /// @brief Get approximate playback position + /// @return Index of audio frame user is currently hearing + /// + /// Returns 0 if playback is stopped or there is no playback thread + int64_t GetCurrentPosition(); + + /// @brief Change playback end position + /// @param pos New end position + void SetEndPosition(int64_t pos); + + /// @brief Change playback volume + /// @param vol Amplification factor + void SetVolume(double vol); +}; + +/// @brief RAII support class to init and de-init the COM library +struct COMInitialization { + + /// Flag set if an inited COM library is managed + bool inited = false; + + /// @brief Destructor, de-inits COM if it is inited + ~COMInitialization() { + if (inited) CoUninitialize(); + } + + /// @brief Initialise the COM library as single-threaded apartment if isn't already inited by us + bool Init() { + if (!inited && SUCCEEDED(CoInitialize(nullptr))) + inited = true; + return inited; + } +}; + +struct ReleaseCOMObject { + void operator()(IUnknown* obj) { + if (obj) obj->Release(); + } +}; + +/// @brief RAII wrapper around Win32 HANDLE type +struct Win32KernelHandle final : public agi::scoped_holder { + /// @brief Create with a managed handle + /// @param handle Win32 handle to manage + Win32KernelHandle(HANDLE handle = 0) :scoped_holder(handle, CloseHandle) {} + + Win32KernelHandle& operator=(HANDLE new_handle) { + scoped_holder::operator=(new_handle); + return *this; + } +}; + +/// @class XAudio2Thread +/// @brief Playback thread class for XAudio2Player +/// +/// Not based on wxThread, but uses Win32 threads directly +class XAudio2Thread :public IXAudio2VoiceCallback { + /// @brief Win32 thread entry point + /// @param parameter Pointer to our thread object + /// @return Thread return value, always 0 here + static unsigned int __stdcall ThreadProc(void* parameter); + /// @brief Thread entry point + void Run(); + + /// @brief Check for error state and throw exception if one occurred + void CheckError(); + + /// Win32 handle to the thread + Win32KernelHandle thread_handle; + + /// Event object, world to thread, set to start playback + Win32KernelHandle event_start_playback; + + /// Event object, world to thread, set to stop playback + Win32KernelHandle event_stop_playback; + + /// Event object, world to thread, set if playback end time was updated + Win32KernelHandle event_update_end_time; + + /// Event object, world to thread, set if the volume was changed + Win32KernelHandle event_set_volume; + + /// Event object, world to thread, set if the thread should end as soon as possible + Win32KernelHandle event_buffer_end; + + /// Event object, world to thread, set if the thread should end as soon as possible + Win32KernelHandle event_kill_self; + + /// Event object, thread to world, set when the thread has entered its main loop + Win32KernelHandle thread_running; + + /// Event object, thread to world, set when playback is ongoing + Win32KernelHandle is_playing; + + /// Event object, thread to world, set if an error state has occurred (implies thread is dying) + Win32KernelHandle error_happened; + + /// Statically allocated error message text describing reason for error_happened being set + const char* error_message = nullptr; + + /// Playback volume, 1.0 is "unchanged" + double volume = 1.0; + + /// Audio frame to start playback at + int64_t start_frame = 0; + + /// Audio frame to end playback at + int64_t end_frame = 0; + + /// Desired length in milliseconds to write ahead of the playback cursor + int wanted_latency; + + /// Multiplier for WantedLatency to get total buffer length + int buffer_length; + + /// System millisecond timestamp of last playback start, used to calculate playback position + ULONGLONG last_playback_restart; + + /// Audio provider to take sample data from + agi::AudioProvider* provider; + + /// Buffer occupied indicator + std::vector buffer_occupied; + +public: + /// @brief Constructor, creates and starts playback thread + /// @param provider Audio provider to take sample data from + /// @param WantedLatency Desired length in milliseconds to write ahead of the playback cursor + /// @param BufferLength Multiplier for WantedLatency to get total buffer length + XAudio2Thread(agi::AudioProvider* provider, int WantedLatency, int BufferLength); + /// @brief Destructor, waits for thread to have died + ~XAudio2Thread(); + + // IXAudio2VoiceCallback + void STDMETHODCALLTYPE OnVoiceProcessingPassStart(UINT32 BytesRequired) override {} + void STDMETHODCALLTYPE OnVoiceProcessingPassEnd() override {} + void STDMETHODCALLTYPE OnStreamEnd() override {} + void STDMETHODCALLTYPE OnBufferStart(void* pBufferContext) override {} + void STDMETHODCALLTYPE OnBufferEnd(void* pBufferContext) override { + intptr_t i = reinterpret_cast(pBufferContext); + buffer_occupied[i] = false; + SetEvent(event_buffer_end); + } + void STDMETHODCALLTYPE OnLoopEnd(void* pBufferContext) override {} + void STDMETHODCALLTYPE OnVoiceError(void* pBufferContext, HRESULT Error) override {} + + /// @brief Start audio playback + /// @param start Audio frame to start playback at + /// @param count Number of audio frames to play + void Play(int64_t start, int64_t count); + + /// @brief Stop audio playback + void Stop(); + + /// @brief Change audio playback end point + /// @param new_end_frame New last audio frame to play + /// + /// Playback stops instantly if new_end_frame is before the current playback position + void SetEndFrame(int64_t new_end_frame); + + /// @brief Change audio playback volume + /// @param new_volume New playback amplification factor, 1.0 is "unchanged" + void SetVolume(double new_volume); + + /// @brief Tell whether audio playback is active + /// @return True if audio is being played back, false if it is not + bool IsPlaying(); + + /// @brief Get approximate current audio frame being heard by the user + /// @return Audio frame index + /// + /// Returns 0 if not playing + int64_t GetCurrentFrame(); + + /// @brief Get audio playback end point + /// @return Audio frame index + int64_t GetEndFrame(); + + /// @brief Tell whether playback thread has died + /// @return True if thread is no longer running + bool IsDead(); +}; + +unsigned int __stdcall XAudio2Thread::ThreadProc(void* parameter) { + static_cast(parameter)->Run(); + return 0; +} + +/// Macro used to set error_message, error_happened and end the thread +#define REPORT_ERROR(msg) \ +{ \ + ResetEvent(is_playing); \ + error_message = "XAudio2Thread: " msg; \ + SetEvent(error_happened); \ + return; \ +} + +void XAudio2Thread::Run() { + COMInitialization COM_library; + if (!COM_library.Init()) { + REPORT_ERROR("Could not initialise COM") + } + IXAudio2* pXAudio2; + IXAudio2SourceVoice* pSourceVoice; + HRESULT hr; + if (FAILED(hr = XAudio2Create(&pXAudio2, 0, XAUDIO2_DEFAULT_PROCESSOR))) { + REPORT_ERROR("Failed initializing XAudio2") + } + IXAudio2MasteringVoice* pMasterVoice = NULL; + if (FAILED(hr = pXAudio2->CreateMasteringVoice(&pMasterVoice))) { + REPORT_ERROR("Failed initializing XAudio2 MasteringVoice") + } + + // Describe the wave format + WAVEFORMATEX wfx; + wfx.nSamplesPerSec = provider->GetSampleRate(); + wfx.cbSize = 0; + bool original = true; + wfx.wFormatTag = provider->AreSamplesFloat() ? WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM; + wfx.nChannels = provider->GetChannels(); + wfx.wBitsPerSample = provider->GetBytesPerSample() * 8; + wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8; + wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; + + if (FAILED(hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &wfx, 0, 2, this))) { + if (hr == XAUDIO2_E_INVALID_CALL) { + // Retry with 16bit mono + original = false; + wfx.wFormatTag = WAVE_FORMAT_PCM; + wfx.nChannels = 1; + wfx.wBitsPerSample = sizeof(int16_t) * 8; + wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8; + wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; + if (FAILED(hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &wfx, 0, 2, this))) { + REPORT_ERROR("Failed initializing XAudio2 SourceVoice") + } + } + else { + REPORT_ERROR("Failed initializing XAudio2 SourceVoice") + } + } + + // Now we're ready to roll! + SetEvent(thread_running); + bool running = true; + + HANDLE events_to_wait[] = { + event_start_playback, + event_stop_playback, + event_update_end_time, + event_set_volume, + event_buffer_end, + event_kill_self + }; + + int64_t next_input_frame = 0; + DWORD buffer_offset = 0; + bool playback_should_be_running = false; + int current_latency = wanted_latency; + const int wanted_frames = wanted_latency * wfx.nSamplesPerSec / 1000; + const DWORD wanted_latency_bytes = wanted_frames * wfx.nBlockAlign; + std::vector > buff(buffer_length); + for (auto& i : buff) + i.resize(wanted_latency_bytes); + + while (running) { + DWORD wait_result = WaitForMultipleObjects(sizeof(events_to_wait) / sizeof(HANDLE), events_to_wait, FALSE, INFINITE); + + switch (wait_result) { + case WAIT_OBJECT_0 + 0: + // Start or restart playback + pSourceVoice->Stop(); + pSourceVoice->FlushSourceBuffers(); + + next_input_frame = start_frame; + playback_should_be_running = true; + pSourceVoice->Start(); + SetEvent(is_playing); + goto do_fill_buffer; + + case WAIT_OBJECT_0 + 1: + stop_playback: + // Stop playing + ResetEvent(is_playing); + pSourceVoice->Stop(); + pSourceVoice->FlushSourceBuffers(); + playback_should_be_running = false; + break; + + case WAIT_OBJECT_0 + 2: + // Set end frame + if (end_frame <= next_input_frame) + goto stop_playback; + goto do_fill_buffer; + + case WAIT_OBJECT_0 + 3: + // Change volume + pSourceVoice->SetVolume(volume); + break; + + case WAIT_OBJECT_0 + 4: + // Buffer end + do_fill_buffer: + // Time to fill more into buffer + if (!playback_should_be_running) + break; + + for (int i = 0; i < buffer_length; ++i) { + if (!buffer_occupied[i]) { + int fill_len = std::min(end_frame - next_input_frame, wanted_frames); + if (fill_len <= 0) + break; + buffer_occupied[i] = true; + if (original) + provider->GetAudio(buff[i].data(), next_input_frame, fill_len); + else + provider->GetInt16MonoAudio(reinterpret_cast(buff[i].data()), next_input_frame, fill_len); + next_input_frame += fill_len; + XAUDIO2_BUFFER xbf; + xbf.Flags = fill_len + next_input_frame == end_frame ? XAUDIO2_END_OF_STREAM : 0; + xbf.AudioBytes = fill_len * wfx.nBlockAlign; + xbf.pAudioData = buff[i].data(); + xbf.PlayBegin = 0; + xbf.PlayLength = 0; + xbf.LoopBegin = 0; + xbf.LoopLength = 0; + xbf.LoopCount = 0; + xbf.pContext = reinterpret_cast(static_cast(i)); + if (FAILED(hr = pSourceVoice->SubmitSourceBuffer(&xbf))) { + REPORT_ERROR("Failed initializing Submit Buffer") + } + } + } + break; + + case WAIT_OBJECT_0 + 5: + // Perform suicide + running = false; + pXAudio2->Release(); + ResetEvent(is_playing); + playback_should_be_running = false; + break; + + default: + REPORT_ERROR("Something bad happened while waiting on events in playback loop, either the wait failed or an event object was abandoned.") + break; + } + } +} + +#undef REPORT_ERROR + +void XAudio2Thread::CheckError() +{ + try { + switch (WaitForSingleObject(error_happened, 0)) + { + case WAIT_OBJECT_0: + throw error_message; + + case WAIT_ABANDONED: + throw "The XAudio2Thread error signal event was abandoned, somehow. This should not happen."; + + case WAIT_FAILED: + throw "Failed checking state of XAudio2Thread error signal event."; + + case WAIT_TIMEOUT: + default: + return; + } + } + catch (...) { + ResetEvent(is_playing); + ResetEvent(thread_running); + throw; + } +} + +XAudio2Thread::XAudio2Thread(agi::AudioProvider* provider, int WantedLatency, int BufferLength) + : event_start_playback(CreateEvent(0, FALSE, FALSE, 0)) + , event_stop_playback(CreateEvent(0, FALSE, FALSE, 0)) + , event_update_end_time(CreateEvent(0, FALSE, FALSE, 0)) + , event_set_volume(CreateEvent(0, FALSE, FALSE, 0)) + , event_buffer_end(CreateEvent(0, FALSE, FALSE, 0)) + , event_kill_self(CreateEvent(0, FALSE, FALSE, 0)) + , thread_running(CreateEvent(0, TRUE, FALSE, 0)) + , is_playing(CreateEvent(0, TRUE, FALSE, 0)) + , error_happened(CreateEvent(0, FALSE, FALSE, 0)) + , wanted_latency(WantedLatency) + , buffer_length(BufferLength < XAUDIO2_MAX_QUEUED_BUFFERS ? BufferLength : XAUDIO2_MAX_QUEUED_BUFFERS) + , provider(provider) + , buffer_occupied(BufferLength) +{ + if (!(thread_handle = (HANDLE)_beginthreadex(0, 0, ThreadProc, this, 0, 0))) { + throw AudioPlayerOpenError("Failed creating playback thread in XAudio2Player. This is bad."); + } + + HANDLE running_or_error[] = { thread_running, error_happened }; + switch (WaitForMultipleObjects(2, running_or_error, FALSE, INFINITE)) { + case WAIT_OBJECT_0: + // running, all good + return; + + case WAIT_OBJECT_0 + 1: + // error happened, we fail + throw AudioPlayerOpenError(error_message ? error_message : "Failed wait for thread start or thread error in XAudio2Player. This is bad."); + + default: + throw AudioPlayerOpenError("Failed wait for thread start or thread error in XAudio2Player. This is bad."); + } +} + +XAudio2Thread::~XAudio2Thread() { + SetEvent(event_kill_self); + WaitForSingleObject(thread_handle, INFINITE); +} + +void XAudio2Thread::Play(int64_t start, int64_t count) +{ + CheckError(); + + start_frame = start; + end_frame = start + count; + SetEvent(event_start_playback); + + last_playback_restart = GetTickCount64(); + + // Block until playback actually begins to avoid race conditions with + // checking if playback is in progress + HANDLE events_to_wait[] = { is_playing, error_happened }; + switch (WaitForMultipleObjects(2, events_to_wait, FALSE, INFINITE)) { + case WAIT_OBJECT_0 + 0: // Playing + LOG_D("audio/player/xaudio2") << "Playback begun"; + break; + case WAIT_OBJECT_0 + 1: // Error + throw error_message; + default: + throw agi::InternalError("Unexpected result from WaitForMultipleObjects in XAudio2Thread::Play"); + } +} + +void XAudio2Thread::Stop() { + CheckError(); + + SetEvent(event_stop_playback); +} + +void XAudio2Thread::SetEndFrame(int64_t new_end_frame) { + CheckError(); + + end_frame = new_end_frame; + SetEvent(event_update_end_time); +} + +void XAudio2Thread::SetVolume(double new_volume) { + CheckError(); + + volume = new_volume; + SetEvent(event_set_volume); +} + +bool XAudio2Thread::IsPlaying() { + CheckError(); + + switch (WaitForSingleObject(is_playing, 0)) + { + case WAIT_ABANDONED: + throw "The XAudio2Thread playback state event was abandoned, somehow. This should not happen."; + + case WAIT_FAILED: + throw "Failed checking state of XAudio2Thread playback state event."; + + case WAIT_OBJECT_0: + return true; + + case WAIT_TIMEOUT: + default: + return false; + } +} + +int64_t XAudio2Thread::GetCurrentFrame() { + CheckError(); + if (!IsPlaying()) return 0; + ULONGLONG milliseconds_elapsed = GetTickCount64() - last_playback_restart; + return start_frame + milliseconds_elapsed * provider->GetSampleRate() / 1000; +} + +int64_t XAudio2Thread::GetEndFrame() { + CheckError(); + return end_frame; +} + +bool XAudio2Thread::IsDead() { + switch (WaitForSingleObject(thread_running, 0)) + { + case WAIT_OBJECT_0: + return false; + default: + return true; + } +} + +XAudio2Player::XAudio2Player(agi::AudioProvider* provider) :AudioPlayer(provider) { + // The buffer will hold BufferLength times WantedLatency milliseconds of audio + WantedLatency = OPT_GET("Player/Audio/DirectSound/Buffer Latency")->GetInt(); + BufferLength = OPT_GET("Player/Audio/DirectSound/Buffer Length")->GetInt(); + + // sanity checking + if (WantedLatency <= 0) + WantedLatency = 100; + if (BufferLength <= 0) + BufferLength = 5; + + try { + thread = agi::make_unique(provider, WantedLatency, BufferLength); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + throw AudioPlayerOpenError(msg); + } +} + +bool XAudio2Player::IsThreadAlive() { + if (thread && thread->IsDead()) + thread.reset(); + return static_cast(thread); +} + +void XAudio2Player::Play(int64_t start, int64_t count) { + try { + thread->Play(start, count); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + } +} + +void XAudio2Player::Stop() { + try { + if (IsThreadAlive()) thread->Stop(); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + } +} + +bool XAudio2Player::IsPlaying() { + try { + if (!IsThreadAlive()) return false; + return thread->IsPlaying(); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + return false; + } +} + +int64_t XAudio2Player::GetEndPosition() { + try { + if (!IsThreadAlive()) return 0; + return thread->GetEndFrame(); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + return 0; + } +} + +int64_t XAudio2Player::GetCurrentPosition() { + try { + if (!IsThreadAlive()) return 0; + return thread->GetCurrentFrame(); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + return 0; + } +} + +void XAudio2Player::SetEndPosition(int64_t pos) { + try { + if (IsThreadAlive()) thread->SetEndFrame(pos); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + } +} + +void XAudio2Player::SetVolume(double vol) { + try { + if (IsThreadAlive()) thread->SetVolume(vol); + } + catch (const char* msg) { + LOG_E("audio/player/xaudio2") << msg; + } +} +} + +std::unique_ptr CreateXAudio2Player(agi::AudioProvider* provider, wxWindow*) { + return agi::make_unique(provider); +} + +#endif // WITH_XAUDIO2