‹ XENON 2How it worksSound driver explorer
Xenon 2: Megablast · extracted from mydumpat0

Sound driver explorer

Everything on this page was derived directly from the resident sound driver and its data tables inside a captured ST memory snapshot (loaded in Ghidra as /mydumpat0) — no Hatari session, audio recording, or external game files are used. The driver implements three independent mechanisms sharing the same three PSG channels: a music tracker, transient effects (SFX) "overlay voices" that borrow a channel from the tune, and one resident sample played through a software PCM DAC. See SOUND.MD for the full write-up this page demonstrates. An earlier pass mis-dispatched the tracker's command opcodes as 0xE0-0xF8; §2 below runs the corrected 0x80-0x98 opcode table against real pattern bytes read live from the dump.

1.Music — the 3-channel tracker

A per-frame register dump replayed through an emulated YM2149: 3 tone/noise channels stepped once per VBL (50 Hz) by the pattern sequencer. See SOUND.MD §4.

0:00.0frame 0 / 9802loop
Tick routine 0x0002CF50 VBL rate 50 Hz Intro 3657 frames Loop 9801 − 3657

2.Pattern disassembly — the corrected 0x80-0x98 opcodes

The three streams below are real pattern bytes, read live from the memory dump at each channel's currently-installed pattern pointer (song 0's three channel offsets, resolved through the driver's own two-level offset tables — not hand-picked or synthesized), then decoded byte-by-byte with the corrected opcode table from SOUND.MD §4.2. Every note, command, parameter, and duration below is read directly off those bytes. Toggle a channel to show/hide its disassembly and mute/solo it in the playback below — both views share the same filter state, the way SOUND.MD §8 argues effects and music already stay independent inside the real driver.

Source addresses: channel A 0x2DC76, B 0x2D99B, C 0x2DB14 (200 bytes each, SoundDataBlobBase_0002c6c6 + song-0's per-channel offset, resolved via DAT_0002d8ef/DAT_0002d8f0 — see FUN_0002cfa0, the driver's "install a song" routine).

How playback is synthesized: each note's volume follows the real envelope selected by the last 0xD0-0xDF command on that channel — 16 decoded sequences of direct 0-15 volume steps (SOUND.MD §4.2.2), stepping at that sequence's own measured rate and holding at its last value once it runs out, exactly as the driver's cursor/hold mechanic works; a fresh note always restarts its envelope from step 0. That decay-to-hold shape is what gives notes separation, not an artificial gate. Each channel's 0x8A/0x8B/0x8C mixer commands are simulated against one shared, accumulating mixer-register byte, using that channel's real +0x2F mask value read from the memory dump — this can gate a note's tone off, add the chip's shared noise generator on top, or both; a note whose pitch index falls beyond the decoded 96-entry table plays as noise-only (or silent) rather than a guessed pitch. Loop/jump commands (0x87/0x93) are shown in the list and followed during playback, using each channel's real jump-list read from the memory dump (SOUND.MD §4.2.4) — when a channel hits 0x87 it jumps to the real target address and keeps playing from there, which is usually a resync point back near that channel's own start but sometimes a genuine variation; this is bounded (an excerpt still ends rather than looping forever) and stops cleanly if a jump lands somewhere bytes weren't fetched for. The lists above show this same followed-loop sequence, so everything played highlights a real row, including whatever a jump landed on. Every channel's volume also has the driver's real global attenuation term applied (DAT_0002d0bd, SOUND.MD §4.1 step 3) — normally zero (song install's default), but exposed here as the Fade slider below since it's a game-triggered fade rather than something the pattern bytes encode. Vibrato/portamento (0x82/0x84/0x88) and transpose (0x92) are applied to the synthesized pitch, straight from their measured parameters. Tick rate uses song 0's real tempo byte (0x4C) against the driver's carry-accumulator formula (SOUND.MD §4.1) rather than a flat 50 Hz.

Controls: the Tempo slider scales tick length (10%-150%) purely to make the excerpt easier to follow by ear — it doesn't change what's decoded, only how fast it's replayed, and takes effect on the next Play excerpt; Fade works the same way (15 = off, matching the driver's own no-attenuation default). The currently-sounding note (and the command/parameter bytes that produced it) highlights live in the lists above while playing; channel toggles mute/unmute in real time, mid-playback. Each column also has its own step controls — step one raw byte at a time, independent of the other two channels and of full playback; landing on a note byte plays it in isolation (using whatever transpose was set earlier in that channel's stream). Click any row to jump the cursor straight to it, or click anywhere in a column and use ↑/↓ — both work regardless of whether the step buttons themselves are still on screen, since the list can get taller than the viewport once you scroll or expand it. Each column's width is independently resizable — drag its bottom-right corner to widen it enough to read a line in full (the row scrolls sideways if all three no longer fit side by side). Height is shared: Expand all opens every list to its full length at once, in sync; click it again to collapse all three back to a fixed, individually-scrollable height. Each list is the channel's real unrolled sequence, not just its first 200 bytes: every 0x87 it hits is actually followed (SOUND.MD §4.2.4), so the list keeps going into whatever real pattern bytes that jump lands on — shown as a gold loop → 0xADDR row — for as long as that's bounded and fetched (a run stops at a dashed ■ row when it hits 0x8E, runs off the edge of fetched data, or reaches the excerpt's length/jump limits). The leading number on each row is that byte's real absolute address, not an offset into a fixed 200-byte block — addresses jump around once a loop is followed, since a channel's stream can now span several disjoint regions of the dump. Align by time re-lays the same unrolled bytes out by tick instead: every channel's row N now starts at the same t-numbered tick (SOUND.MD §4.1's shared per-VBL clock), with blank filler lines padding out any channel that's mid-note or idle while another is busier at that moment — and because this is built from each channel's real unrolled walk, the alignment stays honest across a loop, not just for the first pass through the original excerpt.

Loop jump-lists

Each row is one channel's real 0x87 jump-list (SOUND.MD §4.2.4), laid out in visit order — not a merged graph, because the real mechanism isn't a runtime branch choice: each channel just walks its own list in a fixed cycle, position 0 to N‑1 then back to 0. Most positions point back to that channel's own pattern start (dimmed own start boxes); a detour box is a genuine variation, read live from the dump; an unknown box (outlined red) is a jump target whose bytes were never fetched — a dead branch, not a guess. The gold marker tracks each channel's real live position while Play excerpt is running, computed from the same 0x87 events driving the audio, so it moves exactly when a followed jump actually fires.

Loop-list mesh — true per-edge graph

The same basic-block structure as above, laid out by real Graphviz (fdp, force-directed — nothing hand-positioned) with every real edge drawn, not simplified: each block that ends in 0x87 connects directly to every target its channel's jump-list can resolve to (258 edges total). Node border color is the owning channel; a thicker off-white border marks the one block reachable by more than one channel; dashed red marks the one unfetched jump target. Channel A's blocks share no edges with B or C, so Graphviz's own component-packing (pack/packmode, not a hand-placed position) stacks that island above the B/C cluster. While Play excerpt (above) is running, each channel's current block is outlined and gets a small triangular cursor in that channel's color — driven by the same real event stream as the audio and the strips above, so all three views always agree.

loopgraph b2dc76 0x2DC76 120B, 45n b2dc76->b2dc76 b2dcee 0x2DCEE 60B, 22n b2dc76->b2dcee b2dd2a 0x2DD2A 105B, 40n b2dc76->b2dd2a b2dd93 0x2DD93 89B, 36n b2dc76->b2dd93 b2ddec 0x2DDEC 106B, 38n b2dc76->b2ddec b2de56 0x2DE56 31B, 12n b2dc76->b2de56 b2dec9 0x2DEC9 209B, 78n b2dc76->b2dec9 unknown 0x2DF9A unfetched b2dc76->unknown b2dcee->b2dc76 b2dcee->b2dcee b2dcee->b2dd2a b2dcee->b2dd93 b2dcee->b2ddec b2dcee->b2de56 b2dcee->b2dec9 b2dcee->unknown b2dd2a->b2dc76 b2dd2a->b2dcee b2dd2a->b2dd2a b2dd2a->b2dd93 b2dd2a->b2ddec b2dd2a->b2de56 b2dd2a->b2dec9 b2dd2a->unknown b2dd93->b2dc76 b2dd93->b2dcee b2dd93->b2dd2a b2dd93->b2dd93 b2dd93->b2ddec b2dd93->b2de56 b2dd93->b2dec9 b2dd93->unknown b2ddec->b2dc76 b2ddec->b2dcee b2ddec->b2dd2a b2ddec->b2dd93 b2ddec->b2ddec b2ddec->b2de56 b2ddec->b2dec9 b2ddec->unknown b2de56->b2dc76 b2de56->b2dcee b2de56->b2dd2a b2de56->b2dd93 b2de56->b2ddec b2de56->b2de56 b2de56->b2dec9 b2de56->unknown b2dec9->b2dc76 b2dec9->b2dcee b2dec9->b2dd2a b2dec9->b2dd93 b2dec9->b2ddec b2dec9->b2de56 b2dec9->b2dec9 b2dec9->unknown b2d990 0x2D990 11B, 8n b2d990->b2d990 b2db14 0x2DB14 24B, 12n b2d990->b2db14 b2db2c 0x2DB2C 66B, 36n b2d990->b2db2c b2db6e 0x2DB6E 28B, 13n b2d990->b2db6e b2db8a 0x2DB8A 87B, 40n b2d990->b2db8a b2dbc3 0x2DBC3 30B, 4n b2d990->b2dbc3 b2dbe1 0x2DBE1 26B, 12n b2d990->b2dbe1 b2dbfb 0x2DBFB 48B, 23n b2d990->b2dbfb b2dc2b 0x2DC2B 58B, 21n b2d990->b2dc2b b2dc65 0x2DC65 12B, 5n b2d990->b2dc65 b2dc71 0x2DC71 5B, 0n b2d990->b2dc71 b2d99b 0x2D99B 16B, 1n b2d99b->b2d99b b2d9ab 0x2D9AB 103B, 40n b2d99b->b2d9ab b2d9b7 0x2D9B7 91B, 38n b2d99b->b2d9b7 b2da12 0x2DA12 26B, 12n b2d99b->b2da12 b2da2c 0x2DA2C 35B, 29n b2d99b->b2da2c b2da4f 0x2DA4F 18B, 13n b2d99b->b2da4f b2da61 0x2DA61 169B, 70n b2d99b->b2da61 b2db0a 0x2DB0A 10B, 1n b2d99b->b2db0a b2d99b->b2dbe1 b2d9ab->b2d99b b2d9ab->b2d9ab b2d9ab->b2d9b7 b2d9ab->b2da12 b2d9ab->b2da2c b2d9ab->b2da4f b2d9ab->b2da61 b2d9ab->b2db0a b2d9ab->b2dbe1 b2d9b7->b2d99b b2d9b7->b2d9ab b2d9b7->b2d9b7 b2d9b7->b2da12 b2d9b7->b2da2c b2d9b7->b2da4f b2d9b7->b2da61 b2d9b7->b2db0a b2d9b7->b2dbe1 b2da12->b2d99b b2da12->b2d9ab b2da12->b2d9b7 b2da12->b2da12 b2da12->b2da2c b2da12->b2da4f b2da12->b2da61 b2da12->b2db0a b2da12->b2dbe1 b2da2c->b2d99b b2da2c->b2d9ab b2da2c->b2d9b7 b2da2c->b2da12 b2da2c->b2da2c b2da2c->b2da4f b2da2c->b2da61 b2da2c->b2db0a b2da2c->b2dbe1 b2da4f->b2d99b b2da4f->b2d9ab b2da4f->b2d9b7 b2da4f->b2da12 b2da4f->b2da2c b2da4f->b2da4f b2da4f->b2da61 b2da4f->b2db0a b2da4f->b2dbe1 b2da61->b2d99b b2da61->b2d9ab b2da61->b2d9b7 b2da61->b2da12 b2da61->b2da2c b2da61->b2da4f b2da61->b2da61 b2da61->b2db0a b2da61->b2dbe1 b2db0a->b2d99b b2db0a->b2d9ab b2db0a->b2d9b7 b2db0a->b2da12 b2db0a->b2da2c b2db0a->b2da4f b2db0a->b2da61 b2db0a->b2db0a b2db0a->b2dbe1 b2db14->b2d990 b2db14->b2db14 b2db14->b2db2c b2db14->b2db6e b2db14->b2db8a b2db14->b2dbc3 b2db14->b2dbe1 b2db14->b2dbfb b2db14->b2dc2b b2db14->b2dc65 b2db14->b2dc71 b2db2c->b2d990 b2db2c->b2db14 b2db2c->b2db2c b2db2c->b2db6e b2db2c->b2db8a b2db2c->b2dbc3 b2db2c->b2dbe1 b2db2c->b2dbfb b2db2c->b2dc2b b2db2c->b2dc65 b2db2c->b2dc71 b2db6e->b2d990 b2db6e->b2db14 b2db6e->b2db2c b2db6e->b2db6e b2db6e->b2db8a b2db6e->b2dbc3 b2db6e->b2dbe1 b2db6e->b2dbfb b2db6e->b2dc2b b2db6e->b2dc65 b2db6e->b2dc71 b2db8a->b2d990 b2db8a->b2db14 b2db8a->b2db2c b2db8a->b2db6e b2db8a->b2db8a b2db8a->b2dbc3 b2db8a->b2dbe1 b2db8a->b2dbfb b2db8a->b2dc2b b2db8a->b2dc65 b2db8a->b2dc71 b2dbc3->b2d990 b2dbc3->b2db14 b2dbc3->b2db2c b2dbc3->b2db6e b2dbc3->b2db8a b2dbc3->b2dbc3 b2dbc3->b2dbe1 b2dbc3->b2dbfb b2dbc3->b2dc2b b2dbc3->b2dc65 b2dbc3->b2dc71 b2dbe1->b2d990 b2dbe1->b2d99b b2dbe1->b2d9ab b2dbe1->b2d9b7 b2dbe1->b2da12 b2dbe1->b2da2c b2dbe1->b2da4f b2dbe1->b2da61 b2dbe1->b2db0a b2dbe1->b2db14 b2dbe1->b2db2c b2dbe1->b2db6e b2dbe1->b2db8a b2dbe1->b2dbc3 b2dbe1->b2dbe1 b2dbe1->b2dbe1 b2dbe1->b2dbfb b2dbe1->b2dc2b b2dbe1->b2dc65 b2dbe1->b2dc71 b2dbfb->b2d990 b2dbfb->b2db14 b2dbfb->b2db2c b2dbfb->b2db6e b2dbfb->b2db8a b2dbfb->b2dbc3 b2dbfb->b2dbe1 b2dbfb->b2dbfb b2dbfb->b2dc2b b2dbfb->b2dc65 b2dbfb->b2dc71 b2dc2b->b2d990 b2dc2b->b2db14 b2dc2b->b2db2c b2dc2b->b2db6e b2dc2b->b2db8a b2dc2b->b2dbc3 b2dc2b->b2dbe1 b2dc2b->b2dbfb b2dc2b->b2dc2b b2dc2b->b2dc65 b2dc2b->b2dc71 b2dc65->b2d990 b2dc65->b2db14 b2dc65->b2db2c b2dc65->b2db6e b2dc65->b2db8a b2dc65->b2dbc3 b2dc65->b2dbe1 b2dc65->b2dbfb b2dc65->b2dc2b b2dc65->b2dc65 b2dc65->b2dc71 b2dc71->b2d990 b2dc71->b2db14 b2dc71->b2db2c b2dc71->b2db6e b2dc71->b2db8a b2dc71->b2dbc3 b2dc71->b2dbe1 b2dc71->b2dbfb b2dc71->b2dc2b b2dc71->b2dc65 b2dc71->b2dc71

3.Effects — the 3 overlay voices

Each button below re-synthesizes one SFX from the driver's own measured data: a fixed tone/noise mix at a fixed period, gated by a linear volume-decay envelope read straight out of the memory dump (not guessed). Runs on its own AudioContext, independent of the music player above — muting one never touches the other, which is exactly the separation SOUND.MD §8 leans on for "keep effects, silence music." See SOUND.MD §5.

EffectcmdPeriodTone HzToneNoiseEnvelope stepsDuration

Noise-generator clock isn't stored per effect in the driver (it's shared with whichever pitch the music sequencer's noise-period register currently holds) — this demo uses a representative default. Everything else in the table (period, mixer gating, envelope shape/rate) is read verbatim from the dump.

4.Samples — the PCM8 software DAC

The one sample resident in this dump, extracted byte-for-byte from PCM_SampleData_15320Bytes_0002f182 (15,320 bytes) and wrapped in a plain 8-bit unsigned/4800 Hz WAV header — this is the driver's real sample data, not a resynthesis. In the original driver these bytes are instead streamed through an MFP Timer D interrupt into a 256-entry lookup table that spreads each byte across all three PSG channels' volume registers as a makeshift higher-resolution DAC (SOUND.MD §7) — starting it always stops the tracker/SFX engine first, so it never mixes with sections 1-3 on real hardware.

15,320 bytes4,800 Hz · 8-bit unsigned mono≈3.192s

5.Architecture — muting & mixing

The eventual goal is independently muting music/samples while keeping effects, and potentially mixing in external music. The driver's own control flow already makes this tractable — see SOUND.MD §8 for the full reasoning; summarized here:

SamplesFully independent — only run through the Timer D ISR and always call StopAll first. Disabling the PCM path removes samples with zero effect on music or SFX.
EffectsAlready take priority over music at the final register-shadow merge, unconditionally — an SFX overlay overwrites its channel's tone/volume/mixer regardless of what the music step wrote there first.
MusicComputed by a fully separate code path (ChannelStep_*) merged only at the last step — silencing just its contribution shouldn't disturb an active SFX overlay on the same channel, or the shared noise LFSR.
External musicBecause the merge point is a single 14-byte shadow struct, an emulation-layer hook could substitute external audio for the tracker's contribution there while leaving SFX ticking normally against it.

Not yet implemented or verified against a live capture — this is a reading of the existing driver's control flow, not a tested patch.