mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
audio: stereo downmix, volume boost options (#722)
## Link to GitHub Issue or related Pull Request, if one exists Fixes #717, fixes #647 ## Description of change Adds an option to downmix surround sound (5.1, 7.1, etc) down to stereo (2 speakers). This can be used in most WASAPI games, including Gitadora and FTT. How it works: when the game tries to open surround format (say, 7.1) we create a fake buffer and tell the game that it is supported. In reality we open a 2-channel stream with the real sound card. When the stream begins, we downmix the channels down to two (using one of many algorithms) and output to the sound card. While we're here, implement an option to boost the game volume by some decibel value, which is a feature often requested by SDVX players. This was needed since downmixing can sometimes result in quieter audio. ## Testing Tested GW Delta and FTT. Downmix doesn't work on IIDX (32 bits) but volume boost works.
This commit is contained in:
@@ -504,6 +504,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/audio/backends/mmdevice/device_enumerator.cpp
|
||||
hooks/audio/backends/wasapi/audio_client.cpp
|
||||
hooks/audio/backends/wasapi/audio_render_client.cpp
|
||||
hooks/audio/backends/wasapi/downmix.cpp
|
||||
hooks/audio/backends/wasapi/dummy_audio_client.cpp
|
||||
hooks/audio/backends/wasapi/dummy_audio_clock.cpp
|
||||
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#include "handle.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include <unordered_map>
|
||||
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "cfg/configurator.h"
|
||||
#include "hooks/audio/mme.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
@@ -659,26 +663,32 @@ namespace games::gitadora {
|
||||
}
|
||||
|
||||
// two channel mod
|
||||
if (TWOCHANNEL) {
|
||||
if (is_arena_model()) {
|
||||
log_warning("gitadora", "two channel audio (-2ch) is not supported on Arena Model - use a patch instead");
|
||||
deferredlogs::defer_error_messages({
|
||||
"two channel audio (-2ch) is not supported on Arena Model - use a patch instead",
|
||||
});
|
||||
if (TWOCHANNEL && !is_arena_model()) {
|
||||
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
|
||||
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
|
||||
|
||||
} else {
|
||||
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
|
||||
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
|
||||
|
||||
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
|
||||
if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) ||
|
||||
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) {
|
||||
log_warning("gitadora", "two channel mode failed");
|
||||
}
|
||||
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
|
||||
if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) ||
|
||||
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) {
|
||||
log_warning("gitadora", "two channel mode failed");
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void fix_audio_channel_mask(WAVEFORMATEX *format) {
|
||||
if (!format || format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto ext = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(format);
|
||||
|
||||
// fix the legacy 7.1 channel mask to the modern surround layout
|
||||
// makes it more compatible with modern audio cards
|
||||
if (ext->dwChannelMask == KSAUDIO_SPEAKER_7POINT1) {
|
||||
ext->dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "games/game.h"
|
||||
#include "util/socd_cleaner.h"
|
||||
@@ -25,6 +28,8 @@ namespace games::gitadora {
|
||||
virtual void attach() override;
|
||||
};
|
||||
|
||||
void fix_audio_channel_mask(WAVEFORMATEX *format);
|
||||
|
||||
static inline bool is_drum() {
|
||||
return (
|
||||
avs::game::is_model({ "J32", "K32", "L32" }) ||
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace hooks::audio {
|
||||
// public globals
|
||||
bool ENABLED = true;
|
||||
bool VOLUME_HOOK_ENABLED = true;
|
||||
std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM = std::nullopt;
|
||||
float VOLUME_BOOST = 1.0f;
|
||||
bool USE_DUMMY = false;
|
||||
WAVEFORMATEXTENSIBLE FORMAT {};
|
||||
std::optional<Backend> BACKEND = std::nullopt;
|
||||
|
||||
@@ -16,8 +16,19 @@ namespace hooks::audio {
|
||||
WaveOut,
|
||||
};
|
||||
|
||||
// surround-to-stereo downmix algorithm
|
||||
enum class DownmixAlgorithm {
|
||||
FrontOnly, // keep only the front channels
|
||||
RearOnly, // keep only the rear/back channels
|
||||
SideOnly, // keep only the side channels
|
||||
AC4, // AC-4 stereo downmix coefficients (ETSI TS 103 190-1)
|
||||
Normalize, // all channels equally loud, LFE dropped
|
||||
};
|
||||
|
||||
extern bool ENABLED;
|
||||
extern bool VOLUME_HOOK_ENABLED;
|
||||
extern std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM;
|
||||
extern float VOLUME_BOOST;
|
||||
extern bool USE_DUMMY;
|
||||
extern WAVEFORMATEXTENSIBLE FORMAT;
|
||||
extern std::optional<Backend> BACKEND;
|
||||
|
||||
@@ -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"
|
||||
@@ -41,6 +42,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");
|
||||
|
||||
@@ -145,13 +167,27 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
// when downmixing, open the real device as stereo while the game keeps writing its native
|
||||
// multi-channel format into the scratch buffer.
|
||||
WAVEFORMATEXTENSIBLE stereo_storage = {};
|
||||
const WAVEFORMATEX *device_format = pFormat;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// 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(device_format);
|
||||
|
||||
if (this->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize(
|
||||
@@ -173,17 +209,29 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
// 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);
|
||||
}
|
||||
|
||||
// call next
|
||||
HRESULT ret = pReal->Initialize(
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid);
|
||||
HRESULT ret;
|
||||
if (this->downmix.enabled) {
|
||||
ret = this->downmix.initialize(
|
||||
pReal,
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
device_format,
|
||||
AudioSessionGuid);
|
||||
} else {
|
||||
ret = pReal->Initialize(
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
device_format,
|
||||
AudioSessionGuid);
|
||||
}
|
||||
|
||||
// check for failure
|
||||
if (FAILED(ret)) {
|
||||
@@ -192,7 +240,8 @@ 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);
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -274,6 +323,18 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
|
||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
// when downmixing, the real device is opened as stereo, so check whether the equivalent
|
||||
// stereo format is supported instead of the multi-channel one.
|
||||
if (resolve_downmix(pFormat)) {
|
||||
WAVEFORMATEXTENSIBLE stereo_storage = {};
|
||||
hooks::audio::Downmix::make_stereo_format(pFormat, &stereo_storage);
|
||||
const auto stereo_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
|
||||
|
||||
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, stereo_format, ppClosestMatch));
|
||||
} else if (games::gitadora::is_arena_model()) {
|
||||
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
if (this->backend) {
|
||||
HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch);
|
||||
|
||||
@@ -466,5 +527,6 @@ 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);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "downmix.h"
|
||||
|
||||
#include "audio_render_client.h"
|
||||
|
||||
// {1FBC8530-AF3E-4128-B418-115DE72F76B6}
|
||||
@@ -93,4 +95,12 @@ struct WrappedIAudioClient : IAudioClient3 {
|
||||
AudioBackend *const backend;
|
||||
bool exclusive_mode = false;
|
||||
int frame_size = 0;
|
||||
|
||||
// the format the real device was opened with (after any downmix). used to scale the final
|
||||
// output buffer for the volume boost.
|
||||
WAVEFORMATEXTENSIBLE device_format = {};
|
||||
|
||||
// surround -> stereo downmix. the real device is opened as stereo while the game keeps
|
||||
// writing multi-channel audio into a scratch buffer that we downmix in the render client.
|
||||
hooks::audio::Downmix downmix;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,72 @@
|
||||
#include "audio_render_client.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "audio_client.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "wasapi_private.h"
|
||||
|
||||
const char CLASS_NAME[] = "WrappedIAudioRenderClient";
|
||||
|
||||
// scale every sample of an interleaved device buffer by `gain`, clamped to the format's range.
|
||||
// supports 16/24/32-bit PCM and 32-bit float; other formats are left untouched.
|
||||
static void apply_gain(BYTE *buffer, UINT32 frames, const WAVEFORMATEXTENSIBLE &fmt, float gain) {
|
||||
const WAVEFORMATEX &f = fmt.Format;
|
||||
const size_t samples = (size_t) frames * f.nChannels;
|
||||
|
||||
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT has Data1 == 3, _PCM has Data1 == 1
|
||||
bool is_float = f.wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|
||||
|| (f.wFormatTag == WAVE_FORMAT_EXTENSIBLE && fmt.SubFormat.Data1 == 0x00000003);
|
||||
|
||||
if (is_float && f.wBitsPerSample == 32) {
|
||||
auto p = reinterpret_cast<float *>(buffer);
|
||||
for (size_t i = 0; i < samples; i++) {
|
||||
p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (f.wBitsPerSample) {
|
||||
case 16: {
|
||||
auto p = reinterpret_cast<int16_t *>(buffer);
|
||||
for (size_t i = 0; i < samples; i++) {
|
||||
p[i] = (int16_t) std::clamp((int) std::lround(p[i] * gain), -32768, 32767);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 24: {
|
||||
// packed 24-bit little-endian
|
||||
for (size_t i = 0; i < samples; i++) {
|
||||
BYTE *s = buffer + i * 3;
|
||||
int32_t v = s[0] | (s[1] << 8) | (s[2] << 16);
|
||||
if (v & 0x800000) {
|
||||
v |= ~0xFFFFFF; // sign extend
|
||||
}
|
||||
int64_t scaled = std::clamp<int64_t>(
|
||||
std::llround((double) v * gain), -8388608, 8388607);
|
||||
s[0] = scaled & 0xFF;
|
||||
s[1] = (scaled >> 8) & 0xFF;
|
||||
s[2] = (scaled >> 16) & 0xFF;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 32: {
|
||||
auto p = reinterpret_cast<int32_t *>(buffer);
|
||||
for (size_t i = 0; i < samples; i++) {
|
||||
p[i] = (int32_t) std::clamp(
|
||||
std::llround((double) p[i] * gain),
|
||||
(long long) INT32_MIN, (long long) INT32_MAX);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
@@ -51,6 +113,12 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::GetBuffer(UINT32 NumFramesR
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// surround downmix: reserve the real (stereo) device buffer, but hand the game a
|
||||
// multi-channel scratch buffer that we downmix on release
|
||||
if (this->client->downmix.enabled) {
|
||||
CHECK_RESULT(this->client->downmix.get_buffer(pReal, NumFramesRequested, ppData));
|
||||
}
|
||||
|
||||
// call original
|
||||
HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData);
|
||||
|
||||
@@ -75,14 +143,44 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::ReleaseBuffer(UINT32 NumFra
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// fix for audio pop effect
|
||||
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
|
||||
// resolve the real device buffer for whichever path produced the audio
|
||||
BYTE *device_buffer;
|
||||
if (this->client->downmix.enabled) {
|
||||
|
||||
// zero out = mute
|
||||
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
|
||||
// downmix the game's multi-channel scratch into the real stereo buffer held since GetBuffer
|
||||
this->client->downmix.write_device_buffer(NumFramesWritten, dwFlags);
|
||||
device_buffer = this->client->downmix.current_buffer();
|
||||
} else {
|
||||
device_buffer = this->audio_buffer;
|
||||
|
||||
this->buffers_to_mute--;
|
||||
// mute the first few buffers to avoid a startup pop
|
||||
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
|
||||
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
|
||||
this->buffers_to_mute--;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->ReleaseBuffer(NumFramesWritten, dwFlags));
|
||||
// boost the final output volume just before it reaches the device, layout-agnostic
|
||||
if (hooks::audio::VOLUME_BOOST != 1.0f
|
||||
&& device_buffer != nullptr
|
||||
&& (dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
||||
static std::once_flag boost_printed;
|
||||
std::call_once(boost_printed, []() {
|
||||
log_info("audio::wasapi", "volume boost active: gain={}", hooks::audio::VOLUME_BOOST);
|
||||
});
|
||||
apply_gain(device_buffer, NumFramesWritten, this->client->device_format,
|
||||
hooks::audio::VOLUME_BOOST);
|
||||
}
|
||||
|
||||
HRESULT ret = pReal->ReleaseBuffer(NumFramesWritten, dwFlags);
|
||||
|
||||
if (this->client->downmix.enabled) {
|
||||
this->client->downmix.buffer_released();
|
||||
}
|
||||
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT(CLASS_NAME, __func__, ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
#include "downmix.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include <audioclient.h>
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float ATT_3DB = 0.70710678f;
|
||||
|
||||
// speakers routed to the left/right output; anything else (center) feeds both sides
|
||||
constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT
|
||||
| SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT;
|
||||
constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT
|
||||
| SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT;
|
||||
|
||||
// the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats
|
||||
DWORD read_channel_mask(const WAVEFORMATEX *fmt) {
|
||||
if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
|
||||
&& fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->dwChannelMask;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// call visit(channel_index, speaker_bit) for each present speaker, in channel order
|
||||
template <typename F>
|
||||
void for_each_speaker(DWORD mask, int channels, F &&visit) {
|
||||
int channel = 0;
|
||||
for (int bit = 0; bit < 18 && channel < channels; bit++) {
|
||||
const DWORD speaker = 1u << bit;
|
||||
if (mask & speaker) {
|
||||
visit(channel++, speaker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read one sample at `p` as a normalized float in [-1, 1]
|
||||
static inline float read_sample(const BYTE *p, int bytes, bool is_float) {
|
||||
if (is_float) {
|
||||
float v;
|
||||
memcpy(&v, p, sizeof(float));
|
||||
return v;
|
||||
}
|
||||
switch (bytes) {
|
||||
case 2: {
|
||||
int16_t v;
|
||||
memcpy(&v, p, sizeof(v));
|
||||
return v * (1.0f / 32768.0f);
|
||||
}
|
||||
case 3: {
|
||||
int32_t v = p[0] | (p[1] << 8) | (p[2] << 16);
|
||||
if (v & 0x800000) {
|
||||
v |= ~0xFFFFFF; // sign extend
|
||||
}
|
||||
return v * (1.0f / 8388608.0f);
|
||||
}
|
||||
case 4: {
|
||||
int32_t v;
|
||||
memcpy(&v, p, sizeof(v));
|
||||
return (float) (v * (1.0 / 2147483648.0));
|
||||
}
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// write the normalized float `value` to the sample at `p`, clamping to the format's range
|
||||
static inline void write_sample(BYTE *p, int bytes, bool is_float, float value) {
|
||||
if (is_float) {
|
||||
float v = std::clamp(value, -1.0f, 1.0f);
|
||||
memcpy(p, &v, sizeof(v));
|
||||
return;
|
||||
}
|
||||
switch (bytes) {
|
||||
case 2: {
|
||||
int16_t v = (int16_t) std::clamp(
|
||||
(int) std::lround(value * 32768.0f), -32768, 32767);
|
||||
memcpy(p, &v, sizeof(v));
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
int32_t v = (int32_t) std::clamp<int64_t>(
|
||||
std::llround((double) value * 8388608.0), -8388608, 8388607);
|
||||
p[0] = v & 0xFF;
|
||||
p[1] = (v >> 8) & 0xFF;
|
||||
p[2] = (v >> 16) & 0xFF;
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
int32_t v = (int32_t) std::clamp<int64_t>(
|
||||
std::llround((double) value * 2147483648.0), INT32_MIN, INT32_MAX);
|
||||
memcpy(p, &v, sizeof(v));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
|
||||
DownmixAlgorithm algorithm) {
|
||||
this->enabled = true;
|
||||
this->algorithm = algorithm;
|
||||
this->bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
this->game_frame_size = game_format->nChannels * this->bytes_per_sample;
|
||||
|
||||
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT has Data1 == 3 (matches apply_gain detection)
|
||||
this->is_float = game_format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|
||||
|| (game_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE
|
||||
&& reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(game_format)
|
||||
->SubFormat.Data1 == 0x00000003);
|
||||
|
||||
// supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence
|
||||
const bool supported = this->is_float
|
||||
? this->bytes_per_sample == 4
|
||||
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
|
||||
if (!supported) {
|
||||
log_fatal(
|
||||
"audio::downmix",
|
||||
"unsupported sample format ({}-bit {}), downmix will output silence",
|
||||
game_format->wBitsPerSample, this->is_float ? "float" : "int");
|
||||
}
|
||||
|
||||
this->left_mix.clear();
|
||||
this->right_mix.clear();
|
||||
this->build_layout_mix(game_format);
|
||||
|
||||
make_stereo_format(game_format, stereo_out);
|
||||
}
|
||||
|
||||
void Downmix::make_stereo_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *stereo_out) {
|
||||
const int bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
|
||||
memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE));
|
||||
stereo_out->Format.nChannels = 2;
|
||||
stereo_out->Format.nBlockAlign = 2 * bytes_per_sample;
|
||||
stereo_out->Format.nAvgBytesPerSec =
|
||||
game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign;
|
||||
stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
|
||||
}
|
||||
|
||||
HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
|
||||
|
||||
HRESULT ret = real->Initialize(share_mode, stream_flags, buffer_duration, periodicity,
|
||||
device_format, session_guid);
|
||||
|
||||
// the smaller stereo buffer can end up unaligned for the device when the game sized the
|
||||
// duration for its larger multi-channel format. recover by asking the device for the next
|
||||
// aligned buffer size and re-initializing with a matching duration.
|
||||
if (ret == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
|
||||
UINT32 aligned_frames = 0;
|
||||
if (SUCCEEDED(real->GetBufferSize(&aligned_frames)) && aligned_frames > 0) {
|
||||
REFERENCE_TIME aligned_duration = (REFERENCE_TIME)
|
||||
(10000.0 * 1000 / device_format->nSamplesPerSec * aligned_frames + 0.5);
|
||||
|
||||
log_info("audio::downmix",
|
||||
"buffer not aligned, retrying with {} frames ({} hns)",
|
||||
aligned_frames, aligned_duration);
|
||||
|
||||
ret = real->Initialize(share_mode, stream_flags, aligned_duration,
|
||||
periodicity != 0 ? aligned_duration : 0, device_format, session_guid);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Downmix::add_channel(int channel, DWORD speaker, float gain) {
|
||||
if (speaker & LEFT_SPEAKERS) {
|
||||
this->left_mix.push_back({ channel, gain });
|
||||
} else if (speaker & RIGHT_SPEAKERS) {
|
||||
this->right_mix.push_back({ channel, gain });
|
||||
} else { // center: feed both sides
|
||||
this->left_mix.push_back({ channel, gain });
|
||||
this->right_mix.push_back({ channel, gain });
|
||||
}
|
||||
}
|
||||
|
||||
// AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped
|
||||
void Downmix::build_ac4_mix(DWORD mask, int channels) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker == SPEAKER_LOW_FREQUENCY) {
|
||||
return;
|
||||
}
|
||||
const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
|
||||
this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB);
|
||||
});
|
||||
}
|
||||
|
||||
// keep only the channels in `keep` (front/rear/side), each at unity gain
|
||||
void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker & keep) {
|
||||
this->add_channel(ch, speaker, 1.0f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// keep every channel (LFE dropped), then average each side so its gains sum to unity
|
||||
void Downmix::build_normalize_mix(DWORD mask, int channels) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker != SPEAKER_LOW_FREQUENCY) {
|
||||
this->add_channel(ch, speaker, 1.0f);
|
||||
}
|
||||
});
|
||||
|
||||
for (auto *mix : { &this->left_mix, &this->right_mix }) {
|
||||
if (!mix->empty()) {
|
||||
const float gain = 1.0f / mix->size();
|
||||
for (auto &c : *mix) {
|
||||
c.gain = gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right)
|
||||
void Downmix::build_pairs_mix(int channels, float gain) {
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
(((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain });
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) {
|
||||
const int channels = game_format->nChannels;
|
||||
const DWORD mask = read_channel_mask(game_format);
|
||||
|
||||
// without a mask the layout is unknown: extract/normalize have nothing to act on, so all
|
||||
// algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB)
|
||||
if (mask == 0) {
|
||||
this->build_pairs_mix(channels,
|
||||
this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this->algorithm) {
|
||||
case DownmixAlgorithm::FrontOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
|
||||
break;
|
||||
case DownmixAlgorithm::RearOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER);
|
||||
break;
|
||||
case DownmixAlgorithm::SideOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT);
|
||||
break;
|
||||
case DownmixAlgorithm::Normalize:
|
||||
this->build_normalize_mix(mask, channels);
|
||||
break;
|
||||
case DownmixAlgorithm::AC4:
|
||||
this->build_ac4_mix(mask, channels);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int src_stride = this->game_frame_size;
|
||||
const int dst_stride = 2 * bps;
|
||||
|
||||
if (dst == nullptr || src == nullptr || bps <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// sum each speaker's source channels into the matching stereo output
|
||||
for (UINT32 i = 0; i < frames; i++) {
|
||||
const BYTE *in = src + (size_t) i * src_stride;
|
||||
BYTE *out = dst + (size_t) i * dst_stride;
|
||||
|
||||
float left = 0.0f;
|
||||
float right = 0.0f;
|
||||
for (const auto &c : this->left_mix) {
|
||||
left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
|
||||
}
|
||||
for (const auto &c : this->right_mix) {
|
||||
right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
|
||||
}
|
||||
write_sample(out, bps, this->is_float, left);
|
||||
write_sample(out + bps, bps, this->is_float, right);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
HRESULT ret = real->GetBuffer(frames, &this->device_buffer);
|
||||
if (FAILED(ret)) {
|
||||
this->device_buffer = nullptr;
|
||||
return ret;
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void Downmix::write_device_buffer(UINT32 frames, DWORD flags) {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int dst_stride = 2 * bps;
|
||||
|
||||
if (this->device_buffer == nullptr || frames == 0 || bps <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
if (this->buffers_to_mute > 0) {
|
||||
memset(this->device_buffer, 0, (size_t) frames * dst_stride);
|
||||
this->buffers_to_mute--;
|
||||
} else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
||||
this->process(this->device_buffer, this->scratch.data(), frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
|
||||
struct IAudioClient;
|
||||
struct IAudioRenderClient;
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the
|
||||
// game keeps writing its native multi-channel audio into a scratch buffer; on release that
|
||||
// buffer is mixed down into the two front channels.
|
||||
//
|
||||
// The mix is derived from the source format's speaker mask according to the selected
|
||||
// DownmixAlgorithm:
|
||||
// FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side
|
||||
// AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right
|
||||
// pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped
|
||||
// Normalize - every channel folded in (center to both sides) with each output side averaged
|
||||
// so its channels are equally loud, LFE dropped
|
||||
struct Downmix {
|
||||
|
||||
// a source channel routed into one output speaker at the given gain
|
||||
struct Contribution {
|
||||
int channel;
|
||||
float gain;
|
||||
};
|
||||
|
||||
// map an option value (front/rear/side/ac4/normalize) to its algorithm.
|
||||
static std::optional<DownmixAlgorithm> name_to_algorithm(const char *value) {
|
||||
if (_stricmp(value, "front") == 0) {
|
||||
return DownmixAlgorithm::FrontOnly;
|
||||
} else if (_stricmp(value, "rear") == 0) {
|
||||
return DownmixAlgorithm::RearOnly;
|
||||
} else if (_stricmp(value, "side") == 0) {
|
||||
return DownmixAlgorithm::SideOnly;
|
||||
} else if (_stricmp(value, "ac4") == 0) {
|
||||
return DownmixAlgorithm::AC4;
|
||||
} else if (_stricmp(value, "normalize") == 0) {
|
||||
return DownmixAlgorithm::Normalize;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// human-readable name of an algorithm, for logging.
|
||||
static const char *algorithm_name(DownmixAlgorithm algorithm) {
|
||||
switch (algorithm) {
|
||||
case DownmixAlgorithm::FrontOnly: return "front";
|
||||
case DownmixAlgorithm::RearOnly: return "rear";
|
||||
case DownmixAlgorithm::SideOnly: return "side";
|
||||
case DownmixAlgorithm::AC4: return "ac4";
|
||||
case DownmixAlgorithm::Normalize: return "normalize";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// whether the downmix is active for the current stream
|
||||
bool enabled = false;
|
||||
|
||||
// algorithm used to fold the multi-channel audio into stereo
|
||||
DownmixAlgorithm algorithm = DownmixAlgorithm::AC4;
|
||||
|
||||
// size in bytes of one frame of the game's multi-channel format
|
||||
int game_frame_size = 0;
|
||||
|
||||
// size in bytes of a single sample (per channel)
|
||||
int bytes_per_sample = 0;
|
||||
|
||||
// whether samples are IEEE floating point rather than integer PCM
|
||||
bool is_float = false;
|
||||
|
||||
// enable the downmix for the given game format and fill stereo_out with the equivalent
|
||||
// stereo format to open the real device with.
|
||||
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
|
||||
DownmixAlgorithm algorithm);
|
||||
|
||||
// build the stereo format equivalent to game_format (same sample rate and bit depth).
|
||||
static void make_stereo_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *stereo_out);
|
||||
|
||||
// initialize the real device with the stereo format. downmixing reduces the channel count,
|
||||
// shrinking the buffer's byte size, so the duration the game sized for its multi-channel
|
||||
// format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED
|
||||
// this performs the standard WASAPI realignment and retries.
|
||||
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid);
|
||||
|
||||
// mix `frames` frames of multi-channel `src` down into stereo `dst`.
|
||||
void process(BYTE *dst, const BYTE *src, UINT32 frames) const;
|
||||
|
||||
// grab the real stereo device buffer and hand the game the scratch buffer to write into.
|
||||
HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData);
|
||||
|
||||
// mix the scratch buffer into the stereo device buffer held since get_buffer. the caller
|
||||
// owns releasing the device buffer afterwards (see current_buffer / buffer_released).
|
||||
void write_device_buffer(UINT32 frames, DWORD flags);
|
||||
|
||||
// the real device buffer currently held, or null.
|
||||
BYTE *current_buffer() const { return this->device_buffer; }
|
||||
|
||||
// forget the held device buffer once the caller has released it.
|
||||
void buffer_released() { this->device_buffer = nullptr; }
|
||||
|
||||
private:
|
||||
|
||||
// build the mix from the source speaker layout for the selected algorithm
|
||||
void build_layout_mix(const WAVEFORMATEX *game_format);
|
||||
|
||||
// per-algorithm builders, each filling left_mix / right_mix from the speaker mask
|
||||
void build_ac4_mix(DWORD mask, int channels);
|
||||
void build_extract_mix(DWORD mask, int channels, DWORD keep);
|
||||
void build_normalize_mix(DWORD mask, int channels);
|
||||
|
||||
// fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain`
|
||||
void build_pairs_mix(int channels, float gain);
|
||||
|
||||
// append one source channel to the output side(s) matching its speaker, at `gain`
|
||||
void add_channel(int channel, DWORD speaker, float gain);
|
||||
|
||||
// source channels summed into each output speaker
|
||||
std::vector<Contribution> left_mix;
|
||||
std::vector<Contribution> right_mix;
|
||||
|
||||
// buffer the game writes its multi-channel audio into between get/release
|
||||
std::vector<BYTE> scratch;
|
||||
|
||||
// the real stereo device buffer currently held, or null
|
||||
BYTE *device_buffer = nullptr;
|
||||
|
||||
// leading buffers to silence to avoid a pop on stream start
|
||||
int buffers_to_mute = 16;
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <assert.h>
|
||||
#include <shlwapi.h>
|
||||
#include <windows.h>
|
||||
@@ -70,6 +72,7 @@
|
||||
#include "games/museca/museca.h"
|
||||
#include "hooks/avshook.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/backends/wasapi/downmix.h"
|
||||
#include "hooks/debughook.h"
|
||||
#include "hooks/devicehook.h"
|
||||
#include "hooks/graphics/nvenc_hook.h"
|
||||
@@ -1092,6 +1095,17 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::spice2x_DisableVolumeHook].value_bool()) {
|
||||
hooks::audio::VOLUME_HOOK_ENABLED = false;
|
||||
}
|
||||
if (options[launcher::Options::DownmixAudioToStereo].is_active()) {
|
||||
auto &name = options[launcher::Options::DownmixAudioToStereo].value_text();
|
||||
hooks::audio::DOWNMIX_ALGORITHM = hooks::audio::Downmix::name_to_algorithm(name.c_str());
|
||||
}
|
||||
if (options[launcher::Options::VolumeBoost].is_active()) {
|
||||
const double decibels = std::strtod(
|
||||
options[launcher::Options::VolumeBoost].value_text().c_str(), nullptr);
|
||||
if (decibels > 0.0) {
|
||||
hooks::audio::VOLUME_BOOST = (float) std::pow(10.0, decibels / 20.0);
|
||||
}
|
||||
}
|
||||
|
||||
if (options[launcher::Options::AudioBackend].is_active()) {
|
||||
auto &name = options[launcher::Options::AudioBackend].value_text();
|
||||
|
||||
@@ -1075,10 +1075,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.category = "Game Options (Advanced)",
|
||||
},
|
||||
{
|
||||
.title = "GitaDora Two Channel Audio (DX/SD only)",
|
||||
.title = "GitaDora Two Channel Audio",
|
||||
.name = "2ch",
|
||||
.desc = "Attempt to reduce audio channels down to just two channels. "
|
||||
"This option does nothing for Arena Model (GW Delta and above); need a patch for that.",
|
||||
.desc = "Attempt to reduce audio channels down to just two channels.\n\n"
|
||||
"Arena Model: downmixes 7.1 to stereo using the AC-4 stereo downmix coefficients "
|
||||
"(ETSI TS 103 190-1). The WASAPI Stereo Downmix option, if set, overrides this "
|
||||
"algorithm.",
|
||||
.type = OptionType::Bool,
|
||||
.game_name = "GitaDora",
|
||||
.category = "Game Options",
|
||||
@@ -1939,6 +1941,49 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.hidden = true,
|
||||
.category = "Audio (Hacks)",
|
||||
},
|
||||
{
|
||||
// DownmixAudioToStereo
|
||||
.title = "WASAPI Stereo Downmix",
|
||||
.name = "downmix",
|
||||
.desc = "Downmixes multi-channel (surround) audio output to stereo.\n\n"
|
||||
"Note that in all of the options, subwoofer (LFE) is dropped.\n\n"
|
||||
"ac4 (recommended): AC-4 algorithm (ETSI TS 103 190-1) - front left/right pass "
|
||||
"through, center and surrounds fold in at -3 dB.\n\n"
|
||||
"normalize: all channels equally loud.\n\n"
|
||||
"front: Front channels only.\n\n"
|
||||
"rear: Rear channels only.\n\n"
|
||||
"side: Side channels only.",
|
||||
.type = OptionType::Enum,
|
||||
.category = "Audio",
|
||||
.elements = {
|
||||
{"ac4", "AC-4 (ETSI TS 103 190-1)"},
|
||||
{"normalize", "Normalize (drop LFE)"},
|
||||
{"front", "Front channels only"},
|
||||
{"rear", "Rear channels only"},
|
||||
{"side", "Side channels only"},
|
||||
},
|
||||
},
|
||||
{
|
||||
// VolumeBoost
|
||||
.title = "WASAPI Boost Audio Volume",
|
||||
.name = "volumeboost",
|
||||
.desc = "Artificially amplifies the hooked audio output by the selected amount, applied "
|
||||
"right before the audio reaches the device. Works regardless of channel layout or "
|
||||
"downmixing.\n\n"
|
||||
"Louder settings may cause clipping/distortion, use caution.",
|
||||
.type = OptionType::Enum,
|
||||
.category = "Audio",
|
||||
.elements = {
|
||||
{"3", "+3 dB"},
|
||||
{"6", "+6 dB"},
|
||||
{"9", "+9 dB"},
|
||||
{"12", "+12 dB"},
|
||||
{"15", "+15 dB"},
|
||||
{"20", "+20 dB"},
|
||||
{"25", "+25 dB"},
|
||||
{"30", "+30 dB"},
|
||||
},
|
||||
},
|
||||
{
|
||||
// DelayBy5Seconds
|
||||
.title = "Delay by 5 Seconds (DEPRECATED - use -sleepduration instead)",
|
||||
|
||||
@@ -199,6 +199,8 @@ namespace launcher {
|
||||
AsioDriverId,
|
||||
AsioDriverName,
|
||||
AudioDummy,
|
||||
DownmixAudioToStereo,
|
||||
VolumeBoost,
|
||||
DelayBy5Seconds,
|
||||
spice2x_DelayByNSeconds,
|
||||
LoadStubs,
|
||||
|
||||
Reference in New Issue
Block a user