From 71bee18a0691f12046b5359c507e58d7e6fd84ec Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 23 May 2020 23:12:45 +0200 Subject: [PATCH] Add .ktsl2asbin KTSR [Atelier Ryza (PC/Sw), Attack on Titan (PC/Vita)] --- src/formats.c | 4 +- src/libvgmstream.vcproj | 4 + src/libvgmstream.vcxproj | 1 + src/libvgmstream.vcxproj.filters | 3 + src/meta/ktsr.c | 567 +++++++++++++++++++++++++++++++ src/meta/meta.h | 2 + src/vgmstream.c | 1 + src/vgmstream.h | 1 + 8 files changed, 581 insertions(+), 2 deletions(-) create mode 100644 src/meta/ktsr.c diff --git a/src/formats.c b/src/formats.c index 559666d7..06c4f54c 100644 --- a/src/formats.c +++ b/src/formats.c @@ -1142,7 +1142,7 @@ static const meta_info meta_info_list[] = { {meta_CSTM, "Nintendo CSTM Header"}, {meta_FSTM, "Nintendo FSTM Header"}, {meta_KT_WIIBGM, "Koei Tecmo WiiBGM Header"}, - {meta_KTSS, "Koei Tecmo Nintendo Stream KTSS Header"}, + {meta_KTSS, "Koei Tecmo KTSS header"}, {meta_IDSP_NAMCO, "Namco IDSP header"}, {meta_WIIU_BTSND, "Nintendo Wii U Menu Boot Sound"}, {meta_MCA, "Capcom MCA header"}, @@ -1293,7 +1293,7 @@ static const meta_info meta_info_list[] = { {meta_WWISE_FX, "Audiokinetic Wwise FX header"}, {meta_DIVA, "DIVA header"}, {meta_IMUSE, "LucasArts iMUSE header"}, - + {meta_KTSR, "Koei Tecmo KTSR header"}, }; void get_vgmstream_coding_description(VGMSTREAM *vgmstream, char *out, size_t out_size) { diff --git a/src/libvgmstream.vcproj b/src/libvgmstream.vcproj index f80e4743..7a945012 100644 --- a/src/libvgmstream.vcproj +++ b/src/libvgmstream.vcproj @@ -820,6 +820,10 @@ RelativePath=".\meta\kraw.c" > + + diff --git a/src/libvgmstream.vcxproj b/src/libvgmstream.vcxproj index 9d5923c0..07dfe591 100644 --- a/src/libvgmstream.vcxproj +++ b/src/libvgmstream.vcxproj @@ -190,6 +190,7 @@ + diff --git a/src/libvgmstream.vcxproj.filters b/src/libvgmstream.vcxproj.filters index 06d4dcfd..5e130369 100644 --- a/src/libvgmstream.vcxproj.filters +++ b/src/libvgmstream.vcxproj.filters @@ -1732,6 +1732,9 @@ meta\Source Files + + meta\Source Files + meta\Source Files diff --git a/src/meta/ktsr.c b/src/meta/ktsr.c new file mode 100644 index 00000000..59e93ee7 --- /dev/null +++ b/src/meta/ktsr.c @@ -0,0 +1,567 @@ +#include "meta.h" +#include "../coding/coding.h" +#include "../layout/layout.h" + +typedef enum { NONE, MSADPCM, DSP, GCADPCM, ATRAC9, KVS, /*KNS*/ } ktsr_codec; + +#define MAX_CHANNELS 8 + +typedef struct { + int total_subsongs; + int target_subsong; + ktsr_codec codec; + + int platform; + int format; + + int channels; + int sample_rate; + int32_t num_samples; + int32_t loop_start; + int loop_flag; + off_t extra_offset; + uint32_t channel_layout; + + int is_external; + off_t stream_offsets[MAX_CHANNELS]; + size_t stream_sizes[MAX_CHANNELS]; + + off_t sound_name_offset; + off_t config_name_offset; + char name[255+1]; +} ktsr_header; + +static int parse_ktsr(ktsr_header* ktsr, STREAMFILE* sf); +static layered_layout_data* build_layered_atrac9(ktsr_header* ktsr, STREAMFILE *sf, uint32_t config_data); + + +/* KTSR - Koei Tecmo sound resource countainer */ +VGMSTREAM* init_vgmstream_ktsr(STREAMFILE* sf) { + VGMSTREAM* vgmstream = NULL; + STREAMFILE *sf_b = NULL; + ktsr_header ktsr = {0}; + int target_subsong = sf->stream_index; + int separate_offsets = 0; + + + /* checks */ + /* .ktsl2asbin: common [Atelier Ryza (PC), Attack on Titan: Wings of Freedom (Vita)] + * .ktsr: header ID */ + if (!check_extensions(sf, "ktsl2asbin")) + goto fail; + + /* KTSR can be a memory file (ktsl2asbin), streams (ktsl2stbin) and global config (ktsl2gcbin) + * This accepts ktsl2asbin with internal data, or opening external streams as subsongs. + * Some info from KTSR.bt */ + + if (read_u32be(0x00, sf) != 0x4B545352) /* "KTSR" */ + goto fail; + if (read_u32be(0x04, sf) != 0x777B481A) /* hash(?) id: 0x777B481A=as, 0x0294DDFC=st, 0xC638E69E=gc */ + goto fail; + + if (target_subsong == 0) target_subsong = 1; + ktsr.target_subsong = target_subsong; + + if (!parse_ktsr(&ktsr, sf)) + goto fail; + + /* open companion body */ + if (ktsr.is_external) { + sf_b = open_streamfile_by_ext(sf, "ktsl2stbin"); + if (!sf_b) { + VGM_LOG("KTSR: companion file not found\n"); + goto fail; + } + } + else { + sf_b = sf; + } + + + /* build the VGMSTREAM */ + vgmstream = allocate_vgmstream(ktsr.channels, ktsr.loop_flag); + if (!vgmstream) goto fail; + + vgmstream->meta_type = meta_KTSR; + vgmstream->sample_rate = ktsr.sample_rate; + vgmstream->num_samples = ktsr.num_samples; + vgmstream->loop_start_sample = ktsr.loop_start; + vgmstream->loop_end_sample = ktsr.num_samples; + vgmstream->stream_size = ktsr.stream_sizes[0]; + vgmstream->num_streams = ktsr.total_subsongs; + vgmstream->channel_layout = ktsr.channel_layout; + strcpy(vgmstream->stream_name, ktsr.name); + + switch(ktsr.codec) { + + case MSADPCM: + vgmstream->coding_type = coding_MSADPCM_int; + vgmstream->layout_type = layout_none; + separate_offsets = 1; + + /* 0x00: samples per frame */ + vgmstream->frame_size = read_u16le(ktsr.extra_offset + 0x02, sf_b); + break; + + case DSP: + vgmstream->coding_type = coding_NGC_DSP; + vgmstream->layout_type = layout_none; + separate_offsets = 1; + + dsp_read_coefs_le(vgmstream, sf, ktsr.extra_offset + 0x1c, 0x60); + dsp_read_hist_le (vgmstream, sf, ktsr.extra_offset + 0x40, 0x60); + break; + +#ifdef VGM_USE_ATRAC9 + case ATRAC9: { + /* 0x00: samples per frame */ + /* 0x02: frame size */ + uint32_t config_data = read_u32be(ktsr.extra_offset + 0x04, sf); + if ((config_data & 0xFF) == 0xFE) /* later versions(?) in LE */ + config_data = read_u32le(ktsr.extra_offset + 0x04, sf); + + vgmstream->layout_data = build_layered_atrac9(&ktsr, sf_b, config_data); + if (!vgmstream->layout_data) goto fail; + vgmstream->layout_type = layout_layered; + vgmstream->coding_type = coding_ATRAC9; + break; + +#if 0 + atrac9_config cfg = {0}; + if (ktsr.channels > 1) { + VGM_LOG("1\n"); + goto fail; + } + + /* 0x00: samples per frame */ + /* 0x02: frame size */ + cfg.config_data = read_u32be(ktsr.extra_offset + 0x04, sf_b); + if ((cfg.config_data & 0xFF) == 0xFE) /* later versions(?) in LE */ + cfg.config_data = read_u32le(ktsr.extra_offset + 0x04, sf_b); + + cfg.channels = vgmstream->channels; + cfg.encoder_delay = 256; /* observed default (ex. Attack on Titan PC vs Vita) */ + + vgmstream->codec_data = init_atrac9(&cfg); + if (!vgmstream->codec_data) goto fail; + vgmstream->coding_type = coding_ATRAC9; + vgmstream->layout_type = layout_none; + break; +#endif + } +#endif + +#ifdef VGM_USE_VORBIS + case KVS: { + VGMSTREAM *ogg_vgmstream = NULL; //TODO: meh + STREAMFILE *sf_kvs = setup_subfile_streamfile(sf_b, ktsr.stream_offsets[0], ktsr.stream_sizes[0], "kvs"); + if (!sf_kvs) goto fail; + + ogg_vgmstream = init_vgmstream_ogg_vorbis(sf_kvs); + close_streamfile(sf_kvs); + if (ogg_vgmstream) { + ogg_vgmstream->stream_size = vgmstream->stream_size; + ogg_vgmstream->num_streams = vgmstream->num_streams; + ogg_vgmstream->channel_layout = vgmstream->channel_layout; + /* loops look shared */ + strcpy(ogg_vgmstream->stream_name, vgmstream->stream_name); + + close_vgmstream(vgmstream); + if (sf_b != sf) close_streamfile(sf_b); + return ogg_vgmstream; + } + else { + goto fail; + } + + break; + } +#endif + + default: + goto fail; + } + + + if (!vgmstream_open_stream_bf(vgmstream, sf_b, ktsr.stream_offsets[0], 1)) + goto fail; + + + /* data offset per channel is absolute (not actual interleave since there is padding) in some cases */ + if (separate_offsets) { + int i; + for (i = 0; i < ktsr.channels; i++) { + vgmstream->ch[i].offset = ktsr.stream_offsets[i]; + } + } + + if (sf_b != sf) close_streamfile(sf_b); + return vgmstream; + +fail: + if (sf_b != sf) close_streamfile(sf_b); + close_vgmstream(vgmstream); + return NULL; +} + +static layered_layout_data* build_layered_atrac9(ktsr_header* ktsr, STREAMFILE* sf, uint32_t config_data) { + STREAMFILE* temp_sf = NULL; + layered_layout_data* data = NULL; + int layers = ktsr->channels; + int i; + + + /* init layout */ + data = init_layout_layered(layers); + if (!data) goto fail; + + for (i = 0; i < layers; i++) { + data->layers[i] = allocate_vgmstream(1, 0); + if (!data->layers[i]) goto fail; + + data->layers[i]->sample_rate = ktsr->sample_rate; + data->layers[i]->num_samples = ktsr->num_samples; + +#ifdef VGM_USE_ATRAC9 + { + atrac9_config cfg = {0}; + + cfg.config_data = config_data; + cfg.channels = 1; + cfg.encoder_delay = 256; /* observed default (ex. Attack on Titan PC vs Vita) */ + + data->layers[i]->codec_data = init_atrac9(&cfg); + if (!data->layers[i]->codec_data) goto fail; + data->layers[i]->coding_type = coding_ATRAC9; + data->layers[i]->layout_type = layout_none; + } +#else + goto fail; +#endif + + temp_sf = setup_subfile_streamfile(sf, ktsr->stream_offsets[i], ktsr->stream_sizes[i], NULL); + if (!temp_sf) goto fail; + + if (!vgmstream_open_stream(data->layers[i], temp_sf, 0x00)) + goto fail; + + close_streamfile(temp_sf); + temp_sf = NULL; + } + + /* setup layered VGMSTREAMs */ + if (!setup_layout_layered(data)) + goto fail; + return data; +fail: + close_streamfile(temp_sf); + free_layout_layered(data); + return NULL; +} + + +static int parse_codec(ktsr_header* ktsr) { + + /* platform + format to codec, simplified until more codec combos are found */ + switch(ktsr->platform) { + case 0x01: /* PC */ + if (ktsr->is_external) + ktsr->codec = KVS; + else if (ktsr->format == 0x00) + ktsr->codec = MSADPCM; + else + goto fail; + break; + + case 0x03: /* VITA */ + if (ktsr->is_external) + goto fail; + else if (ktsr->format == 0x01) + ktsr->codec = ATRAC9; + else + goto fail; + break; + + case 0x04: /* Switch */ + if (ktsr->is_external) + goto fail; /* KTSS? */ + else if (ktsr->format == 0x00) + ktsr->codec = DSP; + else + goto fail; + break; + + default: + goto fail; + } + + return 1; +fail: + VGM_LOG("KTSR: unknown codec combo: ext=%x, fmt=%x, ptf=%x\n", ktsr->is_external, ktsr->format, ktsr->platform); + return 0; +} + +static int parse_ktsr_subfile(ktsr_header* ktsr, STREAMFILE* sf, off_t offset) { + off_t suboffset, starts_offset, sizes_offset; + int i; + uint32_t type; + + type = read_u32be(offset + 0x00, sf); + //size = read_u32le(offset + 0x04, sf); + + /* probably could check the flag in sound header, but the format is kinda messy */ + switch(type) { /* hash-id? */ + + case 0x38D0437D: /* external [Nioh (PC), Atelier Ryza (PC)] */ + /* 08 subtype? (ex. 0x522B86B9) + * 0c channels + * 10 ? (always 0x002706B8) + * 14 codec? (05=KVS) + * 18 sample rate + * 1c num samples + * 20 null? + * 24 loop start or -1 (loop end is num samples) + * 28 channel layout (or null?) + * 2c null + * 30 null + * 34 data offset (absolute to external stream, points to actual format and not to mini-header) + * 38 data size + * 3c always 0x0200 + */ + + ktsr->channels = read_u32le(offset + 0x0c, sf); + ktsr->format = read_u32le(offset + 0x14, sf); + /* other fields will be read in the external stream */ + + ktsr->channel_layout= read_u32le(offset + 0x28, sf); + + ktsr->stream_offsets[0] = read_u32le(offset + 0x34, sf); + ktsr->stream_sizes[0] = read_u32le(offset + 0x38, sf); + ktsr->is_external = 1; + + if (ktsr->format != 0x05) { + VGM_LOG("KTSR: unknown subcodec at %lx\n", offset); + goto fail; + } + + break; + + case 0x41FDBD4E: /* internal [Attack on Titan: Wings of Freedom (Vita)] */ + case 0x6FF273F9: /* internal [Attack on Titan: Wings of Freedom (PC/Vita)] */ + case 0x6FCAB62E: /* internal [Marvel Ultimate Alliance 3: The Black Order (Switch)] */ + case 0x6AD86FE9: /* internal [Atelier Ryza (PC/Switch), Persona5 Scramble (Switch)] */ + case 0x10250527: /* internal [Fire Emblem: Three Houses DLC (Switch)] */ + /* 08 subtype? (0x6029DBD2, 0xD20A92F90, 0xDC6FF709) + * 0c channels + * 10 format? (00=platform's ADPCM? 01=ATRAC9?) + * 11 bps? (always 16) + * 12 null + * 14 sample rate + * 18 num samples + * 1c null or 0x100? + * 20 loop start or -1 (loop end is num samples) + * 24 channel layout or null + * 28 header offset (within subfile) + * 2c header size [B, C] + * 30 offset to data start offset [A, C] or to data start+size [B] + * 34 offset to data size [A, C] or same per channel + * 38 always 0x0200 + * -- header + * -- data start offset + * -- data size + */ + + ktsr->channels = read_u32le(offset + 0x0c, sf); + ktsr->format = read_u8 (offset + 0x10, sf); + ktsr->sample_rate = read_s32le(offset + 0x14, sf); + ktsr->num_samples = read_s32le(offset + 0x18, sf); + ktsr->loop_start = read_s32le(offset + 0x20, sf); + ktsr->channel_layout= read_u32le(offset + 0x24, sf); + ktsr->extra_offset = read_u32le(offset + 0x28, sf) + offset; + if (type == 0x41FDBD4E || type == 0x6FF273F9) /* v1 */ + suboffset = offset + 0x2c; + else + suboffset = offset + 0x30; + + if (ktsr->channels > MAX_CHANNELS) { + VGM_LOG("KTSR: max channels found\n"); + goto fail; + } + + starts_offset = read_u32le(suboffset + 0x00, sf) + offset; + sizes_offset = read_u32le(suboffset + 0x04, sf) + offset; + for (i = 0; i < ktsr->channels; i++) { + ktsr->stream_offsets[i] = read_u32le(starts_offset + 0x04*i, sf) + offset; + ktsr->stream_sizes[i] = read_u32le(sizes_offset + 0x04*i, sf); + } + + ktsr->loop_flag = (ktsr->loop_start >= 0); + + break; + + default: + /* streams also have their own chunks like 0x09D4F415, not needed here */ + VGM_LOG("KTSR: unknown subheader at %lx\n", offset); + goto fail; + } + + if (!parse_codec(ktsr)) + goto fail; + + return 1; +fail: + VGM_LOG("KTSR: error parsing subheader\n"); + return 0; +} + +static void build_name(ktsr_header* ktsr, STREAMFILE* sf) { + char sound_name[255] = {0}; + char config_name[255] = {0}; + + /* names can be different or same but usually config is better */ + if (ktsr->sound_name_offset) { + read_string(sound_name, sizeof(sound_name), ktsr->sound_name_offset, sf); + } + if (ktsr->config_name_offset) { + read_string(config_name, sizeof(config_name), ktsr->config_name_offset, sf); + } + + //if (longname[0] && shortname[0]) { + // snprintf(ktsr->name, sizeof(ktsr->name), "%s; %s", longname, shortname); + //} + if (config_name[0]) { + snprintf(ktsr->name, sizeof(ktsr->name), "%s", config_name); + + } + else if (sound_name[0]) { + snprintf(ktsr->name, sizeof(ktsr->name), "%s", sound_name); + } + +} + +static void parse_longname(ktsr_header* ktsr, STREAMFILE* sf, uint32_t target_id) { + /* more configs than sounds is possible so we need target_id first */ + off_t offset, end, name_offset; + uint32_t stream_id; + + offset = 0x40; + end = get_streamfile_size(sf); + while (offset < end) { + uint32_t type = read_u32be(offset + 0x00, sf); /* hash-id? */ + uint32_t size = read_u32le(offset + 0x04, sf); + switch(type) { + case 0xBD888C36: /* config */ + stream_id = read_u32be(offset + 0x08, sf); + if (stream_id != target_id) + break; + + name_offset = read_u32le(offset + 0x28, sf); + if (name_offset > 0) + ktsr->config_name_offset = offset + name_offset; + return; /* id found */ + + default: + break; + } + + offset += size; + } + +} + +static int parse_ktsr(ktsr_header* ktsr, STREAMFILE* sf) { + off_t offset, end, header_offset, name_offset; + uint32_t stream_id = 0, stream_count; + + /* 00: KTSR + * 04: type + * 08: version? + * 0a: unknown (usually 00, 02/03 seen in Vita) + * 0b: platform (01=PC, 03=Vita, 04=Switch) + * 0c: game id? + * 10: null + * 14: null + * 18: file size + * 1c: file size + * up to 40: reserved + * until end: entries (totals not defined) */ + + ktsr->platform = read_u8(0x0b,sf); + + if (read_u32le(0x18, sf) != read_u32le(0x1c, sf)) + goto fail; + if (read_u32le(0x1c, sf) != get_streamfile_size(sf)) + goto fail; + + offset = 0x40; + end = get_streamfile_size(sf); + while (offset < end) { + uint32_t type = read_u32be(offset + 0x00, sf); /* hash-id? */ + uint32_t size = read_u32le(offset + 0x04, sf); + + /* parse chunk-like subfiles, usually N configs then N songs */ + switch(type) { + case 0x6172DBA8: /* padding (empty) */ + case 0xBD888C36: /* config (floats, stream id, etc, may have extended name) */ + case 0xC9C48EC1: /* unknown (has some string inside like "boss") */ + break; + + case 0xC5CCCB70: /* sound (internal data or external stream) */ + //VGM_LOG("info at %lx\n", offset); + ktsr->total_subsongs++; + + /* sound table: + * 08: stream id (used in several places) + * 0c: unknown (low number but not version?) + * 0e: external flag + * 10: sub-streams? + * 14: offset to header offset + * 18: offset to name + * --: name + * --: header offset + * --: header + * --: subheader (varies) */ + + + if (ktsr->total_subsongs == ktsr->target_subsong) { + //;VGM_LOG("KTSR: target at %lx\n", offset); + + stream_id = read_u32be(offset + 0x08,sf); + //ktsr->is_external = read_u16le(offset + 0x0e,sf); + stream_count = read_u32le(offset + 0x10,sf); + if (stream_count != 1) { + VGM_LOG("KTSR: unknown stream count\n"); + goto fail; + } + + header_offset = read_u32le(offset + 0x14, sf); + name_offset = read_u32le(offset + 0x18, sf); + if (name_offset > 0) + ktsr->sound_name_offset = offset + name_offset; + + header_offset = read_u32le(offset + header_offset, sf) + offset; + + if (!parse_ktsr_subfile(ktsr, sf, header_offset)) + goto fail; + } + break; + + default: + /* streams also have their own chunks like 0x09D4F415, not needed here */ + VGM_LOG("KTSR: unknown chunk at %lx\n", offset); + goto fail; + } + + offset += size; + } + + if (ktsr->target_subsong > ktsr->total_subsongs) + goto fail; + + parse_longname(ktsr, sf, stream_id); + build_name(ktsr, sf); + + return 1; +fail: + return 0; +} diff --git a/src/meta/meta.h b/src/meta/meta.h index adb49461..e2b24d2c 100644 --- a/src/meta/meta.h +++ b/src/meta/meta.h @@ -898,4 +898,6 @@ VGMSTREAM* init_vgmstream_diva(STREAMFILE* sf); VGMSTREAM* init_vgmstream_imuse(STREAMFILE* sf); +VGMSTREAM* init_vgmstream_ktsr(STREAMFILE* sf); + #endif /*_META_H*/ diff --git a/src/vgmstream.c b/src/vgmstream.c index 43e37cbe..7cd10c73 100644 --- a/src/vgmstream.c +++ b/src/vgmstream.c @@ -495,6 +495,7 @@ VGMSTREAM * (*init_vgmstream_functions[])(STREAMFILE *streamFile) = { init_vgmstream_bkhd_fx, init_vgmstream_diva, init_vgmstream_imuse, + init_vgmstream_ktsr, /* lowest priority metas (should go after all metas, and TXTH should go before raw formats) */ init_vgmstream_txth, /* proper parsers should supersede TXTH, once added */ diff --git a/src/vgmstream.h b/src/vgmstream.h index 413fe5d6..ba75c4a1 100644 --- a/src/vgmstream.h +++ b/src/vgmstream.h @@ -730,6 +730,7 @@ typedef enum { meta_WWISE_FX, meta_DIVA, meta_IMUSE, + meta_KTSR, } meta_t; /* standard WAVEFORMATEXTENSIBLE speaker positions */