audio: expose raw buffer target in launcher

This commit is contained in:
Matthew Stanley
2026-07-28 20:50:14 -07:00
parent 3c67a52f95
commit 47ff7fb232
6 changed files with 77 additions and 20 deletions
+8 -5
View File
@@ -321,13 +321,16 @@ audio production cadence:
```toml
[audio]
buffer_ms = 60
offer_buffer_ms = true
```
`buffer_ms` accepts 30–500 milliseconds and defaults to 180. Lower values
reduce audible input-to-sound delay, but leave less reserve for frames where a
game temporarily produces no audio and can therefore crackle on affected
titles. This is deliberately a per-game developer choice; it is not read from
the player's `settings.toml`.
`buffer_ms` defaults to 180. Lower values reduce audible input-to-sound delay,
but leave less reserve for frames where a game temporarily produces no audio
and can therefore crackle on affected titles. `offer_buffer_ms` defaults to
false. When a game explicitly enables it, recomp-ui shows a raw, unconstrained
Audio latency debug field and persists the player's choice as
`[audio] buffer_ms` in `settings.toml`. Games that do not opt in keep their
validated developer value and show no control.
## Video Block
+10 -4
View File
@@ -551,12 +551,12 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
const toml::value& audio = toml::find(cfg, "audio");
if (audio.contains("buffer_ms")) {
const auto n = toml::find<int64_t>(audio, "buffer_ms");
if (n < 30 || n > 500) {
throw std::runtime_error(fmt::format(
"[audio] buffer_ms out of range (30..500): {}", n));
}
rt.audio_buffer_ms = static_cast<int>(n);
}
if (audio.contains("offer_buffer_ms")) {
rt.audio_offer_buffer_ms =
toml::find<bool>(audio, "offer_buffer_ms");
}
if (audio.contains("spu_hq")) {
rt.audio_spu_hq = toml::find<bool>(audio, "spu_hq");
}
@@ -2012,6 +2012,10 @@ UserSettings load_user_settings(const fs::path& path) {
}
if (doc.contains("audio")) {
const toml::value& a = toml::find(doc, "audio");
if (a.contains("buffer_ms")) try_get([&]{
const int n = toml::find<int>(a, "buffer_ms");
s.audio_buffer_ms = n; s.has_audio_buffer_ms = true;
});
if (a.contains("spu_hq")) try_get([&]{
s.spu_hq = toml::find<bool>(a, "spu_hq"); s.has_spu_hq = true;
});
@@ -2174,6 +2178,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
if (s.has_adaptive_view)
f << "adaptive_view = " << (s.adaptive_view ? "true" : "false") << "\n";
f << "\n[audio]\n";
if (s.has_audio_buffer_ms)
f << "buffer_ms = " << s.audio_buffer_ms << "\n";
if (s.has_spu_hq)
f << "spu_hq = " << (s.spu_hq ? "true" : "false") << "\n";
if (s.has_skip_launcher)
+8 -2
View File
@@ -401,9 +401,14 @@ struct RuntimeConfig {
// buffer_ms: steady-state host playback cushion. The ecosystem default
// remains 180 ms because it survives long streamed-stage production gaps;
// games with smoother production cadence may opt into a lower value to
// reduce controller-to-sound latency. This is a game-developer setting,
// not a player preference.
// reduce controller-to-sound latency.
// When exposed through offer_buffer_ms this is intentionally a raw debug
// value: recomp-ui does not clamp or quantize it.
int audio_buffer_ms = 180;
// offer_buffer_ms: expose the buffer target in recomp-ui for this game.
// Hidden by default because many games need their validated developer
// value to survive irregular audio-production stalls.
bool audio_offer_buffer_ms = false;
// spu_hq: enable the SPU float-shadow re-render (Catmull-Rom resample, float
// headroom). Verified-enhancement, default OFF — spu_render output is
@@ -980,6 +985,7 @@ struct UserSettings {
bool has_adaptive_view = false; bool adaptive_view = false;
// [audio]
bool has_spu_hq = false; bool spu_hq = false;
bool has_audio_buffer_ms = false; int audio_buffer_ms = 180;
// [bios] / [disc] / [memcard]
bool has_bios_path = false; std::filesystem::path bios_path;
bool has_disc_path = false; std::filesystem::path disc_path;
+31 -8
View File
@@ -137,21 +137,44 @@ note = "Test-only fixture"
[audio]
buffer_ms = 60
offer_buffer_ms = true
)toml");
const auto audio_config = PSXRecompV4::load_game_config(audio_buffer);
check(audio_config.runtime.audio_buffer_ms == 60,
"parser preserves per-game audio buffer target");
check(audio_config.runtime.audio_buffer_ms == 60 &&
audio_config.runtime.audio_offer_buffer_ms,
"parser preserves per-game audio buffer target and UI opt-in");
const auto bad_audio_buffer = write_config(root, "bad-audio-buffer", R"toml(
const auto audio_settings = write_config(root, "audio-settings", R"toml(
[audio]
buffer_ms = 30
)toml");
const auto user_audio =
PSXRecompV4::load_user_settings(audio_settings);
check(user_audio.has_audio_buffer_ms && user_audio.audio_buffer_ms == 30,
"user settings preserve an opted-in audio buffer choice");
PSXRecompV4::UserSettings raw_user_audio;
raw_user_audio.has_audio_buffer_ms = true;
raw_user_audio.audio_buffer_ms = -7;
const auto raw_audio_settings = root / "raw-audio-settings.toml";
check(PSXRecompV4::save_user_settings(raw_audio_settings, raw_user_audio),
"user settings write a raw audio buffer choice");
const auto raw_audio_roundtrip =
PSXRecompV4::load_user_settings(raw_audio_settings);
check(raw_audio_roundtrip.has_audio_buffer_ms &&
raw_audio_roundtrip.audio_buffer_ms == -7,
"user settings round-trip raw audio buffer choices without clamping");
const auto raw_audio_buffer = write_config(root, "raw-audio-buffer", R"toml(
[runtime]
[audio]
buffer_ms = 20
buffer_ms = 1
)toml");
check_throws(
[&] { (void)PSXRecompV4::load_game_config(bad_audio_buffer); },
"[audio] buffer_ms out of range (30..500)",
"parser rejects unsafe audio buffer target");
const auto raw_audio_config =
PSXRecompV4::load_game_config(raw_audio_buffer);
check(raw_audio_config.runtime.audio_buffer_ms == 1,
"parser leaves debug audio buffer targets unconstrained");
const auto duplicate_address = write_config(root, "duplicate-address", R"toml(
[[recompiler.patch]]
+14
View File
@@ -4931,6 +4931,7 @@ int main(int argc, char** argv) {
bool ws_adaptive_view_supported = false;
bool frame_interpolation_offered = true;
bool skip_fmv_offered = true;
bool audio_buffer_offered = false;
bool vulkan_offered = false; /* game.toml [video] offer_vulkan; developer opt-in for launcher visibility */
int resolved_deadzone = -1; /* <0 => keep input.ini/runtime default (12000) */
/* Localization: the effective language (game.toml default -> settings.toml ->
@@ -5195,6 +5196,7 @@ int main(int argc, char** argv) {
(int)gc.ws_backdrop_x_sites.size());
g_audio_spu_hq = gc.runtime.audio_spu_hq;
g_audio_buffer_ms = (double)gc.runtime.audio_buffer_ms;
audio_buffer_offered = gc.runtime.audio_offer_buffer_ms;
g_auto_skip_fmv = gc.runtime.video_auto_skip_fmv ? 1 : 0;
/* [controller] game-declared input defaults (settings.toml/launcher
* still override below). */
@@ -5514,6 +5516,8 @@ int main(int argc, char** argv) {
}
if (us.has_adaptive_view) g_ws_adaptive_view = us.adaptive_view;
if (us.has_spu_hq) g_audio_spu_hq = us.spu_hq;
if (audio_buffer_offered && us.has_audio_buffer_ms)
g_audio_buffer_ms = (double)us.audio_buffer_ms;
/* Bundled BIOS: ignore any persisted bios_path (same clamp-as-well-
* as-hide treatment as lock_mode / ws_offered below) so a stale
* settings.toml or launcher-less build can never point a bundled
@@ -5775,6 +5779,8 @@ int main(int argc, char** argv) {
seed.adaptive_view = g_ws_adaptive_view;
seed.has_adaptive_view = ws_offered;
seed.spu_hq = g_audio_spu_hq; seed.has_spu_hq = true;
seed.audio_buffer_ms = (int)g_audio_buffer_ms;
seed.has_audio_buffer_ms = audio_buffer_offered;
seed.skip_launcher = skip_launcher_setting; seed.has_skip_launcher = true;
if (has_netplay_player_name) {
seed.netplay_player_name = netplay_player_name;
@@ -5865,6 +5871,7 @@ int main(int argc, char** argv) {
ls.frame_interp = seed.frame_interpolation ? 1 : 0;
ls.frame_interp_fps = seed.frame_interpolation_fps;
ls.spu_hq = seed.spu_hq ? 1 : 0;
ls.audio_buffer_ms = seed.audio_buffer_ms;
ls.auto_skip_fmv = seed.auto_skip_fmv ? 1 : 0;
ls.turbo_loads = seed.turbo_loads ? 1 : 0;
/* Localization: index of resolved_language within lang_menu_options
@@ -5976,6 +5983,7 @@ int main(int argc, char** argv) {
gi.renderer_labels = kPsxRendererLabels;
gi.num_renderers = vulkan_offered ? 3 : 2;
gi.has_skip_fmv = skip_fmv_offered ? 1 : 0;
gi.has_audio_buffer_ms = audio_buffer_offered ? 1 : 0;
/* Localization menu: shown only when the game declares languages. */
if (!rui_lang_labels.empty()) {
gi.language_labels = rui_lang_labels.data();
@@ -6077,6 +6085,10 @@ int main(int argc, char** argv) {
seed.frame_interpolation_fps = ls.frame_interp_fps;
seed.has_frame_interpolation_fps = frame_interpolation_offered;
seed.spu_hq = ls.spu_hq != 0; seed.has_spu_hq = true;
if (audio_buffer_offered) {
seed.audio_buffer_ms = ls.audio_buffer_ms;
seed.has_audio_buffer_ms = true;
}
seed.auto_skip_fmv = ls.auto_skip_fmv != 0;
seed.has_auto_skip_fmv = skip_fmv_offered;
seed.turbo_loads = ls.turbo_loads != 0; seed.has_turbo_loads = true;
@@ -6167,6 +6179,8 @@ int main(int argc, char** argv) {
g_video_aspect_den = ws_offered ? seed.aspect_den : 3;
g_ws_adaptive_view = ws_offered && seed.adaptive_view;
g_audio_spu_hq = seed.spu_hq;
if (audio_buffer_offered)
g_audio_buffer_ms = (double)seed.audio_buffer_ms;
skip_launcher_setting = seed.skip_launcher;
if (seed.has_bios_path) {
settings_bios_storage = seed.bios_path.string();
+6 -1
View File
@@ -278,7 +278,12 @@ static void rab__update_controller(rab_bridge *b, int pull_frames) {
double dt_ms = (double)pull_frames * 1000.0 / c->host_rate;
if (dt_ms < 0.1) dt_ms = 0.1;
if (dt_ms > 100.0) dt_ms = 100.0;
double err = (fill_ms - c->target_ms) / c->target_ms;
/* `target_ms` may be exposed as an unconstrained debug value. Keep zero
* mathematically defined without silently changing the requested target. */
double norm_ms = fabs(c->target_ms);
if (norm_ms < 1000.0 / c->source_rate)
norm_ms = 1000.0 / c->source_rate;
double err = (fill_ms - c->target_ms) / norm_ms;
if (fill_ms > c->target_ms - c->deadband_ms &&
fill_ms < c->target_ms + c->deadband_ms) {
err = 0.0; /* deadband */