From 6c53848ea435152b3f7177fc5bdab69c3e0db048 Mon Sep 17 00:00:00 2001 From: "[ ]" <[ ]> Date: Mon, 9 Oct 2023 01:55:17 +0900 Subject: [PATCH] feat: First working pipewire endpoint > uses `T_NOTIF_SPICE` experimental endpoint on bmsound-pw's side > latency should be on par with wasapi shared? --- .idea/markdown-navigator-enh.xml | 10 + .idea/markdown-navigator.xml | 71 +++++ .idea/vcs.xml | 1 - CMakeLists.txt | 2 +- hooks/audio/implementations/pipewire.cpp | 359 +++++++++++++++++------ hooks/audio/implementations/pipewire.h | 38 +-- 6 files changed, 360 insertions(+), 121 deletions(-) create mode 100644 .idea/markdown-navigator-enh.xml create mode 100644 .idea/markdown-navigator.xml diff --git a/.idea/markdown-navigator-enh.xml b/.idea/markdown-navigator-enh.xml new file mode 100644 index 0000000..a8fcc84 --- /dev/null +++ b/.idea/markdown-navigator-enh.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml new file mode 100644 index 0000000..d7e995c --- /dev/null +++ b/.idea/markdown-navigator.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 6cc311d..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e8c0e02..2946fcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -573,7 +573,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest64.rc build/icon.rc cfg add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES}) target_link_libraries(spicetools_spice64 PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winscard - PRIVATE fmt-header-only discord-rpc imgui hash-library minhook openvr_api64 lua_static imm32 dwmapi) + PRIVATE fmt-header-only discord-rpc imgui hash-library minhook openvr_api64 lua_static imm32 dwmapi ntdll) set_target_properties(spicetools_spice64 PROPERTIES PREFIX "") set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64") target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1) diff --git a/hooks/audio/implementations/pipewire.cpp b/hooks/audio/implementations/pipewire.cpp index dd85d0e..6a78b27 100644 --- a/hooks/audio/implementations/pipewire.cpp +++ b/hooks/audio/implementations/pipewire.cpp @@ -1,111 +1,288 @@ +#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" -/* Imports (refer to bmsound-wine.dll.spec) */ -typedef void(*BmswClientInit_t)(const char*); -const WAVEFORMATEXTENSIBLE &PipewireBackend::format() const noexcept { +/* +... 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 (*BmswExperimentalProfile_t)(int); +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 int(*BmswClientFormatSupported_t)(DWORD, DWORD, DWORD); +typedef REFERENCE_TIME(*BmswClientWasapiPeriod_t)(void *); +typedef void (*BmswClientUpdateCallback_t)(void *, void *, void *); +static BmswExperimentalProfile_t BmswExperimentalProfile; +static BmswClientCreate_t BmswClientCreate; +static BmswClientStart_t BmswClientStart; +static BmswClientStop_t BmswClientStop; +static BmswClientDestroy_t BmswClientDestroy; +static BmswClientGetBuffer_t BmswClientGetBuffer; +static BmswClientReleaseBuffer_t BmswClientReleaseBuffer; +static BmswClientFormatSupported_t BmswClientFormatSupported; +static BmswClientWasapiPeriod_t BmswClientWasapiPeriod; +static BmswClientUpdateCallback_t BmswClientUpdateCallback; +static HMODULE bmsw_ = nullptr; +enum profile_exp +{ + T_NONE = 0, + T_AUDIO_FORMAT, + T_NOTIF_CALLBACK, + T_NOTIF_SPICE, + T_TWIN_CURSOR, + T_SPOOFED_LOOP, + T_STATIC_SINE, + T_LAST +}; + +/* 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 BmswClientFormatSupported(pFormat->nSamplesPerSec, pFormat->nChannels, pFormat->wBitsPerSample) == 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__); + if (client_) + { + REFERENCE_TIME ref_time = BmswClientWasapiPeriod(client_); + *default_device_period = ref_time; + *minimum_device_period = ref_time; + } + + 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) + auto ref_time = BmswClientWasapiPeriod(client_); //_INFO: this affects reported latency (1:100ns) + *hnsBufferDuration = ref_time; + *hnsPeriodicity = ref_time; + + //_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() : format_(hooks::audio::FORMAT), client_(nullptr) +{ + log_info("audio::pipewire", "{}", __FUNCTION__); + + // Initialize bmsound-wine.dll once + if (bmsw_) return; + if ((bmsw_ = libutils::try_library(MODULE_PATH / "bmsound-wine.dll"))) + { + BmswExperimentalProfile = (BmswExperimentalProfile_t) GetProcAddress(bmsw_, "BmswExperimentalProfile"); + 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"); + BmswClientFormatSupported = (BmswClientFormatSupported_t) GetProcAddress(bmsw_, "BmswClientFormatSupported"); + BmswClientWasapiPeriod = (BmswClientWasapiPeriod_t) GetProcAddress(bmsw_, "BmswClientWasapiPeriod"); + BmswClientUpdateCallback = (BmswClientUpdateCallback_t) GetProcAddress(bmsw_, "BmswClientUpdateCallback"); + + // Switch library to specific test API + BmswExperimentalProfile(T_NOTIF_SPICE); + + 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: {} FIRST TIME", __FUNCTION__, *buffer_frames); + } + iterc++; + if (iterc > 999999) + { + log_info("audio::pipewire", "on_get_buffer({}), frames: {}", iterc, *buffer_frames); + iterc = 0; + } + + *buffer_frames = 411; + + 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: {} FIRST TIME", __FUNCTION__, num_frames_requested); + } + iterc++; + if (iterc > 999999) + { + log_info("audio::pipewire", "on_get_buffer({}), frames: {}", iterc, num_frames_requested); + 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: {} FIRST TIME", __FUNCTION__, num_frames_written); + } + iterc++; + if (iterc > 999999) + { + log_info("audio::pipewire", "on_release_buffer({}), frames: {}", iterc, 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_initialize( - AUDCLNT_SHAREMODE *ShareMode, - DWORD *StreamFlags, - REFERENCE_TIME *hnsBufferDuration, - REFERENCE_TIME *hnsPeriodicity, - const WAVEFORMATEX *pFormat, - LPCGUID AudioSessionGuid) noexcept +HRESULT PipewireBackend::on_get_stream_latency(REFERENCE_TIME *latency) noexcept { - log_info("audio::pipewire", "on_initialize"); - - // WASAPI init - *ShareMode = AUDCLNT_SHAREMODE_SHARED; - *StreamFlags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | - AUDCLNT_STREAMFLAGS_RATEADJUST | - AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | - AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; - *hnsBufferDuration = 100000; - *hnsPeriodicity = 100000; - - // Start pipewire thread - if (!bmsw_) log_fatal("audio::pipewire", "Library not found: '{}'", (MODULE_PATH / "bmsound-wine.dll").string()); - auto pipewireinit = (BmswClientInit_t)GetProcAddress(bmsw_, "BmswClientInit"); - if (!pipewireinit) log_fatal("audio::pipewire", "Library invalid: '{}'", (MODULE_PATH / "bmsound-wine.dll").string()); - internal_ = std::thread(pipewireinit,GAME_INSTANCE->title()); + log_info("audio::pipewire", "{}", __FUNCTION__); + if (client_) *latency = BmswClientWasapiPeriod(client_); return S_OK; } -HRESULT PipewireBackend::on_get_buffer_size(uint32_t *buffer_frames) noexcept { - *buffer_frames = 0; - - return S_OK; -} -HRESULT PipewireBackend::on_get_stream_latency(REFERENCE_TIME *latency) noexcept { - *latency = 100000; - - return S_OK; -} -HRESULT PipewireBackend::on_get_current_padding(std::optional &padding_frames) noexcept { - +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_is_format_supported( - AUDCLNT_SHAREMODE *ShareMode, - const WAVEFORMATEX *pFormat, - WAVEFORMATEX **ppClosestMatch) noexcept +HRESULT PipewireBackend::on_get_mix_format(WAVEFORMATEX **pp_device_format) 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 PipewireBackend::on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept { + log_info("audio::pipewire", "{}", __FUNCTION__); return E_NOTIMPL; } -HRESULT PipewireBackend::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 PipewireBackend::on_start() noexcept { - return S_OK; -} -HRESULT PipewireBackend::on_stop() noexcept { - return S_OK; -} -HRESULT PipewireBackend::on_set_event_handle(HANDLE *event_handle) { - - *event_handle = CreateEvent(nullptr, true, false, nullptr); - - return S_OK; -} - -HRESULT PipewireBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) { - static BYTE buf[10000]; - *ppData = buf; - - return S_OK; -} -HRESULT PipewireBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) { - - return S_OK; -} -PipewireBackend::PipewireBackend() : format_(hooks::audio::FORMAT), bmsw_(libutils::try_library(MODULE_PATH / "bmsound-wine.dll")) -{ - -} diff --git a/hooks/audio/implementations/pipewire.h b/hooks/audio/implementations/pipewire.h index e491dfd..15bdb2f 100644 --- a/hooks/audio/implementations/pipewire.h +++ b/hooks/audio/implementations/pipewire.h @@ -4,48 +4,30 @@ #include -struct PipewireBackend final : AudioBackend { +struct PipewireBackend final : AudioBackend +{ public: + HANDLE relay_handle_; + explicit PipewireBackend(); - ~PipewireBackend() final = default; - + ~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_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_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_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_; - std::thread internal_; - HMODULE bmsw_; - //BYTE *active_sound_buffer = nullptr; + void *client_; + };