mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-21 23:00:53 -04:00
Fix doubled cutscene dialogue: fold only the fronts while a cutscene plays
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).
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
|
||||
#include <native/audio/audio_client.h>
|
||||
#include <native/audio/audio_system.h>
|
||||
#include <native/audio/conversion.h>
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/ppc.h>
|
||||
@@ -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) {
|
||||
|
||||
+107
-4
@@ -3,13 +3,40 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include <native/audio/render_driver_frame_layout.h>
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/platform.h>
|
||||
#include <rex/types.h>
|
||||
|
||||
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<int64_t> 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::milliseconds>(
|
||||
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);
|
||||
|
||||
|
||||
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user