diff --git a/.gitignore b/.gitignore index 793eecc..a916f75 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/emulator/SOUND-NOTES.md b/emulator/SOUND-NOTES.md new file mode 100644 index 0000000..96cc287 --- /dev/null +++ b/emulator/SOUND-NOTES.md @@ -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=` -> **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. diff --git a/emulator/roms/1mgm.sf2 b/emulator/roms/1mgm.sf2 new file mode 100644 index 0000000..00862e9 Binary files /dev/null and b/emulator/roms/1mgm.sf2 differ diff --git a/emulator/roms/README.md b/emulator/roms/README.md new file mode 100644 index 0000000..a5db0da --- /dev/null +++ b/emulator/roms/README.md @@ -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. diff --git a/emulator/roms/awe32.raw b/emulator/roms/awe32.raw new file mode 100644 index 0000000..4a1b084 Binary files /dev/null and b/emulator/roms/awe32.raw differ diff --git a/emulator/vpx-device/README.md b/emulator/vpx-device/README.md index 1d2e574..a9ca0e4 100644 --- a/emulator/vpx-device/README.md +++ b/emulator/vpx-device/README.md @@ -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 diff --git a/emulator/vpx-device/emu8k.cpp b/emulator/vpx-device/emu8k.cpp new file mode 100644 index 0000000..d269982 --- /dev/null +++ b/emulator/vpx-device/emu8k.cpp @@ -0,0 +1,2399 @@ +#include +#include +#include +#include +#include +#include +#include +#define _USE_MATH_DEFINES +#include +#define HAVE_STDARG_H + +/* EMU8000 wavetable core, vendored from 86Box (src/sound/snd_emu8k.c, + * GPL-2.0-or-later -- license-compatible with this DOSBox-X fork). + * Local changes, kept minimal so upstream diffs stay readable: + * 1. the 86Box include block below is replaced by emu8k_shim.h + emu8k.h; + * 2. emu8k_init()'s ROM load goes through emu8k_shim_rom_fopen() (env + * VWE_AWE_ROM, optional -- absent ROM = silent GM presets, not fatal) + * and the callocs gain C++ casts. + * The VWE dual-AWE32 device built on this core lives in vweawe.cpp. */ +#include "emu8k_shim.h" +#include "emu8k.h" + +#if !defined FILTER_INITIAL && !defined FILTER_MOOG && !defined FILTER_CONSTANT +#if 0 +#define FILTER_INITIAL +#endif +# define FILTER_MOOG +#if 0 +#define FILTER_CONSTANT +#endif +#endif + +#if !defined RESAMPLER_LINEAR && !defined RESAMPLER_CUBIC +#if 0 +#define RESAMPLER_LINEAR +#endif +# define RESAMPLER_CUBIC +#endif + +#if 0 +#define EMU8K_DEBUG_REGISTERS +#endif + +char *PORT_NAMES[][8] = { + /* Data 0 ( 0x620/0x622) */ + { + "AWE_CPF", + "AWE_PTRX", + "AWE_CVCF", + "AWE_VTFT", + "Unk-620-4", + "Unk-620-5", + "AWE_PSST", + "AWE_CSL", + }, + /* Data 1 0xA20 */ + { + "AWE_CCCA", + 0, + /* + "AWE_HWCF4" + "AWE_HWCF5" + "AWE_HWCF6" + "AWE_HWCF7" + "AWE_SMALR" + "AWE_SMARR" + "AWE_SMALW" + "AWE_SMARW" + "AWE_SMLD" + "AWE_SMRD" + "AWE_WC" + "AWE_HWCF1" + "AWE_HWCF2" + "AWE_HWCF3" + */ + 0, //"AWE_INIT1", + 0, //"AWE_INIT3", + "AWE_ENVVOL", + "AWE_DCYSUSV", + "AWE_ENVVAL", + "AWE_DCYSUS", + }, + /* Data 2 0xA22 */ + { + "AWE_CCCA", + 0, + 0, //"AWE_INIT2", + 0, //"AWE_INIT4", + "AWE_ATKHLDV", + "AWE_LFO1VAL", + "AWE_ATKHLD", + "AWE_LFO2VAL", + }, + /* Data 3 0xE20 */ + { + "AWE_IP", + "AWE_IFATN", + "AWE_PEFE", + "AWE_FMMOD", + "AWE_TREMFRQ", + "AWE_FM2FRQ2", + 0, + 0, + }, +}; + +enum { + ENV_STOPPED = 0, + ENV_DELAY = 1, + ENV_ATTACK = 2, + ENV_HOLD = 3, + // ENV_DECAY = 4, + ENV_SUSTAIN = 5, + // ENV_RELEASE = 6, + ENV_RAMP_DOWN = 7, + ENV_RAMP_UP = 8 +}; + +static int random_helper = 0; +int dmareadbit = 0; +int dmawritebit = 0; + +/* cubic and linear tables resolution. Note: higher than 10 does not improve the result. */ +#define CUBIC_RESOLUTION_LOG 10 +#define CUBIC_RESOLUTION (1 << CUBIC_RESOLUTION_LOG) +/* cubic_table coefficients. */ +static float cubic_table[CUBIC_RESOLUTION * 4]; + +/* conversion from current pitch to linear frequency change (in 32.32 fixed point). */ +static int64_t freqtable[65536]; +/* Conversion from initial attenuation to 16 bit unsigned lineal amplitude (currently only a way to update volume target register) */ +static int32_t attentable[256]; +/* Conversion from envelope dbs (once rigth shifted) (0 = 0dBFS, 65535 = -96dbFS and silence ) to 16 bit unsigned lineal amplitude, + * to convert to current volume. (0 to 65536) */ +static int32_t env_vol_db_to_vol_target[65537]; +/* Same as above, but to convert amplitude (once rigth shifted) (0 to 65536) to db (0 = 0dBFS, 65535 = -96dbFS and silence ). + * it is needed so that the delay, attack and hold phase can be added to initial attenuation and tremolo */ +static int32_t env_vol_amplitude_to_db[65537]; +/* Conversion from envelope herts (once right shifted) to octave . it is needed so that the delay, attack and hold phase can be + * added to initial pitch ,lfos pitch , initial filter and lfo filter */ +static int32_t env_mod_hertz_to_octave[65537]; +/* Conversion from envelope amount to time in samples. */ +static int32_t env_attack_to_samples[128]; + +/* This table has been generated using the following formula: + * Get the amount of dBs that have to be added each sample to reach 96dBs in the amount + * of time determined by the encoded value "i". + * float d = 1.0/((env_decay_to_millis[i]/96.0)*44.1); + * int result = round(d*21845); + * The multiplication by 21845 gives a minimum value of 1, and a maximum accumulated value of 1<<21 + * The accumulated value has to be converted to amplitude, and that can be done with the + * env_vol_db_to_vol_target and shifting by 8 + * In other words, the unit of the table is the 1/21845th of a dB per sample frame, to be added or + * substracted to the accumulating value_db of the envelope. */ +static int32_t env_decay_to_dbs_or_oct[128] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 20, 21, 22, 23, 24, 25, 27, 28, 29, 30, 32, + 33, 34, 36, 38, 39, 41, 43, 45, 49, 51, 53, 55, 58, 60, 63, 66, + 69, 72, 75, 78, 82, 85, 89, 93, 97, 102, 106, 111, 116, 121, 126, 132, + 138, 144, 150, 157, 164, 171, 179, 186, 195, 203, 212, 222, 232, 243, 253, 264, + 276, 288, 301, 315, 328, 342, 358, 374, 390, 406, 425, 444, 466, 485, 506, 528, + 553, 580, 602, 634, 660, 689, 721, 755, 780, 820, 849, 897, 932, 970, 1012, 1057, + 1106, 1160, 1219, 1285, 1321, 1399, 1441, 1534, 1585, 1640, 1698, 1829, 1902, 1981, 2068, 2162 +}; + +/* The table "env_decay_to_millis" is based on the table "decay_time_tbl" found in the freebsd/linux + * AWE32 driver. + * I tried calculating it using the instructions in awe32p10 from Judge Dredd, but the formula there + * is wrong. + * + */ +#if 0 +static int32_t env_decay_to_millis[128] = { + 0, 45120, 22614, 15990, 11307, 9508, 7995, 6723, 5653, 5184, 4754, 4359, 3997, 3665, 3361, 3082, + 2828, 2765, 2648, 2535, 2428, 2325, 2226, 2132, 2042, 1955, 1872, 1793, 1717, 1644, 1574, 1507, + 1443, 1382, 1324, 1267, 1214, 1162, 1113, 1066, 978, 936, 897, 859, 822, 787, 754, 722, + 691, 662, 634, 607, 581, 557, 533, 510, 489, 468, 448, 429, 411, 393, 377, 361, + 345, 331, 317, 303, 290, 278, 266, 255, 244, 234, 224, 214, 205, 196, 188, 180, + 172, 165, 158, 151, 145, 139, 133, 127, 122, 117, 112, 107, 102, 98, 94, 90, + 86, 82, 79, 75, 72, 69, 66, 63, 61, 58, 56, 53, 51, 49, 47, 45, + 43, 41, 39, 37, 36, 34, 33, 31, 30, 29, 28, 26, 25, 24, 23, 22, +}; +#endif + +/* Table represeting the LFO waveform (signed 16bits with 32768 max int. >> 15 to move back to +/-1 range). */ +static int32_t lfotable[65536]; +/* Table to transform the speed parameter to emu8k_mem_internal_t range. */ +static int64_t lfofreqtospeed[256]; + +/* LFO used for the chorus. a sine wave.(signed 16bits with 32768 max int. >> 15 to move back to +/-1 range). */ +static double chortable[65536]; + +static const int REV_BUFSIZE_STEP = 242; + +/* These lines come from the awe32faq, describing the NRPN control for the initial filter + * where it describes a linear increment filter instead of an octave-incremented one. + * NRPN LSB 21 (Initial Filter Cutoff) + * Range : [0, 127] + * Unit : 62Hz + * Filter cutoff from 100Hz to 8000Hz + + * This table comes from the awe32faq, describing the NRPN control for the filter Q. + * I don't know if is meant to be interpreted as the actual measured output of the + * filter or what. Especially, I don't understand the "low" and "high" ranges. + * What is otherwise documented is that the Q ranges from 0dB to 24dB and the attenuation + * is half of the Q ( i.e. for 12dB Q, attenuate the input signal with -6dB) +Coeff Low Fc(Hz)Low Q(dB)High Fc(kHz)High Q(dB)DC Attenuation(dB) +* 0 92 5 Flat Flat -0.0 +* 1 93 6 8.5 0.5 -0.5 +* 2 94 8 8.3 1 -1.2 +* 3 95 10 8.2 2 -1.8 +* 4 96 11 8.1 3 -2.5 +* 5 97 13 8.0 4 -3.3 +* 6 98 14 7.9 5 -4.1 +* 7 99 16 7.8 6 -5.5 +* 8 100 17 7.7 7 -6.0 +* 9 100 19 7.5 9 -6.6 +* 10 100 20 7.4 10 -7.2 +* 11 100 22 7.3 11 -7.9 +* 12 100 23 7.2 13 -8.5 +* 13 100 25 7.1 15 -9.3 +* 14 100 26 7.1 16 -10.1 +* 15 100 28 7.0 18 -11.0 +* +* Attenuation as above, codified in amplitude.*/ +static int32_t filter_atten[16] = { + 65536, 61869, 57079, 53269, 49145, 44820, 40877, 34792, 32845, 30653, 28607, + 26392, 24630, 22463, 20487, 18470 +}; + +/*Coefficients for the filters for a defined Q and cutoff.*/ +static int32_t filt_coeffs[16][256][3]; + +#define READ16_SWITCH(addr, var) \ + switch ((addr) &2) { \ + case 0: \ + ret = (var) &0xffff; \ + break; \ + case 2: \ + ret = ((var) >> 16) & 0xffff; \ + break; \ + } + +#define WRITE16_SWITCH(addr, var, val) \ + switch ((addr) &2) { \ + case 0: \ + var = (var & 0xffff0000) | (val); \ + break; \ + case 2: \ + var = (var & 0x0000ffff) | ((val) << 16); \ + break; \ + } + +#ifdef EMU8K_DEBUG_REGISTERS +uint32_t dw_value = 0; +uint32_t last_read = 0; +uint32_t last_write = 0; +uint32_t rep_count_r = 0; +uint32_t rep_count_w = 0; + +# define READ16(addr, var) \ + READ16_SWITCH(addr, var) \ + { \ + const char *name = 0; \ + switch (addr & 0xF02) { \ + case 0x600: \ + case 0x602: \ + name = PORT_NAMES[0][emu8k->cur_reg]; \ + break; \ + case 0xA00: \ + name = PORT_NAMES[1][emu8k->cur_reg]; \ + break; \ + case 0xA02: \ + name = PORT_NAMES[2][emu8k->cur_reg]; \ + break; \ + } \ + if (name == 0) { \ + /*emu8k_log("EMU8K READ %04X-%02X(%d): %04X\n",addr,(emu8k->cur_reg)<<5|emu8k->cur_voice, emu8k->cur_voice,ret);*/ \ + } else { \ + emu8k_log("EMU8K READ %s(%d) (%d): %04X\n", name, (addr & 0x2), emu8k->cur_voice, ret); \ + } \ + } +# define WRITE16(addr, var, val) \ + WRITE16_SWITCH(addr, var, val) \ + { \ + const char *name = 0; \ + switch (addr & 0xF02) { \ + case 0x600: \ + case 0x602: \ + name = PORT_NAMES[0][emu8k->cur_reg]; \ + break; \ + case 0xA00: \ + name = PORT_NAMES[1][emu8k->cur_reg]; \ + break; \ + case 0xA02: \ + name = PORT_NAMES[2][emu8k->cur_reg]; \ + break; \ + } \ + if (name == 0) { \ + /*emu8k_log("EMU8K WRITE %04X-%02X(%d): %04X\n",addr,(emu8k->cur_reg)<<5|emu8k->cur_voice,emu8k->cur_voice, val);*/ \ + } else { \ + emu8k_log("EMU8K WRITE %s(%d) (%d): %04X\n", name, (addr & 0x2), emu8k->cur_voice, val); \ + } \ + } + +#else +# define READ16(addr, var) READ16_SWITCH(addr, var) +# define WRITE16(addr, var, val) WRITE16_SWITCH(addr, var, val) +#endif // EMU8K_DEBUG_REGISTERS + +#ifdef ENABLE_EMU8K_LOG +int emu8k_do_log = ENABLE_EMU8K_LOG; + +static void +emu8k_log(const char *fmt, ...) +{ + va_list ap; + + if (emu8k_do_log) { + va_start(ap, fmt); + pclog_ex(fmt, ap); + va_end(ap); + } +} +#else +# define emu8k_log(fmt, ...) +#endif + +static inline int16_t +EMU8K_READ(emu8k_t *emu8k, uint32_t addr) +{ + register const emu8k_mem_pointers_t addrmem = { { addr } }; + return emu8k->ram_pointers[addrmem.hb_address][addrmem.lw_address]; +} + +#if NOTUSED +static inline int16_t +EMU8K_READ_INTERP_LINEAR(emu8k_t *emu8k, uint32_t int_addr, uint16_t fract) +{ + /* The interpolation in AWE32 used a so-called patented 3-point interpolation + * ( I guess some sort of spline having one point before and one point after). + * Also, it has the consequence that the playback is delayed by one sample. + * I simulate the "one sample later" than the address with addr+1 and addr+2 + * instead of +0 and +1 */ + int16_t dat1 = EMU8K_READ(emu8k, int_addr + 1); + int32_t dat2 = EMU8K_READ(emu8k, int_addr + 2); + dat1 += ((dat2 - (int32_t) dat1) * fract) >> 16; + return dat1; +} +#endif + +static inline int32_t +EMU8K_READ_INTERP_CUBIC(emu8k_t *emu8k, uint32_t int_addr, uint16_t fract) +{ + /*Since there are four floats in the table for each fraction, the position is 16byte aligned. */ + fract >>= 16 - CUBIC_RESOLUTION_LOG; + fract <<= 2; + + /* TODO: I still have to verify how this works, but I think that + * the card could use two oscillators (usually 31 and 32) where it would + * be writing the OPL3 output, and to which, chorus and reverb could be applied to get + * those effects for OPL3 sounds.*/ +#if 0 + if ((addr & EMU8K_FM_MEM_ADDRESS) == EMU8K_FM_MEM_ADDRESS) {} +#endif + + /* This is cubic interpolation. + * Not the same than 3-point interpolation, but a better approximation than linear + * interpolation. + * Also, it takes into account the "Note that the actual audio location is the point + * 1 word higher than this value due to interpolation offset". + * That's why the pointers are 0, 1, 2, 3 and not -1, 0, 1, 2 */ + int32_t dat2 = EMU8K_READ(emu8k, int_addr + 1); + const float *table = &cubic_table[fract]; + const int32_t dat1 = EMU8K_READ(emu8k, int_addr); + const int32_t dat3 = EMU8K_READ(emu8k, int_addr + 2); + const int32_t dat4 = EMU8K_READ(emu8k, int_addr + 3); + /* Note: I've ended using float for the table values to avoid some cases of integer overflow. */ + dat2 = dat1 * table[0] + dat2 * table[1] + dat3 * table[2] + dat4 * table[3]; + return dat2; +} + +static inline void +EMU8K_WRITE(emu8k_t *emu8k, uint32_t addr, uint16_t val) +{ + addr &= EMU8K_MEM_ADDRESS_MASK; + if (!emu8k->ram || addr < EMU8K_RAM_MEM_START || addr >= EMU8K_FM_MEM_ADDRESS) + return; + + /* It looks like if an application writes to a memory part outside of the available + * amount on the card, it wraps, and opencubicplayer uses that to detect the amount + * of memory, as opposed to simply check at the address that it has just tried to write. */ + while (addr >= emu8k->ram_end_addr) + addr -= emu8k->ram_end_addr - EMU8K_RAM_MEM_START; + + emu8k->ram[addr - EMU8K_RAM_MEM_START] = val; +} + +uint16_t +emu8k_inw(uint16_t addr, void *priv) +{ + emu8k_t *emu8k = (emu8k_t *) priv; + uint16_t ret = 0xffff; + +#ifdef EMU8K_DEBUG_REGISTERS + if (addr == 0xE22) { + emu8k_log("EMU8K READ POINTER: %d\n", + ((0x80 | ((random_helper + 1) & 0x1F)) << 8) | (emu8k->cur_reg << 5) | emu8k->cur_voice); + } else if ((addr & 0xF00) == 0x600) { + /* These are automatically reported by READ16 */ + if (rep_count_r > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_r); + rep_count_r = 0; + } + last_read = 0; + } else if ((addr & 0xF00) == 0xA00 && emu8k->cur_reg == 0) { + /* These are automatically reported by READ16 */ + if (rep_count_r > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_r); + rep_count_r = 0; + } + last_read = 0; + } else if ((addr & 0xF00) == 0xA00 && emu8k->cur_reg == 1) { + uint32_t tmpz = ((addr & 0xF00) << 16) | (emu8k->cur_reg << 5); + if (tmpz != last_read) { + if (rep_count_r > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_r); + rep_count_r = 0; + } + last_read = tmpz; + emu8k_log("EMU8K READ RAM I/O or configuration or clock \n"); + } + // emu8k_log("EMU8K READ %04X-%02X(%d/%d)\n",addr,(emu8k->cur_reg)<<5|emu8k->cur_voice, emu8k->cur_reg, emu8k->cur_voice); + } else if ((addr & 0xF00) == 0xA00 && (emu8k->cur_reg == 2 || emu8k->cur_reg == 3)) { + uint32_t tmpz = ((addr & 0xF00) << 16); + if (tmpz != last_read) { + if (rep_count_r > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_r); + rep_count_r = 0; + } + last_read = tmpz; + emu8k_log("EMU8K READ INIT \n"); + } + // emu8k_log("EMU8K READ %04X-%02X(%d/%d)\n",addr,(emu8k->cur_reg)<<5|emu8k->cur_voice, emu8k->cur_reg, emu8k->cur_voice); + } else { + uint32_t tmpz = (addr << 16) | (emu8k->cur_reg << 5) | emu8k->cur_voice; + if (tmpz != last_read) { + char *name = 0; + uint16_t val = 0xBAAD; + if (addr == 0xA20) { + name = PORT_NAMES[1][emu8k->cur_reg]; + switch (emu8k->cur_reg) { + case 2: + val = emu8k->init1[emu8k->cur_voice]; + break; + case 3: + val = emu8k->init3[emu8k->cur_voice]; + break; + case 4: + val = emu8k->voice[emu8k->cur_voice].envvol; + break; + case 5: + val = emu8k->voice[emu8k->cur_voice].dcysusv; + break; + case 6: + val = emu8k->voice[emu8k->cur_voice].envval; + break; + case 7: + val = emu8k->voice[emu8k->cur_voice].dcysus; + break; + } + } else if (addr == 0xA22) { + name = PORT_NAMES[2][emu8k->cur_reg]; + switch (emu8k->cur_reg) { + case 2: + val = emu8k->init2[emu8k->cur_voice]; + break; + case 3: + val = emu8k->init4[emu8k->cur_voice]; + break; + case 4: + val = emu8k->voice[emu8k->cur_voice].atkhldv; + break; + case 5: + val = emu8k->voice[emu8k->cur_voice].lfo1val; + break; + case 6: + val = emu8k->voice[emu8k->cur_voice].atkhld; + break; + case 7: + val = emu8k->voice[emu8k->cur_voice].lfo2val; + break; + } + } else if (addr == 0xE20) { + name = PORT_NAMES[3][emu8k->cur_reg]; + switch (emu8k->cur_reg) { + case 0: + val = emu8k->voice[emu8k->cur_voice].ip; + break; + case 1: + val = emu8k->voice[emu8k->cur_voice].ifatn; + break; + case 2: + val = emu8k->voice[emu8k->cur_voice].pefe; + break; + case 3: + val = emu8k->voice[emu8k->cur_voice].fmmod; + break; + case 4: + val = emu8k->voice[emu8k->cur_voice].tremfrq; + break; + case 5: + val = emu8k->voice[emu8k->cur_voice].fm2frq2; + break; + case 6: + val = 0xffff; + break; + case 7: + val = 0x1c | ((emu8k->id & 0x0002) ? 0xff02 : 0); + break; + } + } + if (rep_count_r > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_r); + } + if (name == 0) { + emu8k_log("EMU8K READ %04X-%02X(%d/%d): %04X\n", addr, (emu8k->cur_reg) << 5 | emu8k->cur_voice, emu8k->cur_reg, emu8k->cur_voice, val); + } else { + emu8k_log("EMU8K READ %s (%d): %04X\n", name, emu8k->cur_voice, val); + } + + rep_count_r = 0; + last_read = tmpz; + } + rep_count_r++; + } +#endif // EMU8K_DEBUG_REGISTERS + + switch (addr & 0xF02) { + case 0x600: + case 0x602: /*Data0. also known as BLASTER+0x400 and EMU+0x000 */ + switch (emu8k->cur_reg) { + case 0: + READ16(addr, emu8k->voice[emu8k->cur_voice].cpf); + return ret; + + case 1: + READ16(addr, emu8k->voice[emu8k->cur_voice].ptrx); + return ret; + + case 2: + READ16(addr, emu8k->voice[emu8k->cur_voice].cvcf); + return ret; + + case 3: + READ16(addr, emu8k->voice[emu8k->cur_voice].vtft); + return ret; + + case 4: + READ16(addr, emu8k->voice[emu8k->cur_voice].z2); + return ret; + + case 5: + READ16(addr, emu8k->voice[emu8k->cur_voice].z1); + return ret; + + case 6: + READ16(addr, emu8k->voice[emu8k->cur_voice].psst); + return ret; + + case 7: + READ16(addr, emu8k->voice[emu8k->cur_voice].csl); + return ret; + + default: + break; + } + break; + + case 0xA00: /*Data1. also known as BLASTER+0x800 and EMU+0x400 */ + switch (emu8k->cur_reg) { + case 0: + READ16(addr, emu8k->voice[emu8k->cur_voice].ccca); + return ret; + + case 1: + switch (emu8k->cur_voice) { + case 9: + READ16(addr, emu8k->hwcf4); + return ret; + case 10: + READ16(addr, emu8k->hwcf5); + return ret; + /* Actually, these two might be command words rather than registers, or some LFO position/buffer reset.*/ + case 13: + READ16(addr, emu8k->hwcf6); + return ret; + case 14: + READ16(addr, emu8k->hwcf7); + return ret; + + case 20: + READ16(addr, emu8k->smalr); + return ret; + case 21: + READ16(addr, emu8k->smarr); + return ret; + case 22: + READ16(addr, emu8k->smalw); + return ret; + case 23: + READ16(addr, emu8k->smarw); + return ret; + + case 26: + { + uint16_t val = emu8k->smld_buffer; + emu8k->smld_buffer = EMU8K_READ(emu8k, emu8k->smalr); + emu8k->smalr = (emu8k->smalr + 1) & EMU8K_MEM_ADDRESS_MASK; + return val; + } + + /*The EMU8000 PGM describes the return values of these registers as 'a VLSI error'*/ + case 29: /*Configuration Word 1*/ + return (emu8k->hwcf1 & 0xfe) | (emu8k->hwcf3 & 0x01); + case 30: /*Configuration Word 2*/ + return ((emu8k->hwcf2 >> 4) & 0x0e) | (emu8k->hwcf1 & 0x01) | ((emu8k->hwcf3 & 0x02) ? 0x10 : 0) | ((emu8k->hwcf3 & 0x04) ? 0x40 : 0) + | ((emu8k->hwcf3 & 0x08) ? 0x20 : 0) | ((emu8k->hwcf3 & 0x10) ? 0x80 : 0); + case 31: /*Configuration Word 3*/ + return emu8k->hwcf2 & 0x1f; + + default: + break; + } + break; + + case 2: + return emu8k->init1[emu8k->cur_voice]; + + case 3: + return emu8k->init3[emu8k->cur_voice]; + + case 4: + return emu8k->voice[emu8k->cur_voice].envvol; + + case 5: + return emu8k->voice[emu8k->cur_voice].dcysusv; + + case 6: + return emu8k->voice[emu8k->cur_voice].envval; + + case 7: + return emu8k->voice[emu8k->cur_voice].dcysus; + + default: + break; + } + break; + + case 0xA02: /*Data2. also known as BLASTER+0x802 and EMU+0x402 */ + switch (emu8k->cur_reg) { + case 0: + READ16(addr, emu8k->voice[emu8k->cur_voice].ccca); + return ret; + + case 1: + switch (emu8k->cur_voice) { + case 9: + READ16(addr, emu8k->hwcf4); + return ret; + case 10: + READ16(addr, emu8k->hwcf5); + return ret; + /* Actually, these two might be command words rather than registers, or some LFO position/buffer reset. */ + case 13: + READ16(addr, emu8k->hwcf6); + return ret; + case 14: + READ16(addr, emu8k->hwcf7); + return ret; + + /* Simulating empty/full bits by unsetting it once read. */ + case 20: + READ16(addr, emu8k->smalr | dmareadbit); + /* xor with itself to set to zero faster. */ + dmareadbit ^= dmareadbit; + return ret; + case 21: + READ16(addr, emu8k->smarr | dmareadbit); + /* xor with itself to set to zero faster.*/ + dmareadbit ^= dmareadbit; + return ret; + case 22: + READ16(addr, emu8k->smalw | dmawritebit); + /*xor with itself to set to zero faster.*/ + dmawritebit ^= dmawritebit; + return ret; + case 23: + READ16(addr, emu8k->smarw | dmawritebit); + /*xor with itself to set to zero faster.*/ + dmawritebit ^= dmawritebit; + return ret; + + case 26: + { + uint16_t val = emu8k->smrd_buffer; + emu8k->smrd_buffer = EMU8K_READ(emu8k, emu8k->smarr); + emu8k->smarr = (emu8k->smarr + 1) & EMU8K_MEM_ADDRESS_MASK; + return val; + } + /*TODO: We need to improve the precision of this clock, since + it is used by programs to wait. Not critical, but should help reduce + the amount of calls and wait time */ + case 27: /*Sample Counter ( 44Khz clock) */ + /* VWE: interpolate between mixer blocks so + * driver busy-waits on the clock resolve at + * sample granularity instead of block steps */ + return (uint16_t)(emu8k->wc + + emu8k_shim_wc_extra()); + + default: + break; + } + break; + + case 2: + return emu8k->init2[emu8k->cur_voice]; + + case 3: + return emu8k->init4[emu8k->cur_voice]; + + case 4: + return emu8k->voice[emu8k->cur_voice].atkhldv; + + case 5: + return emu8k->voice[emu8k->cur_voice].lfo1val; + + case 6: + return emu8k->voice[emu8k->cur_voice].atkhld; + + case 7: + return emu8k->voice[emu8k->cur_voice].lfo2val; + + default: + break; + } + break; + + case 0xE00: /*Data3. also known as BLASTER+0xC00 and EMU+0x800 */ + switch (emu8k->cur_reg) { + case 0: + return emu8k->voice[emu8k->cur_voice].ip; + + case 1: + return emu8k->voice[emu8k->cur_voice].ifatn; + + case 2: + return emu8k->voice[emu8k->cur_voice].pefe; + + case 3: + return emu8k->voice[emu8k->cur_voice].fmmod; + + case 4: + return emu8k->voice[emu8k->cur_voice].tremfrq; + + case 5: + return emu8k->voice[emu8k->cur_voice].fm2frq2; + + case 6: + return 0xffff; + + case 7: /*ID?*/ + return 0x1c | ((emu8k->id & 0x0002) ? 0xff02 : 0); + + default: + break; + } + break; + + case 0xE02: /* Pointer. also known as BLASTER+0xC02 and EMU+0x802 */ + /* LS five bits = channel number, next 3 bits = register number + * and MS 8 bits = VLSI test register. + * Impulse tracker tests the non variability of the LS byte that it has set, and the variability + * of the MS byte to determine that it really is an AWE32. + * cubic player has a similar code, where it waits until value & 0x1000 is nonzero, and then waits again until it changes to zero.*/ + random_helper = (random_helper + 1) & 0x1F; + return ((0x80 | random_helper) << 8) | (emu8k->cur_reg << 5) | emu8k->cur_voice; + + default: + break; + } + emu8k_log("EMU8K READ : Unknown register read: %04X-%02X(%d/%d) \n", addr, (emu8k->cur_reg << 5) | emu8k->cur_voice, emu8k->cur_reg, emu8k->cur_voice); + return 0xffff; +} + +void +emu8k_outw(uint16_t addr, uint16_t val, void *priv) +{ + emu8k_t *emu8k = (emu8k_t *) priv; + + /*TODO: I would like to not call this here, but i found it was needed or else cubic player would not finish opening (take a looot more of time than usual). + * Basically, being here means that the audio is generated in the emulation thread, instead of the audio thread.*/ + emu8k_update(emu8k); + +#ifdef EMU8K_DEBUG_REGISTERS + if (addr == 0xE22) { + // emu8k_log("EMU8K WRITE POINTER: %d\n", val); + } else if ((addr & 0xF00) == 0x600) { + /* These are automatically reported by WRITE16 */ + if (rep_count_w > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_w); + rep_count_w = 0; + } + last_write = 0; + } else if ((addr & 0xF00) == 0xA00 && emu8k->cur_reg == 0) { + /* These are automatically reported by WRITE16 */ + if (rep_count_w > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_w); + rep_count_w = 0; + } + last_write = 0; + } else if ((addr & 0xF00) == 0xA00 && emu8k->cur_reg == 1) { + uint32_t tmpz = ((addr & 0xF00) << 16) | (emu8k->cur_reg << 5); + if (tmpz != last_write) { + if (rep_count_w > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_w); + rep_count_w = 0; + } + last_write = tmpz; + emu8k_log("EMU8K WRITE RAM I/O or configuration \n"); + } + // emu8k_log("EMU8K WRITE %04X-%02X(%d/%d): %04X\n",addr,(emu8k->cur_reg)<<5|emu8k->cur_voice,emu8k->cur_reg,emu8k->cur_voice, val); + } else if ((addr & 0xF00) == 0xA00 && (emu8k->cur_reg == 2 || emu8k->cur_reg == 3)) { + uint32_t tmpz = ((addr & 0xF00) << 16); + if (tmpz != last_write) { + if (rep_count_w > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_w); + rep_count_w = 0; + } + last_write = tmpz; + emu8k_log("EMU8K WRITE INIT \n"); + } + // emu8k_log("EMU8K WRITE %04X-%02X(%d/%d): %04X\n",addr,(emu8k->cur_reg)<<5|emu8k->cur_voice,emu8k->cur_reg,emu8k->cur_voice, val); + } else if (addr != 0xE22) { + uint32_t tmpz = (addr << 16) | (emu8k->cur_reg << 5) | emu8k->cur_voice; + // if (tmpz != last_write) + if (1) { + char *name = 0; + if (addr == 0xA20) { + name = PORT_NAMES[1][emu8k->cur_reg]; + } else if (addr == 0xA22) { + name = PORT_NAMES[2][emu8k->cur_reg]; + } else if (addr == 0xE20) { + name = PORT_NAMES[3][emu8k->cur_reg]; + } + + if (rep_count_w > 1) { + emu8k_log("EMU8K ...... for %d times\n", rep_count_w); + } + if (name == 0) { + emu8k_log("EMU8K WRITE %04X-%02X(%d/%d): %04X\n", addr, (emu8k->cur_reg) << 5 | emu8k->cur_voice, emu8k->cur_reg, emu8k->cur_voice, val); + } else { + emu8k_log("EMU8K WRITE %s (%d): %04X\n", name, emu8k->cur_voice, val); + } + + rep_count_w = 0; + last_write = tmpz; + } + rep_count_w++; + } +#endif // EMU8K_DEBUG_REGISTERS + + switch (addr & 0xF02) { + case 0x600: + case 0x602: /*Data0. also known as BLASTER+0x400 and EMU+0x000 */ + switch (emu8k->cur_reg) { + case 0: + /* The docs says that this value is constantly updating, and it should have no actual effect. Actions should be done over ptrx */ + WRITE16(addr, emu8k->voice[emu8k->cur_voice].cpf, val); + return; + + case 1: + WRITE16(addr, emu8k->voice[emu8k->cur_voice].ptrx, val); + return; + + case 2: + /* The docs says that this value is constantly updating, and it should have no actual effect. Actions should be done over vtft */ + WRITE16(addr, emu8k->voice[emu8k->cur_voice].cvcf, val); + return; + + case 3: + WRITE16(addr, emu8k->voice[emu8k->cur_voice].vtft, val); + return; + + case 4: + WRITE16(addr, emu8k->voice[emu8k->cur_voice].z2, val); + return; + + case 5: + WRITE16(addr, emu8k->voice[emu8k->cur_voice].z1, val); + return; + + case 6: + { + emu8k_voice_t *emu_voice = &emu8k->voice[emu8k->cur_voice]; + WRITE16(addr, emu_voice->psst, val); + /* TODO: Should we update only on MSB update, or this could be used as some sort of hack by applications? */ + emu_voice->loop_start.int_address = emu_voice->psst & EMU8K_MEM_ADDRESS_MASK; + if (addr & 2) { + emu_voice->vol_l = emu_voice->psst_pan; + emu_voice->vol_r = 255 - (emu_voice->psst_pan); + } + } + return; + + case 7: + WRITE16(addr, emu8k->voice[emu8k->cur_voice].csl, val); + /* TODO: Should we update only on MSB update, or this could be used as some sort of hack by applications? */ + emu8k->voice[emu8k->cur_voice].loop_end.int_address = emu8k->voice[emu8k->cur_voice].csl & EMU8K_MEM_ADDRESS_MASK; + return; + + default: + break; + } + break; + + case 0xA00: /*Data1. also known as BLASTER+0x800 and EMU+0x400 */ + switch (emu8k->cur_reg) { + case 0: + WRITE16(addr, emu8k->voice[emu8k->cur_voice].ccca, val); + /* TODO: Should we update only on MSB update, or this could be used as some sort of hack by applications? */ + emu8k->voice[emu8k->cur_voice].addr.int_address = emu8k->voice[emu8k->cur_voice].ccca & EMU8K_MEM_ADDRESS_MASK; + return; + + case 1: + switch (emu8k->cur_voice) { + case 9: + WRITE16(addr, emu8k->hwcf4, val); + return; + case 10: + WRITE16(addr, emu8k->hwcf5, val); + return; + /* Actually, these two might be command words rather than registers, or some LFO position/buffer reset. */ + case 13: + WRITE16(addr, emu8k->hwcf6, val); + return; + case 14: + WRITE16(addr, emu8k->hwcf7, val); + return; + + case 20: + WRITE16(addr, emu8k->smalr, val); + return; + case 21: + WRITE16(addr, emu8k->smarr, val); + return; + case 22: + WRITE16(addr, emu8k->smalw, val); + return; + case 23: + WRITE16(addr, emu8k->smarw, val); + return; + + case 26: + EMU8K_WRITE(emu8k, emu8k->smalw, val); + emu8k->smalw = (emu8k->smalw + 1) & EMU8K_MEM_ADDRESS_MASK; + return; + + case 29: + emu8k->hwcf1 = val; + return; + case 30: + emu8k->hwcf2 = val; + return; + case 31: + emu8k->hwcf3 = val; + return; + + default: + break; + } + break; + + case 2: + emu8k->init1[emu8k->cur_voice] = val; + /* Skip if in first/second initialization step */ + if (emu8k->init1[0] != 0x03FF) { + switch (emu8k->cur_voice) { + case 0x3: + emu8k->reverb_engine.out_mix = val & 0xFF; + break; + case 0x5: + { + for (uint8_t c = 0; c < 8; c++) { + emu8k->reverb_engine.allpass[c].feedback = (val & 0xFF) / ((float) 0xFF); + } + } + break; + case 0x7: + emu8k->reverb_engine.link_return_type = (val == 0x8474) ? 1 : 0; + break; + case 0xF: + emu8k->reverb_engine.reflections[0].output_gain = ((val & 0xF0) >> 4) / 15.0; + break; + case 0x17: + emu8k->reverb_engine.reflections[1].output_gain = ((val & 0xF0) >> 4) / 15.0; + break; + case 0x1F: + emu8k->reverb_engine.reflections[2].output_gain = ((val & 0xF0) >> 4) / 15.0; + break; + case 0x9: + emu8k->reverb_engine.reflections[0].feedback = (val & 0xF) / 15.0; + break; + case 0xB: +#if 0 + emu8k->reverb_engine.reflections[0].feedback_r = (val&0xF)/15.0; +#endif + break; + case 0x11: + emu8k->reverb_engine.reflections[1].feedback = (val & 0xF) / 15.0; + break; + case 0x13: +#if 0 + emu8k->reverb_engine.reflections[1].feedback_r = (val&0xF)/15.0; +#endif + break; + case 0x19: + emu8k->reverb_engine.reflections[2].feedback = (val & 0xF) / 15.0; + break; + case 0x1B: +#if 0 + emu8k->reverb_engine.reflections[2].feedback_r = (val&0xF)/15.0; +#endif + break; + + default: + break; + } + } + return; + + case 3: + emu8k->init3[emu8k->cur_voice] = val; + /* Skip if in first/second initialization step */ + if (emu8k->init1[0] != 0x03FF) { + switch (emu8k->cur_voice) { + case 9: + emu8k->chorus_engine.feedback = (val & 0xFF); + break; + case 12: + /* Limiting this to a sane value given our buffer. */ + emu8k->chorus_engine.delay_samples_central = (val & 0x1FFF); + break; + + case 1: + emu8k->reverb_engine.refl_in_amp = val & 0xFF; + break; + case 3: +#if 0 + emu8k->reverb_engine.refl_in_amp_r = val&0xFF; +#endif + break; + + default: + break; + } + } + return; + + case 4: + emu8k->voice[emu8k->cur_voice].envvol = val; + emu8k->voice[emu8k->cur_voice].vol_envelope.delay_samples = ENVVOL_TO_EMU_SAMPLES(val); + return; + + case 5: + { + emu8k->voice[emu8k->cur_voice].dcysusv = val; + emu8k_envelope_t *const vol_env = &emu8k->voice[emu8k->cur_voice].vol_envelope; + int old_on = emu8k->voice[emu8k->cur_voice].env_engine_on; + emu8k->voice[emu8k->cur_voice].env_engine_on = DCYSUSV_GENERATOR_ENGINE_ON(val); + + if (emu8k->voice[emu8k->cur_voice].env_engine_on && old_on != emu8k->voice[emu8k->cur_voice].env_engine_on) { + if (emu8k->hwcf3 != 0x04) { + /* This is a hack for some programs like Doom or cubic player 1.7 that don't initialize + the hwcfg and init registers (doom does not init the card at all. only tests the cfg registers) */ + emu8k->hwcf3 = 0x04; + } + + // reset lfos. + emu8k->voice[emu8k->cur_voice].lfo1_count.addr = 0; + emu8k->voice[emu8k->cur_voice].lfo2_count.addr = 0; + // Trigger envelopes + if (ATKHLDV_TRIGGER(emu8k->voice[emu8k->cur_voice].atkhldv)) { + vol_env->value_amp_hz = 0; + if (vol_env->delay_samples) { + vol_env->state = ENV_DELAY; + } else if (vol_env->attack_amount_amp_hz == 0) { + vol_env->state = ENV_STOPPED; + } else { + vol_env->state = ENV_ATTACK; + /* TODO: Verify if "never attack" means eternal mute, + * or it means skip attack, go to hold". + if (vol_env->attack_amount == 0) + { + vol_env->value = (1 << 21); + vol_env->state = ENV_HOLD; + }*/ + } + } + + if (ATKHLD_TRIGGER(emu8k->voice[emu8k->cur_voice].atkhld)) { + emu8k_envelope_t *const mod_env = &emu8k->voice[emu8k->cur_voice].mod_envelope; + mod_env->value_amp_hz = 0; + mod_env->value_db_oct = 0; + if (mod_env->delay_samples) { + mod_env->state = ENV_DELAY; + } else if (mod_env->attack_amount_amp_hz == 0) { + mod_env->state = ENV_STOPPED; + } else { + mod_env->state = ENV_ATTACK; + /* TODO: Verify if "never attack" means eternal start, + * or it means skip attack, go to hold". + if (mod_env->attack_amount == 0) + { + mod_env->value = (1 << 21); + mod_env->state = ENV_HOLD; + }*/ + } + } + } + + /* Converting the input in dBs to envelope value range. */ + vol_env->sustain_value_db_oct = DCYSUSV_SUS_TO_ENV_RANGE(DCYSUSV_SUSVALUE_GET(val)); + vol_env->ramp_amount_db_oct = env_decay_to_dbs_or_oct[DCYSUSV_DECAYRELEASE_GET(val)]; + if (DCYSUSV_IS_RELEASE(val)) { + if (vol_env->state == ENV_DELAY || vol_env->state == ENV_ATTACK || vol_env->state == ENV_HOLD) { + vol_env->value_db_oct = env_vol_amplitude_to_db[vol_env->value_amp_hz >> 5] << 5; + if (vol_env->value_db_oct > (1 << 21)) + vol_env->value_db_oct = 1 << 21; + } + + vol_env->state = (vol_env->value_db_oct >= vol_env->sustain_value_db_oct) ? ENV_RAMP_DOWN : ENV_RAMP_UP; + } + } + return; + + case 6: + emu8k->voice[emu8k->cur_voice].envval = val; + emu8k->voice[emu8k->cur_voice].mod_envelope.delay_samples = ENVVAL_TO_EMU_SAMPLES(val); + return; + + case 7: + { + // TODO: Look for a bug on delay (first trigger it works, next trigger it doesn't) + emu8k->voice[emu8k->cur_voice].dcysus = val; + emu8k_envelope_t *const mod_env = &emu8k->voice[emu8k->cur_voice].mod_envelope; + /* Converting the input in octaves to envelope value range. */ + mod_env->sustain_value_db_oct = DCYSUS_SUS_TO_ENV_RANGE(DCYSUS_SUSVALUE_GET(val)); + mod_env->ramp_amount_db_oct = env_decay_to_dbs_or_oct[DCYSUS_DECAYRELEASE_GET(val)]; + if (DCYSUS_IS_RELEASE(val)) { + if (mod_env->state == ENV_DELAY || mod_env->state == ENV_ATTACK || mod_env->state == ENV_HOLD) { + mod_env->value_db_oct = env_mod_hertz_to_octave[mod_env->value_amp_hz >> 9] << 9; + if (mod_env->value_db_oct >= (1 << 21)) + mod_env->value_db_oct = (1 << 21) - 1; + } + + mod_env->state = (mod_env->value_db_oct >= mod_env->sustain_value_db_oct) ? ENV_RAMP_DOWN : ENV_RAMP_UP; + } + } + return; + + default: + break; + } + break; + + case 0xA02: /*Data2. also known as BLASTER+0x802 and EMU+0x402 */ + switch (emu8k->cur_reg) { + case 0: + { + emu8k_voice_t *emu_voice = &emu8k->voice[emu8k->cur_voice]; + WRITE16(addr, emu_voice->ccca, val); + emu_voice->addr.int_address = emu_voice->ccca & EMU8K_MEM_ADDRESS_MASK; + uint32_t paramq = CCCA_FILTQ_GET(emu_voice->ccca); + emu_voice->filt_att = filter_atten[paramq]; + emu_voice->filterq_idx = paramq; + } + return; + + case 1: + switch (emu8k->cur_voice) { + case 9: + WRITE16(addr, emu8k->hwcf4, val); + /* Skip if in first/second initialization step */ + if (emu8k->init1[0] != 0x03FF) { + /*(1/256th of a 44Khz sample) */ + /* clip the value to a reasonable value given our buffer */ + int32_t tmp = emu8k->hwcf4 & 0x1FFFFF; + emu8k->chorus_engine.delay_offset_samples_right = ((double) tmp) / 256.0; + } + return; + case 10: + WRITE16(addr, emu8k->hwcf5, val); + /* Skip if in first/second initialization step */ + if (emu8k->init1[0] != 0x03FF) { + /* The scale of this value is unknown. I've taken it as milliHz. + * Another interpretation could be periods. (and so, Hz = 1/period)*/ + double osc_speed = emu8k->hwcf5; //*1.316; +#if 1 // milliHz + /*milliHz to lfotable samples.*/ + osc_speed *= 65.536 / 44100.0; +#elif 0 // periods + /* 44.1Khz ticks to lfotable samples.*/ + osc_speed = 65.536 / osc_speed; +#endif + /*left shift 32bits for 32.32 fixed.point*/ + osc_speed *= 65536.0 * 65536.0; + emu8k->chorus_engine.lfo_inc.addr = (uint64_t) osc_speed; + } + return; + /* Actually, these two might be command words rather than registers, or some LFO position/buffer reset.*/ + case 13: + WRITE16(addr, emu8k->hwcf6, val); + return; + case 14: + WRITE16(addr, emu8k->hwcf7, val); + return; + + case 20: /*Top 8 bits are for Empty (MT) bit or non-addressable.*/ + WRITE16(addr, emu8k->smalr, val & 0xFF); + dmareadbit = 0x8000; + return; + case 21: /*Top 8 bits are for Empty (MT) bit or non-addressable.*/ + WRITE16(addr, emu8k->smarr, val & 0xFF); + dmareadbit = 0x8000; + return; + case 22: /*Top 8 bits are for full bit or non-addressable.*/ + WRITE16(addr, emu8k->smalw, val & 0xFF); + return; + case 23: /*Top 8 bits are for full bit or non-addressable.*/ + WRITE16(addr, emu8k->smarw, val & 0xFF); + return; + + case 26: + dmawritebit = 0x8000; + EMU8K_WRITE(emu8k, emu8k->smarw, val); + emu8k->smarw++; + return; + + default: + break; + } + break; + + case 2: + emu8k->init2[emu8k->cur_voice] = val; + /* Skip if in first/second initialization step */ + if (emu8k->init1[0] != 0x03FF) { + switch (emu8k->cur_voice) { + case 0x14: + { + int multip = ((val & 0xF00) >> 8) + 18; + emu8k->reverb_engine.reflections[5].bufsize = multip * REV_BUFSIZE_STEP; + emu8k->reverb_engine.tailL.bufsize = (multip + 1) * REV_BUFSIZE_STEP; + if (emu8k->reverb_engine.link_return_type == 0) { + emu8k->reverb_engine.tailR.bufsize = (multip + 1) * REV_BUFSIZE_STEP; + } + } + break; + case 0x16: + if (emu8k->reverb_engine.link_return_type == 1) { + int multip = ((val & 0xF00) >> 8) + 18; + emu8k->reverb_engine.tailR.bufsize = (multip + 1) * REV_BUFSIZE_STEP; + } + break; + case 0x7: + emu8k->reverb_engine.reflections[3].output_gain = ((val & 0xF0) >> 4) / 15.0; + break; + case 0xf: + emu8k->reverb_engine.reflections[4].output_gain = ((val & 0xF0) >> 4) / 15.0; + break; + case 0x17: + emu8k->reverb_engine.reflections[5].output_gain = ((val & 0xF0) >> 4) / 15.0; + break; + case 0x1d: + { + for (uint8_t c = 0; c < 6; c++) { + emu8k->reverb_engine.reflections[c].damp1 = (val & 0xFF) / 255.0; + emu8k->reverb_engine.reflections[c].damp2 = (0xFF - (val & 0xFF)) / 255.0; + emu8k->reverb_engine.reflections[c].filterstore = 0; + } + emu8k->reverb_engine.damper.damp1 = (val & 0xFF) / 255.0; + emu8k->reverb_engine.damper.damp2 = (0xFF - (val & 0xFF)) / 255.0; + emu8k->reverb_engine.damper.filterstore = 0; + } + break; + case 0x1f: /* filter r */ + break; + case 0x1: + emu8k->reverb_engine.reflections[3].feedback = (val & 0xF) / 15.0; + break; + case 0x3: +#if 0 + emu8k->reverb_engine.reflections[3].feedback_r = (val&0xF)/15.0; +#endif + break; + case 0x9: + emu8k->reverb_engine.reflections[4].feedback = (val & 0xF) / 15.0; + break; + case 0xb: +#if 0 + emu8k->reverb_engine.reflections[4].feedback_r = (val&0xF)/15.0; +#endif + break; + case 0x11: + emu8k->reverb_engine.reflections[5].feedback = (val & 0xF) / 15.0; + break; + case 0x13: +#if 0 + emu8k->reverb_engine.reflections[5].feedback_r = (val&0xF)/15.0; +#endif + break; + + default: + break; + } + } + return; + + case 3: + emu8k->init4[emu8k->cur_voice] = val; + /* Skip if in first/second initialization step */ + if (emu8k->init1[0] != 0x03FF) { + switch (emu8k->cur_voice) { + case 0x3: + { + int32_t samples = ((val & 0xFF) * emu8k->chorus_engine.delay_samples_central) >> 8; + emu8k->chorus_engine.lfodepth_multip = samples; + } + break; + + case 0x1F: + emu8k->reverb_engine.link_return_amp = val & 0xFF; + break; + + default: + break; + } + } + return; + + case 4: + { + emu8k->voice[emu8k->cur_voice].atkhldv = val; + emu8k_envelope_t *const vol_env = &emu8k->voice[emu8k->cur_voice].vol_envelope; + vol_env->attack_samples = env_attack_to_samples[ATKHLDV_ATTACK(val)]; + if (vol_env->attack_samples == 0) { + vol_env->attack_amount_amp_hz = 0; + } else { + /* Linear amplitude increase each sample. */ + vol_env->attack_amount_amp_hz = (1 << 21) / vol_env->attack_samples; + } + vol_env->hold_samples = ATKHLDV_HOLD_TO_EMU_SAMPLES(val); + if (ATKHLDV_TRIGGER(val) && emu8k->voice[emu8k->cur_voice].env_engine_on) { + /*TODO: I assume that "envelope trigger" is the same as new note + * (since changing the IP can be done when modulating pitch too) */ + emu8k->voice[emu8k->cur_voice].lfo1_count.addr = 0; + emu8k->voice[emu8k->cur_voice].lfo2_count.addr = 0; + + vol_env->value_amp_hz = 0; + if (vol_env->delay_samples) { + vol_env->state = ENV_DELAY; + } else if (vol_env->attack_amount_amp_hz == 0) { + vol_env->state = ENV_STOPPED; + } else { + vol_env->state = ENV_ATTACK; + /* TODO: Verify if "never attack" means eternal mute, + * or it means skip attack, go to hold". + if (vol_env->attack_amount == 0) + { + vol_env->value = (1 << 21); + vol_env->state = ENV_HOLD; + }*/ + } + } + } + return; + + case 5: + emu8k->voice[emu8k->cur_voice].lfo1val = val; + /* TODO: verify if this is set once, or set every time. */ + emu8k->voice[emu8k->cur_voice].lfo1_delay_samples = LFOxVAL_TO_EMU_SAMPLES(val); + return; + + case 6: + { + emu8k->voice[emu8k->cur_voice].atkhld = val; + emu8k_envelope_t *const mod_env = &emu8k->voice[emu8k->cur_voice].mod_envelope; + mod_env->attack_samples = env_attack_to_samples[ATKHLD_ATTACK(val)]; + if (mod_env->attack_samples == 0) { + mod_env->attack_amount_amp_hz = 0; + } else { + /* Linear amplitude increase each sample. */ + mod_env->attack_amount_amp_hz = (1 << 21) / mod_env->attack_samples; + } + mod_env->hold_samples = ATKHLD_HOLD_TO_EMU_SAMPLES(val); + if (ATKHLD_TRIGGER(val) && emu8k->voice[emu8k->cur_voice].env_engine_on) { + mod_env->value_amp_hz = 0; + mod_env->value_db_oct = 0; + if (mod_env->delay_samples) { + mod_env->state = ENV_DELAY; + } else if (mod_env->attack_amount_amp_hz == 0) { + mod_env->state = ENV_STOPPED; + } else { + mod_env->state = ENV_ATTACK; + /* TODO: Verify if "never attack" means eternal start, + * or it means skip attack, go to hold". + if (mod_env->attack_amount == 0) + { + mod_env->value = (1 << 21); + mod_env->state = ENV_HOLD; + }*/ + } + } + } + return; + + case 7: + emu8k->voice[emu8k->cur_voice].lfo2val = val; + emu8k->voice[emu8k->cur_voice].lfo2_delay_samples = LFOxVAL_TO_EMU_SAMPLES(val); + + return; + + default: + break; + } + break; + + case 0xE00: /*Data3. also known as BLASTER+0xC00 and EMU+0x800 */ + switch (emu8k->cur_reg) { + case 0: + emu8k->voice[emu8k->cur_voice].ip = val; + emu8k->voice[emu8k->cur_voice].ptrx_pit_target = freqtable[val] >> 18; + return; + + case 1: + { + emu8k_voice_t *const the_voice = &emu8k->voice[emu8k->cur_voice]; + if ((val & 0xFF) == 0 && the_voice->cvcf_curr_volume == 0 && the_voice->vtft_vol_target == 0 + && the_voice->dcysusv == 0x80 && the_voice->ip == 0) { + // Patch to avoid some clicking noises with Impulse tracker or other software that sets + // different values to 0 to set noteoff, but here, 0 means no attenuation = full volume. + return; + } + the_voice->ifatn = val; + the_voice->initial_att = (((int32_t) the_voice->ifatn_attenuation << 21) / 0xFF); + the_voice->vtft_vol_target = attentable[the_voice->ifatn_attenuation]; + + the_voice->initial_filter = (((int32_t) the_voice->ifatn_init_filter << 21) / 0xFF); + if (the_voice->ifatn_init_filter == 0xFF) { + the_voice->vtft_filter_target = 0xFFFF; + } else { + the_voice->vtft_filter_target = the_voice->initial_filter >> 5; + } + } + return; + + case 2: + { + emu8k_voice_t *const the_voice = &emu8k->voice[emu8k->cur_voice]; + the_voice->pefe = val; + + int divider = (the_voice->pefe_modenv_filter_height < 0) ? 0x80 : 0x7F; + the_voice->fixed_modenv_filter_height = ((int32_t) the_voice->pefe_modenv_filter_height) * 0x4000 / divider; + + divider = (the_voice->pefe_modenv_pitch_height < 0) ? 0x80 : 0x7F; + the_voice->fixed_modenv_pitch_height = ((int32_t) the_voice->pefe_modenv_pitch_height) * 0x4000 / divider; + } + return; + + case 3: + { + emu8k_voice_t *const the_voice = &emu8k->voice[emu8k->cur_voice]; + the_voice->fmmod = val; + + int divider = (the_voice->fmmod_lfo1_filt_mod < 0) ? 0x80 : 0x7F; + the_voice->fixed_lfo1_filt_mod = ((int32_t) the_voice->fmmod_lfo1_filt_mod) * 0x4000 / divider; + + divider = (the_voice->fmmod_lfo1_vibrato < 0) ? 0x80 : 0x7F; + the_voice->fixed_lfo1_vibrato = ((int32_t) the_voice->fmmod_lfo1_vibrato) * 0x4000 / divider; + } + return; + + case 4: + { + emu8k_voice_t *const the_voice = &emu8k->voice[emu8k->cur_voice]; + the_voice->tremfrq = val; + the_voice->lfo1_speed = lfofreqtospeed[the_voice->tremfrq_lfo1_freq]; + + int divider = (the_voice->tremfrq_lfo1_tremolo < 0) ? 0x80 : 0x7F; + the_voice->fixed_lfo1_tremolo = ((int32_t) the_voice->tremfrq_lfo1_tremolo) * 0x4000 / divider; + } + return; + + case 5: + { + emu8k_voice_t *const the_voice = &emu8k->voice[emu8k->cur_voice]; + the_voice->fm2frq2 = val; + the_voice->lfo2_speed = lfofreqtospeed[the_voice->fm2frq2_lfo2_freq]; + + int divider = (the_voice->fm2frq2_lfo2_vibrato < 0) ? 0x80 : 0x7F; + the_voice->fixed_lfo2_vibrato = ((int32_t) the_voice->fm2frq2_lfo2_vibrato) * 0x4000 / divider; + } + return; + + case 7: /*ID? I believe that this allows applications to know if the emu is in use by another application */ + emu8k->id = val; + return; + + default: + break; + } + break; + + case 0xE02: /* Pointer. also known as BLASTER+0xC02 and EMU+0x802 */ + emu8k->cur_voice = (val & 31); + emu8k->cur_reg = ((val >> 5) & 7); + return; + + default: + break; + } + emu8k_log("EMU8K WRITE: Unknown register write: %04X-%02X(%d/%d): %04X \n", addr, (emu8k->cur_reg) << 5 | emu8k->cur_voice, + emu8k->cur_reg, emu8k->cur_voice, val); +} + +uint8_t +emu8k_inb(uint16_t addr, void *priv) +{ + /* Reading a single byte is a feature that at least Impulse tracker uses, + * but only on detection code and not for odd addresses.*/ + if (addr & 1) + return emu8k_inw(addr & ~1, priv) >> 1; + return emu8k_inw(addr, priv) & 0xff; +} + +void +emu8k_outb(uint16_t addr, uint8_t val, void *priv) +{ + /* TODO: AWE32 docs says that you cannot write in bytes, but if + * an app were to use this implementation, the content of the LS Byte would be lost.*/ + if (addr & 1) + emu8k_outw(addr & ~1, val << 8, priv); + else + emu8k_outw(addr, val, priv); +} + +/* TODO: This is not a correct emulation, just a workalike implementation. */ +void +emu8k_work_chorus(int32_t *inbuf, int32_t *outbuf, emu8k_chorus_eng_t *engine, int count) +{ + for (int pos = 0; pos < count; pos++) { + double lfo_inter1 = chortable[engine->lfo_pos.int_address]; +#if 0 + double lfo_inter2 = chortable[(engine->lfo_pos.int_address+1)&0xFFFF]; +#endif + + double offset_lfo = lfo_inter1; //= lfo_inter1 + ((lfo_inter2-lfo_inter1)*engine->lfo_pos.fract_address/65536.0); + offset_lfo *= engine->lfodepth_multip; + + /* Work left */ + double readdouble = (double) engine->write - (double) engine->delay_samples_central - offset_lfo; + int read = (int32_t) floor(readdouble); + int fraction_part = (readdouble - (double) read) * 65536.0; + int next_value = read + 1; + if (read < 0) { + read += EMU8K_LFOCHORUS_SIZE; + if (next_value < 0) + next_value += EMU8K_LFOCHORUS_SIZE; + } else if (next_value >= EMU8K_LFOCHORUS_SIZE) { + next_value -= EMU8K_LFOCHORUS_SIZE; + if (read >= EMU8K_LFOCHORUS_SIZE) + read -= EMU8K_LFOCHORUS_SIZE; + } + int32_t dat1 = engine->chorus_left_buffer[read]; + int32_t dat2 = engine->chorus_left_buffer[next_value]; + dat1 += ((dat2 - dat1) * fraction_part) >> 16; + + engine->chorus_left_buffer[engine->write] = *inbuf + ((dat1 * engine->feedback) >> 8); + + /* Work right */ + readdouble = (double) engine->write - (double) engine->delay_samples_central - engine->delay_offset_samples_right - offset_lfo; + read = (int32_t) floor(readdouble); + next_value = read + 1; + if (read < 0) { + read += EMU8K_LFOCHORUS_SIZE; + if (next_value < 0) + next_value += EMU8K_LFOCHORUS_SIZE; + } else if (next_value >= EMU8K_LFOCHORUS_SIZE) { + next_value -= EMU8K_LFOCHORUS_SIZE; + if (read >= EMU8K_LFOCHORUS_SIZE) + read -= EMU8K_LFOCHORUS_SIZE; + } + int32_t dat3 = engine->chorus_right_buffer[read]; + int32_t dat4 = engine->chorus_right_buffer[next_value]; + dat3 += ((dat4 - dat3) * fraction_part) >> 16; + + engine->chorus_right_buffer[engine->write] = *inbuf + ((dat3 * engine->feedback) >> 8); + + ++engine->write; + engine->write %= EMU8K_LFOCHORUS_SIZE; + engine->lfo_pos.addr += engine->lfo_inc.addr; + engine->lfo_pos.int_address &= 0xFFFF; + + (*outbuf++) += dat1; + (*outbuf++) += dat3; + inbuf++; + } +} + +int32_t +emu8k_reverb_comb_work(emu8k_reverb_combfilter_t *comb, int32_t in) +{ + + int32_t bufin; + /* get echo */ + int32_t output = comb->reflection[comb->read_pos]; + /* apply lowpass */ + comb->filterstore = (output * comb->damp2) + (comb->filterstore * comb->damp1); + /* appply feedback */ + bufin = in - (comb->filterstore * comb->feedback); + /* store new value in delayed buffer */ + comb->reflection[comb->read_pos] = bufin; + + if (++comb->read_pos >= comb->bufsize) + comb->read_pos = 0; + + return output * comb->output_gain; +} + +int32_t +emu8k_reverb_diffuser_work(emu8k_reverb_combfilter_t *comb, int32_t in) +{ + + int32_t bufout = comb->reflection[comb->read_pos]; + /*diffuse*/ + int32_t bufin = -in + (bufout * comb->feedback); + int32_t output = bufout - (bufin * comb->feedback); + /* store new value in delayed buffer */ + comb->reflection[comb->read_pos] = bufin; + + if (++comb->read_pos >= comb->bufsize) + comb->read_pos = 0; + + return output; +} + +int32_t +emu8k_reverb_tail_work(emu8k_reverb_combfilter_t *comb, emu8k_reverb_combfilter_t *allpasses, int32_t in) +{ + int32_t output = comb->reflection[comb->read_pos]; + /* store new value in delayed buffer */ + comb->reflection[comb->read_pos] = in; + +#if 0 + output = emu8k_reverb_allpass_work(&allpasses[0],output); +#endif + output = emu8k_reverb_diffuser_work(&allpasses[1], output); + output = emu8k_reverb_diffuser_work(&allpasses[2], output); +#if 0 + output = emu8k_reverb_allpass_work(&allpasses[3],output); +#endif + + if (++comb->read_pos >= comb->bufsize) + comb->read_pos = 0; + + return output; +} +int32_t +emu8k_reverb_damper_work(emu8k_reverb_combfilter_t *comb, int32_t in) +{ + /* apply lowpass */ + comb->filterstore = (in * comb->damp2) + (comb->filterstore * comb->damp1); + return comb->filterstore; +} + +/* TODO: This is not a correct emulation, just a workalike implementation. */ +void +emu8k_work_reverb(int32_t *inbuf, int32_t *outbuf, emu8k_reverb_eng_t *engine, int count) +{ + int pos; + if (engine->link_return_type) { + for (pos = 0; pos < count; pos++) { + int32_t dat1; + int32_t dat2; + int32_t in; + int32_t in2; + in = emu8k_reverb_damper_work(&engine->damper, inbuf[pos]); + in2 = (in * engine->refl_in_amp) >> 8; + dat2 = emu8k_reverb_comb_work(&engine->reflections[0], in2); + dat2 += emu8k_reverb_comb_work(&engine->reflections[1], in2); + dat1 = emu8k_reverb_comb_work(&engine->reflections[2], in2); + dat2 += emu8k_reverb_comb_work(&engine->reflections[3], in2); + dat1 += emu8k_reverb_comb_work(&engine->reflections[4], in2); + dat2 += emu8k_reverb_comb_work(&engine->reflections[5], in2); + + dat1 += (emu8k_reverb_tail_work(&engine->tailL, &engine->allpass[0], in + dat1) * engine->link_return_amp) >> 8; + dat2 += (emu8k_reverb_tail_work(&engine->tailR, &engine->allpass[4], in + dat2) * engine->link_return_amp) >> 8; + + (*outbuf++) += (dat1 * engine->out_mix) >> 8; + (*outbuf++) += (dat2 * engine->out_mix) >> 8; + } + } else { + for (pos = 0; pos < count; pos++) { + int32_t dat1; + int32_t dat2; + int32_t in; + int32_t in2; + in = emu8k_reverb_damper_work(&engine->damper, inbuf[pos]); + in2 = (in * engine->refl_in_amp) >> 8; + dat1 = emu8k_reverb_comb_work(&engine->reflections[0], in2); + dat1 += emu8k_reverb_comb_work(&engine->reflections[1], in2); + dat1 += emu8k_reverb_comb_work(&engine->reflections[2], in2); + dat1 += emu8k_reverb_comb_work(&engine->reflections[3], in2); + dat1 += emu8k_reverb_comb_work(&engine->reflections[4], in2); + dat1 += emu8k_reverb_comb_work(&engine->reflections[5], in2); + dat2 = dat1; + + dat1 += (emu8k_reverb_tail_work(&engine->tailL, &engine->allpass[0], in + dat1) * engine->link_return_amp) >> 8; + dat2 += (emu8k_reverb_tail_work(&engine->tailR, &engine->allpass[4], in + dat2) * engine->link_return_amp) >> 8; + + (*outbuf++) += (dat1 * engine->out_mix) >> 8; + (*outbuf++) += (dat2 * engine->out_mix) >> 8; + } + } +} +void +emu8k_work_eq(UNUSED(int32_t *inoutbuf), UNUSED(int count)) +{ + // TODO: Work EQ over buf +} + +int32_t +emu8k_vol_slide(emu8k_slide_t *slide, int32_t target) +{ + if (slide->last < target) { + slide->last += 0x400; + if (slide->last > target) + slide->last = target; + } else if (slide->last > target) { + slide->last -= 0x400; + if (slide->last < target) + slide->last = target; + } + return slide->last; +} + +#if 0 +int32_t old_pitch[32] = { 0 }; +int32_t old_cut[32] = { 0 }; +int32_t old_vol[32] = { 0 }; +#endif +void +emu8k_update(emu8k_t *emu8k) +{ + if (emu8k->pos >= wavetable_pos_global) + return; + + const int num_samples = wavetable_pos_global - emu8k->pos; + int32_t *buf; + emu8k_voice_t *emu_voice; + int pos; + int num_active = 0; + + /* Voices section */ + for (uint8_t c = 0; c < 32; c++) { + emu_voice = &emu8k->voice[c]; + buf = &emu8k->buffer[emu8k->pos * 2]; + + /* Skip entirely idle voices — no sound output and no envelope to process. */ + if (!emu_voice->env_engine_on && !emu_voice->cvcf_curr_volume) + continue; + + num_active++; + + for (pos = emu8k->pos; pos < wavetable_pos_global; pos++) { + int32_t dat; + + if (emu_voice->cvcf_curr_volume) { + /* Waveform oscillator */ +#ifdef RESAMPLER_LINEAR + dat = EMU8K_READ_INTERP_LINEAR(emu8k, emu_voice->addr.int_address, + emu_voice->addr.fract_address); + +#elif defined RESAMPLER_CUBIC + dat = EMU8K_READ_INTERP_CUBIC(emu8k, emu_voice->addr.int_address, + emu_voice->addr.fract_address); +#endif + + /* Filter section */ + if (emu_voice->filterq_idx || emu_voice->cvcf_curr_filt_ctoff != 0xFFFF) { + int cutoff = emu_voice->cvcf_curr_filt_ctoff >> 8; + const int64_t coef0 = filt_coeffs[emu_voice->filterq_idx][cutoff][0]; + const int64_t coef1 = filt_coeffs[emu_voice->filterq_idx][cutoff][1]; + const int64_t coef2 = filt_coeffs[emu_voice->filterq_idx][cutoff][2]; +/* clip at twice the range */ +#define ClipBuffer(buf) (buf < -16777216) ? -16777216 : (buf > 16777216) ? 16777216 \ + : buf + +#ifdef FILTER_INITIAL +# define NOOP(x) (void) x; + NOOP(coef1) + /* Apply expected attenuation. (FILTER_MOOG does it implicitly, but this one doesn't). + * Work in 24bits. */ + dat = (dat * emu_voice->filt_att) >> 8; + + int64_t vhp = ((-emu_voice->filt_buffer[0] * coef2) >> 24) - emu_voice->filt_buffer[1] - dat; + emu_voice->filt_buffer[1] += (emu_voice->filt_buffer[0] * coef0) >> 24; + emu_voice->filt_buffer[0] += (vhp * coef0) >> 24; + dat = (int32_t) (emu_voice->filt_buffer[1] >> 8); + if (dat > 32767) + dat = 32767; + else if (dat < -32768) + dat = -32768; + +#elif defined FILTER_MOOG + + /*move to 24bits*/ + dat <<= 8; + + dat -= (coef2 * emu_voice->filt_buffer[4]) >> 24; /*feedback*/ + int64_t t1 = emu_voice->filt_buffer[1]; + emu_voice->filt_buffer[1] = ((dat + emu_voice->filt_buffer[0]) * coef0 - emu_voice->filt_buffer[1] * coef1) >> 24; + emu_voice->filt_buffer[1] = ClipBuffer(emu_voice->filt_buffer[1]); + + int64_t t2 = emu_voice->filt_buffer[2]; + emu_voice->filt_buffer[2] = ((emu_voice->filt_buffer[1] + t1) * coef0 - emu_voice->filt_buffer[2] * coef1) >> 24; + emu_voice->filt_buffer[2] = ClipBuffer(emu_voice->filt_buffer[2]); + + int64_t t3 = emu_voice->filt_buffer[3]; + emu_voice->filt_buffer[3] = ((emu_voice->filt_buffer[2] + t2) * coef0 - emu_voice->filt_buffer[3] * coef1) >> 24; + emu_voice->filt_buffer[3] = ClipBuffer(emu_voice->filt_buffer[3]); + + emu_voice->filt_buffer[4] = ((emu_voice->filt_buffer[3] + t3) * coef0 - emu_voice->filt_buffer[4] * coef1) >> 24; + emu_voice->filt_buffer[4] = ClipBuffer(emu_voice->filt_buffer[4]); + + emu_voice->filt_buffer[0] = ClipBuffer(dat); + + dat = (int32_t) (emu_voice->filt_buffer[4] >> 8); + if (dat > 32767) + dat = 32767; + else if (dat < -32768) + dat = -32768; + +#elif defined FILTER_CONSTANT + + /* Apply expected attenuation. (FILTER_MOOG does it implicitly, but this one is constant gain). + * Also stay at 24bits.*/ + dat = (dat * emu_voice->filt_att) >> 8; + + emu_voice->filt_buffer[0] = (coef1 * emu_voice->filt_buffer[0] + + coef0 * (dat + ((coef2 * (emu_voice->filt_buffer[0] - emu_voice->filt_buffer[1])) >> 24))) + >> 24; + emu_voice->filt_buffer[1] = (coef1 * emu_voice->filt_buffer[1] + + coef0 * emu_voice->filt_buffer[0]) + >> 24; + + emu_voice->filt_buffer[0] = ClipBuffer(emu_voice->filt_buffer[0]); + emu_voice->filt_buffer[1] = ClipBuffer(emu_voice->filt_buffer[1]); + + dat = (int32_t) (emu_voice->filt_buffer[1] >> 8); + if (dat > 32767) + dat = 32767; + else if (dat < -32768) + dat = -32768; + +#endif + } + if ((emu8k->hwcf3 & 0x04) && !CCCA_DMA_ACTIVE(emu_voice->ccca)) { + /*volume and pan*/ + dat = (dat * emu_voice->cvcf_curr_volume) >> 16; + + (*buf++) += (dat * emu_voice->vol_l) >> 8; + (*buf++) += (dat * emu_voice->vol_r) >> 8; + + /* Effects section */ + if (emu_voice->ptrx_revb_send > 0) { + emu8k->reverb_in_buffer[pos] += (dat * emu_voice->ptrx_revb_send) >> 8; + } + if (emu_voice->csl_chor_send > 0) { + emu8k->chorus_in_buffer[pos] += (dat * emu_voice->csl_chor_send) >> 8; + } + } + } + + if (emu_voice->env_engine_on) { + int32_t attenuation = emu_voice->initial_att; + int32_t filtercut = emu_voice->initial_filter; + int32_t currentpitch = emu_voice->ip; + /* run envelopes */ + emu8k_envelope_t *volenv = &emu_voice->vol_envelope; + switch (volenv->state) { + case ENV_DELAY: + volenv->delay_samples--; + if (volenv->delay_samples <= 0) { + volenv->state = ENV_ATTACK; + volenv->delay_samples = 0; + } + attenuation = 0x1FFFFF; + break; + + case ENV_ATTACK: + /* Attack amount is in linear amplitude */ + volenv->value_amp_hz += volenv->attack_amount_amp_hz; + if (volenv->value_amp_hz >= (1 << 21)) { + volenv->value_amp_hz = 1 << 21; + volenv->value_db_oct = 0; + if (volenv->hold_samples) { + volenv->state = ENV_HOLD; + } else { + /* RAMP_UP since db value is inverted and it is 0 at this point. */ + volenv->state = ENV_RAMP_UP; + } + } + attenuation += env_vol_amplitude_to_db[volenv->value_amp_hz >> 5] << 5; + break; + + case ENV_HOLD: + volenv->hold_samples--; + if (volenv->hold_samples <= 0) { + volenv->state = ENV_RAMP_UP; + } + attenuation += volenv->value_db_oct; + break; + + case ENV_RAMP_DOWN: + /* Decay/release amount is in fraction of dBs and is always positive */ + volenv->value_db_oct -= volenv->ramp_amount_db_oct; + if (volenv->value_db_oct <= volenv->sustain_value_db_oct) { + volenv->value_db_oct = volenv->sustain_value_db_oct; + volenv->state = ENV_SUSTAIN; + } + attenuation += volenv->value_db_oct; + break; + + case ENV_RAMP_UP: + /* Decay/release amount is in fraction of dBs and is always positive */ + volenv->value_db_oct += volenv->ramp_amount_db_oct; + if (volenv->value_db_oct >= volenv->sustain_value_db_oct) { + volenv->value_db_oct = volenv->sustain_value_db_oct; + volenv->state = ENV_SUSTAIN; + } + attenuation += volenv->value_db_oct; + break; + + case ENV_SUSTAIN: + attenuation += volenv->value_db_oct; + break; + + case ENV_STOPPED: + attenuation = 0x1FFFFF; + break; + + default: + break; + } + + emu8k_envelope_t *modenv = &emu_voice->mod_envelope; + switch (modenv->state) { + case ENV_DELAY: + modenv->delay_samples--; + if (modenv->delay_samples <= 0) { + modenv->state = ENV_ATTACK; + modenv->delay_samples = 0; + } + break; + + case ENV_ATTACK: + /* Attack amount is in linear amplitude */ + modenv->value_amp_hz += modenv->attack_amount_amp_hz; + modenv->value_db_oct = env_mod_hertz_to_octave[modenv->value_amp_hz >> 5] << 5; + if (modenv->value_amp_hz >= (1 << 21)) { + modenv->value_amp_hz = 1 << 21; + modenv->value_db_oct = 1 << 21; + if (modenv->hold_samples) { + modenv->state = ENV_HOLD; + } else { + modenv->state = ENV_RAMP_DOWN; + } + } + break; + + case ENV_HOLD: + modenv->hold_samples--; + if (modenv->hold_samples <= 0) { + modenv->state = ENV_RAMP_UP; + } + break; + + case ENV_RAMP_DOWN: + /* Decay/release amount is in fraction of octave and is always positive */ + modenv->value_db_oct -= modenv->ramp_amount_db_oct; + if (modenv->value_db_oct <= modenv->sustain_value_db_oct) { + modenv->value_db_oct = modenv->sustain_value_db_oct; + modenv->state = ENV_SUSTAIN; + } + break; + + case ENV_RAMP_UP: + /* Decay/release amount is in fraction of octave and is always positive */ + modenv->value_db_oct += modenv->ramp_amount_db_oct; + if (modenv->value_db_oct >= modenv->sustain_value_db_oct) { + modenv->value_db_oct = modenv->sustain_value_db_oct; + modenv->state = ENV_SUSTAIN; + } + break; + + default: + break; + } + + /* run lfos */ + if (emu_voice->lfo1_delay_samples) { + emu_voice->lfo1_delay_samples--; + } else { + emu_voice->lfo1_count.addr += emu_voice->lfo1_speed; + emu_voice->lfo1_count.int_address &= 0xFFFF; + } + if (emu_voice->lfo2_delay_samples) { + emu_voice->lfo2_delay_samples--; + } else { + emu_voice->lfo2_count.addr += emu_voice->lfo2_speed; + emu_voice->lfo2_count.int_address &= 0xFFFF; + } + + if (emu_voice->fixed_modenv_pitch_height) { + /* modenv range 1<<21, pitch height range 1<<14 desired range 0x1000 (+/-one octave) */ + currentpitch += ((modenv->value_db_oct >> 9) * emu_voice->fixed_modenv_pitch_height) >> 14; + } + + if (emu_voice->fixed_lfo1_vibrato) { + /* table range 1<<15, pitch mod range 1<<14 desired range 0x1000 (+/-one octave) */ + int32_t lfo1_vibrato = (lfotable[emu_voice->lfo1_count.int_address] * emu_voice->fixed_lfo1_vibrato) >> 17; + currentpitch += lfo1_vibrato; + } + if (emu_voice->fixed_lfo2_vibrato) { + /* table range 1<<15, pitch mod range 1<<14 desired range 0x1000 (+/-one octave) */ + int32_t lfo2_vibrato = (lfotable[emu_voice->lfo2_count.int_address] * emu_voice->fixed_lfo2_vibrato) >> 17; + currentpitch += lfo2_vibrato; + } + + if (emu_voice->fixed_modenv_filter_height) { + /* modenv range 1<<21, pitch height range 1<<14 desired range 0x200000 (+/-full filter range) */ + filtercut += ((modenv->value_db_oct >> 9) * emu_voice->fixed_modenv_filter_height) >> 5; + } + + if (emu_voice->fixed_lfo1_filt_mod) { + /* table range 1<<15, pitch mod range 1<<14 desired range 0x100000 (+/-three octaves) */ + int32_t lfo1_filtmod = (lfotable[emu_voice->lfo1_count.int_address] * emu_voice->fixed_lfo1_filt_mod) >> 9; + filtercut += lfo1_filtmod; + } + + if (emu_voice->fixed_lfo1_tremolo) { + /* table range 1<<15, pitch mod range 1<<14 desired range 0x40000 (+/-12dBs). */ + int32_t lfo1_tremolo = (lfotable[emu_voice->lfo1_count.int_address] * emu_voice->fixed_lfo1_tremolo) >> 11; + attenuation += lfo1_tremolo; + } + + if (currentpitch > 0xFFFF) + currentpitch = 0xFFFF; + if (currentpitch < 0) + currentpitch = 0; + if (attenuation > 0x1FFFFF) + attenuation = 0x1FFFFF; + if (attenuation < 0) + attenuation = 0; + if (filtercut > 0x1FFFFF) + filtercut = 0x1FFFFF; + if (filtercut < 0) + filtercut = 0; + + emu_voice->vtft_vol_target = env_vol_db_to_vol_target[attenuation >> 5]; + emu_voice->vtft_filter_target = filtercut >> 5; + emu_voice->ptrx_pit_target = freqtable[currentpitch] >> 18; + } + /* + I've recopilated these sentences to get an idea of how to loop + + - Set its PSST register and its CLS register to zero to cause no loops to occur. + -Setting the Loop Start Offset and the Loop End Offset to the same value, will cause the oscillator to loop the entire memory. + + -Setting the PlayPosition greater than the Loop End Offset, will cause the oscillator to play in reverse, back to the Loop End Offset. + It's pretty neat, but appears to be uncontrollable (the rate at which the samples are played in reverse). + + -Note that due to interpolator offset, the actual loop point is one greater than the start address + -Note that due to interpolator offset, the actual loop point will end at an address one greater than the loop address + -Note that the actual audio location is the point 1 word higher than this value due to interpolation offset + -In programs that use the awe, they generally set the loop address as "loopaddress -1" to compensate for the above. + (Note: I am already using address+1 in the interpolators so these things are already as they should.) + */ + emu_voice->addr.addr += ((uint64_t) emu_voice->cpf_curr_pitch) << 18; + if (emu_voice->addr.addr >= emu_voice->loop_end.addr) { + emu_voice->addr.int_address -= (emu_voice->loop_end.int_address - emu_voice->loop_start.int_address); + emu_voice->addr.int_address &= EMU8K_MEM_ADDRESS_MASK; + } + + /* TODO: How and when are the target and current values updated */ + emu_voice->cpf_curr_pitch = emu_voice->ptrx_pit_target; + emu_voice->cvcf_curr_volume = emu8k_vol_slide(&emu_voice->volumeslide, emu_voice->vtft_vol_target); + emu_voice->cvcf_curr_filt_ctoff = emu_voice->vtft_filter_target; + } + + /* Update EMU voice registers. */ + emu_voice->ccca = (((uint32_t) emu_voice->ccca_qcontrol) << 24) | emu_voice->addr.int_address; + emu_voice->cpf_curr_frac_addr = emu_voice->addr.fract_address; + +#if 0 + if (emu_voice->cvcf_curr_volume != old_vol[c]) { + pclog("EMUVOL (%d):%d\n", c, emu_voice->cvcf_curr_volume); + old_vol[c]=emu_voice->cvcf_curr_volume; + } + pclog("EMUFILT :%d\n", emu_voice->cvcf_curr_filt_ctoff); +#endif + } + + /* Only run reverb/chorus/EQ when at least one voice was active. */ + if (num_active > 0) { + buf = &emu8k->buffer[emu8k->pos * 2]; + emu8k_work_reverb(&emu8k->reverb_in_buffer[emu8k->pos], buf, &emu8k->reverb_engine, num_samples); + emu8k_work_chorus(&emu8k->chorus_in_buffer[emu8k->pos], buf, &emu8k->chorus_engine, num_samples); + emu8k_work_eq(buf, num_samples); + } + + /* Update EMU clock. */ + emu8k->wc += num_samples; + + emu8k->pos = wavetable_pos_global; +} + +void +emu8k_reset_buffer(emu8k_t *emu8k) +{ + emu8k->pos = 0; + memset(emu8k->buffer, 0, sizeof(emu8k->buffer)); + memset(emu8k->chorus_in_buffer, 0, sizeof(emu8k->chorus_in_buffer)); + memset(emu8k->reverb_in_buffer, 0, sizeof(emu8k->reverb_in_buffer)); +} + +void +emu8k_change_addr(emu8k_t *emu8k, uint16_t emu_addr) +{ + if (emu8k->addr) { + io_removehandler(emu8k->addr, 0x0004, emu8k_inb, emu8k_inw, NULL, emu8k_outb, emu8k_outw, NULL, emu8k); + io_removehandler(emu8k->addr + 0x400, 0x0004, emu8k_inb, emu8k_inw, NULL, emu8k_outb, emu8k_outw, NULL, emu8k); + io_removehandler(emu8k->addr + 0x800, 0x0004, emu8k_inb, emu8k_inw, NULL, emu8k_outb, emu8k_outw, NULL, emu8k); + emu8k->addr = 0; + } + if (emu_addr) { + emu8k->addr = emu_addr; + io_sethandler(emu8k->addr, 0x0004, emu8k_inb, emu8k_inw, NULL, emu8k_outb, emu8k_outw, NULL, emu8k); + io_sethandler(emu8k->addr + 0x400, 0x0004, emu8k_inb, emu8k_inw, NULL, emu8k_outb, emu8k_outw, NULL, emu8k); + io_sethandler(emu8k->addr + 0x800, 0x0004, emu8k_inb, emu8k_inw, NULL, emu8k_outb, emu8k_outw, NULL, emu8k); + } +} + +/* onboard_ram in kilobytes */ +void +emu8k_init(emu8k_t *emu8k, uint16_t emu_addr, int onboard_ram) +{ + uint32_t const BLOCK_SIZE_WORDS = 0x10000; + FILE *fp; + int c; + double out; + + emu8k->rom = (int16_t *)calloc(1024, 1024); + fp = emu8k_shim_rom_fopen(); + if (fp) { + if (fread(emu8k->rom, 1, 1048576, fp) != 1048576) + pclog("VWE AWE32: short GM ROM read; remainder stays silent\n"); + fclose(fp); + /*AWE-DUMP creates ROM images offset by 2 bytes, so if we detect this + then correct it*/ + if (emu8k->rom[3] == 0x314d && emu8k->rom[4] == 0x474d) { + memmove(&emu8k->rom[0], &emu8k->rom[1], (1024 * 1024) - 2); + emu8k->rom[0x7ffff] = 0; + } + } else { + pclog("VWE AWE32: no GM ROM (set VWE_AWE_ROM=); " + "ROM-sample presets will be silent\n"); + } + + emu8k->empty = (int16_t *)calloc(2, BLOCK_SIZE_WORDS); + + int j = 0; + for (; j < 0x8; j++) { + emu8k->ram_pointers[j] = emu8k->rom + (j * BLOCK_SIZE_WORDS); + } + for (; j < 0x20; j++) { + emu8k->ram_pointers[j] = emu8k->empty; + } + + if (onboard_ram) { + /*Clip to 28MB, since that's the max that we can address. */ + if (onboard_ram > 0x7000) + onboard_ram = 0x7000; + emu8k->ram = (int16_t *)calloc(1024, onboard_ram); + const int i_end = onboard_ram >> 7; + int i = 0; + for (; i < i_end; i++, j++) { + emu8k->ram_pointers[j] = emu8k->ram + (i * BLOCK_SIZE_WORDS); + } + emu8k->ram_end_addr = EMU8K_RAM_MEM_START + (onboard_ram << 9); + } else { + emu8k->ram = 0; + emu8k->ram_end_addr = EMU8K_RAM_MEM_START; + } + for (; j < 0x100; j++) { + emu8k->ram_pointers[j] = emu8k->empty; + } + + emu8k_reset_buffer(emu8k); + + emu8k_change_addr(emu8k, emu_addr); + + /*Create frequency table. (Convert initial pitch register value to a linear speed change) + * The input is encoded such as 0xe000 is center note (no pitch shift) + * and from then on , changing up or down 0x1000 (4096) increments/decrements an octave. + * Note that this is in reference to the 44.1Khz clock that the channels play at. + * The 65536 * 65536 is in order to left-shift the 32bit value to a 64bit value as a 32.32 fixed point. + */ + for (c = 0; c < 0x10000; c++) { + freqtable[c] = (uint64_t) (exp2((double) (c - 0xe000) / 4096.0) * 65536.0 * 65536.0); + } + /* Shortcut: minimum pitch equals stopped. I don't really know if this is true, but it's better + * since some programs set the pitch to 0 for unused channels. */ + freqtable[0] = 0; + + /* starting at 65535 because it is used for "volume target" register conversion. */ + out = 65535.0; + for (c = 0; c < 256; c++) { + attentable[c] = (int32_t) out; + out /= sqrt(1.09018); /*0.375 dB steps*/ + } + /* Shortcut: max attenuation is silent, not -96dB. */ + attentable[255] = 0; + + /* Note: these two tables have "db" inverted: 0 dB is max volume, 65535 "db" (-96.32dBFS) is silence. + * Important: Using 65535 as max output value because this is intended to be used with the volume target register! */ + out = 65535.0; + for (c = 0; c < 0x10000; c++) { +#if 0 + double db = -(c*6.0205999/65535.0)*16.0; + out = powf(10.f,db/20.f) * 65536.0; +#endif + env_vol_db_to_vol_target[c] = (int32_t) out; + /* calculated from the 65536th root of 65536 */ + out /= 1.00016923970; + } + /* Shortcut: max attenuation is silent, not -96dB. */ + env_vol_db_to_vol_target[0x10000 - 1] = 0; + /* One more position to accept max value being 65536. */ + env_vol_db_to_vol_target[0x10000] = 0; + + for (c = 1; c < 0x10000; c++) { + out = -680.32142884264 * 20.0 * log10(((double) c) / 65535.0); + env_vol_amplitude_to_db[c] = (int32_t) out; + } + /*Shortcut: max attenuation is silent, not -96dB.*/ + env_vol_amplitude_to_db[0] = 65535; + /* One more position to accept max value being 65536. */ + env_vol_amplitude_to_db[0x10000] = 0; + + for (c = 1; c < 0x10000; c++) { + out = log2((((double) c) / 0x10000) + 1.0) * 65536.0; + env_mod_hertz_to_octave[c] = (int32_t) out; + } + /*No hertz change, no octave change. */ + env_mod_hertz_to_octave[0] = 0; + /* One more position to accept max value being 65536. */ + env_mod_hertz_to_octave[0x10000] = 65536; + + /* This formula comes from vince vu/judge dredd's awe32p10 and corresponds to what the freebsd/linux AWE32 driver has. */ + float millis; + for (c = 0; c < 128; c++) { + if (c == 0) + millis = 0; /* This means never attack. */ + else if (c < 32) + millis = 11878.0 / c; + else + millis = 360 * exp((c - 32) / (16.0 / log(1.0 / 2.0))); + + env_attack_to_samples[c] = 44.1 * millis; + /* This is an alternate formula with linear increments, but probably incorrect: + * millis = (256+4096*(0x7F-c)) */ + } + + /* The LFOs use a triangular waveform starting at zero and going 1/-1/1/-1. + * This table is stored in signed 16bits precision, with a period of 65536 samples */ + for (c = 0; c < 65536; c++) { + int d = (c + 16384) & 65535; + if (d >= 32768) + lfotable[c] = 32768 + ((32768 - d) * 2); + else + lfotable[c] = (d * 2) - 32768; + } + /* The 65536 * 65536 is in order to left-shift the 32bit value to a 64bit value as a 32.32 fixed point. */ + out = 0.01; + for (c = 0; c < 256; c++) { + lfofreqtospeed[c] = (uint64_t) (out * 65536.0 / 44100.0 * 65536.0 * 65536.0); + out += 0.042; + } + + for (c = 0; c < 65536; c++) { + chortable[c] = sin(c * M_PI / 32768.0); + } + + /* Filter coefficients tables. Note: Values are multiplied by *16777216 to left shift 24 bits. (i.e. 8.24 fixed point) */ + for (uint8_t qidx = 0; qidx < 16; qidx++) { + out = 125.0; /* Start at 125Hz */ + for (c = 0; c < 256; c++) { +#ifdef FILTER_INITIAL + float w0 = sin(2.0 * M_PI * out / 44100.0); + /* The value 102.5f has been selected a bit randomly. Pretends to reach 0.2929 at w0 = 1.0 */ + float q = (qidx / 102.5f) * (1.0 + 1.0 / w0); + /* Limit max value. Else it would be 470. */ + if (q > 200) + q = 200; + filt_coeffs[qidx][c][0] = (int32_t) (w0 * 16777216.0); + filt_coeffs[qidx][c][1] = 16777216.0; + filt_coeffs[qidx][c][2] = (int32_t) ((1.0f / (0.7071f + q)) * 16777216.0); +#elif defined FILTER_MOOG + float w0 = sin(2.0 * M_PI * out / 44100.0); + float q_factor = 1.0f - w0; + float p = w0 + 0.8f * w0 * q_factor; + float f = p + p - 1.0f; + float resonance = (1.0 - pow(2.0, -qidx * 24.0 / 90.0)) * 0.8; + float q = resonance * (1.0f + 0.5f * q_factor * (w0 + 5.6f * q_factor * q_factor)); + filt_coeffs[qidx][c][0] = (int32_t) (p * 16777216.0); + filt_coeffs[qidx][c][1] = (int32_t) (f * 16777216.0); + filt_coeffs[qidx][c][2] = (int32_t) (q * 16777216.0); +#elif defined FILTER_CONSTANT + float q = (1.0 - pow(2.0, -qidx * 24.0 / 90.0)) * 0.8; + float coef0 = sin(2.0 * M_PI * out / 44100.0); + float coef1 = 1.0 - coef0; + float coef2 = q * (1.0 + 1.0 / coef1); + filt_coeffs[qidx][c][0] = (int32_t) (coef0 * 16777216.0); + filt_coeffs[qidx][c][1] = (int32_t) (coef1 * 16777216.0); + filt_coeffs[qidx][c][2] = (int32_t) (coef2 * 16777216.0); +#endif // FILTER_TYPE + /* 42.66 divisions per octave (the doc says quarter seminotes which is 48, but then it would be almost an octave less) */ + out *= 1.016378315; + /* 42 divisions. This moves the max frequency to 8.5Khz.*/ + // out *= 1.0166404394; + /* This is a linear increment method, that corresponds to the NRPN table, but contradicts the EMU8KPRM doc: */ + // out = 100.0 + (c+1.0)*31.25; //31.25Hz steps */ + } + } + /* NOTE! read_pos and buffer content is implicitly initialized to zero by the sb_t structure memset on sb_awe32_init() */ + emu8k->reverb_engine.reflections[0].bufsize = 2 * REV_BUFSIZE_STEP; + emu8k->reverb_engine.reflections[1].bufsize = 4 * REV_BUFSIZE_STEP; + emu8k->reverb_engine.reflections[2].bufsize = 8 * REV_BUFSIZE_STEP; + emu8k->reverb_engine.reflections[3].bufsize = 13 * REV_BUFSIZE_STEP; + emu8k->reverb_engine.reflections[4].bufsize = 19 * REV_BUFSIZE_STEP; + emu8k->reverb_engine.reflections[5].bufsize = 26 * REV_BUFSIZE_STEP; + + /*This is a bit random.*/ + for (c = 0; c < 4; c++) { + emu8k->reverb_engine.allpass[3 - c].feedback = 0.5; + emu8k->reverb_engine.allpass[3 - c].bufsize = (4 * c) * REV_BUFSIZE_STEP + 55; + emu8k->reverb_engine.allpass[7 - c].feedback = 0.5; + emu8k->reverb_engine.allpass[7 - c].bufsize = (4 * c) * REV_BUFSIZE_STEP + 55; + } + + /* Cubic Resampling ( 4point cubic spline) */ + double const resdouble = 1.0 / (double) CUBIC_RESOLUTION; + for (c = 0; c < CUBIC_RESOLUTION; c++) { + double x = (double) c * resdouble; + /* Cubic resolution is made of four table, but I've put them all in one table to optimize memory access. */ + cubic_table[c * 4] = (-0.5 * x * x * x + x * x - 0.5 * x); + cubic_table[c * 4 + 1] = (1.5 * x * x * x - 2.5 * x * x + 1.0); + cubic_table[c * 4 + 2] = (-1.5 * x * x * x + 2.0 * x * x + 0.5 * x); + cubic_table[c * 4 + 3] = (0.5 * x * x * x - 0.5 * x * x); + } + /* Even when the documentation says that this has to be written by applications to initialize the card, + * several applications and drivers ( aweman on windows, linux oss driver..) read it to detect an AWE card. */ + emu8k->hwcf1 = 0x59; + emu8k->hwcf2 = 0x20; + /* Initial state is muted. 0x04 is unmuted. */ + emu8k->hwcf3 = 0x00; +} + +void +emu8k_close(emu8k_t *emu8k) +{ + if (emu8k->rom) + free(emu8k->rom); + if (emu8k->ram) + free(emu8k->ram); +} diff --git a/emulator/vpx-device/emu8k.h b/emulator/vpx-device/emu8k.h new file mode 100644 index 0000000..57b916f --- /dev/null +++ b/emulator/vpx-device/emu8k.h @@ -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 +#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*/ diff --git a/emulator/vpx-device/emu8k_shim.h b/emulator/vpx-device/emu8k_shim.h new file mode 100644 index 0000000..7ea428b --- /dev/null +++ b/emulator/vpx-device/emu8k_shim.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 +#include +#include +#include +#include + +/* 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 diff --git a/emulator/vpx-device/vweawe.cpp b/emulator/vpx-device/vweawe.cpp new file mode 100644 index 0000000..fed6f99 --- /dev/null +++ b/emulator/vpx-device/vweawe.cpp @@ -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 + * /awe_front.s16 + /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 +#include + +#include "emu8k_shim.h" +#include "emu8k.h" + +int wavetable_pos_global = 0; + +/* one lock serializes all EMU8000 state access: guest port I/O (emulation + * thread) vs synthesis (render thread). Held only for register accesses and + * per-chunk renders, so contention stays in the microseconds. */ +static CRITICAL_SECTION awe_lock; + +/* WC interpolation: real-time samples elapsed since the render thread's + * last pass, so guest busy-waits on the 44kHz sample counter see smooth + * advancement between render chunks */ +static volatile LONGLONG awe_last_render_qpc = 0; +static LONGLONG awe_qpc_freq = 1; +unsigned emu8k_shim_wc_extra(void) { + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + LONGLONG d = now.QuadPart - awe_last_render_qpc; + if (d < 0) d = 0; + LONGLONG smp = (d * 44100) / awe_qpc_freq; + if (smp > 4410) smp = 4410; + return (unsigned)smp; +} + +/* activity stats (reported every ~10s of rendered audio per card) */ +struct AweStats { unsigned long wr, rd_wc, rd, smld_w; }; +static AweStats awe_stats[2]; /* 0 = front, 1 = rear */ + +/* ---- 86Box io_sethandler shim: dispatch DOSBox port I/O into the core --- */ +struct AweIoRange { + uint16_t base, size; + void *priv; + emu8k_io_inb_t inb; emu8k_io_inw_t inw; + emu8k_io_outb_t outb; emu8k_io_outw_t outw; + bool used; +}; +static AweIoRange awe_io[16]; + +static AweIoRange *awe_io_find(Bitu port) { + for (size_t i = 0; i < sizeof(awe_io) / sizeof(awe_io[0]); i++) + if (awe_io[i].used && port >= awe_io[i].base && + port < (Bitu)(awe_io[i].base + awe_io[i].size)) + return &awe_io[i]; + return NULL; +} + +/* VWE_AWE_LOG=1: trace the first accesses + growth-of-count milestones */ +static int awe_log_on = -1; +static unsigned long awe_access_count = 0; +static void awe_io_trace(const char *dir, Bitu port, Bitu val, Bitu iolen) { + if (awe_log_on < 0) { + const char *l = getenv("VWE_AWE_LOG"); + awe_log_on = (l && l[0] && l[0] != '0') ? 1 : 0; + } + if (!awe_log_on) return; + awe_access_count++; + if (awe_access_count <= 64 || + (awe_access_count & (awe_access_count - 1)) == 0) + fprintf(stderr, "VWE AWE32 io[%lu]: %s %03lx val=%04lx len=%lu\n", + awe_access_count, dir, (unsigned long)port, + (unsigned long)val, (unsigned long)iolen); +} + +/* PTR shadow per card: mirror of the core's cur_reg/cur_voice, so the glue + * can identify sample-memory (SMALW/SMLD/SMRD) traffic */ +static uint16_t awe_ptr[2]; +static int awe_sm_lines = 0; +static void awe_sm_trace(const char *dir, int ci, Bitu port, Bitu val) { + unsigned reg = (awe_ptr[ci] >> 5) & 7, voice = awe_ptr[ci] & 0x1F; + if (reg != 1) return; + if (voice == 26 && dir[0] == 'W') { /* SMLD data: count only */ + awe_stats[ci].smld_w++; + return; + } + /* log the write-pointer setups: each upload chunk starts with one */ + if (dir[0] != 'W' || (voice != 22 && voice != 23)) return; + if (awe_log_on <= 0 || awe_sm_lines >= 200) return; + awe_sm_lines++; + fprintf(stderr, "VWE AWE32 sm[%d]: card%d W %s(%s) val=%04lx\n", + awe_sm_lines, ci, (voice == 22) ? "SMALW" : "SMARW", + (port & 2) ? "hi/D2" : "lo/D1", (unsigned long)val); +} + +static Bitu awe_io_read(Bitu port, Bitu iolen) { + AweIoRange *r = awe_io_find(port); + if (!r) return ~0ul; + Bitu v; + EnterCriticalSection(&awe_lock); + if (iolen >= 2 && r->inw) v = r->inw((uint16_t)port, r->priv); + else if (r->inb) v = r->inb((uint16_t)port, r->priv); + else v = ~0ul; + LeaveCriticalSection(&awe_lock); + AweStats &s = awe_stats[(port & 0x40) ? 1 : 0]; + if ((port & 0xF9F) == 0xA02) s.rd_wc++; else s.rd++; /* A22/A42 */ + if ((port & 0xF1C) == 0xA00) /* A20-A23/A40-A43 */ + awe_sm_trace("R", (port & 0x40) ? 1 : 0, port, v); + awe_io_trace("R", port, v, iolen); + return v; +} +static void awe_io_write(Bitu port, Bitu val, Bitu iolen) { + AweIoRange *r = awe_io_find(port); + if (!r) return; + const int ci = (port & 0x40) ? 1 : 0; + awe_stats[ci].wr++; + if ((port & 0xF9E) == 0xE02) { /* E22/E23/E42/E43 */ + /* mirror the core's byte-write semantics (low byte lost on odd) */ + if (iolen >= 2) awe_ptr[ci] = (uint16_t)val; + else if (port & 1) awe_ptr[ci] = (uint16_t)(val << 8); + else awe_ptr[ci] = (uint16_t)(val & 0xFF); + } + if ((port & 0xF1C) == 0xA00) + awe_sm_trace("W", ci, port, val); + awe_io_trace("W", port, val, iolen); + EnterCriticalSection(&awe_lock); + if (iolen >= 2 && r->outw) r->outw((uint16_t)port, (uint16_t)val, r->priv); + else if (r->outb) r->outb((uint16_t)port, (uint8_t)val, r->priv); + LeaveCriticalSection(&awe_lock); +} + +void io_sethandler(uint16_t base, int size, + emu8k_io_inb_t inb, emu8k_io_inw_t inw, emu8k_io_inl_t, + emu8k_io_outb_t outb, emu8k_io_outw_t outw, + emu8k_io_outl_t, void *priv) { + for (size_t i = 0; i < sizeof(awe_io) / sizeof(awe_io[0]); i++) { + if (awe_io[i].used) continue; + awe_io[i].base = base; awe_io[i].size = (uint16_t)size; + awe_io[i].priv = priv; + awe_io[i].inb = inb; awe_io[i].inw = inw; + awe_io[i].outb = outb; awe_io[i].outw = outw; + awe_io[i].used = true; + IO_RegisterReadHandler(base, awe_io_read, IO_MB | IO_MW, (Bitu)size); + IO_RegisterWriteHandler(base, awe_io_write, IO_MB | IO_MW, (Bitu)size); + return; + } + fprintf(stderr, "VWE AWE32: io_sethandler table full\n"); +} +void io_removehandler(uint16_t base, int size, + emu8k_io_inb_t, emu8k_io_inw_t, emu8k_io_inl_t, + emu8k_io_outb_t, emu8k_io_outw_t, emu8k_io_outl_t, + void *priv) { + for (size_t i = 0; i < sizeof(awe_io) / sizeof(awe_io[0]); i++) + if (awe_io[i].used && awe_io[i].base == base && + awe_io[i].size == (uint16_t)size && awe_io[i].priv == priv) + awe_io[i].used = false; /* DOSBox handler stays; range inert */ +} + +FILE *emu8k_shim_rom_fopen(void) { + const char *p = getenv("VWE_AWE_ROM"); + if (!p || !p[0]) return NULL; + FILE *f = fopen(p, "rb"); + if (!f) + fprintf(stderr, "VWE AWE32: cannot open VWE_AWE_ROM '%s'\n", p); + return f; +} + +/* ---- the two cards + autonomous render thread --------------------------- */ +static emu8k_t awe_front, awe_rear; +static FILE *awe_dump_front = NULL, *awe_dump_rear = NULL; +static int awe_shift = 0; +static int awe_lead_ms = 80; +static HWAVEOUT awe_wo = NULL; +static volatile LONG awe_stop = 0; + +/* winmm slot ring: SLOT_FRAMES per buffer, SLOTS in flight max */ +#define AWE_SLOT_FRAMES 441 /* 10ms at 44100 */ +#define AWE_SLOTS 28 /* absolute pool size */ +static WAVEHDR awe_hdr[AWE_SLOTS]; +static int16_t awe_slotbuf[AWE_SLOTS][AWE_SLOT_FRAMES * 2]; +static 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" : ""); +}