/* * 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 #include #include /* 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