# Reconstructed FM sound effects

2026-09-18. [Open the effects gallery](../analysis/audio/effects/index.html).

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](https://github.com/nukeykt/Nuked-OPL3), 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:

- One voice on channel zero, no competing music/effects or voice stealing.
- No distance attenuation, camera panning or Doppler processing.
- Effect records use their stored initial pitch and volume. Their low duration
  byte is interpreted as 16 timer units per count, as in `36A6`; auditions assume
  500 timer units/second, consistent with the previously observed approximate
  PIT rate. Exact hardware timing and frame scheduling remain uncalibrated.
- Zero-duration ambient entries and object baselines are held two seconds.
  Reference patch auditions use pitch parameter 686, level B000 and one second.
- Two seconds of release follows key-off; this may truncate very long releases.
- World-dependent callback changes and activation conditions are not emulated.
  The exception is record `3832`: its original clock-driven pitch callback `365F`
  is executed at 20 ms intervals, beginning from timer zero.
- OPL waveform enable is set for audition, rhythm mode disabled. Buffered chip
  writes model register delivery, not the original CPU/PIT busy-wait spacing.

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`](../tools/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](../analysis/audio/hardware-effects/detection.json) and
[relevant disassembly](../analysis/audio/hardware-effects/relevant-disassembly.txt).

### 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](https://github.com/torvalds/linux/blob/master/sound/isa/sb/emu8000.c).
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](../analysis/audio/hardware-effects/awe32-fm-initialisation.json)
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](../analysis/audio/hardware-effects/awe32-initial-registers.json)
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

- [All definitions and slot aliases](../analysis/audio/effects/definitions.json)
- [Verification scope](../analysis/audio/effects/verification.json)
- Individual `.json` traces: `[sample_index, register, value]`, at 48 kHz.
- `.wav`: stereo signed 16-bit PCM; `.ogg`: convenient listening copies.

```sh
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](ambient-sounds.md) for verified source coordinates, regional selection, listening identifications and map overlays.

[Particle/effect recipes](effect-recipes.md) 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](combined-effect-audio.md) 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](../analysis/audio/effects/index.html).
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.
