Align nixAC to spice2x folder structure

This commit is contained in:
2026-08-06 16:03:22 +02:00
parent 3ddd34d51d
commit 64260623bf
1060 changed files with 0 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,153 @@
#pragma once
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <optional>
#include <thread>
#include "external/asio/asio.h"
#include "external/asio/iasiodrv.h"
#include "external/readerwriterqueue/readerwriterqueue.h"
#include "hooks/audio/audio_private.h"
#include "hooks/audio/buffer.h"
#include "backend.h"
struct AsioBackend;
extern AsioBackend *ASIO_BACKEND;
struct BufferEntry {
BYTE *buffer;
size_t length;
size_t read;
};
struct AsioInstanceInfo {
long inputs = 0;
long outputs = 0;
long buffer_min_size = 0;
long buffer_max_size = 0;
long buffer_preferred_size = 0;
long buffer_granularity = 0;
long input_latency = 0;
long output_latency = 0;
};
struct AsioBackend final : AudioBackend {
public:
explicit AsioBackend();
~AsioBackend() final;
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<uint32_t> &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) noexcept override;
HRESULT on_start() noexcept override;
HRESULT on_stop() noexcept override;
HRESULT on_set_event_handle(HANDLE *event_handle) noexcept override;
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **pp_data) noexcept override;
HRESULT on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) noexcept override;
// for overlay
inline const AsioDriverInfo &driver_info() const noexcept {
return this->driver_info_;
}
inline const std::vector<AsioChannelInfo> &channel_info() const noexcept {
return this->asio_channel_info_;
}
inline const AsioInstanceInfo &asio_info() const noexcept {
return this->asio_info_;
}
void open_control_panel();
std::atomic<uint32_t> queued_frames = 0;
std::atomic<size_t> queued_bytes = 0;
private:
using AsioFunction = std::function<AsioError()>;
enum class AsioThreadState {
Closed,
Failed,
Running,
ShuttingDown,
};
struct AsioThreadMessage {
AsioFunction fn;
bool result_needed;
};
void set_thread_state(AsioThreadState state);
bool load_driver();
bool update_driver_info();
bool update_latency();
bool set_initial_format(WAVEFORMATEXTENSIBLE &target);
bool init();
bool unload_driver();
void reset();
AsioError run_on_asio_thread(AsioFunction fn, bool result_needed = true);
// ASIO callbacks
static void buffer_switch(long double_buffer_index, AsioBool direct_process);
static void sample_rate_did_change(AsioSampleRate sample_rate);
static long asio_message(long selector, long value, void *message, double *opt);
// helper methods
static bool is_supported_subformat(const WAVEFORMATEXTENSIBLE &format_ex) noexcept;
REFERENCE_TIME compute_ref_time() const;
REFERENCE_TIME compute_latency_ref_time() const;
std::thread asio_thread;
std::atomic_bool asio_thread_initialized = false;
std::mutex asio_thread_state_lock;
// TODO: use `std::atomic<T>::wait` when stabilized in MSVC
std::condition_variable asio_thread_state_cv;
std::atomic<AsioThreadState> asio_thread_state = AsioThreadState::Closed;
moodycamel::BlockingReaderWriterQueue<AsioThreadMessage> asio_msg_queue_func;
moodycamel::BlockingReaderWriterQueue<AsioError> asio_msg_queue_result;
moodycamel::ReaderWriterQueue<BufferEntry> queue;
std::optional<HANDLE> relay_handle = std::nullopt;
IAsio *asio_driver = nullptr;
AsioCallbacks asio_callbacks {};
AsioDriverInfo driver_info_ {};
AsioInstanceInfo asio_info_;
std::vector<AsioChannelInfo> asio_channel_info_;
std::vector<AsioBufferInfo> asio_buffers;
SampleType asio_sample_type = SampleType::UNSUPPORTED;
std::atomic_bool started = false;
const WAVEFORMATEXTENSIBLE &format_;
WAVEFORMATEXTENSIBLE last_checked_format {};
//std::vector<BYTE> last_sound_buffer;
std::vector<double> conversion_sound_buffer;
BYTE *active_sound_buffer = nullptr;
};
@@ -0,0 +1,55 @@
#pragma once
#include <cstdint>
#include <optional>
#include <windows.h>
#include <audioclient.h>
#include <ksmedia.h>
struct WrappedIAudioClient;
struct AudioBackend {
public:
virtual ~AudioBackend() = default;
[[nodiscard]] virtual const WAVEFORMATEXTENSIBLE &format() const noexcept = 0;
#pragma region IAudioClient
virtual HRESULT on_initialize(
AUDCLNT_SHAREMODE *ShareMode,
DWORD *StreamFlags,
REFERENCE_TIME *hnsBufferDuration,
REFERENCE_TIME *hnsPeriodicity,
const WAVEFORMATEX *pFormat,
LPCGUID AudioSessionGuid) = 0;
virtual HRESULT on_get_buffer_size(uint32_t *buffer_frames) = 0;
virtual HRESULT on_get_stream_latency(REFERENCE_TIME *latency) = 0;
virtual HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) = 0;
virtual HRESULT on_is_format_supported(
AUDCLNT_SHAREMODE *ShareMode,
const WAVEFORMATEX *pFormat,
WAVEFORMATEX **ppClosestMatch) = 0;
virtual HRESULT on_get_mix_format(WAVEFORMATEX **pp_device_format) = 0;
virtual HRESULT on_get_device_period(
REFERENCE_TIME *default_device_period,
REFERENCE_TIME *minimum_device_period) = 0;
virtual HRESULT on_start() = 0;
virtual HRESULT on_stop() = 0;
virtual HRESULT on_set_event_handle(HANDLE *event_handle) = 0;
#pragma endregion
#pragma region IAudioRenderClient
virtual HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) = 0;
virtual HRESULT on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) = 0;
#pragma endregion
};
@@ -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<uint32_t> &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)
{
}
@@ -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<uint32_t> &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;
};
@@ -0,0 +1,300 @@
#include <winternl.h>
#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 int(*BmswClientAwaitBuffer_t)(void *);
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;
static BmswClientAwaitBuffer_t BmswClientAwaitBuffer;
[[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);
notif_ = std::thread(PipewireBackend::notif_poll, 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::pipewire", "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__);
// Take over WASAPI's owned handle pre-initialized by client and replace with always off dummy, cleaning up previously owned handle
if (relay_handle_) CloseHandle(relay_handle_);
relay_handle_ = *event_handle;
*event_handle = CreateEvent(nullptr, true, false, nullptr);
//BmswClientUpdateCallback(client_, (void *) 0, relay_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_(CreateEvent(nullptr, true, false, nullptr)), format_(hooks::audio::FORMAT), client_(nullptr), notif_state_(-1)
{
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");
BmswClientAwaitBuffer = (BmswClientAwaitBuffer_t) GetProcAddress(bmsw_, "BmswClientAwaitBuffer");
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::pipewire", "SetEvent failed({}): {}",
last_error,
std::system_category().message(last_error));
}
}
void PipewireBackend::notif_poll(PipewireBackend *self)
{
self->notif_state_ = BmswClientAwaitBuffer(self->client_);
while (self->notif_state_ == 1)
{
BmswClientAwaitBuffer(self->client_);
callback_notify(self);
}
self->notif_state_ = -1;
}
// 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);
if (notif_state_ == -1) 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 (notif_state_ == 1)
{
notif_state_ = 0;
notif_.join();
if (notif_state_ != -1)
{
log_warning("audio::pipewire", "Errors during thread cleanup: '{}'", (const int) notif_state_);
}
}
if (client_) BmswClientDestroy(client_);
if (relay_handle_) CloseHandle(relay_handle_);
}
/* 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<uint32_t> &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;
}
@@ -0,0 +1,38 @@
#pragma once
#include "backend.h"
#include <thread>
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<uint32_t> &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;
static void notif_poll(PipewireBackend *self);
private:
int fpc_;
REFERENCE_TIME wrt_;
const WAVEFORMATEXTENSIBLE &format_;
void *client_;
std::thread notif_;
volatile int notif_state_;
};
@@ -0,0 +1,243 @@
#include "wave_out.h"
#include "hooks/audio/audio.h"
#include "hooks/audio/backends/wasapi/audio_client.h"
#include "hooks/audio/backends/wasapi/defs.h"
#include "hooks/audio/util.h"
#include "hooks/audio/buffer.h"
static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME;
HRESULT WaveOutBackend::init(uint32_t buffer_size) {
MMRESULT ret;
if (format_.Format.nSamplesPerSec == 0)
{
log_warning("audio::wave_out", "format_ condition race");
return static_cast<HRESULT>(MMSYSERR_ERROR);
}
hooks::audio::FORMAT.Format.wFormatTag = WAVE_FORMAT_PCM;
log_info("audio::wave_out", "initializing waveOut backend with {} channels, {} Hz, {}-bit, {} format",
format_.Format.nChannels,
format_.Format.nSamplesPerSec,
format_.Format.wBitsPerSample,
format_.Format.wFormatTag);
log_info("audio::wave_out", "... nBlockAlign : {} bytes", format_.Format.nBlockAlign);
log_info("audio::wave_out", "... nAvgBytesPerSec : {} bytes", format_.Format.nAvgBytesPerSec);
log_info("audio::wave_out", "... buffer reftime : {} ms", WASAPI_TARGET_REFTIME / 10000.f);
log_info("audio::wave_out", "... buffer count : {} buffers", _countof(this->hdrs));
ret = waveOutOpen(
&this->handle,
WAVE_MAPPER,
reinterpret_cast<const WAVEFORMATEX *>(&format_.Format),
reinterpret_cast<DWORD_PTR>(this->dispatcher_event),
reinterpret_cast<DWORD_PTR>(nullptr),
CALLBACK_EVENT);
if (ret != MMSYSERR_NOERROR) {
log_warning("audio::wave_out", "failed to initialize waveOut backend, hr={:#08x}",
static_cast<unsigned>(ret));
return static_cast<HRESULT>(ret);
}
// initialize buffers
log_info("audio::wave_out", "... device handle : {}", fmt::ptr(this->handle));
for (auto &hdr : this->hdrs) {
memset(&hdr, 0, sizeof(hdr));
hdr.lpData = new char[buffer_size] {};
hdr.dwBufferLength = buffer_size;
hdr.dwBytesRecorded = 0;
hdr.dwUser = 0;
hdr.dwFlags = 0;
hdr.dwLoops = 0;
hdr.lpNext = nullptr;
ret = waveOutPrepareHeader(this->handle, &hdr, sizeof(hdr));
if (ret != MMSYSERR_NOERROR) {
log_warning("audio::wave_out", "failed to prepare waveOut header, hr=0x{:08x}",
static_cast<unsigned>(ret));
return static_cast<HRESULT>(ret);
}
ret = waveOutWrite(this->handle, &hdr, sizeof(hdr));
if (ret != MMSYSERR_NOERROR) {
log_warning("audio::wave_out", "failed to write waveOut header, hr=0x{:08x}",
static_cast<unsigned>(ret));
return static_cast<HRESULT>(ret);
}
}
// mark as initialized
this->initialized = true;
return S_OK;
}
const WAVEFORMATEXTENSIBLE &WaveOutBackend::format() const noexcept {
return format_;
}
HRESULT WaveOutBackend::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 = WASAPI_TARGET_REFTIME;
*hnsPeriodicity = WASAPI_TARGET_REFTIME;
log_info("audio::wave_out", "on_initialize");
// this backend only supports stereo audio
if (pFormat->nChannels > 2) {
return AUDCLNT_E_UNSUPPORTED_FORMAT;
}
return S_OK;
}
HRESULT WaveOutBackend::on_get_buffer_size(uint32_t *buffer_frames) noexcept {
*buffer_frames = _countof(this->hdrs);
return S_OK;
}
HRESULT WaveOutBackend::on_get_stream_latency(REFERENCE_TIME *latency) noexcept {
*latency = WASAPI_TARGET_REFTIME;
return S_OK;
}
HRESULT WaveOutBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) noexcept {
size_t queued_bytes = 0;
for (auto &hdr : this->hdrs) {
if (hdr.dwFlags & WHDR_DONE) {
queued_bytes += static_cast<unsigned>(hdr.dwBufferLength);
}
}
auto frames = static_cast<uint32_t>(queued_bytes / format_.Format.nBlockAlign);
//log_info("audio::wave_out", "queued_bytes = {}, frames = {}", queued_bytes, frames);
padding_frames = frames;
return S_OK;
}
HRESULT WaveOutBackend::on_is_format_supported(
AUDCLNT_SHAREMODE *ShareMode,
const WAVEFORMATEX *pFormat,
WAVEFORMATEX **ppClosestMatch) noexcept
{
// always support 44.1 kHz, stereo, 16-bits per channel with custom backends
if (*ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE &&
pFormat->nChannels == 2 &&
pFormat->nSamplesPerSec == 44100 &&
pFormat->wBitsPerSample == 16)
{
return S_OK;
}
//return waveOutOpen(nullptr, WAVE_MAPPER, reinterpret_cast<const WAVEFORMATEX *>(&format_.Format), NULL, NULL, WAVE_FORMAT_QUERY) == MMSYSERR_NOERROR ? S_OK : AUDCLNT_E_UNSUPPORTED_FORMAT;
return AUDCLNT_E_UNSUPPORTED_FORMAT;
}
HRESULT WaveOutBackend::on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept {
return E_NOTIMPL;
}
HRESULT WaveOutBackend::on_get_device_period(
REFERENCE_TIME *default_device_period,
REFERENCE_TIME *minimum_device_period)
{
*default_device_period = WASAPI_TARGET_REFTIME;
*minimum_device_period = WASAPI_TARGET_REFTIME;
return S_OK;
}
HRESULT WaveOutBackend::on_start() noexcept {
return S_OK;
}
HRESULT WaveOutBackend::on_stop() noexcept {
return S_OK;
}
HRESULT WaveOutBackend::on_set_event_handle(HANDLE *event_handle) {
this->relay_event = *event_handle;
this->dispatcher_event = CreateEvent(nullptr, true, false, nullptr);
*event_handle = this->dispatcher_event;
return S_OK;
}
HRESULT WaveOutBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
size_t buffer_size = format_.Format.nBlockAlign * num_frames_requested;
if (!this->initialized) {
this->init(buffer_size);
}
const size_t converted_size = required_buffer_size(num_frames_requested, format_.Format.nChannels, SampleType::SINT_16);
const size_t max_size = std::max(buffer_size, converted_size);
// wait for a free slot
WaitForSingleObject(this->dispatcher_event, INFINITE);
// allocate temporary sound buffer
this->active_sound_buffer = reinterpret_cast<BYTE *>(CoTaskMemAlloc(max_size));
// hand the buffer to the callee
*ppData = this->active_sound_buffer;
return S_OK;
}
HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) {
bool written = false;
// reset the dispatcher event
ResetEvent(this->dispatcher_event);
while (!written) {
for (WAVEHDR &hdr : this->hdrs) {
if (hdr.dwFlags & WHDR_DONE) {
memcpy(hdr.lpData, this->active_sound_buffer, hdr.dwBufferLength);
// write the data to the device now
MMRESULT ret = waveOutWrite(this->handle, &hdr, sizeof(hdr));
if (ret != MMSYSERR_NOERROR) {
log_warning("audio::wave_out", "failed to write waveOut data, hr={:#08x}",
static_cast<unsigned>(ret));
}
written = true;
break;
}
}
// avoid pegging the CPU
if (!written) {
Sleep(1);
}
}
// free temporary sound buffer
CoTaskMemFree(this->active_sound_buffer);
this->active_sound_buffer = nullptr;
// trigger game audio callback
SetEvent(this->relay_event);
return S_OK;
}
WaveOutBackend::WaveOutBackend() : format_(hooks::audio::FORMAT)
{
}
@@ -0,0 +1,60 @@
#pragma once
#include <mmdeviceapi.h>
#include <mmsystem.h>
#include "backend.h"
#define WASAPI_BUFFER_COUNT 3
#define TARGET_REFTIME (100000) // 10 ms
struct WaveOutBackend final : AudioBackend {
public:
explicit WaveOutBackend();
~WaveOutBackend() final = default;
HRESULT init(uint32_t buffer_size);
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<uint32_t> &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:
WrappedIAudioClient *client;
const WAVEFORMATEXTENSIBLE &format_;
bool initialized = false;
HANDLE relay_event = nullptr;
HANDLE dispatcher_event = nullptr;
HWAVEOUT handle = nullptr;
WAVEHDR hdrs[WASAPI_BUFFER_COUNT] {};
BYTE *active_sound_buffer = nullptr;
};