From bc75131f6f2da3dc31f09ca878b2ae2ce2e09230 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 28 Oct 2026 08:59:15 +0100 Subject: [PATCH 1/2] Keep playable PCM continuous and re-prime only after actual depletion --- runtime/src/main.cpp | 7 +++-- runtime/src/recomp_audio_drc.h | 39 ++++++++++++++++++++++---- runtime/tests/test_audio_sink_resume.c | 19 +++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 4585d14d1..5d4946da3 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -5172,12 +5172,15 @@ static void sdl_audio_update(int hard_mute_active, int turbo_sink_active) { if (sink_was_active) { sink_was_active = 0; /* Preserve queued PCM; re-prime only when the sink depleted the ring. */ - if (psx_audio_sync_enabled() && sdl_audio_device && s_drc_ready) { + const bool bridge_resume = psx_audio_sync_enabled() && sdl_audio_device && s_drc_ready && !audio_legacy_mode(); + if (bridge_resume) { psx_sdl_audio_lock(sdl_audio_device); rab_resume_after_sink(&s_drc); psx_sdl_audio_unlock(sdl_audio_device); } - sdl_audio_fadein_left = sdl_audio_fade_samples; + /* The bridge joins retained/resumed PCM and ramps its consumer gain. + * A second zero-start producer ramp would create a queued seam. */ + sdl_audio_fadein_left = bridge_resume ? 0 : sdl_audio_fade_samples; audio_trace_event(AUDIO_EV_UNMUTE, (uint32_t)sdl_audio_fadein_left, 2); g_audio_unmute_resync = 1; diff --git a/runtime/src/recomp_audio_drc.h b/runtime/src/recomp_audio_drc.h index f464f01ff..aa030596c 100644 --- a/runtime/src/recomp_audio_drc.h +++ b/runtime/src/recomp_audio_drc.h @@ -56,7 +56,7 @@ typedef struct { typedef struct { uint64_t pushed_frames; uint64_t pulled_frames; - uint64_t underrun_events; /* times the ring ran dry mid-pull */ + uint64_t underrun_events; /* depletion episodes; excludes re-prime silence */ uint64_t overflow_drops; /* source frames dropped on ring overflow */ double last_fill_ms; double last_correction; /* applied ratio correction (signed) */ @@ -85,6 +85,9 @@ typedef struct rab_bridge { int primed; /* set once fill first reaches target */ float last_out[2]; /* last emitted sample per channel (for holds) */ + /* Host-only declick join after intentional output discard, not a queue. */ + float resume_from[2]; + int resume_left, resume_total; rab_stats stats; } rab_bridge; @@ -258,8 +261,15 @@ void rab_push(rab_bridge *b, const int16_t *in, int frames) { } int64_t w = (int64_t)(b->in_count % (uint64_t)b->cap); float *slot = b->ring + w * ch; - for (int c = 0; c < ch; ++c) - slot[c] = (float)in[f * ch + c] * (1.0f / 32768.0f); + for (int c = 0; c < ch; ++c) { + float sample = (float)in[f * ch + c] * (1.0f / 32768.0f); + if (b->resume_left > 0) { + const float mix = 1.0f - (float)b->resume_left / (float)b->resume_total; + sample = b->resume_from[c] + (sample - b->resume_from[c]) * mix; + } + slot[c] = sample; + } + if (b->resume_left > 0) --b->resume_left; b->in_count++; fill += 1.0; } @@ -318,7 +328,20 @@ static void rab__update_controller(rab_bridge *b, int pull_frames) { } void rab_resume_after_sink(rab_bridge *b) { - if (rab_fill_ms(b) < b->cfg.target_ms) b->primed = 0; + /* A below-target ring can still contain playable audio. Do not stop it + * merely to refill: only a truly depleted/unprimed bridge needs priming. */ + const int64_t newest_needed = (int64_t)floor(b->out_pos) + b->half; + const int playable = b->primed && newest_needed < (int64_t)b->in_count; + if (!playable) b->primed = 0; + b->resume_left = b->resume_total = 0; + if (playable && b->in_count) { + const int64_t last = (int64_t)((b->in_count - 1u) % (uint64_t)b->cap); + for (int c = 0; c < b->cfg.channels; ++c) + b->resume_from[c] = b->ring[last * b->cfg.channels + c]; + b->resume_total = (int)(b->cfg.source_rate * 0.003); + if (b->resume_total < 1) b->resume_total = 1; + b->resume_left = b->resume_total; + } b->err_lp = b->corr = b->corr_i = 0.0; b->cur_step = b->cfg.source_rate / b->cfg.host_rate; } @@ -356,8 +379,14 @@ void rab_pull(rab_bridge *b, int16_t *out, int frames) { /* underrun / not yet primed: hold last sample, let gain fade it out */ s[0] = b->last_out[0]; if (ch == 2) s[1] = b->last_out[1]; - if (b->primed && newest_needed >= (int64_t)b->in_count) + if (b->primed && newest_needed >= (int64_t)b->in_count) { + /* Rebuild the existing target after a true depletion. Staying + * primed here resumes with one tiny block and can starve again + * on every small producer jitter. Count one depletion episode; + * intentional startup/refill silence is not another underrun. */ b->stats.underrun_events++; + b->primed = 0; + } } /* smooth gate: fade toward 1 when delivering real audio, toward 0 else */ diff --git a/runtime/tests/test_audio_sink_resume.c b/runtime/tests/test_audio_sink_resume.c index 994debb2f..e7a031624 100644 --- a/runtime/tests/test_audio_sink_resume.c +++ b/runtime/tests/test_audio_sink_resume.c @@ -6,7 +6,26 @@ #include #include #include +static void test_live_and_depleted_resume(void) { + rab_bridge b;rab_config c;rab_config_defaults(&c);c.source_rate=c.host_rate=44100;c.target_ms=80; + assert(!rab_init(&b,&c));int16_t pcm[8820],out[9600];for(int i=0;i<8820;i++)pcm[i]=12000; + rab_push(&b,pcm,4410);rab_pull(&b,out,1323);assert(b.primed&&rab_fill_ms(&b)<80); + uint64_t count=b.in_count;double pos=b.out_pos;rab_resume_after_sink(&b); + assert(b.primed&&b.in_count==count&&b.out_pos==pos);rab_pull(&b,out,441); + for(int i=0;i<441;i++)assert(out[2*i]>11000); + rab_free(&b); + assert(!rab_init(&b,&c));rab_push(&b,pcm,4410);rab_pull(&b,out,441);rab_resume_after_sink(&b); + for(int i=0;i<1764;i++)pcm[i]=-12000;rab_push(&b,pcm,882);rab_pull(&b,out,4800); + for(int i=3900;i<4450;i++)assert(abs(out[2*i]-out[2*(i-1)])<350); + rab_pull(&b,out,441);assert(!b.primed);uint64_t under=b.stats.underrun_events;assert(under>0); + rab_pull(&b,out,441);assert(b.stats.underrun_events==under); + for(int i=0;i<8820;i++)pcm[i]=12000; + rab_push(&b,pcm,882);rab_pull(&b,out,441);assert(!b.primed); + rab_push(&b,pcm,3528);rab_pull(&b,out,441);assert(b.primed); + rab_free(&b); +} int main(void) { + test_live_and_depleted_resume(); rab_bridge b;rab_config c;rab_config_defaults(&c);c.target_ms=80; assert(rab_init(&b,&c)==0); int16_t pcm[8820];for(int i=0;i<8820;i++)pcm[i]=(int16_t)(i%251*80); From e01cbfec7b5c0be8840d3277cd427af580840e3b Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 28 Oct 2026 15:12:16 +0100 Subject: [PATCH 2/2] Expose next-session audio target and opt-in host diagnostics --- runtime/CMakeLists.txt | 3 ++ runtime/include/audio_sync.h | 8 +++++ runtime/src/audio_sync.c | 17 ++++++++-- runtime/src/main.cpp | 44 ++++++++++++++++++++++---- runtime/tests/test_audio_sync_target.c | 22 +++++++++++++ 5 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 runtime/tests/test_audio_sync_target.c diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 775e3075b..7d5a3148a 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -23,6 +23,9 @@ psxrecomp_add_runtime_target(psx-runtime # (docs/ENHANCEMENTS.md G1.10). Titles get their _pgxp the same way. if(BUILD_TESTING) + add_executable(audio_sync_target_test tests/test_audio_sync_target.c src/audio_sync.c) + target_include_directories(audio_sync_target_test PRIVATE include) + add_test(NAME audio_sync_target_test COMMAND audio_sync_target_test) add_executable(gpu_fmv_hd_test tests/test_gpu_fmv_hd.cpp src/gpu_fmv_hd.c) target_include_directories(gpu_fmv_hd_test PRIVATE include ${PSX_SDL_INCLUDE_DIRS}) target_compile_definitions(gpu_fmv_hd_test PRIVATE SDL_MAIN_HANDLED PSX_FMV_HD_TESTING=1 diff --git a/runtime/include/audio_sync.h b/runtime/include/audio_sync.h index 3cd8388c7..4b5268892 100644 --- a/runtime/include/audio_sync.h +++ b/runtime/include/audio_sync.h @@ -4,7 +4,15 @@ extern "C" { #endif /* Call before audio device creation. No guest state, clocks or mixer changes. */ +#define PSX_AUDIO_SYNC_MIN_MS 20 +#define PSX_AUDIO_SYNC_MAX_MS 180 +#define PSX_AUDIO_SYNC_DEFAULT_MS 70 +/* Session configuration only; call during committed mod activation before + * device creation. Disabling resets the request, not the user's saved option. */ void psx_audio_sync_set_enabled(int enabled); +int psx_audio_sync_set_target(int target_ms); +void psx_audio_sync_set_diagnostics(int enabled); +int psx_audio_sync_diagnostics(void); int psx_audio_sync_enabled(void); int psx_audio_sync_target(int configured_fallback_ms); #ifdef __cplusplus diff --git a/runtime/src/audio_sync.c b/runtime/src/audio_sync.c index 2e7e57410..33b5a5d98 100644 --- a/runtime/src/audio_sync.c +++ b/runtime/src/audio_sync.c @@ -1,5 +1,16 @@ #include "audio_sync.h" -static int enabled; -void psx_audio_sync_set_enabled(int value) { enabled = value != 0; } +static int enabled, target = PSX_AUDIO_SYNC_DEFAULT_MS, diagnostics; +void psx_audio_sync_set_enabled(int value) { + enabled = value != 0; + if (!enabled) { target = PSX_AUDIO_SYNC_DEFAULT_MS; diagnostics = 0; } +} int psx_audio_sync_enabled(void) { return enabled; } -int psx_audio_sync_target(int fallback) { return enabled ? 80 : fallback; } +int psx_audio_sync_set_target(int value) { + if (value < PSX_AUDIO_SYNC_MIN_MS || value > PSX_AUDIO_SYNC_MAX_MS || value % 5) + return 0; + target = value; + return 1; +} +int psx_audio_sync_target(int fallback) { return enabled ? target : fallback; } +void psx_audio_sync_set_diagnostics(int value) { diagnostics = value != 0; } +int psx_audio_sync_diagnostics(void) { return enabled && diagnostics; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 5d4946da3..53bc3cf38 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -2866,6 +2866,15 @@ static int g_turbo_audio_sink_config_enabled = 0; static int g_turbo_load_wall_multiplier = 0; static int g_turbo_load_release_frames = TURBO_LOADS_RELEASE_FRAMES; static SDL_AudioDeviceID sdl_audio_device; +static uint64_t audio_host_closes, audio_stream_clears, audio_bridge_inits; +static void audio_clear_tracked(SDL_AudioDeviceID device) { + ++audio_stream_clears; + psx_sdl_audio_clear(device); +} +static void audio_close_tracked(SDL_AudioDeviceID device) { + ++audio_host_closes; + psx_sdl_audio_close(device); +} static int16_t sdl_audio_buf[2048 * 2]; /* DRC bridge. Producer (sdl_audio_pump) runs on the main loop thread under @@ -4255,8 +4264,8 @@ static void shutdown_runtime(void) { overlay_capture_wait_pending(); overlay_capture_write_json(); if (sdl_audio_device) { - psx_sdl_audio_clear(sdl_audio_device); - psx_sdl_audio_close(sdl_audio_device); /* stops the pull callback */ + audio_clear_tracked(sdl_audio_device); + audio_close_tracked(sdl_audio_device); /* stops the pull callback */ sdl_audio_device = 0; } if (s_drc_ready) { rab_free(&s_drc); s_drc_ready = false; } @@ -4272,8 +4281,8 @@ static void teardown_game_session_keep_lobby(void) { psx_rewind_shutdown(); memcard_flush_all(); if (sdl_audio_device) { - psx_sdl_audio_clear(sdl_audio_device); - psx_sdl_audio_close(sdl_audio_device); + audio_clear_tracked(sdl_audio_device); + audio_close_tracked(sdl_audio_device); sdl_audio_device = 0; } if (s_drc_ready) { rab_free(&s_drc); s_drc_ready = false; } @@ -4396,7 +4405,7 @@ static void sdl_audio_pump(bool discard_output = false) { spu_output_flush(); g_audio_cycle_resync = 0; if (legacy && sdl_audio_device) - psx_sdl_audio_clear(sdl_audio_device); + audio_clear_tracked(sdl_audio_device); else g_audio_unmute_resync = 1; /* skip mute-drain underrun reports */ return; @@ -9135,6 +9144,28 @@ static bool local_mouse_live(int override_word) { return pad_ext_live(&g) != 0; } +/* Opt-in host diagnostics: sampled at most once per second on the producer + * thread. No callback I/O, guest timing changes or extra audio queue. Counts + * are process-lifetime totals; underruns are depletion episodes, overflow is + * dropped frames, and closes do not include driver-internal resets. */ +static void audio_buffer_diagnostics_tick() { + if (!psx_audio_sync_diagnostics() || !sdl_audio_device || !s_drc_ready) return; + static uint64_t previous = 0; + const uint64_t now = SDL_GetPerformanceCounter(); + const uint64_t freq = SDL_GetPerformanceFrequency(); + if (previous && now - previous < freq) return; + previous = now; + psx_sdl_audio_lock(sdl_audio_device); + const double fill = rab_fill_ms(&s_drc), active = s_drc.cfg.target_ms; + const rab_stats stats = s_drc.stats; + psx_sdl_audio_unlock(sdl_audio_device); + std::fprintf(stderr, "audio-buffer time_ms=%.1f configured_target_ms=%d active_target_ms=%.1f fill_ms=%.2f underruns=%llu overflow_frames=%llu device_closes=%llu stream_clears=%llu bridge_initializations=%llu\n", + now * 1000.0 / freq, psx_audio_sync_target(180), active, fill, + (unsigned long long)stats.underrun_events, (unsigned long long)stats.overflow_drops, + (unsigned long long)audio_host_closes, (unsigned long long)audio_stream_clears, + (unsigned long long)audio_bridge_inits); +} + static NetplayVblankEpilogue sdl_vblank_present_body(void) { NetplayVblankEpilogue ep{}; /* Guest quantum for this vblank is complete. Drop top-level-resume armed @@ -9192,6 +9223,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { runtime_perf_frame_begin(); RuntimePerfFrameScope runtime_perf_frame_scope; runtime_perf_diag_tick(); + audio_buffer_diagnostics_tick(); /* Lightweight frontend telemetry from PR #13. Count simulated vblanks rather * than presents so turbo and skipped-frame modes still report game speed. @@ -20964,7 +20996,7 @@ int main(int argc, char** argv) { cfg.source_rate = 44100.0; /* SPU render rate */ cfg.host_rate = (double)have.freq; /* actual device rate */ cfg.target_ms = (double)resolved_audio_buffer_ms; - if (rab_init(&s_drc, &cfg) == 0) s_drc_ready = true; + if (rab_init(&s_drc, &cfg) == 0) { s_drc_ready = true; ++audio_bridge_inits; } } g_audio_host_rate = have.freq; audio_trace_set_tap_rate(AUDIO_TAP_HOST, (uint32_t)have.freq); diff --git a/runtime/tests/test_audio_sync_target.c b/runtime/tests/test_audio_sync_target.c new file mode 100644 index 000000000..98e163e70 --- /dev/null +++ b/runtime/tests/test_audio_sync_target.c @@ -0,0 +1,22 @@ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include "audio_sync.h" +#include +int main(void) { + psx_audio_sync_set_enabled(0); + assert(psx_audio_sync_target(160) == 160); + psx_audio_sync_set_enabled(1); + assert(psx_audio_sync_target(180) == 70); + assert(psx_audio_sync_set_target(180)); + assert(psx_audio_sync_target(180) == 180); + assert(psx_audio_sync_set_target(70)); + assert(!psx_audio_sync_set_target(19)); + assert(!psx_audio_sync_set_target(181)); + assert(!psx_audio_sync_set_target(71)); + assert(psx_audio_sync_target(180) == 70); + psx_audio_sync_set_diagnostics(1); assert(psx_audio_sync_diagnostics()); + psx_audio_sync_set_enabled(0); assert(!psx_audio_sync_diagnostics()); + assert(psx_audio_sync_target(180) == 180); + psx_audio_sync_set_enabled(1); assert(psx_audio_sync_target(180) == 70); +}