DARKER Research archive

Reconstructed FM sound effects

2026-09-18. Open the effects gallery.

The main executable contains 39 ten-byte FM instrument patches, 19 timed / ambient effect records, and 33 ship/projectile sound bindings. These are now exported as structured JSON. There are 74 audible auditions: 39 reference patch tones, 18 nonzero-level effect-record previews, and 17 distinct baseline object sounds shared by the 33 slots. These are categories of previews, not a claim that the game contains 74 distinct sound effects.

Each has WAV, Ogg and sample-indexed register-trace exports. Record 37BA has zero initial volume and is exported without inventing an audible level.

Fidelity and scope

The register sequences come from executing the original 1121, 10F4 and 39A9 routines in Unicorn. Hardware polling at 1185 is intercepted. Register writes feed the Nuked OPL3 emulator, in its default OPL2-compatible mode, at 48 kHz. Unlike the music previews, these use the extracted FM patches directly, not General MIDI substitute instruments. The upstream code, license and pinned provenance are included under tools/vendor/nuked-opl3/.

The auditions deliberately supply controlled inputs:

Consequently these are useful, source-grounded isolated reconstructions. They are not a complete reproduction of the game's live sound scene. Preserve the JSON definitions and context when using the WAVs; a continuous engine sound is not intrinsically a fixed two-second sample.

Hardware detection and standalone music devices

The music selector and gameplay effects detection are independent. Executable 9C53 calls detector EAE2 before loading the selected music driver at 9C66–9C98. The detector first clears effects-hardware byte 0E81, scans for a Sound Blaster DSP, and falls back to the AdLib-compatible OPL timer test at port 0388 when no DSP is found. It does not consult music selector 0AB4.

If the OPL test fails, 0E81 remains zero. Both voice initialisation (0EDB) and live effects allocation (33F1) return immediately. This path has no Gravis-sample, Roland-MIDI or PC-speaker replacement for the gameplay effects. Music can still run through the independently selected driver if that driver initialises successfully.

Original hardware arrangement Gameplay effects outcome
Bare Gravis UltraSound without OPL hardware or compatibility software FM detection fails; gameplay effects disabled, independently of Gravis music
Standalone LAPC-I / MT-32 or SCC-1 / Sound Canvas setup without an OPL device Same: music device does not replace the FM effects synthesiser
Gravis or Roland music plus an AdLib-compatible OPL device FM effects can run alongside the selected music device
Sound Blaster / AWE32 with working OPL hardware FM effects use the detected FM path

These conclusions concern the original detector and effects code, not a claim of a full DOS playthrough on each physical card. A TSR or other compatibility layer can change what hardware the program detects; it is outside the bare-card cases above. Our reconstruction deliberately supplies an FM effects backend regardless of music choice, so choosing Gravis or Roland music does not mute its effects.

verify_effects_hardware.py executes the original detection path in Unicorn with no Sound Blaster DSP and with/without a responding OPL device, for all five music driver indices and the disabled music index. All 12 cases preserve the independence: no OPL gives flag zero and no voice-initialisation writes; a responding OPL gives flag one and initialisation writes. The disabled live-effects branch is also executed. The harness intercepts hardware delay routines and OPL writes, supplies OPL timer status, and does not emulate music-driver startup or a full DOS machine. See results and relevant disassembly.

AWE32 FM reverb and chorus: dry configuration

The original AWE32 library does initialise the FM input channels, but sets both FM effects sends to zero. This differs from the reverb/chorus used for its sampled music voices.

Driver resource 00/37, routine 2B8E, calls the FM input setup at 28E0 when the library reserves channels 30 and 31. Their loop addresses are the special FM input areas FFFFE0..FFFFE8 and FFFFF0..FFFFF8.

Channel (zero-based) PTRX initially written FM reverb send, bits 8–15 CSL written FM chorus send, bits 24–31
30 00000000 0 00FFFFE8 0
31 000000FF 0 00FFFFF8 0

The subsequent synchronisation write changes the high half of channel 30's PTRX to 4828, leaving its reverb-send byte zero. The register meanings and FM channel assignments agree with the Linux EMU8000 FM initialiser. The game's additional AWE32 API call AH=16, CX=0090 at 9CB4 reaches 13E3 in the driver and adjusts music volume; it does not enable FM effects.

The verification tool executes 28E0 and asserts both zero-send fields; its register trace is retained. A separate execution of the complete embedded initialiser 1002, using the existing EMU8000 backend, also left those sends at zero; final port/register-selection values are retained as supporting evidence. These checks establish register setup, not an analogue recording of a physical AWE32.

There is consequently no evidence here of a missing wet FM-effects mode to reconstruct. The EMU8000 OPL-input extension remains deferred. Selecting the ordinary FM music driver does not run this AWE32 initialiser at all; externally configured AWEUTIL effects could therefore be a separate machine-configuration variable, not an effect setting selected by Darker's FM music mode.

Instrument format

1121 indexes 0F26 + 10*patch_id. The table ends at 10AC, immediately before the nine 8-byte voice records. All 39 stored patches have byte 0 equal to 40.

Byte Interpretation from register writes
0, bits 6–7 Pitch block bias, (value & C0)/16 - 4 (zero for these patches)
1, low nibble Feedback / connection register C0
1, bits 6–7 First operator waveform selector
1, bits 4–5 Second operator waveform selector
2 / 6 Operator characteristic registers 20 / 23
3 First operator level register 40
4 / 8 Operator sustain/release registers 80 / 83
5 / 9 Operator attack/decay registers 60 / 63
7 Not consumed by this patch-loading path; retained in raw bytes

The second operator level is supplied dynamically through register 43, so it is not invented as a static patch field. 10F4 converts the supplied pitch parameter to F-number and block: repeatedly halve values at least 768, increasing block, then write A0 and B0. The 39A9 path computes carrier attenuation from the supplied level. Independent Python calculations are asserted against the original writes. Key gating is significant: flag 20 supplies the sustained key-on state; flag 08 avoids the explicit retrigger path. The auditions verify key-on at onset and key-off at their declared endpoint.

Timed records and object bindings

The 19 records at 3792..390D are 20 bytes each:

Offset Meaning
+0 Runtime deadline
+2 Associated object pointer / context
+4 Initial pitch parameter
+6 Duration byte
+7 Behaviour flags
+8,+A,+C Position components
+E Initial level
+10 Update/condition callback
+12 Effect identity byte
+13 Instrument patch ID

The record at 37CE is scheduled when the HUD boost-reserve count gains a full pip (5700–571B); Enter boost activation instead uses 3792. Other identities are intentionally presented by source address until gameplay and call sites establish a name. Some callbacks update pitch or position before admission to the audible list; stored values alone do not exhaust their behavior.

3556–358A reads the 33 special-object records at 1926 + 24*slot: callback at +12, patch at +14, level high byte at +15, and pitch at +16 (offsets hexadecimal). The level low byte is set to FF. Identical patch/pitch/level triples share an audition, while every slot and callback remains in the JSON.

This establishes Caero slot 25's baseline patch 7 and Skimma slot 26's baseline patch 8. Slots sharing a preview are not necessarily acoustically identical in play: different callbacks, speed and position can change the result.

Remaining live effects behavior

3488 admits audible sources based on distance/level and orders them for voice allocation. 39A9 programs the selected voice. Its spatial path computes relative geometry and calls 3ACD, which incorporates orientation and speed into a Doppler-like ratio. The stereo path begins at 3A2A.

Object callbacks 3914, 391D, 3942, 3957, 3969 and 3980 couple pitch, level and audibility to speed, pitch angle, lifetime and engine/cockpit state. For example, 391D adds speed >> 2 plus a timer/object-dependent variation to the initial pitch. Reconstructing these live controls is the next step toward matching a full play session; it should not be replaced with looping arbitrary WAV snippets in the modern engine.

Files and reproduction

PYTHONPATH=/tmp/darker-python python3 tools/extract_sound_effects.py
python3 tools/effect_gallery.py

Requires Unicorn, a C compiler and ffmpeg. The renderer is an offline tool; this work does not establish the eventual engine's audio dependency or final asset schema. It preserves both reusable procedural definitions and their provenance.

See ambient sound locations for verified source coordinates, regional selection, listening identifications and map overlays.

Particle/effect recipes additionally export 48 distinct packed sound-layer records attached to visual effects, including their patch, pitch, duration and level. These are not additional FM patches. Combined recipe auditions now render their concurrent layers as 15 shared sequences for all 21 recipe entry points.

In-flight message chirps

The mission-message handler requests a notification at native C427 only for nonempty, unsuppressed messages. B906 selects the sound by player craft:

Craft Effect record FM patch Pitch parameter Level Duration
Caero 38BE 31 13056 C000h 160 ticks (about 0.32 s)
Either Skimma 38E6 38 3464 D800h 112 ticks (about 0.224 s)

Both records can be auditioned in the effects gallery. They are nonspatial notifications, without distance attenuation or Doppler. They still compete for the original nine physical sound channels. Repeated messages restart the same fixed record; these are not independent overlapping notification voices.

Investigation of a missing-chirp playtest report found that the reconstruction was already triggering the Caero notification in mission 80. A live trace confirmed its full lifetime and a separate PCM test confirmed audible output. The reported Caero discrepancy remains unconfirmed. A separate error was found and corrected: the Skimma had incorrectly used the Caero notification instead of its own record.

Emulator-dependent notification timbre

A follow-up comparison established that the Caero notification in the reconstruction sounds like the extracted 38BE audition, while retail under DOSBox sounds clearer and louder. This narrows the discrepancy to assumptions shared by extraction and reconstruction, rather than a missing message trigger.

A controlled diagnostic fed the same captured 38BE OPL register sequence to DOSBox 0.74-3's DBOPL core and Nuked OPL3. DBOPL was also run at 44100 Hz, matching the local DOSBox oplrate, versus the reconstruction's 48000 Hz output. The patch uses FM operator waveforms 2 and 3 and pitch parameter 13056. Its high-frequency content produces substantially different audible spectra between these synthesis paths and sample rates. At 48000 Hz the isolated peak amplitudes were almost identical (DBOPL 1439, Nuked 1438 in signed 16-bit units), so a simple missing gain multiplier is not established by this comparison.

This is evidence for an emulator/rate-dependent timbre discrepancy, not yet a recording-based match to the live retail notification. Hardware initialisation, channel history and the live mixture remain possible contributors. Do not retune the original patch or increase its level merely to compensate for this unresolved difference.

Longplay recording comparison

The locally supplied darker-longplay-mt32.mkv contains the Caero notification at approximately 2:28.8, alongside “Well done—you can return to base”. The recording was described as an MT-32 longplay; its container metadata does not identify the sound-effects hardware or emulator. That description alone must not be treated as evidence of a physical OPL recording.

A spectral comparison used the mono mix of 148.80–148.96 seconds, subtracting background power measured at 149.10–149.50 seconds. Welch spectra used 4096-sample segments; isolated reference spectra used 40–190 ms after key-on and were interpolated onto the recording's 48 kHz frequency grid. Comparing linear power between 2 and 15 kHz gave these cosine similarities (shape comparisons, not perceptual quality scores):

Isolated synthesis reference Spectral similarity
DOSBox 0.74-3 DBOPL, 44100 Hz 0.957
DOSBox 0.74-3 DBOPL, 49716 Hz 0.031
Nuked OPL3, 48000 Hz output 0.024

Prominent shared recording/DBOPL-44100 peaks are approximately 4652, 7723, 9258 and 14684 Hz. This strongly supports the clearer DOSBox-style notification heard in this longplay. Crucially, it matches the 44100 Hz synthesis path, not merely the choice of emulator. The recording therefore supplies a useful reference for that playback configuration, but does not settle which path best reproduces physical OPL hardware. No patch pitch or level adjustment follows from this comparison; recording gain, compression and other simultaneous sounds prevent a direct loudness calibration.

Transient sound-record reuse

The effect and gun-impact lists each contain sixteen reusable sound records. Native 1CBF allocates a free record, or recycles the oldest active record when the list is full. Retirement at 1CD4 pushes records back onto the free list; newest-first traversal therefore affects the order of subsequent allocations. A recycled record can still own a physical OPL channel, so a new sound must retrigger that channel while retaining its ownership. Treating every new effect as a completely new identity loses this distinction during busy combat.

The reconstruction now preserves slot identity separately from note generation. 512 consecutive native allocation/retirement comparisons cover saturation, eviction and free-list reuse, and a mixer check covers a reused slot retaining its channel while starting a fresh note. This verifies that scheduling boundary, not every integrated hardware-register timing case.