From 06ba2a950f4856ca96500b6cceb275377667dff8 Mon Sep 17 00:00:00 2001 From: Scan Date: Wed, 28 Aug 2024 11:33:31 -0400 Subject: [PATCH 1/4] Re-implement dummy backend on 24-08-24 --- CMakeLists.txt | 1 + hooks/audio/audio.h | 3 + hooks/audio/backends/wasapi/audio_client.cpp | 4 + hooks/audio/implementations/none.cpp | 99 ++++++++++++++++++++ hooks/audio/implementations/none.h | 48 ++++++++++ launcher/options.cpp | 2 +- 6 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 hooks/audio/implementations/none.cpp create mode 100644 hooks/audio/implementations/none.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fa18ba2..5b0dbe6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -472,6 +472,7 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/audio/backends/wasapi/util.cpp hooks/audio/implementations/asio.cpp hooks/audio/implementations/wave_out.cpp + hooks/audio/implementations/none.cpp hooks/avshook.cpp hooks/cfgmgr32hook.cpp hooks/debughook.cpp diff --git a/hooks/audio/audio.h b/hooks/audio/audio.h index 7ea4517..a86915d 100644 --- a/hooks/audio/audio.h +++ b/hooks/audio/audio.h @@ -13,6 +13,7 @@ namespace hooks::audio { enum class Backend { Asio, WaveOut, + None, }; extern bool ENABLED; @@ -32,6 +33,8 @@ namespace hooks::audio { return Backend::Asio; } else if (_stricmp(value, "waveout") == 0) { return Backend::WaveOut; + } else if (_stricmp(value, "none") == 0) { + return Backend::None; } return std::nullopt; diff --git a/hooks/audio/backends/wasapi/audio_client.cpp b/hooks/audio/backends/wasapi/audio_client.cpp index 222506c..dfa0204 100644 --- a/hooks/audio/backends/wasapi/audio_client.cpp +++ b/hooks/audio/backends/wasapi/audio_client.cpp @@ -9,6 +9,7 @@ #include "hooks/audio/backends/wasapi/util.h" #include "hooks/audio/implementations/asio.h" #include "hooks/audio/implementations/wave_out.h" +#include "hooks/audio/implementations/none.h" //#include "util/co_task_mem_ptr.h" #include "defs.h" @@ -160,6 +161,9 @@ IAudioClient *wrap_audio_client(IAudioClient *audio_client) { case hooks::audio::Backend::WaveOut: backend = new WaveOutBackend(); break; + case hooks::audio::Backend::None: + backend = new NoneBackend(); + break; default: break; } diff --git a/hooks/audio/implementations/none.cpp b/hooks/audio/implementations/none.cpp new file mode 100644 index 0000000..7cffe35 --- /dev/null +++ b/hooks/audio/implementations/none.cpp @@ -0,0 +1,99 @@ +#include "none.h" +#include "hooks/audio/audio.h" +#include "hooks/audio/backends/wasapi/audio_client.h" + + +const WAVEFORMATEXTENSIBLE &NoneBackend::format() const noexcept { + return format_; +} + +HRESULT NoneBackend::on_initialize( + AUDCLNT_SHAREMODE *ShareMode, + DWORD *StreamFlags, + REFERENCE_TIME *hnsBufferDuration, + REFERENCE_TIME *hnsPeriodicity, + const WAVEFORMATEX *pFormat, + LPCGUID AudioSessionGuid) noexcept +{ + *ShareMode = AUDCLNT_SHAREMODE_SHARED; + *StreamFlags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | + AUDCLNT_STREAMFLAGS_RATEADJUST | + AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | + AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; + *hnsBufferDuration = 100000; + *hnsPeriodicity = 100000; + log_info("audio::none", "on_initialize"); + + return S_OK; +} +HRESULT NoneBackend::on_get_buffer_size(uint32_t *buffer_frames) noexcept { + *buffer_frames = 0; + + return S_OK; +} +HRESULT NoneBackend::on_get_stream_latency(REFERENCE_TIME *latency) noexcept { + *latency = 100000; + + return S_OK; +} +HRESULT NoneBackend::on_get_current_padding(std::optional &padding_frames) noexcept { + + padding_frames = 0; + + return S_OK; +} +HRESULT NoneBackend::on_is_format_supported( + AUDCLNT_SHAREMODE *ShareMode, + const WAVEFORMATEX *pFormat, + WAVEFORMATEX **ppClosestMatch) noexcept +{ + // only accept 44.1 kHz, stereo, 16-bits per channel + if (*ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE && + pFormat->nChannels == 2 && + pFormat->nSamplesPerSec == 44100 && + pFormat->wBitsPerSample == 16) + { + return S_OK; + } + + return AUDCLNT_E_UNSUPPORTED_FORMAT; +} +HRESULT NoneBackend::on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept { + return E_NOTIMPL; +} +HRESULT NoneBackend::on_get_device_period( + REFERENCE_TIME *default_device_period, + REFERENCE_TIME *minimum_device_period) +{ + *default_device_period = 10000; + *minimum_device_period = 10000; + + return S_OK; +} +HRESULT NoneBackend::on_start() noexcept { + return S_OK; +} +HRESULT NoneBackend::on_stop() noexcept { + return S_OK; +} +HRESULT NoneBackend::on_set_event_handle(HANDLE *event_handle) { + + *event_handle = CreateEvent(nullptr, true, false, nullptr); + + return S_OK; +} + +HRESULT NoneBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) { + static BYTE buf[10000]; + *ppData = buf; + + return S_OK; +} +HRESULT NoneBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) { + + return S_OK; +} +NoneBackend::NoneBackend() : format_(hooks::audio::FORMAT) +{ + +} diff --git a/hooks/audio/implementations/none.h b/hooks/audio/implementations/none.h new file mode 100644 index 0000000..e595703 --- /dev/null +++ b/hooks/audio/implementations/none.h @@ -0,0 +1,48 @@ +#pragma once + +#include "backend.h" + + +struct NoneBackend final : AudioBackend { +public: + explicit NoneBackend(); + ~NoneBackend() final = default; + + [[nodiscard]] const WAVEFORMATEXTENSIBLE &format() const noexcept override; + + HRESULT on_initialize( + AUDCLNT_SHAREMODE *ShareMode, + DWORD *StreamFlags, + REFERENCE_TIME *hnsBufferDuration, + REFERENCE_TIME *hnsPeriodicity, + const WAVEFORMATEX *pFormat, + LPCGUID AudioSessionGuid) noexcept override; + + HRESULT on_get_buffer_size(uint32_t *buffer_frames) noexcept override; + HRESULT on_get_stream_latency(REFERENCE_TIME *latency) noexcept override; + HRESULT on_get_current_padding(std::optional &padding_frames) noexcept override; + + HRESULT on_is_format_supported( + AUDCLNT_SHAREMODE *ShareMode, + const WAVEFORMATEX *pFormat, + WAVEFORMATEX **ppClosestMatch) noexcept override; + + HRESULT on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept override; + + HRESULT on_get_device_period( + REFERENCE_TIME *default_device_period, + REFERENCE_TIME *minimum_device_period) override; + + HRESULT on_start() noexcept override; + HRESULT on_stop() noexcept override; + HRESULT on_set_event_handle(HANDLE *event_handle) override; + + HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override; + HRESULT on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) override; + +private: + + const WAVEFORMATEXTENSIBLE &format_; + BYTE *active_sound_buffer = nullptr; +}; + diff --git a/launcher/options.cpp b/launcher/options.cpp index eb0a799..4fe96f0 100644 --- a/launcher/options.cpp +++ b/launcher/options.cpp @@ -1341,7 +1341,7 @@ static const std::vector OPTION_DEFINITIONS = { " Does nothing for games that do not output to exclusive WASAPI", .type = OptionType::Enum, .category = "Audio", - .elements = {{"asio", "ASIO"}, {"waveout", "waveOut"}}, + .elements = {{"asio", "ASIO"}, {"waveout", "waveOut"},{"none", "None"}}, }, { .title = "Spice Audio Hook ASIO Driver ID", -- 2.45.2 From 8db3a552d6c609a526ddf44806206a041d565545 Mon Sep 17 00:00:00 2001 From: Scan Date: Wed, 28 Aug 2024 11:43:11 -0400 Subject: [PATCH 2/4] Re-implement Pipewire backend on 24-08-24 --- CMakeLists.txt | 1 + hooks/audio/audio.h | 5 +- hooks/audio/backends/wasapi/audio_client.cpp | 4 + hooks/audio/implementations/pipewire.cpp | 273 +++++++++++++++++++ hooks/audio/implementations/pipewire.h | 35 +++ launcher/options.cpp | 2 +- 6 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 hooks/audio/implementations/pipewire.cpp create mode 100644 hooks/audio/implementations/pipewire.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b0dbe6..5bb9c1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -473,6 +473,7 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/audio/implementations/asio.cpp hooks/audio/implementations/wave_out.cpp hooks/audio/implementations/none.cpp + hooks/audio/implementations/pipewire.cpp hooks/avshook.cpp hooks/cfgmgr32hook.cpp hooks/debughook.cpp diff --git a/hooks/audio/audio.h b/hooks/audio/audio.h index a86915d..aaf8e3d 100644 --- a/hooks/audio/audio.h +++ b/hooks/audio/audio.h @@ -13,7 +13,8 @@ namespace hooks::audio { enum class Backend { Asio, WaveOut, - None, + Pipewire, + None }; extern bool ENABLED; @@ -33,6 +34,8 @@ namespace hooks::audio { return Backend::Asio; } else if (_stricmp(value, "waveout") == 0) { return Backend::WaveOut; + } else if (_stricmp(value, "pipewire") == 0) { + return Backend::Pipewire; } else if (_stricmp(value, "none") == 0) { return Backend::None; } diff --git a/hooks/audio/backends/wasapi/audio_client.cpp b/hooks/audio/backends/wasapi/audio_client.cpp index dfa0204..6aa0d8d 100644 --- a/hooks/audio/backends/wasapi/audio_client.cpp +++ b/hooks/audio/backends/wasapi/audio_client.cpp @@ -9,6 +9,7 @@ #include "hooks/audio/backends/wasapi/util.h" #include "hooks/audio/implementations/asio.h" #include "hooks/audio/implementations/wave_out.h" +#include "hooks/audio/implementations/pipewire.h" #include "hooks/audio/implementations/none.h" //#include "util/co_task_mem_ptr.h" @@ -161,6 +162,9 @@ IAudioClient *wrap_audio_client(IAudioClient *audio_client) { case hooks::audio::Backend::WaveOut: backend = new WaveOutBackend(); break; + case hooks::audio::Backend::Pipewire: + backend = new PipewireBackend(); + break; case hooks::audio::Backend::None: backend = new NoneBackend(); break; diff --git a/hooks/audio/implementations/pipewire.cpp b/hooks/audio/implementations/pipewire.cpp new file mode 100644 index 0000000..ac76e29 --- /dev/null +++ b/hooks/audio/implementations/pipewire.cpp @@ -0,0 +1,273 @@ +#include +#include "pipewire.h" +#include "hooks/audio/audio.h" +#include "hooks/audio/backends/wasapi/audio_client.h" +#include "util/libutils.h" +#include "launcher/launcher.h" +#include "hooks/audio/util.h" +#include "hooks/audio/backends/wasapi/util.h" + + +/* +... ShareMode : AUDCLNT_SHAREMODE_EXCLUSIVE <- backend to reimplement, THIS is the audio engine, GAME is client https://learn.microsoft.com/en-us/windows/win32/coreaudio/audclnt-streamflags-xxx-constants +... StreamFlags : AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST <- first is the whole reason for relay_handle_ requirement, second flag not tested +... hnsBufferDuration : 10000 <- wasapi people inventing new time units, 1ms (since 1unit==100ns, 10000*0.0001ms) +... hnsPeriodicity : 10000 <- same as above (1ms) +... nChannels : 2 <- channel_count +... nSamplesPerSec : 44100 <- bitrate +... nAvgBytesPerSec : 176400 <- raw buffer size per second (bitrate * stride)(bytes) +... nBlockAlign : 4 <- stride (bytes) +... wBitsPerSample : 16 <- sample size (bits) +... wValidBitsPerSample : 16 <- same as above +... dwChannelMask : SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT <- position config + _INFO: Code pipeline + INIT: on_is_format_supported()?->on_initialize()->on_set_event_handle()-:(LOOPx1):->on_start() + LOOP: if relay_handle_: on_get_buffer_size()->on_get_buffer()->..->on_release_buffer() + DEINIT: on_stop() + + _REV: POSSIBLE ALTERNATIVES: + Check if the stream is driving. The stream needs to have the + * PW_STREAM_FLAG_DRIVER set. When the stream is driving, + * pw_stream_trigger_process() needs to be called when data is + * available (output) or needed (input). Since 0.3.34 + bool pw_stream_is_driving(struct pw_stream *stream); + * */ + +/* Imports/Exports (refer to bmsound-wine.dll.spec) */ +typedef void (*BmswConfigInit_t)(const char *); +typedef void (*BmswExperimentalForceProfile_t)(const char *); +typedef int(*BmswClientFormatIsSupported_t)(DWORD, DWORD, DWORD, void *); +typedef int(*BmswClientFormatPeriodFPC_t)(void *); +typedef REFERENCE_TIME(*BmswClientFormatPeriodWRT_t)(void *); +typedef void *(*BmswClientCreate_t)(const char *, void *, void *); +typedef int(*BmswClientStart_t)(void *); +typedef int(*BmswClientStop_t)(void *); +typedef int(*BmswClientDestroy_t)(void *); +typedef unsigned char *(*BmswClientGetBuffer_t)(void *, uint32_t); +typedef int(*BmswClientReleaseBuffer_t)(void *, uint32_t); +typedef void (*BmswClientUpdateCallback_t)(void *, void *, void *); +static BmswConfigInit_t BmswConfigInit; +[[maybe_unused]] static BmswExperimentalForceProfile_t BmswExperimentalForceProfile; +static BmswClientFormatIsSupported_t BmswClientFormatIsSupported; +static BmswClientFormatPeriodFPC_t BmswClientFormatPeriodFPC; +static BmswClientFormatPeriodWRT_t BmswClientFormatPeriodWRT; +static BmswClientCreate_t BmswClientCreate; +static BmswClientStart_t BmswClientStart; +static BmswClientStop_t BmswClientStop; +static BmswClientDestroy_t BmswClientDestroy; +static BmswClientGetBuffer_t BmswClientGetBuffer; +static BmswClientReleaseBuffer_t BmswClientReleaseBuffer; +[[maybe_unused]] static BmswClientUpdateCallback_t BmswClientUpdateCallback; +static HMODULE bmsw_ = nullptr; + +/* Audio init (unless specified otherwise, run once at start) */ +// Reports to game whether each requested audio format is available for device (first success return will be used) +HRESULT PipewireBackend::on_is_format_supported(AUDCLNT_SHAREMODE *ShareMode, const WAVEFORMATEX *pFormat, WAVEFORMATEX **ppClosestMatch) noexcept +{ + // Format reporting and filtering + log_info("audio::pipewire", "{}", __FUNCTION__); + log_misc("audio::pipewire", "Checking backend support for {} channels, {} Hz, {}-bit", + pFormat->nChannels, + pFormat->nSamplesPerSec, + pFormat->wBitsPerSample); + + // IIDX? will always request format that was last checked through this function + if (*ShareMode != AUDCLNT_SHAREMODE_EXCLUSIVE) return AUDCLNT_E_UNSUPPORTED_FORMAT; + + // Request format support from real backend (only accepts 44.1 kHz, stereo, 16-bits per channel for now) + return BmswClientFormatIsSupported(pFormat->nSamplesPerSec, pFormat->nChannels, pFormat->wBitsPerSample, nullptr) == 0 ? S_OK : AUDCLNT_E_UNSUPPORTED_FORMAT; +} +// Populate hnsPeriodicity, _INFO: runs before on_initialize, requires configured client stream data +HRESULT PipewireBackend::on_get_device_period(REFERENCE_TIME *default_device_period, REFERENCE_TIME *minimum_device_period) +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + *default_device_period = wrt_; + *minimum_device_period = wrt_; + + return S_OK; +} +// This expects to be run once on_is_format_supported() succeeds, initializes backend based on passed arguments (and updates them if needed) +HRESULT PipewireBackend::on_initialize(AUDCLNT_SHAREMODE *ShareMode, DWORD *StreamFlags, REFERENCE_TIME *hnsBufferDuration, REFERENCE_TIME *hnsPeriodicity, const WAVEFORMATEX *pFormat, LPCGUID AudioSessionGuid) noexcept +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + + // Initialize pipewire client (without starting the thread) + client_ = BmswClientCreate(GAME_INSTANCE->title(), nullptr, nullptr);//(void *)callback_notify,this + + if (!client_) + log_fatal("audio::pipewire", "Client could not be initialized"); + log_info("audio::pipewire", "Client initialized: '{}'", fmt::ptr(client_)); + + // Adjust WASAPI configuration visible to game (passed arguments are populated and should match that of last on_is_format_supported call) + *hnsBufferDuration = wrt_; + *hnsPeriodicity = wrt_; + + //_TODO: Init info + log_info("audio::asio", "Device Info:"); + log_info("audio::pipewire", "... hnsBufferDuration : {}", *hnsBufferDuration); + log_info("audio::pipewire", "... hnsPeriodicity : {}", *hnsPeriodicity); + + return S_OK; +} +// This takes ownership over shared event handle exclusive to AUDCLNT_STREAMFLAGS_EVENTCALLBACK _INFO: first loop iteration runs directly after this, before on_start() call +HRESULT PipewireBackend::on_set_event_handle(HANDLE *event_handle) +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + this->relay_handle_ = *event_handle; // take over WASAPI's owned handle pre-initialized by client + //BmswClientUpdateCallback(client_, (void *) 0, this->relay_handle_); + *event_handle = CreateEvent(nullptr, true, false, nullptr); // replace WASAPI's handle with always off handle + + return S_OK; + + ULONG ntbuf_size = sizeof(OBJECT_NAME_INFORMATION) + 1024; + char ntbuf[ntbuf_size]; + OBJECT_NAME_INFORMATION *ntinfo = (OBJECT_NAME_INFORMATION *) ntbuf; + NTSTATUS status = NtQueryObject(*event_handle, ObjectNameInformation, ntinfo, ntbuf_size, &ntbuf_size); + if (NT_SUCCESS(status)) + { + // possible to rename anonymous handle to access by name + } +} +HRESULT PipewireBackend::on_start() noexcept +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + BmswClientStart(client_); + return S_OK; +} +PipewireBackend::PipewireBackend() : relay_handle_(nullptr), format_(hooks::audio::FORMAT), client_(nullptr) +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + + // Initialize bmsound-wine.dll once + if (!bmsw_ && (bmsw_ = libutils::try_library(MODULE_PATH / "bmsound-wine.dll"))) + { + BmswConfigInit = (BmswConfigInit_t) GetProcAddress(bmsw_, "BmswConfigInit"); + BmswExperimentalForceProfile = (BmswExperimentalForceProfile_t) GetProcAddress(bmsw_, "BmswExperimentalForceProfile"); + BmswClientFormatIsSupported = (BmswClientFormatIsSupported_t) GetProcAddress(bmsw_, "BmswClientFormatIsSupported"); + BmswClientFormatPeriodWRT = (BmswClientFormatPeriodWRT_t) GetProcAddress(bmsw_, "BmswClientFormatPeriodWRT"); + BmswClientFormatPeriodFPC = (BmswClientFormatPeriodFPC_t) GetProcAddress(bmsw_, "BmswClientFormatPeriodFPC"); + BmswClientCreate = (BmswClientCreate_t) GetProcAddress(bmsw_, "BmswClientCreate"); + BmswClientStart = (BmswClientStart_t) GetProcAddress(bmsw_, "BmswClientStart"); + BmswClientStop = (BmswClientStop_t) GetProcAddress(bmsw_, "BmswClientStop"); + BmswClientDestroy = (BmswClientDestroy_t) GetProcAddress(bmsw_, "BmswClientDestroy"); + BmswClientGetBuffer = (BmswClientGetBuffer_t) GetProcAddress(bmsw_, "BmswClientGetBuffer"); + BmswClientReleaseBuffer = (BmswClientReleaseBuffer_t) GetProcAddress(bmsw_, "BmswClientReleaseBuffer"); + BmswClientUpdateCallback = (BmswClientUpdateCallback_t) GetProcAddress(bmsw_, "BmswClientUpdateCallback"); + + // Load config + BmswConfigInit("prop/linux.json"); + //BmswExperimentalForceProfile("notif_spice"); + } + if (bmsw_) + { + // Sync config + wrt_ = BmswClientFormatPeriodWRT(nullptr); //_INFO: wrt affects reported latency (1:100ns) + fpc_ = BmswClientFormatPeriodFPC(nullptr); + return; + } + log_fatal("audio::pipewire", "Library not found: '{}'", (MODULE_PATH / "bmsound-wine.dll").string()); +} + +/* Audio callback loop (reacts to state of relay_handle_, most likely separate thread */ +// _REV: Synchronizing client and backend with nanotime may be necessary (bmsound-pw endpoint dependant) +inline static void callback_notify(void *self) +{ + //log_info("audio::pipewire", "{}", __FUNCTION__); + auto *self_ = (PipewireBackend *) self; + //std::this_thread::sleep_for(std::chrono::nanoseconds(10 * 1000 * 1000 - 227)); + if (!SetEvent(self_->relay_handle_)) // has to be called for game-side audio loop to continue + { + DWORD last_error = GetLastError(); + + log_warning("audio::asio", "AsioBackend::buffer_switch: SetEvent failed: {} ({})", + last_error, + std::system_category().message(last_error)); + } +} +// Amount in frames of data we may handle at once (which should always end up being what gets send by client, unless overrun happens) +HRESULT PipewireBackend::on_get_buffer_size(uint32_t *buffer_frames) noexcept +{ + static int iterc = -1; + if (iterc == -1) + { + log_info("audio::pipewire", "{}, frames: {} (INITIAL HIT)", __FUNCTION__, fpc_); + iterc = 0; + } + + *buffer_frames = fpc_; + + return S_OK; +} +// Wants a raw stream buffer to be assigned into *ppData, this stream will have x amount of sound frames stored into it by a client and assumes exclusive access until next on_release_buffer +HRESULT PipewireBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) +{ + static int iterc = -1; + if (iterc == -1) + { + log_info("audio::pipewire", "{}, frames: {} (INITIAL HIT)", __FUNCTION__, num_frames_requested); + } + iterc++; + if (iterc > 999999) + { + log_info("audio::pipewire", "on_get_buffer, frames: {} (HIT {})", num_frames_requested, iterc); + iterc = 0; + } + + *ppData = BmswClientGetBuffer(client_, num_frames_requested); + + return S_OK; +} +// This releases access to buffer from last on_get_buffer and implies x amount of frames being valid data to be streamed +HRESULT PipewireBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) +{ + static int iterc = -1; + if (iterc == -1) + { + log_info("audio::pipewire", "{}, frames: {} (INITIAL HIT)", __FUNCTION__, num_frames_written); + iterc = 0; + } + + BmswClientReleaseBuffer(client_, num_frames_written); + callback_notify(this); + + return S_OK; +} + +/* Audio deinit (unless specified otherwise, run once at termination) */ +HRESULT PipewireBackend::on_stop() noexcept +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + BmswClientStop(client_); + return S_OK; +} +PipewireBackend::~PipewireBackend() +{ + log_info("audio::pipewire", "~PipewireBackend"); + if (client_) BmswClientDestroy(client_); +} + +/* Unsorted (unknown callers, should be considered as unimplemented/untested) *///_BUG: does this need implementation? +const WAVEFORMATEXTENSIBLE &PipewireBackend::format() const noexcept +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + return format_; +} +HRESULT PipewireBackend::on_get_stream_latency(REFERENCE_TIME *latency) noexcept +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + *latency = BmswClientFormatPeriodWRT(client_); + + return S_OK; +} +HRESULT PipewireBackend::on_get_current_padding(std::optional &padding_frames) noexcept +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + padding_frames = 0; + + return S_OK; +} +HRESULT PipewireBackend::on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + return E_NOTIMPL; +} diff --git a/hooks/audio/implementations/pipewire.h b/hooks/audio/implementations/pipewire.h new file mode 100644 index 0000000..f777dc1 --- /dev/null +++ b/hooks/audio/implementations/pipewire.h @@ -0,0 +1,35 @@ +#pragma once + +#include "backend.h" +#include + + +struct PipewireBackend final : AudioBackend +{ +public: + HANDLE relay_handle_; + + explicit PipewireBackend(); + ~PipewireBackend() final; + [[nodiscard]] const WAVEFORMATEXTENSIBLE &format() const noexcept override; + HRESULT on_initialize(AUDCLNT_SHAREMODE *ShareMode, DWORD *StreamFlags, REFERENCE_TIME *hnsBufferDuration, REFERENCE_TIME *hnsPeriodicity, const WAVEFORMATEX *pFormat, LPCGUID AudioSessionGuid) noexcept override; + HRESULT on_get_buffer_size(uint32_t *buffer_frames) noexcept override; + HRESULT on_get_stream_latency(REFERENCE_TIME *latency) noexcept override; + HRESULT on_get_current_padding(std::optional &padding_frames) noexcept override; + HRESULT on_is_format_supported(AUDCLNT_SHAREMODE *ShareMode, const WAVEFORMATEX *pFormat, WAVEFORMATEX **ppClosestMatch) noexcept override; + HRESULT on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept override; + HRESULT on_get_device_period(REFERENCE_TIME *default_device_period, REFERENCE_TIME *minimum_device_period) override; + HRESULT on_start() noexcept override; + HRESULT on_stop() noexcept override; + HRESULT on_set_event_handle(HANDLE *event_handle) override; + HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override; + HRESULT on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) override; + +private: + int fpc_; + REFERENCE_TIME wrt_; + const WAVEFORMATEXTENSIBLE &format_; + void *client_; + +}; + diff --git a/launcher/options.cpp b/launcher/options.cpp index 4fe96f0..c983575 100644 --- a/launcher/options.cpp +++ b/launcher/options.cpp @@ -1341,7 +1341,7 @@ static const std::vector OPTION_DEFINITIONS = { " Does nothing for games that do not output to exclusive WASAPI", .type = OptionType::Enum, .category = "Audio", - .elements = {{"asio", "ASIO"}, {"waveout", "waveOut"},{"none", "None"}}, + .elements = {{"asio", "ASIO"}, {"waveout", "waveOut"},{"pipewire", "Pipewire (Linux)"},{"none", "None"}}, }, { .title = "Spice Audio Hook ASIO Driver ID", -- 2.45.2 From 915397b28f31c06aa38b361b5f1055d9572e8547 Mon Sep 17 00:00:00 2001 From: Scan Date: Wed, 28 Aug 2024 11:56:30 -0400 Subject: [PATCH 3/4] Add global access for selected game object on 24-08-24 --- games/game.cpp | 4 ++++ games/game.h | 1 + launcher/launcher.cpp | 2 ++ launcher/launcher.h | 2 ++ 4 files changed, 9 insertions(+) diff --git a/games/game.cpp b/games/game.cpp index 54e7f10..3701f4c 100644 --- a/games/game.cpp +++ b/games/game.cpp @@ -5,6 +5,10 @@ games::Game::Game(std::string name) { this->name = name; } +const char *games::Game::title() { + return this->name.c_str(); +} + void games::Game::attach() { log_info("game", "attach: {}", name); } diff --git a/games/game.h b/games/game.h index 775434d..a0dee5f 100644 --- a/games/game.h +++ b/games/game.h @@ -15,6 +15,7 @@ namespace games { // where the main magic will happen virtual void attach(); + virtual const char *title(); // optional virtual void pre_attach(); diff --git a/launcher/launcher.cpp b/launcher/launcher.cpp index fc8a259..796fe1c 100644 --- a/launcher/launcher.cpp +++ b/launcher/launcher.cpp @@ -121,6 +121,7 @@ std::string LOG_FILE_PATH = ""; int LAUNCHER_ARGC = 0; char **LAUNCHER_ARGV = nullptr; std::unique_ptr> LAUNCHER_OPTIONS; +games::Game *GAME_INSTANCE = nullptr; std::string CARD_OVERRIDES[2]; // sub-systems @@ -1616,6 +1617,7 @@ int main_implementation(int argc, char *argv[]) { avs::core::set_default_heap_size("kamunity.dll"); games.push_back(new games::qks::QKSGame()); } + GAME_INSTANCE = games.back(); // apply user heap size, if defined if (user_heap_size > 0) { diff --git a/launcher/launcher.h b/launcher/launcher.h index 8d1ac12..3b51236 100644 --- a/launcher/launcher.h +++ b/launcher/launcher.h @@ -7,6 +7,7 @@ #include #include "cfg/option.h" +#include "games/game.h" namespace rawinput { class RawInputManager; @@ -21,6 +22,7 @@ extern std::string LOG_FILE_PATH; extern int LAUNCHER_ARGC; extern char **LAUNCHER_ARGV; extern std::unique_ptr> LAUNCHER_OPTIONS; +extern games::Game *GAME_INSTANCE; extern std::unique_ptr API_CONTROLLER; extern std::unique_ptr RI_MGR; -- 2.45.2 From badae421da20d749345a40b323fa13b661f0d264 Mon Sep 17 00:00:00 2001 From: Scan Date: Wed, 28 Aug 2024 12:17:45 -0400 Subject: [PATCH 4/4] Update readme to be less vague about how the project is built --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ce350ec..654393a 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,23 @@ -SpiceTools +SpiceTools for Linux (Experimental) ========== This is a loader for various arcade games developed by 573. The project is using CMake as it's build system, with a custom build script for making packages ready for distribution and to keep it easy for people not knowing how to use CMake. +This fork aims to re-implement a Pipewire backend for WASAPI exclusive mode that was originally written for the 23-09-29 build of spice2x. ## Building/Distribution We're currently using Arch Linux for building the binaries. You'll need: -- MinGW-64 packages (can be found in the AUR) +- MinGW-64 packages (can be found in the AUR, package name is mingw-w64-cmake) - bash - git - zip - upx (optional) For any other GNU/Linux distributions (or Windows lol), you're on your -own. +own. Don't use the Docker scripts, they're broken. To build the project, run: @@ -24,6 +25,10 @@ To build the project, run: ## Build Configuration +Note: Individuals using AUR helper tools like yay may experience errors when building the project, as a result of missing toolchain files. Please make sure to select "packages to cleanBuild?" when installing with yay, or better yet, use the PKGBUILD directly. + +This is due to the fact that the spice2x Arch build scripts expect that you have toolchain files in /usr/share/mingw which are only generated if you build MinGW from source. + You can tweak some settings at the beginning of the build script. You might want to modify the paths of the toolchains if yours differ. It's also possible to disable UPX compression and source distribution for -- 2.45.2