Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ psxrecomp_add_runtime_target(psx-runtime
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_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
Expand Down
8 changes: 8 additions & 0 deletions runtime/include/audio_sync.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 14 additions & 3 deletions runtime/src/audio_sync.c
Original file line number Diff line number Diff line change
@@ -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; }
51 changes: 43 additions & 8 deletions runtime/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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; }
Expand All @@ -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; }
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -5172,12 +5181,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;
Expand Down Expand Up @@ -9132,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
Expand Down Expand Up @@ -9189,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.
Expand Down Expand Up @@ -20961,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);
Expand Down
39 changes: 34 additions & 5 deletions runtime/src/recomp_audio_drc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 */
Expand Down
19 changes: 19 additions & 0 deletions runtime/tests/test_audio_sink_resume.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,26 @@
#include <assert.h>
#include <string.h>
#include <stdio.h>
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);
Expand Down
22 changes: 22 additions & 0 deletions runtime/tests/test_audio_sync_target.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#ifdef NDEBUG
#undef NDEBUG
#endif
#include "audio_sync.h"
#include <assert.h>
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);
}
Loading