XPlor/definitions/fmod/fsb.xscript
njohnson f0030ea6f2 Add new format definitions for FMOD, THQ, Volition, and Wii
- FMOD: FSB audio bank format support
- THQ: PAK archives, G4RC textures, RAD video, STR strings, GML scripts
- Volition: VPP archives, PEG textures, ASM container format
- Wii: BNR banner format with metadata extraction

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 12:10:58 -05:00

341 lines
7.5 KiB
Plaintext

// FMOD Sample Bank (FSB) - Audio Container Format
// Firelight Technologies Pty, Ltd.
// Supports FSB3, FSB4, FSB5
//
// Reference: https://github.com/gdawg/fsbext
// Root FSB type - auto-detects version
type fmod_fsb [root, display="FMOD Sample Bank"] byteorder LE
{
criteria {
require ascii(bytesat(0, 4)) == "FSB3" ||
ascii(bytesat(0, 4)) == "FSB4" ||
ascii(bytesat(0, 4)) == "FSB5" ||
_ext == "fsb";
}
magic = ascii(read(4));
ui("magic", "Magic");
if (magic == "FSB3") {
i32 num_samples;
ui("num_samples", "Sample Count");
i32 sample_header_size;
ui("sample_header_size", "Sample Headers Size");
i32 data_size;
ui("data_size", "Audio Data Size");
u32 version;
ui("version", "Version");
u32 mode;
ui("mode", "Mode Flags");
_header_end = 24;
_audio_data_start = _header_end + sample_header_size;
_current_audio_offset = _audio_data_start;
if (num_samples > 0 && num_samples < 10000) {
repeat(num_samples) {
_sample = parse_here("fsb3_sample_header");
_compressed_size = get(_sample, "length_compressed");
_sample_name = get(_sample, "name");
_sample_mode = get(_sample, "mode");
// Export and preview audio file
if (_compressed_size > 0 && _compressed_size < 100000000) {
_audio_bytes = bytesat(_current_audio_offset, _compressed_size);
_export_name = _sample_name + ".raw";
if ((_sample_mode & 0x200) != 0) {
_export_name = _sample_name + ".mp3";
}
write_file(_export_name, _audio_bytes);
set_preview(_export_name, _audio_bytes);
set_viewer("audio");
}
_current_audio_offset = _current_audio_offset + _compressed_size;
content = push("content", _sample);
}
}
}
if (magic == "FSB4") {
content = parse_here("fsb4_content");
}
if (magic == "FSB5") {
content = parse_here("fsb5_content");
}
}
// FSB3 Sample Header
type fsb3_sample_header [display="Sample"] byteorder LE
{
u16 header_size;
ui("header_size", "Header Size");
_name_bytes = read(30);
name = clean(ascii(_name_bytes));
ui("name", "Name");
set_name(name);
u32 length_samples;
ui("length_samples", "Length (samples)");
u32 length_compressed;
ui("length_compressed", "Compressed Size");
u32 loop_start;
ui("loop_start", "Loop Start");
u32 loop_end;
ui("loop_end", "Loop End");
u32 mode;
ui("mode", "Mode Flags");
i32 frequency;
ui("frequency", "Frequency (Hz)");
u16 volume;
ui("volume", "Default Volume");
i16 pan;
ui("pan", "Default Pan");
u16 priority;
ui("priority", "Priority");
u16 channels;
ui("channels", "Channels");
f32 min_distance;
ui("min_distance", "Min Distance");
f32 max_distance;
ui("max_distance", "Max Distance");
i32 var_freq;
ui("var_freq", "Frequency Variance");
u16 var_vol;
ui("var_vol", "Volume Variance");
i16 var_pan;
ui("var_pan", "Pan Variance");
_bytes_read = 80;
_extra_bytes = header_size - _bytes_read;
if (_extra_bytes > 0 && _extra_bytes < 10000) {
skip(_extra_bytes);
}
// Decode format
format = "Unknown";
ui("format", "Format");
if ((mode & 0x200) != 0) {
format = "MPEG";
ui("format", "Format");
}
if ((mode & 0x400000) != 0) {
format = "IMA ADPCM";
ui("format", "Format");
}
if ((mode & 0x1000000) != 0) {
format = "XMA";
ui("format", "Format");
}
if ((mode & 0x2000000) != 0) {
format = "GC ADPCM";
ui("format", "Format");
}
if ((mode & 0x800000) != 0) {
format = "VAG";
ui("format", "Format");
}
if ((mode & 0x10) != 0 && (mode & 0x200) == 0 &&
(mode & 0x400000) == 0 && (mode & 0x1000000) == 0) {
format = "PCM16";
ui("format", "Format");
}
if ((mode & 0x8) != 0) {
format = "PCM8";
ui("format", "Format");
}
}
// FSB4 Content
type fsb4_content [display="FSB4 Content"] byteorder LE
{
i32 num_samples;
ui("num_samples", "Sample Count");
i32 sample_header_size;
ui("sample_header_size", "Sample Headers Size");
i32 data_size;
ui("data_size", "Audio Data Size");
u32 version;
ui("version", "Version");
u32 mode;
ui("mode", "Mode Flags");
skip(8);
hash = read(16);
ui("hash", "Hash");
_header_end = 48;
_audio_data_start = _header_end + sample_header_size;
if (num_samples > 0 && num_samples < 10000) {
_current_audio_offset = _audio_data_start;
repeat(num_samples) {
_sample = parse_here("fsb3_sample_header");
_compressed_size = get(_sample, "length_compressed");
_sample_name = get(_sample, "name");
_sample_mode = get(_sample, "mode");
// Export and preview audio file
if (_compressed_size > 0 && _compressed_size < 100000000) {
_audio_bytes = bytesat(_current_audio_offset, _compressed_size);
_export_name = _sample_name + ".raw";
if ((_sample_mode & 0x200) != 0) {
_export_name = _sample_name + ".mp3";
}
write_file(_export_name, _audio_bytes);
set_preview(_export_name, _audio_bytes);
set_viewer("audio");
}
_current_audio_offset = _current_audio_offset + _compressed_size;
content = push("content", _sample);
}
}
}
// FSB5 Content
type fsb5_content [display="FSB5 Content"] byteorder LE
{
i32 version;
ui("version", "Version");
i32 num_samples;
ui("num_samples", "Sample Count");
i32 sample_header_size;
ui("sample_header_size", "Sample Headers Size");
i32 name_table_size;
ui("name_table_size", "Name Table Size");
i32 data_size;
ui("data_size", "Audio Data Size");
u32 mode;
ui("mode", "Mode/Codec");
skip(8);
hash = read(16);
ui("hash", "Hash");
skip(8);
_header_end = 60;
_data_start = _header_end + sample_header_size + name_table_size;
info = "FSB5 uses compact variable-length headers";
ui("info", "Note");
}
// Embedded parser for PAK files
type fmod_fsb_embedded [display="FMOD Sample Bank"] byteorder LE
{
magic = ascii(read(4));
ui("magic", "Magic");
if (magic == "FSB3") {
i32 num_samples;
ui("num_samples", "Sample Count");
i32 sample_header_size;
ui("sample_header_size", "Sample Headers Size");
i32 data_size;
ui("data_size", "Audio Data Size");
u32 version;
ui("version", "Version");
u32 mode;
ui("mode", "Mode Flags");
_header_end = 24;
_audio_data_start = _header_end + sample_header_size;
_current_audio_offset = _audio_data_start;
if (num_samples > 0 && num_samples < 10000) {
repeat(num_samples) {
_sample = parse_here("fsb3_sample_header");
_compressed_size = get(_sample, "length_compressed");
_sample_name = get(_sample, "name");
_sample_mode = get(_sample, "mode");
// Export and preview audio file
if (_compressed_size > 0 && _compressed_size < 100000000) {
_audio_bytes = bytesat(_current_audio_offset, _compressed_size);
_export_name = _sample_name + ".raw";
if ((_sample_mode & 0x200) != 0) {
_export_name = _sample_name + ".mp3";
}
write_file(_export_name, _audio_bytes);
set_preview(_export_name, _audio_bytes);
set_viewer("audio");
}
_current_audio_offset = _current_audio_offset + _compressed_size;
content = push("content", _sample);
}
}
}
if (magic == "FSB4") {
content = parse_here("fsb4_content");
}
if (magic == "FSB5") {
content = parse_here("fsb5_content");
}
}