From 08e3ae4872a53e5b3c23f048fe848e52310b4ec4 Mon Sep 17 00:00:00 2001 From: bnnm Date: Sun, 7 Jan 2024 16:47:46 +0100 Subject: [PATCH] Add bank .rsd RedSpark [M&L Dream Team (3DS), Imabikisou (Wii)] --- src/meta/redspark.c | 452 ++++++++++++++++++++++++++++++------------ src/util/endianness.h | 6 +- 2 files changed, 335 insertions(+), 123 deletions(-) diff --git a/src/meta/redspark.c b/src/meta/redspark.c index 15c39be8..421ed103 100644 --- a/src/meta/redspark.c +++ b/src/meta/redspark.c @@ -1,149 +1,357 @@ #include "meta.h" -#include "../coding/coding.h" -#include "../util.h" +#include "../util/endianness.h" -static uint32_t rotlwi(uint32_t x, uint32_t r) { - return (x << r) | (x >> (32-r)); -} +#define MAX_CHANNELS 4 +typedef struct { + bool loop_flag; + int channels; + int sample_rate; + int32_t num_samples; + int32_t loop_start; + int32_t loop_end; + int16_t coefs[MAX_CHANNELS][16]; -static uint32_t find_key(uint32_t firstword) { - uint32_t expected = 0x52656453; - return firstword ^ expected; -} + int total_subsongs; -/* RSD - RedSpark (MadWorld) - RS3D - RedSpark (Mario & Luigi: Dream Team I fi*/ -VGMSTREAM * init_vgmstream_redspark(STREAMFILE *streamFile) { - VGMSTREAM * vgmstream = NULL; - char filename[PATH_LIMIT]; - off_t start_offset; - int loop_flag; - int channel_count; - int dt_flag = 0; - uint32_t key; - enum {encsize = 0x1000}; - uint8_t buf[encsize]; - int32_t(*get_32bit)(const uint8_t *p) = NULL; - int16_t(*get_16bit)(const uint8_t *p) = NULL; - get_16bit = get_16bitBE; - get_32bit = get_32bitBE; + uint32_t stream_offset; + uint32_t stream_size; - /* check extension, case insensitive */ - streamFile->get_name(streamFile,filename,sizeof(filename)); - if (strcasecmp("rsd", filename_extension(filename))) goto fail; - /* decrypt into buffer */ - { - uint32_t data; - int i; - if (read_streamfile(buf,0,encsize,streamFile)!=encsize) goto fail; - if (memcmp(&buf[0], "RedSpark", 8)) { //Check to see if already decrypted - key = find_key(get_32bitBE(&buf[0])); - data = get_32bitBE(&buf[0]) ^ key; - put_32bitBE(&buf[0], data); - key = rotlwi(key, 11); + bool dummy; +} redspark_header_t; - for (i = 4; i < encsize; i += 4) { - key = rotlwi(key, 3) + key; - data = get_32bitBE(&buf[i]) ^ key; - put_32bitBE(&buf[i], data); - } - } - else { - get_16bit = get_16bitLE; - get_32bit = get_32bitLE; - dt_flag = 1; +static bool parse_header(redspark_header_t* h, STREAMFILE* sf, bool is_new); - for (i = 4; i < encsize; i += 4) { - data = get_32bitBE(&buf[i]); - put_32bitBE(&buf[i], data); - } - } +/* RedSpark - Games with audio by RedSpark Ltd. (Minoru Akao) [MadWorld (Wii), Imabikisou (Wii), Mario & Luigi: Dream Team (3DS)] */ +VGMSTREAM* init_vgmstream_redspark(STREAMFILE* sf) { + VGMSTREAM* vgmstream = NULL; + bool is_new; + + + /* checks*/ + uint32_t head_id = read_u32be(0x00, sf); + if (head_id == get_id32be("RedS")) { + is_new = true; /* M&L */ + } + else if (head_id > 0x15800000 && head_id < 0x1B800000) { + is_new = false; /* others: header is encrypted but in predictable ranges to fail faster (will do extra checks later) */ + } + else { + return NULL; } - /* check header */ - if (memcmp(&buf[0],"RedSpark",8)) - goto fail; + if (!check_extensions(sf, "rsd")) + return NULL; - loop_flag = (buf[0x4f] != 0); - channel_count = buf[0x4e]; + redspark_header_t h = {0}; + if (!parse_header(&h, sf, is_new)) + return NULL; - /* make sure loop info is the only two cue points */ - if (loop_flag && buf[0x4f] != 2) goto fail; - /* build the VGMSTREAM */ - vgmstream = allocate_vgmstream(channel_count,loop_flag); + /* build the VGMSTREAM */ + vgmstream = allocate_vgmstream(h.channels, h.loop_flag); if (!vgmstream) goto fail; - /* fill in the vital statistics */ - start_offset = 0x1000; - vgmstream->channels = channel_count; - vgmstream->sample_rate = get_32bit(&buf[0x3c]); - vgmstream->coding_type = coding_NGC_DSP; - if (dt_flag) - vgmstream->num_samples = get_32bit(&buf[0x40]); - else - vgmstream->num_samples = get_32bit(&buf[0x40])*14; - if (loop_flag) { - off_t start = 0x54; - start += channel_count*8; - if (dt_flag) { - vgmstream->loop_start_sample = get_32bit(&buf[start+4]); - vgmstream->loop_end_sample = (get_32bit(&buf[start+0xc])); - } - else { - vgmstream->loop_start_sample = get_32bit(&buf[start+4])*14; - vgmstream->loop_end_sample = (get_32bit(&buf[start+0xc])+1)*14; - } - if (vgmstream->loop_end_sample > vgmstream->num_samples) { - vgmstream->loop_end_sample = vgmstream->num_samples; - } - } - - if (channel_count >= 2) { - vgmstream->layout_type = layout_interleave; - vgmstream->interleave_block_size = 8; - } else { - vgmstream->layout_type = layout_none; - } vgmstream->meta_type = meta_REDSPARK; + vgmstream->sample_rate = h.sample_rate; + vgmstream->num_samples = h.num_samples; + vgmstream->loop_start_sample = h.loop_start; + vgmstream->loop_end_sample = h.loop_end; + vgmstream->num_streams = h.total_subsongs; + vgmstream->stream_size = h.stream_size; - { - off_t start = 0x54; - int i,j; + vgmstream->coding_type = coding_NGC_DSP; + vgmstream->layout_type = layout_interleave; + vgmstream->interleave_block_size = 0x08; - start += channel_count * 8; - if (loop_flag) { - start += 16; - } - - for (j = 0; j < channel_count; j++) { - for (i=0;i<16;i++) { - vgmstream->ch[j].adpcm_coef[i] = - get_16bit(&buf[start+0x2e*j+i*2]); - } + for (int ch = 0; ch < h.channels; ch++) { + for (int i = 0; i < 16; i++) { + vgmstream->ch[ch].adpcm_coef[i] = h.coefs[ch][i]; } } - /* open the file for reading */ - { - int i; - STREAMFILE * file; - file = streamFile->open(streamFile,filename,STREAMFILE_DEFAULT_BUFFER_SIZE); - if (!file) goto fail; - for (i=0;ich[i].streamfile = file; - - vgmstream->ch[i].channel_start_offset= - vgmstream->ch[i].offset=start_offset + i*vgmstream->interleave_block_size; - - } + if (h.dummy) { + vgmstream->num_samples = h.sample_rate; + vgmstream->coding_type = coding_SILENCE; } + if (!vgmstream_open_stream(vgmstream, sf, h.stream_offset)) + goto fail; return vgmstream; - - /* clean up anything we may have opened */ fail: - if (vgmstream) close_vgmstream(vgmstream); + close_vgmstream(vgmstream); return NULL; } + + +static uint32_t rotlwi(uint32_t x, uint32_t r) { + return (x << r) | (x >> (32 - r)); +} + +static uint32_t decrypt_chunk(uint8_t* buf, int buf_size, uint32_t key) { + uint32_t data; + int pos = 0; + + if (key == 0) { + /* initial key seems to only vary slightly between files though (doesn't seem to depend on size/name) */ + key = get_u32be(buf + 0x00) ^ get_id32be("RedS"); + data = get_u32be(buf + 0x00) ^ key; + put_u32be(&buf[0], data); + key = rotlwi(key, 11); + + pos = 0x04; + } + + for (int i = pos; i < buf_size; i += 0x04) { + key = rotlwi(key, 3) + key; + data = get_u32be(buf + i) ^ key; + put_u32be(buf + i, data); + } + + return key; /* to resume decrypting if needed */ +} + +#define HEADER_MAX 0x2800 /* seen 0x2420 in one bank */ + +/* header is encrypted except in M&L 3DS so decrypt + handle in buffer; format + * base 0x30 header + subheader with tables/headers depending on type */ +static bool parse_header(redspark_header_t* h, STREAMFILE* sf, bool is_new) { + uint8_t buf[HEADER_MAX]; + /* LE on 3DS */ + get_u16_t get_u16 = !is_new ? get_u16be : get_u16le; + get_u32_t get_u32 = !is_new ? get_u32be : get_u32le; + uint32_t curr_key = 0x00; + int target_subsong = sf->stream_index; + + /* base header: + 00 "RedSpark" + 08 chunk size (usually data size, smaller for subspark) + 0c type/chunk? 00010000=streams, 00000000=bus/se + 10 ? (low number, usually same for multiple files) + 14 file id or null + 18 data offset + 1c bank flag + 1e 0909=stream, 0404=stream (MW) or RedSpark-within aka 'subspark' (Imabikisou), 0000=bus/se config (no audio) + 20 data size (usually file size, or header + subheader size for subsparks) + 24-30 null/reserved + */ + int base_size = 0x30; + if (read_streamfile(buf, 0x00, base_size,sf) != base_size) + return false; + if (!is_new) + curr_key = decrypt_chunk(buf, 0x30, curr_key); + + /* get base header */ + if (get_u64be(buf + 0x00) != get_id64be("RedSpark")) + return false; + uint32_t data_offset = get_u32(buf + 0x18); + uint32_t data_size = get_u32(buf + 0x20); + int bank_flag = get_u16(buf + 0x1c); + int type = get_u16(buf + 0x1e); + + if (data_offset >= HEADER_MAX) + return false; + + + /* get subheader and prepare offsets */ + int redspark_pos; + int sub_offset, sub_size; + int head_pos; + if (data_offset == 0x30) { + /* 'subspark', seen in a few sfx in Imabikisou (earlier version of banks) */ + /* at data offset (not encrypted): + 00 always 017F0100 (LE?) + 04-10: null + 10 file id + 13 string? size + 14 encrypted string? (ends with 0) + at subchunk_size: + another RedSpark with bank subflag + */ + /* base sub-RedSpark + reset flags */ + redspark_pos = data_size; + if (read_streamfile(buf + redspark_pos, data_size, base_size, sf) != base_size) + return false; + if (!is_new) + curr_key = decrypt_chunk(buf + redspark_pos, base_size, 0); /* new header */ + + /* setup rest of header (handled below) */ + uint32_t subdata_offset = get_u32(buf + redspark_pos + 0x18); + bank_flag = get_u16(buf + redspark_pos + 0x1c); + type = get_u16(buf + redspark_pos + 0x1e); + + data_offset = redspark_pos + subdata_offset; + if (data_offset >= HEADER_MAX) + return false; + + sub_offset = redspark_pos + base_size; + sub_size = data_offset - sub_offset; + head_pos = sub_offset; + } + else { + redspark_pos = 0x00; + sub_offset = base_size; + sub_size = data_offset - sub_offset; + head_pos = sub_offset; + } + + /* read + decrypt rest of header */ + if (read_streamfile(buf + sub_offset, sub_offset, sub_size, sf) != sub_size) + return false; + if (!is_new) + decrypt_chunk(buf + sub_offset, sub_size, curr_key); + //VGM_LOGB(buf, data_offset, 0); + + + /* bus/se config */ + if (type == 0x0000) { + /* 30 number of entries (after data offset) + per entry + 00 channel offset (after all entries) + per channel at the above offset + 00 channel config (similar to channel config in streams but extended) */ + vgm_logi("RedSpark: file has no audio\n"); + return false; + } + + + /* main info */ + uint32_t coef_offset; + if (bank_flag) { + /* bank with N subsongs (seen in M&L sfx packs and in subsparks) */ + /* 00 data size + 0c entries (only 1 is possible) + 0e entries again + 20+ table + per entry: + 00 absolute offset to header + per header + 00 null? + 04 stream size + 08 sample rate (null in subspark) + 0c nibbles (old) or samples (new), (null in subspark so uses loops) + 10 stream offset (from data_offset) + 14 loop end (num samples if non-looped) + 18 loop start of -1 it not looped + 20 config? + 24 config? + 28 coefs + hists + 5c channel config? + */ + + h->total_subsongs = get_u16(buf + head_pos + 0x0c); + if (!check_subsongs(&target_subsong, h->total_subsongs)) + return false; + + int target_pos = head_pos + 0x20 + (target_subsong - 1) * 0x04; + if (target_pos + 0x04 >= HEADER_MAX) + return false; + target_pos = get_u32(buf + target_pos) + redspark_pos; + if (target_pos + 0x70 >= HEADER_MAX) + return false; + + h->stream_size = get_u32(buf + target_pos + 0x04); + h->sample_rate = get_u32(buf + target_pos + 0x08); + h->num_samples = get_u32(buf + target_pos + 0x0c); + h->stream_offset = get_u32(buf + target_pos + 0x10) + data_offset; + h->loop_end = get_u32(buf + target_pos + 0x14); + h->loop_start = get_u32(buf + target_pos + 0x18); + coef_offset = target_pos + 0x28; + + h->channels = 1; + h->loop_flag = (h->loop_start != -1); /* TODO: many files sound kind of odd */ + if (h->num_samples == 0) + h->num_samples = h->loop_end; + if (h->sample_rate == 0) + h->sample_rate = 32000; + + /* empty entry */ + if (h->stream_size == 0) + h->dummy = true; + } + else { + /* stream */ + /* 00 data size (after data offset) + 04 null + 08 null + 0c sample rate + 10 frames (old) or samples (new) + 14 some chunk? (0x10000, 0xc000) + 18 null + 1c null + 1e channels (usually 1-2, sometimes 4 in MW) + 1f num loop cues (2 == loop) + 20 cues again? + 21 volume? + 22 null + 24+ variable + per channel + 00 config per channel, size 0x08 (number/panning/volume/etc?) + per loop point + 00 chunk size? + 04 value + per channel + 00 dsp coefs + hists (size 0x2E) + if cues: + 00 offset? + 04 null + per cue: + 00 name size + 01 string ("Loop Start" / "Loop End") + */ + + h->sample_rate = get_u32(buf + head_pos + 0x0c); + h->num_samples = get_u32(buf + head_pos + 0x10); + h->channels = get_u8(buf + head_pos + 0x1e); + int loop_cues = get_u8(buf + head_pos + 0x1f); + + head_pos += 0x24; + + /* just in case to avoid bad reads outside buf */ + if (h->channels > MAX_CHANNELS) + return false; + head_pos += h->channels * 0x08; + + h->loop_flag = (loop_cues != 0); + if (h->loop_flag) { + /* only two cue points */ + if (loop_cues != 0 && loop_cues != 2) + return false; + h->loop_start = get_u32(buf + head_pos + 0x04); + h->loop_end = get_u32(buf + head_pos + 0x0c); + head_pos += 0x10; + } + + coef_offset = head_pos; + h->stream_offset = data_offset; + h->stream_size = data_size; + } + + /* coefs from decrypted buf (could read hist but it's 0 in DSPs) */ + for (int ch = 0; ch < h->channels; ch++) { + for (int i = 0; i < 16; i++) { + h->coefs[ch][i] = get_u16(buf + coef_offset + 0x2e * ch + i * 2); + } + } + + /* fixes */ + if (!is_new) { + h->loop_end += 1; + + if (bank_flag) { + h->num_samples /= 2 * 0x8; + h->loop_start /= 2 * 0x8; + h->loop_end /= 2 * 0x8; + } + + h->num_samples *= 14; + h->loop_start *= 14; + h->loop_end *= 14; + + if (h->loop_end > h->num_samples) /* needed for some files */ + h->loop_end = h->num_samples; + } + /* new + bank may need +1 */ + + return true; +} diff --git a/src/util/endianness.h b/src/util/endianness.h index ab95f352..0c1d2de4 100644 --- a/src/util/endianness.h +++ b/src/util/endianness.h @@ -13,7 +13,11 @@ typedef uint16_t (*read_u16_t)(off_t, STREAMFILE*); typedef int16_t (*read_s16_t)(off_t, STREAMFILE*); typedef float (*read_f32_t)(off_t, STREAMFILE*); -typedef int16_t (*get_s16_t)(const uint8_t*); +typedef uint16_t (*get_u16_t)(const uint8_t*); +typedef int16_t (*get_s16_t)(const uint8_t*); +typedef uint32_t (*get_u32_t)(const uint8_t*); +typedef int32_t (*get_s32_t)(const uint8_t*); + /* guess byte endianness from a given value, return true if big endian and false if little endian */ static inline int guess_endian16(off_t offset, STREAMFILE* sf) {