2020-05-23 23:12:45 +02:00
|
|
|
#include "meta.h"
|
|
|
|
#include "../coding/coding.h"
|
|
|
|
#include "../layout/layout.h"
|
2023-10-14 16:58:55 +02:00
|
|
|
#include "../util/companion_files.h"
|
2023-10-22 22:30:08 +02:00
|
|
|
#include "ktsr_streamfile.h"
|
2020-05-23 23:12:45 +02:00
|
|
|
|
2023-01-23 00:52:16 +01:00
|
|
|
typedef enum { NONE, MSADPCM, DSP, GCADPCM, ATRAC9, RIFF_ATRAC9, KOVS, KTSS, } ktsr_codec;
|
2020-05-23 23:12:45 +02:00
|
|
|
|
|
|
|
#define MAX_CHANNELS 8
|
|
|
|
|
|
|
|
typedef struct {
|
2023-10-14 16:58:55 +02:00
|
|
|
uint32_t base_offset;
|
2023-10-08 12:33:10 +02:00
|
|
|
bool is_srsa;
|
2020-05-23 23:12:45 +02:00
|
|
|
int total_subsongs;
|
|
|
|
int target_subsong;
|
|
|
|
ktsr_codec codec;
|
|
|
|
|
2023-10-14 12:52:14 +02:00
|
|
|
uint32_t audio_id;
|
2020-05-23 23:12:45 +02:00
|
|
|
int platform;
|
|
|
|
int format;
|
2023-10-14 12:52:14 +02:00
|
|
|
uint32_t sound_id;
|
|
|
|
uint32_t sound_flags;
|
|
|
|
uint32_t config_flags;
|
2020-05-23 23:12:45 +02:00
|
|
|
|
|
|
|
int channels;
|
|
|
|
int sample_rate;
|
|
|
|
int32_t num_samples;
|
|
|
|
int32_t loop_start;
|
|
|
|
int loop_flag;
|
2021-09-15 23:22:17 +02:00
|
|
|
uint32_t extra_offset;
|
2020-05-23 23:12:45 +02:00
|
|
|
uint32_t channel_layout;
|
|
|
|
|
|
|
|
int is_external;
|
2021-09-04 22:06:54 +02:00
|
|
|
uint32_t stream_offsets[MAX_CHANNELS];
|
|
|
|
uint32_t stream_sizes[MAX_CHANNELS];
|
2020-05-23 23:12:45 +02:00
|
|
|
|
2021-09-15 23:22:17 +02:00
|
|
|
uint32_t sound_name_offset;
|
|
|
|
uint32_t config_name_offset;
|
2020-05-23 23:12:45 +02:00
|
|
|
char name[255+1];
|
|
|
|
} ktsr_header;
|
|
|
|
|
2023-10-08 12:33:10 +02:00
|
|
|
static VGMSTREAM* init_vgmstream_ktsr_internal(STREAMFILE* sf, bool is_srsa);
|
2024-04-13 20:24:40 +02:00
|
|
|
static bool parse_ktsr(ktsr_header* ktsr, STREAMFILE* sf);
|
2020-05-23 23:12:45 +02:00
|
|
|
static layered_layout_data* build_layered_atrac9(ktsr_header* ktsr, STREAMFILE *sf, uint32_t config_data);
|
2023-01-23 00:52:16 +01:00
|
|
|
static VGMSTREAM* init_vgmstream_ktsr_sub(STREAMFILE* sf_b, ktsr_header* ktsr, VGMSTREAM* (*init_vgmstream)(STREAMFILE* sf), const char* ext);
|
2020-05-23 23:12:45 +02:00
|
|
|
|
2023-01-23 00:52:16 +01:00
|
|
|
/* KTSR - Koei Tecmo sound resource container */
|
2020-05-23 23:12:45 +02:00
|
|
|
VGMSTREAM* init_vgmstream_ktsr(STREAMFILE* sf) {
|
2023-10-14 16:58:55 +02:00
|
|
|
|
|
|
|
/* checks */
|
|
|
|
if (!is_id32be(0x00, sf, "KTSR"))
|
|
|
|
return NULL;
|
|
|
|
/* others: see internal */
|
|
|
|
|
|
|
|
/* .ktsl2asbin: common [Atelier Ryza (PC/Switch), Nioh (PC)]
|
|
|
|
* .asbin: Warriors Orochi 4 (PC) */
|
|
|
|
if (!check_extensions(sf, "ktsl2asbin,asbin"))
|
|
|
|
return NULL;
|
|
|
|
|
2023-10-08 12:33:10 +02:00
|
|
|
return init_vgmstream_ktsr_internal(sf, false) ;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ASRS - container of KTSR found in newer games */
|
|
|
|
VGMSTREAM* init_vgmstream_asrs(STREAMFILE* sf) {
|
|
|
|
|
|
|
|
/* checks */
|
|
|
|
if (!is_id32be(0x00, sf, "ASRS"))
|
|
|
|
return NULL;
|
|
|
|
/* 0x04: null */
|
|
|
|
/* 0x08: file size */
|
|
|
|
/* 0x0c: null */
|
|
|
|
|
2023-10-14 16:58:55 +02:00
|
|
|
/* .srsa: header id (as generated by common tools, probably "(something)asbin") */
|
2023-10-08 12:33:10 +02:00
|
|
|
if (!check_extensions(sf, "srsa"))
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
/* mini-container of memory-KTSR (streams also have a "TSRS" for stream-KTSR).
|
2023-10-14 16:58:55 +02:00
|
|
|
* .srsa/srst usually have hashed filenames, so it isn't easy to match them, so this
|
|
|
|
* is mainly useful for .srsa with internal streams. */
|
2023-10-08 12:33:10 +02:00
|
|
|
|
2023-10-14 16:58:55 +02:00
|
|
|
return init_vgmstream_ktsr_internal(sf, true);
|
2023-10-08 12:33:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static VGMSTREAM* init_vgmstream_ktsr_internal(STREAMFILE* sf, bool is_srsa) {
|
2020-05-23 23:12:45 +02:00
|
|
|
VGMSTREAM* vgmstream = NULL;
|
2021-09-12 20:03:38 +02:00
|
|
|
STREAMFILE* sf_b = NULL;
|
2020-05-23 23:12:45 +02:00
|
|
|
ktsr_header ktsr = {0};
|
|
|
|
int target_subsong = sf->stream_index;
|
|
|
|
int separate_offsets = 0;
|
|
|
|
|
2023-10-14 16:58:55 +02:00
|
|
|
ktsr.is_srsa = is_srsa;
|
|
|
|
if (ktsr.is_srsa) {
|
|
|
|
ktsr.base_offset = 0x10;
|
|
|
|
}
|
|
|
|
|
2020-05-23 23:12:45 +02:00
|
|
|
|
|
|
|
/* checks */
|
2023-10-14 16:58:55 +02:00
|
|
|
if (!is_id32be(ktsr.base_offset + 0x00, sf, "KTSR"))
|
2023-10-08 12:33:10 +02:00
|
|
|
return NULL;
|
2023-10-14 16:58:55 +02:00
|
|
|
if (read_u32be(ktsr.base_offset + 0x04, sf) != 0x777B481A) /* hash id: 0x777B481A=as, 0x0294DDFC=st, 0xC638E69E=gc */
|
2023-10-08 12:33:10 +02:00
|
|
|
return NULL;
|
|
|
|
|
2020-05-23 23:12:45 +02:00
|
|
|
|
|
|
|
/* KTSR can be a memory file (ktsl2asbin), streams (ktsl2stbin) and global config (ktsl2gcbin)
|
2021-09-12 20:03:38 +02:00
|
|
|
* This accepts .ktsl2asbin with internal data or external streams as subsongs.
|
2023-10-14 12:52:14 +02:00
|
|
|
* Hashes are meant to be read LE, but here are BE for easier comparison (they probably correspond
|
|
|
|
* to some text but are pre-hashed in exes). Some info from KTSR.bt */
|
2020-05-23 23:12:45 +02:00
|
|
|
|
|
|
|
if (target_subsong == 0) target_subsong = 1;
|
|
|
|
ktsr.target_subsong = target_subsong;
|
|
|
|
|
|
|
|
if (!parse_ktsr(&ktsr, sf))
|
|
|
|
goto fail;
|
|
|
|
|
2024-04-13 20:24:40 +02:00
|
|
|
if (ktsr.total_subsongs == 0) {
|
|
|
|
vgm_logi("KTSR: file has no subsongs\n");
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2020-05-23 23:12:45 +02:00
|
|
|
/* open companion body */
|
|
|
|
if (ktsr.is_external) {
|
2023-10-14 16:58:55 +02:00
|
|
|
if (ktsr.is_srsa) {
|
|
|
|
/* try parsing TXTM if present, since .srsa+srst have hashed names and don't match unless renamed */
|
|
|
|
sf_b = read_filemap_file(sf, 0);
|
|
|
|
}
|
2023-10-08 12:33:10 +02:00
|
|
|
|
2020-05-23 23:12:45 +02:00
|
|
|
if (!sf_b) {
|
2023-10-14 16:58:55 +02:00
|
|
|
/* try (name).(ext), as seen in older games */
|
|
|
|
const char* companion_ext = check_extensions(sf, "asbin") ? "stbin" : "ktsl2stbin";
|
|
|
|
if (ktsr.is_srsa)
|
|
|
|
companion_ext = "srst";
|
|
|
|
|
|
|
|
sf_b = open_streamfile_by_ext(sf, companion_ext);
|
|
|
|
if (!sf_b) {
|
|
|
|
vgm_logi("KTSR: companion file '*.%s' not found\n", companion_ext);
|
|
|
|
goto fail;
|
|
|
|
}
|
2020-05-23 23:12:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
sf_b = sf;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-01-23 00:52:16 +01:00
|
|
|
/* subfiles */
|
|
|
|
{
|
|
|
|
VGMSTREAM* (*init_vgmstream)(STREAMFILE* sf) = NULL;
|
|
|
|
const char* ext;
|
|
|
|
switch(ktsr.codec) {
|
|
|
|
case RIFF_ATRAC9: init_vgmstream = init_vgmstream_riff; ext = "at9"; break;
|
|
|
|
case KOVS: init_vgmstream = init_vgmstream_ogg_vorbis; ext = "kvs"; break;
|
|
|
|
case KTSS: init_vgmstream = init_vgmstream_ktss; ext = "ktss"; break;
|
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (init_vgmstream) {
|
|
|
|
vgmstream = init_vgmstream_ktsr_sub(sf_b, &ktsr, init_vgmstream, ext);
|
|
|
|
if (!vgmstream) goto fail;
|
|
|
|
|
|
|
|
if (sf_b != sf) close_streamfile(sf_b);
|
|
|
|
return vgmstream;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-23 23:12:45 +02:00
|
|
|
/* 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;
|
|
|
|
}
|
|
|
|
#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;
|
|
|
|
}
|
|
|
|
|
2023-10-22 22:30:08 +02:00
|
|
|
// TODO improve, unify with other metas that do similar stuff
|
2023-01-23 00:52:16 +01:00
|
|
|
static VGMSTREAM* init_vgmstream_ktsr_sub(STREAMFILE* sf_b, ktsr_header* ktsr, VGMSTREAM* (*init_vgmstream)(STREAMFILE* sf), const char* ext) {
|
|
|
|
VGMSTREAM* sub_vgmstream = NULL;
|
2023-10-22 22:30:08 +02:00
|
|
|
STREAMFILE* temp_sf = NULL;
|
|
|
|
|
|
|
|
temp_sf = setup_ktsr_streamfile(sf_b, ktsr->is_external, ktsr->stream_offsets[0], ktsr->stream_sizes[0], ext);
|
2023-01-23 00:52:16 +01:00
|
|
|
if (!temp_sf) return NULL;
|
|
|
|
|
|
|
|
sub_vgmstream = init_vgmstream(temp_sf);
|
|
|
|
close_streamfile(temp_sf);
|
2023-10-22 22:30:08 +02:00
|
|
|
if (!sub_vgmstream) {
|
|
|
|
VGM_LOG("ktsr: can't open subfile at %x (size %x)\n", ktsr->stream_offsets[0], ktsr->stream_sizes[0]);
|
|
|
|
return NULL;
|
|
|
|
}
|
2023-01-23 00:52:16 +01:00
|
|
|
|
|
|
|
sub_vgmstream->stream_size = ktsr->stream_sizes[0];
|
|
|
|
sub_vgmstream->num_streams = ktsr->total_subsongs;
|
|
|
|
sub_vgmstream->channel_layout = ktsr->channel_layout;
|
|
|
|
|
|
|
|
strcpy(sub_vgmstream->stream_name, ktsr->name);
|
|
|
|
|
|
|
|
return sub_vgmstream;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-05-23 23:12:45 +02:00
|
|
|
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 */
|
2023-10-08 12:33:10 +02:00
|
|
|
case 0x05: /* PC/Steam [Fate/Samurai Remnant (PC)] */
|
2022-05-13 16:39:50 +02:00
|
|
|
if (ktsr->is_external) {
|
|
|
|
if (ktsr->format == 0x0005)
|
2023-01-23 00:52:16 +01:00
|
|
|
ktsr->codec = KOVS; // Atelier Ryza (PC)
|
2022-05-13 16:39:50 +02:00
|
|
|
else
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
else if (ktsr->format == 0x0000) {
|
2023-01-23 00:52:16 +01:00
|
|
|
ktsr->codec = MSADPCM; // Warrior Orochi 4 (PC)
|
2022-05-13 16:39:50 +02:00
|
|
|
}
|
|
|
|
else {
|
2020-05-23 23:12:45 +02:00
|
|
|
goto fail;
|
2022-05-13 16:39:50 +02:00
|
|
|
}
|
2020-05-23 23:12:45 +02:00
|
|
|
break;
|
|
|
|
|
2022-05-13 16:39:50 +02:00
|
|
|
case 0x03: /* PS4/VITA */
|
|
|
|
if (ktsr->is_external) {
|
|
|
|
if (ktsr->format == 0x1001)
|
2023-01-23 00:52:16 +01:00
|
|
|
ktsr->codec = RIFF_ATRAC9; // Nioh (PS4)
|
2022-05-13 16:39:50 +02:00
|
|
|
else
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
else if (ktsr->format == 0x0001)
|
2023-01-23 00:52:16 +01:00
|
|
|
ktsr->codec = ATRAC9; // Attack on Titan: Wings of Freedom (Vita)
|
2020-05-23 23:12:45 +02:00
|
|
|
else
|
|
|
|
goto fail;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x04: /* Switch */
|
2023-01-23 00:52:16 +01:00
|
|
|
if (ktsr->is_external) {
|
|
|
|
if (ktsr->format == 0x0005)
|
|
|
|
ktsr->codec = KTSS; // [Ultra Kaiju Monster Rancher (Switch)]
|
2023-11-11 12:40:05 +01:00
|
|
|
else if (ktsr->format == 0x1000)
|
|
|
|
ktsr->codec = KTSS; // [Fire Emblem: Three Houses (Switch)-some DSP voices]
|
2023-01-23 00:52:16 +01:00
|
|
|
else
|
|
|
|
goto fail;
|
|
|
|
}
|
2022-05-13 16:39:50 +02:00
|
|
|
else if (ktsr->format == 0x0000)
|
2023-11-11 12:40:05 +01:00
|
|
|
ktsr->codec = DSP; // [Fire Emblem: Three Houses (Switch)]
|
2020-05-23 23:12:45 +02:00
|
|
|
else
|
|
|
|
goto fail;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
fail:
|
2023-01-23 00:52:16 +01:00
|
|
|
VGM_LOG("ktsr: unknown codec combo: external=%x, format=%x, platform=%x\n", ktsr->is_external, ktsr->format, ktsr->platform);
|
2020-05-23 23:12:45 +02:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2024-04-13 20:24:40 +02:00
|
|
|
static bool parse_ktsr_subfile(ktsr_header* ktsr, STREAMFILE* sf, uint32_t offset) {
|
2021-09-15 23:22:17 +02:00
|
|
|
uint32_t suboffset, starts_offset, sizes_offset;
|
2020-05-23 23:12:45 +02:00
|
|
|
int i;
|
|
|
|
uint32_t type;
|
|
|
|
|
2021-09-12 20:03:38 +02:00
|
|
|
type = read_u32be(offset + 0x00, sf); /* hash-id? */
|
2020-05-23 23:12:45 +02:00
|
|
|
//size = read_u32le(offset + 0x04, sf);
|
|
|
|
|
|
|
|
/* probably could check the flag in sound header, but the format is kinda messy */
|
2021-09-12 20:03:38 +02:00
|
|
|
switch(type) {
|
2020-05-23 23:12:45 +02:00
|
|
|
|
2022-05-13 16:39:50 +02:00
|
|
|
case 0x38D0437D: /* external [Nioh (PC/PS4), Atelier Ryza (PC)] */
|
|
|
|
case 0x3DEA478D: /* external [Nioh (PC)] (smaller) */
|
2020-07-11 21:25:12 +02:00
|
|
|
case 0xDF92529F: /* external [Atelier Ryza (PC)] */
|
2020-09-12 15:03:43 +02:00
|
|
|
case 0x6422007C: /* external [Atelier Ryza (PC)] */
|
2023-10-22 22:30:08 +02:00
|
|
|
case 0x793A1FD7: /* external [Stranger of Paradise (PS4)]-encrypted */
|
|
|
|
case 0xA0F4FC6C: /* external [Stranger of Paradise (PS4)]-encrypted */
|
2020-05-23 23:12:45 +02:00
|
|
|
/* 08 subtype? (ex. 0x522B86B9)
|
|
|
|
* 0c channels
|
2023-10-22 22:30:08 +02:00
|
|
|
* 10 ? (always 0x002706B8 / 7864523D in SoP)
|
2022-05-13 16:39:50 +02:00
|
|
|
* 14 external codec
|
2020-05-23 23:12:45 +02:00
|
|
|
* 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 */
|
|
|
|
|
2021-09-12 20:03:38 +02:00
|
|
|
ktsr->channel_layout = read_u32le(offset + 0x28, sf);
|
2020-05-23 23:12:45 +02:00
|
|
|
|
2021-09-12 20:03:38 +02:00
|
|
|
if (type == 0x3DEA478D) { /* Nioh (PC) has one less field, some files only [ABS.ktsl2asbin] */
|
|
|
|
ktsr->stream_offsets[0] = read_u32le(offset + 0x30, sf);
|
|
|
|
ktsr->stream_sizes[0] = read_u32le(offset + 0x34, sf);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
ktsr->stream_offsets[0] = read_u32le(offset + 0x34, sf);
|
|
|
|
ktsr->stream_sizes[0] = read_u32le(offset + 0x38, sf);
|
|
|
|
}
|
2020-05-23 23:12:45 +02:00
|
|
|
ktsr->is_external = 1;
|
|
|
|
|
|
|
|
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) {
|
2021-09-15 23:22:17 +02:00
|
|
|
VGM_LOG("ktsr: max channels found\n");
|
2020-05-23 23:12:45 +02:00
|
|
|
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 */
|
2021-09-15 23:22:17 +02:00
|
|
|
VGM_LOG("ktsr: unknown subheader at %x\n", offset);
|
2020-05-23 23:12:45 +02:00
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!parse_codec(ktsr))
|
|
|
|
goto fail;
|
|
|
|
|
2024-04-13 20:24:40 +02:00
|
|
|
return true;
|
2020-05-23 23:12:45 +02:00
|
|
|
fail:
|
2021-09-15 23:22:17 +02:00
|
|
|
VGM_LOG("ktsr: error parsing subheader\n");
|
2024-04-13 20:24:40 +02:00
|
|
|
return false;
|
2020-05-23 23:12:45 +02:00
|
|
|
}
|
|
|
|
|
2023-10-14 20:01:38 +02:00
|
|
|
/* ktsr engine reads+decrypts in the same func based on passed flag tho (reversed from exe)
|
|
|
|
* Strings are usually ASCII but Shift-JIS is used in rare cases (0x0c3e2edf.srsa) */
|
2023-10-14 12:52:14 +02:00
|
|
|
static void decrypt_string_ktsr(char* buf, size_t buf_size, uint32_t seed) {
|
|
|
|
for (int i = 0; i < buf_size - 1; i++) {
|
|
|
|
if (!buf[i]) /* just in case */
|
|
|
|
break;
|
|
|
|
|
|
|
|
seed = 0x343FD * seed + 0x269EC3; /* classic rand */
|
|
|
|
buf[i] ^= (seed >> 16) & 0xFF; /* 3rd byte */
|
|
|
|
if (!buf[i]) /* end null is also encrypted (but there are extra nulls after it anyway) */
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* like read_string but allow any value since it can be encrypted */
|
|
|
|
static size_t read_string_ktsr(char* buf, size_t buf_size, off_t offset, STREAMFILE* sf) {
|
|
|
|
int pos;
|
|
|
|
|
|
|
|
for (pos = 0; pos < buf_size; pos++) {
|
|
|
|
uint8_t byte = read_u8(offset + pos, sf);
|
|
|
|
char c = (char)byte;
|
|
|
|
if (buf) buf[pos] = c;
|
|
|
|
if (c == '\0')
|
|
|
|
return pos;
|
|
|
|
if (pos+1 == buf_size) {
|
|
|
|
if (buf) buf[pos] = '\0';
|
|
|
|
return buf_size;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-05-23 23:12:45 +02:00
|
|
|
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) {
|
2023-10-14 12:52:14 +02:00
|
|
|
read_string_ktsr(sound_name, sizeof(sound_name), ktsr->sound_name_offset, sf);
|
|
|
|
if (ktsr->sound_flags & 0x0008)
|
|
|
|
decrypt_string_ktsr(sound_name, sizeof(sound_name), ktsr->audio_id);
|
2020-05-23 23:12:45 +02:00
|
|
|
}
|
|
|
|
if (ktsr->config_name_offset) {
|
2023-10-14 12:52:14 +02:00
|
|
|
read_string_ktsr(config_name, sizeof(config_name), ktsr->config_name_offset, sf);
|
|
|
|
if (ktsr->config_flags & 0x0200)
|
|
|
|
decrypt_string_ktsr(config_name, sizeof(config_name), ktsr->audio_id);
|
2020-05-23 23:12:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
//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);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-10-14 12:52:14 +02:00
|
|
|
static void parse_longname(ktsr_header* ktsr, STREAMFILE* sf) {
|
2020-05-23 23:12:45 +02:00
|
|
|
/* more configs than sounds is possible so we need target_id first */
|
2021-09-15 23:22:17 +02:00
|
|
|
uint32_t offset, end, name_offset;
|
2020-05-23 23:12:45 +02:00
|
|
|
uint32_t stream_id;
|
|
|
|
|
2023-10-14 16:58:55 +02:00
|
|
|
offset = 0x40 + ktsr->base_offset;
|
|
|
|
end = get_streamfile_size(sf) - ktsr->base_offset;
|
2020-05-23 23:12:45 +02:00
|
|
|
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 */
|
2023-10-14 12:52:14 +02:00
|
|
|
stream_id = read_u32le(offset + 0x08, sf);
|
|
|
|
if (stream_id != ktsr->sound_id)
|
2020-05-23 23:12:45 +02:00
|
|
|
break;
|
|
|
|
|
2023-10-14 12:52:14 +02:00
|
|
|
ktsr->config_flags = read_u32le(offset + 0x0c, sf);
|
|
|
|
|
2020-05-23 23:12:45 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-13 20:24:40 +02:00
|
|
|
static bool parse_ktsr(ktsr_header* ktsr, STREAMFILE* sf) {
|
2021-09-15 23:22:17 +02:00
|
|
|
uint32_t offset, end, header_offset, name_offset;
|
2023-10-14 12:52:14 +02:00
|
|
|
uint32_t stream_count;
|
2020-05-23 23:12:45 +02:00
|
|
|
|
|
|
|
/* 00: KTSR
|
|
|
|
* 04: type
|
|
|
|
* 08: version?
|
|
|
|
* 0a: unknown (usually 00, 02/03 seen in Vita)
|
|
|
|
* 0b: platform (01=PC, 03=Vita, 04=Switch)
|
2023-10-08 12:33:10 +02:00
|
|
|
* 0c: audio id? (seen in multiple files/games and used as Ogg stream IDs)
|
2020-05-23 23:12:45 +02:00
|
|
|
* 10: null
|
|
|
|
* 14: null
|
|
|
|
* 18: file size
|
|
|
|
* 1c: file size
|
|
|
|
* up to 40: reserved
|
|
|
|
* until end: entries (totals not defined) */
|
|
|
|
|
2023-10-14 16:58:55 +02:00
|
|
|
ktsr->platform = read_u8(ktsr->base_offset + 0x0b,sf);
|
|
|
|
ktsr->audio_id = read_u32le(ktsr->base_offset + 0x0c,sf);
|
2020-05-23 23:12:45 +02:00
|
|
|
|
2023-10-14 16:58:55 +02:00
|
|
|
if (read_u32le(ktsr->base_offset + 0x18, sf) != read_u32le(ktsr->base_offset + 0x1c, sf))
|
2020-05-23 23:12:45 +02:00
|
|
|
goto fail;
|
2023-10-14 16:58:55 +02:00
|
|
|
if (read_u32le(ktsr->base_offset + 0x1c, sf) != get_streamfile_size(sf) - ktsr->base_offset) {
|
2023-10-14 12:52:14 +02:00
|
|
|
vgm_logi("KTSR: incorrect file size (bad rip?)\n");
|
2020-05-23 23:12:45 +02:00
|
|
|
goto fail;
|
2023-10-14 12:52:14 +02:00
|
|
|
}
|
2020-05-23 23:12:45 +02:00
|
|
|
|
2023-10-14 16:58:55 +02:00
|
|
|
offset = 0x40 + ktsr->base_offset;
|
|
|
|
end = get_streamfile_size(sf) - ktsr->base_offset;
|
2020-05-23 23:12:45 +02:00
|
|
|
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) {
|
2023-10-08 12:33:10 +02:00
|
|
|
case 0x6172DBA8: /* ? (mostly empty) */
|
|
|
|
case 0xBD888C36: /* cue? (floats, stream id, etc, may have extended name; can have sub-chunks)-appears N times */
|
2020-05-23 23:12:45 +02:00
|
|
|
case 0xC9C48EC1: /* unknown (has some string inside like "boss") */
|
2023-10-14 12:52:14 +02:00
|
|
|
case 0xA9D23BF1: /* "state container", some kind of config/floats, with names like 'State_bgm01'..N */
|
2021-06-20 10:32:51 +02:00
|
|
|
case 0x836FBECA: /* unknown (~0x300, encrypted? table + data) */
|
2020-05-23 23:12:45 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 0xC5CCCB70: /* sound (internal data or external stream) */
|
|
|
|
ktsr->total_subsongs++;
|
|
|
|
|
|
|
|
/* sound table:
|
2023-10-14 12:52:14 +02:00
|
|
|
* 08: current/stream id (used in several places)
|
|
|
|
* 0c: flags (sounds only; other types are similar but different bits)
|
|
|
|
* 0x08: encrypted names (only used after ASRS was introduced?)
|
|
|
|
* 0x10000: external flag
|
2020-05-23 23:12:45 +02:00
|
|
|
* 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) {
|
|
|
|
|
2023-10-14 12:52:14 +02:00
|
|
|
ktsr->sound_id = read_u32le(offset + 0x08,sf);
|
|
|
|
ktsr->sound_flags = read_u32le(offset + 0x0c,sf);
|
2020-05-23 23:12:45 +02:00
|
|
|
stream_count = read_u32le(offset + 0x10,sf);
|
|
|
|
if (stream_count != 1) {
|
2021-09-15 23:22:17 +02:00
|
|
|
VGM_LOG("ktsr: unknown stream count\n");
|
2020-05-23 23:12:45 +02:00
|
|
|
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 */
|
2021-09-15 23:22:17 +02:00
|
|
|
VGM_LOG("ktsr: unknown chunk at %x\n", offset);
|
2020-05-23 23:12:45 +02:00
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
offset += size;
|
|
|
|
}
|
|
|
|
|
2024-04-13 20:24:40 +02:00
|
|
|
if (ktsr->total_subsongs == 0) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-05-23 23:12:45 +02:00
|
|
|
if (ktsr->target_subsong > ktsr->total_subsongs)
|
|
|
|
goto fail;
|
|
|
|
|
2023-10-14 12:52:14 +02:00
|
|
|
parse_longname(ktsr, sf);
|
2020-05-23 23:12:45 +02:00
|
|
|
build_name(ktsr, sf);
|
|
|
|
|
2023-10-14 19:49:50 +02:00
|
|
|
/* skip TSRS header (internals are pre-adjusted) */
|
|
|
|
if (ktsr->is_external && ktsr->base_offset) {
|
2023-10-08 12:33:10 +02:00
|
|
|
for (int i = 0; i < ktsr->channels; i++) {
|
2023-10-14 16:58:55 +02:00
|
|
|
ktsr->stream_offsets[i] += ktsr->base_offset;
|
2023-10-08 12:33:10 +02:00
|
|
|
}
|
2023-10-14 16:58:55 +02:00
|
|
|
|
2023-10-14 19:49:50 +02:00
|
|
|
ktsr->extra_offset += ktsr->base_offset; /* ? */
|
2023-10-08 12:33:10 +02:00
|
|
|
}
|
|
|
|
|
2024-04-13 20:24:40 +02:00
|
|
|
return true;
|
2020-05-23 23:12:45 +02:00
|
|
|
fail:
|
2021-09-15 23:22:17 +02:00
|
|
|
vgm_logi("KTSR: unknown variation (report)\n");
|
2024-04-13 20:24:40 +02:00
|
|
|
return false;
|
2020-05-23 23:12:45 +02:00
|
|
|
}
|