From edcb631442ba2d4bec56506117b21ba037671fc4 Mon Sep 17 00:00:00 2001 From: Dipshet <264011288+Dipshet@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:25:30 +0200 Subject: [PATCH] Fix doubled cutscene dialogue: fold only the fronts while a cutscene plays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AC6's cutscene mixer submits its 5.1 premix to all six speaker slots as decorrelated near-copies of one mix; six real speakers separate them acoustically, but the port's 6→2 fold sums them, combing speech into the long-reported doubled dialogue. While the demo sequencer ticks, fold only the fronts (verified lossless). audio_cutscene_downmix (default on). --- src/ac6_backend_fixes/ac6_cutscene_resync.cpp | 7 ++ .../include/native/audio/conversion.h | 111 +++++++++++++++++- .../src/native/audio/audio_system.cpp | 41 +++++++ 3 files changed, 155 insertions(+), 4 deletions(-) diff --git a/src/ac6_backend_fixes/ac6_cutscene_resync.cpp b/src/ac6_backend_fixes/ac6_cutscene_resync.cpp index f05607a9..ed773a68 100644 --- a/src/ac6_backend_fixes/ac6_cutscene_resync.cpp +++ b/src/ac6_backend_fixes/ac6_cutscene_resync.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -121,6 +122,12 @@ void ExecWithResync(PPCContext& ctx, uint8_t* base, } g_session.last_exec_ms = now_ms; + // Stamp the cinematic-audio gate for the stereo fold-down (conversion.h): + // the demo wrappers tick only while an in-engine cutscene plays, so their + // freshness is the "cutscene audio active" signal. Unconditional - the + // stamp is independent of whether the resync behavior itself is enabled. + rex::audio::NotifyCinematicAudioTick(now_ms); + uint64_t audio_samples = 0; const bool clock_ok = ReadAudioClockSamples(ctx, &audio_samples); if (clock_ok && !g_session.clock_valid) { diff --git a/thirdparty/rexglue-sdk/include/native/audio/conversion.h b/thirdparty/rexglue-sdk/include/native/audio/conversion.h index 46c828cc..7a91375a 100644 --- a/thirdparty/rexglue-sdk/include/native/audio/conversion.h +++ b/thirdparty/rexglue-sdk/include/native/audio/conversion.h @@ -3,13 +3,40 @@ #pragma once +#include +#include +#include #include #include #include +#include #include #include +REXCVAR_DECLARE(bool, audio_cutscene_downmix); +REXCVAR_DECLARE(double, audio_downmix_center_gain); +REXCVAR_DECLARE(double, audio_downmix_surround_gain); +REXCVAR_DECLARE(double, audio_downmix_lfe_gain); +REXCVAR_DECLARE(double, audio_downmix_cutscene_center_gain); +REXCVAR_DECLARE(double, audio_downmix_cutscene_surround_gain); +REXCVAR_DECLARE(double, audio_downmix_cutscene_lfe_gain); +REXCVAR_DECLARE(double, audio_downmix_cutscene_trim); +REXCVAR_DECLARE(double, audio_downmix_cutscene_ramp_ms); + +namespace rex::audio { + +// Wall-clock ms of the last in-engine cutscene sequencer tick, stamped by the +// demo-tick hook (ac6_cutscene_resync). Lets the stereo fold-down apply +// cutscene-specific gains without reaching into game code. INT64_MIN = never. +inline std::atomic g_last_cinematic_audio_tick_ms{INT64_MIN}; + +inline void NotifyCinematicAudioTick(int64_t now_ms) { + g_last_cinematic_audio_tick_ms.store(now_ms, std::memory_order_relaxed); +} + +} // namespace rex::audio + namespace rex::audio::conversion { inline constexpr float kStereoDownmixCenterGain = 0.70710678f; @@ -20,6 +47,81 @@ inline constexpr float kStereoDownmixNormalize = 1.0f / (1.0f + kStereoDownmixCenterGain + kStereoDownmixSurroundGain + kStereoDownmixLfeGain); +// Live fold-down gains for the path AC6 actually plays through (the AMD64 +// planar fold below; the other variants keep the compile-time constants). +// The base gains default to those constants; the cutscene set applies while +// the demo sequencer is ticking, because AC6's cutscene mixer submits its premix +// spread across ALL six speaker slots as decorrelated near-copies of one mix +// (measured: equal RMS on every channel, inter-channel correlation 0.69-0.94 +// at exactly lag 0, identical structure across scenes). Real speakers +// separate the copies acoustically; an electrical 6-to-2 sum combs them - +// heard as doubled dialogue. The fronts alone carry the complete mix, so the +// cutscene defaults fold only the fronts. Gameplay audio (discrete channels) +// is bit-identical to the old constants. +struct StereoDownmixGains { + float center; + float surround; + float lfe; + float normalize; +}; + +inline StereoDownmixGains GetStereoDownmixGains() { + // The cutscene gains engage while demo-wrapper ticks are fresh; f slews + // over ramp_ms so fold changes never step. Known cosmetic (accepted): the + // wrapper keeps ticking through the gallery's menu->scene transitions, so + // the gallery's front-weighted transition SFX plays through the + // fronts-only fold hot; campaign flows are unaffected. + const int64_t now_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + const int64_t last_tick = + g_last_cinematic_audio_tick_ms.load(std::memory_order_relaxed); + const bool engaged = REXCVAR_GET(audio_cutscene_downmix) && + last_tick != INT64_MIN && (now_ms - last_tick) <= 250; + static float f_state = 0.0f; // 0 = base fold, 1 = full cutscene gains + static int64_t f_last_ms = INT64_MIN; + const double ramp = std::max(1.0, REXCVAR_GET(audio_downmix_cutscene_ramp_ms)); + float step = 1.0f; + if (f_last_ms != INT64_MIN && now_ms >= f_last_ms) { + step = float(std::min(1.0, double(now_ms - f_last_ms) / ramp)); + } + f_last_ms = now_ms; + const float target = engaged ? 1.0f : 0.0f; + if (target > f_state) { + f_state = std::min(target, f_state + step); + } else { + f_state = std::max(target, f_state - step); + } + const float f = f_state; + auto clamp_gain = [](double v) { + return std::min(2.0f, std::max(0.0f, float(v))); + }; + auto blend = [&](double cutscene_value, double base_value) { + const float base = clamp_gain(base_value); + const float cut = + cutscene_value >= 0.0 ? clamp_gain(cutscene_value) : base; + return base + (cut - base) * f; + }; + StereoDownmixGains gains; + gains.center = blend(REXCVAR_GET(audio_downmix_cutscene_center_gain), + REXCVAR_GET(audio_downmix_center_gain)); + gains.surround = blend(REXCVAR_GET(audio_downmix_cutscene_surround_gain), + REXCVAR_GET(audio_downmix_surround_gain)); + gains.lfe = blend(REXCVAR_GET(audio_downmix_cutscene_lfe_gain), + REXCVAR_GET(audio_downmix_lfe_gain)); + // Normalization follows the live gains so loudness stays consistent at any + // setting; at the stock base gains this equals the old fixed constant, so + // gameplay output is bit-identical. The near-copy cutscene mixes are + // self-correcting under it (fold of N unity-ish copies divided by the gain + // sum lands at the same level whichever channels fold); the measured + // residual vs the old fold is +0.6 dB, cancelled by the default trim. + gains.normalize = 1.0f / (1.0f + gains.center + gains.surround + gains.lfe); + const float trim = std::min( + 2.0f, std::max(0.0f, float(REXCVAR_GET(audio_downmix_cutscene_trim)))); + gains.normalize *= 1.0f + (trim - 1.0f) * f; + return gains; +} + inline float SanitizeGuestAudioSample(float sample) { if (!std::isfinite(sample)) { return 0.0f; @@ -70,10 +172,11 @@ inline void sequential_6_BE_to_interleaved_2_LE(float* output, const float* inpu const __m128i byte_swap_shuffle = _mm_set_epi8(12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3); - const __m128 center_gain = _mm_set1_ps(kStereoDownmixCenterGain); - const __m128 surround_gain = _mm_set1_ps(kStereoDownmixSurroundGain); - const __m128 lfe_gain = _mm_set1_ps(kStereoDownmixLfeGain); - const __m128 normalize = _mm_set1_ps(kStereoDownmixNormalize); + const StereoDownmixGains live_gains = GetStereoDownmixGains(); + const __m128 center_gain = _mm_set1_ps(live_gains.center); + const __m128 surround_gain = _mm_set1_ps(live_gains.surround); + const __m128 lfe_gain = _mm_set1_ps(live_gains.lfe); + const __m128 normalize = _mm_set1_ps(live_gains.normalize); const __m128 peak_headroom = _mm_set1_ps(kStereoDownmixPeakHeadroom); const __m128 sign_mask = _mm_set1_ps(-0.0f); diff --git a/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp b/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp index 5b0127ae..df138624 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp @@ -19,6 +19,47 @@ REXCVAR_DEFINE_BOOL(audio_trace_render_driver_verbose, false, "Audio", "Trace render-driver activity"); REXCVAR_DEFINE_BOOL(audio_deep_trace, false, "Audio", "Enable verbose runtime audio tracing"); +REXCVAR_DEFINE_BOOL(audio_cutscene_downmix, true, "Audio", + "Master switch for the cutscene-specific stereo fold-down " + "(fronts-only fold while the demo sequencer is active - " + "removes the doubled/combed dialogue caused by summing the " + "cutscene mixer's six decorrelated near-copies to stereo). " + "false = the original summing fold everywhere; the " + "audio_downmix_cutscene_* cvars then have no effect. " + "Gameplay audio is identical either way."); +REXCVAR_DEFINE_DOUBLE(audio_downmix_center_gain, 0.70710678, "Audio", + "Front-center gain in the 6ch-to-stereo fold-down (was a " + "compile-time constant; default unchanged). Normalization " + "follows the live gains. Read continuously."); +REXCVAR_DEFINE_DOUBLE(audio_downmix_surround_gain, 0.5, "Audio", + "Rear-channel gain in the 6ch-to-stereo fold-down. See " + "audio_downmix_center_gain."); +REXCVAR_DEFINE_DOUBLE(audio_downmix_lfe_gain, 0.0, "Audio", + "LFE gain in the 6ch-to-stereo fold-down. See " + "audio_downmix_center_gain."); +REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_center_gain, 0.0, "Audio", + "Front-center fold-down gain used ONLY while an in-engine " + "cutscene is playing (demo sequencer active); negative = " + "inherit audio_downmix_center_gain. AC6's cutscene mixer " + "spreads the premix across ALL six speaker slots as " + "decorrelated near-copies of one mix; the fronts alone " + "carry the complete mix, so the cutscene fold takes only " + "them - center and surround default to 0 here."); +REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_surround_gain, 0.0, "Audio", + "Rear-channel fold-down gain during in-engine cutscenes; " + "negative = inherit. See audio_downmix_cutscene_center_gain."); +REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_lfe_gain, -1.0, "Audio", + "LFE fold-down gain during in-engine cutscenes; negative = " + "inherit. See audio_downmix_cutscene_center_gain."); +REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_trim, 0.933, "Audio", + "Extra output gain on the cutscene fold (ramped in with " + "the cutscene gains). Default 0.933 = the measured RMS " + "ratio of the fronts-only fold vs the stock summing fold " + "(-0.6 dB), so cutscene loudness matches the original " + "baseline; 1.0 = no trim."); +REXCVAR_DEFINE_DOUBLE(audio_downmix_cutscene_ramp_ms, 250.0, "Audio", + "Slew time between the base fold and the cutscene gains " + "when the demo-sequencer signal engages or releases."); REXCVAR_DEFINE_BOOL(audio_xma_loop_guard, true, "Audio", "Require a real loop window (loop_start < loop_end, " "read_offset >= loop_end) before engaging XMA loop "