Overview

Implementation: crates/vab (header parser + extractor + ADPCM decoder, sharing F0/F1 filter constants with crates/xa). The format itself is documented externally; the Legaia-specific notes - the parts that trip up a stock VAB extractor - are:

  • The bank is usually wrapped. The dominant on-disc carrier is the scene-VAB-prefixed streaming shape - the VAB body is preceded by a 4-byte chunk0 header, so the VABp magic is not at offset 0. crates/vab::parse_header(buf, offset) accepts a starting offset so callers can skip the wrapper.
  • There are a lot of them. A bulk scan finds 1191 VABp headers across 239 PROT entries (numbered entries in PROT.DAT, the disc's single big archive). Top: 0889_sound_data2 (207), 0891_level_up (206), 0890_sound_data2 (203) - multi-bank archives. The vab_01 cluster (1072..1194) is the standard distributed-bank layout: 120 entries with 1–3 banks each.
  • Don't trust the filename. Extraction filename labels carry the +2 CDNAME numbering shift (the historical “vab_01 without VAB headers” was that shift - the retail vab_01 block, extraction 1070..1192, is wall-to-wall VAB); trust the VABp magic + loader constant over a filename label.
  • Program numbers are not tone-page indices. The 128-slot ProgAtr table is indexed by program number (what a SEQ ProgramChange or SFX descriptor names), but the tone pages that follow are packed - one page per used program, in slot order - so a program resolves to its page by rank among the used slots. Retail builds that rank map at VAB open (FUN_80068D94 writes it into the ProgAtr +8 reserved words; the program-change FUN_80068B98 reads it back). It matters: 66 of the 217 wrapped PROT-entry banks - 43 of 77 music banks - use sparse program sets, and indexing pages with the raw program number mis-tones or silently drops most of their instruments. The engine expands pages into slot space at upload (VabBank::upload).
  • The VAG size table is 1-indexed. The trailing VAG size table (256 × u16; a VAG is one sample body) has vag_table[1..=vs] holding each sample's size in 8-byte units, so vag_table[0] is a reserved leading spacer. It is universally 0 across the retail corpus (986 / 986 VABs, asserted by the disc-gated corpus_vag_spacer test) - it is not a master pitch / sample-rate shift, so no pitch offset is derived from it (VabReport::vag_table_spacer surfaces the raw byte only).

API

use legaia_vab::parse_header;
let header = parse_header(buf, offset)?;
println!("VAB v{} ps={} ts={}", header.version, header.ps, header.ts);

For bulk extraction of every VAB and per-program WAV files, see the vab CLI documented in tooling/extraction.md.

See also