Merge branch 'main' into nac-spice22x
Merge official spice2x 'main', up tob53447bNotes:dc82c98is where the nixac fork starts. - Upstream's OBJECT-library + forbidden-static-DLL-import build refactor merged with nixAC's blob/dsdmo resources - SPICE_XP / runtime large-address-aware detection builds have been integrated from upstream - README.md grabs from upstream; .gitignore keeps nixAC's fork-specific entries
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/util.h"
|
||||
#include "hooks/audio/backends/wasapi/util.h"
|
||||
@@ -43,6 +44,27 @@ static void fix_rec_format(WAVEFORMATEX *pFormat) {
|
||||
pFormat->nAvgBytesPerSec = pFormat->nSamplesPerSec * pFormat->nBlockAlign;
|
||||
}
|
||||
|
||||
// decide whether the given multi-channel format should be downmixed to stereo and which algorithm
|
||||
// to use. an explicit user selection (-downmix) takes precedence; otherwise gitadora arena
|
||||
// two-channel mode defaults to the AC-4 algorithm.
|
||||
static std::optional<hooks::audio::DownmixAlgorithm> resolve_downmix(const WAVEFORMATEX *format) {
|
||||
if (format == nullptr
|
||||
|| format->nChannels <= 2
|
||||
|| format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (hooks::audio::DOWNMIX_ALGORITHM.has_value()) {
|
||||
return hooks::audio::DOWNMIX_ALGORITHM;
|
||||
}
|
||||
|
||||
if (games::gitadora::is_arena_model() && games::gitadora::TWOCHANNEL) {
|
||||
return hooks::audio::DownmixAlgorithm::AC4;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
IAudioClient *wrap_audio_client(IAudioClient *audio_client) {
|
||||
log_misc("audio::wasapi", "wrapping IAudioClient");
|
||||
|
||||
@@ -153,13 +175,51 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
// apply the -wasapishared option: redirect an exclusive request to shared mode. once redirected,
|
||||
// spice's own downmix/resample paths below are skipped (gated on redirected_from_exclusive) and
|
||||
// the shared engine handles any format conversion via AUTOCONVERTPCM (PCM / float only).
|
||||
if (hooks::audio::SharedRedirect::wants(ShareMode, pFormat)) {
|
||||
this->shared.apply(&ShareMode, &StreamFlags, &hnsPeriodicity);
|
||||
} else if (hooks::audio::WASAPI_COMPATIBILITY_MODE && ShareMode == AUDCLNT_SHAREMODE_SHARED) {
|
||||
log_warning(
|
||||
"audio::wasapi",
|
||||
"-wasapishared is enabled but the game is already opening a shared-mode stream; "
|
||||
"the option has no effect");
|
||||
}
|
||||
|
||||
WAVEFORMATEXTENSIBLE stereo_storage = {};
|
||||
WAVEFORMATEXTENSIBLE resample_storage = {};
|
||||
const WAVEFORMATEX *device_format = pFormat;
|
||||
|
||||
if (!this->shared.redirected_from_exclusive) {
|
||||
|
||||
// when downmixing, open the real device as stereo while the game keeps writing its native
|
||||
// multi-channel format into the scratch buffer.
|
||||
if (auto algorithm = resolve_downmix(pFormat)) {
|
||||
this->downmix.setup(pFormat, &stereo_storage, *algorithm);
|
||||
device_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
|
||||
log_info("audio::wasapi", "downmix enabled: {} channels -> 2 channels ({})",
|
||||
pFormat->nChannels, hooks::audio::Downmix::algorithm_name(*algorithm));
|
||||
} else if (games::gitadora::is_arena_model()) {
|
||||
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
// when resampling, open the real device at the target rate while the game keeps writing its
|
||||
// native-rate audio into the scratch buffer. this runs on whatever device_format is now: the
|
||||
// game's native format, or the stereo format produced above when downmix is also active, so
|
||||
// the two stages chain as multi-channel -> stereo -> resampled stereo.
|
||||
if (auto target_rate = hooks::audio::Resampler::resolve(device_format)) {
|
||||
const uint32_t src_rate = device_format->nSamplesPerSec;
|
||||
this->resample.setup(device_format, &resample_storage, *target_rate);
|
||||
device_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
|
||||
log_info("audio::wasapi", "resample enabled: {} Hz -> {} Hz{}",
|
||||
src_rate, *target_rate, this->downmix.enabled ? " (after downmix)" : "");
|
||||
}
|
||||
}
|
||||
|
||||
// verbose output
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
||||
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
||||
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
||||
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
||||
print_format(pFormat);
|
||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, device_format);
|
||||
|
||||
if (this->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize(
|
||||
@@ -171,27 +231,67 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
AudioSessionGuid));
|
||||
|
||||
log_info("audio::wasapi", "AudioBackend::on_initialize call finished");
|
||||
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
||||
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
||||
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
||||
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
||||
print_format(pFormat);
|
||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
|
||||
}
|
||||
|
||||
// check for exclusive mode
|
||||
if (ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
||||
this->exclusive_mode = true;
|
||||
this->frame_size = pFormat->nChannels * (pFormat->wBitsPerSample / 8);
|
||||
this->frame_size = device_format->nChannels * (device_format->wBitsPerSample / 8);
|
||||
|
||||
// optionally enlarge the exclusive buffer. games request a very small buffer (e.g. 3 ms)
|
||||
// which some endpoints (notably NVIDIA HDMI/DP display audio) cannot service in time,
|
||||
// underrunning mid-period and crackling. a larger buffer gives the device slack. exclusive
|
||||
// mode requires periodicity == buffer_duration, so raise both together; the initialize
|
||||
// paths below handle any required buffer-size realignment.
|
||||
if (hooks::audio::EXCLUSIVE_BUFFER_MS.has_value()) {
|
||||
const REFERENCE_TIME min_duration =
|
||||
(REFERENCE_TIME) hooks::audio::EXCLUSIVE_BUFFER_MS.value() * 10000;
|
||||
if (hnsBufferDuration < min_duration) {
|
||||
log_info("audio::wasapi",
|
||||
"raising exclusive buffer from {} hns to {} hns ({} ms)",
|
||||
hnsBufferDuration, min_duration, hooks::audio::EXCLUSIVE_BUFFER_MS.value());
|
||||
hnsBufferDuration = min_duration;
|
||||
if (hnsPeriodicity != 0) {
|
||||
hnsPeriodicity = min_duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call next
|
||||
HRESULT ret = pReal->Initialize(
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid);
|
||||
// call next. the resampler owns the device interaction whenever it is active (including when
|
||||
// chained after the downmix), otherwise the downmix does, otherwise the device is opened
|
||||
// directly.
|
||||
HRESULT ret;
|
||||
if (this->resample.enabled) {
|
||||
ret = this->resample.initialize(
|
||||
pReal,
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
device_format,
|
||||
AudioSessionGuid);
|
||||
} else if (this->downmix.enabled) {
|
||||
ret = this->downmix.initialize(
|
||||
pReal,
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
device_format,
|
||||
AudioSessionGuid);
|
||||
} else {
|
||||
ret = initialize_with_alignment_retry(
|
||||
pReal,
|
||||
"audio::wasapi",
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
device_format,
|
||||
AudioSessionGuid);
|
||||
}
|
||||
|
||||
// check for failure
|
||||
if (FAILED(ret)) {
|
||||
@@ -200,8 +300,17 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
}
|
||||
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize success, hr={}", FMT_HRESULT(ret));
|
||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||
copy_wave_format(&hooks::audio::FORMAT, device_format);
|
||||
copy_wave_format(&this->device_format, device_format);
|
||||
|
||||
// arm the shared-mode buffer bridge so the redirected game's full-buffer writes are paced to
|
||||
// the device instead of overflowing the shared buffer (AUDCLNT_E_BUFFER_TOO_LARGE).
|
||||
if (this->shared.redirected_from_exclusive) {
|
||||
this->shared.enable_bridge(
|
||||
device_format->nChannels * (device_format->wBitsPerSample / 8));
|
||||
}
|
||||
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::Initialize");
|
||||
return ret;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
@@ -222,7 +331,21 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferF
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->GetBufferSize(pNumBufferFrames));
|
||||
HRESULT ret = pReal->GetBufferSize(pNumBufferFrames);
|
||||
|
||||
// report the buffer size at the game's native rate; the real device buffer is at the
|
||||
// resampled rate, so translate it back so the game paces its writes correctly.
|
||||
if (SUCCEEDED(ret) && this->resample.enabled && pNumBufferFrames) {
|
||||
*pNumBufferFrames = this->resample.frames_device_to_game(*pNumBufferFrames);
|
||||
}
|
||||
|
||||
// redirected to shared mode: clamp the reported buffer to one device period (see SharedRedirect).
|
||||
if (SUCCEEDED(ret) && this->shared.redirected_from_exclusive && pNumBufferFrames) {
|
||||
*pNumBufferFrames = this->shared.clamp_buffer_size(
|
||||
pReal, this->device_format.Format.nSamplesPerSec, *pNumBufferFrames);
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetStreamLatency(REFERENCE_TIME *phnsLatency) {
|
||||
static std::once_flag printed;
|
||||
@@ -264,7 +387,21 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetCurrentPadding(UINT32 *pNumPad
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->GetCurrentPadding(pNumPaddingFrames));
|
||||
HRESULT ret = pReal->GetCurrentPadding(pNumPaddingFrames);
|
||||
|
||||
// the device buffer is at the resampled rate; report padding at the game's native rate so the
|
||||
// game's free-space calculation stays paced correctly.
|
||||
if (SUCCEEDED(ret) && this->resample.enabled && pNumPaddingFrames) {
|
||||
*pNumPaddingFrames = this->resample.padding_device_to_game(*pNumPaddingFrames);
|
||||
}
|
||||
|
||||
// shared-mode bridge: the game writes into a FIFO, not the device buffer, so report the FIFO's
|
||||
// fill level rather than the device's padding (which is in a different buffer space).
|
||||
if (SUCCEEDED(ret) && this->shared.bridge_enabled() && pNumPaddingFrames) {
|
||||
*pNumPaddingFrames = this->shared.virtual_padding();
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
|
||||
AUDCLNT_SHAREMODE ShareMode,
|
||||
@@ -282,6 +419,54 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
|
||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
// log the format the game is asking about
|
||||
log_info("audio::wasapi", "IAudioClient::IsFormatSupported hook hit");
|
||||
print_format(ShareMode, pFormat);
|
||||
|
||||
// under the exclusive->shared redirect, report the exclusive format as supported so the game
|
||||
// doesn't fall back before reaching Initialize.
|
||||
if (hooks::audio::SharedRedirect::wants(ShareMode, pFormat)) {
|
||||
log_info("audio::wasapi", "... reporting supported (will redirect to shared mode)");
|
||||
if (ppClosestMatch) {
|
||||
*ppClosestMatch = nullptr;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// when downmixing, the real device is opened as stereo, so check whether the equivalent
|
||||
// stereo format is supported instead of the multi-channel one. when resampling is also active
|
||||
// it chains onto that stereo format, so check the resampled stereo format.
|
||||
if (resolve_downmix(pFormat)) {
|
||||
WAVEFORMATEXTENSIBLE stereo_storage = {};
|
||||
hooks::audio::Downmix::make_stereo_format(pFormat, &stereo_storage);
|
||||
const WAVEFORMATEX *check_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
|
||||
|
||||
WAVEFORMATEXTENSIBLE resample_storage = {};
|
||||
if (auto target_rate = hooks::audio::Resampler::resolve(check_format)) {
|
||||
hooks::audio::Resampler::make_device_format(check_format, &resample_storage, *target_rate);
|
||||
check_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
|
||||
}
|
||||
|
||||
log_info("audio::wasapi", "... checking device format instead (after downmix/resample):");
|
||||
print_format(check_format);
|
||||
|
||||
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, check_format, ppClosestMatch));
|
||||
} else if (games::gitadora::is_arena_model()) {
|
||||
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
} else if (auto target_rate = hooks::audio::Resampler::resolve(pFormat)) {
|
||||
|
||||
// when resampling, the real device is opened at the target rate, so check whether the
|
||||
// equivalent format at that rate is supported instead of the game's native rate.
|
||||
WAVEFORMATEXTENSIBLE resample_storage = {};
|
||||
hooks::audio::Resampler::make_device_format(pFormat, &resample_storage, *target_rate);
|
||||
const auto resample_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
|
||||
|
||||
log_info("audio::wasapi", "... checking device format instead (after resample):");
|
||||
print_format(resample_format);
|
||||
|
||||
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, resample_format, ppClosestMatch));
|
||||
}
|
||||
|
||||
if (this->backend) {
|
||||
HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch);
|
||||
|
||||
@@ -474,5 +659,7 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
|
||||
|
||||
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
|
||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||
copy_wave_format(&this->device_format, pFormat);
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::InitializeSharedAudioStream");
|
||||
return ret;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user