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