- 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>
341 lines
7.5 KiB
Plaintext
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");
|
|
}
|
|
}
|