mirror of
https://github.com/vgmstream/vgmstream.git
synced 2024-12-19 18:05:52 +01:00
896 lines
34 KiB
C
896 lines
34 KiB
C
#include "meta.h"
|
|
#include "../coding/coding.h"
|
|
#include "sqex_sead_streamfile.h"
|
|
|
|
|
|
typedef struct {
|
|
int big_endian;
|
|
|
|
int is_sab;
|
|
int is_mab;
|
|
|
|
int total_subsongs;
|
|
int target_subsong;
|
|
|
|
uint16_t mtrl_index;
|
|
uint16_t mtrl_number;
|
|
int loop_flag;
|
|
int channels;
|
|
int codec;
|
|
int sample_rate;
|
|
int loop_start;
|
|
int loop_end;
|
|
off_t mtrl_offset;
|
|
off_t extradata_offset;
|
|
size_t extradata_size;
|
|
size_t stream_size;
|
|
uint16_t extradata_id;
|
|
|
|
off_t filename_offset;
|
|
size_t filename_size;
|
|
off_t muscname_offset;
|
|
size_t muscname_size;
|
|
off_t sectname_offset;
|
|
size_t sectname_size;
|
|
off_t modename_offset;
|
|
size_t modename_size;
|
|
off_t instname_offset;
|
|
size_t instname_size;
|
|
off_t sndname_offset;
|
|
size_t sndname_size;
|
|
|
|
off_t sections_offset;
|
|
off_t snd_section_offset;
|
|
off_t seq_section_offset;
|
|
off_t trk_section_offset;
|
|
off_t musc_section_offset;
|
|
off_t inst_section_offset;
|
|
off_t mtrl_section_offset;
|
|
|
|
char readable_name[STREAM_NAME_SIZE];
|
|
|
|
} sead_header;
|
|
|
|
static int parse_sead(sead_header *sead, STREAMFILE *sf);
|
|
|
|
|
|
/* SABF/MABF - Square Enix's "sead" audio games [Dragon Quest Builders (PS3), Dissidia Opera Omnia (mobile), FF XV (PS4)] */
|
|
VGMSTREAM* init_vgmstream_sqex_sead(STREAMFILE* sf) {
|
|
VGMSTREAM* vgmstream = NULL;
|
|
sead_header sead = {0};
|
|
off_t start_offset;
|
|
int target_subsong = sf->stream_index;
|
|
uint32_t (*read_u32)(off_t,STREAMFILE*) = NULL;
|
|
uint16_t (*read_u16)(off_t,STREAMFILE*) = NULL;
|
|
|
|
|
|
/* checks */
|
|
/* .sab: sound/bgm
|
|
* .mab: music
|
|
* .sbin: Dissidia Opera Omnia .sab */
|
|
if (!check_extensions(sf,"sab,mab,sbin"))
|
|
goto fail;
|
|
|
|
if (read_u32be(0x00, sf) == 0x73616266) { /* "sabf" */
|
|
sead.is_sab = 1;
|
|
} else if (read_u32be(0x00, sf) == 0x6D616266) { /* "mabf" */
|
|
sead.is_mab = 1;
|
|
} else {
|
|
goto fail;
|
|
}
|
|
|
|
/* SEAD handles both sab/mab in the same lib, and other similar files (config, engine, etc).
|
|
* Has some chunks pointing to sections, and each section entry (usually starting with section
|
|
* version/reserved/size) is always padded to 0x10. Most values are unsigned. */
|
|
|
|
|
|
sead.big_endian = guess_endianness16bit(0x06, sf); /* no flag, use size */
|
|
if (sead.big_endian) {
|
|
read_u32 = read_u32be;
|
|
read_u16 = read_u16be;
|
|
} else {
|
|
read_u32 = read_u32le;
|
|
read_u16 = read_u16le;
|
|
}
|
|
|
|
sead.target_subsong = target_subsong;
|
|
|
|
if (!parse_sead(&sead, sf))
|
|
goto fail;
|
|
|
|
|
|
/* build the VGMSTREAM */
|
|
vgmstream = allocate_vgmstream(sead.channels, sead.loop_flag);
|
|
if (!vgmstream) goto fail;
|
|
|
|
vgmstream->meta_type = sead.is_sab ? meta_SQEX_SAB : meta_SQEX_MAB;
|
|
vgmstream->sample_rate = sead.sample_rate;
|
|
vgmstream->num_streams = sead.total_subsongs;
|
|
vgmstream->stream_size = sead.stream_size;
|
|
strcpy(vgmstream->stream_name, sead.readable_name);
|
|
|
|
switch(sead.codec) {
|
|
|
|
case 0x01: { /* PCM [Chrono Trigger (PC) sfx] */
|
|
start_offset = sead.extradata_offset + sead.extradata_size;
|
|
|
|
vgmstream->coding_type = coding_PCM16LE;
|
|
vgmstream->layout_type = layout_interleave;
|
|
vgmstream->interleave_block_size = 0x02;
|
|
|
|
/* no known extradata */
|
|
|
|
vgmstream->num_samples = pcm_bytes_to_samples(sead.stream_size, vgmstream->channels, 16);
|
|
vgmstream->loop_start_sample = sead.loop_start;
|
|
vgmstream->loop_end_sample = sead.loop_end;
|
|
break;
|
|
}
|
|
|
|
case 0x02: { /* MSADPCM [Dragon Quest Builders (Vita) sfx] */
|
|
start_offset = sead.extradata_offset + sead.extradata_size;
|
|
|
|
vgmstream->coding_type = coding_MSADPCM;
|
|
vgmstream->layout_type = layout_none;
|
|
vgmstream->frame_size = read_u16(sead.extradata_offset+0x04, sf);
|
|
|
|
/* extradata: */
|
|
/* 0x00: version */
|
|
/* 0x01: reserved */
|
|
/* 0x02: size */
|
|
/* 0x02: frame size */
|
|
/* 0x06: reserved */
|
|
/* 0x08: loop start offset */
|
|
/* 0x0c: loop end offset */
|
|
|
|
/* much like AKBs, loop values are slightly different, probably more accurate
|
|
* (if no loop, loop_end doubles as num_samples) */
|
|
vgmstream->num_samples = msadpcm_bytes_to_samples(sead.stream_size, vgmstream->frame_size, vgmstream->channels);
|
|
vgmstream->loop_start_sample = read_u32(sead.extradata_offset+0x08, sf);
|
|
vgmstream->loop_end_sample = read_u32(sead.extradata_offset+0x0c, sf);
|
|
break;
|
|
}
|
|
|
|
#ifdef VGM_USE_VORBIS
|
|
case 0x03: { /* VORBIS (Ogg subfile) [Final Fantasy XV Benchmark sfx (PC)] */
|
|
VGMSTREAM *ogg_vgmstream = NULL;
|
|
ogg_vorbis_meta_info_t ovmi = {0};
|
|
off_t subfile_offset = sead.extradata_offset + sead.extradata_size;
|
|
|
|
ovmi.meta_type = vgmstream->meta_type;
|
|
ovmi.total_subsongs = sead.total_subsongs;
|
|
ovmi.stream_size = sead.stream_size;
|
|
|
|
/* extradata: */
|
|
/* 0x00: version */
|
|
/* 0x01: reserved */
|
|
/* 0x02: size */
|
|
/* 0x04: loop start offset */
|
|
/* 0x08: loop end offset */
|
|
/* 0x0c: num samples */
|
|
/* 0x10: header size */
|
|
/* 0x14: seek table size */
|
|
/* 0x18: reserved x2 */
|
|
/* 0x20: seek table */
|
|
|
|
ogg_vgmstream = init_vgmstream_ogg_vorbis_callbacks(sf, NULL, subfile_offset, &ovmi);
|
|
if (ogg_vgmstream) {
|
|
ogg_vgmstream->num_streams = vgmstream->num_streams;
|
|
ogg_vgmstream->stream_size = vgmstream->stream_size;
|
|
strcpy(ogg_vgmstream->stream_name, vgmstream->stream_name);
|
|
|
|
close_vgmstream(vgmstream);
|
|
return ogg_vgmstream;
|
|
}
|
|
else {
|
|
goto fail;
|
|
}
|
|
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
#ifdef VGM_USE_ATRAC9
|
|
case 0x04: { /* ATRAC9 [Dragon Quest Builders (Vita), Final Fantaxy XV (PS4)] */
|
|
atrac9_config cfg = {0};
|
|
|
|
start_offset = sead.extradata_offset + sead.extradata_size;
|
|
|
|
cfg.channels = vgmstream->channels;
|
|
cfg.config_data = read_u32(sead.extradata_offset+0x0c, sf);
|
|
cfg.encoder_delay = read_u32(sead.extradata_offset+0x18, sf);
|
|
|
|
/* extradata: */
|
|
/* 0x00: version */
|
|
/* 0x01: reserved */
|
|
/* 0x02: size */
|
|
/* 0x04: block align */
|
|
/* 0x06: block samples */
|
|
/* 0x08: channel mask */
|
|
/* 0x1c: config */
|
|
/* 0x10: samples */
|
|
/* 0x14: "overlap delay" */
|
|
/* 0x18: "encoder delay" */
|
|
/* 0x1c: sample rate */
|
|
/* 0x24: loop start */
|
|
/* 0x28: loop end */
|
|
|
|
vgmstream->codec_data = init_atrac9(&cfg);
|
|
if (!vgmstream->codec_data) goto fail;
|
|
vgmstream->coding_type = coding_ATRAC9;
|
|
vgmstream->layout_type = layout_none;
|
|
|
|
vgmstream->channel_layout = read_u32(sead.extradata_offset+0x08,sf);
|
|
vgmstream->sample_rate = read_u32(sead.extradata_offset+0x1c,sf); /* SAB's sample rate can be different/wrong */
|
|
vgmstream->num_samples = read_u32(sead.extradata_offset+0x10,sf); /* loop values above are also weird and ignored */
|
|
vgmstream->loop_start_sample = read_u32(sead.extradata_offset+0x20, sf) - (sead.loop_flag ? cfg.encoder_delay : 0); //loop_start
|
|
vgmstream->loop_end_sample = read_u32(sead.extradata_offset+0x24, sf) - (sead.loop_flag ? cfg.encoder_delay : 0); //loop_end
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
#ifdef VGM_USE_MPEG
|
|
case 0x06: { /* MSMP3 (MSF subfile) [Dragon Quest Builders (PS3)] */
|
|
mpeg_codec_data *mpeg_data = NULL;
|
|
mpeg_custom_config cfg = {0};
|
|
|
|
start_offset = sead.extradata_offset + sead.extradata_size;
|
|
|
|
/* extradata: */
|
|
/* proper MSF header, but sample rate/loops are ignored in favor of SAB's */
|
|
|
|
mpeg_data = init_mpeg_custom(sf, start_offset, &vgmstream->coding_type, vgmstream->channels, MPEG_STANDARD, &cfg);
|
|
if (!mpeg_data) goto fail;
|
|
vgmstream->codec_data = mpeg_data;
|
|
vgmstream->layout_type = layout_none;
|
|
|
|
vgmstream->num_samples = mpeg_bytes_to_samples(sead.stream_size, mpeg_data);
|
|
vgmstream->loop_start_sample = sead.loop_start;
|
|
vgmstream->loop_end_sample = sead.loop_end;
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
case 0x07: { /* HCA (subfile) [Dissidia Opera Omnia (Mobile), Final Fantaxy XV (PS4)] */
|
|
VGMSTREAM *temp_vgmstream = NULL;
|
|
STREAMFILE *temp_sf = NULL;
|
|
off_t subfile_offset = sead.extradata_offset + 0x10;
|
|
size_t subfile_size = sead.stream_size + sead.extradata_size - 0x10;
|
|
|
|
size_t key_start = sead.extradata_id & 0xff;
|
|
size_t header_size = read_u16(sead.extradata_offset+0x02, sf);
|
|
int encryption = read_u8(sead.extradata_offset+0x0d, sf);
|
|
/* encryption type 0x01 found in Final Fantasy XII TZA (PS4/PC)
|
|
* (XOR subtable is fed to HCA's engine instead of generating from a key) */
|
|
|
|
/* extradata (mostly same as HCA's): */
|
|
/* 0x00: version */
|
|
/* 0x01: size */
|
|
/* 0x02: header size */
|
|
/* 0x04: frame size */
|
|
/* 0x06: loop start frame */
|
|
/* 0x08: loop end frame */
|
|
/* 0x0a: inserted samples (encoder delay) */
|
|
/* 0x0c: "use mixer" flag */
|
|
/* 0x0d: encryption flag */
|
|
/* 0x0e: reserved x2 */
|
|
/* 0x10+ HCA header */
|
|
|
|
temp_sf = setup_sqex_sead_streamfile(sf, subfile_offset, subfile_size, encryption, header_size, key_start);
|
|
if (!temp_sf) goto fail;
|
|
|
|
temp_vgmstream = init_vgmstream_hca(temp_sf);
|
|
if (temp_vgmstream) {
|
|
/* loops can be slightly different (~1000 samples) but probably HCA's are more accurate */
|
|
temp_vgmstream->num_streams = vgmstream->num_streams;
|
|
temp_vgmstream->stream_size = vgmstream->stream_size;
|
|
temp_vgmstream->meta_type = vgmstream->meta_type;
|
|
strcpy(temp_vgmstream->stream_name, vgmstream->stream_name);
|
|
|
|
close_streamfile(temp_sf);
|
|
close_vgmstream(vgmstream);
|
|
return temp_vgmstream;
|
|
}
|
|
else {
|
|
close_streamfile(temp_sf);
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
case 0x00: /* NONE / dummy entry */
|
|
case 0x05: /* XMA2 (extradata may be a XMA2 fmt extra chunk) */
|
|
case 0x08: /* SWITCHOPUS (no extradata?) */
|
|
default:
|
|
VGM_LOG("SQEX SEAD: unknown codec %x\n", sead.codec);
|
|
goto fail;
|
|
}
|
|
|
|
strcpy(vgmstream->stream_name, sead.readable_name);
|
|
|
|
/* open the file for reading */
|
|
if ( !vgmstream_open_stream(vgmstream, sf, start_offset) )
|
|
goto fail;
|
|
return vgmstream;
|
|
|
|
fail:
|
|
close_vgmstream(vgmstream);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static void build_readable_name(char * buf, size_t buf_size, sead_header *sead, STREAMFILE *sf) {
|
|
|
|
if (sead->is_sab) {
|
|
char descriptor[255], name[255];
|
|
|
|
if (sead->filename_size > 255 || sead->sndname_size > 255) goto fail;
|
|
|
|
read_string(descriptor,sead->filename_size+1,sead->filename_offset, sf);
|
|
read_string(name,sead->sndname_size+1,sead->sndname_offset, sf);
|
|
|
|
snprintf(buf,buf_size, "%s/%s", descriptor, name);
|
|
}
|
|
else {
|
|
char descriptor[255], name[255], mode[255];
|
|
|
|
if (sead->filename_size > 255 || sead->muscname_size > 255 || sead->sectname_size > 255 || sead->modename_size > 255) goto fail;
|
|
|
|
read_string(descriptor,sead->filename_size+1,sead->filename_offset, sf);
|
|
//read_string(filename,sead->muscname_size+1,sead->muscname_offset, sf); /* same as filename, not too interesting */
|
|
if (sead->sectname_offset)
|
|
read_string(name,sead->sectname_size+1,sead->sectname_offset, sf);
|
|
else if (sead->instname_offset)
|
|
read_string(name,sead->instname_size+1,sead->instname_offset, sf);
|
|
else
|
|
strcpy(name, "?");
|
|
if (sead->modename_offset > 0)
|
|
read_string(mode,sead->modename_size+1,sead->modename_offset, sf);
|
|
|
|
/* default mode in most files */
|
|
if (sead->modename_offset == 0 || strcmp(mode, "Mode") == 0 || strcmp(mode, "Mode0") == 0)
|
|
snprintf(buf,buf_size, "%s/%s", descriptor, name);
|
|
else
|
|
snprintf(buf,buf_size, "%s/%s/%s", descriptor, name, mode);
|
|
}
|
|
|
|
return;
|
|
fail:
|
|
VGM_LOG("SEAD: bad name found\n");
|
|
}
|
|
|
|
static void parse_sead_mab_name(sead_header *sead, STREAMFILE *sf) {
|
|
uint32_t (*read_u32)(off_t,STREAMFILE*) = sead->big_endian ? read_u32be : read_u32le;
|
|
uint16_t (*read_u16)(off_t,STREAMFILE*) = sead->big_endian ? read_u16be : read_u16le;
|
|
int i, j, k, entries;
|
|
off_t target_muscname_offset = 0, target_sectname_offset = 0;
|
|
size_t target_muscname_size = 0, target_sectname_size = 0;
|
|
|
|
|
|
/* find names referencing to our material stream, usually:
|
|
* - music > sections > layers (<> meters) > material index
|
|
* - instruments > instrument materials > material index */
|
|
|
|
|
|
/* parse musics */
|
|
entries = read_u16(sead->musc_section_offset + 0x04, sf);
|
|
for (i = 0; i < entries; i++) {
|
|
off_t musc_offset = sead->musc_section_offset + read_u32(sead->musc_section_offset + 0x10 + i*0x04, sf);
|
|
off_t muscname_offset, table_offset;
|
|
size_t musc_size, muscname_size;
|
|
int musc_version, sect_count, mode_count;
|
|
|
|
musc_version = read_u8 (musc_offset + 0x00, sf);
|
|
/* 0x01: "output"? */
|
|
musc_size = read_u16(musc_offset + 0x02, sf);
|
|
sect_count = read_u8 (musc_offset + 0x04, sf);
|
|
mode_count = read_u8 (musc_offset + 0x05, sf);
|
|
/* 0x06: category? */
|
|
/* 0x07: priority (default 0x80) */
|
|
/* 0x08: number */
|
|
/* 0x0a: flags */
|
|
/* 0x0b: distance attenuation curve? */
|
|
/* 0x0c: interior factor (f32) */
|
|
/* 0x10: name in <=v8 */
|
|
/* 0x20: audible range? (f32) */
|
|
/* 0x24: inner range? (f32) */
|
|
/* 0x28: volume (f32) */
|
|
/* 0x2c: send busses x4 / send volumes (f32) x4 */
|
|
/* 0x40: counts: aux sends/end methods/start methods/zero-ones */
|
|
/* 0x44: sample rate */
|
|
muscname_size = read_u8(musc_offset + 0x48, sf);
|
|
/* 0x49: port? */
|
|
/* 0x4a: reserved */
|
|
/* 0x4c: audio length (float) */
|
|
/* 0x50+: extra data in later versions */
|
|
|
|
if (musc_version <= 8) {
|
|
muscname_offset = musc_offset + 0x10;
|
|
muscname_size = 0x0f;
|
|
}
|
|
else {
|
|
muscname_offset = musc_offset + musc_size;
|
|
}
|
|
|
|
table_offset = align_size_to_block(muscname_offset + muscname_size + 0x01, 0x10);
|
|
|
|
/* parse sections (layered parts that possibly transition into others using "meter" info) */
|
|
for (j = 0; j < sect_count; j++) {
|
|
off_t sect_offset = musc_offset + read_u32(table_offset + j*0x04, sf);
|
|
off_t sectname_offset, subtable_offset;
|
|
size_t sect_size, sectname_size;
|
|
int sect_version, layr_count;
|
|
|
|
sect_version = read_u8 (sect_offset + 0x00, sf);
|
|
/* 0x01: number */
|
|
sect_size = read_u16(sect_offset + 0x02, sf);
|
|
|
|
if (sect_version <= 7) {
|
|
/* 0x04: meter count */
|
|
layr_count = read_u8(sect_offset + 0x05, sf);
|
|
/* 0x06: custom points count */
|
|
/* 0x08: entry points (sample) */
|
|
/* 0x0c: exit points (sample) */
|
|
/* 0x10: loop start */
|
|
/* 0x14: loop end */
|
|
/* 0x18+: meter transition timing info (offsets, points, curves, etc) */
|
|
sectname_offset = 0x30;
|
|
sectname_size = 0x0f;
|
|
|
|
subtable_offset = sect_offset + sect_size;
|
|
}
|
|
else {
|
|
sectname_size = read_u8(sect_offset + 0x04, sf);
|
|
layr_count = read_u8(sect_offset + 0x05, sf);
|
|
/* 0x06: custom points count */
|
|
/* 0x08: entry point (sample) */
|
|
/* 0x0c: exit point (sample) */
|
|
/* 0x10: loop start */
|
|
/* 0x14: loop end */
|
|
/* 0x18: meter count */
|
|
/* 0x1c+: meter transition timing info (offsets, points, curves, etc) */
|
|
sectname_offset = sect_offset + sect_size;
|
|
|
|
subtable_offset = align_size_to_block(sectname_offset + sectname_size + 0x01, 0x10);
|
|
}
|
|
|
|
|
|
if (j + 1== sead->target_subsong) {
|
|
target_muscname_offset = muscname_offset;
|
|
target_muscname_size = muscname_size;
|
|
target_sectname_offset = sectname_offset;
|
|
target_sectname_size = sectname_size;
|
|
}
|
|
|
|
/* parse layers */
|
|
for (k = 0; k < layr_count; k++) {
|
|
off_t layr_offset = sect_offset + read_u32(subtable_offset + k*0x04, sf);
|
|
int mtrl_index;
|
|
|
|
/* 0x00: version */
|
|
/* 0x01: flags */
|
|
/* 0x02: size */
|
|
mtrl_index = read_u16(layr_offset + 0x04, sf);
|
|
/* 0x06: loop count */
|
|
/* 0x08: offset */
|
|
/* 0x0c: end point (sample) */
|
|
|
|
if (mtrl_index == sead->mtrl_index) {
|
|
sead->muscname_offset = muscname_offset;
|
|
sead->muscname_size = muscname_size;
|
|
sead->sectname_offset = sectname_offset;
|
|
sead->sectname_size = sectname_size;
|
|
//break;
|
|
}
|
|
}
|
|
|
|
/* use last name for cloned materials (see below) */
|
|
//if (sead->sectname_offset > 0)
|
|
// break;
|
|
|
|
/* meters offset go after layer offsets, not useful */
|
|
}
|
|
|
|
/* in some files (ex. FF12 TZA) materials are cloned, but sections only point to one of the
|
|
* clones, so one material has multiple section names while others have none. For those cases
|
|
* we can try to match names by subsong number */
|
|
if (sead->sectname_offset == 0) {
|
|
sead->muscname_offset = target_muscname_offset;
|
|
sead->muscname_size = target_muscname_size;
|
|
sead->sectname_offset = target_sectname_offset;
|
|
sead->sectname_size = target_sectname_size;
|
|
VGM_LOG("MAB: voodoo name matching\n");
|
|
}
|
|
|
|
|
|
/* modes have names (almost always "Mode" and only 1 entry, rarely "Water" / "Restaurant" / etc)
|
|
* and seem referenced manually (in-game events) and alter sound parameters instead of being
|
|
* an actual playable thing (only found multiple in FFXV's bgm_gardina) */
|
|
|
|
/* hack to use mode as subsong name, which for the only known file looks correct */
|
|
if (mode_count == sead->total_subsongs)
|
|
i = sead->target_subsong - 1;
|
|
else
|
|
i = 0;
|
|
|
|
{ //for (i = 0; i < mode_count; i++) {
|
|
off_t mode_offset = musc_offset + read_u32(table_offset + sect_count*0x04 + i*0x04, sf);
|
|
off_t modename_offset;
|
|
size_t mode_size, modename_size;
|
|
int mode_version;
|
|
|
|
mode_version = read_u8 (mode_offset + 0x00, sf);
|
|
/* 0x01: flags */
|
|
mode_size = read_u16(mode_offset + 0x02, sf);
|
|
/* 0x04: number */
|
|
modename_size = read_u8 (mode_offset + 0x06, sf);
|
|
/* 0x07: reserved */
|
|
/* 0x08: transition param offset */
|
|
/* 0x0c: reserved */
|
|
/* 0x10: volume */
|
|
/* 0x14: pitch */
|
|
/* 0x18: lowpass */
|
|
/* 0x1c: speed */
|
|
/* 0x20: name <=v2 (otherwise null) */
|
|
|
|
if (mode_version <= 2) {
|
|
modename_offset = mode_offset + 0x20;
|
|
modename_size = 0x0f;
|
|
}
|
|
else {
|
|
modename_offset = mode_offset + mode_size;
|
|
}
|
|
|
|
sead->modename_offset = modename_offset;
|
|
sead->modename_size = modename_size;
|
|
}
|
|
}
|
|
|
|
|
|
/* parse instruments (very rare, ex. KH3 tut) */
|
|
entries = read_u16(sead->inst_section_offset + 0x04, sf);
|
|
for (i = 0; i < entries; i++) {
|
|
off_t inst_offset = sead->inst_section_offset + read_u32(sead->inst_section_offset + 0x10 + i*0x04, sf);
|
|
off_t instname_offset, mtrl_offset;
|
|
size_t instname_size;
|
|
int mtrl_count;
|
|
|
|
/* 0x00: version */
|
|
/* 0x01: "output"? */
|
|
/* 0x02: size */
|
|
/* 0x04: type (normal, random, switch, etc) */
|
|
mtrl_count = read_u8(inst_offset + 0x05, sf);
|
|
/* 0x06: category */
|
|
/* 0x07: priority */
|
|
/* 0x08: number */
|
|
/* 0x0a: flags */
|
|
/* 0x0b: distance attenuation curve */
|
|
/* 0x0c: interior factor (f32) */
|
|
/* 0x10: audible range (f32) */
|
|
/* 0x14: inner range (f32) */
|
|
/* 0x18: play length (f32) */
|
|
/* 0x1c: reserved */
|
|
/* 0x20: ? (f32) */
|
|
/* 0x30: name */
|
|
|
|
instname_offset = inst_offset + 0x30;
|
|
instname_size = 0x0F;
|
|
|
|
mtrl_offset = instname_offset + instname_size + 0x01;
|
|
|
|
/* parse instrument materials */
|
|
for (j = 0; j < mtrl_count; j++) {
|
|
size_t mtrl_size;
|
|
int mtrl_index;
|
|
|
|
/* 0x00: version */
|
|
/* 0x01: value (meaning depends on type) */
|
|
mtrl_size = read_u16(mtrl_offset + 0x02, sf);
|
|
mtrl_index = read_u16(mtrl_offset + 0x04, sf);
|
|
/* 0x06: number */
|
|
/* 0x08: volume */
|
|
/* 0x0c: sync point */
|
|
/* 0x10: sample rate */
|
|
/* 0x14-20: reserved */
|
|
|
|
if (mtrl_index == sead->mtrl_index) {
|
|
sead->instname_offset = instname_offset;
|
|
sead->instname_size = instname_size;
|
|
break;
|
|
}
|
|
|
|
mtrl_offset += mtrl_size;
|
|
}
|
|
|
|
if (sead->instname_offset > 0)
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void parse_sead_sab_name(sead_header *sead, STREAMFILE *sf) {
|
|
uint32_t (*read_u32)(off_t,STREAMFILE*) = sead->big_endian ? read_u32be : read_u32le;
|
|
uint16_t (*read_u16)(off_t,STREAMFILE*) = sead->big_endian ? read_u16be : read_u16le;
|
|
int i, j, entries;
|
|
|
|
/* find names referencing to our material stream, usually:
|
|
* - sound > sequence index
|
|
* - sequence > command > track index
|
|
* - track > material index
|
|
*
|
|
* most of the time sounds<>materials go 1:1 but there are exceptions
|
|
* (ex. DQB se_break_soil, FFXV aircraftzeroone, FFXV 03bt100031pc00)
|
|
*
|
|
* some configs (mainly zero-ones, that seem to be a kind of random sound) have a name too,
|
|
* but it's usually "Default" "(name)ZeroOne" and other uninteresting stuff */
|
|
|
|
/* parse sounds */
|
|
entries = read_u16(sead->snd_section_offset + 0x04, sf);
|
|
for (i = 0; i < entries; i++) {
|
|
off_t snd_offset = sead->snd_section_offset + read_u32(sead->snd_section_offset + 0x10 + i*0x04, sf);
|
|
size_t snd_size, sndname_size;
|
|
off_t seqi_start, seqi_offset, sndname_offset;
|
|
int snd_version, seqi_count;
|
|
|
|
snd_version = read_u8(snd_offset + 0x00, sf);
|
|
/* 0x01: work? */
|
|
snd_size = read_u16(snd_offset + 0x02, sf);
|
|
/* 0x04: type */
|
|
seqi_count = read_u8(snd_offset + 0x05, sf); /* may be 0 */
|
|
/* 0x06: category */
|
|
/* 0x07: priority (default 0x80) */
|
|
/* 0x08: number */
|
|
/* 0x0a: start+end macro */
|
|
/* 0x0c: volume */
|
|
/* 0x10: cycle config */
|
|
/* 0x1a: header size? */
|
|
seqi_start = read_u16(snd_offset + 0x1a, sf);
|
|
/* 0x1c: audible range */
|
|
/* 0x20: output/curve/port */
|
|
/* 0x23: reserved/name size */
|
|
/* 0x24: play length */
|
|
/* 0x28+: more config params */
|
|
|
|
if (snd_version <= 8) {
|
|
sndname_size = 0x0F;
|
|
sndname_offset = snd_offset + 0x50;
|
|
}
|
|
else {
|
|
sndname_size = read_u8(snd_offset + 0x23, sf);
|
|
sndname_offset = snd_offset + snd_size;
|
|
}
|
|
|
|
/* parse sequence info */
|
|
seqi_offset = snd_offset + seqi_start;
|
|
for (j = 0; j < seqi_count; j++) {
|
|
size_t seqi_size;
|
|
int seq_index;
|
|
|
|
/* 0x00: version */
|
|
/* 0x01: reserved */
|
|
seqi_size = read_u16(seqi_offset + 0x02, sf);
|
|
seq_index = read_u16(seqi_offset + 0x04, sf);
|
|
/* 0x06: sequence ID */
|
|
/* 0x08: reserved x2 */
|
|
|
|
seqi_offset += seqi_size;
|
|
|
|
/* parse sequence */
|
|
{
|
|
off_t seq_offset = sead->seq_section_offset + read_u32(sead->seq_section_offset + 0x10 + seq_index*0x04, sf);
|
|
off_t cmnd_start, cmnd_offset;
|
|
int seq_version;
|
|
|
|
seq_version = read_u8(seq_offset + 0x00, sf);
|
|
/* 0x01: reserved */
|
|
/* 0x02: size */
|
|
|
|
if (seq_version <= 2) {
|
|
/* 0x04: config depending on type */
|
|
/* 0x10: number */
|
|
/* 0x12: volume zero-one offsets */
|
|
/* 0x12: pitch zero-one offsets */
|
|
cmnd_start = read_u16(seq_offset + 0x16, sf);
|
|
/* 0x18: reserved x2 */
|
|
}
|
|
else {
|
|
/* 0x04: id */
|
|
cmnd_start = read_u16(seq_offset + 0x06, sf);
|
|
/* 0x08: zero-ones count */
|
|
/* 0x09: reserved */
|
|
/* 0x10: config depending on type */
|
|
}
|
|
|
|
|
|
/* parse sequence commands (breaks once an end command is found) */
|
|
cmnd_offset = seq_offset + cmnd_start;
|
|
while (cmnd_offset < sead->trk_section_offset) {
|
|
uint8_t cmnd_size, cmnd_type, cmnd_body;
|
|
|
|
/* 0x00: version (each command may have a different version) */
|
|
cmnd_size = read_u8(cmnd_offset + 0x01, sf); /* doesn't include body */
|
|
cmnd_type = read_u8(cmnd_offset + 0x02, sf);
|
|
cmnd_body = read_u8(cmnd_offset + 0x03, sf);
|
|
|
|
/* 0=none, 1=end, 2=key on, 3=interval, 4=keyoff, 5=wat, 6=loop start, 7=loop end, 8=trigger */
|
|
if (cmnd_type == 0x02) {
|
|
uint32_t trk_index;
|
|
|
|
trk_index = read_u32(cmnd_offset + cmnd_size + 0x00, sf);
|
|
/* 0x04: is loop */
|
|
/* 0x05: reserved */
|
|
/* 0x06: track id */
|
|
/* 0x08: play length (f32) */
|
|
|
|
/* parse track */
|
|
{
|
|
off_t trk_offset = sead->trk_section_offset + read_u32(sead->trk_section_offset + 0x10 + trk_index*0x04, sf);
|
|
uint8_t trk_type;
|
|
uint16_t mtrl_index;
|
|
|
|
/* 0x00: version */
|
|
trk_type = read_u8(trk_offset + 0x01, sf);
|
|
/* 0x02: size */
|
|
/* 0x04: config depending on type */
|
|
/* 0x08: id */
|
|
/* 0x0a: child id */
|
|
/* 0x0c: reserved */
|
|
|
|
/* 0=none, 1=material, 2=sound */
|
|
if (trk_type == 0x01) {
|
|
mtrl_index = read_u16(trk_offset + 0x04, sf);
|
|
/* 0x02: bank */
|
|
|
|
/* assumes same bank, not sure if bank info is even inside .sab */
|
|
if (mtrl_index == sead->mtrl_index) {
|
|
sead->sndname_offset = sndname_offset;
|
|
sead->sndname_size = sndname_size;
|
|
}
|
|
}
|
|
else if (trk_type == 0x02) {
|
|
/* 0x00: index */
|
|
/* 0x02: reserved */
|
|
/* parse sound again? */
|
|
}
|
|
}
|
|
}
|
|
|
|
cmnd_offset += cmnd_size + cmnd_body;
|
|
|
|
/* commands normally end when a type 0=none is found */
|
|
if (cmnd_type <= 0x00 || cmnd_type > 0x08)
|
|
break;
|
|
if (sead->sndname_offset > 0)
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static int parse_sead(sead_header *sead, STREAMFILE *sf) {
|
|
uint32_t (*read_u32)(off_t,STREAMFILE*) = sead->big_endian ? read_u32be : read_u32le;
|
|
uint16_t (*read_u16)(off_t,STREAMFILE*) = sead->big_endian ? read_u16be : read_u16le;
|
|
|
|
/** base header **/
|
|
/* 0x00: id */
|
|
/* 0x04: version (usually 0x02, rarely 0x01 ex FF XV early songs) */
|
|
/* 0x05: flags */
|
|
/* 0x06: size */
|
|
/* 0x08: chunk count (in practice mab=3, sab=4) */
|
|
sead->filename_size = read_u8(0x09, sf);
|
|
/* 0x0a: number (shared between multiple sab/mab though) */
|
|
if (read_u32(0x0c, sf) != get_streamfile_size(sf))
|
|
goto fail;
|
|
|
|
/* not set/reserved when version == 1 (name is part of size) */
|
|
if (sead->filename_size == 0)
|
|
sead->filename_size = 0x0f;
|
|
sead->filename_offset = 0x10; /* file description ("BGM", "Music2", "SE", etc, long names are ok) */
|
|
sead->sections_offset = sead->filename_offset + (sead->filename_size + 0x01); /* string null matters for padding */
|
|
sead->sections_offset = align_size_to_block(sead->sections_offset, 0x10);
|
|
|
|
/* roughly, mab play audio by calling musics/instruments, and sab with sounds/sequences/track. Both
|
|
* point to a "material" or stream wave that may be reused. We only want material streams as subsongs,
|
|
* but also try to find names from musics/sounds/etc. */
|
|
|
|
/** chunk table elements (offsets to sections) **/
|
|
/* 0x00: id */
|
|
/* 0x04: version */
|
|
/* 0x05: reserved */
|
|
/* 0x06: size */
|
|
/* 0x08: offset */
|
|
/* 0x0c: reserved */
|
|
if (sead->is_sab) {
|
|
if (read_u32be(sead->sections_offset + 0x00, sf) != 0x736E6420) goto fail; /* "snd " (sounds) */
|
|
if (read_u32be(sead->sections_offset + 0x10, sf) != 0x73657120) goto fail; /* "seq " (sequences) */
|
|
if (read_u32be(sead->sections_offset + 0x20, sf) != 0x74726B20) goto fail; /* "trk " (tracks) */
|
|
if (read_u32be(sead->sections_offset + 0x30, sf) != 0x6D74726C) goto fail; /* "mtrl" (material headers/streams) */
|
|
sead->snd_section_offset = read_u32(sead->sections_offset + 0x08, sf);
|
|
sead->seq_section_offset = read_u32(sead->sections_offset + 0x18, sf);
|
|
sead->trk_section_offset = read_u32(sead->sections_offset + 0x28, sf);
|
|
sead->mtrl_section_offset = read_u32(sead->sections_offset + 0x38, sf);
|
|
}
|
|
else if (sead->is_mab) {
|
|
if (read_u32be(sead->sections_offset + 0x00, sf) != 0x6D757363) goto fail; /* "musc" (musics) */
|
|
if (read_u32be(sead->sections_offset + 0x10, sf) != 0x696E7374) goto fail; /* "inst" (instruments) */
|
|
if (read_u32be(sead->sections_offset + 0x20, sf) != 0x6D74726C) goto fail; /* "mtrl" (material headers/streams) */
|
|
sead->musc_section_offset = read_u32(sead->sections_offset + 0x08, sf);
|
|
sead->inst_section_offset = read_u32(sead->sections_offset + 0x18, sf);
|
|
sead->mtrl_section_offset = read_u32(sead->sections_offset + 0x28, sf);
|
|
}
|
|
else {
|
|
goto fail;
|
|
}
|
|
|
|
|
|
/* section "chunk" format at offset: */
|
|
/* 0x00: version */
|
|
/* 0x01: reserved */
|
|
/* 0x02: size */
|
|
/* 0x04: entries */
|
|
/* 0x06: reserved */
|
|
/* 0x10+ 0x04*entry: offset to entry from table start */
|
|
|
|
|
|
/* find target material offset */
|
|
{
|
|
int i, entries;
|
|
|
|
entries = read_u16(sead->mtrl_section_offset + 0x04, sf);
|
|
|
|
if (sead->target_subsong == 0) sead->target_subsong = 1;
|
|
sead->total_subsongs = 0;
|
|
sead->mtrl_offset = 0;
|
|
|
|
/* manually find subsongs as entries can be dummy (ex. sfx banks in Dissidia Opera Omnia) */
|
|
for (i = 0; i < entries; i++) {
|
|
off_t entry_offset = sead->mtrl_section_offset + read_u32(sead->mtrl_section_offset + 0x10 + i*0x04, sf);
|
|
|
|
if (read_u8(entry_offset + 0x05, sf) == 0) {
|
|
continue; /* codec 0 when dummy (see stream header) */
|
|
}
|
|
|
|
sead->total_subsongs++;
|
|
if (!sead->mtrl_offset && sead->total_subsongs == sead->target_subsong) {
|
|
sead->mtrl_offset = entry_offset;
|
|
sead->mtrl_index = i;
|
|
}
|
|
}
|
|
|
|
/* SAB can contain 0 entries too */
|
|
if (sead->mtrl_offset == 0)
|
|
goto fail;
|
|
}
|
|
|
|
/** stream header **/
|
|
/* 0x00: version */
|
|
/* 0x01: reserved */
|
|
/* 0x02: size */
|
|
sead->channels = read_u8 (sead->mtrl_offset + 0x04, sf);
|
|
sead->codec = read_u8 (sead->mtrl_offset + 0x05, sf); /* format */
|
|
sead->mtrl_number = read_u16(sead->mtrl_offset + 0x06, sf); /* 0..N */
|
|
sead->sample_rate = read_u32(sead->mtrl_offset + 0x08, sf);
|
|
sead->loop_start = read_u32(sead->mtrl_offset + 0x0c, sf); /* in samples but usually ignored */
|
|
sead->loop_end = read_u32(sead->mtrl_offset + 0x10, sf);
|
|
sead->extradata_size= read_u32(sead->mtrl_offset + 0x14, sf); /* including subfile header, can be 0 */
|
|
sead->stream_size = read_u32(sead->mtrl_offset + 0x18, sf); /* not including subfile header */
|
|
sead->extradata_id = read_u16(sead->mtrl_offset + 0x1c, sf);
|
|
/* 0x1e: reserved */
|
|
|
|
sead->loop_flag = (sead->loop_end > 0);
|
|
sead->extradata_offset = sead->mtrl_offset + 0x20;
|
|
|
|
if (sead->is_sab) {
|
|
parse_sead_sab_name(sead, sf);
|
|
}
|
|
else if (sead->is_mab) {
|
|
parse_sead_mab_name(sead, sf);
|
|
}
|
|
|
|
build_readable_name(sead->readable_name, sizeof(sead->readable_name), sead, sf);
|
|
|
|
return 1;
|
|
fail:
|
|
return 0;
|
|
}
|