// 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 #include 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_DIRECTSOUND