mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
Usage strings (header + runtime) now document the press_frames arg, and the schedule parser walks with strtol instead of strtok/atoi: argv is no longer mutated, and empty/non-numeric/negative tokens are skipped instead of becoming a phantom A-press at frame 0. Co-Authored-By: Claude Fable 5 <[email protected]>
404 lines
12 KiB
C
404 lines
12 KiB
C
/* test_audio_long_capture.c -- run a core for N seconds and emit a
|
|
* per-second audio fingerprint so two builds can be compared.
|
|
*
|
|
* Usage:
|
|
* ./test/test_audio_long_capture <core.dylib> <rom.jag> <seconds> \
|
|
* [bios=0|1] [out.wav] [press_frames]
|
|
* press_frames: comma-separated frame numbers; the A button is held 20
|
|
* frames from each (navigates title/menus into gameplay).
|
|
*
|
|
* Output (one line per second):
|
|
* sec=NN samples=NN nonsilent=NN peak=NN rms=NN nonzero_runs=NN
|
|
*
|
|
* Final summary:
|
|
* TOTAL nonsilent=NN peak_global=NN sustained_secs=NN
|
|
*
|
|
* "Sustained" = a 1-second window where >50% of samples are non-silent
|
|
* (proxy for "music playing", since SFX bursts < 50%). Comparing two
|
|
* builds: if v_old has sustained=12 and v_new has sustained=0, music
|
|
* regressed.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stdarg.h>
|
|
#include <math.h>
|
|
#include <dlfcn.h>
|
|
#include "../libretro-common/include/libretro.h"
|
|
|
|
#define SAMPLE_RATE 48000
|
|
#define SAMPLES_PER_FRAME 800 /* 48000 / 60 */
|
|
#define FRAMES_PER_SEC 60
|
|
#define SILENCE_THRESHOLD 256 /* |sample| > this is "non-silent" */
|
|
|
|
static int g_seconds = 60;
|
|
static int g_use_bios = 0;
|
|
static const char *g_rom = NULL;
|
|
static const char *g_wav_out = NULL;
|
|
static FILE *g_wav_fp = NULL;
|
|
static uint64_t g_wav_samples = 0;
|
|
|
|
typedef struct {
|
|
uint64_t total_samples;
|
|
uint64_t nonsilent_samples;
|
|
int peak_global;
|
|
int sustained_seconds;
|
|
/* per-second */
|
|
uint32_t per_sec_nonsilent[600];
|
|
int32_t per_sec_peak[600];
|
|
double per_sec_rms[600];
|
|
} stats_t;
|
|
|
|
static stats_t S;
|
|
static int current_sec = 0;
|
|
static uint32_t cur_sec_nonsilent = 0;
|
|
static int32_t cur_sec_peak = 0;
|
|
static double cur_sec_rms_acc = 0.0;
|
|
static uint32_t cur_sec_count = 0;
|
|
static int frames_in_sec = 0;
|
|
|
|
/* ---- libretro callback stubs ---- */
|
|
|
|
static retro_environment_t env_cb;
|
|
static retro_video_refresh_t video_cb;
|
|
static retro_audio_sample_t sample_cb;
|
|
static retro_audio_sample_batch_t batch_cb;
|
|
static retro_input_poll_t poll_cb;
|
|
static retro_input_state_t state_cb;
|
|
|
|
static void env_log(enum retro_log_level level, const char *fmt, ...)
|
|
{
|
|
(void)level;
|
|
(void)fmt;
|
|
}
|
|
|
|
static struct retro_log_callback log_cb = { env_log };
|
|
static const char *system_dir = "test/roms/private";
|
|
static struct retro_variable last_variable;
|
|
|
|
static bool environment(unsigned cmd, void *data)
|
|
{
|
|
switch (cmd) {
|
|
case RETRO_ENVIRONMENT_GET_LOG_INTERFACE:
|
|
*(struct retro_log_callback *)data = log_cb;
|
|
return true;
|
|
case RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
|
|
*(const char **)data = system_dir;
|
|
return true;
|
|
case RETRO_ENVIRONMENT_GET_VARIABLE: {
|
|
struct retro_variable *v = (struct retro_variable *)data;
|
|
if (!v || !v->key) return false;
|
|
if (strcmp(v->key, "virtualjaguar_bios") == 0) {
|
|
v->value = g_use_bios ? "enabled" : "disabled";
|
|
return true;
|
|
}
|
|
if (strcmp(v->key, "virtualjaguar_usefastblitter") == 0) {
|
|
v->value = "disabled";
|
|
return true;
|
|
}
|
|
v->value = NULL;
|
|
return false;
|
|
}
|
|
case RETRO_ENVIRONMENT_SET_PIXEL_FORMAT:
|
|
case RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS:
|
|
case RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL:
|
|
case RETRO_ENVIRONMENT_SET_MEMORY_MAPS:
|
|
case RETRO_ENVIRONMENT_SET_VARIABLES:
|
|
case RETRO_ENVIRONMENT_SET_GEOMETRY:
|
|
case RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS:
|
|
case RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2:
|
|
case RETRO_ENVIRONMENT_SET_CONTROLLER_INFO:
|
|
return true;
|
|
case RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE: {
|
|
bool *u = (bool *)data;
|
|
if (u) *u = false;
|
|
return true;
|
|
}
|
|
case RETRO_ENVIRONMENT_GET_CAN_DUPE: {
|
|
bool *c = (bool *)data;
|
|
if (c) *c = true;
|
|
return true;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
(void)last_variable;
|
|
}
|
|
|
|
static void video_refresh(const void *data, unsigned width, unsigned height, size_t pitch)
|
|
{
|
|
(void)data; (void)width; (void)height; (void)pitch;
|
|
}
|
|
static void input_poll(void) {}
|
|
|
|
/* Optional A-press schedule (argv[6]: comma-separated frame numbers,
|
|
each held 20 frames) — enough to advance title/menu screens into
|
|
gameplay so in-game audio can be captured. */
|
|
#define MAX_PRESSES 16
|
|
#define PRESS_HOLD 20
|
|
static int g_press_frames[MAX_PRESSES];
|
|
static int g_num_presses = 0;
|
|
static int g_cur_frame = 0;
|
|
|
|
static int16_t input_state(unsigned port, unsigned device, unsigned index, unsigned id)
|
|
{
|
|
int i;
|
|
(void)index;
|
|
if (port != 0 || device != RETRO_DEVICE_JOYPAD || id != RETRO_DEVICE_ID_JOYPAD_A)
|
|
return 0;
|
|
for (i = 0; i < g_num_presses; i++)
|
|
if (g_cur_frame >= g_press_frames[i]
|
|
&& g_cur_frame < g_press_frames[i] + PRESS_HOLD)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
static void audio_sample_one(int16_t l, int16_t r) { (void)l; (void)r; }
|
|
|
|
static void account_sample(int16_t l, int16_t r)
|
|
{
|
|
int absl = l < 0 ? -l : l;
|
|
int absr = r < 0 ? -r : r;
|
|
int peak = absl > absr ? absl : absr;
|
|
S.total_samples++;
|
|
if (peak > SILENCE_THRESHOLD) {
|
|
S.nonsilent_samples++;
|
|
cur_sec_nonsilent++;
|
|
}
|
|
if (peak > S.peak_global)
|
|
S.peak_global = peak;
|
|
if (peak > cur_sec_peak)
|
|
cur_sec_peak = peak;
|
|
cur_sec_rms_acc += (double)peak * (double)peak;
|
|
cur_sec_count++;
|
|
}
|
|
|
|
static size_t audio_sample_batch(const int16_t *data, size_t frames)
|
|
{
|
|
size_t i;
|
|
for (i = 0; i < frames; i++) {
|
|
account_sample(data[i * 2 + 0], data[i * 2 + 1]);
|
|
}
|
|
if (g_wav_fp) {
|
|
fwrite(data, sizeof(int16_t) * 2, frames, g_wav_fp);
|
|
g_wav_samples += frames;
|
|
}
|
|
return frames;
|
|
}
|
|
|
|
/* Write a 44-byte canonical WAV header for stereo s16 at 48kHz.
|
|
* Call after we know the final sample count. */
|
|
static void wav_write_header(FILE *f, uint64_t total_stereo_samples)
|
|
{
|
|
uint32_t data_bytes = (uint32_t)(total_stereo_samples * 4);
|
|
uint32_t fmt_chunk_size = 16;
|
|
uint32_t riff_size = 4 + (8 + fmt_chunk_size) + (8 + data_bytes);
|
|
uint16_t audio_format = 1;
|
|
uint16_t num_channels = 2;
|
|
uint32_t sample_rate = 48000;
|
|
uint16_t bits_per_sample = 16;
|
|
uint16_t block_align = num_channels * bits_per_sample / 8;
|
|
uint32_t byte_rate = sample_rate * block_align;
|
|
fseek(f, 0, SEEK_SET);
|
|
fwrite("RIFF", 1, 4, f);
|
|
fwrite(&riff_size, 4, 1, f);
|
|
fwrite("WAVE", 1, 4, f);
|
|
fwrite("fmt ", 1, 4, f);
|
|
fwrite(&fmt_chunk_size, 4, 1, f);
|
|
fwrite(&audio_format, 2, 1, f);
|
|
fwrite(&num_channels, 2, 1, f);
|
|
fwrite(&sample_rate, 4, 1, f);
|
|
fwrite(&byte_rate, 4, 1, f);
|
|
fwrite(&block_align, 2, 1, f);
|
|
fwrite(&bits_per_sample, 2, 1, f);
|
|
fwrite("data", 1, 4, f);
|
|
fwrite(&data_bytes, 4, 1, f);
|
|
}
|
|
|
|
static void emit_second(void)
|
|
{
|
|
double rms = 0.0;
|
|
uint32_t pct;
|
|
if (cur_sec_count > 0)
|
|
rms = sqrt(cur_sec_rms_acc / (double)cur_sec_count);
|
|
pct = (cur_sec_nonsilent * 100) / (cur_sec_count == 0 ? 1 : cur_sec_count);
|
|
S.per_sec_nonsilent[current_sec] = cur_sec_nonsilent;
|
|
S.per_sec_peak[current_sec] = cur_sec_peak;
|
|
S.per_sec_rms[current_sec] = rms;
|
|
if (pct >= 50) S.sustained_seconds++;
|
|
printf("sec=%2d samples=%5u nonsilent=%5u (%3u%%) peak=%6d rms=%7.1f\n",
|
|
current_sec, cur_sec_count, cur_sec_nonsilent, pct,
|
|
cur_sec_peak, rms);
|
|
fflush(stdout);
|
|
current_sec++;
|
|
cur_sec_nonsilent = 0;
|
|
cur_sec_peak = 0;
|
|
cur_sec_rms_acc = 0.0;
|
|
cur_sec_count = 0;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
void *handle;
|
|
const char *core_path;
|
|
FILE *f;
|
|
long sz;
|
|
void *rom_buf;
|
|
struct retro_game_info game;
|
|
int frame;
|
|
|
|
/* Symbols */
|
|
void (*retro_set_environment)(retro_environment_t);
|
|
void (*retro_set_video_refresh)(retro_video_refresh_t);
|
|
void (*retro_set_audio_sample)(retro_audio_sample_t);
|
|
void (*retro_set_audio_sample_batch)(retro_audio_sample_batch_t);
|
|
void (*retro_set_input_poll)(retro_input_poll_t);
|
|
void (*retro_set_input_state)(retro_input_state_t);
|
|
void (*retro_init)(void);
|
|
void (*retro_deinit)(void);
|
|
bool (*retro_load_game)(const struct retro_game_info *);
|
|
void (*retro_unload_game)(void);
|
|
void (*retro_run)(void);
|
|
void (*retro_reset)(void);
|
|
|
|
if (argc < 4) {
|
|
fprintf(stderr, "Usage: %s <core.dylib> <rom> <seconds> [bios=0|1] "
|
|
"[out.wav] [press_frames]\n"
|
|
" press_frames: comma-separated frame numbers; the A button "
|
|
"is held 20 frames from each\n",
|
|
argv[0]);
|
|
return 2;
|
|
}
|
|
core_path = argv[1];
|
|
g_rom = argv[2];
|
|
g_seconds = atoi(argv[3]);
|
|
if (g_seconds < 1 || g_seconds > 600) g_seconds = 60;
|
|
if (argc >= 5) g_use_bios = atoi(argv[4]) ? 1 : 0;
|
|
if (argc >= 7) {
|
|
/* strtol walk, no strtok: argv stays unmodified, and empty or
|
|
* non-numeric tokens (e.g. "10,,20" or "10,x") are skipped
|
|
* instead of silently becoming an A-press at frame 0. */
|
|
const char *p = argv[6];
|
|
while (*p && g_num_presses < MAX_PRESSES) {
|
|
char *end;
|
|
long v = strtol(p, &end, 10);
|
|
if (end != p && v > 0 && v < 1000000)
|
|
g_press_frames[g_num_presses++] = (int)v;
|
|
p = (end != p) ? end : p + 1;
|
|
while (*p && *p != ',') p++; /* skip trailing junk in token */
|
|
if (*p == ',') p++;
|
|
}
|
|
}
|
|
if (argc >= 6) {
|
|
g_wav_out = argv[5];
|
|
g_wav_fp = fopen(g_wav_out, "wb");
|
|
if (!g_wav_fp) {
|
|
fprintf(stderr, "open WAV %s: failed\n", g_wav_out);
|
|
return 2;
|
|
}
|
|
/* Reserve 44 bytes for header; we patch on close. */
|
|
{
|
|
char zero[44] = {0};
|
|
fwrite(zero, 1, 44, g_wav_fp);
|
|
}
|
|
}
|
|
|
|
handle = dlopen(core_path, RTLD_NOW | RTLD_LOCAL);
|
|
if (!handle) {
|
|
fprintf(stderr, "dlopen failed: %s\n", dlerror());
|
|
return 2;
|
|
}
|
|
|
|
#define LOAD(s) do { \
|
|
*(void **)&s = dlsym(handle, #s); \
|
|
if (!s) { fprintf(stderr, "dlsym " #s " failed\n"); return 2; } \
|
|
} while (0)
|
|
LOAD(retro_set_environment);
|
|
LOAD(retro_set_video_refresh);
|
|
LOAD(retro_set_audio_sample);
|
|
LOAD(retro_set_audio_sample_batch);
|
|
LOAD(retro_set_input_poll);
|
|
LOAD(retro_set_input_state);
|
|
LOAD(retro_init);
|
|
LOAD(retro_deinit);
|
|
LOAD(retro_load_game);
|
|
LOAD(retro_unload_game);
|
|
LOAD(retro_run);
|
|
LOAD(retro_reset);
|
|
#undef LOAD
|
|
|
|
env_cb = environment;
|
|
video_cb = video_refresh;
|
|
sample_cb = audio_sample_one;
|
|
batch_cb = audio_sample_batch;
|
|
poll_cb = input_poll;
|
|
state_cb = input_state;
|
|
|
|
retro_set_environment(env_cb);
|
|
retro_set_video_refresh(video_cb);
|
|
retro_set_audio_sample(sample_cb);
|
|
retro_set_audio_sample_batch(batch_cb);
|
|
retro_set_input_poll(poll_cb);
|
|
retro_set_input_state(state_cb);
|
|
|
|
retro_init();
|
|
|
|
f = fopen(g_rom, "rb");
|
|
if (!f) { fprintf(stderr, "open ROM failed\n"); return 2; }
|
|
fseek(f, 0, SEEK_END); sz = ftell(f); fseek(f, 0, SEEK_SET);
|
|
rom_buf = malloc((size_t)sz);
|
|
if (fread(rom_buf, 1, (size_t)sz, f) != (size_t)sz) {
|
|
fprintf(stderr, "fread short\n"); return 2;
|
|
}
|
|
fclose(f);
|
|
|
|
game.path = g_rom;
|
|
game.data = rom_buf;
|
|
game.size = (size_t)sz;
|
|
game.meta = NULL;
|
|
|
|
if (!retro_load_game(&game)) {
|
|
fprintf(stderr, "retro_load_game failed\n");
|
|
return 2;
|
|
}
|
|
|
|
memset(&S, 0, sizeof(S));
|
|
printf("=== %s | bios=%d | %d sec | rom=%s ===\n",
|
|
core_path, g_use_bios, g_seconds, g_rom);
|
|
|
|
for (frame = 0; frame < g_seconds * FRAMES_PER_SEC; frame++) {
|
|
g_cur_frame = frame;
|
|
retro_run();
|
|
frames_in_sec++;
|
|
if (frames_in_sec >= FRAMES_PER_SEC) {
|
|
emit_second();
|
|
frames_in_sec = 0;
|
|
}
|
|
}
|
|
|
|
printf("--- TOTAL ---\n");
|
|
printf("total_samples=%llu nonsilent=%llu (%.1f%%) peak_global=%d "
|
|
"sustained_secs=%d/%d\n",
|
|
(unsigned long long)S.total_samples,
|
|
(unsigned long long)S.nonsilent_samples,
|
|
100.0 * (double)S.nonsilent_samples
|
|
/ (double)(S.total_samples == 0 ? 1 : S.total_samples),
|
|
S.peak_global,
|
|
S.sustained_seconds, g_seconds);
|
|
|
|
if (g_wav_fp) {
|
|
wav_write_header(g_wav_fp, g_wav_samples);
|
|
fclose(g_wav_fp);
|
|
printf("WAV: wrote %llu stereo frames to %s\n",
|
|
(unsigned long long)g_wav_samples, g_wav_out);
|
|
}
|
|
|
|
retro_unload_game();
|
|
retro_deinit();
|
|
free(rom_buf);
|
|
dlclose(handle);
|
|
return 0;
|
|
}
|