- Volition VPP: Unified BE/LE types using inheritance pattern - THQA PAK: Child types now inherit byte order from parent - Various XScript definition updates and fixes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
234 lines
6.6 KiB
Plaintext
234 lines
6.6 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 ui("FMOD Sample Bank", root) 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");
|
|
|
|
if (magic == "FSB3") {
|
|
i32 num_samples ui("Sample Count");
|
|
i32 sample_header_size ui("Sample Headers Size");
|
|
i32 data_size ui("Audio Data Size");
|
|
u32 version ui("Version");
|
|
u32 mode ui("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 ui("Sample") byteorder LE
|
|
{
|
|
u16 header_size ui("Header Size");
|
|
|
|
_name_bytes = read(30);
|
|
name = clean(ascii(_name_bytes)) ui("Name");
|
|
set_name(name);
|
|
|
|
u32 length_samples ui("Length (samples)");
|
|
u32 length_compressed ui("Compressed Size");
|
|
u32 loop_start ui("Loop Start");
|
|
u32 loop_end ui("Loop End");
|
|
u32 mode ui("Mode Flags");
|
|
i32 frequency ui("Frequency (Hz)");
|
|
u16 volume ui("Default Volume");
|
|
i16 pan ui("Default Pan");
|
|
u16 priority ui("Priority");
|
|
u16 channels ui("Channels");
|
|
|
|
f32 min_distance ui("Min Distance");
|
|
f32 max_distance ui("Max Distance");
|
|
|
|
i32 var_freq ui("Frequency Variance");
|
|
u16 var_vol ui("Volume Variance");
|
|
i16 var_pan ui("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");
|
|
if ((mode & 0x200) != 0) {
|
|
format = "MPEG" ui("Format");
|
|
}
|
|
if ((mode & 0x400000) != 0) {
|
|
format = "IMA ADPCM" ui("Format");
|
|
}
|
|
if ((mode & 0x1000000) != 0) {
|
|
format = "XMA" ui("Format");
|
|
}
|
|
if ((mode & 0x2000000) != 0) {
|
|
format = "GC ADPCM" ui("Format");
|
|
}
|
|
if ((mode & 0x800000) != 0) {
|
|
format = "VAG" ui("Format");
|
|
}
|
|
if ((mode & 0x10) != 0 && (mode & 0x200) == 0 &&
|
|
(mode & 0x400000) == 0 && (mode & 0x1000000) == 0) {
|
|
format = "PCM16" ui("Format");
|
|
}
|
|
if ((mode & 0x8) != 0) {
|
|
format = "PCM8" ui("Format");
|
|
}
|
|
}
|
|
|
|
// FSB4 Content
|
|
type fsb4_content ui("FSB4 Content") byteorder LE
|
|
{
|
|
i32 num_samples ui("Sample Count");
|
|
i32 sample_header_size ui("Sample Headers Size");
|
|
i32 data_size ui("Audio Data Size");
|
|
u32 version ui("Version");
|
|
u32 mode ui("Mode Flags");
|
|
|
|
skip(8);
|
|
hash = read(16) ui("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 ui("FSB5 Content") byteorder LE
|
|
{
|
|
i32 version ui("Version");
|
|
i32 num_samples ui("Sample Count");
|
|
i32 sample_header_size ui("Sample Headers Size");
|
|
i32 name_table_size ui("Name Table Size");
|
|
i32 data_size ui("Audio Data Size");
|
|
u32 mode ui("Mode/Codec");
|
|
|
|
skip(8);
|
|
hash = read(16) ui("Hash");
|
|
skip(8);
|
|
|
|
_header_end = 60;
|
|
_data_start = _header_end + sample_header_size + name_table_size;
|
|
|
|
info = "FSB5 uses compact variable-length headers" ui("Note");
|
|
}
|
|
|
|
// Embedded parser for PAK files
|
|
type fmod_fsb_embedded ui("FMOD Sample Bank") byteorder LE
|
|
{
|
|
magic = ascii(read(4)) ui("Magic");
|
|
|
|
if (magic == "FSB3") {
|
|
i32 num_samples ui("Sample Count");
|
|
i32 sample_header_size ui("Sample Headers Size");
|
|
i32 data_size ui("Audio Data Size");
|
|
u32 version ui("Version");
|
|
u32 mode ui("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");
|
|
}
|
|
}
|