At a glance

In the game
Cutscene dialogue and FMV audio, streamed music tracks, and the per-character Art shouts in battle (Vahn's file is XA2, Noa's XA4, Gala's XA6).
Magic / marker
No file magic - per-sector CD-XA subheader with submode AUDIO | FORM2 (0x04 | 0x20)
Lives in
Top-level disc files XA1.XA, XA2.XA, ... (one or more per release) - not inside PROT.DAT; each file multiplexes up to 16 channels keyed by (file_no, ch_no)
Parser
crates/xa - ADPCM decoder (lib.rs), sector demuxer (demux.rs), WAV export; CLI xa demux-disc-all
Confidence
Confirmed - the decoder is bit-exact, sample for sample, against an external lossless reference decode of a real cutscene track (disc-gated oracle xa_pcm_matches_reference).

Sector layout

A CD stores 2352 bytes per sector. Data files use 2048 of them and spend the rest on error correction; streaming audio (Mode 2 Form 2) trades most of that correction away for 276 more bytes of payload per sector, because a click in an audio stream matters less than a corrupted byte in a program. Each on-disc XA sector is the standard PSX raw layout:

OffsetSizeFieldMeaning
+0x00012sync00 + 10 x FF + 00
+0x00C4headerMM SS FF mode (mode 2)
+0x0108CD-XA subheaderfile_no, ch_no, submode, coding_info + a duplicated copy
+0x0182304user data18 sound groups x 128 B of ADPCM audio; the trailing 0x14 bytes of the 2324-byte Form 2 payload are padding
+0x92C4EDCError-detection code
Consecutive sectors of one XA file belong to different channels; the demuxer sorts them by (file_no, ch_no) into per-channel streams disc sectors (2352 B each) ch 0 ch 1 ch 2 ... ch 0 ch 1 ch 2 ... channel 0 stream 2304 B 2304 B ... each 2304 B payload = 18 sound groups x 128 B; a group decodes to 224 samples (4-bit)
The interleave is what lets the drive stream several voice channels at a fixed read speed; it also means a naive 2048-byte file copy shreds the audio (see the history note under Demuxing).

Subheader bits

Submode bits relevant for audio detection:

bitmeaning
0x04AUDIO
0x20FORM2

Coding-info bits:

bitsmeaning
0 (0x01)stereo (vs mono)
2..=3sample rate (00 = 37.8 kHz, 01 = 18.9 kHz)
4..=5bits/sample (00 = 4-bit, 01 = 8-bit)

The 18 sound groups inside the user data are 128-byte CD-XA ADPCM blocks; see the lib doc-comment for the per-block layout (8 sound units, 28 lines x 4 bytes).

Demuxing: getting the tracks out

Each .XA file is really many audio channels multiplexed together, one channel per sector, tagged by the per-sector (file_no, ch_no) subheader. crates/xa/src/demux.rs (demux_disc_range) reads raw 2352-byte sectors, parses each subheader, filters to AUDIO + FORM2, and splits the audio data into one buffer per (file_no, ch_no) tuple. After that step the per-channel buffer is a clean concatenation of standard 128-byte sound groups that the ADPCM decoder handles directly.

The xa demux-disc-all subcommand drives this across the whole disc - it walks the ISO9660 tree, finds every *.XA, and demuxes each at its own per-sector sample rate / channel mode read from the subheaders (no guessed global rate). This is the “get the voices and streamed music out” command:

./target/release/xa demux-disc-all \
    "/path/to/Legend of Legaia (USA).bin" \
    --out extracted/xa_demux

One WAV lands per (file_no, ch_no) channel under extracted/xa_demux/, named <xa-stem>_fileN_chM.wav. The single-file xa demux-disc --lba --size variant remains for targeting one entry by directory offset. legaia-extract runs the demux automatically and writes the WAVs to extracted/XA_WAV/.

Pacing is data-driven per track - the whole point. A track that varies channel mode (the NA disc has 16-channel mono voice files like XA4/XA6 alongside 8-channel stereo music like XA5/XA7/XA8/XA9) decodes each channel at its real width. Channels report their width via coding_info; the decoder handles both 4-bit and 8-bit, and any other (unexpected) width is skipped with a warning rather than mis-decoded.

History: the "non-standard interleave" reading

The earliest extracted-XA tooling truncated each on-disc sector to 2048 bytes (Form 1 mode), which silently dropped 276 bytes per sector of audio and collapsed every channel into a single shuffled byte sequence, because the per-sector (file_no, ch_no) subheader was discarded. Only ~10 % of 128-byte sound groups in that output passed the standard XA rule bytes 8..16 == bytes 0..8. That is all the "non-standard interleave" shorthand of earlier doc revisions amounted to - not a bespoke Legaia muxing scheme, just Form-1-truncation damage. The same truncation made the old convert path read a stereo track as mono and play it at 2x speed. The extracted/XA/*.XA files the disc-extract step still copies are those Form-1-truncated bytes - usable for byte-stable hashing only, never for decoding. More retired readings: do not re-walk.

Sound-group decode (4-bit)

Each 128-byte sound group holds 8 sound units of 28 samples. The decode is bit-exact against an external lossless reference decode of a real cutscene track (every interleaved sample matches), so the layout below is confirmed, not inferred.

OffsetSizeFieldMeaning
+016parameter bytesOne (filter << 4) | range byte per sound unit, filter 0..=3, range 0..=12, with the redundant copy interleaved within each half (layout below)
+16112sample nibbles28 lines x 4 bytes; unit u reads byte u / 2 of each line, low nibble for even u, high nibble for odd

Parameter bytes (0..16). The redundant copy is interleaved within each half, not appended:

byte:  0  1  2  3   4  5  6  7   8  9 10 11  12 13 14 15
unit: p0 p1 p2 p3  p0 p1 p2 p3  p4 p5 p6 p7  p4 p5 p6 p7

So unit u's parameter byte is at u + (if u < 4 { 0 } else { 4 }). Reading bytes 0..8 as eight sequential params - the "appended mirror" reading - mis-assigns the parameters of units 4..7 and is the classic CD-XA decode trap.

Sample nibbles (16..128). 28 lines of 4 bytes:

line byte:  0           1           2           3
nibble:   lo=unit0     lo=unit2    lo=unit4    lo=unit6
          hi=unit1     hi=unit3    hi=unit5    hi=unit7

Per-sample reconstruction (filter coefficients in 1/64 units, k0 = {0, 0.9375, 1.796875, 1.53125}, k1 = {0, 0, -0.8125, -0.859375}):

shifted = (sign_extend(nibble, 4) << 12) >> range
value   = shifted + k0 * prev1 + k1 * prev2
output  = clip16(round_half_away_from_zero(value))
prev2   = prev1;  prev1 = value     // history is the UNCLAMPED, UNROUNDED value

The predictor history is the full-precision reconstructed value, not the rounded+clamped 16-bit output. Re-feeding the clamped output instead is audible only at high volume - the prediction drifts on loud sound-units and rails to the opposite extreme - which is exactly the symptom that bit-exact history feedback removes.

Stereo de-interleave. The LEFT channel is the even units (0,2,4,6) and the RIGHT channel is the odd units (1,3,5,7); output is L,R interleaved, pairing (0,1),(2,3),(4,5),(6,7). Each channel keeps its own (prev1, prev2) history.

Sound-group decode (8-bit)

The 8-bit mode (subheader coding_info bits 4..5 = 01) uses the same 128-byte group but packs 4 sound units of 28 full-byte samples instead of 8 nibble units:

  • Parameter bytes (0..16). The 4 unit params live at bytes 0..4, mirrored three more times at 4..8 / 8..12 / 12..16; the live copy is bytes 0..4. Same (filter << 4) | range encoding.
  • Sample bytes (16..128). 28 lines of 4 bytes; unit u reads byte u of each line (one full signed 8-bit sample per byte, 0x80 = -128).
  • Reconstruction. Identical filter math, but the sample sits in the top byte of the 16-bit word before the gain shift: shifted = (sign_extend(byte, 8) << 8) >> range.
  • Stereo de-interleave. LEFT = even units (0,2), RIGHT = odd units (1,3).

This yields 112 samples/group vs the 4-bit path's 224. Select it with legaia_xa::DecodeOptions { bits: BitsPerSample::Eight, .. } (the demux path maps each channel's reported width automatically; the CLI exposes --bits 8). The whole NA corpus is 4-bit, so 4-bit is the default and the 8-bit path is exercised by synthetic unit tests.

What's still open

  • 8-bit ADPCM verification. The NA corpus is entirely 4-bit, 37.8 kHz (demux-disc-all reports bits_per_sample = 4 for every channel of every *.XA file), so nothing on the NA disc exercises 8-bit - it is covered by synthetic unit tests, not verified bit-exact against a real 8-bit source. A JP/EU build that uses 8-bit would decode without code changes.
  • Per-cutscene file-no / ch-no map. demux-disc emits one WAV per channel keyed by (file_no, ch_no). The mapping from cutscene name to expected channel pair lives inside the cutscene-overlay's mode driver, which is not yet captured. Until that is reversed, the WAV-to-cutscene assignment is manual. The Art-shout side is better pinned: crates/art's arts_voice tables give each character's file and candidate-channel pool.

How we know

SubjectSource
Mode 2 / Form 2 sector layoutPSX BIOS docs + legaia-iso::raw
Subheader interpretationcrates/xa/src/demux.rs
4-bit ADPCM filter coefficientscrates/xa/src/lib.rs
Sound-group decode (param + nibble layout, predictor)bit-exact, sample-for-sample, against an external lossless reference decode of a real cutscene track; pinned by the disc-gated xa_pcm_matches_reference oracle in crates/xa/tests/pcm_reference.rs.
Form-1-truncation diagnosisdirect comparison: 90 % of 128-byte groups in the Form-1-truncated extracted/XA/*.XA bytes fail the bytes 8..16 == bytes 0..8 invariant.

See also