From 351cce26b3b03a6813bbc2b14fb88079ca058389 Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 8 Jul 2026 14:06:11 -0500 Subject: [PATCH] AWE32: VWE_AWE_WAV crackle-hunt recording taps Three WAVs written from the render thread: per-card PRE-limiter streams as float32 (clipping baked in upstream shows as flat tops above full scale) and the POST-limiter int16 mix (what the user hears). Headers re-patched every second so files survive a hard kill. launch_pod.ps1 arms it into the work dir by default. Diagnosis logic: flat tops in a card tap = per-voice clipping inside the EMU8000 core; clean cards + dirty mix = limiter artifact; all clean = winmm path. Co-Authored-By: Claude Fable 5 --- emulator/render-bridge/launch_pod.ps1 | 1 + emulator/vpx-device/vweawe.cpp | 73 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/emulator/render-bridge/launch_pod.ps1 b/emulator/render-bridge/launch_pod.ps1 index 1d80e96..2e53e17 100644 --- a/emulator/render-bridge/launch_pod.ps1 +++ b/emulator/render-bridge/launch_pod.ps1 @@ -58,6 +58,7 @@ if ($NoSound) { $env:VWE_AWE32 = '1' $env:VWE_AWE_ROM = (Resolve-Path "$PSScriptRoot\..\roms\awe32.raw").Path $env:VWE_AWE_LOG = '1' # 10s reports incl. limiter stats (crackle diagnosis) + $env:VWE_AWE_WAV = "$Work\awe" # crackle taps: awe_front/_rear/_mix.wav } $dosbox = (Resolve-Path "$PSScriptRoot\..\src\src\dosbox-x.exe").Path diff --git a/emulator/vpx-device/vweawe.cpp b/emulator/vpx-device/vweawe.cpp index 391baf3..b4d36f3 100644 --- a/emulator/vpx-device/vweawe.cpp +++ b/emulator/vpx-device/vweawe.cpp @@ -37,6 +37,9 @@ * VWE_AWE_LEAD_MS=N audio queue depth in ms (default 80, min 30 max 250) * VWE_AWE_DUMP=dir append raw s16le stereo 44100 streams to * /awe_front.s16 + /awe_rear.s16 + * VWE_AWE_WAV=prefix crackle-hunt taps: _front/_rear.wav = + * per-card PRE-limiter float32 (overs survive) + + * _mix.wav = post-limiter int16 (as heard) * VWE_AWE_LOG=1 port trace + 10s activity reports (smldW counter = * SoundFont upload progress; 99 = upload refused) */ @@ -233,6 +236,56 @@ static int32_t awe_lim_gain = 1 << 16; /* Q16, <= 1.0 */ static unsigned long awe_lim_engaged = 0; /* frames ducked (diagnostic) */ static int awe_prelim_peak[2]; /* per-card pre-limit |peak| */ +/* VWE_AWE_WAV=: diagnostic recording taps for the crackle hunt. + * Three 44.1k stereo WAVs: _front.wav / _rear.wav = per-card + * PRE-limiter streams as float32 (values >1.0 survive, so clipping baked + * in upstream is visible as flat tops ABOVE full scale), and + * _mix.wav = the POST-limiter int16 the user actually hears. + * Localizes the crackle: flat tops already in a card tap = per-voice + * clipping inside the EMU8000 core; clean taps but dirty mix = limiter + * artifact; everything clean = the winmm path. Headers are re-patched + * every second so the files survive a hard kill. */ +static FILE *awe_wav_card[2]; +static FILE *awe_wav_mix; +static uint32_t awe_wav_frames; + +static void awe_wav_header(FILE *f, int fmt_tag, int bits) { + uint32_t rate = 44100, four = 0; + uint16_t ch = 2, b16 = (uint16_t)bits, tag = (uint16_t)fmt_tag; + uint16_t align = (uint16_t)(ch * (bits / 8)); + uint32_t bps = rate * align, fmtlen = 16; + fwrite("RIFF", 1, 4, f); fwrite(&four, 4, 1, f); + fwrite("WAVEfmt ", 1, 8, f); + fwrite(&fmtlen, 4, 1, f); + fwrite(&tag, 2, 1, f); fwrite(&ch, 2, 1, f); + fwrite(&rate, 4, 1, f); fwrite(&bps, 4, 1, f); + fwrite(&align, 2, 1, f); fwrite(&b16, 2, 1, f); + fwrite("data", 1, 4, f); fwrite(&four, 4, 1, f); +} + +static void awe_wav_patch(FILE *f, uint32_t data_bytes) { + uint32_t riff = 36 + data_bytes; + fseek(f, 4, SEEK_SET); fwrite(&riff, 4, 1, f); + fseek(f, 40, SEEK_SET); fwrite(&data_bytes, 4, 1, f); + fseek(f, 0, SEEK_END); + fflush(f); +} + +static void awe_wav_open(const char *prefix) { + char path[512]; + static const char *suffix[2] = { "_front.wav", "_rear.wav" }; + for (int ci = 0; ci < 2; ci++) { + snprintf(path, sizeof path, "%s%s", prefix, suffix[ci]); + awe_wav_card[ci] = fopen(path, "wb"); + if (awe_wav_card[ci]) awe_wav_header(awe_wav_card[ci], 3, 32); + } + snprintf(path, sizeof path, "%s_mix.wav", prefix); + awe_wav_mix = fopen(path, "wb"); + if (awe_wav_mix) awe_wav_header(awe_wav_mix, 1, 16); + fprintf(stderr, "VWE AWE32: recording taps -> %s_{front,rear,mix}.wav\n", + prefix); +} + static void awe_render_chunk(emu8k_t *card, int ci, unsigned n) { wavetable_pos_global = (int)n; emu8k_update(card); @@ -305,6 +358,24 @@ static DWORD WINAPI awe_thread_proc(LPVOID) { out[i * 2] = (int16_t)l; out[i * 2 + 1] = (int16_t)r; } + if (awe_wav_mix) { /* VWE_AWE_WAV taps */ + float fb[AWE_SLOT_FRAMES * 2]; + for (int ci = 0; ci < 2; ci++) { + if (!awe_wav_card[ci]) continue; + for (unsigned i = 0; i < AWE_SLOT_FRAMES * 2; i++) + fb[i] = (float)awe_cardbuf[ci][i] / 32768.0f; + fwrite(fb, sizeof(float), AWE_SLOT_FRAMES * 2, + awe_wav_card[ci]); + } + fwrite(out, sizeof(int16_t), AWE_SLOT_FRAMES * 2, awe_wav_mix); + awe_wav_frames += AWE_SLOT_FRAMES; + if (awe_wav_frames % 44100 < AWE_SLOT_FRAMES) { /* ~1s */ + for (int ci = 0; ci < 2; ci++) + if (awe_wav_card[ci]) + awe_wav_patch(awe_wav_card[ci], awe_wav_frames * 8); + awe_wav_patch(awe_wav_mix, awe_wav_frames * 4); + } + } if (awe_prelim_peak[0] > peak[0]) peak[0] = awe_prelim_peak[0]; if (awe_prelim_peak[1] > peak[1]) peak[1] = awe_prelim_peak[1]; awe_prelim_peak[0] = awe_prelim_peak[1] = 0; @@ -418,6 +489,8 @@ void VWEAWE_Init(void) { if (lm && atoi(lm) > 0) awe_lead_ms = atoi(lm); if (awe_lead_ms < 30) awe_lead_ms = 30; if (awe_lead_ms > 250) awe_lead_ms = 250; + const char *wv = getenv("VWE_AWE_WAV"); + if (wv && wv[0]) awe_wav_open(wv); InitializeCriticalSection(&awe_lock); LARGE_INTEGER f;