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;