Merge branch 'v0.22.x'
This commit is contained in:
commit
25354b9d8c
|
@ -172,7 +172,15 @@ FileDescriptor::CreatePipe(FileDescriptor &r, FileDescriptor &w) noexcept
|
|||
#endif
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
#ifdef _WIN32
|
||||
|
||||
void
|
||||
FileDescriptor::SetBinaryMode() noexcept
|
||||
{
|
||||
_setmode(fd, _O_BINARY);
|
||||
}
|
||||
|
||||
#else // !_WIN32
|
||||
|
||||
bool
|
||||
FileDescriptor::CreatePipeNonBlock(FileDescriptor &r,
|
||||
|
|
|
@ -147,10 +147,13 @@ public:
|
|||
#ifdef _WIN32
|
||||
void EnableCloseOnExec() noexcept {}
|
||||
void DisableCloseOnExec() noexcept {}
|
||||
void SetBinaryMode() noexcept;
|
||||
#else
|
||||
static bool CreatePipeNonBlock(FileDescriptor &r,
|
||||
FileDescriptor &w) noexcept;
|
||||
|
||||
void SetBinaryMode() noexcept {}
|
||||
|
||||
/**
|
||||
* Enable non-blocking mode on this file descriptor.
|
||||
*/
|
||||
|
|
|
@ -44,7 +44,11 @@ public:
|
|||
void Close() noexcept override {}
|
||||
|
||||
int GetVolume() override {
|
||||
auto future = COMWorker::Async([&]() -> int {
|
||||
auto com_worker = wasapi_output_get_com_worker(output);
|
||||
if (!com_worker)
|
||||
return -1;
|
||||
|
||||
auto future = com_worker->Async([&]() -> int {
|
||||
HRESULT result;
|
||||
float volume_level;
|
||||
|
||||
|
@ -55,9 +59,9 @@ public:
|
|||
result = endpoint_volume->GetMasterVolumeLevelScalar(
|
||||
&volume_level);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"Unable to get master "
|
||||
"volume level");
|
||||
throw MakeHResultError(result,
|
||||
"Unable to get master "
|
||||
"volume level");
|
||||
}
|
||||
} else {
|
||||
auto session_volume =
|
||||
|
@ -65,7 +69,7 @@ public:
|
|||
|
||||
result = session_volume->GetMasterVolume(&volume_level);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
throw MakeHResultError(
|
||||
result, "Unable to get master volume");
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +80,11 @@ public:
|
|||
}
|
||||
|
||||
void SetVolume(unsigned volume) override {
|
||||
COMWorker::Async([&]() {
|
||||
auto com_worker = wasapi_output_get_com_worker(output);
|
||||
if (!com_worker)
|
||||
throw std::runtime_error("Cannot set WASAPI volume");
|
||||
|
||||
com_worker->Async([&]() {
|
||||
HRESULT result;
|
||||
const float volume_level = volume / 100.0f;
|
||||
|
||||
|
@ -87,7 +95,7 @@ public:
|
|||
result = endpoint_volume->SetMasterVolumeLevelScalar(
|
||||
volume_level, nullptr);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
throw MakeHResultError(
|
||||
result,
|
||||
"Unable to set master volume level");
|
||||
}
|
||||
|
@ -98,7 +106,7 @@ public:
|
|||
result = session_volume->SetMasterVolume(volume_level,
|
||||
nullptr);
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
throw MakeHResultError(
|
||||
result, "Unable to set master volume");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,8 @@ GetBufferSizeInFrames(IAudioClient &client)
|
|||
|
||||
HRESULT result = client.GetBufferSize(&buffer_size_in_frames);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result,
|
||||
"Unable to get audio client buffer size");
|
||||
throw MakeHResultError(result,
|
||||
"Unable to get audio client buffer size");
|
||||
|
||||
return buffer_size_in_frames;
|
||||
}
|
||||
|
@ -46,8 +46,8 @@ GetCurrentPaddingFrames(IAudioClient &client)
|
|||
|
||||
HRESULT result = client.GetCurrentPadding(&padding_frames);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result,
|
||||
"Failed to get current padding");
|
||||
throw MakeHResultError(result,
|
||||
"Failed to get current padding");
|
||||
|
||||
return padding_frames;
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ GetMixFormat(IAudioClient &client)
|
|||
|
||||
HRESULT result = client.GetMixFormat(&f);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "GetMixFormat failed");
|
||||
throw MakeHResultError(result, "GetMixFormat failed");
|
||||
|
||||
return ComHeapPtr{f};
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ Start(IAudioClient &client)
|
|||
{
|
||||
HRESULT result = client.Start();
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "Failed to start client");
|
||||
throw MakeHResultError(result, "Failed to start client");
|
||||
}
|
||||
|
||||
inline void
|
||||
|
@ -77,7 +77,7 @@ Stop(IAudioClient &client)
|
|||
{
|
||||
HRESULT result = client.Stop();
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "Failed to stop client");
|
||||
throw MakeHResultError(result, "Failed to stop client");
|
||||
}
|
||||
|
||||
inline void
|
||||
|
@ -85,7 +85,7 @@ SetEventHandle(IAudioClient &client, HANDLE h)
|
|||
{
|
||||
HRESULT result = client.SetEventHandle(h);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "Unable to set event handle");
|
||||
throw MakeHResultError(result, "Unable to set event handle");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
|
@ -95,7 +95,7 @@ GetService(IAudioClient &client)
|
|||
T *p = nullptr;
|
||||
HRESULT result = client.GetService(IID_PPV_ARGS(&p));
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "Unable to get service");
|
||||
throw MakeHResultError(result, "Unable to get service");
|
||||
|
||||
return ComPtr{p};
|
||||
}
|
||||
|
|
|
@ -33,8 +33,8 @@ GetDefaultAudioEndpoint(IMMDeviceEnumerator &e)
|
|||
HRESULT result = e.GetDefaultAudioEndpoint(eRender, eMultimedia,
|
||||
&device);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result,
|
||||
"Unable to get default device for multimedia");
|
||||
throw MakeHResultError(result,
|
||||
"Unable to get default device for multimedia");
|
||||
|
||||
return ComPtr{device};
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ EnumAudioEndpoints(IMMDeviceEnumerator &e)
|
|||
HRESULT result = e.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE,
|
||||
&dc);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "Unable to enumerate devices");
|
||||
throw MakeHResultError(result, "Unable to enumerate devices");
|
||||
|
||||
return ComPtr{dc};
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ GetCount(IMMDeviceCollection &dc)
|
|||
|
||||
HRESULT result = dc.GetCount(&count);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "Collection->GetCount failed");
|
||||
throw MakeHResultError(result, "Collection->GetCount failed");
|
||||
|
||||
return count;
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ Item(IMMDeviceCollection &dc, UINT i)
|
|||
|
||||
auto result = dc.Item(i, &device);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "Collection->Item failed");
|
||||
throw MakeHResultError(result, "Collection->Item failed");
|
||||
|
||||
return ComPtr{device};
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ GetState(IMMDevice &device)
|
|||
|
||||
HRESULT result = device.GetState(&state);;
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "Unable to get device status");
|
||||
throw MakeHResultError(result, "Unable to get device status");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ Activate(IMMDevice &device)
|
|||
HRESULT result = device.Activate(__uuidof(T), CLSCTX_ALL,
|
||||
nullptr, (void **)&p);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result, "Unable to activate device");
|
||||
throw MakeHResultError(result, "Unable to activate device");
|
||||
|
||||
return ComPtr{p};
|
||||
}
|
||||
|
@ -108,8 +108,8 @@ OpenPropertyStore(IMMDevice &device)
|
|||
|
||||
HRESULT result = device.OpenPropertyStore(STGM_READ, &property_store);
|
||||
if (FAILED(result))
|
||||
throw FormatHResultError(result,
|
||||
"Device->OpenPropertyStore failed");
|
||||
throw MakeHResultError(result,
|
||||
"Device->OpenPropertyStore failed");
|
||||
|
||||
return ComPtr{property_store};
|
||||
}
|
||||
|
|
|
@ -20,10 +20,13 @@
|
|||
#ifndef MPD_WASAPI_OUTPUT_FOR_MIXER_HXX
|
||||
#define MPD_WASAPI_OUTPUT_FOR_MIXER_HXX
|
||||
|
||||
#include <memory>
|
||||
|
||||
struct IMMDevice;
|
||||
struct IAudioClient;
|
||||
class AudioOutput;
|
||||
class WasapiOutput;
|
||||
class COMWorker;
|
||||
|
||||
[[gnu::pure]]
|
||||
WasapiOutput &
|
||||
|
@ -33,6 +36,10 @@ wasapi_output_downcast(AudioOutput &output) noexcept;
|
|||
bool
|
||||
wasapi_is_exclusive(WasapiOutput &output) noexcept;
|
||||
|
||||
[[gnu::pure]]
|
||||
std::shared_ptr<COMWorker>
|
||||
wasapi_output_get_com_worker(WasapiOutput &output) noexcept;
|
||||
|
||||
[[gnu::pure]]
|
||||
IMMDevice *
|
||||
wasapi_output_get_device(WasapiOutput &output) noexcept;
|
||||
|
|
|
@ -61,7 +61,9 @@
|
|||
namespace {
|
||||
static constexpr Domain wasapi_output_domain("wasapi_output");
|
||||
|
||||
gcc_const constexpr uint32_t GetChannelMask(const uint8_t channels) noexcept {
|
||||
constexpr uint32_t
|
||||
GetChannelMask(const uint8_t channels) noexcept
|
||||
{
|
||||
switch (channels) {
|
||||
case 1:
|
||||
return KSAUDIO_SPEAKER_MONO;
|
||||
|
@ -86,18 +88,9 @@ gcc_const constexpr uint32_t GetChannelMask(const uint8_t channels) noexcept {
|
|||
}
|
||||
|
||||
template <typename Functor>
|
||||
inline bool SafeTry(Functor &&functor) {
|
||||
try {
|
||||
functor();
|
||||
return true;
|
||||
} catch (...) {
|
||||
FormatError(std::current_exception(), "%s");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Functor>
|
||||
inline bool SafeSilenceTry(Functor &&functor) {
|
||||
inline bool
|
||||
SafeSilenceTry(Functor &&functor) noexcept
|
||||
{
|
||||
try {
|
||||
functor();
|
||||
return true;
|
||||
|
@ -106,7 +99,9 @@ inline bool SafeSilenceTry(Functor &&functor) {
|
|||
}
|
||||
}
|
||||
|
||||
std::vector<WAVEFORMATEXTENSIBLE> GetFormats(const AudioFormat &audio_format) noexcept {
|
||||
std::vector<WAVEFORMATEXTENSIBLE>
|
||||
GetFormats(const AudioFormat &audio_format) noexcept
|
||||
{
|
||||
#ifdef ENABLE_DSD
|
||||
if (audio_format.format == SampleFormat::DSD) {
|
||||
AudioFormat dop_format = audio_format;
|
||||
|
@ -152,57 +147,154 @@ std::vector<WAVEFORMATEXTENSIBLE> GetFormats(const AudioFormat &audio_format) no
|
|||
}
|
||||
|
||||
#ifdef ENABLE_DSD
|
||||
void SetDSDFallback(AudioFormat &audio_format) noexcept {
|
||||
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
|
||||
|
||||
class WasapiOutputThread : public Thread {
|
||||
public:
|
||||
enum class Status : uint32_t { FINISH, PLAY, PAUSE };
|
||||
WasapiOutputThread(IAudioClient *_client,
|
||||
ComPtr<IAudioRenderClient> &&_render_client,
|
||||
const UINT32 _frame_size, const UINT32 _buffer_size_in_frames,
|
||||
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 WaitDataPoped() noexcept { data_poped.Wait(); }
|
||||
void CheckException() {
|
||||
if (error.occur.load()) {
|
||||
auto err = std::exchange(error.ptr, nullptr);
|
||||
error.thrown.Set();
|
||||
std::rethrow_exception(err);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
friend class WasapiOutput;
|
||||
class WasapiOutputThread {
|
||||
Thread thread{BIND_THIS_METHOD(Work)};
|
||||
WinEvent event;
|
||||
WinEvent data_poped;
|
||||
IAudioClient *client;
|
||||
IAudioClient &client;
|
||||
ComPtr<IAudioRenderClient> render_client;
|
||||
const UINT32 frame_size;
|
||||
const UINT32 buffer_size_in_frames;
|
||||
bool is_exclusive;
|
||||
const bool is_exclusive;
|
||||
|
||||
/**
|
||||
* This flag is only used by the calling thread
|
||||
* (i.e. #OutputThread), and specifies whether the
|
||||
* WasapiOutputThread has been told to play via Play(). This
|
||||
* variable is somewhat redundant because we already have
|
||||
* "state", but using this variable saves some overhead for
|
||||
* atomic operations.
|
||||
*/
|
||||
bool playing = false;
|
||||
|
||||
bool started = false;
|
||||
|
||||
std::atomic_bool cancel = false;
|
||||
|
||||
std::atomic_bool empty = true;
|
||||
|
||||
enum class Status : uint32_t { FINISH, PLAY, PAUSE };
|
||||
|
||||
alignas(BOOST_LOCKFREE_CACHELINE_BYTES) std::atomic<Status> status =
|
||||
Status::PAUSE;
|
||||
alignas(BOOST_LOCKFREE_CACHELINE_BYTES) struct {
|
||||
std::atomic_bool occur = false;
|
||||
std::exception_ptr ptr = nullptr;
|
||||
WinEvent thrown;
|
||||
} error;
|
||||
boost::lockfree::spsc_queue<BYTE> spsc_buffer;
|
||||
|
||||
public:
|
||||
WasapiOutputThread(IAudioClient &_client,
|
||||
ComPtr<IAudioRenderClient> &&_render_client,
|
||||
const UINT32 _frame_size, const UINT32 _buffer_size_in_frames,
|
||||
bool _is_exclusive)
|
||||
: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)
|
||||
{
|
||||
SetEventHandle(client, event.handle());
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
void Finish() noexcept {
|
||||
SetStatus(Status::FINISH);
|
||||
thread.Join();
|
||||
}
|
||||
|
||||
void Play() noexcept {
|
||||
playing = true;
|
||||
SetStatus(Status::PLAY);
|
||||
}
|
||||
|
||||
void Pause() noexcept {
|
||||
if (!playing)
|
||||
return;
|
||||
|
||||
playing = false;
|
||||
SetStatus(Status::PAUSE);
|
||||
}
|
||||
|
||||
std::size_t Push(ConstBuffer<void> input) noexcept {
|
||||
empty.store(false);
|
||||
|
||||
std::size_t consumed =
|
||||
spsc_buffer.push(static_cast<const BYTE *>(input.data),
|
||||
input.size);
|
||||
|
||||
if (!playing) {
|
||||
playing = true;
|
||||
Play();
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the buffer is empty, and if not, wait a bit.
|
||||
*
|
||||
* Throws on error.
|
||||
*
|
||||
* @return true if the buffer is now empty
|
||||
*/
|
||||
bool Drain() {
|
||||
if (empty)
|
||||
return true;
|
||||
|
||||
CheckException();
|
||||
Wait();
|
||||
CheckException();
|
||||
|
||||
return empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the thread to discard the buffer (and wait for
|
||||
* completion). This needs to be done inside this thread,
|
||||
* because only the consumer thread is allowed to do that.
|
||||
*/
|
||||
void Cancel() noexcept {
|
||||
cancel.store(true);
|
||||
event.Set();
|
||||
|
||||
while (cancel.load() && !error.occur.load())
|
||||
Wait();
|
||||
|
||||
/* not rethrowing the exception here via
|
||||
CheckException() because this method must be
|
||||
"noexcept"; the next WasapiOutput::Play() call will
|
||||
throw */
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the thread to finish some work (e.g. until some
|
||||
* buffer space becomes available).
|
||||
*/
|
||||
void Wait() noexcept {
|
||||
data_poped.Wait();
|
||||
}
|
||||
|
||||
void InterruptWaiter() noexcept {
|
||||
data_poped.Set();
|
||||
}
|
||||
|
||||
void CheckException() {
|
||||
if (error.occur.load()) {
|
||||
std::rethrow_exception(error.ptr);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void SetStatus(Status s) noexcept {
|
||||
status.store(s);
|
||||
event.Set();
|
||||
|
@ -211,30 +303,61 @@ private:
|
|||
};
|
||||
|
||||
class WasapiOutput final : public AudioOutput {
|
||||
const bool is_exclusive;
|
||||
const bool enumerate_devices;
|
||||
#ifdef ENABLE_DSD
|
||||
const bool dop_setting;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Only valid if the output is open.
|
||||
*/
|
||||
bool paused;
|
||||
|
||||
std::atomic_flag not_interrupted = true;
|
||||
|
||||
const std::string device_config;
|
||||
|
||||
std::shared_ptr<COMWorker> com_worker;
|
||||
ComPtr<IMMDevice> device;
|
||||
ComPtr<IAudioClient> client;
|
||||
WAVEFORMATEXTENSIBLE device_format;
|
||||
std::optional<WasapiOutputThread> thread;
|
||||
std::size_t watermark;
|
||||
std::optional<PcmExport> pcm_export;
|
||||
|
||||
public:
|
||||
static AudioOutput *Create(EventLoop &, const ConfigBlock &block);
|
||||
WasapiOutput(const ConfigBlock &block);
|
||||
|
||||
auto GetComWorker() noexcept {
|
||||
// TODO: protect access to the shard_ptr
|
||||
return com_worker;
|
||||
}
|
||||
|
||||
void Enable() override {
|
||||
COMWorker::Aquire();
|
||||
com_worker = std::make_shared<COMWorker>();
|
||||
|
||||
try {
|
||||
COMWorker::Async([&]() { OpenDevice(); }).get();
|
||||
com_worker->Async([&]() { ChooseDevice(); }).get();
|
||||
} catch (...) {
|
||||
COMWorker::Release();
|
||||
com_worker.reset();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
void Disable() noexcept override {
|
||||
COMWorker::Async([&]() { DoDisable(); }).get();
|
||||
COMWorker::Release();
|
||||
com_worker->Async([&]() { DoDisable(); }).get();
|
||||
com_worker.reset();
|
||||
}
|
||||
void Open(AudioFormat &audio_format) override {
|
||||
COMWorker::Async([&]() { DoOpen(audio_format); }).get();
|
||||
com_worker->Async([&]() { DoOpen(audio_format); }).get();
|
||||
paused = false;
|
||||
}
|
||||
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;
|
||||
void Cancel() noexcept override;
|
||||
bool Pause() override;
|
||||
void Interrupt() noexcept override;
|
||||
|
||||
|
@ -245,22 +368,6 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
std::atomic_flag not_interrupted = true;
|
||||
bool is_started = false;
|
||||
bool is_exclusive;
|
||||
bool enumerate_devices;
|
||||
#ifdef ENABLE_DSD
|
||||
bool dop_setting;
|
||||
#endif
|
||||
std::string device_config;
|
||||
ComPtr<IMMDeviceEnumerator> enumerator;
|
||||
ComPtr<IMMDevice> device;
|
||||
ComPtr<IAudioClient> client;
|
||||
WAVEFORMATEXTENSIBLE device_format;
|
||||
std::optional<WasapiOutputThread> thread;
|
||||
std::size_t watermark;
|
||||
std::optional<PcmExport> 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;
|
||||
|
@ -268,126 +375,179 @@ private:
|
|||
void DoDisable() noexcept;
|
||||
void DoOpen(AudioFormat &audio_format);
|
||||
|
||||
void OpenDevice();
|
||||
void ChooseDevice();
|
||||
bool TryFormatExclusive(const AudioFormat &audio_format);
|
||||
void FindExclusiveFormatSupported(AudioFormat &audio_format);
|
||||
void FindSharedFormatSupported(AudioFormat &audio_format);
|
||||
void EnumerateDevices();
|
||||
ComPtr<IMMDevice> GetDevice(unsigned int index);
|
||||
ComPtr<IMMDevice> SearchDevice(std::string_view name);
|
||||
static void EnumerateDevices(IMMDeviceEnumerator &enumerator);
|
||||
static ComPtr<IMMDevice> GetDevice(IMMDeviceEnumerator &enumerator,
|
||||
unsigned index);
|
||||
static ComPtr<IMMDevice> SearchDevice(IMMDeviceEnumerator &enumerator,
|
||||
std::string_view name);
|
||||
};
|
||||
|
||||
WasapiOutput &wasapi_output_downcast(AudioOutput &output) noexcept {
|
||||
WasapiOutput &
|
||||
wasapi_output_downcast(AudioOutput &output) noexcept
|
||||
{
|
||||
return static_cast<WasapiOutput &>(output);
|
||||
}
|
||||
|
||||
bool wasapi_is_exclusive(WasapiOutput &output) noexcept { return output.is_exclusive; }
|
||||
bool
|
||||
wasapi_is_exclusive(WasapiOutput &output) noexcept
|
||||
{
|
||||
return output.is_exclusive;
|
||||
}
|
||||
|
||||
IMMDevice *wasapi_output_get_device(WasapiOutput &output) noexcept {
|
||||
std::shared_ptr<COMWorker>
|
||||
wasapi_output_get_com_worker(WasapiOutput &output) noexcept
|
||||
{
|
||||
return output.GetComWorker();
|
||||
}
|
||||
|
||||
IMMDevice *
|
||||
wasapi_output_get_device(WasapiOutput &output) noexcept
|
||||
{
|
||||
return output.device.get();
|
||||
}
|
||||
|
||||
IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept {
|
||||
IAudioClient *
|
||||
wasapi_output_get_client(WasapiOutput &output) noexcept
|
||||
{
|
||||
return output.client.get();
|
||||
}
|
||||
|
||||
void WasapiOutputThread::Work() noexcept {
|
||||
inline void
|
||||
WasapiOutputThread::Work() noexcept
|
||||
try {
|
||||
SetThreadName("Wasapi Output Worker");
|
||||
FormatDebug(wasapi_output_domain, "Working thread started");
|
||||
COM com{true};
|
||||
while (true) {
|
||||
try {
|
||||
event.Wait();
|
||||
COM com;
|
||||
|
||||
Status current_state = status.load();
|
||||
if (current_state == Status::FINISH) {
|
||||
FormatDebug(wasapi_output_domain,
|
||||
"Working thread stopped");
|
||||
return;
|
||||
AtScopeExit(this) {
|
||||
if (started) {
|
||||
try {
|
||||
Stop(client);
|
||||
} catch (...) {
|
||||
LogError(std::current_exception());
|
||||
}
|
||||
|
||||
UINT32 write_in_frames = buffer_size_in_frames;
|
||||
if (!is_exclusive) {
|
||||
UINT32 data_in_frames =
|
||||
GetCurrentPaddingFrames(*client);
|
||||
|
||||
if (data_in_frames >= buffer_size_in_frames) {
|
||||
continue;
|
||||
}
|
||||
write_in_frames -= data_in_frames;
|
||||
}
|
||||
|
||||
BYTE *data;
|
||||
DWORD mode = 0;
|
||||
|
||||
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, mode);
|
||||
};
|
||||
|
||||
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");
|
||||
}
|
||||
} catch (...) {
|
||||
error.ptr = std::current_exception();
|
||||
error.occur.store(true);
|
||||
error.thrown.Wait();
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
event.Wait();
|
||||
|
||||
if (cancel.load()) {
|
||||
spsc_buffer.consume_all([](auto &&) {});
|
||||
cancel.store(false);
|
||||
empty.store(true);
|
||||
InterruptWaiter();
|
||||
}
|
||||
|
||||
Status current_state = status.load();
|
||||
switch (current_state) {
|
||||
case Status::FINISH:
|
||||
FormatDebug(wasapi_output_domain,
|
||||
"Working thread stopped");
|
||||
return;
|
||||
|
||||
case Status::PAUSE:
|
||||
if (!started)
|
||||
/* don't bother starting the
|
||||
IAudioClient if we're paused */
|
||||
continue;
|
||||
|
||||
/* stop the IAudioClient while paused; it will
|
||||
be restarted as soon as we're asked to
|
||||
resume playback */
|
||||
Stop(client);
|
||||
started = false;
|
||||
continue;
|
||||
|
||||
case Status::PLAY:
|
||||
break;
|
||||
}
|
||||
|
||||
UINT32 write_in_frames = buffer_size_in_frames;
|
||||
if (!is_exclusive) {
|
||||
UINT32 data_in_frames =
|
||||
GetCurrentPaddingFrames(client);
|
||||
if (data_in_frames >= buffer_size_in_frames) {
|
||||
continue;
|
||||
}
|
||||
write_in_frames -= data_in_frames;
|
||||
}
|
||||
|
||||
BYTE *data;
|
||||
DWORD mode = 0;
|
||||
|
||||
if (HRESULT result =
|
||||
render_client->GetBuffer(write_in_frames, &data);
|
||||
FAILED(result)) {
|
||||
throw MakeHResultError(result, "Failed to get buffer");
|
||||
}
|
||||
|
||||
AtScopeExit(&) {
|
||||
render_client->ReleaseBuffer(write_in_frames, mode);
|
||||
|
||||
if (!started) {
|
||||
Start(client);
|
||||
started = true;
|
||||
}
|
||||
};
|
||||
|
||||
const UINT32 write_size = write_in_frames * frame_size;
|
||||
UINT32 new_data_size = 0;
|
||||
new_data_size = spsc_buffer.pop(data, write_size);
|
||||
if (new_data_size == 0)
|
||||
empty.store(true);
|
||||
|
||||
std::fill_n(data + new_data_size,
|
||||
write_size - new_data_size, 0);
|
||||
InterruptWaiter();
|
||||
}
|
||||
} catch (...) {
|
||||
error.ptr = std::current_exception();
|
||||
error.occur.store(true);
|
||||
|
||||
/* wake up the client thread which may be inside Wait() */
|
||||
InterruptWaiter();
|
||||
}
|
||||
|
||||
AudioOutput *WasapiOutput::Create(EventLoop &, const ConfigBlock &block) {
|
||||
AudioOutput *
|
||||
WasapiOutput::Create(EventLoop &, const ConfigBlock &block)
|
||||
{
|
||||
return new WasapiOutput(block);
|
||||
}
|
||||
|
||||
WasapiOutput::WasapiOutput(const ConfigBlock &block)
|
||||
: AudioOutput(FLAG_ENABLE_DISABLE | FLAG_PAUSE),
|
||||
is_exclusive(block.GetBlockValue("exclusive", false)),
|
||||
enumerate_devices(block.GetBlockValue("enumerate", false)),
|
||||
:AudioOutput(FLAG_ENABLE_DISABLE | FLAG_PAUSE),
|
||||
is_exclusive(block.GetBlockValue("exclusive", false)),
|
||||
enumerate_devices(block.GetBlockValue("enumerate", false)),
|
||||
#ifdef ENABLE_DSD
|
||||
dop_setting(block.GetBlockValue("dop", false)),
|
||||
dop_setting(block.GetBlockValue("dop", false)),
|
||||
#endif
|
||||
device_config(block.GetBlockValue("device", "")) {
|
||||
device_config(block.GetBlockValue("device", ""))
|
||||
{
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
void
|
||||
WasapiOutput::DoDisable() noexcept
|
||||
{
|
||||
assert(!thread);
|
||||
|
||||
device.reset();
|
||||
enumerator.reset();
|
||||
}
|
||||
|
||||
/// run inside COMWorkerThread
|
||||
void WasapiOutput::DoOpen(AudioFormat &audio_format) {
|
||||
void
|
||||
WasapiOutput::DoOpen(AudioFormat &audio_format)
|
||||
{
|
||||
client.reset();
|
||||
|
||||
if (GetState(*device) != DEVICE_STATE_ACTIVE) {
|
||||
device.reset();
|
||||
OpenDevice();
|
||||
ChooseDevice();
|
||||
}
|
||||
|
||||
client = Activate<IAudioClient>(*device);
|
||||
|
@ -444,7 +604,7 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) {
|
|||
if (HRESULT result =
|
||||
client->GetDevicePeriod(&default_device_period, &min_device_period);
|
||||
FAILED(result)) {
|
||||
throw FormatHResultError(result, "Unable to get device period");
|
||||
throw MakeHResultError(result, "Unable to get device period");
|
||||
}
|
||||
FormatDebug(wasapi_output_domain,
|
||||
"Default device period: %I64u ns, Minimum device period: "
|
||||
|
@ -492,8 +652,7 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) {
|
|||
}
|
||||
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(
|
||||
result, "Unable to initialize audio client");
|
||||
throw MakeHResultError(result, "Unable to initialize audio client");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -502,8 +661,8 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) {
|
|||
buffer_duration, 0,
|
||||
reinterpret_cast<WAVEFORMATEX *>(&device_format), nullptr);
|
||||
FAILED(result)) {
|
||||
throw FormatHResultError(result,
|
||||
"Unable to initialize audio client");
|
||||
throw MakeHResultError(result,
|
||||
"Unable to initialize audio client");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -512,56 +671,49 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) {
|
|||
const UINT32 buffer_size_in_frames = GetBufferSizeInFrames(*client);
|
||||
|
||||
watermark = buffer_size_in_frames * 3 * FrameSize();
|
||||
thread.emplace(client.get(), std::move(render_client), FrameSize(),
|
||||
thread.emplace(*client, std::move(render_client), FrameSize(),
|
||||
buffer_size_in_frames, is_exclusive);
|
||||
|
||||
SetEventHandle(*client, thread->event.handle());
|
||||
|
||||
thread->Start();
|
||||
paused = false;
|
||||
}
|
||||
|
||||
void WasapiOutput::Close() noexcept {
|
||||
void
|
||||
WasapiOutput::Close() noexcept
|
||||
{
|
||||
assert(thread);
|
||||
|
||||
try {
|
||||
COMWorker::Async([&]() {
|
||||
Stop(*client);
|
||||
}).get();
|
||||
thread->CheckException();
|
||||
} catch (std::exception &err) {
|
||||
FormatError(wasapi_output_domain, "exception while stoping: %s",
|
||||
err.what());
|
||||
} catch (...) {
|
||||
FormatError(std::current_exception(),
|
||||
"exception while stopping");
|
||||
}
|
||||
is_started = false;
|
||||
thread->Finish();
|
||||
thread->Join();
|
||||
COMWorker::Async([&]() {
|
||||
com_worker->Async([&]() {
|
||||
thread.reset();
|
||||
client.reset();
|
||||
}).get();
|
||||
pcm_export.reset();
|
||||
}
|
||||
|
||||
std::chrono::steady_clock::duration WasapiOutput::Delay() const noexcept {
|
||||
if (!is_started) {
|
||||
std::chrono::steady_clock::duration
|
||||
WasapiOutput::Delay() const noexcept
|
||||
{
|
||||
if (paused) {
|
||||
// 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;
|
||||
return std::chrono::steady_clock::duration::zero();
|
||||
}
|
||||
|
||||
size_t WasapiOutput::Play(const void *chunk, size_t size) {
|
||||
size_t
|
||||
WasapiOutput::Play(const void *chunk, size_t size)
|
||||
{
|
||||
assert(thread);
|
||||
|
||||
paused = false;
|
||||
|
||||
not_interrupted.test_and_set();
|
||||
|
||||
ConstBuffer<void> input(chunk, size);
|
||||
|
@ -572,25 +724,17 @@ size_t WasapiOutput::Play(const void *chunk, size_t size) {
|
|||
return size;
|
||||
|
||||
do {
|
||||
const size_t consumed_size = thread->spsc_buffer.push(
|
||||
static_cast<const BYTE *>(input.data), input.size);
|
||||
const size_t consumed_size = thread->Push({chunk, size});
|
||||
|
||||
if (consumed_size == 0) {
|
||||
assert(is_started);
|
||||
thread->WaitDataPoped();
|
||||
thread->Wait();
|
||||
thread->CheckException();
|
||||
if (!not_interrupted.test_and_set()) {
|
||||
throw AudioOutputInterrupted{};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_started) {
|
||||
is_started = true;
|
||||
thread->Play();
|
||||
COMWorker::Async([&]() {
|
||||
Start(*client);
|
||||
}).wait();
|
||||
}
|
||||
|
||||
thread->CheckException();
|
||||
|
||||
if (pcm_export) {
|
||||
|
@ -600,58 +744,82 @@ size_t WasapiOutput::Play(const void *chunk, size_t size) {
|
|||
} while (true);
|
||||
}
|
||||
|
||||
bool WasapiOutput::Pause() {
|
||||
if (is_started) {
|
||||
thread->Pause();
|
||||
is_started = false;
|
||||
}
|
||||
bool
|
||||
WasapiOutput::Pause()
|
||||
{
|
||||
paused = true;
|
||||
thread->Pause();
|
||||
thread->CheckException();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WasapiOutput::Interrupt() noexcept {
|
||||
void
|
||||
WasapiOutput::Interrupt() noexcept
|
||||
{
|
||||
if (thread) {
|
||||
not_interrupted.clear();
|
||||
thread->data_poped.Set();
|
||||
thread->InterruptWaiter();
|
||||
}
|
||||
}
|
||||
|
||||
void WasapiOutput::Drain() {
|
||||
void
|
||||
WasapiOutput::Drain()
|
||||
{
|
||||
assert(thread);
|
||||
|
||||
thread->spsc_buffer.consume_all([](auto &&) {});
|
||||
thread->CheckException();
|
||||
not_interrupted.test_and_set();
|
||||
|
||||
while (!thread->Drain()) {
|
||||
if (!not_interrupted.test_and_set())
|
||||
throw AudioOutputInterrupted{};
|
||||
}
|
||||
|
||||
/* TODO: this needs to wait until the hardware has really
|
||||
finished playing */
|
||||
}
|
||||
|
||||
void
|
||||
WasapiOutput::Cancel() noexcept
|
||||
{
|
||||
assert(thread);
|
||||
|
||||
thread->Cancel();
|
||||
}
|
||||
|
||||
/// run inside COMWorkerThread
|
||||
void WasapiOutput::OpenDevice() {
|
||||
void
|
||||
WasapiOutput::ChooseDevice()
|
||||
{
|
||||
ComPtr<IMMDeviceEnumerator> enumerator;
|
||||
enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
|
||||
CLSCTX_INPROC_SERVER);
|
||||
|
||||
if (enumerate_devices) {
|
||||
try {
|
||||
EnumerateDevices();
|
||||
EnumerateDevices(*enumerator);
|
||||
} catch (...) {
|
||||
LogError(std::current_exception());
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int id = kErrorId;
|
||||
if (!device_config.empty()) {
|
||||
unsigned int id;
|
||||
if (!SafeSilenceTry([this, &id]() { id = std::stoul(device_config); })) {
|
||||
device = SearchDevice(device_config);
|
||||
device = SearchDevice(*enumerator, device_config);
|
||||
if (!device)
|
||||
throw FormatRuntimeError("Device '%s' not found",
|
||||
device_config.c_str());
|
||||
} else
|
||||
device = GetDevice(id);
|
||||
device = GetDevice(*enumerator, id);
|
||||
} else {
|
||||
device = GetDefaultAudioEndpoint(*enumerator);
|
||||
}
|
||||
}
|
||||
|
||||
/// run inside COMWorkerThread
|
||||
bool WasapiOutput::TryFormatExclusive(const AudioFormat &audio_format) {
|
||||
bool
|
||||
WasapiOutput::TryFormatExclusive(const AudioFormat &audio_format)
|
||||
{
|
||||
for (auto test_format : GetFormats(audio_format)) {
|
||||
HRESULT result = client->IsFormatSupported(
|
||||
AUDCLNT_SHAREMODE_EXCLUSIVE,
|
||||
|
@ -675,7 +843,9 @@ bool WasapiOutput::TryFormatExclusive(const AudioFormat &audio_format) {
|
|||
}
|
||||
|
||||
/// run inside COMWorkerThread
|
||||
void WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format) {
|
||||
void
|
||||
WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format)
|
||||
{
|
||||
for (uint8_t channels : {0, 2, 6, 8, 7, 1, 4, 5, 3}) {
|
||||
if (audio_format.channels == channels) {
|
||||
continue;
|
||||
|
@ -735,7 +905,9 @@ void WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format) {
|
|||
}
|
||||
|
||||
/// run inside COMWorkerThread
|
||||
void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) {
|
||||
void
|
||||
WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format)
|
||||
{
|
||||
HRESULT result;
|
||||
|
||||
// In shared mode, different sample rate is always unsupported.
|
||||
|
@ -760,7 +932,7 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) {
|
|||
}
|
||||
|
||||
if (FAILED(result) && result != AUDCLNT_E_UNSUPPORTED_FORMAT) {
|
||||
throw FormatHResultError(result, "IsFormatSupported failed");
|
||||
throw MakeHResultError(result, "IsFormatSupported failed");
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
|
@ -789,7 +961,7 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) {
|
|||
result_string.c_str());
|
||||
}
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Format is not supported");
|
||||
throw MakeHResultError(result, "Format is not supported");
|
||||
}
|
||||
break;
|
||||
case S_FALSE:
|
||||
|
@ -838,8 +1010,10 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) {
|
|||
}
|
||||
|
||||
/// run inside COMWorkerThread
|
||||
void WasapiOutput::EnumerateDevices() {
|
||||
const auto device_collection = EnumAudioEndpoints(*enumerator);
|
||||
void
|
||||
WasapiOutput::EnumerateDevices(IMMDeviceEnumerator &enumerator)
|
||||
{
|
||||
const auto device_collection = EnumAudioEndpoints(enumerator);
|
||||
|
||||
const UINT count = GetCount(*device_collection);
|
||||
for (UINT i = 0; i < count; ++i) {
|
||||
|
@ -860,17 +1034,18 @@ void WasapiOutput::EnumerateDevices() {
|
|||
|
||||
/// run inside COMWorkerThread
|
||||
ComPtr<IMMDevice>
|
||||
WasapiOutput::GetDevice(unsigned int index)
|
||||
WasapiOutput::GetDevice(IMMDeviceEnumerator &enumerator, unsigned index)
|
||||
{
|
||||
const auto device_collection = EnumAudioEndpoints(*enumerator);
|
||||
const auto device_collection = EnumAudioEndpoints(enumerator);
|
||||
return Item(*device_collection, index);
|
||||
}
|
||||
|
||||
/// run inside COMWorkerThread
|
||||
ComPtr<IMMDevice>
|
||||
WasapiOutput::SearchDevice(std::string_view name)
|
||||
WasapiOutput::SearchDevice(IMMDeviceEnumerator &enumerator,
|
||||
std::string_view name)
|
||||
{
|
||||
const auto device_collection = EnumAudioEndpoints(*enumerator);
|
||||
const auto device_collection = EnumAudioEndpoints(enumerator);
|
||||
|
||||
const UINT count = GetCount(*device_collection);
|
||||
for (UINT i = 0; i < count; ++i) {
|
||||
|
@ -885,7 +1060,11 @@ WasapiOutput::SearchDevice(std::string_view name)
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
static bool wasapi_output_test_default_device() { return true; }
|
||||
static bool
|
||||
wasapi_output_test_default_device()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const struct AudioOutputPlugin wasapi_output_plugin = {
|
||||
"wasapi",
|
||||
|
|
|
@ -29,17 +29,9 @@
|
|||
class COM {
|
||||
public:
|
||||
COM() {
|
||||
if (HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
if (HRESULT result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED|COINIT_DISABLE_OLE1DDE);
|
||||
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(
|
||||
throw MakeHResultError(
|
||||
result,
|
||||
"Unable to initialize COM with COINIT_APARTMENTTHREADED");
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ public:
|
|||
::CoCreateInstance(class_id, unknown_outer, class_context,
|
||||
__uuidof(T), reinterpret_cast<void **>(&ptr));
|
||||
if (FAILED(result)) {
|
||||
throw FormatHResultError(result, "Unable to create instance");
|
||||
throw MakeHResultError(result, "Unable to create instance");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,13 +21,11 @@
|
|||
#include "Com.hxx"
|
||||
#include "thread/Name.hxx"
|
||||
|
||||
Mutex COMWorker::mutex;
|
||||
unsigned int COMWorker::reference_count = 0;
|
||||
std::optional<COMWorker::COMWorkerThread> COMWorker::thread;
|
||||
|
||||
void COMWorker::COMWorkerThread::Work() noexcept {
|
||||
void
|
||||
COMWorker::Work() noexcept
|
||||
{
|
||||
SetThreadName("COM Worker");
|
||||
COM com{true};
|
||||
COM com;
|
||||
while (true) {
|
||||
if (!running_flag.test_and_set()) {
|
||||
return;
|
||||
|
|
|
@ -22,76 +22,46 @@
|
|||
|
||||
#include "WinEvent.hxx"
|
||||
#include "thread/Future.hxx"
|
||||
#include "thread/Mutex.hxx"
|
||||
#include "thread/Thread.hxx"
|
||||
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
// Worker thread for all COM operation
|
||||
class COMWorker {
|
||||
private:
|
||||
class COMWorkerThread : public Thread {
|
||||
public:
|
||||
COMWorkerThread() : Thread{BIND_THIS_METHOD(Work)} {}
|
||||
Thread 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<void()> &function) {
|
||||
spsc_buffer.push(function);
|
||||
event.Set();
|
||||
}
|
||||
|
||||
boost::lockfree::spsc_queue<std::function<void()>> spsc_buffer{32};
|
||||
std::atomic_flag running_flag = true;
|
||||
WinEvent event{};
|
||||
};
|
||||
boost::lockfree::spsc_queue<std::function<void()>> 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();
|
||||
}
|
||||
COMWorker() {
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
template <typename Function, typename... Args>
|
||||
static auto Async(Function &&function, Args &&...args) {
|
||||
using R = std::invoke_result_t<std::decay_t<Function>,
|
||||
std::decay_t<Args>...>;
|
||||
~COMWorker() noexcept {
|
||||
Finish();
|
||||
thread.Join();
|
||||
}
|
||||
|
||||
COMWorker(const COMWorker &) = delete;
|
||||
COMWorker &operator=(const COMWorker &) = delete;
|
||||
|
||||
template<typename Function>
|
||||
auto Async(Function &&function) {
|
||||
using R = std::invoke_result_t<std::decay_t<Function>>;
|
||||
auto promise = std::make_shared<Promise<R>>();
|
||||
auto future = promise->get_future();
|
||||
thread->Push([function = std::forward<Function>(function),
|
||||
args = std::make_tuple(std::forward<Args>(args)...),
|
||||
Push([function = std::forward<Function>(function),
|
||||
promise = std::move(promise)]() mutable {
|
||||
try {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
std::apply(std::forward<Function>(function),
|
||||
std::move(args));
|
||||
std::invoke(std::forward<Function>(function));
|
||||
promise->set_value();
|
||||
} else {
|
||||
promise->set_value(std::apply(
|
||||
std::forward<Function>(function),
|
||||
std::move(args)));
|
||||
promise->set_value(std::invoke(std::forward<Function>(function)));
|
||||
}
|
||||
} catch (...) {
|
||||
promise->set_exception(std::current_exception());
|
||||
|
@ -101,9 +71,17 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
static Mutex mutex;
|
||||
static unsigned int reference_count;
|
||||
static std::optional<COMWorkerThread> thread;
|
||||
void Finish() noexcept {
|
||||
running_flag.clear();
|
||||
event.Set();
|
||||
}
|
||||
|
||||
void Push(const std::function<void()> &function) {
|
||||
spsc_buffer.push(function);
|
||||
event.Set();
|
||||
}
|
||||
|
||||
void Work() noexcept;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
#include "HResult.hxx"
|
||||
#include "system/Error.hxx"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdarg>
|
||||
|
@ -27,11 +28,21 @@
|
|||
std::string
|
||||
HResultCategory::message(int Errcode) const
|
||||
{
|
||||
char buffer[256];
|
||||
|
||||
/* FormatMessage() supports some HRESULT values (depending on
|
||||
the Windows version) */
|
||||
if (FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |
|
||||
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, Errcode, 0,
|
||||
buffer, sizeof(buffer),
|
||||
nullptr))
|
||||
return buffer;
|
||||
|
||||
const auto msg = HRESULTToString(Errcode);
|
||||
if (!msg.empty())
|
||||
return std::string(msg);
|
||||
|
||||
char buffer[11]; // "0x12345678\0"
|
||||
int size = snprintf(buffer, sizeof(buffer), "0x%1x", Errcode);
|
||||
assert(2 <= size && size <= 10);
|
||||
return std::string(buffer, size);
|
||||
|
|
|
@ -50,6 +50,8 @@ case x:
|
|||
C(AUDCLNT_E_SERVICE_NOT_RUNNING);
|
||||
C(AUDCLNT_E_UNSUPPORTED_FORMAT);
|
||||
C(AUDCLNT_E_WRONG_ENDPOINT_TYPE);
|
||||
C(AUDCLNT_E_NOT_INITIALIZED);
|
||||
C(AUDCLNT_E_NOT_STOPPED);
|
||||
C(CO_E_NOTINITIALIZED);
|
||||
C(E_INVALIDARG);
|
||||
C(E_OUTOFMEMORY);
|
||||
|
@ -74,6 +76,13 @@ static inline const std::error_category &hresult_category() noexcept {
|
|||
return hresult_category_instance;
|
||||
}
|
||||
|
||||
inline std::system_error
|
||||
MakeHResultError(HRESULT result, const char *msg) noexcept
|
||||
{
|
||||
return std::system_error(std::error_code(result, hresult_category()),
|
||||
msg);
|
||||
}
|
||||
|
||||
gcc_printf(2, 3) std::system_error
|
||||
FormatHResultError(HRESULT result, const char *fmt, ...) noexcept;
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
#include "pcm/Convert.hxx"
|
||||
#include "fs/Path.hxx"
|
||||
#include "fs/NarrowPath.hxx"
|
||||
#include "io/FileDescriptor.hxx"
|
||||
#include "util/ConstBuffer.hxx"
|
||||
#include "util/StaticFifoBuffer.hxx"
|
||||
#include "util/OptionDef.hxx"
|
||||
|
@ -101,26 +102,21 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
try {
|
||||
const auto c = ParseCommandLine(argc, argv);
|
||||
static void
|
||||
RunConvert(PcmConvert &convert, size_t in_frame_size,
|
||||
FileDescriptor in_fd, FileDescriptor out_fd)
|
||||
{
|
||||
in_fd.SetBinaryMode();
|
||||
out_fd.SetBinaryMode();
|
||||
|
||||
SetLogThreshold(c.verbose ? LogLevel::DEBUG : LogLevel::INFO);
|
||||
const GlobalInit init(c.config_path);
|
||||
|
||||
const size_t in_frame_size = c.in_audio_format.GetFrameSize();
|
||||
|
||||
PcmConvert state(c.in_audio_format, c.out_audio_format);
|
||||
|
||||
StaticFifoBuffer<uint8_t, 4096> buffer;
|
||||
StaticFifoBuffer<std::byte, 4096> buffer;
|
||||
|
||||
while (true) {
|
||||
{
|
||||
const auto dest = buffer.Write();
|
||||
assert(!dest.empty());
|
||||
|
||||
ssize_t nbytes = read(0, dest.data, dest.size);
|
||||
ssize_t nbytes = in_fd.Read(dest.data, dest.size);
|
||||
if (nbytes <= 0)
|
||||
break;
|
||||
|
||||
|
@ -136,20 +132,31 @@ try {
|
|||
|
||||
buffer.Consume(src.size);
|
||||
|
||||
auto output = state.Convert({src.data, src.size});
|
||||
|
||||
[[maybe_unused]] ssize_t ignored = write(1, output.data,
|
||||
output.size);
|
||||
auto output = convert.Convert({src.data, src.size});
|
||||
out_fd.FullWrite(output.data, output.size);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
auto output = state.Flush();
|
||||
auto output = convert.Flush();
|
||||
if (output.IsNull())
|
||||
break;
|
||||
|
||||
[[maybe_unused]] ssize_t ignored = write(1, output.data,
|
||||
output.size);
|
||||
out_fd.FullWrite(output.data, output.size);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
try {
|
||||
const auto c = ParseCommandLine(argc, argv);
|
||||
|
||||
SetLogThreshold(c.verbose ? LogLevel::DEBUG : LogLevel::INFO);
|
||||
const GlobalInit init(c.config_path);
|
||||
|
||||
PcmConvert state(c.in_audio_format, c.out_audio_format);
|
||||
RunConvert(state, c.in_audio_format.GetFrameSize(),
|
||||
FileDescriptor(STDIN_FILENO),
|
||||
FileDescriptor(STDOUT_FILENO));
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
} catch (...) {
|
||||
|
|
|
@ -164,6 +164,8 @@ static int
|
|||
dump_input_stream(InputStream &is, FileDescriptor out,
|
||||
offset_type seek, size_t chunk_size)
|
||||
{
|
||||
out.SetBinaryMode();
|
||||
|
||||
std::unique_lock<Mutex> lock(is.mutex);
|
||||
|
||||
if (seek > 0)
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
#include "util/StringBuffer.hxx"
|
||||
#include "util/RuntimeError.hxx"
|
||||
#include "util/ScopeExit.hxx"
|
||||
#include "util/StaticFifoBuffer.hxx"
|
||||
#include "util/PrintException.hxx"
|
||||
#include "LogBackend.hxx"
|
||||
|
||||
|
@ -113,8 +114,11 @@ LoadAudioOutput(const ConfigData &config, EventLoop &event_loop,
|
|||
}
|
||||
|
||||
static void
|
||||
run_output(AudioOutput &ao, AudioFormat audio_format)
|
||||
RunOutput(AudioOutput &ao, AudioFormat audio_format,
|
||||
FileDescriptor in_fd)
|
||||
{
|
||||
in_fd.SetBinaryMode();
|
||||
|
||||
/* open the audio output */
|
||||
|
||||
ao.Enable();
|
||||
|
@ -126,33 +130,40 @@ run_output(AudioOutput &ao, AudioFormat audio_format)
|
|||
fprintf(stderr, "audio_format=%s\n",
|
||||
ToString(audio_format).c_str());
|
||||
|
||||
size_t frame_size = audio_format.GetFrameSize();
|
||||
const size_t in_frame_size = audio_format.GetFrameSize();
|
||||
|
||||
/* play */
|
||||
|
||||
size_t length = 0;
|
||||
char buffer[4096];
|
||||
StaticFifoBuffer<std::byte, 4096> buffer;
|
||||
|
||||
while (true) {
|
||||
if (length < sizeof(buffer)) {
|
||||
ssize_t nbytes = read(0, buffer + length,
|
||||
sizeof(buffer) - length);
|
||||
{
|
||||
const auto dest = buffer.Write();
|
||||
assert(!dest.empty());
|
||||
|
||||
ssize_t nbytes = in_fd.Read(dest.data, dest.size);
|
||||
if (nbytes <= 0)
|
||||
break;
|
||||
|
||||
length += (size_t)nbytes;
|
||||
buffer.Append(nbytes);
|
||||
}
|
||||
|
||||
size_t play_length = (length / frame_size) * frame_size;
|
||||
if (play_length > 0) {
|
||||
size_t consumed = ao.Play(buffer, play_length);
|
||||
auto src = buffer.Read();
|
||||
assert(!src.empty());
|
||||
|
||||
assert(consumed <= length);
|
||||
assert(consumed % frame_size == 0);
|
||||
src.size -= src.size % in_frame_size;
|
||||
if (src.empty())
|
||||
continue;
|
||||
|
||||
length -= consumed;
|
||||
memmove(buffer, buffer + consumed, length);
|
||||
}
|
||||
size_t consumed = ao.Play(src.data, src.size);
|
||||
|
||||
assert(consumed <= src.size);
|
||||
assert(consumed % in_frame_size == 0);
|
||||
|
||||
buffer.Consume(consumed);
|
||||
}
|
||||
|
||||
ao.Drain();
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
@ -174,7 +185,7 @@ try {
|
|||
|
||||
/* do it */
|
||||
|
||||
run_output(*ao, c.audio_format);
|
||||
RunOutput(*ao, c.audio_format, FileDescriptor(STDIN_FILENO));
|
||||
|
||||
/* cleanup and exit */
|
||||
|
||||
|
|
Loading…
Reference in New Issue