diff --git a/NEWS b/NEWS index 3bb6cf8a8..ae4534789 100644 --- a/NEWS +++ b/NEWS @@ -7,8 +7,14 @@ ver 0.23 (not yet released) - snapcast: new plugin ver 0.22.7 (not yet released) +* protocol + - don't use glibc extension to parse time stamps * decoder - ffmpeg: fix build problem with FFmpeg 3.4 +* storage + - curl: don't use glibc extension +* output + - wasapi: add algorithm for finding usable audio format ver 0.22.6 (2021/02/16) * fix missing tags on songs in queue diff --git a/doc/user.rst b/doc/user.rst index f63d241b7..f82faf734 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -141,6 +141,15 @@ Basically, there are two ways to compile :program:`MPD` for Windows: This section is about the latter. +You need: + +* `mingw-w64 `__ +* `Meson 0.49.0 `__ and `Ninja + `__ +* cmake +* pkg-config +* quilt + Just like with the native build, unpack the :program:`MPD` source tarball and change into the directory. Then, instead of :program:`meson`, type: @@ -168,6 +177,11 @@ You need: * Android SDK * `Android NDK r22 `_ +* `Meson 0.49.0 `__ and `Ninja + `__ +* cmake +* pkg-config +* quilt Just like with the native build, unpack the :program:`MPD` source tarball and change into the directory. Then, instead of diff --git a/meson.build b/meson.build index 0eb9a2a03..e374db54c 100644 --- a/meson.build +++ b/meson.build @@ -261,7 +261,6 @@ sources = [ 'src/LogInit.cxx', 'src/ls.cxx', 'src/Instance.cxx', - 'src/win32/Win32Main.cxx', 'src/MusicBuffer.cxx', 'src/MusicPipe.cxx', 'src/MusicChunk.cxx', @@ -309,6 +308,13 @@ sources = [ 'src/PlaylistFile.cxx', ] +if is_windows + sources += [ + 'src/win32/Win32Main.cxx', + 'src/win32/ComWorker.cxx', + ] +endif + if not is_android sources += [ 'src/CommandLine.cxx', diff --git a/src/mixer/plugins/WasapiMixerPlugin.cxx b/src/mixer/plugins/WasapiMixerPlugin.cxx index d45af3dfa..bc2604633 100644 --- a/src/mixer/plugins/WasapiMixerPlugin.cxx +++ b/src/mixer/plugins/WasapiMixerPlugin.cxx @@ -19,7 +19,7 @@ #include "mixer/MixerInternal.hxx" #include "output/plugins/WasapiOutputPlugin.hxx" -#include "win32/Com.hxx" +#include "win32/ComWorker.hxx" #include "win32/HResult.hxx" #include @@ -28,92 +28,103 @@ class WasapiMixer final : public Mixer { WasapiOutput &output; - std::optional com; public: WasapiMixer(WasapiOutput &_output, MixerListener &_listener) : Mixer(wasapi_mixer_plugin, _listener), output(_output) {} - void Open() override { com.emplace(); } + void Open() override {} - void Close() noexcept override { com.reset(); } + void Close() noexcept override {} int GetVolume() override { - HRESULT result; - float volume_level; + auto future = COMWorker::Async([&]() -> int { + HRESULT result; + float volume_level; - if (wasapi_is_exclusive(output)) { - ComPtr endpoint_volume; - result = wasapi_output_get_device(output)->Activate( - __uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr, - endpoint_volume.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get device endpoint volume"); + if (wasapi_is_exclusive(output)) { + ComPtr endpoint_volume; + result = wasapi_output_get_device(output)->Activate( + __uuidof(IAudioEndpointVolume), CLSCTX_ALL, + nullptr, endpoint_volume.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError(result, + "Unable to get device " + "endpoint volume"); + } + + result = endpoint_volume->GetMasterVolumeLevelScalar( + &volume_level); + if (FAILED(result)) { + throw FormatHResultError(result, + "Unable to get master " + "volume level"); + } + } else { + ComPtr session_volume; + result = wasapi_output_get_client(output)->GetService( + __uuidof(ISimpleAudioVolume), + session_volume.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError(result, + "Unable to get client " + "session volume"); + } + + result = session_volume->GetMasterVolume(&volume_level); + if (FAILED(result)) { + throw FormatHResultError( + result, "Unable to get master volume"); + } } - result = endpoint_volume->GetMasterVolumeLevelScalar( - &volume_level); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get master volume level"); - } - } else { - ComPtr session_volume; - result = wasapi_output_get_client(output)->GetService( - __uuidof(ISimpleAudioVolume), - session_volume.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get client session volume"); - } - - result = session_volume->GetMasterVolume(&volume_level); - if (FAILED(result)) { - throw FormatHResultError(result, - "Unable to get master volume"); - } - } - - return std::lround(volume_level * 100.0f); + return std::lround(volume_level * 100.0f); + }); + return future.get(); } void SetVolume(unsigned volume) override { - HRESULT result; - const float volume_level = volume / 100.0f; + COMWorker::Async([&]() { + HRESULT result; + const float volume_level = volume / 100.0f; - if (wasapi_is_exclusive(output)) { - ComPtr endpoint_volume; - result = wasapi_output_get_device(output)->Activate( - __uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr, - endpoint_volume.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get device endpoint volume"); - } + if (wasapi_is_exclusive(output)) { + ComPtr endpoint_volume; + result = wasapi_output_get_device(output)->Activate( + __uuidof(IAudioEndpointVolume), CLSCTX_ALL, + nullptr, endpoint_volume.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to get device endpoint volume"); + } - result = endpoint_volume->SetMasterVolumeLevelScalar(volume_level, - nullptr); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to set master volume level"); - } - } else { - ComPtr session_volume; - result = wasapi_output_get_client(output)->GetService( - __uuidof(ISimpleAudioVolume), - session_volume.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get client session volume"); - } + result = endpoint_volume->SetMasterVolumeLevelScalar( + volume_level, nullptr); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to set master volume level"); + } + } else { + ComPtr session_volume; + result = wasapi_output_get_client(output)->GetService( + __uuidof(ISimpleAudioVolume), + session_volume.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to get client session volume"); + } - result = session_volume->SetMasterVolume(volume_level, nullptr); - if (FAILED(result)) { - throw FormatHResultError(result, - "Unable to set master volume"); + result = session_volume->SetMasterVolume(volume_level, + nullptr); + if (FAILED(result)) { + throw FormatHResultError( + result, "Unable to set master volume"); + } } - } + }).get(); } }; diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/WasapiOutputPlugin.cxx index fc46d439a..99f7dacf4 100644 --- a/src/output/plugins/WasapiOutputPlugin.cxx +++ b/src/output/plugins/WasapiOutputPlugin.cxx @@ -22,16 +22,21 @@ #include "WasapiOutputPlugin.hxx" #include "lib/icu/Win32.hxx" #include "mixer/MixerList.hxx" +#include "output/Error.hxx" +#include "pcm/Export.hxx" #include "thread/Cond.hxx" #include "thread/Mutex.hxx" #include "thread/Name.hxx" #include "thread/Thread.hxx" #include "util/AllocatedString.hxx" +#include "util/ConstBuffer.hxx" #include "util/Domain.hxx" #include "util/RuntimeError.hxx" #include "util/ScopeExit.hxx" +#include "util/StringBuffer.hxx" #include "win32/Com.hxx" #include "win32/ComHeapPtr.hxx" +#include "win32/ComWorker.hxx" #include "win32/HResult.hxx" #include "win32/WinEvent.hxx" @@ -91,25 +96,47 @@ inline bool SafeSilenceTry(Functor &&functor) { } } -inline void SetFormat(WAVEFORMATEXTENSIBLE &device_format, - const AudioFormat &audio_format) noexcept { - device_format.dwChannelMask = GetChannelMask(audio_format.channels); - device_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; - device_format.Format.nChannels = audio_format.channels; - device_format.Format.nSamplesPerSec = audio_format.sample_rate; - device_format.Format.nBlockAlign = audio_format.GetFrameSize(); - device_format.Format.nAvgBytesPerSec = - audio_format.sample_rate * audio_format.GetFrameSize(); - device_format.Format.wBitsPerSample = audio_format.GetSampleSize() * 8; - device_format.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); - device_format.Samples.wValidBitsPerSample = audio_format.GetSampleSize() * 8; - if (audio_format.format == SampleFormat::FLOAT) { - device_format.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; +std::vector GetFormats(const AudioFormat &audio_format) noexcept { + std::vector Result; + if (audio_format.format == SampleFormat::S24_P32) { + Result.resize(2); + Result[0].Format.wBitsPerSample = 24; + Result[0].Samples.wValidBitsPerSample = 24; + Result[1].Format.wBitsPerSample = 32; + Result[1].Samples.wValidBitsPerSample = 24; } else { - device_format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + Result.resize(1); + Result[0].Format.wBitsPerSample = audio_format.GetSampleSize() * 8; + Result[0].Samples.wValidBitsPerSample = audio_format.GetSampleSize() * 8; } + const DWORD mask = GetChannelMask(audio_format.channels); + const GUID guid = audio_format.format == SampleFormat::FLOAT + ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT + : KSDATAFORMAT_SUBTYPE_PCM; + for (auto &device_format : Result) { + device_format.dwChannelMask = mask; + device_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + device_format.Format.nChannels = audio_format.channels; + device_format.Format.nSamplesPerSec = audio_format.sample_rate; + device_format.Format.cbSize = + sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); + device_format.SubFormat = guid; + device_format.Format.nBlockAlign = device_format.Format.nChannels * + device_format.Format.wBitsPerSample / + 8; + device_format.Format.nAvgBytesPerSec = + audio_format.sample_rate * device_format.Format.nBlockAlign; + } + return Result; } +#ifdef ENABLE_DSD +void SetDSDFallback(AudioFormat &audio_format) noexcept { + audio_format.format = SampleFormat::FLOAT; + audio_format.sample_rate = 384000; +} +#endif + inline constexpr const unsigned int kErrorId = -1; } // namespace @@ -117,55 +144,47 @@ inline constexpr const unsigned int kErrorId = -1; class WasapiOutputThread : public Thread { public: enum class Status : uint32_t { FINISH, PLAY, PAUSE }; - WasapiOutputThread(std::shared_ptr _event, ComPtr _client, + WasapiOutputThread(IAudioClient *_client, ComPtr &&_render_client, const UINT32 _frame_size, const UINT32 _buffer_size_in_frames, - bool _is_exclusive, - boost::lockfree::spsc_queue &_spsc_buffer) - : Thread(BIND_THIS_METHOD(Work)), event(std::move(_event)), - client(std::move(_client)), render_client(std::move(_render_client)), - frame_size(_frame_size), buffer_size_in_frames(_buffer_size_in_frames), - is_exclusive(_is_exclusive), spsc_buffer(_spsc_buffer) {} + bool _is_exclusive) + : Thread(BIND_THIS_METHOD(Work)), client(_client), + render_client(std::move(_render_client)), frame_size(_frame_size), + buffer_size_in_frames(_buffer_size_in_frames), is_exclusive(_is_exclusive), + spsc_buffer(_buffer_size_in_frames * 4 * _frame_size) {} void Finish() noexcept { return SetStatus(Status::FINISH); } void Play() noexcept { return SetStatus(Status::PLAY); } void Pause() noexcept { return SetStatus(Status::PAUSE); } - void WaitWrite() noexcept { - std::unique_lock lock(write.mutex); - write.cond.wait(lock); - } + void WaitDataPoped() noexcept { data_poped.Wait(INFINITE); } void CheckException() { - std::unique_lock lock(error.mutex); - if (error.error_ptr) { - std::exception_ptr err = std::exchange(error.error_ptr, nullptr); - error.cond.notify_all(); + if (error.occur.load()) { + auto err = std::exchange(error.ptr, nullptr); + error.thrown.Set(); std::rethrow_exception(err); } } private: - std::shared_ptr event; - std::optional com; - ComPtr client; + friend class WasapiOutput; + WinEvent event; + WinEvent data_poped; + IAudioClient *client; ComPtr render_client; const UINT32 frame_size; const UINT32 buffer_size_in_frames; bool is_exclusive; - boost::lockfree::spsc_queue &spsc_buffer; alignas(BOOST_LOCKFREE_CACHELINE_BYTES) std::atomic status = Status::PAUSE; alignas(BOOST_LOCKFREE_CACHELINE_BYTES) struct { - Mutex mutex; - Cond cond; - } write{}; - alignas(BOOST_LOCKFREE_CACHELINE_BYTES) struct { - Mutex mutex; - Cond cond; - std::exception_ptr error_ptr = nullptr; - } error{}; + std::atomic_bool occur = false; + std::exception_ptr ptr = nullptr; + WinEvent thrown; + } error; + boost::lockfree::spsc_queue spsc_buffer; void SetStatus(Status s) noexcept { status.store(s); - event->Set(); + event.Set(); } void Work() noexcept; }; @@ -174,14 +193,23 @@ class WasapiOutput final : public AudioOutput { public: static AudioOutput *Create(EventLoop &, const ConfigBlock &block); WasapiOutput(const ConfigBlock &block); - void Enable() override; - void Disable() noexcept override; - void Open(AudioFormat &audio_format) override; + void Enable() override { + COMWorker::Aquire(); + COMWorker::Async([&]() { OpenDevice(); }).get(); + } + void Disable() noexcept override { + COMWorker::Async([&]() { DoDisable(); }).get(); + COMWorker::Release(); + } + void Open(AudioFormat &audio_format) override { + COMWorker::Async([&]() { DoOpen(audio_format); }).get(); + } void Close() noexcept override; std::chrono::steady_clock::duration Delay() const noexcept override; size_t Play(const void *chunk, size_t size) override; void Drain() override; bool Pause() override; + void Interrupt() noexcept override; constexpr bool Exclusive() const { return is_exclusive; } constexpr size_t FrameSize() const { return device_format.Format.nBlockAlign; } @@ -190,25 +218,29 @@ public: } private: + std::atomic_flag not_interrupted = true; bool is_started = false; bool is_exclusive; bool enumerate_devices; std::string device_config; std::vector> device_desc; - std::shared_ptr event; - std::optional com; ComPtr enumerator; ComPtr device; ComPtr client; WAVEFORMATEXTENSIBLE device_format; - std::unique_ptr thread; - std::unique_ptr> spsc_buffer; + std::optional thread; std::size_t watermark; + std::optional pcm_export; friend bool wasapi_is_exclusive(WasapiOutput &output) noexcept; friend IMMDevice *wasapi_output_get_device(WasapiOutput &output) noexcept; friend IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept; + void DoDisable() noexcept; + void DoOpen(AudioFormat &audio_format); + + void OpenDevice(); + bool TryFormatExclusive(const AudioFormat &audio_format); void FindExclusiveFormatSupported(AudioFormat &audio_format); void FindSharedFormatSupported(AudioFormat &audio_format); void EnumerateDevices(); @@ -234,18 +266,10 @@ IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept { void WasapiOutputThread::Work() noexcept { SetThreadName("Wasapi Output Worker"); FormatDebug(wasapi_output_domain, "Working thread started"); - try { - com.emplace(); - } catch (...) { - std::unique_lock lock(error.mutex); - error.error_ptr = std::current_exception(); - error.cond.wait(lock); - assert(error.error_ptr == nullptr); - return; - } + COM com{true}; while (true) { try { - event->Wait(INFINITE); + event.Wait(INFINITE); Status current_state = status.load(); if (current_state == Status::FINISH) { @@ -254,51 +278,51 @@ void WasapiOutputThread::Work() noexcept { return; } - AtScopeExit(&) { write.cond.notify_all(); }; - - HRESULT result; - UINT32 data_in_frames; - result = client->GetCurrentPadding(&data_in_frames); - if (FAILED(result)) { - throw FormatHResultError(result, - "Failed to get current padding"); - } - UINT32 write_in_frames = buffer_size_in_frames; if (!is_exclusive) { + UINT32 data_in_frames; + if (HRESULT result = + client->GetCurrentPadding(&data_in_frames); + FAILED(result)) { + throw FormatHResultError( + result, "Failed to get current padding"); + } + if (data_in_frames >= buffer_size_in_frames) { continue; } write_in_frames -= data_in_frames; - } else if (data_in_frames >= buffer_size_in_frames * 2) { - continue; } BYTE *data; + DWORD mode = 0; - result = render_client->GetBuffer(write_in_frames, &data); - if (FAILED(result)) { + if (HRESULT result = + render_client->GetBuffer(write_in_frames, &data); + FAILED(result)) { throw FormatHResultError(result, "Failed to get buffer"); } AtScopeExit(&) { - render_client->ReleaseBuffer(write_in_frames, 0); + render_client->ReleaseBuffer(write_in_frames, mode); }; - const UINT32 write_size = write_in_frames * frame_size; - UINT32 new_data_size = 0; if (current_state == Status::PLAY) { + const UINT32 write_size = write_in_frames * frame_size; + UINT32 new_data_size = 0; new_data_size = spsc_buffer.pop(data, write_size); + std::fill_n(data + new_data_size, + write_size - new_data_size, 0); + data_poped.Set(); } else { + mode = AUDCLNT_BUFFERFLAGS_SILENT; FormatDebug(wasapi_output_domain, "Working thread paused"); } - std::fill_n(data + new_data_size, write_size - new_data_size, 0); } catch (...) { - std::unique_lock lock(error.mutex); - error.error_ptr = std::current_exception(); - error.cond.wait(lock); - assert(error.error_ptr == nullptr); + error.ptr = std::current_exception(); + error.occur.store(true); + error.thrown.Wait(INFINITE); } } } @@ -313,15 +337,300 @@ WasapiOutput::WasapiOutput(const ConfigBlock &block) enumerate_devices(block.GetBlockValue("enumerate", false)), device_config(block.GetBlockValue("device", "")) {} -void WasapiOutput::Enable() { - com.emplace(); - event = std::make_shared(); +/// run inside COMWorkerThread +void WasapiOutput::DoDisable() noexcept { + if (thread) { + try { + thread->Finish(); + thread->Join(); + } catch (std::exception &err) { + FormatError(wasapi_output_domain, "exception while disabling: %s", + err.what()); + } + thread.reset(); + client.reset(); + } + device.reset(); + enumerator.reset(); +} + +/// run inside COMWorkerThread +void WasapiOutput::DoOpen(AudioFormat &audio_format) { + client.reset(); + + DWORD state; + if (HRESULT result = device->GetState(&state); FAILED(result)) { + throw FormatHResultError(result, "Unable to get device status"); + } + if (state != DEVICE_STATE_ACTIVE) { + device.reset(); + OpenDevice(); + } + + if (HRESULT result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, + client.AddressCast()); + FAILED(result)) { + throw FormatHResultError(result, "Unable to activate audio client"); + } + + if (audio_format.channels > 8) { + audio_format.channels = 8; + } + +#ifdef ENABLE_DSD + if (audio_format.format == SampleFormat::DSD) { + SetDSDFallback(audio_format); + } +#endif + + if (Exclusive()) { + FindExclusiveFormatSupported(audio_format); + } else { + FindSharedFormatSupported(audio_format); + } + bool require_export = audio_format.format == SampleFormat::S24_P32; + if (require_export) { + PcmExport::Params params; + params.dsd_mode = PcmExport::DsdMode::NONE; + params.shift8 = false; + params.pack24 = false; + if (device_format.Format.wBitsPerSample == 32 && + device_format.Samples.wValidBitsPerSample == 24) { + params.shift8 = true; + } + if (device_format.Format.wBitsPerSample == 24) { + params.pack24 = true; + } + FormatDebug(wasapi_output_domain, "Packing data: shift8=%d pack24=%d", + int(params.shift8), int(params.pack24)); + pcm_export.emplace(); + pcm_export->Open(audio_format.format, audio_format.channels, params); + } + + using s = std::chrono::seconds; + using ms = std::chrono::milliseconds; + using ns = std::chrono::nanoseconds; + using hundred_ns = std::chrono::duration>; + + // The unit in REFERENCE_TIME is hundred nanoseconds + REFERENCE_TIME default_device_period, min_device_period; + + if (HRESULT result = + client->GetDevicePeriod(&default_device_period, &min_device_period); + FAILED(result)) { + throw FormatHResultError(result, "Unable to get device period"); + } + FormatDebug(wasapi_output_domain, + "Default device period: %I64u ns, Minimum device period: " + "%I64u ns", + ns(hundred_ns(default_device_period)).count(), + ns(hundred_ns(min_device_period)).count()); + + REFERENCE_TIME buffer_duration; + if (Exclusive()) { + buffer_duration = default_device_period; + } else { + const REFERENCE_TIME align = hundred_ns(ms(50)).count(); + buffer_duration = (align / default_device_period) * default_device_period; + } + FormatDebug(wasapi_output_domain, "Buffer duration: %I64u ns", + size_t(ns(hundred_ns(buffer_duration)).count())); + + if (Exclusive()) { + if (HRESULT result = client->Initialize( + AUDCLNT_SHAREMODE_EXCLUSIVE, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, buffer_duration, + buffer_duration, + reinterpret_cast(&device_format), nullptr); + FAILED(result)) { + if (result == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) { + // https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize + UINT32 buffer_size_in_frames = 0; + result = client->GetBufferSize(&buffer_size_in_frames); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to get audio client buffer size"); + } + buffer_duration = + std::ceil(double(buffer_size_in_frames * + hundred_ns(s(1)).count()) / + SampleRate()); + FormatDebug( + wasapi_output_domain, + "Aligned buffer duration: %I64u ns", + size_t(ns(hundred_ns(buffer_duration)).count())); + client.reset(); + result = device->Activate(__uuidof(IAudioClient), + CLSCTX_ALL, nullptr, + client.AddressCast()); + if (FAILED(result)) { + throw FormatHResultError( + result, + "Unable to activate audio client"); + } + result = client->Initialize( + AUDCLNT_SHAREMODE_EXCLUSIVE, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + buffer_duration, buffer_duration, + reinterpret_cast(&device_format), + nullptr); + } + + if (FAILED(result)) { + throw FormatHResultError( + result, "Unable to initialize audio client"); + } + } + } else { + if (HRESULT result = client->Initialize( + AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + buffer_duration, 0, + reinterpret_cast(&device_format), nullptr); + FAILED(result)) { + throw FormatHResultError(result, + "Unable to initialize audio client"); + } + } + + ComPtr render_client; + if (HRESULT result = client->GetService(IID_PPV_ARGS(render_client.Address())); + FAILED(result)) { + throw FormatHResultError(result, "Unable to get new render client"); + } + + UINT32 buffer_size_in_frames; + if (HRESULT result = client->GetBufferSize(&buffer_size_in_frames); + FAILED(result)) { + throw FormatHResultError(result, + "Unable to get audio client buffer size"); + } + + watermark = buffer_size_in_frames * 3 * FrameSize(); + thread.emplace(client.get(), std::move(render_client), FrameSize(), + buffer_size_in_frames, is_exclusive); + + if (HRESULT result = client->SetEventHandle(thread->event.handle()); + FAILED(result)) { + throw FormatHResultError(result, "Unable to set event handler"); + } + + thread->Start(); +} + +void WasapiOutput::Close() noexcept { + assert(thread); + + try { + COMWorker::Async([&]() { + if (HRESULT result = client->Stop(); FAILED(result)) { + throw FormatHResultError(result, "Failed to stop client"); + } + }).get(); + thread->CheckException(); + } catch (std::exception &err) { + FormatError(wasapi_output_domain, "exception while stoping: %s", + err.what()); + } + is_started = false; + thread->Finish(); + thread->Join(); + COMWorker::Async([&]() { + thread.reset(); + client.reset(); + }).get(); + pcm_export.reset(); +} + +std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept { + if (!is_started) { + // idle while paused + return std::chrono::seconds(1); + } + + assert(thread); + + const size_t data_size = thread->spsc_buffer.read_available(); + const size_t delay_size = std::max(data_size, watermark) - watermark; + + using s = std::chrono::seconds; + using duration = std::chrono::steady_clock::duration; + auto result = duration(s(delay_size)) / device_format.Format.nAvgBytesPerSec; + return result; +} + +size_t WasapiOutput::Play(const void *chunk, size_t size) { + assert(thread); + + not_interrupted.test_and_set(); + + ConstBuffer input(chunk, size); + if (pcm_export) { + input = pcm_export->Export(input); + } + if (input.empty()) + return size; + + do { + const size_t consumed_size = thread->spsc_buffer.push( + static_cast(input.data), input.size); + if (consumed_size == 0) { + assert(is_started); + thread->WaitDataPoped(); + if (!not_interrupted.test_and_set()) { + throw AudioOutputInterrupted{}; + } + continue; + } + + if (!is_started) { + is_started = true; + thread->Play(); + COMWorker::Async([&]() { + if (HRESULT result = client->Start(); FAILED(result)) { + throw FormatHResultError( + result, "Failed to start client"); + } + }).wait(); + } + + thread->CheckException(); + + if (pcm_export) { + return pcm_export->CalcInputSize(consumed_size); + } + return consumed_size; + } while (true); +} + +bool WasapiOutput::Pause() { + if (is_started) { + thread->Pause(); + is_started = false; + } + thread->CheckException(); + return true; +} + +void WasapiOutput::Interrupt() noexcept { + if (thread) { + not_interrupted.clear(); + thread->data_poped.Set(); + } +} + +void WasapiOutput::Drain() { + assert(thread); + + thread->spsc_buffer.consume_all([](auto &&) {}); + thread->CheckException(); +} + +/// run inside COMWorkerThread +void WasapiOutput::OpenDevice() { enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER); - device_desc.clear(); - device.reset(); - if (enumerate_devices && SafeTry([this]() { EnumerateDevices(); })) { for (const auto &[device, desc] : device_desc) { FormatNotice(wasapi_output_domain, @@ -349,297 +658,73 @@ void WasapiOutput::Enable() { device_desc.clear(); } -void WasapiOutput::Disable() noexcept { - if (thread) { - try { - thread->Finish(); - thread->Join(); - } catch (std::exception &err) { - FormatError(wasapi_output_domain, "exception while disabling: %s", - err.what()); - } - thread.reset(); - spsc_buffer.reset(); - client.reset(); - } - device.reset(); - enumerator.reset(); - com.reset(); - event.reset(); -} - -void WasapiOutput::Open(AudioFormat &audio_format) { - if (audio_format.channels == 0) { - throw FormatInvalidArgument("channels should > 0"); - } - - client.reset(); - - HRESULT result; - result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - client.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError(result, "Unable to activate audio client"); - } - - if (audio_format.format == SampleFormat::S24_P32) { - audio_format.format = SampleFormat::S32; - } - if (audio_format.channels > 8) { - audio_format.channels = 8; - } - - if (Exclusive()) { - FindExclusiveFormatSupported(audio_format); - } else { - FindSharedFormatSupported(audio_format); - } - - using s = std::chrono::seconds; - using ms = std::chrono::milliseconds; - using ns = std::chrono::nanoseconds; - using hundred_ns = std::chrono::duration>; - - // The unit in REFERENCE_TIME is hundred nanoseconds - REFERENCE_TIME device_period; - result = client->GetDevicePeriod(&device_period, nullptr); - if (FAILED(result)) { - throw FormatHResultError(result, "Unable to get device period"); - } - FormatDebug(wasapi_output_domain, "Device period: %I64u ns", - size_t(ns(hundred_ns(device_period)).count())); - - REFERENCE_TIME buffer_duration = device_period; - if (!Exclusive()) { - const REFERENCE_TIME align = hundred_ns(ms(50)).count(); - buffer_duration = (align / device_period) * device_period; - } - FormatDebug(wasapi_output_domain, "Buffer duration: %I64u ns", - size_t(ns(hundred_ns(buffer_duration)).count())); - - if (Exclusive()) { - result = client->Initialize( +/// run inside COMWorkerThread +bool WasapiOutput::TryFormatExclusive(const AudioFormat &audio_format) { + for (auto test_format : GetFormats(audio_format)) { + HRESULT result = client->IsFormatSupported( AUDCLNT_SHAREMODE_EXCLUSIVE, - AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - buffer_duration, buffer_duration, - reinterpret_cast(&device_format), nullptr); - if (result == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) { - // https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize - UINT32 buffer_size_in_frames = 0; - result = client->GetBufferSize(&buffer_size_in_frames); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to get audio client buffer size"); - } - buffer_duration = std::ceil( - double(buffer_size_in_frames * hundred_ns(s(1)).count()) / - SampleRate()); - FormatDebug(wasapi_output_domain, - "Aligned buffer duration: %I64u ns", - size_t(ns(hundred_ns(buffer_duration)).count())); - client.reset(); - result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, - nullptr, client.AddressCast()); - if (FAILED(result)) { - throw FormatHResultError( - result, "Unable to activate audio client"); - } - result = client->Initialize( - AUDCLNT_SHAREMODE_EXCLUSIVE, - AUDCLNT_STREAMFLAGS_NOPERSIST | - AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - buffer_duration, buffer_duration, - reinterpret_cast(&device_format), - nullptr); + reinterpret_cast(&test_format), nullptr); + const auto format_string = ToString(audio_format); + const auto result_string = std::string(HRESULTToString(result)); + FormatDebug(wasapi_output_domain, "Trying %s %lu %u-%u (exclusive) -> %s", + format_string.c_str(), test_format.Format.nSamplesPerSec, + test_format.Format.wBitsPerSample, + test_format.Samples.wValidBitsPerSample, + result_string.c_str()); + if (SUCCEEDED(result)) { + device_format = test_format; + return true; } - } else { - result = client->Initialize( - AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - buffer_duration, 0, - reinterpret_cast(&device_format), nullptr); } - - if (FAILED(result)) { - throw FormatHResultError(result, "Unable to initialize audio client"); - } - - ComPtr render_client; - result = client->GetService(IID_PPV_ARGS(render_client.Address())); - if (FAILED(result)) { - throw FormatHResultError(result, "Unable to get new render client"); - } - - result = client->SetEventHandle(event->handle()); - if (FAILED(result)) { - throw FormatHResultError(result, "Unable to set event handler"); - } - - UINT32 buffer_size_in_frames; - result = client->GetBufferSize(&buffer_size_in_frames); - if (FAILED(result)) { - throw FormatHResultError(result, - "Unable to get audio client buffer size"); - } - - watermark = buffer_size_in_frames * 3 * FrameSize(); - spsc_buffer = std::make_unique>( - buffer_size_in_frames * 4 * FrameSize()); - thread = std::make_unique( - event, client, std::move(render_client), FrameSize(), - buffer_size_in_frames, is_exclusive, *spsc_buffer); - thread->Start(); -} - -void WasapiOutput::Close() noexcept { - assert(client && thread); - Pause(); - thread->Finish(); - thread->Join(); - thread.reset(); - spsc_buffer.reset(); - client.reset(); -} - -std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept { - if (!client || !is_started) { - return std::chrono::steady_clock::duration::zero(); - } - - const size_t data_size = spsc_buffer->read_available(); - const size_t delay_size = std::max(data_size, watermark) - watermark; - - using s = std::chrono::seconds; - using duration = std::chrono::steady_clock::duration; - auto result = duration(s(delay_size)) / device_format.Format.nAvgBytesPerSec; - return result; -} - -size_t WasapiOutput::Play(const void *chunk, size_t size) { - if (!client || !thread) { - return 0; - } - - do { - const size_t consumed_size = - spsc_buffer->push(static_cast(chunk), size); - if (consumed_size == 0) { - assert(is_started); - thread->WaitWrite(); - continue; - } - - if (!is_started) { - is_started = true; - - thread->Play(); - - HRESULT result; - result = client->Start(); - if (FAILED(result)) { - throw FormatHResultError(result, - "Failed to start client"); - } - } - - thread->CheckException(); - - return consumed_size; - } while (true); -} - -void WasapiOutput::Drain() { - spsc_buffer->consume_all([](auto &&) {}); - thread->CheckException(); -} - -bool WasapiOutput::Pause() { - if (!client || !thread) { - return false; - } - if (!is_started) { - return true; - } - - HRESULT result; - result = client->Stop(); - if (FAILED(result)) { - throw FormatHResultError(result, "Failed to stop client"); - } - - is_started = false; - thread->Pause(); - thread->CheckException(); - - return true; + return false; } +/// run inside COMWorkerThread void WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format) { - SetFormat(device_format, audio_format); - - do { - HRESULT result; - result = client->IsFormatSupported( - AUDCLNT_SHAREMODE_EXCLUSIVE, - reinterpret_cast(&device_format), nullptr); - - switch (result) { - case S_OK: - return; - case AUDCLNT_E_UNSUPPORTED_FORMAT: - break; - default: - throw FormatHResultError(result, "IsFormatSupported failed"); - } - - // Trying PCM fallback. - if (audio_format.format == SampleFormat::FLOAT) { - audio_format.format = SampleFormat::S32; + for (uint8_t channels : {0, 2, 6, 8, 7, 1, 4, 5, 3}) { + if (audio_format.channels == channels) { continue; } - - // Trying sample rate fallback. - if (audio_format.sample_rate > 96000) { - audio_format.sample_rate = 96000; - continue; + if (channels == 0) { + channels = audio_format.channels; } - - if (audio_format.sample_rate > 88200) { - audio_format.sample_rate = 88200; - continue; + auto old_channels = std::exchange(audio_format.channels, channels); + for (uint32_t rate : {0, 384000, 352800, 192000, 176400, 96000, 88200, + 48000, 44100, 32000, 22050, 16000, 11025, 8000}) { + if (audio_format.sample_rate <= rate) { + continue; + } + if (rate == 0) { + rate = audio_format.sample_rate; + } + auto old_rate = std::exchange(audio_format.sample_rate, rate); + for (SampleFormat format : { + SampleFormat::UNDEFINED, + SampleFormat::S32, + SampleFormat::S24_P32, + SampleFormat::S16, + SampleFormat::S8, + }) { + if (audio_format.format == format) { + continue; + } + if (format == SampleFormat::UNDEFINED) { + format = audio_format.format; + } + auto old_format = + std::exchange(audio_format.format, format); + if (TryFormatExclusive(audio_format)) { + return; + } + audio_format.format = old_format; + } + audio_format.sample_rate = old_rate; } - - if (audio_format.sample_rate > 64000) { - audio_format.sample_rate = 64000; - continue; - } - - if (audio_format.sample_rate > 48000) { - audio_format.sample_rate = 48000; - continue; - } - - // Trying 2 channels fallback. - if (audio_format.channels > 2) { - audio_format.channels = 2; - continue; - } - - // Trying S16 fallback. - if (audio_format.format == SampleFormat::S32) { - audio_format.format = SampleFormat::S16; - continue; - } - - if (audio_format.sample_rate > 41100) { - audio_format.sample_rate = 41100; - continue; - } - - throw FormatHResultError(result, "Format is not supported"); - } while (true); + audio_format.channels = old_channels; + } } +/// run inside COMWorkerThread void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { HRESULT result; ComHeapPtr mixer_format; @@ -649,15 +734,23 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { if (FAILED(result)) { throw FormatHResultError(result, "GetMixFormat failed"); } - audio_format.sample_rate = device_format.Format.nSamplesPerSec; - - SetFormat(device_format, audio_format); + audio_format.sample_rate = mixer_format->nSamplesPerSec; + device_format = GetFormats(audio_format).front(); ComHeapPtr closest_format; result = client->IsFormatSupported( AUDCLNT_SHAREMODE_SHARED, reinterpret_cast(&device_format), closest_format.AddressCast()); + { + const auto format_string = ToString(audio_format); + const auto result_string = std::string(HRESULTToString(result)); + FormatDebug(wasapi_output_domain, "Trying %s %lu %u-%u (shared) -> %s", + format_string.c_str(), device_format.Format.nSamplesPerSec, + device_format.Format.wBitsPerSample, + device_format.Samples.wValidBitsPerSample, + result_string.c_str()); + } if (FAILED(result) && result != AUDCLNT_E_UNSUPPORTED_FORMAT) { throw FormatHResultError(result, "IsFormatSupported failed"); @@ -671,12 +764,23 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { // Trying channels fallback. audio_format.channels = mixer_format->nChannels; - SetFormat(device_format, audio_format); + device_format = GetFormats(audio_format).front(); result = client->IsFormatSupported( AUDCLNT_SHAREMODE_SHARED, reinterpret_cast(&device_format), closest_format.AddressCast()); + { + const auto format_string = ToString(audio_format); + const auto result_string = std::string(HRESULTToString(result)); + FormatDebug(wasapi_output_domain, + "Trying %s %lu %u-%u (shared) -> %s", + format_string.c_str(), + device_format.Format.nSamplesPerSec, + device_format.Format.wBitsPerSample, + device_format.Samples.wValidBitsPerSample, + result_string.c_str()); + } if (FAILED(result)) { throw FormatHResultError(result, "Format is not supported"); } @@ -715,7 +819,10 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { audio_format.format = SampleFormat::S16; break; case 32: - audio_format.format = SampleFormat::S32; + audio_format.format = + device_format.Samples.wValidBitsPerSample == 32 + ? SampleFormat::S32 + : SampleFormat::S24_P32; break; } } else if (device_format.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) { @@ -723,6 +830,7 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) { } } +/// run inside COMWorkerThread void WasapiOutput::EnumerateDevices() { if (!device_desc.empty()) { return; @@ -775,6 +883,7 @@ void WasapiOutput::EnumerateDevices() { } } +/// run inside COMWorkerThread void WasapiOutput::GetDevice(unsigned int index) { HRESULT result; @@ -791,6 +900,7 @@ void WasapiOutput::GetDevice(unsigned int index) { } } +/// run inside COMWorkerThread unsigned int WasapiOutput::SearchDevice(std::string_view name) { if (!SafeTry([this]() { EnumerateDevices(); })) { return kErrorId; @@ -808,6 +918,7 @@ unsigned int WasapiOutput::SearchDevice(std::string_view name) { return iter->first; } +/// run inside COMWorkerThread void WasapiOutput::GetDefaultDevice() { HRESULT result; result = enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, diff --git a/src/storage/plugins/CurlStorage.cxx b/src/storage/plugins/CurlStorage.cxx index d05b90a31..9d862eb32 100644 --- a/src/storage/plugins/CurlStorage.cxx +++ b/src/storage/plugins/CurlStorage.cxx @@ -193,7 +193,7 @@ ParseTimeStamp(const char *s) { try { // TODO: make this more robust - return ParseTimePoint(s, "%a, %d %b %Y %T %Z"); + return ParseTimePoint(s, "%a, %d %b %Y %T"); } catch (...) { return std::chrono::system_clock::time_point::min(); } diff --git a/src/thread/Future.hxx b/src/thread/Future.hxx new file mode 100644 index 000000000..a277066f9 --- /dev/null +++ b/src/thread/Future.hxx @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef THREAD_FUTURE_HXX +#define THREAD_FUTURE_HXX + +#ifdef _WIN32 + +#include "WindowsFuture.hxx" + +template +using Future = WinFuture; +template +using Promise = WinPromise; + +#else + +#include + +template +using Future = std::future; +template +using Promise = std::promise; + +#endif + +#endif diff --git a/src/thread/WindowsFuture.hxx b/src/thread/WindowsFuture.hxx new file mode 100644 index 000000000..f5d304a1e --- /dev/null +++ b/src/thread/WindowsFuture.hxx @@ -0,0 +1,394 @@ +/* + * Copyright 2020-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef THREAD_WINDOWS_FUTURE_HXX +#define THREAD_WINDOWS_FUTURE_HXX + +#include "CriticalSection.hxx" +#include "WindowsCond.hxx" +#include +#include +#include + +enum class WinFutureErrc : int { + future_already_retrieved = 1, + promise_already_satisfied, + no_state, + broken_promise, +}; + +enum class WinFutureStatus { ready, timeout, deferred }; + +static inline const std::error_category &win_future_category() noexcept; +class WinFutureCategory : public std::error_category { +public: + const char *name() const noexcept override { return "win_future"; } + std::string message(int Errcode) const override { + using namespace std::literals; + switch (static_cast(Errcode)) { + case WinFutureErrc::broken_promise: + return "Broken promise"s; + case WinFutureErrc::future_already_retrieved: + return "Future already retrieved"s; + case WinFutureErrc::promise_already_satisfied: + return "Promise already satisfied"s; + case WinFutureErrc::no_state: + return "No associated state"s; + default: + return "Unknown error"s; + } + } + std::error_condition default_error_condition(int code) const noexcept override { + return std::error_condition(code, win_future_category()); + } +}; +static inline const std::error_category &win_future_category() noexcept { + static const WinFutureCategory win_future_category_instance{}; + return win_future_category_instance; +} + +class WinFutureError : public std::logic_error { +public: + WinFutureError(WinFutureErrc errcode) + : WinFutureError( + std::error_code(static_cast(errcode), win_future_category())) {} + +private: + explicit WinFutureError(std::error_code errcode) + : std::logic_error("WinFutureError: " + errcode.message()), code(errcode) {} + std::error_code code; +}; + +template +class WinFutureState { +private: + mutable CriticalSection mutex; + WindowsCond condition; + std::variant result; + bool retrieved = false; + bool ready = false; + +public: + bool is_ready() const noexcept { + std::unique_lock lock(mutex); + return ready; + } + + bool already_retrieved() const noexcept { + std::unique_lock lock(mutex); + return retrieved; + } + + void wait() { + std::unique_lock lock(mutex); + condition.wait(lock, [this]() { return ready; }); + } + + template + WinFutureStatus + wait_for(const std::chrono::duration &timeout_duration) const { + std::unique_lock lock(mutex); + // deferred function not support yet + if (condition.wait_for(lock, timeout_duration, + [this]() { return ready; })) { + return WinFutureStatus::ready; + } + return WinFutureStatus::timeout; + } + + virtual T &get_value() { + std::unique_lock lock(mutex); + if (retrieved) { + throw WinFutureError(WinFutureErrc::future_already_retrieved); + } + if (auto eptr = std::get_if(&result)) { + std::rethrow_exception(*eptr); + } + retrieved = true; + condition.wait(lock, [this]() { return ready; }); + if (auto eptr = std::get_if(&result)) { + std::rethrow_exception(*eptr); + } + return *std::get_if(&result); + } + + void set_value(const T &value) { + std::unique_lock lock(mutex); + if (!std::holds_alternative(&result)) { + throw WinFutureError(WinFutureErrc::promise_already_satisfied); + } + result.template emplace(value); + ready = true; + condition.notify_all(); + } + + void set_value(T &&value) { + std::unique_lock lock(mutex); + if (!std::holds_alternative(result)) { + throw WinFutureError(WinFutureErrc::promise_already_satisfied); + } + result.template emplace(std::move(value)); + ready = true; + condition.notify_all(); + } + + void set_exception(std::exception_ptr eptr) { + std::unique_lock lock(mutex); + if (!std::holds_alternative(result)) { + throw WinFutureError(WinFutureErrc::promise_already_satisfied); + } + result.template emplace(eptr); + ready = true; + condition.notify_all(); + } +}; + +template +class WinFutureStateManager { +public: + WinFutureStateManager() = default; + WinFutureStateManager(std::shared_ptr> new_state) + : state(std::move(new_state)) {} + WinFutureStateManager(const WinFutureStateManager &) = default; + WinFutureStateManager &operator=(const WinFutureStateManager &) = default; + WinFutureStateManager(WinFutureStateManager &&) = default; + WinFutureStateManager &operator=(WinFutureStateManager &&) = default; + + [[nodiscard]] bool valid() const noexcept { return static_cast(state); } + + void wait() const { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + state->wait(); + } + + template + WinFutureStatus + wait_for(const std::chrono::duration &timeout_duration) const { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + return state->wait_for(timeout_duration); + } + + T &get_value() const { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + return state->get_value(); + } + + void set_value(const T &value) { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + state->set_value(value); + } + + void set_value(T &&value) { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + state->set_value(std::move(value)); + } + + void set_exception(std::exception_ptr eptr) { + if (!valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + state->set_exception(eptr); + } + +private: + std::shared_ptr> state; +}; + +template +class WinFuture : public WinFutureStateManager { + using Base = WinFutureStateManager; + static_assert(!std::is_array_v && std::is_object_v && + std::is_destructible_v, + "T in future must meet the Cpp17Destructible requirements " + "(N4878 [futures.unique.future]/4)."); + +public: + WinFuture() noexcept = default; + WinFuture(WinFuture &&) noexcept = default; + WinFuture &operator=(WinFuture &&) noexcept = default; + WinFuture(const WinFuture &) noexcept = delete; + WinFuture &operator=(const WinFuture &) noexcept = delete; + + WinFuture(const Base &base, std::monostate) : Base(base) {} + ~WinFuture() noexcept = default; + T get() { + WinFuture local(std::move(*this)); + return std::move(local.get_value()); + } + +private: + using Base::get_value; + using Base::set_exception; + using Base::set_value; +}; + +template +class WinFuture : public WinFutureStateManager { + using Base = WinFutureStateManager; + +public: + WinFuture() noexcept = default; + WinFuture(WinFuture &&) noexcept = default; + WinFuture &operator=(WinFuture &&) noexcept = default; + WinFuture(const WinFuture &) noexcept = delete; + WinFuture &operator=(const WinFuture &) noexcept = delete; + + WinFuture(const Base &base, std::monostate) : Base(base) {} + ~WinFuture() noexcept = default; + T &get() { + WinFuture local(std::move(*this)); + return *local.get_value(); + } + +private: + using Base::get_value; + using Base::set_exception; + using Base::set_value; +}; + +template <> +class WinFuture : public WinFutureStateManager { + using Base = WinFutureStateManager; + +public: + WinFuture() noexcept = default; + WinFuture(WinFuture &&) noexcept = default; + WinFuture &operator=(WinFuture &&) noexcept = default; + WinFuture(const WinFuture &) noexcept = delete; + WinFuture &operator=(const WinFuture &) noexcept = delete; + + WinFuture(const Base &base, std::monostate) : Base(base) {} + ~WinFuture() noexcept = default; + void get() { + WinFuture local(std::move(*this)); + local.get_value(); + } + +private: + using Base::get_value; + using Base::set_exception; + using Base::set_value; +}; + +template +class WinPromiseBase { +public: + WinPromiseBase(std::shared_ptr> new_state) + : state(std::move(new_state)) {} + WinPromiseBase(WinPromiseBase &&) = default; + WinPromiseBase &operator=(WinPromiseBase &&) = default; + WinPromiseBase(const WinPromiseBase &) = delete; + WinPromiseBase &operator=(const WinPromiseBase &) = delete; + + WinFutureStateManager &get_state_for_set() { + if (!state.valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + return state; + } + + WinFutureStateManager &get_state_for_future() { + if (!state.valid()) { + throw WinFutureError(WinFutureErrc::no_state); + } + if (future_retrieved) { + throw WinFutureError(WinFutureErrc::future_already_retrieved); + } + future_retrieved = true; + return state; + } + +private: + WinFutureStateManager state; + bool future_retrieved = false; +}; + +template +class WinPromise { +public: + WinPromise() : base(std::make_shared>()) {} + WinPromise(WinPromise &&) = default; + WinPromise(const WinPromise &) = delete; + ~WinPromise() noexcept {} + [[nodiscard]] WinFuture get_future() { + return WinFuture(base.get_state_for_future(), std::monostate()); + } + void set_value(const T &value) { base.get_state_for_set().set_value(value); } + void set_value(T &&value) { + base.get_state_for_set().set_value(std::forward(value)); + } + void set_exception(std::exception_ptr eptr) { + base.get_state_for_set().set_exception(eptr); + } + +private: + WinPromiseBase base; +}; + +template +class WinPromise { +public: + WinPromise() : base(std::make_shared>()) {} + WinPromise(WinPromise &&) = default; + WinPromise(const WinPromise &) = delete; + ~WinPromise() noexcept {} + [[nodiscard]] WinFuture get_future() { + return WinFuture(base.get_state_for_future(), std::monostate()); + } + void set_value(T &value) { + base.get_state_for_set().set_value(std::addressof(value)); + } + void set_exception(std::exception_ptr eptr) { + base.get_state_for_set().set_exception(eptr); + } + +private: + WinPromiseBase base; +}; + +template <> +class WinPromise { +public: + WinPromise() : base(std::make_shared>()) {} + WinPromise(WinPromise &&) = default; + WinPromise(const WinPromise &) = delete; + ~WinPromise() noexcept {} + [[nodiscard]] WinFuture get_future() { + return WinFuture(base.get_state_for_future(), std::monostate()); + } + void set_value() { base.get_state_for_set().set_value(0); } + void set_exception(std::exception_ptr eptr) { + base.get_state_for_set().set_exception(eptr); + } + +private: + WinPromiseBase base; +}; + +#endif diff --git a/src/time/ISO8601.cxx b/src/time/ISO8601.cxx index e2197e3ad..be20ee803 100644 --- a/src/time/ISO8601.cxx +++ b/src/time/ISO8601.cxx @@ -202,7 +202,7 @@ ParseISO8601(const char *s) } /* parse the date */ - const char *end = strptime(s, "%F", &tm); + const char *end = strptime(s, "%Y-%m-%d", &tm); if (end == nullptr) { /* try without field separators */ end = strptime(s, "%Y%m%d", &tm); diff --git a/src/win32/Com.hxx b/src/win32/Com.hxx index ded264ee2..a2aa062a1 100644 --- a/src/win32/Com.hxx +++ b/src/win32/Com.hxx @@ -29,9 +29,19 @@ class COM { public: COM() { - HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - if (FAILED(result)) { - throw FormatHResultError(result, "Unable to initialize COM"); + if (HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + FAILED(result)) { + throw FormatHResultError( + result, + "Unable to initialize COM with COINIT_MULTITHREADED"); + } + } + COM(bool) { + if (HRESULT result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + FAILED(result)) { + throw FormatHResultError( + result, + "Unable to initialize COM with COINIT_APARTMENTTHREADED"); } } ~COM() noexcept { CoUninitialize(); } diff --git a/src/win32/ComWorker.cxx b/src/win32/ComWorker.cxx new file mode 100644 index 000000000..d708dffbe --- /dev/null +++ b/src/win32/ComWorker.cxx @@ -0,0 +1,49 @@ +/* + * Copyright 2020 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#include "ComWorker.hxx" +#include "Log.hxx" +#include "thread/Name.hxx" +#include "util/Domain.hxx" +#include "win32/Com.hxx" + +namespace { +static constexpr Domain com_worker_domain("com_worker"); +} + +Mutex COMWorker::mutex; +unsigned int COMWorker::reference_count = 0; +std::optional COMWorker::thread; + +void COMWorker::COMWorkerThread::Work() noexcept { + FormatDebug(com_worker_domain, "Working thread started"); + SetThreadName("COM Worker"); + COM com{true}; + while (true) { + if (!running_flag.test_and_set()) { + FormatDebug(com_worker_domain, "Working thread ended"); + return; + } + while (!spsc_buffer.empty()) { + std::function function; + spsc_buffer.pop(function); + function(); + } + event.Wait(200); + } +} diff --git a/src/win32/ComWorker.hxx b/src/win32/ComWorker.hxx new file mode 100644 index 000000000..cd3bd5db6 --- /dev/null +++ b/src/win32/ComWorker.hxx @@ -0,0 +1,110 @@ +/* + * Copyright 2020 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_WIN32_COM_WORKER_HXX +#define MPD_WIN32_COM_WORKER_HXX + +#include +#include +#include +#include + +#include "thread/Future.hxx" +#include "thread/Mutex.hxx" +#include "thread/Thread.hxx" +#include "win32/WinEvent.hxx" +#include +#include + +// Worker thread for all COM operation +class COMWorker { +private: + class COMWorkerThread : public Thread { + public: + COMWorkerThread() : Thread{BIND_THIS_METHOD(Work)} {} + + private: + friend class COMWorker; + void Work() noexcept; + void Finish() noexcept { + running_flag.clear(); + event.Set(); + } + void Push(const std::function &function) { + spsc_buffer.push(function); + event.Set(); + } + + boost::lockfree::spsc_queue> spsc_buffer{32}; + std::atomic_flag running_flag = true; + WinEvent event{}; + }; + +public: + static void Aquire() { + std::unique_lock locker(mutex); + if (reference_count == 0) { + thread.emplace(); + thread->Start(); + } + ++reference_count; + } + static void Release() noexcept { + std::unique_lock locker(mutex); + --reference_count; + if (reference_count == 0) { + thread->Finish(); + thread->Join(); + thread.reset(); + } + } + + template + static auto Async(Function &&function, Args &&...args) { + using R = std::invoke_result_t, + std::decay_t...>; + auto promise = std::make_shared>(); + auto future = promise->get_future(); + thread->Push([function = std::forward(function), + args = std::make_tuple(std::forward(args)...), + promise = std::move(promise)]() mutable { + try { + if constexpr (std::is_void_v) { + std::apply(std::forward(function), + std::move(args)); + promise->set_value(); + } else { + promise->set_value(std::apply( + std::forward(function), + std::move(args))); + } + } catch (...) { + promise->set_exception(std::current_exception()); + } + }); + return future; + } + +private: + static Mutex mutex; + static unsigned int reference_count; + static std::optional thread; +}; + +#endif diff --git a/src/win32/Win32Main.cxx b/src/win32/Win32Main.cxx index f3b356bc0..81a2b274f 100644 --- a/src/win32/Win32Main.cxx +++ b/src/win32/Win32Main.cxx @@ -18,9 +18,6 @@ */ #include "Main.hxx" - -#ifdef _WIN32 - #include "util/Compiler.h" #include "Instance.hxx" #include "system/FatalError.hxx" @@ -155,5 +152,3 @@ void win32_app_stopping() else running.store(false); } - -#endif