/* VWE dual-AWE32 wavetable device (Tesla/Red Planet pod sound) * * The production pods carried TWO Sound Blaster AWE32 ISA cards (confirmed * from hardware 2026-07-04: AWE32s with 2x 30-pin DRAM SIMMs each), driving * the four cockpit speakers as front/rear stereo pairs: * * AWE_FRONT = A220 I5 D1 H5 P330 T6 -> EMU8000 at 0x620/0xA20/0xE20 * AWE_REAR = A240 I7 D3 H6 P300 T6 -> EMU8000 at 0x640/0xA40/0xE40 * * The game (BTL4OPT.EXE, HMI SOS MIDI layer with the AWE32 driver linked in) * is MIDI-only wavetable: it uploads SoundFont banks (AUDIO/AUDIO1.RES + * AUDIO2.RES, RIFF sfbk) into card DRAM and plays voices with AWE NRPN * steering. It never streams PCM through the SB16 DSP, so the front card's * DSP is DOSBox-X's native [sblaster] (set irq=5 to match I5) and the rear * card only needs the minimal DSP/mixer stub at 0x240 provided here. * NOTE: the HMI driver verifies the AWE32 GM ROM (the banks declare * irom=1MGM) and silently refuses the SBK upload without one -- supply a * 1MB ROM image via VWE_AWE_ROM or all voices play silence. * * The EMU8000 itself is the vendored 86Box core (emu8k.cpp/.h). Synthesis * runs on a DEDICATED THREAD with its own winmm audio output, decoupled * from the emulation thread: the real cards were autonomous silicon, so * sustained voices keep sounding even while the emulation thread stalls * (e.g. RIO retry storms during mission staging). Both cards are summed * into one stereo stream (headset) through a peak limiter (the raw voice * accumulator routinely exceeds int16; hard-clamping it was the "generator * out" speech crackle -- the SBK samples themselves are clean); per-card * host routing for the real 4-speaker cockpit comes later (SOUND-NOTES.md). * * Environment (host-side, like the VPX device): * VWE_AWE32=1 enable the device (inert otherwise) * VWE_AWE_RAM_KB=N sample DRAM per card, KB (default 8192 = the pods' * 2x4MB SIMM fit; clamped 512..28672) * VWE_AWE_ROM=path awe32.raw 1MB GM ROM image (REQUIRED for the SBK * upload -- see above; without it, silence) * VWE_AWE_SHIFT=N output attenuation shift (default 0) * 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) */ #include "dosbox.h" #include "inout.h" #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #include "emu8k_shim.h" #include "emu8k.h" int wavetable_pos_global = 0; /* one lock serializes all EMU8000 state access: guest port I/O (emulation * thread) vs synthesis (render thread). Held only for register accesses and * per-chunk renders, so contention stays in the microseconds. */ static CRITICAL_SECTION awe_lock; /* WC interpolation: real-time samples elapsed since the render thread's * last pass, so guest busy-waits on the 44kHz sample counter see smooth * advancement between render chunks */ static volatile LONGLONG awe_last_render_qpc = 0; static LONGLONG awe_qpc_freq = 1; unsigned emu8k_shim_wc_extra(void) { LARGE_INTEGER now; QueryPerformanceCounter(&now); LONGLONG d = now.QuadPart - awe_last_render_qpc; if (d < 0) d = 0; LONGLONG smp = (d * 44100) / awe_qpc_freq; if (smp > 4410) smp = 4410; return (unsigned)smp; } /* activity stats (reported every ~10s of rendered audio per card) */ struct AweStats { unsigned long wr, rd_wc, rd, smld_w; }; static AweStats awe_stats[2]; /* 0 = front, 1 = rear */ /* ---- 86Box io_sethandler shim: dispatch DOSBox port I/O into the core --- */ struct AweIoRange { uint16_t base, size; void *priv; emu8k_io_inb_t inb; emu8k_io_inw_t inw; emu8k_io_outb_t outb; emu8k_io_outw_t outw; bool used; }; static AweIoRange awe_io[16]; static AweIoRange *awe_io_find(Bitu port) { for (size_t i = 0; i < sizeof(awe_io) / sizeof(awe_io[0]); i++) if (awe_io[i].used && port >= awe_io[i].base && port < (Bitu)(awe_io[i].base + awe_io[i].size)) return &awe_io[i]; return NULL; } /* VWE_AWE_LOG=1: trace the first accesses + growth-of-count milestones */ static int awe_log_on = -1; static unsigned long awe_access_count = 0; static void awe_io_trace(const char *dir, Bitu port, Bitu val, Bitu iolen) { if (awe_log_on < 0) { const char *l = getenv("VWE_AWE_LOG"); awe_log_on = (l && l[0] && l[0] != '0') ? 1 : 0; } if (!awe_log_on) return; awe_access_count++; if (awe_access_count <= 64 || (awe_access_count & (awe_access_count - 1)) == 0) fprintf(stderr, "VWE AWE32 io[%lu]: %s %03lx val=%04lx len=%lu\n", awe_access_count, dir, (unsigned long)port, (unsigned long)val, (unsigned long)iolen); } /* PTR shadow per card: mirror of the core's cur_reg/cur_voice, so the glue * can identify sample-memory (SMALW/SMLD/SMRD) traffic */ static uint16_t awe_ptr[2]; static int awe_sm_lines = 0; static void awe_sm_trace(const char *dir, int ci, Bitu port, Bitu val) { unsigned reg = (awe_ptr[ci] >> 5) & 7, voice = awe_ptr[ci] & 0x1F; if (reg != 1) return; if (voice == 26 && dir[0] == 'W') { /* SMLD data: count only */ awe_stats[ci].smld_w++; return; } /* log the write-pointer setups: each upload chunk starts with one */ if (dir[0] != 'W' || (voice != 22 && voice != 23)) return; if (awe_log_on <= 0 || awe_sm_lines >= 200) return; awe_sm_lines++; fprintf(stderr, "VWE AWE32 sm[%d]: card%d W %s(%s) val=%04lx\n", awe_sm_lines, ci, (voice == 22) ? "SMALW" : "SMARW", (port & 2) ? "hi/D2" : "lo/D1", (unsigned long)val); } static Bitu awe_io_read(Bitu port, Bitu iolen) { AweIoRange *r = awe_io_find(port); if (!r) return ~0ul; Bitu v; EnterCriticalSection(&awe_lock); if (iolen >= 2 && r->inw) v = r->inw((uint16_t)port, r->priv); else if (r->inb) v = r->inb((uint16_t)port, r->priv); else v = ~0ul; LeaveCriticalSection(&awe_lock); AweStats &s = awe_stats[(port & 0x40) ? 1 : 0]; if ((port & 0xF9F) == 0xA02) s.rd_wc++; else s.rd++; /* A22/A42 */ if ((port & 0xF1C) == 0xA00) /* A20-A23/A40-A43 */ awe_sm_trace("R", (port & 0x40) ? 1 : 0, port, v); awe_io_trace("R", port, v, iolen); return v; } static void awe_io_write(Bitu port, Bitu val, Bitu iolen) { AweIoRange *r = awe_io_find(port); if (!r) return; const int ci = (port & 0x40) ? 1 : 0; awe_stats[ci].wr++; if ((port & 0xF9E) == 0xE02) { /* E22/E23/E42/E43 */ /* mirror the core's byte-write semantics (low byte lost on odd) */ if (iolen >= 2) awe_ptr[ci] = (uint16_t)val; else if (port & 1) awe_ptr[ci] = (uint16_t)(val << 8); else awe_ptr[ci] = (uint16_t)(val & 0xFF); } if ((port & 0xF1C) == 0xA00) awe_sm_trace("W", ci, port, val); awe_io_trace("W", port, val, iolen); EnterCriticalSection(&awe_lock); if (iolen >= 2 && r->outw) r->outw((uint16_t)port, (uint16_t)val, r->priv); else if (r->outb) r->outb((uint16_t)port, (uint8_t)val, r->priv); LeaveCriticalSection(&awe_lock); } void io_sethandler(uint16_t base, int size, emu8k_io_inb_t inb, emu8k_io_inw_t inw, emu8k_io_inl_t, emu8k_io_outb_t outb, emu8k_io_outw_t outw, emu8k_io_outl_t, void *priv) { for (size_t i = 0; i < sizeof(awe_io) / sizeof(awe_io[0]); i++) { if (awe_io[i].used) continue; awe_io[i].base = base; awe_io[i].size = (uint16_t)size; awe_io[i].priv = priv; awe_io[i].inb = inb; awe_io[i].inw = inw; awe_io[i].outb = outb; awe_io[i].outw = outw; awe_io[i].used = true; IO_RegisterReadHandler(base, awe_io_read, IO_MB | IO_MW, (Bitu)size); IO_RegisterWriteHandler(base, awe_io_write, IO_MB | IO_MW, (Bitu)size); return; } fprintf(stderr, "VWE AWE32: io_sethandler table full\n"); } void io_removehandler(uint16_t base, int size, emu8k_io_inb_t, emu8k_io_inw_t, emu8k_io_inl_t, emu8k_io_outb_t, emu8k_io_outw_t, emu8k_io_outl_t, void *priv) { for (size_t i = 0; i < sizeof(awe_io) / sizeof(awe_io[0]); i++) if (awe_io[i].used && awe_io[i].base == base && awe_io[i].size == (uint16_t)size && awe_io[i].priv == priv) awe_io[i].used = false; /* DOSBox handler stays; range inert */ } FILE *emu8k_shim_rom_fopen(void) { const char *p = getenv("VWE_AWE_ROM"); if (!p || !p[0]) return NULL; FILE *f = fopen(p, "rb"); if (!f) fprintf(stderr, "VWE AWE32: cannot open VWE_AWE_ROM '%s'\n", p); return f; } /* ---- the two cards + autonomous render thread --------------------------- */ static emu8k_t awe_front, awe_rear; static FILE *awe_dump_front = NULL, *awe_dump_rear = NULL; static int awe_shift = 0; static int awe_lead_ms = 80; static HWAVEOUT awe_wo = NULL; static volatile LONG awe_stop = 0; /* winmm slot ring: SLOT_FRAMES per buffer, SLOTS in flight max */ #define AWE_SLOT_FRAMES 441 /* 10ms at 44100 */ #define AWE_SLOTS 28 /* absolute pool size */ static WAVEHDR awe_hdr[AWE_SLOTS]; static int16_t awe_slotbuf[AWE_SLOTS][AWE_SLOT_FRAMES * 2]; static int32_t awe_cardbuf[2][AWE_SLOT_FRAMES * 2]; /* Output limiter (always on). The EMU8000 core sums its 32 voices into a raw * int32 accumulator; a busy mix (speech over the engine loop, weapon hits) * easily exceeds int16 and the old hard clamp flat-topped it -- the user's * "generator out" crackle (baseline WAV from the SBK proved the sample itself * is clean). Instead of clipping, duck: instant attack to keep the summed * peak under AWE_LIM_TARGET, ~46ms release back to unity. Stereo-linked so * the image doesn't wander. The safety clamp after it should never engage. */ #define AWE_LIM_TARGET 32000 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); for (unsigned i = 0; i < n * 2; i++) { int32_t v = card->buffer[i] >> awe_shift; awe_cardbuf[ci][i] = v; /* full-range; limited at sum */ if (v < 0) v = -v; if (v > awe_prelim_peak[ci]) awe_prelim_peak[ci] = v; } memset(card->buffer, 0, n * 2 * sizeof(int32_t)); memset(card->chorus_in_buffer, 0, n * sizeof(int32_t)); memset(card->reverb_in_buffer, 0, n * sizeof(int32_t)); card->pos = 0; wavetable_pos_global = 0; } static DWORD WINAPI awe_thread_proc(LPVOID) { unsigned long frames[2] = { 0, 0 }, last_report[2] = { 0, 0 }; int peak[2] = { 0, 0 }; const unsigned lead_slots = (unsigned)(awe_lead_ms / 10); while (!awe_stop) { /* reclaim finished slots, count in-flight */ unsigned queued = 0; int free_slot = -1; for (int i = 0; i < AWE_SLOTS; i++) { if (awe_hdr[i].dwFlags & WHDR_PREPARED) { if (awe_hdr[i].dwFlags & WHDR_DONE) waveOutUnprepareHeader(awe_wo, &awe_hdr[i], sizeof(WAVEHDR)); else { queued++; continue; } } if (free_slot < 0) free_slot = i; } if (queued >= lead_slots || free_slot < 0) { Sleep(2); continue; } /* render one 10ms chunk from both cards, sum into the slot */ LARGE_INTEGER now; EnterCriticalSection(&awe_lock); awe_render_chunk(&awe_front, 0, AWE_SLOT_FRAMES); awe_render_chunk(&awe_rear, 1, AWE_SLOT_FRAMES); QueryPerformanceCounter(&now); awe_last_render_qpc = now.QuadPart; LeaveCriticalSection(&awe_lock); int16_t *out = awe_slotbuf[free_slot]; for (unsigned i = 0; i < AWE_SLOT_FRAMES; i++) { int32_t l = awe_cardbuf[0][i * 2] + awe_cardbuf[1][i * 2]; int32_t r = awe_cardbuf[0][i * 2 + 1] + awe_cardbuf[1][i * 2 + 1]; /* stereo-linked peak limiter: instant attack, ~46ms release */ int32_t pk = l < 0 ? -l : l; int32_t pr = r < 0 ? -r : r; if (pr > pk) pk = pr; if (pk > AWE_LIM_TARGET) { int32_t need = (int32_t)(((int64_t)AWE_LIM_TARGET << 16) / pk); if (need < awe_lim_gain) awe_lim_gain = need; } if (awe_lim_gain < (1 << 16)) { awe_lim_engaged++; l = (int32_t)(((int64_t)l * awe_lim_gain) >> 16); r = (int32_t)(((int64_t)r * awe_lim_gain) >> 16); /* release: exponential toward unity, min step 1 so it * actually GETS there (>>11 alone stalls 2048 short) */ int32_t step = ((1 << 16) - awe_lim_gain) >> 11; awe_lim_gain += step ? step : 1; if (awe_lim_gain > (1 << 16)) awe_lim_gain = 1 << 16; } if (l > 32767) l = 32767; else if (l < -32768) l = -32768; if (r > 32767) r = 32767; else if (r < -32768) r = -32768; 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; WAVEHDR *h = &awe_hdr[free_slot]; memset(h, 0, sizeof(WAVEHDR)); h->lpData = (LPSTR)out; h->dwBufferLength = AWE_SLOT_FRAMES * 4; waveOutPrepareHeader(awe_wo, h, sizeof(WAVEHDR)); waveOutWrite(awe_wo, h, sizeof(WAVEHDR)); if (awe_dump_front || awe_dump_rear) { /* dumps stay s16le: clamp the raw int32 card stream per file */ static int16_t dump16[AWE_SLOT_FRAMES * 2]; for (int ci = 0; ci < 2; ci++) { FILE *df = ci ? awe_dump_rear : awe_dump_front; if (!df) continue; for (unsigned i = 0; i < AWE_SLOT_FRAMES * 2; i++) { int32_t v = awe_cardbuf[ci][i]; if (v > 32767) v = 32767; else if (v < -32768) v = -32768; dump16[i] = (int16_t)v; } fwrite(dump16, 4, AWE_SLOT_FRAMES, df); } } for (int ci = 0; ci < 2; ci++) { frames[ci] += AWE_SLOT_FRAMES; if (awe_log_on > 0 && frames[ci] - last_report[ci] >= 441000) { last_report[ci] = frames[ci]; emu8k_t *card = ci ? &awe_rear : &awe_front; int voices = 0; for (int i = 0; i < 32; i++) if (card->voice[i].cvcf_curr_volume) voices++; fprintf(stderr, "VWE AWE32%c 10s: wr=%lu wcRd=%lu rd=%lu " "smldW=%lu voices=%d peak=%d limF=%lu\n", ci ? 'R' : 'F', awe_stats[ci].wr, awe_stats[ci].rd_wc, awe_stats[ci].rd, awe_stats[ci].smld_w, voices, peak[ci], awe_lim_engaged); peak[ci] = 0; if (ci) awe_lim_engaged = 0; } } } return 0; } /* ---- rear-card SB16 front-end stub at 0x240 ----------------------------- * Just enough DSP + mixer for detection (sb16set, SOS probes): reset -> * 0xAA, E0 identification, E1 version 4.13, mixer register file. The DSP * digital path is never used by the game (MIDI-only engine). */ static uint8_t rdsp_mixer_idx = 0, rdsp_mixer[256]; static uint8_t rdsp_q[8]; static int rdsp_qn = 0, rdsp_qr = 0; static uint8_t rdsp_reset_latch = 0; static int rdsp_pending_e0 = 0; static void rdsp_push(uint8_t b) { if (rdsp_qn < (int)sizeof(rdsp_q)) rdsp_q[(rdsp_qr + rdsp_qn++) % 8] = b; } static Bitu rdsp_read(Bitu port, Bitu /*iolen*/) { switch (port & 0xF) { case 0x4: return rdsp_mixer_idx; case 0x5: return rdsp_mixer[rdsp_mixer_idx]; case 0xA: if (rdsp_qn) { uint8_t b = rdsp_q[rdsp_qr]; rdsp_qr = (rdsp_qr + 1) % 8; rdsp_qn--; return b; } return 0xFF; case 0xC: return 0x00; /* ready for command */ case 0xE: return rdsp_qn ? 0xFF : 0x7F; /* bit7 = data available */ default: return 0xFF; } } static void rdsp_write(Bitu port, Bitu val, Bitu /*iolen*/) { switch (port & 0xF) { case 0x4: rdsp_mixer_idx = (uint8_t)val; break; case 0x5: if (rdsp_mixer_idx == 0) memset(rdsp_mixer, 0, sizeof(rdsp_mixer)); else rdsp_mixer[rdsp_mixer_idx] = (uint8_t)val; break; case 0x6: if (rdsp_reset_latch == 1 && (val & 1) == 0) { rdsp_qn = rdsp_qr = 0; rdsp_pending_e0 = 0; rdsp_push(0xAA); } rdsp_reset_latch = (uint8_t)(val & 1); break; case 0xC: if (rdsp_pending_e0) { rdsp_push((uint8_t)~val); rdsp_pending_e0 = 0; break; } switch (val) { case 0xE0: rdsp_pending_e0 = 1; break; /* identify */ case 0xE1: rdsp_push(4); rdsp_push(13); break; /* version */ default: break; /* swallow */ } break; default: break; } } /* ---- init ---------------------------------------------------------------- */ void VWEAWE_Init(void) { const char *en = getenv("VWE_AWE32"); if (!en || !en[0] || en[0] == '0') return; int ram_kb = 8192; const char *r = getenv("VWE_AWE_RAM_KB"); if (r && atoi(r) > 0) ram_kb = atoi(r); if (ram_kb < 512) ram_kb = 512; if (ram_kb > 28672) ram_kb = 28672; const char *sh = getenv("VWE_AWE_SHIFT"); if (sh) awe_shift = atoi(sh); if (awe_shift < 0) awe_shift = 0; if (awe_shift > 15) awe_shift = 15; const char *lm = getenv("VWE_AWE_LEAD_MS"); 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; QueryPerformanceFrequency(&f); awe_qpc_freq = f.QuadPart ? f.QuadPart : 1; emu8k_init(&awe_front, 0x620, ram_kb); emu8k_init(&awe_rear, 0x640, ram_kb); WAVEFORMATEX wfx; memset(&wfx, 0, sizeof wfx); wfx.wFormatTag = WAVE_FORMAT_PCM; wfx.nChannels = 2; wfx.nSamplesPerSec = 44100; wfx.wBitsPerSample = 16; wfx.nBlockAlign = 4; wfx.nAvgBytesPerSec = 44100 * 4; if (waveOutOpen(&awe_wo, WAVE_MAPPER, &wfx, 0, 0, CALLBACK_NULL) != MMSYSERR_NOERROR) { fprintf(stderr, "VWE AWE32: waveOutOpen failed -- no audio output\n"); awe_wo = NULL; } else { HANDLE th = CreateThread(NULL, 0, awe_thread_proc, NULL, 0, NULL); if (th) { SetThreadPriority(th, THREAD_PRIORITY_TIME_CRITICAL); CloseHandle(th); } } IO_RegisterReadHandler(0x240, rdsp_read, IO_MB, 16); IO_RegisterWriteHandler(0x240, rdsp_write, IO_MB, 16); const char *dd = getenv("VWE_AWE_DUMP"); if (dd && dd[0]) { char path[1024]; snprintf(path, sizeof path, "%s\\awe_front.s16", dd); awe_dump_front = fopen(path, "ab"); snprintf(path, sizeof path, "%s\\awe_rear.s16", dd); awe_dump_rear = fopen(path, "ab"); } fprintf(stderr, "VWE AWE32: front EMU8000 @620h + rear @640h (DSP stub " "@240h), %dKB DRAM/card, GM ROM %s, shift %d, own render " "thread + waveout (%dms lead)%s\n", ram_kb, getenv("VWE_AWE_ROM") ? "loaded" : "ABSENT (silence!)", awe_shift, awe_lead_ms, dd ? ", dumping" : ""); }