Sound: dual-AWE32 EMU8000 emulation -- the pod has audio

Vendored 86Box EMU8000 core (GPL-2, emu8k.cpp/.h + shim) plus vweawe.cpp:
two cards at the production addresses (0x620/0x640 register triplets),
rear-card DSP/mixer stub at 0x240, synthesis on an autonomous render
thread with direct winmm output so sustained voices ride through
emulation-thread stalls (RIO staging retries) like the real silicon did.
WC sample-counter interpolation between render chunks keeps HMI SOS
busy-waits fast. VWE_AWE32/VWE_AWE_ROM/VWE_AWE_RAM_KB/VWE_AWE_SHIFT/
VWE_AWE_LEAD_MS/VWE_AWE_DUMP/VWE_AWE_LOG env knobs.

emulator/roms: the AWE32 1MGM GM ROM dumped from our own pod card (SF2)
plus the reconstructed raw image the emulated EMU8000 loads. The HMI
driver refuses the AUDIO*.RES SoundFont upload without it (banks declare
irom=1MGM) -- that was the root cause of the first silent runs; full
debug chain in SOUND-NOTES.md. Repo serves the pod-owner community only.

.gitignore: drop the ROM excludes and un-ignore ALPHA_1/ and sda4/
(committed separately).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-04 19:40:15 -05:00
co-authored by Claude Fable 5
parent dabc204d0d
commit a0a4260fd4
10 changed files with 3893 additions and 20 deletions
-6
View File
@@ -1,9 +1,3 @@
# Developer hard-drive dump — not part of the release tree
/sda4/
# hard drive dump of a production system (preserved in-repo as ALPHA_1.zip)
/ALPHA_1/
# Emulator working files (downloaded binaries, game image, captures) # Emulator working files (downloaded binaries, game image, captures)
/emulator/dosbox-x/ /emulator/dosbox-x/
/emulator/image/ /emulator/image/
+169
View File
@@ -0,0 +1,169 @@
# Sound subsystem — recon & plan
Goal: bring the pod's audio up in the DOSBox-X fork, faithful to the
production hardware: **two AWE32 ISA cards driving four cockpit speakers**
(front pair + rear pair).
## What the production system actually was (recon 2026-07-04)
- **Cards** (`REL410/BT/SETENV.BAT:158`, `VGL_LABS/PARAMETR.BAT:172`):
- `AWE_FRONT = A220 I5 D1 H5 P330 T6`
- `AWE_REAR = A240 I7 D3 H6 P300 T6`
Sound is enabled by SETENV **arg 3 = `s`** (`n` = NOSOUND — our current
emulator confs run `call setenv.bat r s n g`, i.e. sound explicitly OFF).
Mixer preset via `c:\sb16\sb16set` + `audio\ctmix.cfg` (not in the ALPHA_1
image — lived on the pod's local C:); `icom.cfg` variant opens **line-in
on the front card** when `L4INTERCOM=ON` (analog intercom mix).
`AWE_MASTER_VOLUME` / `c:\setvol.bat` = per-pod volume.
- **The game does NOT stream PCM through the SB16 DSP.** The audio engine
(`CODE/RP/MUNGA_L4/L4AUD*.CPP|HPP`, shared MUNGA "Audio Manager" brick,
1995) is **MIDI-only wavetable synthesis**:
- HMI **Sound Operating System** MIDI layer (`sosm.h`,
`sosMIDISendMIDIData`), `_MIDI_DRIVER_TYPE _MIDI_AWE32`, driver **linked
into BTL4OPT.EXE** (`sosMIDIInitDriver` string present; no external
.MDI).
- Each `AudioCard` parses its BLASTER-style env var (`AWE_FRONT` /
`AWE_REAR` -> srcAddx/irq/dma/**mpuAddx**/**emuAddx**) and drives the
card's **EMU8000** sampler: note on/off, program change, pitch bend,
and the AWE NRPNs (pitch 16, filter cutoff 21, vol attack 11). 32
voices, 16 channels per card. `LoadSBK()` uploads SoundFont banks into
card sample DRAM.
- `AudioHardware` = `frontCard` + `rearCard`.
- **All sound content is SoundFont banks**: `REL410/BT/AUDIO/AUDIO1.RES`
(3.7MB) and `AUDIO2.RES` (3.4MB) are RIFF `sfbk` (SBK/SF1, `isng` =
EMU8000) — renamed SoundFonts. `AUDIO.INI [AudioResources]` loads BOTH
into EACH card. Both banks per card ⇒ >7MB samples ⇒ **the pod AWE32s
had DRAM upgrades** (stock is 512KB); emulate with 8MB.
- **3D audio is computed in software** by the game's AudioRenderer
(`AUDIO.INI [AudioRenderer]`): amplitude + HF rolloff with knees, doppler
(speed_of_sound 250), **ITD 1.5ms**, source compression, reverb scale —
rendered by steering wavetable voices per ear across the front/rear
cards. Quad positional audio, 1995.
- **HARDWARE CONFIRMED (Cyd, 2026-07-04)**: pulled a card from a cockpit —
they are indeed **AWE32s, each with 2× 30-pin memory SIMMs** fitted (the
predicted DRAM upgrade; both banks per card ⇒ likely 2×4MB SIMMs = 8MB).
Also confirmed: **the intercom hardware was removed from the cockpits**
at some point — the intercom path (line-in mix, `icom.cfg`,
`L4INTERCOM`) is documented here for the record but will NOT be
restored; plan item 8 is closed as won't-do (mic/comms, if ever wanted,
would be a new host-side feature, not a restoration).
- **Period test suite exists**: `VWETEST/VGLTEST/` — TEST.BAT, SETENV1-9,
and diagnostic SBKs (PINKNOIS pink noise, SPKPLACE/SPKBASE placement,
QCHNTSTF/QCHNTSTB quad front/back, phase tests) + PINKNOIS.TXT
procedure. This is VWE's own speaker validation battery — use it to
validate the emulation before the game.
## The gap
DOSBox-X emulates one SB16 (DSP/mixer/OPL) and an MPU-401 — but **no
EMU8000**, and no second card. The game's entire audio path is EMU8000
register programming at `base+0x400/+0x800/+0xC00` (0x620/0xA20/0xE20 and
0x640/0xA40/0xE40). Nothing will sound until the EMU8000 exists.
## Plan
1. **EMU8000 core in the fork** (the big rock). Port the emu8k core from
86Box (GPL-2, same license as DOSBox-X; mature implementation incl.
sample-RAM read/write via SMALR/SMALW, 32 voices, filters, envelopes,
chorus/reverb). Two instances (front/rear), I/O hooks at both cards'
EMU8000 port triplets, 8MB sample DRAM each, each mixed into its own
DOSBox mixer channel (`AWEFRONT`, `AWEREAR`) at 44.1kHz. Config via env
or conf section, following the vpxlog device pattern.
2. **Second-card detection plumbing.** Keep DOSBox-X's native SB16 as the
front card — conf `[sblaster] sbtype=sb16 irq=5 dma=1 hdma=5` to match
`I5`. Add a minimal stub at 0x240 (DSP reset/version 4.xx + mixer
register file) so probes/sb16set succeed for the rear card. No DMA
audio needed anywhere (MIDI-only engine; SB IRQ/DMA unused — I5/I7
conflicts are a non-issue in emulation).
3. **Source verification pass** (before/while coding): read
`L4AUDIO.CPP`/`L4AUDRND.CPP`/`L4AUDRES.CPP` for the SOS init sequence
(timer rate, card detection order, failure modes), the exact SBK load
path, and what `InitMIDIReceive`/`GetMIDIByte` (MPU input!) is used for
in pod mode — open question.
4. **Enable sound**: gauge confs arg3 `n``s`. sb16set calls will error
harmlessly (not in image) — emulated mixer defaults stand in; optionally
add period SB16 utils to a `SB16/` dir later for authenticity. Do NOT
flip the flag before step 1 lands (unknown hang risk probing absent
EMU8000 — or do one throwaway run first to record the failure mode).
5. **Validate with VGLTEST** in the emulator: pink noise per speaker, quad
front/back tests — the way VWE techs did it.
6. **In-mission validation**: arena1 — engine pitch vs throttle (NRPN),
weapons, front/rear pan while turning past sources. Archive captures via
DOSBox-X mixer WAV record.
7. **Quad host output (cockpit build)**: start with front+rear summed to
stereo; then map to a 4.0 SDL output or send the rear card's channel to
a second host audio device for the physical rear pair.
8. ~~Intercom~~**closed, won't-do**: the intercom hardware was removed
from the cockpits (confirmed 2026-07-04). Remaining deferred items:
MPU MIDI-receive consumer, AUDIOMR.INI (mission-review audio profile)
differences.
## STATUS 2026-07-04: FIRST SOUND ACHIEVED (steps 1-4 done)
Implemented in the fork: vendored 86Box EMU8000 core (`emu8k.cpp/.h`, GPL-2)
+ `vweawe.cpp` glue (two cards at 0x620/0x640 triplets, per-card mixer
channels AWE32F/AWE32R, 0x240 DSP/mixer stub, `VWEAWE_Init` from sdlmain,
env: VWE_AWE32=1, VWE_AWE_RAM_KB, VWE_AWE_ROM, VWE_AWE_SHIFT, VWE_AWE_DUMP
(s16 stream dump), VWE_AWE_LOG=1 (port trace + 10s activity reports incl.
smldW upload counter + active voices + peak)).
Debug chain that got to sound, for the record:
1. Game run: SOS driver alive, EMU8000 detected, DRAM probe PASSES
(FFFF/AAAA/5555 bank scan verified in the SM trace), notes played
(voices>0) but **smldW=99** -- SoundFont upload never ran, voices played
zeroed DRAM = silence.
2. AWEUTIL /S (production boot runs it per card, PARAMETR.BAT:181-186)
hung polling WC: first lead. WC itself verified good after adding
block-interpolation (emu8k_shim_wc_extra; killed the 268M-poll storm).
3. Root cause: the banks declare `irom=1MGM` and the HMI AWE32 driver (and
AWEUTIL) verify the GM ROM before accepting SBKs. Emulated cards had a
zeroed ROM -> silent rejection.
4. Fix: reconstructed a 1MB ROM image from a user-supplied 1MGM.SF2 (smpl
chunk 1,047,588 B end-aligned into 1MB + ID words 0x314D/0x474D at word
offsets 2/3) -> `VWE_AWE_ROM=<path>` -> **both cards uploaded 3,513,581
sample words (full 7MB) and the mission plays real audio on both
channels** (front peak 11.8k, rear hit full-scale 32768).
**Architecture rev 2 (same day): autonomous synth thread.** User feedback:
sound great, but RIO stalls (staging-phase drops pre-date sound; the game
can't service the board while loading — button-press resync is the player
ritual) made audio chop, because DOSBox's mixer renders on the emulation
thread. Fix: synthesis moved to a dedicated render thread with its OWN
winmm output (10ms slots, VWE_AWE_LEAD_MS queue, default 80ms), EMU8000
state guarded by one critical section against guest port I/O. Like the
real silicon, sustained voices keep sounding through emulation-thread
stalls. Both cards sum to one stereo stream for now (user runs a headset);
per-card devices for the 4-speaker cockpit later. Note: our audio no
longer routes through the DOSBox mixer (its volume controls don't affect
it). Also: first render-path rev needlessly memset 64KB/card/ms — fixed
(clear only consumed region) before the thread move; both changes reduce
emulation-thread load vs the first-sound build.
Follow-ups:
- ROM provenance settled: `emulator/roms/1mgm.sf2` was dumped from one of
OUR OWN pod AWE32 cards; the repo serves the pod-owner community only,
so it is committed along with the reconstructed `emulator/roms/awe32.raw`
(recipe: SF2 smpl chunk end-aligned into 1MB + ID words 314D/474D at
word offsets 2-3). Point `VWE_AWE_ROM` at `emulator\roms\awe32.raw`.
The reconstruction may misalign ROM-resident GM presets (VWE banks are
RAM samples, unaffected) -- a raw linear re-dump of the ROM chips would
perfect it if GM playback ever matters.
- Rear card hit peak 32768 = clipping; consider VWE_AWE_SHIFT=1 or mixer
channel trim once real gameplay levels are assessed.
- Upload pacing is ~32K words/s (driver polls between words) -> adds ~2min
per card to boot. Optimization candidate later.
- Re-enable `aweutil /s` in the boot sequence now that a ROM exists
(currently skipped in the scratch conf) -- production authenticity.
- Promote the scratch gauge_sound.conf changes into the repo gauge confs
once validated: [sblaster] irq=5, production DIAGNOSE(+AWEUTIL) init
block, setenv arg3 `s`.
## Risks / notes
- emu8k port: integration surface is small (ISA I/O + mixer callback), but
verify the driver's DRAM size probe and chip ID/revision reads pass.
- HMI SOS hooks the PIT for MIDI timing — routine under DOSBox.
- Ran fine under 32RTM/DPMI on real pods, so no new protected-mode risk.
- RP (Red Planet) uses the same MUNGA audio brick and its own AUDIO*.RES —
everything here carries over.
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
# Card ROMs
- **`1mgm.sf2`** — the AWE32's 1MB "1MGM" General MIDI sample ROM, dumped
from one of our own pod AWE32 cards (the cockpits each carried two, with
2x 30-pin DRAM SIMMs). Preserved here because the pod audio banks
(`ALPHA_1/REL410/BT/AUDIO/AUDIO*.RES`) declare `irom=1MGM`, and the HMI
SOS AWE32 driver refuses the SoundFont upload if the card has no GM ROM.
- **`awe32.raw`** — the same content reconstructed as the raw 1MB ROM image
the emulated EMU8000 loads (`VWE_AWE_ROM=emulator\roms\awe32.raw`):
the SF2's `smpl` chunk (1,047,588 bytes) end-aligned into a zeroed 1MB
buffer, with the `1MGM` ID words (`0x314D 0x474D`) at word offsets 23.
Rebuild with any RIFF parser if the SF2 changes; the recipe lives in
SOUND-NOTES.md.
This archive and its contents are preserved for the pod-owner community
and are not distributed outside it.
Binary file not shown.
+31 -14
View File
@@ -1,28 +1,45 @@
# VPX device — DOSBox-X integration # VPX + AWE32 devices — DOSBox-X integration
Our original source for the emulated Division VPX link adapter. Kept here under Our original source for the emulated pod hardware. Kept here under
version control because the DOSBox-X source tree itself version control because the DOSBox-X source tree itself
(`emulator/src/`, ~490 MB) is git-ignored. (`emulator/src/`, ~490 MB) is git-ignored.
- **`vpxlog.cpp`** — Phase 1 logging device. Impersonates the INMOS C012 link - **`vpxlog.cpp`** — the Division VPX link adapter (INMOS C012 at I/O base
adapter at I/O base `0x150`, answers status reads so the game keeps `0x150`), grown through Phase 3: iserver handshake responder, FIFO scene
transmitting, and logs every access to `$VPXLOG`. (Phase 2 will grow this decode, GL Division renderer (gallery shading model), VDB video-head
into a responding transputer-monitor + i860-loader + frame-stream renderer, splitter windows.
or fork into a separate `vpx.cpp`.) - **`vweawe.cpp`** — the dual-AWE32 sound device: two vendored EMU8000
cores at 0x620/0x640 (+0x400/+0x800 triplets), rear-card DSP/mixer stub
at 0x240, autonomous render thread with direct winmm output. Needs the
GM ROM (`emulator/roms/awe32.raw`) via `VWE_AWE_ROM` or SoundFont
uploads are refused (banks declare `irom=1MGM`). See
`emulator/SOUND-NOTES.md`.
- **`emu8k.cpp` / `emu8k.h` / `emu8k_shim.h`** — EMU8000 wavetable core
vendored from 86Box (GPL-2.0-or-later, same license as DOSBox-X), with
a minimal shim; local changes are listed in the emu8k.cpp header.
## Applying to a DOSBox-X source checkout ## Applying to a DOSBox-X source checkout
Tested against DOSBox-X `v2026.06.02`, MSYS2 mingw64. Tested against DOSBox-X `v2026.06.02`, MSYS2 mingw64.
1. Copy the device in: 1. Copy the devices in:
``` ```
cp emulator/vpx-device/vpxlog.cpp emulator/src/src/hardware/vpxlog.cpp cp emulator/vpx-device/vpxlog.cpp emulator/src/src/hardware/
cp emulator/vpx-device/emu8k.cpp emulator/src/src/hardware/
cp emulator/vpx-device/emu8k.h emulator/src/src/hardware/
cp emulator/vpx-device/emu8k_shim.h emulator/src/src/hardware/
cp emulator/vpx-device/vweawe.cpp emulator/src/src/hardware/
``` ```
2. Add it to the hardware build — in `src/src/hardware/Makefile.am`, append 2. Add them to the hardware build — in `src/src/hardware/Makefile.am`, append
`vpxlog.cpp` to `libhardware_a_SOURCES` (we inserted it after `glide.cpp`). `vpxlog.cpp emu8k.cpp vweawe.cpp` to `libhardware_a_SOURCES` (we inserted
3. Call the init — in `src/src/gui/sdlmain.cpp`, declare `void VPXLOG_Init();` them after `glide.cpp`; if you edit the generated `Makefile`/`Makefile.in`
next to the other `*_Init()` prototypes and call `VPXLOG_Init();` right after by hand instead of re-running automake, also mirror the `$(OBJEXT)` list,
`GLIDE_Init();` in the machine bring-up sequence. the `.Po` depfile list, and the depfile include markers — grep for how
`vpxlog` appears and copy the pattern).
3. Call the inits — in `src/src/gui/sdlmain.cpp`, declare `void VPXLOG_Init();`
and `void VWEAWE_Init();` next to the other `*_Init()` prototypes; call
`VPXLOG_Init();` right after `GLIDE_Init();` and `VWEAWE_Init();` right
after `SBLASTER_Init();` (it needs the mixer initialized).
4. Build: 4. Build:
``` ```
cd emulator/src cd emulator/src
File diff suppressed because it is too large Load Diff
+807
View File
@@ -0,0 +1,807 @@
/* EMU8000 wavetable core, vendored from 86Box (src/include/86box/snd_emu8k.h,
* GPL-2.0-or-later -- license-compatible with this DOSBox-X fork). Local
* changes: this prelude only (stdint + WTBUFLEN, which 86Box provides via
* its sound.h). See vweawe.cpp for the VWE dual-AWE32 device built on it. */
#include <stdint.h>
#ifndef WTBUFLEN
#define WTBUFLEN 4096
#endif
#ifndef SOUND_EMU8K_H
#define SOUND_EMU8K_H
/* All these defines are in samples, not in bytes. */
#define EMU8K_MEM_ADDRESS_MASK 0xFFFFFF
#define EMU8K_RAM_MEM_START 0x200000
#define EMU8K_FM_MEM_ADDRESS 0xFFFFE0
#define EMU8K_RAM_POINTERS_MASK 0x3F
#define EMU8K_LFOCHORUS_SIZE 0x4000
/*
* Everything in this file assumes little endian
*/
/* used for the increment of oscillator position*/
typedef struct emu8k_mem_internal_t {
union {
uint64_t addr;
struct {
uint16_t fract_lw_address;
uint16_t fract_address;
uint32_t int_address;
};
};
} emu8k_mem_internal_t;
/* used for access to ram pointers from oscillator position. */
typedef struct emu8k_mem_pointers_t {
union {
uint32_t addr;
struct {
uint16_t lw_address;
uint8_t hb_address;
uint8_t unused_address;
};
};
} emu8k_mem_pointers_t;
/*
* From the Soundfount 2.0 fileformat Spec.:
*
An envelope generates a control signal in six phases.
When key-on occurs, a delay period begins during which the envelope value is zero.
The envelope then rises in a convex curve to a value of one during the attack phase.
" Note that the attack is convex; the curve is nominally such that when applied to a
decibel or semitone parameter, the result is linear in amplitude or Hz respectively"
When a value of one is reached, the envelope enters a hold phase during which it remains at one.
When the hold phase ends, the envelope enters a decay phase during which its value decreases linearly to a sustain level.
" For the Volume Envelope, the decay phase linearly ramps toward the sustain level, causing a constant dB change for each time unit. "
When the sustain level is reached, the envelope enters sustain phase, during which the envelope stays at the sustain level.
Whenever a key-off occurs, the envelope immediately enters a release phase during which the value linearly ramps from the current value to zero.
" For the Volume Envelope, the release phase linearly ramps toward zero from the current level, causing a constant dB change for each time unit"
When zero is reached, the envelope value remains at zero.
Modulation of pitch and filter cutoff are in octaves, semitones, and cents.
These parameters can be modulated to varying degree, either positively or negatively, by the modulation envelope.
The degree of modulation is specified in cents for the full-scale attack peak.
The volume envelope operates in dB, with the attack peak providing a full scale output, appropriately scaled by the initial volume.
The zero value, however, is actually zero gain.
The implementation in the EMU8000 provides for 96 dB of amplitude control.
When 96 dB of attenuation is reached in the final gain amplifier, an abrupt jump to zero gain
(infinite dB of attenuation) occurs. In a 16-bit system, this jump is inaudible
*/
/* It seems that the envelopes don't really have a decay/release stage,
* but instead they have a volume ramper that can be triggered
* automatically (after hold period), or manually (by activating release)
* and the "sustain" value is the target of any of both cases.
* Some programs like cubic player and AWEAmp use this, and it was
* described in the following way in Vince Vu/Judge Dredd's awe32p10.txt:
* If the MSB (most significant bit or bit 15) of this register is set,
* the Decay/Release will begin immediately, overriding the Delay, Attack,
* and Hold. Otherwise the Decay/Release will wait until the Delay, Attack,
* and Hold are finished. If you set the MSB of this register, you can use
* it as a volume ramper, as on the GUS. The upper byte (except the MSB),
* contains the destination volume, and the lower byte contains the ramp time.
*/
/* attack_amount is linear amplitude (added directly to value).
* ramp_amount_db is linear dB (added directly to value too, but needs conversion to get linear amplitude).
* value range is 21bits for both, linear amplitude being 1<<21 = 0dBFS and 0 = -96dBFS (which is shortcut to silence),
* and db amplutide being 0 = 0dBFS and -(1<<21) = -96dBFS (which is shortcut to silence).
* This allows to operate db values by simply adding them.
*/
typedef struct emu8k_envelope_t {
int state;
int32_t delay_samples;
int32_t hold_samples;
int32_t attack_samples;
int32_t value_amp_hz;
int32_t value_db_oct;
int32_t sustain_value_db_oct;
int32_t attack_amount_amp_hz;
int32_t ramp_amount_db_oct;
} emu8k_envelope_t;
typedef struct emu8k_chorus_eng_t {
int32_t write;
int32_t feedback;
int32_t delay_samples_central;
double lfodepth_multip;
double delay_offset_samples_right;
emu8k_mem_internal_t lfo_inc;
emu8k_mem_internal_t lfo_pos;
int32_t chorus_left_buffer[EMU8K_LFOCHORUS_SIZE];
int32_t chorus_right_buffer[EMU8K_LFOCHORUS_SIZE];
} emu8k_chorus_eng_t;
/* 32 * 242. 32 comes from the "right" room resso case.*/
#define MAX_REFL_SIZE 7744
/* Reverb parameters description, extracted from AST sources.
Mix level
Decay
Link return amp
Link type Switches between normal or panned
Room reso ( ms) L&R (Ref 6 +1)
Ref 1 x2 (11 ms)R
Ref 2 x4 (22 ms)R
Ref 3 x8 (44 ms)L
Ref 4 x13(71 ms)R
Ref 5 x19(105ms)L
Ref 6 x ( ms)R (multiplier changes with room reso)
Ref 1-6 filter L&R
Ref 1-6 amp L&R
Ref 1 feedback L&R
Ref 2 feedback L&R
Ref 3 feedback L&R
Ref 4 feedback L&R
Ref 5 feedback L&R
Ref 6 feedback L&R
*/
typedef struct emu8k_reverb_combfilter_t {
int read_pos;
int32_t reflection[MAX_REFL_SIZE];
float output_gain;
float feedback;
float damp1;
float damp2;
int bufsize;
int32_t filterstore;
} emu8k_reverb_combfilter_t;
typedef struct emu8k_reverb_eng_t {
int16_t out_mix;
int16_t link_return_amp; /* tail part output gain ? */
int8_t link_return_type;
uint8_t refl_in_amp;
emu8k_reverb_combfilter_t reflections[6];
emu8k_reverb_combfilter_t allpass[8];
emu8k_reverb_combfilter_t tailL;
emu8k_reverb_combfilter_t tailR;
emu8k_reverb_combfilter_t damper;
} emu8k_reverb_eng_t;
typedef struct emu8k_slide_t {
int32_t last;
} emu8k_slide_t;
typedef struct emu8k_voice_t {
union {
uint32_t cpf;
struct {
uint16_t cpf_curr_frac_addr; /* fractional part of the playing cursor. */
uint16_t cpf_curr_pitch; /* 0x4000 = no shift. Linear increment */
};
};
union {
uint32_t ptrx;
struct {
uint8_t ptrx_pan_aux;
uint8_t ptrx_revb_send;
uint16_t ptrx_pit_target; /* target pitch to which slide at curr_pitch speed. */
};
};
union {
uint32_t cvcf;
struct {
uint16_t cvcf_curr_filt_ctoff;
uint16_t cvcf_curr_volume;
};
};
emu8k_slide_t volumeslide;
union {
uint32_t vtft;
struct {
uint16_t vtft_filter_target;
uint16_t vtft_vol_target; /* written to by the envelope engine. */
};
};
/* These registers are used at least by the Windows drivers, and seem to be resetting
* something, similarly to targets and current, but... of what?
* what is curious is that if they are already zero, they are not written to, so it really
* looks like they are information about the status of the channel. (lfo position maybe?) */
uint32_t z2;
uint32_t z1;
union {
uint32_t psst;
struct {
uint16_t psst_lw_address;
uint8_t psst_hw_address;
uint8_t psst_pan;
};
#define PSST_LOOP_START_MASK 0x00FFFFFF /* In samples, i.e. uint16_t array[BOARD_RAM/2]; */
};
union {
uint32_t csl;
struct {
uint16_t csl_lw_address;
uint8_t csl_hw_address;
uint8_t csl_chor_send;
};
#define CSL_LOOP_END_MASK 0x00FFFFFF /* In samples, i.e. uint16_t array[BOARD_RAM/2]; */
};
union {
uint32_t ccca;
struct {
uint16_t ccca_lw_addr;
uint8_t ccca_hb_addr;
uint8_t ccca_qcontrol;
};
};
#define CCCA_FILTQ_GET(ccca) (ccca >> 28)
#define CCCA_FILTQ_SET(ccca, q) ccca = (ccca & 0x0FFFFFFF) | (q << 28)
/* Bit 27 should always be zero on EMU8000 */
#define CCCA_DMA_ACTIVE(ccca) (ccca & 0x04000000)
#define CCCA_DMA_WRITE_MODE(ccca) (ccca & 0x02000000)
#define CCCA_DMA_WRITE_RIGHT(ccca) (ccca & 0x01000000)
uint16_t envvol;
#define ENVVOL_NODELAY(envol) (envvol & 0x8000)
/* Verified with a soundfont bank. 7FFF is the minimum delay time, and 0 is the max delay time */
#define ENVVOL_TO_EMU_SAMPLES(envvol) (envvol & 0x8000) ? 0 : ((0x8000 - (envvol & 0x7FFF)) << 5)
uint16_t dcysusv;
#define DCYSUSV_IS_RELEASE(dcysusv) (dcysusv & 0x8000)
#define DCYSUSV_GENERATOR_ENGINE_ON(dcysusv) !(dcysusv & 0x0080)
#define DCYSUSV_SUSVALUE_GET(dcysusv) ((dcysusv >> 8) & 0x7F)
/* Inverting the range compared to documentation because the envelope runs from 0dBFS = 0 to -96dBFS = (1 <<21) */
#define DCYSUSV_SUS_TO_ENV_RANGE(susvalue) (((0x7F - susvalue) << 21) / 0x7F)
#define DCYSUSV_DECAYRELEASE_GET(dcysusv) (dcysusv & 0x7F)
uint16_t envval;
#define ENVVAL_NODELAY(enval) (envval & 0x8000)
/* Verified with a soundfont bank. 7FFF is the minimum delay time, and 0 is the max delay time */
#define ENVVAL_TO_EMU_SAMPLES(envval) (envval & 0x8000) ? 0 : ((0x8000 - (envval & 0x7FFF)) << 5)
uint16_t dcysus;
#define DCYSUS_IS_RELEASE(dcysus) (dcysus & 0x8000)
#define DCYSUS_SUSVALUE_GET(dcysus) ((dcysus >> 8) & 0x7F)
#define DCYSUS_SUS_TO_ENV_RANGE(susvalue) ((susvalue << 21) / 0x7F)
#define DCYSUS_DECAYRELEASE_GET(dcysus) (dcysus & 0x7F)
uint16_t atkhldv;
#define ATKHLDV_TRIGGER(atkhldv) !(atkhldv & 0x8000)
#define ATKHLDV_HOLD(atkhldv) ((atkhldv >> 8) & 0x7F)
#define ATKHLDV_HOLD_TO_EMU_SAMPLES(atkhldv) (4096 * (0x7F - ((atkhldv >> 8) & 0x7F)))
#define ATKHLDV_ATTACK(atkhldv) (atkhldv & 0x7F)
uint16_t lfo1val, lfo2val;
#define LFOxVAL_NODELAY(lfoxval) (lfoxval & 0x8000)
#define LFOxVAL_TO_EMU_SAMPLES(lfoxval) (lfoxval & 0x8000) ? 0 : ((0x8000 - (lfoxval & 0x7FFF)) << 5)
uint16_t atkhld;
#define ATKHLD_TRIGGER(atkhld) !(atkhld & 0x8000)
#define ATKHLD_HOLD(atkhld) ((atkhld >> 8) & 0x7F)
#define ATKHLD_HOLD_TO_EMU_SAMPLES(atkhld) (4096 * (0x7F - ((atkhld >> 8) & 0x7F)))
#define ATKHLD_ATTACK(atkhld) (atkhld & 0x7F)
uint16_t ip;
#define INTIAL_PITCH_CENTER 0xE000
#define INTIAL_PITCH_OCTAVE 0x1000
union {
uint16_t ifatn;
struct {
uint8_t ifatn_attenuation;
uint8_t ifatn_init_filter;
};
};
union {
uint16_t pefe;
struct {
int8_t pefe_modenv_filter_height;
int8_t pefe_modenv_pitch_height;
};
};
union {
uint16_t fmmod;
struct {
int8_t fmmod_lfo1_filt_mod;
int8_t fmmod_lfo1_vibrato;
};
};
union {
uint16_t tremfrq;
struct {
uint8_t tremfrq_lfo1_freq;
int8_t tremfrq_lfo1_tremolo;
};
};
union {
uint16_t fm2frq2;
struct {
uint8_t fm2frq2_lfo2_freq;
int8_t fm2frq2_lfo2_vibrato;
};
};
int env_engine_on;
emu8k_mem_internal_t addr;
emu8k_mem_internal_t loop_start;
emu8k_mem_internal_t loop_end;
int32_t initial_att;
int32_t initial_filter;
emu8k_envelope_t vol_envelope;
emu8k_envelope_t mod_envelope;
int64_t lfo1_speed;
int64_t lfo2_speed;
emu8k_mem_internal_t lfo1_count;
emu8k_mem_internal_t lfo2_count;
int32_t lfo1_delay_samples;
int32_t lfo2_delay_samples;
int vol_l;
int vol_r;
int16_t fixed_modenv_filter_height;
int16_t fixed_modenv_pitch_height;
int16_t fixed_lfo1_filt_mod;
int16_t fixed_lfo1_vibrato;
int16_t fixed_lfo1_tremolo;
int16_t fixed_lfo2_vibrato;
/* filter internal data. */
int filterq_idx;
int32_t filt_att;
int64_t filt_buffer[5];
} emu8k_voice_t;
typedef struct emu8k_t {
emu8k_voice_t voice[32];
uint16_t hwcf1;
uint16_t hwcf2;
uint16_t hwcf3;
uint32_t hwcf4;
uint32_t hwcf5;
uint32_t hwcf6;
uint32_t hwcf7;
uint16_t init1[32];
uint16_t init2[32];
uint16_t init3[32];
uint16_t init4[32];
uint32_t smalr;
uint32_t smarr;
uint32_t smalw;
uint32_t smarw;
uint16_t smld_buffer;
uint16_t smrd_buffer;
uint16_t wc;
uint16_t id;
/* The empty block is used to act as an unallocated memory returning zero. */
int16_t *ram;
int16_t *rom;
int16_t *empty;
/* RAM pointers are a way to avoid checking ram boundaries on read */
int16_t *ram_pointers[0x100];
uint32_t ram_end_addr;
int cur_reg;
int cur_voice;
int16_t out_l;
int16_t out_r;
emu8k_chorus_eng_t chorus_engine;
int32_t chorus_in_buffer[WTBUFLEN];
emu8k_reverb_eng_t reverb_engine;
int32_t reverb_in_buffer[WTBUFLEN];
int pos;
int32_t buffer[WTBUFLEN * 2];
uint16_t addr;
} emu8k_t;
void emu8k_change_addr(emu8k_t *emu8k, uint16_t emu_addr);
void emu8k_init(emu8k_t *emu8k, uint16_t emu_addr, int onboard_ram);
void emu8k_close(emu8k_t *emu8k);
void emu8k_reset_buffer(emu8k_t *emu8k);
void emu8k_update(emu8k_t *emu8k);
#define EMU8K_ROM_PATH "roms/sound/creative/awe32.raw"
/*
Section E - Introduction to the EMU8000 Chip
The EMU8000 has its roots in E-mu's Proteus sample playback
modules and their renowned Emulator sampler. The EMU8000 has
32 individual oscillators, each playing back at 44.1 kHz. By
incorporating sophisticated sample interpolation algorithms
and digital filtering, the EMU8000 is capable of producing
high fidelity sample playback.
The EMU8000 has an extensive modulation capability using two
sine-wave LFOs (Low Frequency Oscillator) and two multi-
stage envelope generators.
What exactly does modulation mean? Modulation means to
dynamically change a parameter of an audio signal, whether
it be the volume (amplitude modulation, or tremolo), pitch
(frequency modulation, or vibrato) or filter cutoff
frequency (filter modulation, or wah-wah). To modulate
something we would require a modulation source, and a
modulation destination. In the EMU8000, the modulation
sources are the LFOs and the envelope generators, and the
modulation destinations can be the pitch, the volume or the
filter cutoff frequency.
The EMU8000's LFOs and envelope generators provide a complex
modulation environment. Each sound producing element of the
EMU8000 consists of a resonant low-pass filter, two LFOs, in
which one modulates the pitch (LFO2), and the other
modulates pitch, filter cutoff and volume (LFO1)
simultaneously. There are two envelope generators; envelope
1 contours both pitch and filter cutoff simultaneously, and
envelope 2 contours volume. The output stage consists of an
effects engine that mixes the dry signals with the
Reverb/chorus level signals to produce the final mix.
What are the EMU8000 sound elements?
Each of the sound elements in an EMU8000 consists of the
following:
Oscillator
An oscillator is the source of an audio signal.
Low Pass Filter
The low pass filter is responsible for modifying the
timbres of an instrument. The low pass filter's filter
cutoff values can be varied from 100 Hz to 8000 Hz. By
changing the values of the filter cutoff, a myriad of
analogue sounding filter sweeps can be achieved. An
example of a GM instrument that makes use of filter sweep
is instrument number 87, Lead 7 (fifths).
Amplifier
The amplifier determines the loudness of an audio signal.
LFO1
An LFO, or Low Frequency Oscillator, is normally used to
periodically modulate, that is, change a sound parameter,
whether it be volume (amplitude modulation), pitch
(frequency modulation) or filter cutoff (filter
modulation). It operates at sub-audio frequency from
0.042 Hz to 10.71 Hz. The LFO1 in the EMU8000 modulates
the pitch, volume and filter cutoff simultaneously.
LFO2
The LFO2 is similar to the LFO1, except that it modulates
the pitch of the audio signal only.
Resonance
A filter alone would be like an equalizer, making a
bright audio signal duller, but the addition of resonance
greatly increases the creative potential of a filter.
Increasing the resonance of a filter makes it emphasize
signals at the cutoff frequency, giving the audio signal
a subtle wah-wah, that is, imagine a siren sound going
from bright to dull to bright again periodically.
LFO1 to Volume (Tremolo)
The LFO1's output is routed to the amplifier, with the
depth of oscillation determined by LFO1 to Volume. LFO1
to Volume produces tremolo, which is a periodic
fluctuation of volume. Lets say you are listening to a
piece of music on your home stereo system. When you
rapidly increase and decrease the playback volume, you
are creating tremolo effect, and the speed in which you
increases and decreases the volume is the tremolo rate
(which corresponds to the speed at which the LFO is
oscillating). An example of a GM instrument that makes
use of LFO1 to Volume is instrument number 45, Tremolo
Strings.
LFO1 to Filter Cutoff (Wah-Wah)
The LFO1's output is routed to the filter, with the depth
of oscillation determined by LFO1 to Filter. LFO1 to
Filter produces a periodic fluctuation in the filter
cutoff frequency, producing an effect very similar to
that of a wah-wah guitar (see resonance for a description
of wah-wah) An example of a GM instrument that makes
use of LFO1 to Filter Cutoff is instrument number 19,
Rock Organ.
LFO1 to Pitch (Vibrato)
The LFO1's output is routed to the oscillator, with the
depth of oscillation determined by LFO1 to Pitch. LFO1 to
Pitch produces a periodic fluctuation in the pitch of the
oscillator, producing a vibrato effect. An example of a
GM instrument that makes use of LFO1 to Pitch is
instrument number 57, Trumpet.
LFO2 to Pitch (Vibrato)
The LFO1 in the EMU8000 can simultaneously modulate
pitch, volume and filter. LFO2, on the other hand,
modulates only the pitch, with the depth of modulation
determined by LFO2 to Pitch. LFO2 to Pitch produces a
periodic fluctuation in the pitch of the oscillator,
producing a vibrato effect. When this is coupled with
LFO1 to Pitch, a complex vibrato effect can be achieved.
Volume Envelope
The character of a musical instrument is largely
determined by its volume envelope, the way in which the
level of the sound changes with time. For example,
percussive sounds usually start suddenly and then die
away, whereas a bowed sound might take quite some time to
start and then sustain at a more or less fixed level.
A six-stage envelope makes up the volume envelope of the
EMU8000. The six stages are delay, attack, hold, decay,
sustain and release. The stages can be described as
follows:
Delay The time between when a key is played and when
the attack phase begins
Attack The time it takes to go from zero to the peak
(full) level.
Hold The time the envelope will stay at the peak
level before starting the decay phase.
Decay The time it takes the envelope to go from the
peak level to the sustain level.
Sustain The level at which the envelope remains as long
as a key is held down.
Release The time it takes the envelope to fall to the
zero level after the key is released.
Using these six parameters can yield very realistic
reproduction of the volume envelope characteristics of
many musical instruments.
Pitch and Filter Envelope
The pitch and filter envelope is similar to the volume
envelope in that it has the same envelope stages. The
difference between them is that whereas the volume
envelope contours the volume of the instrument over time,
the pitch and filter envelope contours the pitch and
filter values of the instrument over time. The pitch
envelope is particularly useful in putting the finishing
touches in simulating a natural instrument. For example,
some wind instruments tend to go slightly sharp when they
are first blown, and this characteristic can be simulated
by setting up a pitch envelope with a fairly fast attack
and decay. The filter envelope, on the other hand, is
useful in creating synthetic sci-fi sound textures. An
example of a GM instrument that makes use of the filter
envelope is instrument number 86, Pad 8 (Sweep).
Pitch/Filter Envelope Modulation
These two parameters determine the modulation depth of
the pitch and filter envelope. In the wind instrument
example above, a small amount of pitch envelope
modulation is desirable to simulate its natural pitch
characteristics.
This rich modulation capability of the EMU8000 is fully
exploited by the SB AWE32 MIDI drivers. The driver also
provides you with a means to change these parameters over
MIDI in real time. Refer to the section "How do I change an
instrument's sound parameter in real time" for more
information.
Room 1 - 3
This group of reverb variation simulates the natural
ambiance of a room. Room 1 simulates a small room, Room 2
simulates a slightly bigger room, and Room 3 simulates a
big room.
Hall 1 - 2
This group of reverb variation simulates the natural
ambiance of a concert hall. It has greater depth than the
room variations. Again, Hall 1 simulates a small hall,
and Hall 2 simulates a larger hall.
Plate
Back in the old days, reverb effects were sometimes
produced using a metal plate, and this type of reverb
produces a metallic echo. The SB AWE32's Plate variation
simulates this form of reverb.
Delay
This reverb produces a delay, that is, echo effect.
Panning Delay
This reverb variation produces a delay effect that is
continuously panned left and right.
Chorus 1 - 4
Chorus produces a "beating" effect. The chorus effects
are more prominent going from chorus 1 to chorus 4.
Feedback Chorus
This chorus variation simulates a soft "swishing" effect.
Flanger
This chorus variation produces a more prominent feedback
chorus effect.
Short Delay
This chorus variation simulates a delay repeated in a
short time.
Short Delay (feed back)
This chorus variation simulates a short delay repeated
(feedback) many times.
Registers to write the Chorus Parameters to (all are 16-bit, unless noted):
(codified as in register,port,voice. port 0=0x620, 2=0x622, 4=0xA20, 6=0xA22, 8=0xE20)
( 3409 = register 3, port A20, voice 9)
0x3409
0x340C
0x3603
0x1409 (32-Bit)
0x140A (32-Bit)
then write 0x8000 to 0x140D (32-Bit)
and then 0x0000 to 0x140E (32-Bit)
Chorus Parameters:
Chorus 1 Chorus 2 Chorus 3 Chorus 4 Feedback Flanger
0xE600 0xE608 0xE610 0xE620 0xE680 0xE6E0
0x03F6 0x031A 0x031A 0x0269 0x04D3 0x044E
0xBC2C 0xBC6E 0xBC84 0xBC6E 0xBCA6 0xBC37
0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x006D 0x017C 0x0083 0x017C 0x005B 0x0026
Short Delay Short Delay + Feedback
0xE600 0xE6C0
0x0B06 0x0B06
0xBC00 0xBC00
0xE000 0xE000
0x0083 0x0083
// Chorus Params
typedef struct {
WORD FbkLevel; // Feedback Level (0xE600-0xE6FF)
WORD Delay; // Delay (0-0x0DA3) [1/44100 sec]
WORD LfoDepth; // LFO Depth (0xBC00-0xBCFF)
DWORD DelayR; // Right Delay (0-0xFFFFFFFF) [1/256/44100 sec]
DWORD LfoFreq; // LFO Frequency (0-0xFFFFFFFF)
} CHORUS_TYPE;
Registers to write the Reverb Parameters to (they are all 16-bit):
(codified as in register,port,voice. port 0=0x620, 2=0x622, 4=0xA20, 6=0xA22, 8=0xE20)
( 3409 = register 3, port A20, voice 9)
0x2403,0x2405,0x361F,0x2407,0x2614,0x2616,0x240F,0x2417,
0x241F,0x2607,0x260F,0x2617,0x261D,0x261F,0x3401,0x3403,
0x2409,0x240B,0x2411,0x2413,0x2419,0x241B,0x2601,0x2603,
0x2609,0x260B,0x2611,0x2613
Reverb Parameters:
Room 1:
0xB488,0xA450,0x9550,0x84B5,0x383A,0x3EB5,0x72F4,0x72A4,
0x7254,0x7204,0x7204,0x7204,0x4416,0x4516,0xA490,0xA590,
0x842A,0x852A,0x842A,0x852A,0x8429,0x8529,0x8429,0x8529,
0x8428,0x8528,0x8428,0x8528
Room 2:
0xB488,0xA458,0x9558,0x84B5,0x383A,0x3EB5,0x7284,0x7254,
0x7224,0x7224,0x7254,0x7284,0x4448,0x4548,0xA440,0xA540,
0x842A,0x852A,0x842A,0x852A,0x8429,0x8529,0x8429,0x8529,
0x8428,0x8528,0x8428,0x8528
Room 3:
0xB488,0xA460,0x9560,0x84B5,0x383A,0x3EB5,0x7284,0x7254,
0x7224,0x7224,0x7254,0x7284,0x4416,0x4516,0xA490,0xA590,
0x842C,0x852C,0x842C,0x852C,0x842B,0x852B,0x842B,0x852B,
0x842A,0x852A,0x842A,0x852A
Hall 1:
0xB488,0xA470,0x9570,0x84B5,0x383A,0x3EB5,0x7284,0x7254,
0x7224,0x7224,0x7254,0x7284,0x4448,0x4548,0xA440,0xA540,
0x842B,0x852B,0x842B,0x852B,0x842A,0x852A,0x842A,0x852A,
0x8429,0x8529,0x8429,0x8529
Hall 2:
0xB488,0xA470,0x9570,0x84B5,0x383A,0x3EB5,0x7254,0x7234,
0x7224,0x7254,0x7264,0x7294,0x44C3,0x45C3,0xA404,0xA504,
0x842A,0x852A,0x842A,0x852A,0x8429,0x8529,0x8429,0x8529,
0x8428,0x8528,0x8428,0x8528
Plate:
0xB4FF,0xA470,0x9570,0x84B5,0x383A,0x3EB5,0x7234,0x7234,
0x7234,0x7234,0x7234,0x7234,0x4448,0x4548,0xA440,0xA540,
0x842A,0x852A,0x842A,0x852A,0x8429,0x8529,0x8429,0x8529,
0x8428,0x8528,0x8428,0x8528
Delay:
0xB4FF,0xA470,0x9500,0x84B5,0x333A,0x39B5,0x7204,0x7204,
0x7204,0x7204,0x7204,0x72F4,0x4400,0x4500,0xA4FF,0xA5FF,
0x8420,0x8520,0x8420,0x8520,0x8420,0x8520,0x8420,0x8520,
0x8420,0x8520,0x8420,0x8520
Panning Delay:
0xB4FF,0xA490,0x9590,0x8474,0x333A,0x39B5,0x7204,0x7204,
0x7204,0x7204,0x7204,0x72F4,0x4400,0x4500,0xA4FF,0xA5FF,
0x8420,0x8520,0x8420,0x8520,0x8420,0x8520,0x8420,0x8520,
0x8420,0x8520,0x8420,0x8520
Registers to write the EQ Parameters to (16-Bit):
(codified as in register,port,voice. port 0=0x620, 2=0x622, 4=0xA20, 6=0xA22, 8=0xE20)
( 3409 = register 3, port A20, voice 9)
Bass:
0x3601
0x3611
Treble:
0x3411
0x3413
0x341B
0x3607
0x360B
0x360D
0x3617
0x3619
Total:
write the 0x0263 + 3rd parameter of the Bass EQ + 9th parameter of Treble EQ to 0x3615.
write the 0x8363 + 3rd parameter of the Bass EQ + 9th parameter of Treble EQ to 0x3615.
Bass Parameters:
0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
0xD26A 0xD25B 0xD24C 0xD23D 0xD21F 0xC208 0xC219 0xC22A 0xC24C 0xC26E 0xC248 0xC26A
0xD36A 0xD35B 0xD34C 0xD33D 0xC31F 0xC308 0xC308 0xC32A 0xC34C 0xC36E 0xC384 0xC36A
0x0000 0x0000 0x0000 0x0000 0x0000 0x0001 0x0001 0x0001 0x0001 0x0001 0x0002 0x0002
Treble Parameters:
0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
0x821E 0x821E 0x821E 0x821E 0x821E 0x821E 0x821E 0x821E 0x821E 0x821E 0x821D 0x821C
0xC26A 0xC25B 0xC24C 0xC23D 0xC21F 0xD208 0xD208 0xD208 0xD208 0xD208 0xD219 0xD22A
0x031E 0x031E 0x031E 0x031E 0x031E 0x031E 0x031E 0x031E 0x031E 0x031E 0x031D 0x031C
0xC36A 0xC35B 0xC34C 0xC33D 0xC31F 0xD308 0xD308 0xD308 0xD308 0xD308 0xD319 0xD32A
0x021E 0x021E 0x021E 0x021E 0x021E 0x021E 0x021D 0x021C 0x021A 0x0219 0x0219 0x0219
0xD208 0xD208 0xD208 0xD208 0xD208 0xD208 0xD219 0xD22A 0xD24C 0xD26E 0xD26E 0xD26E
0x831E 0x831E 0x831E 0x831E 0x831E 0x831E 0x831D 0x831C 0x831A 0x8319 0x8319 0x8319
0xD308 0xD308 0xD308 0xD308 0xD308 0xD308 0xD3019 0xD32A 0xD34C 0xD36E 0xD36E 0xD36E
0x0001 0x0001 0x0001 0x0001 0x0001 0x0002 0x0002 0x0002 0x0002 0x0002 0x0002 0x0002
*/
#endif /*SOUND_EMU8K_H*/
+48
View File
@@ -0,0 +1,48 @@
/* Minimal 86Box environment shim for the vendored EMU8000 core (emu8k.cpp).
* Supplies exactly what snd_emu8k.c pulled from 86Box headers:
* - io_sethandler/io_removehandler (implemented in vweawe.cpp on top of
* DOSBox-X IO_Register*Handler, with the 86Box handler signatures)
* - wavetable_pos_global (the render-target sample clock; vweawe.cpp sets
* it per mixer block instead of 86Box's per-frame sound clock)
* - pclog/pclog_ex/fatal logging and the UNUSED() parameter marker
* - emu8k_shim_rom_fopen(): optional AWE32 GM ROM via $VWE_AWE_ROM
*/
#ifndef VWE_EMU8K_SHIM_H
#define VWE_EMU8K_SHIM_H
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 86Box io.h handler signatures (priv-pointer style) */
typedef uint8_t (*emu8k_io_inb_t)(uint16_t addr, void *priv);
typedef uint16_t (*emu8k_io_inw_t)(uint16_t addr, void *priv);
typedef uint32_t (*emu8k_io_inl_t)(uint16_t addr, void *priv);
typedef void (*emu8k_io_outb_t)(uint16_t addr, uint8_t val, void *priv);
typedef void (*emu8k_io_outw_t)(uint16_t addr, uint16_t val, void *priv);
typedef void (*emu8k_io_outl_t)(uint16_t addr, uint32_t val, void *priv);
void io_sethandler(uint16_t base, int size,
emu8k_io_inb_t inb, emu8k_io_inw_t inw, emu8k_io_inl_t inl,
emu8k_io_outb_t outb, emu8k_io_outw_t outw,
emu8k_io_outl_t outl, void *priv);
void io_removehandler(uint16_t base, int size,
emu8k_io_inb_t inb, emu8k_io_inw_t inw,
emu8k_io_inl_t inl, emu8k_io_outb_t outb,
emu8k_io_outw_t outw, emu8k_io_outl_t outl, void *priv);
/* render-target sample position for emu8k_update() (86Box sound.h) */
extern int wavetable_pos_global;
FILE *emu8k_shim_rom_fopen(void);
/* samples elapsed since the last mixer render (WC interpolation) */
unsigned emu8k_shim_wc_extra(void);
#define pclog(...) fprintf(stderr, __VA_ARGS__)
#define pclog_ex(fmt, ap) vfprintf(stderr, fmt, ap)
#define fatal(...) fprintf(stderr, __VA_ARGS__)
#define UNUSED(arg) arg
#endif
+423
View File
@@ -0,0 +1,423 @@
/* 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); per-card host routing for the real
* 4-speaker cockpit comes later (see 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
* <dir>/awe_front.s16 + <dir>/awe_rear.s16
* 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 <windows.h>
#include <mmsystem.h>
#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 int16_t awe_cardbuf[2][AWE_SLOT_FRAMES * 2];
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;
if (v > 32767) v = 32767; else if (v < -32768) v = -32768;
awe_cardbuf[ci][i] = (int16_t)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 * 2; i++) {
int32_t v = (int32_t)awe_cardbuf[0][i] + awe_cardbuf[1][i];
if (v > 32767) v = 32767; else if (v < -32768) v = -32768;
out[i] = (int16_t)v;
int a0 = awe_cardbuf[0][i] < 0 ? -awe_cardbuf[0][i]
: awe_cardbuf[0][i];
int a1 = awe_cardbuf[1][i] < 0 ? -awe_cardbuf[1][i]
: awe_cardbuf[1][i];
if (a0 > peak[0]) peak[0] = a0;
if (a1 > peak[1]) peak[1] = a1;
}
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)
fwrite(awe_cardbuf[0], 4, AWE_SLOT_FRAMES, awe_dump_front);
if (awe_dump_rear)
fwrite(awe_cardbuf[1], 4, AWE_SLOT_FRAMES, awe_dump_rear);
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\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]);
peak[ci] = 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;
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" : "");
}