Files
psxrecomp/runtime/src/audio_trace.c
T

281 lines
9.7 KiB
C

/*
* audio_trace.c - always-on audio observability rings. See audio_trace.h.
*
* Debug-tool builds keep 4 PCM taps x 2^22 stereo frames x 4 bytes = 64 MiB
* static history, plus a 2^19-entry event ring (16 MiB). Production
* PSX_NO_DEBUG_TOOLS builds retain counters/events but compile out PCM history
* storage and WAV dumps.
*/
#include "audio_trace.h"
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
/* 2^22 frames @ 44100 ~= 95 s per tap. Power of two so wrap is a mask. */
#ifndef PSX_NO_DEBUG_TOOLS
#define PCM_RING_FRAMES (1u << 22)
#define PCM_RING_MASK (PCM_RING_FRAMES - 1u)
#endif
#define EV_RING_CAP (1u << 19)
#define EV_RING_MASK (EV_RING_CAP - 1u)
/* Audibility threshold shared with snesrecomp's dropped_audible metric:
* |sample| > 256 ~= -42 dBFS. Below that, gaps/drops are inaudible and must
* not trip regression counters (snesrecomp a1c659f). */
#define AUDIBLE_ABS 256
typedef struct {
#ifndef PSX_NO_DEBUG_TOOLS
int16_t pcm[PCM_RING_FRAMES * 2];
#endif
_Atomic uint64_t head; /* frames ever written; ring pos = head & MASK */
uint64_t nonzero;
uint64_t audible;
int32_t peak;
} PcmTap;
static PcmTap s_taps[AUDIO_TAP_COUNT];
/* Per-tap sample rate for WAV headers. SPU-side taps are natively 44100;
* the host tap follows the device rate in bridge/pull mode. */
static uint32_t s_tap_rate[AUDIO_TAP_COUNT] = { 44100u, 44100u, 44100u, 44100u };
void audio_trace_set_tap_rate(int tap, uint32_t rate)
{
if (tap < 0 || tap >= AUDIO_TAP_COUNT || rate == 0) return;
s_tap_rate[tap] = rate;
}
uint32_t audio_trace_tap_rate(int tap)
{
if (tap < 0 || tap >= AUDIO_TAP_COUNT) return 44100u;
return s_tap_rate[tap];
}
static AudioTraceEvent s_events[EV_RING_CAP];
static _Atomic uint64_t s_event_head;
static _Atomic uint32_t s_noted_frame;
/* Host pump counters (single writer: the pump/render thread). */
static uint64_t s_pump_calls;
static uint64_t s_pump_skips;
static uint64_t s_underruns;
static uint32_t s_queue_hiwater;
static uint32_t s_queue_lowater = 0xFFFFFFFFu;
static uint64_t s_mute_events;
static uint64_t s_unmute_events;
void audio_trace_init(void)
{
for (int t = 0; t < AUDIO_TAP_COUNT; t++) {
atomic_store(&s_taps[t].head, 0);
s_taps[t].nonzero = 0;
s_taps[t].audible = 0;
s_taps[t].peak = 0;
}
atomic_store(&s_event_head, 0);
atomic_store(&s_noted_frame, 0);
s_pump_calls = 0;
s_pump_skips = 0;
s_underruns = 0;
s_queue_hiwater = 0;
s_queue_lowater = 0xFFFFFFFFu;
s_mute_events = 0;
s_unmute_events = 0;
}
void audio_trace_note_frame(uint32_t frame)
{
atomic_store_explicit(&s_noted_frame, frame, memory_order_relaxed);
}
void audio_trace_pcm(int tap, const int16_t *stereo, int frames)
{
if (tap < 0 || tap >= AUDIO_TAP_COUNT || !stereo || frames <= 0) return;
PcmTap *t = &s_taps[tap];
uint64_t head = atomic_load_explicit(&t->head, memory_order_relaxed);
for (int f = 0; f < frames; f++) {
int16_t l = stereo[f * 2 + 0];
int16_t r = stereo[f * 2 + 1];
#ifndef PSX_NO_DEBUG_TOOLS
uint32_t pos = (uint32_t)((head + (uint64_t)f) & PCM_RING_MASK);
t->pcm[pos * 2 + 0] = l;
t->pcm[pos * 2 + 1] = r;
#endif
int32_t al = l < 0 ? -(int32_t)l : l;
int32_t ar = r < 0 ? -(int32_t)r : r;
int32_t a = al > ar ? al : ar;
if (a) t->nonzero++;
if (a > AUDIBLE_ABS) t->audible++;
if (a > t->peak) t->peak = a;
}
atomic_store_explicit(&t->head, head + (uint64_t)frames,
memory_order_release);
}
void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b)
{
uint64_t seq = atomic_load_explicit(&s_event_head, memory_order_relaxed);
AudioTraceEvent *e = &s_events[(uint32_t)(seq & EV_RING_MASK)];
e->seq = seq;
e->sample_idx = atomic_load_explicit(&s_taps[AUDIO_TAP_SPU_OUT].head,
memory_order_relaxed);
e->frame = atomic_load_explicit(&s_noted_frame, memory_order_relaxed);
e->kind = kind;
e->reserved = 0;
e->a = a;
e->b = b;
atomic_store_explicit(&s_event_head, seq + 1, memory_order_release);
switch (kind) {
case AUDIO_EV_RENDER:
s_pump_calls++;
if (b > s_queue_hiwater) s_queue_hiwater = b;
if (b < s_queue_lowater) s_queue_lowater = b;
break;
case AUDIO_EV_PUMP_SKIP: s_pump_skips++; break;
case AUDIO_EV_UNDERRUN: s_underruns++; break;
case AUDIO_EV_MUTE: s_mute_events++; break;
case AUDIO_EV_UNMUTE: s_unmute_events++; break;
default: break;
}
}
void audio_trace_get_stats(AudioTraceStats *out)
{
if (!out) return;
memset(out, 0, sizeof(*out));
for (int t = 0; t < AUDIO_TAP_COUNT; t++) {
out->tap_frames[t] = atomic_load_explicit(&s_taps[t].head,
memory_order_acquire);
out->tap_nonzero[t] = s_taps[t].nonzero;
out->tap_audible[t] = s_taps[t].audible;
out->tap_peak[t] = s_taps[t].peak;
}
out->pump_calls = s_pump_calls;
out->pump_skips = s_pump_skips;
out->underruns = s_underruns;
out->queue_hiwater = s_queue_hiwater;
out->queue_lowater = s_queue_lowater == 0xFFFFFFFFu ? 0 : s_queue_lowater;
out->mute_events = s_mute_events;
out->unmute_events = s_unmute_events;
out->events_total = atomic_load_explicit(&s_event_head,
memory_order_acquire);
}
uint64_t audio_trace_tap_total(int tap)
{
if (tap < 0 || tap >= AUDIO_TAP_COUNT) return 0;
return atomic_load_explicit(&s_taps[tap].head, memory_order_acquire);
}
uint64_t audio_trace_events_total(void)
{
return atomic_load_explicit(&s_event_head, memory_order_acquire);
}
uint32_t audio_trace_events_get(AudioTraceEvent *out, uint32_t max)
{
if (!out || max == 0) return 0;
uint64_t total = atomic_load_explicit(&s_event_head, memory_order_acquire);
uint64_t avail = total < (uint64_t)EV_RING_CAP ? total : (uint64_t)EV_RING_CAP;
if ((uint64_t)max > avail) max = (uint32_t)avail;
uint64_t first = total - (uint64_t)max;
for (uint32_t i = 0; i < max; i++)
out[i] = s_events[(uint32_t)((first + i) & EV_RING_MASK)];
return max;
}
/* ---- WAV dump --------------------------------------------------------- */
#ifndef PSX_NO_DEBUG_TOOLS
static void wav_write_header(FILE *fp, uint32_t data_bytes, uint32_t rate)
{
uint8_t h[44];
uint32_t byte_rate = rate * 4u;
uint32_t riff_size = 36u + data_bytes;
memcpy(h + 0, "RIFF", 4);
h[4] = (uint8_t)riff_size; h[5] = (uint8_t)(riff_size >> 8);
h[6] = (uint8_t)(riff_size >> 16); h[7] = (uint8_t)(riff_size >> 24);
memcpy(h + 8, "WAVEfmt ", 8);
h[16] = 16; h[17] = 0; h[18] = 0; h[19] = 0; /* fmt chunk size */
h[20] = 1; h[21] = 0; /* PCM */
h[22] = 2; h[23] = 0; /* stereo */
h[24] = (uint8_t)rate; h[25] = (uint8_t)(rate >> 8);
h[26] = (uint8_t)(rate >> 16); h[27] = (uint8_t)(rate >> 24);
h[28] = (uint8_t)byte_rate; h[29] = (uint8_t)(byte_rate >> 8);
h[30] = (uint8_t)(byte_rate >> 16); h[31] = (uint8_t)(byte_rate >> 24);
h[32] = 4; h[33] = 0; /* block align */
h[34] = 16; h[35] = 0; /* bits per sample */
memcpy(h + 36, "data", 4);
h[40] = (uint8_t)data_bytes; h[41] = (uint8_t)(data_bytes >> 8);
h[42] = (uint8_t)(data_bytes >> 16); h[43] = (uint8_t)(data_bytes >> 24);
fwrite(h, 1, sizeof(h), fp);
}
int64_t audio_trace_dump_wav(int tap, const char *path,
int64_t start, uint64_t count)
{
if (tap < 0 || tap >= AUDIO_TAP_COUNT || !path || !path[0]) return -1;
PcmTap *t = &s_taps[tap];
/* Snapshot the head; everything in [head - avail, head) is stable
* (append-only, single writer) unless the writer laps us — the ring
* holds ~95 s, a dump takes well under a second. */
uint64_t head = atomic_load_explicit(&t->head, memory_order_acquire);
uint64_t avail = head < (uint64_t)PCM_RING_FRAMES ? head
: (uint64_t)PCM_RING_FRAMES;
if (avail == 0) return -1;
uint64_t oldest = head - avail;
uint64_t s; /* absolute first frame of the slice */
if (start < 0) {
s = oldest;
if (count == 0 || count > avail) count = avail;
} else {
s = (uint64_t)start;
if (s < oldest) s = oldest; /* evicted prefix clamps */
if (s >= head) return -1;
uint64_t max = head - s;
if (count == 0 || count > max) count = max;
}
/* Cap a single WAV at the ring capacity (paranoia; avail <= cap). */
if (count > (uint64_t)PCM_RING_FRAMES) count = PCM_RING_FRAMES;
FILE *fp = fopen(path, "wb");
if (!fp) return -1;
/* Stamp the tap's true rate (SPU taps: exactly 44100; host tap: device
* rate in bridge mode). A mislabeled header silently poisons every
* offline A/B (snesrecomp da33c06). */
wav_write_header(fp, (uint32_t)(count * 4u), s_tap_rate[tap]);
uint64_t done = 0;
while (done < count) {
uint32_t pos = (uint32_t)((s + done) & PCM_RING_MASK);
uint32_t chunk = PCM_RING_FRAMES - pos;
if ((uint64_t)chunk > count - done) chunk = (uint32_t)(count - done);
if (fwrite(&t->pcm[(size_t)pos * 2u], 4u, chunk, fp) != chunk) {
fclose(fp);
return -1;
}
done += chunk;
}
fclose(fp);
return (int64_t)count;
}
#else
int64_t audio_trace_dump_wav(int tap, const char *path,
int64_t start, uint64_t count)
{
(void)tap;
(void)path;
(void)start;
(void)count;
return -1;
}
#endif