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:
@@ -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/dosbox-x/
|
||||
/emulator/image/
|
||||
|
||||
@@ -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.
@@ -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 2–3.
|
||||
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.
@@ -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
|
||||
(`emulator/src/`, ~490 MB) is git-ignored.
|
||||
|
||||
- **`vpxlog.cpp`** — Phase 1 logging device. Impersonates the INMOS C012 link
|
||||
adapter at I/O base `0x150`, answers status reads so the game keeps
|
||||
transmitting, and logs every access to `$VPXLOG`. (Phase 2 will grow this
|
||||
into a responding transputer-monitor + i860-loader + frame-stream renderer,
|
||||
or fork into a separate `vpx.cpp`.)
|
||||
- **`vpxlog.cpp`** — the Division VPX link adapter (INMOS C012 at I/O base
|
||||
`0x150`), grown through Phase 3: iserver handshake responder, FIFO scene
|
||||
decode, GL Division renderer (gallery shading model), VDB video-head
|
||||
splitter windows.
|
||||
- **`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
|
||||
|
||||
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
|
||||
`vpxlog.cpp` to `libhardware_a_SOURCES` (we inserted it after `glide.cpp`).
|
||||
3. Call the init — in `src/src/gui/sdlmain.cpp`, declare `void VPXLOG_Init();`
|
||||
next to the other `*_Init()` prototypes and call `VPXLOG_Init();` right after
|
||||
`GLIDE_Init();` in the machine bring-up sequence.
|
||||
2. Add them to the hardware build — in `src/src/hardware/Makefile.am`, append
|
||||
`vpxlog.cpp emu8k.cpp vweawe.cpp` to `libhardware_a_SOURCES` (we inserted
|
||||
them after `glide.cpp`; if you edit the generated `Makefile`/`Makefile.in`
|
||||
by hand instead of re-running automake, also mirror the `$(OBJEXT)` list,
|
||||
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:
|
||||
```
|
||||
cd emulator/src
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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*/
|
||||
@@ -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
|
||||
@@ -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" : "");
|
||||
}
|
||||
Reference in New Issue
Block a user