Research notes

A snapshot of MUSIC.md from the Earthsiege analysis. Code addresses and capture filenames refer to the research workspace; this site carries selected notes and display data.

Native music research

Earthsiege uses HMI's protected-mode Sound Operating System. The 20 HMP resources include separate device-targeted tracks. A GM soundfont rendition is therefore an audition of one routing choice, not an authoritative transcription of the FM soundtrack. DESERT is a conspicuous example with no GM melody in the normal GM/PCM fixture.

The music browser keeps one row per resource and offers OPL2, OPL3 and soundfont auditions. Original compressed game resources are unchanged. FLAC exports use maximum compression.

Original-code boundaries

native_music.py runs original DBSIM 0x6e4c1 (song initialization/routing), 0x6ea6b (start), 0x6ed60 (delta reader) and the far timer callback 0x7ef95. The header field at 0x38 is passed to timer registration as Hz; all supplied songs use 120. Earlier provisional names bpm and declared_ppqn remain JSON aliases for compatibility, not established musical meanings.

check_music.py checks all 103,958 stored delta values and consumed-byte counts, including five-byte unsigned cases. Eighty routing fixtures cover twenty resources with GM/PCM, FM/PCM, callback/PCM and all listed device classes. Forty bounded native scheduler captures contain 169,947 packets. Those packet captures stop at entry to 0x715c5, before downstream channel allocation and gain; they are not audio captures.

native_fm.py extends that fixture through original channel/gain processing, the original archived FM module and the built-in digital MIDI driver. Module selectors reproduce the original separate code/data bases. Original 0x81687 queries the module and builds its five entry pointers; 0x71076 installs the preloaded module, and 0x72275 supplies the banks. native_opl.py supplies explicit virtual IN/OUT ports and records OPL register writes. The selected banks are MELODIC.BNK and DRUM.BNK, matching SoundManager's original loading path. It also calls 0x7225a(0), as SoundManager does: SOS channel remapping is disabled, gain processing remains active, and the FM module handles note polyphony. DONS.BNK is retained but has no established use in that path.

The digital MIDI driver is initialized with an explicit existing virtual digital backend, eight voices and velocity-controlled gain, matching the SoundManager descriptor. Its original bank reader, note/volume/pan handlers and voice-list bookkeeping execute. Host boundaries are sample start/stop, sample volume/pan, and completion notifications. The supplied unsigned 8-bit PCM is rendered at the game's default 11,025 Hz, resampled for the 44,100 Hz audition.

check_fm.py repeats OPL2 and OPL3 command captures and checks original PCM active-voice counts throughout. The separate LGPL Nuked OPL3 core supplies chip output; its unmodified source, license and provenance are in tools/vendor/nuked-opl3.

Device identifiers

Identifier Original implementation
0xa000 Generic/serial MIDI; HMP routing also accepts installed 0xa001 or 0xa008
0xa001 MPU-401 module
0xa002 OPL2 fmmidi.com; FM HMP routing also accepts 0xa009
0xa003 Built-in callback bridge, not an unidentified sound card
0xa004 MT-32 module
0xa005 Built-in digital-percussion MIDI driver
0xa006 Internal-speaker module
0xa008 AWE MIDI module
0xa009 OPL3 fmmidi3.com
0xa00a Gravis Ultrasound MIDI module

The callback bridge at 0x81719 invokes the installed far function at 0xa2594; 0x8175a sets it. Some HMP tracks routed to this bridge contain periodic program-change pulses. The normal game loading path explicitly installs its selected physical driver and 0xa005; it does not install this callback bridge. The endpoint audit below separates this unused routing option from ordinary game music.

Audition limits

Native FM auditions preserve the original scheduler's loop controls and physical-driver choices. They stop at a declared source-duration bound, then run original note cleanup and a two-second release tail. The export length is a listening bound; the game can continue looping. No game branch callbacks or in-game music transitions are supplied.

ISA delays, Sound Blaster DAC/mixer arithmetic and the analogue output stage are not emulated. Host PCM interpolation, stereo balance, FM/PCM relative mix, immediate stop handling and completion notifications at scheduler boundaries are labelled approximations. Overall gain is reduced only if necessary to keep the exported mix below clipping, and is recorded beside each FLAC. No loudness normalization is imposed on quieter material.

To rebuild native auditions:

.venv/bin/python tools/render_native_music.py
.venv/bin/python tools/render_native_music.py --opl3

The first run builds a small local C wrapper around Nuked OPL3. Regenerating HMP event exports preserves existing render metadata. Native protocol checks require the original executable and resources; they do not replace them with copied disassembly in a game implementation.

When songs play

analysis/music/selection.json exports the shell's twenty registered sound IDs and all 57 SOUNDS.STR records (including empty reserved entries). The browser now labels music roles rather than leaving their use implicit in filenames.

The GO shell's region-to-song table is [1,1,3,4,3,3,1] for regions 1000–7000: MENU, MENU, SHELLA, SHELLB, SHELLA, SHELLA, MENU. 0x395a0 queues a music change when the selected ID differs; the presentation loop 0x12980 applies it. Campaign overview scenes override the region music with RUBBLE for node 0, ICE for nodes 1–2, DESERT for 3–4, and VOLCANO for 5–7.

INTRO1 accompanies the Dynamix/lead-in sequence; INTRO2 accompanies the introduction, its replay and campaign victory sequences. Player death selects DEATH; campaign failure selects LOSING. WIN2 and SHELLC are registered resources with no identified selector in the complete supplied region/campaign/return/intro dispatch paths: the campaign victory dispatcher explicitly chooses INTRO2, so a filename alone must not substitute for the actual call.

In-mission transitions

Original DBSIM 0x585dc selects one of BATTLE1A, BATTLE2A and BATTLE3A with the shared visual/audio RNG. BATTLE1B–3B are paired encounter variants, three sound IDs later. tools/check_music_selection.py independently matches 1,200 selection/fade states, playback commands, volume and full RNG state. Distance, recognition, bearing and original pool iteration all run unchanged; only sound loading/playback and the final MIDI output are host boundaries.

After the unsigned clock passes a 300-tick deadline, an enabled, living side-1 actor can request the encounter variant if it is within 25,000 world units, or if the original cockpit detection and ±8,999-angle bearing checks both pass. A transition sets a new deadline. It fades the current song by 15 volume units per update. Original signed-byte volume clamping takes 100 through 85, 70, 55, 40, 25, 10 and 0; on the following update, the old song stops and its paired song loads at volume 100. This is a fade-out followed by a restart, not an overlapping crossfade. These updates occur before simulation/pause dispatch, not on the HMP's 120 Hz event clock.

The switch back has an unusual original condition: after no qualifying ground/actor threat, a living, enabled flyer farther than 18,928 units prevents the transition back to A. It does not check that flyer's side or activation group here. The comparison direction was verified in the instructions and varied in the independent fixture; no supposedly more sensible proximity test is substituted.

When the current song is not playing, the controller normally restarts it. The original early return for an already-active qualifying encounter skips that final restart check in that update. Music-disabled or absent-driver state bypasses the controller entirely. The 300 ticks belong to the engine's 0x981c4 clock, which increments on every second timer callback; their wall-clock conversion must follow timer setup rather than assuming they are milliseconds.

Positional effects and source coalescing

check_spatial_audio.py compares 6,144 positional source states and 6,000 submitted positions, including 909 enabled flushes. Original gain/pan calculations, source queuing, position setters and listener refresh run unchanged; only the final hardware gain setter is recorded. Results are in analysis/music/spatial-audio.json.

0x4fb60 updates up to sixty positional slots. Distance uses the same approximate 3D metric as other engine consumers, with zero replaced by one. At or beyond the configured cutoff, gain becomes zero. Otherwise gain is base - distance * base / cutoff. Large cutoffs shift both the cutoff and relative coordinates before calculating. Pan projects the normalized source direction onto the transformed listener X axis, then uses the original integer mapping to its 16-bit pan field; 0x4fdd8 applies this to one slot when its position changes.

Two original 1.0 details are now confirmed rather than corrected in the research:

These are software routing rules. They do not establish the exact analogue output level or mixer interpolation of a particular historical sound card.

Completed consumer audit and live runtime traces

usage-audit.json joins the original call sites, absolute references, song tables and all four complete DOSBox recordings. Normal SoundManager loaders (DBSIM 0x4ef8c, GO 0x4240a) install the configured physical MIDI driver and the digital-percussion driver 0xa005. They do not install 0xa003, whose callbacks remain an HMI library capability. No callback setter or dispatch invocation occurs during the four original playback runs. Its routed HMP tracks remain available for inspection, with no identified game-side purpose in this release. This does not assert that other applications using HMI ignore that device.

The original bank-loading paths (DBSIM 0x4f6d4, GO 0x42bf9) use MELODIC.BNK, DRUM.BNK and DRUM32.DIG. No DONS basename/reference occurs in either supplied application image or any extracted resource. It is retained as an alternate bank with no demonstrated game loader. WIN2 and SHELLC likewise remain available as unselected registered music; their names are not substituted for observed music choices. Computed access and different builds remain outside this bounded finding.

Complete playback observes 32,915 original MIDI output packets, 4,428 digital sample starts, 210 PCM MIDI completion callbacks, 174 SNC mouth-update calls, 16 song starts and 176 gain changes. The actual HMI drivers, DOS timer and digital backend execute inside DOSBox. Music A/B switches, speech and ordinary weapon effects coexist in these traces; this closes the earlier software-scheduling and completion boundary. Per-tape resource names, clocks and transition states are linked from the replay gallery. These are command/completion traces, not new audio recordings or a claim about historical analogue output. The standalone auditions retain their documented mixing limits.

PCM controls and archived digital mixer

capture_pcm_sound.py executes original PCM construction (0x507c8), gain, rate and loop setters, play (0x509c4), stop, HMI's 32-voice allocator, and completion (0x4e7fc). Loaded sample storage and completion timing are explicit boundaries. 2,560 stateful cases include 594 sample submissions and 273 replacements when channels are full. SOUNDS.STR attributes are loop count, base volume, preload flag and cache priority; the preload byte is not a repeat flag. Repeated plays can overlap, replacing the object's tracked handle while an older voice continues. The positional-volume query (0x5049c) multiplies by 32767 after division; its large result affects victim selection.

native_pcm.py loads the actual tr8m.com / tr8st.com mixer modules from HMIDRV.386, with their separate protected-mode selectors. Their dispatcher and self-generated mix loops execute unchanged. check_pcm_mixer.py compares 1,024 kernels / 262,144 source samples including signed gain quantization, the signed-byte intermediate overflow, saturating addition and stereo balance. capture_pcm_dma.py records 9,600 complete original ring-buffer/voice states; native C++ matches their ring CRCs, loop counts, positions and delayed releases. Virtual DMA count reads are the hardware boundary. Initial buffer lead is 256 bytes for mono and 128 for stereo, with guarded ahead-of-playback filling. Loop/end processing can discard fewer than four remaining source bytes.

Two details refine the earlier statement that effects may change pitch:

The native implementation defines the original positional-volume divide-by-zero case as zero. Unused custom subrange descriptors lack an initialized tail-length word; native subranges are bounded to sample data. These are documented defensive choices, not discoveries of valid authored sample-loop behavior. The modern output device's rate conversion and historical analogue circuitry remain separate.

Speech starts only when the SNC's initial mouth frame is nonzero and speech is enabled. All 556 simulation SNC resources have a nonzero initial frame; the gate is nevertheless preserved. Portrait clock and radio-ready state remain separate from successful digital voice allocation.

Authored footstep triggers and native playback

capture_footsteps.py executes DBSIM 0x144dc for 1,800 stateful cases. The requested movement sequence takes precedence over the current sequence. Foot contacts use each attachment's animated local Y: forward walk/turn/stopping, reverse walk/stopping and fast gait select threshold pairs 0, 1 and 2. Reverse playback reverses both comparisons. The disabled sequence uses attachment type 2, world Z, pair 3 and the initially armed fifth latch for sound 31. Foot contacts request sound 29 at the actor origin, not at the attachment. Comparisons are strict. Missing attachment objects do not advance the original latch pointer.

capture_herc_steps.py adds 2,560 complete movement/aiming updates on the eight original constructed player HERCs. Original attachment allocation, part lookup and hierarchy transforms execute. The corresponding native C++ animation path matches all 279 footstep requests. The native mission now consumes these and the projectile/explosion, collapse, collision, radar and shared flyer-engine requests through the verified PCM mixer. All 37 simulation samples and four live wingmate acknowledgement paths produce audible PCM in automated integration checks. The later whole-game audio checks below compare request order and identify the remaining engine-start timing limit.

capture_audio_controls.py records 6,000 original calls to the yaw/pitch sound gates (0x14afc, 0x14dc4), target tones (0x1552c) and cockpit-engine update (0x58574). Independent yaw/pitch latches share sound 33; the threshold is strictly greater than 192, and signed -32768 retains its original wrapped magnitude. Target acquisition/change uses 20, lock pulses 21 and lock loss 22. Pulses follow clock bit 6, not an invented interval timer. The sensor caller returns before the sound update if there is no target. Cockpit view 4 is external: it stops engine loop 45; other views start it only after startup sample 19 has finished. Native controllers and emitted requests agree with all captured states.

Native driver reconstruction checks

The C++ integration has additional original-code fixtures in analysis/music:

Two driver details surfaced while matching these captures. OPL3 implements nine paired stereo voices. Its downward bend indexes pitch range by voice, while the OPL2 driver indexes by MIDI channel. Both modules' 16-byte release cache overlaps the frequency cache for high operator indices, so later note starts can write release values influenced by previous frequencies. The native port preserves these behaviours instead of substituting generic MIDI-to-OPL conventions.

The percussion driver's volume and pan are shared by the device, regardless of MIDI channel. When its eight tracked entries are full, it attempts to evict a voice on the incoming channel; if none exists, it can start an untracked sample. The shared 32-voice digital backend still bounds actual playback. Its physical ring-slot removal order is retained, including wraparound.

Normal SoundObject MIDI playback does install a loop filter: 0x505c8 passes 0x4e718 to HMI 0x6ee87. This is separate from the optional callback driver and controller-119 callback, whose no-established-consumer boundaries still apply. Earlier native scheduler notes conflated them; the C++ documentation has been corrected. Markers without bit 7 always pass. Marked loops consult the configured/current repeat word: zero rejects, 65535 repeats indefinitely, and other nonzero values decrement and pass. SOUNDS.STR supplies 255 for combat music.

capture_music_loops.py executes that callback and its original loop setter for 3,072 playing/stopped, branch-ID and repeat-count combinations. capture_hmp_object_loops.py installs it in the original scheduler for MENU and BATTLE1A with counts 0, 1 and 65535, capturing 44,444 timed packets across six 23,000-tick runs. Zero and finite repeats finish; infinite repeats remain active. The native counterparts agree. These complement the driver-isolated auditions, which intentionally omit the game callback and use a declared duration bound.

The native live mission now connects these controllers to encounter observations and shared UI RNG, original music fades, OPL3 synthesis and the same 32-voice PCM backend used by effects. The host mix is 44.1 kHz signed stereo with equal FM/PCM headroom; original 11.025 kHz PCM values are held between DAC updates. This mix is not a measurement of an original card's analogue output balance.

Integrated replay audio

World sound requests dispatch at their original call sites, preserving order relative to UI, radio and effect requests. Radio opening/closing tones respect the original already-playing gate. MFD/HDD buttons, shield and weapon controls, weapon selection, options and alerts produce their original feedback. Alert-input fixtures also compare the original click count for each response.

Original 58574 selects music before gating the cockpit engine, then flushes positioned effects against the retained camera position before simulation. The listener refresh keeps the original ten-call cadence and identity orientation. Previously the native path refreshed after simulation against the HERC's ground position, shifting distant engines' audible boundaries.

The four replay tests additionally compare 4,474 ordered non-engine requests (1,328 / 1,500 / 1,052 / 594), including their frame and call order. All requests are retained in separate ESREQ001 fixtures generated from the unchanged DOSBox trace; expected requests are never fed into the game. Optional EARTHSIEGE_REPLAY_AUDIO writes the native requests, driver acceptance and active voice counts. Audio advances against the recorded outer clocks during replay.

Engine 16/45 starts depend on whether their prior sample is still playing and whether the shared 32-voice pool can accept it. They remain in the comparison reports, with the current counts in the final request-order section below. The lower-panel and retained-page visual differences found during this work are resolved in PIXEL_FIDELITY.md.

Flyer engine destruction state

Original 1afa8 reads the live actor's byte at 93 before stopping or positioning engine sound 16. The integration had read a cached decision-state flag, which can remain stale until another controller refresh. It now reads flyer_actor's actual destruction state; the replay audit also compares that flag against the original on every frame. Gameplay, random state and return files still agree.

This removed the stale destruction-state cause of repeated engine starts. The remaining differences depend on channel availability and sample completion; the final request-order section below gives the current counts.

Complete sound request ordering

Original 58574 gates cockpit-engine sound 45 on cockpit view 4, not on camera ownership. During a prepared transition those change on different frames. Reading the view state restores the original stop timing, including a frozen options preview. ESREQ002 now checks 4,474 non-engine starts and all 2,599 stops in exact combined frame/call order (7,073 requests), including engine stops. Engine starts/retries remain separately reported because they depend on the PCM completion and voice-allocation timeline: current differing-frame counts for cockpit engine 45 are 17/5/1/10, and flyer engine 16 differs in two DEMO3 frames.

Roland devices and the missing MT-32 arrangement

The native game now offers OPL2, OPL3, SC-55 v1.21, SCC-1A v1.30, MT-32/CM-32L and General MIDI SoundFont playback. Ctrl+Shift+M selects the device and saves it beside the game saves. Munt, Nuked-SC55 and TinySoundFont adapters reuse the approaches from Darker and TFX. Firmware fetch helpers and automatic asset lookup follow those projects too. The native music guide covers setup, locations and the two distinct MT-32 choices.

The supplied installer advertises an MT-32 option, but none of the twenty HMP resources assigns a melodic track to device 0xa004. This includes the separate SSET/SOUNDSET.HMP setup song. Original scheduler routing leaves those tracks at 255, meaning no installed destination. Only tracks assigned to the separately installed digital driver 0xa005 survive. In MENU, its first sampled-percussion hit occurs at tick 220, about 1.83 seconds after playback starts.

This is an original-data limitation in the supplied 1.0 release. The relevant boundaries were checked directly:

tools/capture_midi_pipeline.py executes the original installation, scheduler, gain, physical module and digital-percussion driver. All twenty songs are captured with each physical driver for 722 steps, including six master-volume levels, explicit PCM completion, stop and cleanup. analysis/music/midi-pipeline.json records 27,644 MPU-401 bytes, 3,120 MT-32 bytes and 1,546 digital events per selection. The MT-32 bytes are setup, volume and cleanup commands, not a missing melody arrangement. Native C++ matches all 30,764 bytes and 3,092 events. Ready MPU ports, timer registration and sample completion are controlled host inputs; this fixture does not measure hardware bus latency.

The strict MT-32 choice preserves the original omission. A separate MPU-401 experiment sends the available General MIDI parts to Munt, without rewriting program numbers or inventing a patch bank. Its instrumentation can therefore sound quite different. SC-55, SCC-1A and SoundFont choices retain any GM gaps in the data as well: DESERT, for example, has no GM melody route.

The analysis browser's existing exports remain OPL2, OPL3 and SoundFont listening copies. Adding native hardware backends does not relabel those files as Roland recordings. All synthesis modes are tested with actual local firmware or a SoundFont in the native build, including percussion, suspension, resume and switching devices. The complete native suite passes 272 tests, with six audio suites also passing ASan/UBSan and leak checking.

The private-window report is retained in analysis/music/window-check/report.json, with screenshots of selection, restoration after restart and the resumed menu. It uses an isolated Xvfb display and SDL dummy audio; real synthesis is exercised by the separate firmware tests.

Expansion media audit

The supplied expansion was inventoried after the original 1.0 audio checkpoint. It adds 13 HMP files: six battle songs and seven shell songs. None is byte-identical to the 20 base songs. Their hardware mappings and complete events are decoded under analysis/releases/music/ and linked in the editions browser. None of these 13 files assigns a track with nonzero-velocity note-on events to device 0xa004, the original MT-32 driver ID. This is a file-header/event observation; the expansion executable's music selection and driver execution still need comparison. Existing FLAC auditions and native audio tests cover the base game. See INSTALL_MEDIA.md for the expansion scope.