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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-08 14:06:11 -05:00
co-authored by Claude Fable 5
parent ab4a5b8702
commit 351cce26b3
2 changed files with 74 additions and 0 deletions
+1
View File
@@ -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
+73
View File
@@ -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
* <dir>/awe_front.s16 + <dir>/awe_rear.s16
* VWE_AWE_WAV=prefix crackle-hunt taps: <prefix>_front/_rear.wav =
* per-card PRE-limiter float32 (overs survive) +
* <prefix>_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=<prefix>: diagnostic recording taps for the crackle hunt.
* Three 44.1k stereo WAVs: <prefix>_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
* <prefix>_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;