From 158c10fc66576a8ee87919951e562eac8a01af0b Mon Sep 17 00:00:00 2001 From: bnnm Date: Sun, 29 Dec 2024 17:33:56 +0100 Subject: [PATCH 01/11] Fix json loop info affecting web player --- cli/vgmstream_cli_utils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/vgmstream_cli_utils.c b/cli/vgmstream_cli_utils.c index 30a68c8e..41722d4b 100644 --- a/cli/vgmstream_cli_utils.c +++ b/cli/vgmstream_cli_utils.c @@ -256,7 +256,7 @@ void print_json_info(VGMSTREAM* vgm, cli_config_t* cfg, const char* vgmstream_ve if (info.loop_info.end > info.loop_info.start) { vjson_obj_open(&j); vjson_keyint(&j, "start", info.loop_info.start); - vjson_keyint(&j, "end", info.loop_info.start); + vjson_keyint(&j, "end", info.loop_info.end); vjson_obj_close(&j); } else { From d61b8f3c4c6bcbb8c6e7828d5d2c329e93578424 Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 14:56:30 +0100 Subject: [PATCH 02/11] RIFF: sample cleanup and fix some labl cases --- src/formats.c | 3 +- src/meta/riff.c | 459 +++++++++++++++++++++++++----------------- src/vgmstream_types.h | 9 +- 3 files changed, 279 insertions(+), 192 deletions(-) diff --git a/src/formats.c b/src/formats.c index 8ca3e58c..e83f6378 100644 --- a/src/formats.c +++ b/src/formats.c @@ -1096,6 +1096,7 @@ static const meta_info meta_info_list[] = { {meta_RIFF_WAVE_labl, "RIFF WAVE header (labl looping)"}, {meta_RIFF_WAVE_smpl, "RIFF WAVE header (smpl looping)"}, {meta_RIFF_WAVE_wsmp, "RIFF WAVE header (wsmp looping)"}, + {meta_RIFF_WAVE_cue_rgn, "RIFF WAVE header (cue/rgn looping)"}, {meta_RIFX_WAVE, "RIFX WAVE header"}, {meta_RIFX_WAVE_smpl, "RIFX WAVE header (smpl looping)"}, {meta_XNB, "Microsoft XNA Game Studio header"}, @@ -1150,7 +1151,7 @@ static const meta_info meta_info_list[] = { {meta_P2BT_MOVE_VISA, "Konami P2BT/MOVE/VISA header"}, {meta_GBTS, "Konami GBTS header"}, {meta_NGC_DSP_IADP, "IADP Header"}, - {meta_RIFF_WAVE_MWV, "RIFF WAVE header (ctrl looping)"}, + {meta_RIFF_WAVE_ctrl, "RIFF WAVE header (ctrl looping)"}, {meta_FFCC_STR, "Final Fantasy: Crystal Chronicles STR header"}, {meta_SAT_BAKA, "Konami BAKA header"}, {meta_SWAV, "Nintendo SWAV header"}, diff --git a/src/meta/riff.c b/src/meta/riff.c index 2d4ceda4..4b29b84c 100644 --- a/src/meta/riff.c +++ b/src/meta/riff.c @@ -9,72 +9,129 @@ /* RIFF - Resource Interchange File Format, standard container used in many games */ -/* return milliseconds */ -static long parse_adtl_marker(unsigned char* marker) { - long hh,mm,ss,ms; +typedef struct { + bool loop_flag; - if (memcmp("Marker ",marker,7)) return -1; + bool loop_smpl; + int32_t loop_start_smpl; + int32_t loop_end_smpl; - if (4 != sscanf((char*)marker+7,"%ld:%ld:%ld.%ld",&hh,&mm,&ss,&ms)) + bool loop_labl; + long loop_start_ms; + long loop_end_ms; + + bool loop_cue; + int32_t loop_start_cue; + + bool loop_ctrl; + int32_t loop_start_ctrl; + + bool loop_wsmp; + int32_t loop_start_wsmp; + int32_t loop_end_wsmp; + + bool loop_nxbf; + int32_t loop_start_nxbf; + +} riff_sample_into_t; + + +/* parse "Marker hh:mm:ss.ms" to milliseconds */ +static long parse_adtl_marker_ms(unsigned char* marker) { + int hh, mm, ss, ms; + int n, m; + + if (memcmp("Marker ", marker, 7) != 0) return -1; - return ((hh*60+mm)*60+ss)*1000+ms; + // 00:00:00.NNN, rare (ms as-is) + m = sscanf((char*)marker + 7,"%02d:%02d:%02d.%03d%n", &hh, &mm, &ss, &ms, &n); + if (m == 4 && n == 12) { + return ((hh * 60 + mm) * 60 + ss) * 1000 + ms; + } + + // 00:00:00.NN, common (ms .15 = 150) + m = sscanf((char*)marker + 7,"%02d:%02d:%02d.%02d",&hh,&mm,&ss,&ms); + if (m == 4) { + return ((hh * 60 + mm) * 60 + ss) * 1000 + ms * 10; + } + + return -1; } -/* loop points have been found hiding here */ -static void parse_adtl(off_t adtl_offset, off_t adtl_length, STREAMFILE* sf, long* loop_start, long* loop_end, int* loop_flag) { - int loop_start_found = 0; - int loop_end_found = 0; - off_t current_chunk = adtl_offset+0x04; +/* loop points have been found hiding here (ex. set by Sound Forge) */ +static void parse_adtl(uint32_t adtl_offset, uint32_t adtl_length, STREAMFILE* sf, riff_sample_into_t* si) { + bool loop_start_found = false; + bool loop_end_found = false; + uint32_t current_chunk = adtl_offset + 0x04; + unsigned char label_content[128]; //arbitrary max while (current_chunk < adtl_offset + adtl_length) { - uint32_t chunk_type = read_u32be(current_chunk+0x00,sf); - uint32_t chunk_size = read_u32le(current_chunk+0x04,sf); + uint32_t chunk_type = read_u32be(current_chunk + 0x00,sf); + uint32_t chunk_size = read_u32le(current_chunk + 0x04,sf); - if (current_chunk+0x08+chunk_size > adtl_offset+adtl_length) - return; + if (current_chunk + 0x08 + chunk_size > adtl_offset + adtl_length) { + return; // broken adtl? + } switch(chunk_type) { - case 0x6c61626c: { /* "labl" */ - unsigned char *labelcontent = malloc(chunk_size-0x04); - if (!labelcontent) return; - if (read_streamfile(labelcontent,current_chunk+0x0c, chunk_size-0x04,sf) != chunk_size-0x04) { - free(labelcontent); - return; - } + case 0x6c61626c: { /* "labl" [Advanced Power Dolls 2 (PC), Redline (PC)] */ + int label_size = chunk_size - 0x04; + if (label_size >= sizeof(label_content)) + break; - switch (read_32bitLE(current_chunk+8,sf)) { + int cue_id = read_s32le(current_chunk + 0x08 + 0x00,sf); + if (read_streamfile(label_content, current_chunk + 0x08 + 0x04, label_size,sf) != label_size) + return; + label_content[label_size] = '\0'; // labl null-terminates but just in case + + int loop_value = parse_adtl_marker_ms(label_content); + if (loop_value < 0) + break; + + switch (cue_id) { case 1: - if (!loop_start_found && (*loop_start = parse_adtl_marker(labelcontent)) >= 0) - loop_start_found = 1; + if (loop_start_found) + break; + si->loop_start_ms = loop_value; + loop_start_found = (loop_value >= 0); break; case 2: - if (!loop_end_found && (*loop_end = parse_adtl_marker(labelcontent)) >= 0) - loop_end_found = 1; + if (loop_end_found) + break; + si->loop_end_ms = loop_value; + loop_end_found = (loop_value >= 0); break; default: break; } - free(labelcontent); break; } - default: + + default: // "note" also exists break; } - current_chunk += 8 + chunk_size; + /* chunks are even-adjusted like main RIFF chunks */ + if (chunk_size % 0x02 && current_chunk + 0x08 + chunk_size + 0x01 <= adtl_offset + adtl_length) + chunk_size += 0x01; + + current_chunk += 0x08 + chunk_size; } - if (loop_start_found && loop_end_found) - *loop_flag = 1; + if (loop_start_found && loop_end_found) { + si->loop_labl = true; + si->loop_flag = true; + } /* labels don't seem to be consistently ordered */ - if (*loop_start > *loop_end) { - long temp = *loop_start; - *loop_start = *loop_end; - *loop_end = temp; + if (si->loop_start_ms > si->loop_end_ms) { + long temp = si->loop_start_ms; + si->loop_start_ms = si->loop_end_ms; + si->loop_end_ms = temp; } + } typedef struct { @@ -91,9 +148,9 @@ typedef struct { int coding_type; int interleave; - int is_at3; - int is_at3p; - int is_at9; + bool is_at3; + bool is_at3p; + bool is_at9; } riff_fmt_chunk; static int read_fmt(int big_endian, STREAMFILE* sf, off_t offset, riff_fmt_chunk* fmt) { @@ -264,7 +321,7 @@ static int read_fmt(int big_endian, STREAMFILE* sf, off_t offset, riff_fmt_chunk #ifdef VGM_USE_FFMPEG case 0x0270: /* ATRAC3 */ fmt->coding_type = coding_FFmpeg; - fmt->is_at3 = 1; + fmt->is_at3 = true; break; #endif @@ -293,7 +350,7 @@ static int read_fmt(int big_endian, STREAMFILE* sf, off_t offset, riff_fmt_chunk if (guid1 == 0xE923AABF && guid2 == 0xCB584471 && guid3 == 0xA119FFFA && guid4 == 0x01E4CE62) { #ifdef VGM_USE_FFMPEG fmt->coding_type = coding_FFmpeg; - fmt->is_at3p = 1; + fmt->is_at3p = true; break; #else goto fail; @@ -304,7 +361,7 @@ static int read_fmt(int big_endian, STREAMFILE* sf, off_t offset, riff_fmt_chunk /* ATRAC9 GUID (0x47E142D2,36BA,4D8D,88,FC,61,65,4F,8C,83,6C) */ if (guid1 == 0x47E142D2 && guid2 == 0x36BA4D8D && guid3 == 0x88FC6165 && guid4 == 0x4F8C836C) { fmt->coding_type = coding_ATRAC9; - fmt->is_at9 = 1; + fmt->is_at9 = true; break; } #endif @@ -330,26 +387,7 @@ static size_t get_ue4_msadpcm_interleave(STREAMFILE* sf, riff_fmt_chunk* fmt, of VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { VGMSTREAM* vgmstream = NULL; - riff_fmt_chunk fmt = {0}; - size_t file_size, riff_size, data_size = 0; - off_t start_offset = 0; - - int fact_sample_count = 0; - int fact_sample_skip = 0; - - int loop_flag = 0; - long loop_start_ms = -1, loop_end_ms = -1; - int32_t loop_start_wsmp = -1, loop_end_wsmp = -1; - int32_t loop_start_smpl = -1, loop_end_smpl = -1; - int32_t loop_start_cue = -1; - int32_t loop_start_nxbf = -1; - - int FormatChunkFound = 0, DataChunkFound = 0, JunkFound = 0; - - off_t mwv_pflt_offset = 0; - off_t mwv_ctrl_offset = 0; - int ignore_riff_size = 0; /* checks*/ @@ -361,7 +399,15 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { if (!is_id32be(0x08,sf, "WAVE")) return NULL; - file_size = get_streamfile_size(sf); + + riff_fmt_chunk fmt = {0}; + riff_sample_into_t si = {0}; + off_t start_offset = 0; + off_t mwv_pflt_offset = 0; + + int fact_sample_count = 0; + int fact_sample_skip = 0; + /* .lwav: to avoid hijacking .wav * .xwav: fake for Xbox games (not needed anymore) @@ -408,7 +454,12 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { return NULL; } + + bool fmt_chunk_found = false, data_chunk_found = false, junk_chunk_found = false; + bool ignore_riff_size = false; + /* some games have wonky sizes, selectively fix to catch bad rips and new mutations */ + file_size = get_streamfile_size(sf); if (file_size != riff_size + 0x08) { uint16_t codec = read_u16le(0x14,sf); @@ -450,10 +501,10 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { } } - else if (riff_size >= file_size && read_32bitBE(0x24,sf) == 0x4E584246) /* "NXBF" */ + else if (riff_size >= file_size && is_id32be(0x24,sf, "NXBF")) riff_size = file_size - 0x08; /* [R:Racing Evolution (Xbox)] */ - else if (codec == 0x0011 && (riff_size / 2 / 2 == read_32bitLE(0x30,sf))) /* riff_size = pcm_size (always stereo, has fact at 0x30) */ + else if (codec == 0x0011 && (riff_size / 2 / 2 == read_u32le(0x30,sf))) /* riff_size = pcm_size (always stereo, has fact at 0x30) */ riff_size = file_size - 0x08; /* [Asphalt 6 (iOS)] (sfx/memory wavs have ok sizes?) */ else if (codec == 0xFFFE && riff_size + 0x08 + 0x30 == file_size) @@ -485,6 +536,7 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { goto fail; } + /* read through chunks to verify format and find metadata */ { uint32_t current_chunk = 0x0c; /* start with first chunk */ @@ -501,8 +553,9 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { switch(chunk_id) { case 0x666d7420: /* "fmt " */ - if (FormatChunkFound) goto fail; /* only one per file */ - FormatChunkFound = 1; + if (fmt_chunk_found) + goto fail; /* only one per file */ + fmt_chunk_found = true; if (!read_fmt(0, sf, current_chunk, &fmt)) goto fail; @@ -513,20 +566,19 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { break; case 0x64617461: /* "data" */ - if (DataChunkFound) goto fail; /* only one per file */ - DataChunkFound = 1; + if (data_chunk_found) + goto fail; /* only one per file */ + data_chunk_found = true; start_offset = current_chunk + 0x08; data_size = chunk_size; break; case 0x4C495354: /* "LIST" */ - switch (read_32bitBE(current_chunk+0x08, sf)) { + switch (read_u32be(current_chunk+0x08, sf)) { case 0x6164746C: /* "adtl" */ /* yay, atdl is its own little world */ - parse_adtl(current_chunk + 0x8, chunk_size, - sf, - &loop_start_ms,&loop_end_ms,&loop_flag); + parse_adtl(current_chunk + 0x08, chunk_size, sf, &si); break; default: break; @@ -534,57 +586,85 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { break; case 0x736D706C: /* "smpl" (RIFFMIDISample + MIDILoop chunk) */ - /* check loop count/loop info (most common) */ - /* 0x00: manufacturer id, 0x04: product id, 0x08: sample period, 0x0c: unity node, - * 0x10: pitch fraction, 0x14: SMPTE format, 0x18: SMPTE offset, 0x1c: loop count, 0x20: sampler data */ - if (read_32bitLE(current_chunk+0x08+0x1c, sf) == 1) { /* handle only one loop (could contain N MIDILoop) */ - /* 0x24: cue point id, 0x28: type (0=forward, 1=alternating, 2=backward) - * 0x2c: start, 0x30: end, 0x34: fraction, 0x38: play count */ - if (read_32bitLE(current_chunk+0x08+0x28, sf) == 0) { /* loop forward */ - loop_flag = 1; - loop_start_smpl = read_32bitLE(current_chunk+0x08+0x2c, sf); - loop_end_smpl = read_32bitLE(current_chunk+0x08+0x30, sf) + 1; /* must add 1 as per spec (ok for standard WAV/AT3/AT9) */ - } + /* check loop count/loop info (most fields are reserved for midi and null/irrelevant for RIFF) */ + // 0x00: manufacturer id + // 0x04: product id + // 0x08: sample period + // 0x0c: unity node + // 0x10: pitch fraction + // 0x14: SMPTE format + // 0x18: SMPTE offset + // 0x1c: loop count (may contain N MIDILoop) + // 0x20: sampler data + // 0x24: per loop point: + // 0x00: cue point id + // 0x04: type (0=forward, 1=alternating, 2=backward) + // 0x08: loop start + // 0x0c: loop end + // 0x10: fraction + // 0x14: play count + if (read_u32le(current_chunk + 0x08 + 0x1c, sf) != 1) { /* handle only 1 loop */ + VGM_LOG("RIFF: found multiple smpl loop points, ignoring\n"); + break; + } + + if (read_u32le(current_chunk + 0x08 + 0x24 + 0x04, sf) == 0) { /* loop forward */ + si.loop_start_smpl = read_s32le(current_chunk + 0x08 + 0x24 + 0x08, sf); + si.loop_end_smpl = read_s32le(current_chunk + 0x08 + 0x24 + 0x0c, sf) + 1; /* must add 1 as per spec (ok for standard WAV/AT3/AT9) */ + si.loop_smpl = true; + si.loop_flag = true; } break; case 0x77736D70: /* "wsmp" (RIFFDLSSample + DLSLoop chunk) */ - /* check loop count/info (found in some Xbox games: Halo (non-looping), Dynasty Warriors 3, Crimson Sea) */ - /* 0x00: size, 0x04: unity note, 0x06: fine tune, 0x08: gain, 0x10: loop count */ - if (chunk_size >= 0x24 - && read_32bitLE(current_chunk+0x08+0x00, sf) == 0x14 - && read_32bitLE(current_chunk+0x08+0x10, sf) > 0 - && read_32bitLE(current_chunk+0x08+0x14, sf) == 0x10) { - /* 0x14: size, 0x18: loop type (0=forward, 1=release), 0x1c: loop start, 0x20: loop length */ - if (read_32bitLE(current_chunk+0x08+0x18, sf) == 0) { /* loop forward */ - loop_flag = 1; - loop_start_wsmp = read_32bitLE(current_chunk+0x08+0x1c, sf); - loop_end_wsmp = read_32bitLE(current_chunk+0x08+0x20, sf); /* must not add 1 as per spec */ - loop_end_wsmp += loop_start_wsmp; - } + /* check loop count/info (found in some Xbox games: Halo (non-looping), Dynasty Warriors 3/4/5, Crimson Sea) */ + // 0x00: size + // 0x04: unity note + // 0x06: fine tune + // 0x08: gain + // 0x10: loop count + // 0x14: per loop: + // 0x00: size + // 0x04: loop type (0=forward, 1=release) + // 0x08: loop start + // 0x0c: loop length + if (chunk_size < 0x24 + || read_u32le(current_chunk + 0x08 + 0x00, sf) != 0x14 + || read_s32le(current_chunk + 0x08 + 0x10, sf) <= 0 + || read_u32le(current_chunk + 0x08 + 0x14, sf) != 0x10) { + VGM_LOG("RIFF: found incorrect wsmp loop points, ignoring\n"); + break; + } + + if (read_u32le(current_chunk + 0x08 + 0x14 + 0x04, sf) == 0) { /* loop forward */ + si.loop_start_wsmp = read_s32le(current_chunk + 0x08 + 0x14 + 0x08, sf); + si.loop_end_wsmp = read_s32le(current_chunk + 0x08 + 0x14 + 0x0c, sf); /* must *not* add 1 as per spec (region) */ + si.loop_end_wsmp += si.loop_start_wsmp; + si.loop_wsmp = true; + si.loop_flag = true; } break; case 0x66616374: /* "fact" */ if (chunk_size == 0x04) { /* standard (usually for ADPCM, MS recommends setting for non-PCM codecs but optional) */ - fact_sample_count = read_32bitLE(current_chunk+0x08, sf); + fact_sample_count = read_s32le(current_chunk+0x08, sf); } else if (chunk_size == 0x10 && is_id32be(current_chunk+0x08+0x04, sf, "LyN ")) { goto fail; /* parsed elsewhere */ } else if ((fmt.is_at3 || fmt.is_at3p) && chunk_size == 0x08) { /* early AT3 (mainly PSP games) */ - fact_sample_count = read_32bitLE(current_chunk+0x08, sf); - fact_sample_skip = read_32bitLE(current_chunk+0x0c, sf); /* base skip samples */ + fact_sample_count = read_s32le(current_chunk+0x08, sf); + fact_sample_skip = read_s32le(current_chunk+0x0c, sf); /* base skip samples */ } else if ((fmt.is_at3 || fmt.is_at3p) && chunk_size == 0x0c) { /* late AT3 (mainly PS3 games and few PSP games) */ - fact_sample_count = read_32bitLE(current_chunk+0x08, sf); + fact_sample_count = read_s32le(current_chunk+0x08, sf); /* 0x0c: base skip samples, ignored by decoder */ - fact_sample_skip = read_32bitLE(current_chunk+0x10, sf); /* skip samples with extra 184 */ + fact_sample_skip = read_s32le(current_chunk+0x10, sf); /* skip samples with extra 184 */ } else if (fmt.is_at9 && chunk_size == 0x0c) { - fact_sample_count = read_32bitLE(current_chunk+0x08, sf); + fact_sample_count = read_s32le(current_chunk+0x08, sf); /* 0x0c: base skip samples (same as next field) */ - fact_sample_skip = read_32bitLE(current_chunk+0x10, sf); + fact_sample_skip = read_s32le(current_chunk+0x10, sf); } break; @@ -596,44 +676,47 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { break; case 0x6374726c: /* "ctrl" (.mwv extension) */ - loop_flag = read_32bitLE(current_chunk+0x08, sf); - mwv_ctrl_offset = current_chunk; + si.loop_flag = read_s32le(current_chunk + 0x08 + 0x00, sf); + si.loop_start_ctrl = read_s32le(current_chunk + 0x08 + 0x04, sf); + si.loop_ctrl = true; break; - case 0x63756520: /* "cue " (used in Source Engine for storing loop points) */ + case 0x63756520: /* "cue " (used in Source Engine, also seen cue + adtl rgn in Sound Forge) [Team Fortress 2 (PC)] */ if (fmt.coding_type == coding_PCM8_U || fmt.coding_type == coding_PCM16LE || fmt.coding_type == coding_MSADPCM) { - uint32_t num_cues = read_32bitLE(current_chunk + 0x08, sf); + uint32_t num_cues = read_s32le(current_chunk + 0x08, sf); if (num_cues > 0) { /* the second cue sets loop end point but it's not actually used by the engine */ - loop_flag = 1; - loop_start_cue = read_32bitLE(current_chunk + 0x20, sf); + si.loop_start_cue = read_s32le(current_chunk + 0x20, sf); + si.loop_cue = true; + si.loop_flag = true; } } break; case 0x4E584246: /* "NXBF" (Namco NuSound v1) [R:Racing Evolution (Xbox)] */ /* very similar to NUS's NPSF, but not quite like Cstr */ - /* 0x00: "NXBF" id */ - /* 0x04: version? (0x00001000 = 1.00?) */ - /* 0x08: data size */ - /* 0x0c: channels */ - /* 0x10: null */ - loop_start_nxbf = read_32bitLE(current_chunk + 0x08 + 0x14, sf); - /* 0x18: sample rate */ - /* 0x1c: volume? (0x3e8 = 1000 = max) */ - /* 0x20: type/flags? */ - /* 0x24: flag? */ - /* 0x28: null */ - /* 0x2c: null */ - /* 0x30: always 0x40 */ - loop_flag = (loop_start_nxbf >= 0); + // 0x00: "NXBF" id + // 0x04: version? (0x00001000 = 1.00?) + // 0x08: data size + // 0x0c: channels + // 0x10: null + si.loop_start_nxbf = read_s32le(current_chunk + 0x08 + 0x14, sf); + // 0x18: sample rate + // 0x1c: volume? (0x3e8 = 1000 = max) + // 0x20: type/flags? + // 0x24: flag? + // 0x28: null + // 0x2c: null + // 0x30: always 0x40 + si.loop_nxbf = true; + si.loop_flag = (si.loop_start_nxbf >= 0); break; case 0x4A554E4B: /* "JUNK" */ - JunkFound = 1; + junk_chunk_found = true; break; @@ -654,23 +737,26 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { current_chunk += 0x08 + chunk_size; } + + if (!fmt_chunk_found || !data_chunk_found) + goto fail; } - if (!FormatChunkFound || !DataChunkFound) goto fail; - //todo improve detection using fmt sizes/values as Wwise's don't match the RIFF standard + //todo improve detection using fmt sizes/values as Wwise's don't match the RIFF standard for some codecs /* JUNK is an optional Wwise chunk, and Wwise hijacks the MSADPCM/MS_IMA/XBOX IMA ids (how nice). * To ensure their stuff is parsed in wwise.c we reject their JUNK, which they put almost always. * As JUNK is legal (if unusual) we only reject those codecs. * (ex. Cave PC games have PCM16LE + JUNK + smpl created by "Samplitude software") */ - if (JunkFound + if (junk_chunk_found + && (fmt.coding_type == coding_MSADPCM || fmt.coding_type == coding_XBOX_IMA /*|| fmt.coding_type==coding_MS_IMA*/) && check_extensions(sf,"wav,lwav") /* for some .MED IMA */ - && (fmt.coding_type==coding_MSADPCM /*|| fmt.coding_type==coding_MS_IMA*/ || fmt.coding_type==coding_XBOX_IMA)) + ) goto fail; /* ignore Beyond Good & Evil HD PS3 evil reuse of PCM codec */ if (fmt.coding_type == coding_PCM16LE && - read_u32be(start_offset+0x00, sf) == 0x4D534643 && /* "MSF\43" */ + read_u32be(start_offset+0x00, sf) == get_id32be("MSF\x43") && read_u32be(start_offset+0x34, sf) == 0xFFFFFFFF && /* always */ read_u32be(start_offset+0x38, sf) == 0xFFFFFFFF && read_u32be(start_offset+0x3c, sf) == 0xFFFFFFFF) @@ -681,13 +767,13 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { goto fail; /* ignore Gitaroo Man Live! (PSP) multi-RIFF (to allow chunked TXTH) */ - if (fmt.is_at3 && get_streamfile_size(sf) > 0x2800 && read_32bitBE(0x2800, sf) == 0x52494646) { /* "RIFF" */ + if (fmt.is_at3 && get_streamfile_size(sf) > 0x2800 && read_u32be(0x2800, sf) == get_id32be("RIFF")) { goto fail; } /* build the VGMSTREAM */ - vgmstream = allocate_vgmstream(fmt.channels,loop_flag); + vgmstream = allocate_vgmstream(fmt.channels, si.loop_flag); if (!vgmstream) goto fail; vgmstream->sample_rate = fmt.sample_rate; @@ -739,30 +825,28 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { vgmstream->num_samples = pcm_bytes_to_samples(data_size, fmt.channels, fmt.bps); break; - case coding_LEVEL5: + case coding_LEVEL5: { vgmstream->num_samples = data_size / 0x12 / fmt.channels * 32; /* coefs */ - { - int i, ch; - const int filter_order = 3; - int filter_count = read_32bitLE(mwv_pflt_offset+0x0c, sf); - if (filter_count > 0x20) goto fail; + const int filter_order = 3; + int filter_count = read_s32le(mwv_pflt_offset+0x0c, sf); + if (filter_count > 0x20) goto fail; - if (!mwv_pflt_offset || - read_32bitLE(mwv_pflt_offset+0x08, sf) != filter_order || - read_32bitLE(mwv_pflt_offset+0x04, sf) < 8 + filter_count * 4 * filter_order) - goto fail; + if (!mwv_pflt_offset || + read_s32le(mwv_pflt_offset+0x08, sf) != filter_order || + read_s32le(mwv_pflt_offset+0x04, sf) < 8 + filter_count * 4 * filter_order) + goto fail; - for (ch = 0; ch < fmt.channels; ch++) { - for (i = 0; i < filter_count * filter_order; i++) { - int coef = read_32bitLE(mwv_pflt_offset+0x10+i*0x04, sf); - vgmstream->ch[ch].adpcm_coef_3by32[i] = coef; - } + for (int ch = 0; ch < fmt.channels; ch++) { + for (int i = 0; i < filter_count * filter_order; i++) { + int coef = read_s32le(mwv_pflt_offset+0x10+i*0x04, sf); + vgmstream->ch[ch].adpcm_coef_3by32[i] = coef; } } break; + } case coding_MSADPCM: vgmstream->num_samples = msadpcm_bytes_to_samples(data_size, fmt.block_size, fmt.channels); @@ -800,14 +884,14 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { if (!vgmstream->codec_data) goto fail; vgmstream->num_samples = fact_sample_count; - if (loop_flag) { + if (si.loop_flag) { /* adjust RIFF loop/sample absolute values (with skip samples) */ - loop_start_smpl -= fact_sample_skip; - loop_end_smpl -= fact_sample_skip; + si.loop_start_smpl -= fact_sample_skip; + si.loop_end_smpl -= fact_sample_skip; /* happens with official tools when "fact" is not found */ if (vgmstream->num_samples == 0) - vgmstream->num_samples = loop_end_smpl; + vgmstream->num_samples = si.loop_end_smpl; } break; @@ -818,7 +902,7 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { atrac9_config cfg = {0}; cfg.channels = vgmstream->channels; - cfg.config_data = read_32bitBE(fmt.offset+0x08+0x2c,sf); + cfg.config_data = read_u32be(fmt.offset+0x08+0x2c,sf); cfg.encoder_delay = fact_sample_skip; vgmstream->codec_data = init_atrac9(&cfg); @@ -826,9 +910,9 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { vgmstream->num_samples = fact_sample_count; /* RIFF loop/sample values are absolute (with skip samples), adjust */ - if (loop_flag) { - loop_start_smpl -= fact_sample_skip; - loop_end_smpl -= fact_sample_skip; + if (si.loop_flag) { + si.loop_start_smpl -= fact_sample_skip; + si.loop_end_smpl -= fact_sample_skip; } break; @@ -893,39 +977,41 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { /* meta, loops */ vgmstream->meta_type = meta_RIFF_WAVE; - if (loop_flag) { - if (loop_start_ms >= 0) { - vgmstream->loop_start_sample = (long long)loop_start_ms*fmt.sample_rate/1000; - vgmstream->loop_end_sample = (long long)loop_end_ms*fmt.sample_rate/1000; - vgmstream->meta_type = meta_RIFF_WAVE_labl; - } - else if (loop_start_smpl >= 0) { - vgmstream->loop_start_sample = loop_start_smpl; - vgmstream->loop_end_sample = loop_end_smpl; - /* end must add +1, but check in case of faulty tools */ + if (si.loop_flag) { + /* order matters as tools may rarely include multiple chunks (like smpl + cue/rgn) [Redline (PC)] */ + if (si.loop_smpl) { + vgmstream->loop_start_sample = si.loop_start_smpl; + vgmstream->loop_end_sample = si.loop_end_smpl; + vgmstream->meta_type = meta_RIFF_WAVE_smpl; + + // end adds +1 as per spec, but check in case of faulty tools if (vgmstream->loop_end_sample - 1 == vgmstream->num_samples) vgmstream->loop_end_sample--; - - vgmstream->meta_type = meta_RIFF_WAVE_smpl; } - else if (loop_start_wsmp >= 0) { - vgmstream->loop_start_sample = loop_start_wsmp; - vgmstream->loop_end_sample = loop_end_wsmp; + else if (si.loop_labl && si.loop_start_ms >= 0) { + vgmstream->loop_start_sample = (long long)si.loop_start_ms * fmt.sample_rate / 1000; + vgmstream->loop_end_sample = (long long)si.loop_end_ms * fmt.sample_rate / 1000; + vgmstream->meta_type = meta_RIFF_WAVE_labl; + } + else if (si.loop_cue) { + vgmstream->loop_start_sample = si.loop_start_cue; + vgmstream->loop_end_sample = vgmstream->num_samples; + vgmstream->meta_type = meta_RIFF_WAVE_cue_rgn; + } + else if (si.loop_ctrl && fmt.coding_type == coding_LEVEL5) { + vgmstream->loop_start_sample = si.loop_start_ctrl; + vgmstream->loop_end_sample = vgmstream->num_samples; + vgmstream->meta_type = meta_RIFF_WAVE_ctrl; + } + else if (si.loop_wsmp) { + vgmstream->loop_start_sample = si.loop_start_wsmp; + vgmstream->loop_end_sample = si.loop_end_wsmp; vgmstream->meta_type = meta_RIFF_WAVE_wsmp; } - else if (fmt.coding_type == coding_LEVEL5 && mwv_ctrl_offset) { - vgmstream->loop_start_sample = read_s32le(mwv_ctrl_offset + 0x0c, sf); - vgmstream->loop_end_sample = vgmstream->num_samples; - vgmstream->meta_type = meta_RIFF_WAVE_MWV; - } - else if (loop_start_cue != -1) { - vgmstream->loop_start_sample = loop_start_cue; - vgmstream->loop_end_sample = vgmstream->num_samples; - } - else if (loop_start_nxbf != -1) { + else if (si.loop_nxbf) { switch (fmt.coding_type) { case coding_PCM16LE: - vgmstream->loop_start_sample = pcm_bytes_to_samples(loop_start_nxbf, vgmstream->channels, 16); + vgmstream->loop_start_sample = pcm16_bytes_to_samples(si.loop_start_nxbf, vgmstream->channels); vgmstream->loop_end_sample = vgmstream->num_samples; break; default: @@ -936,7 +1022,6 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { if (!vgmstream_open_stream(vgmstream, sf, start_offset)) goto fail; - return vgmstream; fail: @@ -1117,8 +1202,8 @@ VGMSTREAM* init_vgmstream_rifx(STREAMFILE* sf) { off_t current_chunk = 0xc; /* start with first chunk */ while (current_chunk < file_size && current_chunk < riff_size+8) { - uint32_t chunk_type = read_32bitBE(current_chunk,sf); - off_t chunk_size = read_32bitBE(current_chunk+4,sf); + uint32_t chunk_type = read_u32be(current_chunk,sf); + off_t chunk_size = read_u32be(current_chunk+4,sf); if (current_chunk+8+chunk_size > file_size) goto fail; @@ -1142,11 +1227,11 @@ VGMSTREAM* init_vgmstream_rifx(STREAMFILE* sf) { break; case 0x736D706C: /* smpl */ /* check loop count and loop info */ - if (read_32bitBE(current_chunk+0x24, sf)==1) { - if (read_32bitBE(current_chunk+0x2c+4, sf)==0) { + if (read_u32be(current_chunk+0x24, sf)==1) { + if (read_u32be(current_chunk+0x2c+4, sf)==0) { loop_flag = 1; - loop_start_offset = read_32bitBE(current_chunk+0x2c+8, sf); - loop_end_offset = read_32bitBE(current_chunk+0x2c+0xc,sf) + 1; + loop_start_offset = read_u32be(current_chunk+0x2c+8, sf); + loop_end_offset = read_u32be(current_chunk+0x2c+0xc,sf) + 1; } } break; diff --git a/src/vgmstream_types.h b/src/vgmstream_types.h index 96eb0586..cc72f5bc 100644 --- a/src/vgmstream_types.h +++ b/src/vgmstream_types.h @@ -408,10 +408,11 @@ typedef enum { meta_WS_AUD, meta_RIFF_WAVE, /* RIFF, for WAVs */ meta_RIFF_WAVE_POS, /* .wav + .pos for looping (Ys Complete PC) */ - meta_RIFF_WAVE_labl, /* RIFF w/ loop Markers in LIST-adtl-labl */ - meta_RIFF_WAVE_smpl, /* RIFF w/ loop data in smpl chunk */ - meta_RIFF_WAVE_wsmp, /* RIFF w/ loop data in wsmp chunk */ - meta_RIFF_WAVE_MWV, /* .mwv RIFF w/ loop data in ctrl chunk pflt */ + meta_RIFF_WAVE_labl, + meta_RIFF_WAVE_smpl, + meta_RIFF_WAVE_wsmp, + meta_RIFF_WAVE_ctrl, + meta_RIFF_WAVE_cue_rgn, meta_RIFX_WAVE, /* RIFX, for big-endian WAVs */ meta_RIFX_WAVE_smpl, /* RIFX w/ loop data in smpl chunk */ meta_XNB, /* XNA Game Studio 4.0 */ From 0cddd959cdad19116d7958f673ee50e75be9cc99 Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 16:36:36 +0100 Subject: [PATCH 03/11] Add RIFF rgn loops [Touhou Suimusou (PC)] --- src/formats.c | 4 +- src/meta/riff.c | 133 +++++++++++++++++++++++++++++++++--------- src/vgmstream_types.h | 2 +- 3 files changed, 108 insertions(+), 31 deletions(-) diff --git a/src/formats.c b/src/formats.c index e83f6378..c3dfe85a 100644 --- a/src/formats.c +++ b/src/formats.c @@ -1093,10 +1093,10 @@ static const meta_info meta_info_list[] = { {meta_VIG_KCES, "Konami .VIG header"}, {meta_HXD, "Tecmo HXD header"}, {meta_VSV, "Square Enix .vsv Header"}, - {meta_RIFF_WAVE_labl, "RIFF WAVE header (labl looping)"}, {meta_RIFF_WAVE_smpl, "RIFF WAVE header (smpl looping)"}, {meta_RIFF_WAVE_wsmp, "RIFF WAVE header (wsmp looping)"}, - {meta_RIFF_WAVE_cue_rgn, "RIFF WAVE header (cue/rgn looping)"}, + {meta_RIFF_WAVE_labl, "RIFF WAVE header (labl looping)"}, + {meta_RIFF_WAVE_cue, "RIFF WAVE header (cue looping)"}, {meta_RIFX_WAVE, "RIFX WAVE header"}, {meta_RIFX_WAVE_smpl, "RIFX WAVE header (smpl looping)"}, {meta_XNB, "Microsoft XNA Game Studio header"}, diff --git a/src/meta/riff.c b/src/meta/riff.c index 4b29b84c..86a66e48 100644 --- a/src/meta/riff.c +++ b/src/meta/riff.c @@ -16,12 +16,16 @@ typedef struct { int32_t loop_start_smpl; int32_t loop_end_smpl; + bool loop_cue; + int32_t loop_start_cue; + int32_t loop_end_cue; + bool loop_labl; long loop_start_ms; long loop_end_ms; - bool loop_cue; - int32_t loop_start_cue; + bool loop_rgn; + long loop_region_size; bool loop_ctrl; int32_t loop_start_ctrl; @@ -61,8 +65,8 @@ static long parse_adtl_marker_ms(unsigned char* marker) { /* loop points have been found hiding here (ex. set by Sound Forge) */ static void parse_adtl(uint32_t adtl_offset, uint32_t adtl_length, STREAMFILE* sf, riff_sample_into_t* si) { - bool loop_start_found = false; - bool loop_end_found = false; + bool labl_start_found = false; + bool labl_end_found = false; uint32_t current_chunk = adtl_offset + 0x04; unsigned char label_content[128]; //arbitrary max @@ -85,22 +89,23 @@ static void parse_adtl(uint32_t adtl_offset, uint32_t adtl_length, STREAMFILE* s return; label_content[label_size] = '\0'; // labl null-terminates but just in case + // find "Marker", though rarely "loop" or "Region" can be found to mark loop cues [Portal (PC), Touhou Suimusou (PC)] int loop_value = parse_adtl_marker_ms(label_content); if (loop_value < 0) break; switch (cue_id) { case 1: - if (loop_start_found) + if (labl_start_found) break; si->loop_start_ms = loop_value; - loop_start_found = (loop_value >= 0); + labl_start_found = (loop_value >= 0); break; case 2: - if (loop_end_found) + if (labl_end_found) break; si->loop_end_ms = loop_value; - loop_end_found = (loop_value >= 0); + labl_end_found = (loop_value >= 0); break; default: break; @@ -109,6 +114,27 @@ static void parse_adtl(uint32_t adtl_offset, uint32_t adtl_length, STREAMFILE* s break; } + case 0x6C747874: { /* "ltxt" [Touhou Suimusou (PC), Redline (PC)] */ + if (si->loop_rgn) + break; + + int cue_id = read_s32le(current_chunk + 0x08 + 0x00,sf); + int32_t cue_point = read_s32le(current_chunk + 0x08 + 0x04,sf); + if (!is_id32be(current_chunk + 0x08 + 0x08, sf, "rgn ")) + break; + if (cue_id == 1) { + si->loop_rgn = true; + si->loop_region_size = cue_point; + + // assumes cues go first (cue_id should exist?) + if (si->loop_cue && !si->loop_end_cue) { + si->loop_end_cue = si->loop_start_cue + si->loop_region_size; + } + } + + break; + } + default: // "note" also exists break; } @@ -120,11 +146,17 @@ static void parse_adtl(uint32_t adtl_offset, uint32_t adtl_length, STREAMFILE* s current_chunk += 0x08 + chunk_size; } - if (loop_start_found && loop_end_found) { + if (labl_start_found && labl_end_found) { si->loop_labl = true; si->loop_flag = true; } + // in rare cases loop start cue+labl is found, but doesn't seem to mean loop [Caesar III (PC)] + if (labl_start_found && !labl_end_found) { + si->loop_labl = true; + si->loop_flag = false; // 1 cue is treated as loop start, so force as loop end + } + /* labels don't seem to be consistently ordered */ if (si->loop_start_ms > si->loop_end_ms) { long temp = si->loop_start_ms; @@ -610,7 +642,7 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { if (read_u32le(current_chunk + 0x08 + 0x24 + 0x04, sf) == 0) { /* loop forward */ si.loop_start_smpl = read_s32le(current_chunk + 0x08 + 0x24 + 0x08, sf); - si.loop_end_smpl = read_s32le(current_chunk + 0x08 + 0x24 + 0x0c, sf) + 1; /* must add 1 as per spec (ok for standard WAV/AT3/AT9) */ + si.loop_end_smpl = read_s32le(current_chunk + 0x08 + 0x24 + 0x0c, sf); si.loop_smpl = true; si.loop_flag = true; } @@ -681,20 +713,50 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { si.loop_ctrl = true; break; - case 0x63756520: /* "cue " (used in Source Engine, also seen cue + adtl rgn in Sound Forge) [Team Fortress 2 (PC)] */ - if (fmt.coding_type == coding_PCM8_U || - fmt.coding_type == coding_PCM16LE || - fmt.coding_type == coding_MSADPCM) { - uint32_t num_cues = read_s32le(current_chunk + 0x08, sf); + case 0x63756520: { /* "cue " (used in Source Engine, also seen cue + adtl in Sound Forge) [Team Fortress 2 (PC)] */ + if (!(fmt.coding_type == coding_PCM8_U || fmt.coding_type == coding_PCM16LE || fmt.coding_type == coding_MSADPCM)) + break; - if (num_cues > 0) { - /* the second cue sets loop end point but it's not actually used by the engine */ - si.loop_start_cue = read_s32le(current_chunk + 0x20, sf); - si.loop_cue = true; - si.loop_flag = true; + /* handle loop_start or start + end (more are possible but usually means custom regions); + * could have have other meanings but is often used for loops */ + int num_cues = read_s32le(current_chunk + 0x08 + 0x00, sf); + if (num_cues <= 0 || num_cues > 2) + break; + + uint32_t cue_offset = current_chunk + 0x08 + 0x04; + for (int i = 0; i < num_cues; i++) { + // 0x00: id (usually 0x01, 0x02 ... but may be unordered) + // 0x04: position (usually same as sample point) + // 0x08: fourcc type + // 0x0c: "chunk start", relative offset (null in practice) + // 0x10: "block start", relative offset (null in practice) + // 0x14: sample offset + uint32_t cue_id = read_s32le(cue_offset + 0x00, sf); + uint32_t cue_point = read_s32le(cue_offset + 0x14, sf); + cue_offset += 0x18; + + switch (cue_id) { + case 1: + si.loop_start_cue = cue_point; + break; + case 2: + si.loop_end_cue = cue_point; + break; } } + + // cues may be unordered so swap if needed + if (si.loop_end_cue > 0 && si.loop_start_cue > si.loop_end_cue) { + int32_t tmp = si.loop_start_cue; + si.loop_start_cue = si.loop_end_cue; + si.loop_end_cue = tmp; + } + si.loop_cue = true; + si.loop_flag = true; + + /* assumes "cue" goes before "adtl" (has extra detection for some cases) */ break; + } case 0x4E584246: /* "NXBF" (Namco NuSound v1) [R:Racing Evolution (Xbox)] */ /* very similar to NUS's NPSF, but not quite like Cstr */ @@ -891,7 +953,7 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { /* happens with official tools when "fact" is not found */ if (vgmstream->num_samples == 0) - vgmstream->num_samples = si.loop_end_smpl; + vgmstream->num_samples = si.loop_end_smpl + 1; } break; @@ -978,25 +1040,41 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { /* meta, loops */ vgmstream->meta_type = meta_RIFF_WAVE; if (si.loop_flag) { - /* order matters as tools may rarely include multiple chunks (like smpl + cue/rgn) [Redline (PC)] */ - if (si.loop_smpl) { + /* order matters as tools may rarely include multiple chunks (like smpl + cue/adtl) [Redline (PC)] */ + if (si.loop_smpl) { /* most common */ vgmstream->loop_start_sample = si.loop_start_smpl; - vgmstream->loop_end_sample = si.loop_end_smpl; + vgmstream->loop_end_sample = si.loop_end_smpl + 1; vgmstream->meta_type = meta_RIFF_WAVE_smpl; // end adds +1 as per spec, but check in case of faulty tools if (vgmstream->loop_end_sample - 1 == vgmstream->num_samples) vgmstream->loop_end_sample--; } - else if (si.loop_labl && si.loop_start_ms >= 0) { + else if (si.loop_cue && si.loop_labl) { /* [Advanced Power Dolls 2 (PC)] */ + /* favor cues as labels are valid but converted samples are slightly off */ + vgmstream->loop_start_sample = si.loop_start_cue; + vgmstream->loop_end_sample = si.loop_end_cue + 1; + vgmstream->meta_type = meta_RIFF_WAVE_cue; + + // end adds +1 as per spec, but check in case of faulty tools + if (vgmstream->loop_end_sample - 1 == vgmstream->num_samples) + vgmstream->loop_end_sample--; + } + else if (si.loop_cue && si.loop_rgn) { /* [Touhou Suimusou (PC)] */ + vgmstream->loop_start_sample = si.loop_start_cue; + vgmstream->loop_end_sample = si.loop_end_cue; + vgmstream->meta_type = meta_RIFF_WAVE_cue; + } + else if (si.loop_labl && si.loop_start_ms >= 0) { /* possible without cue? */ vgmstream->loop_start_sample = (long long)si.loop_start_ms * fmt.sample_rate / 1000; vgmstream->loop_end_sample = (long long)si.loop_end_ms * fmt.sample_rate / 1000; vgmstream->meta_type = meta_RIFF_WAVE_labl; } - else if (si.loop_cue) { + else if (si.loop_cue) { /* [Team Fortress 2 (PC), Portal (PC)] */ + /* in Source engine ignores the loop end cue; usually doesn't set labl/ltxt (seen "loop" label in Portal) */ vgmstream->loop_start_sample = si.loop_start_cue; vgmstream->loop_end_sample = vgmstream->num_samples; - vgmstream->meta_type = meta_RIFF_WAVE_cue_rgn; + vgmstream->meta_type = meta_RIFF_WAVE_cue; } else if (si.loop_ctrl && fmt.coding_type == coding_LEVEL5) { vgmstream->loop_start_sample = si.loop_start_ctrl; @@ -1290,7 +1368,6 @@ VGMSTREAM* init_vgmstream_rifx(STREAMFILE* sf) { if (!vgmstream_open_stream(vgmstream, sf, start_offset)) goto fail; return vgmstream; - fail: close_vgmstream(vgmstream); return NULL; diff --git a/src/vgmstream_types.h b/src/vgmstream_types.h index cc72f5bc..ec66d657 100644 --- a/src/vgmstream_types.h +++ b/src/vgmstream_types.h @@ -412,7 +412,7 @@ typedef enum { meta_RIFF_WAVE_smpl, meta_RIFF_WAVE_wsmp, meta_RIFF_WAVE_ctrl, - meta_RIFF_WAVE_cue_rgn, + meta_RIFF_WAVE_cue, meta_RIFX_WAVE, /* RIFX, for big-endian WAVs */ meta_RIFX_WAVE_smpl, /* RIFX w/ loop data in smpl chunk */ meta_XNB, /* XNA Game Studio 4.0 */ From 6afc026195aa1f15fb375694fea4b2eb6ac3eda0 Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 16:36:46 +0100 Subject: [PATCH 04/11] Add HCA key --- src/meta/hca_keys.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/meta/hca_keys.h b/src/meta/hca_keys.h index 575fe5ea..3174f51a 100644 --- a/src/meta/hca_keys.h +++ b/src/meta/hca_keys.h @@ -1501,6 +1501,9 @@ static const hcakey_info hcakey_list[] = { // Kingdom Hearts 3 (Steam) {10095514444067761537u}, // 8C1A78E6077BED81 + + // Muv-Luv Dimensions (Android) + {8848}, // 0000000000002290 }; #endif From 85a6957be4365efe8938e8fb369d770c39672534 Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 16:36:58 +0100 Subject: [PATCH 05/11] Add ADX key --- src/meta/adx_keys.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/meta/adx_keys.h b/src/meta/adx_keys.h index e9a2b35f..de4e992e 100644 --- a/src/meta/adx_keys.h +++ b/src/meta/adx_keys.h @@ -277,11 +277,11 @@ static const adxkey_info adxkey9_list[] = { {0x0000,0x0000,0x0000, NULL,9923540143823782}, // 002341683D2FDBA6 /* ARGONAVIS -Kimi ga Mita Stage e- (Android) */ - {0x069c,0x06e9,0x0323, NULL,0}, // guessed with VGAudio (possible key: 34E06E8192 / 227103637906) + {0x0000,0x0000,0x0000, NULL,301179795002661}, // 000111EBE2B1D525 (+ AWB subkeys) }; static const int adxkey8_list_count = sizeof(adxkey8_list) / sizeof(adxkey8_list[0]); static const int adxkey9_list_count = sizeof(adxkey9_list) / sizeof(adxkey9_list[0]); -#endif/*_ADX_KEYS_H_*/ +#endif From 84f25abdf1245a8c6475a2663294414fe81f4815 Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 16:37:51 +0100 Subject: [PATCH 06/11] Fix CLI loop forever to stdout --- cli/vgmstream_cli.c | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/cli/vgmstream_cli.c b/cli/vgmstream_cli.c index 6615356d..ec9538a5 100644 --- a/cli/vgmstream_cli.c +++ b/cli/vgmstream_cli.c @@ -379,20 +379,7 @@ static bool write_file(VGMSTREAM* vgmstream, cli_config_t* cfg) { // decode only: outfile is NULL (won't write anything) } - - /* decode forever */ - while (cfg->play_forever && !cfg->decode_only) { - int to_get = cfg->sample_buffer_size; - - render_vgmstream(buf, to_get, vgmstream); - - wav_swap_samples_le(buf, channels * to_get, 0); - fwrite(buf, sizeof(sample_t), to_get * channels, outfile); - /* should write infinitely until program kill */ - } - - - /* slap on a .wav header */ + /* slap on a .wav header (note that this goes before decodes in case of printing to stdout) */ if (!cfg->decode_only) { uint8_t wav_buf[0x100]; size_t bytes_done; @@ -410,6 +397,16 @@ static bool write_file(VGMSTREAM* vgmstream, cli_config_t* cfg) { fwrite(wav_buf, sizeof(uint8_t), bytes_done, outfile); } + /* decode forever */ + while (cfg->play_forever && !cfg->decode_only) { + int to_get = cfg->sample_buffer_size; + + render_vgmstream(buf, to_get, vgmstream); + + wav_swap_samples_le(buf, channels * to_get, 0); + fwrite(buf, sizeof(sample_t), to_get * channels, outfile); + /* should write infinitely until program kill */ + } /* decode */ for (int i = 0; i < len_samples; i += cfg->sample_buffer_size) { From 97e46436d7514de00f7102c66e00536266672888 Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 16:38:15 +0100 Subject: [PATCH 07/11] Fix disabling loops also erasing loop points --- src/vgmstream.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vgmstream.c b/src/vgmstream.c index 6234e652..9780c359 100644 --- a/src/vgmstream.c +++ b/src/vgmstream.c @@ -136,13 +136,16 @@ void setup_vgmstream(VGMSTREAM* vgmstream) { vgmstream->loop_end_sample = 0; } } - + +#if 0 + //TODO: this removes loop info after disabling loops externally (this must be called), though this is not very useful /* clean as loops are readable metadata but loop fields may contain garbage * (done *after* dual stereo as it needs loop fields to match) */ if (!vgmstream->loop_flag) { vgmstream->loop_start_sample = 0; vgmstream->loop_end_sample = 0; } +#endif /* save start things so we can restart when seeking */ memcpy(vgmstream->start_ch, vgmstream->ch, sizeof(VGMSTREAMCHANNEL)*vgmstream->channels); @@ -220,7 +223,12 @@ VGMSTREAM* allocate_vgmstream(int channels, int loop_flag) { vgmstream->loop_flag = loop_flag; vgmstream->mixer = mixer_init(vgmstream->channels); /* pre-init */ - //if (!vgmstream->mixer) goto fail; + if (!vgmstream->mixer) goto fail; + +#if VGM_TEST_DECODER + vgmstream->decode_state = decode_init(); + if (!vgmstream->decode_state) goto fail; +#endif //TODO: improve/init later to minimize memory /* garbage buffer for seeking/discarding (local bufs may cause stack overflows with segments/layers) @@ -412,6 +420,9 @@ static bool merge_vgmstream(VGMSTREAM* opened_vgmstream, VGMSTREAM* new_vgmstrea opened_vgmstream->layout_type = layout_none; /* fixes some odd cases */ /* discard the second VGMSTREAM */ +#if VGM_TEST_DECODER + decode_free(new_vgmstream); +#endif mixer_free(new_vgmstream->mixer); free(new_vgmstream->tmpbuf); free(new_vgmstream->start_vgmstream); From e76be71d51d0efdf3e9c78b67f9ed389489d765f Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 16:40:01 +0100 Subject: [PATCH 08/11] doc --- doc/BUILD.md | 6 +++++- doc/USAGE.md | 5 +++-- src/coding/vorbis_custom_utils_wwise.c | 1 - 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/BUILD.md b/doc/BUILD.md index 1fdae4f8..8b840f57 100644 --- a/doc/BUILD.md +++ b/doc/BUILD.md @@ -22,7 +22,7 @@ Though it's rather flexible (like using Windows with GCC and autotools), some co ```sh sudo apt-get update # base deps -sudo apt-get install -y gcc g++ make cmake build-essential git +sudo apt-get install -y gcc g++ make cmake build-essential git pkg-config # optional: for extra formats (can be ommited to build with static libs) sudo apt-get install -y libmpg123-dev libvorbis-dev libspeex-dev sudo apt-get install -y libavformat-dev libavcodec-dev libavutil-dev libswresample-dev @@ -123,11 +123,15 @@ First, follow the *Emscripten* installation instructions: Though basically: ```sh +apt-get install -y python3 + git clone https://github.com/emscripten-core/emsdk cd emsdk ./emsdk install latest ./emsdk activate latest source ./emsdk_env.sh + +emsdk ``` Then you should be able to build it on **Linux** (**Windows** should be possible too, but has some issues at the moment), for example with CMake: diff --git a/doc/USAGE.md b/doc/USAGE.md index 36bbe388..7e3b5231 100644 --- a/doc/USAGE.md +++ b/doc/USAGE.md @@ -545,8 +545,9 @@ willow.mpf: willow.mus,willow_o.mus bgm_2_streamfiles.awb: bgm_2.acb ``` ``` -# hashes of SE1_Common_BGM + ext [Hyrule Warriors: Age of Calamity (Switch)] -0x3a160928.srsa: 0x272c6efb.srsa +# hashes of SE1_Common_BGM + SRSA/SRST [Hyrule Warriors: Age of Calamity (Switch)] +# (more exactly "R_SRSA[SE1_Common_BGM]" and "R_SRST[SE1_Common_BGM]") +0x3a160928.srsa: 0x272c6efb.srst ``` ``` # Snack World (Switch) names for .awb (single .acb for all .awb, order matters) diff --git a/src/coding/vorbis_custom_utils_wwise.c b/src/coding/vorbis_custom_utils_wwise.c index 00657d8b..510b0611 100644 --- a/src/coding/vorbis_custom_utils_wwise.c +++ b/src/coding/vorbis_custom_utils_wwise.c @@ -44,7 +44,6 @@ static int load_wvc_array(uint8_t* buf, size_t bufsize, uint32_t codebook_id, ww /** * Wwise stores a reduced setup, and packets have mini headers with the size, and data packets * may reduced as well. The format evolved over time so there are many variations. - * The Wwise implementation uses Tremor (fixed-point Vorbis) but shouldn't matter. * * Format reverse-engineered by hcs in ww2ogg (https://github.com/hcs64/ww2ogg). */ From edcfe5b16d979ffeb0a8979871a971c862d1d633 Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 16:44:26 +0100 Subject: [PATCH 09/11] doc --- src/meta/ktsr.c | 51 ++++++++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/src/meta/ktsr.c b/src/meta/ktsr.c index b76d927f..eb37ecb4 100644 --- a/src/meta/ktsr.c +++ b/src/meta/ktsr.c @@ -325,46 +325,36 @@ static int parse_codec(ktsr_header* ktsr) { switch(ktsr->platform) { case 0x01: /* PC */ case 0x05: /* PC/Steam [Fate/Samurai Remnant (PC)] */ - if (ktsr->is_external) { - if (ktsr->format == 0x0005) - ktsr->codec = KOVS; // Atelier Ryza (PC) - else - goto fail; - } - else if (ktsr->format == 0x0000) { + if (ktsr->format == 0x0000 && !ktsr->is_external) ktsr->codec = MSADPCM; // Warrior Orochi 4 (PC) - } - else { + //else if (ktsr->format == 0x0001) + // ktsr->codec = KA1A; // Dynasty Warriors Origins (PC) + else if (ktsr->format == 0x0005 && ktsr->is_external) + ktsr->codec = KOVS; // Atelier Ryza (PC) + //else if (ktsr->format == 0x1001 && ktsr->is_external) + // ktsr->codec = KA1A; // Dynasty Warriors Origins (PC) + else goto fail; - } break; case 0x03: /* PS4/VITA */ - if (ktsr->is_external) { - if (ktsr->format == 0x1001) - ktsr->codec = RIFF_ATRAC9; // Nioh (PS4) - else if (ktsr->format == 0x0005) - ktsr->codec = KTAC; // Blue Reflection Tie (PS4) - else - goto fail; - } - else if (ktsr->format == 0x0001) + if (ktsr->format == 0x0001 && !ktsr->is_external) ktsr->codec = ATRAC9; // Attack on Titan: Wings of Freedom (Vita) + else if (ktsr->format == 0x0005 && ktsr->is_external) + ktsr->codec = KTAC; // Blue Reflection Tie (PS4) + else if (ktsr->format == 0x1001 && ktsr->is_external) + ktsr->codec = RIFF_ATRAC9; // Nioh (PS4) else goto fail; break; case 0x04: /* Switch */ - if (ktsr->is_external) { - if (ktsr->format == 0x0005) - ktsr->codec = KTSS; // [Ultra Kaiju Monster Rancher (Switch)] - else if (ktsr->format == 0x1000) - ktsr->codec = KTSS; // [Fire Emblem: Three Houses (Switch)-some DSP voices] - else - goto fail; - } - else if (ktsr->format == 0x0000) + if (ktsr->format == 0x0000 && !ktsr->is_external) ktsr->codec = DSP; // [Fire Emblem: Three Houses (Switch)] + else if (ktsr->format == 0x0005 && ktsr->is_external) + ktsr->codec = KTSS; // [Ultra Kaiju Monster Rancher (Switch)] + else if (ktsr->format == 0x1000 && ktsr->is_external) + ktsr->codec = KTSS; // [Fire Emblem: Three Houses (Switch)-some DSP voices] else goto fail; break; @@ -630,7 +620,8 @@ static bool parse_ktsr(ktsr_header* ktsr, STREAMFILE* sf) { case 0xBD888C36: /* cue? (floats, stream id, etc, may have extended name; can have sub-chunks)-appears N times */ case 0xC9C48EC1: /* unknown (has some string inside like "boss") */ case 0xA9D23BF1: /* "state container", some kind of config/floats, with names like 'State_bgm01'..N */ - case 0x836FBECA: /* unknown (~0x300, encrypted? table + data) */ + case 0x836FBECA: /* random sfxs? (ex. weapon sfx variations; IDs + encrypted name table + data) */ + case 0x2d232c98: /* big mix of tables, found in DWO BGM srsa */ break; case 0xC5CCCB70: /* sound (internal data or external stream) */ @@ -673,7 +664,7 @@ static bool parse_ktsr(ktsr_header* ktsr, STREAMFILE* sf) { default: /* streams also have their own chunks like 0x09D4F415, not needed here */ - VGM_LOG("ktsr: unknown chunk at %x\n", offset); + VGM_LOG("ktsr: unknown chunk 0x%08x at %x\n", type, offset); goto fail; } From c1520530a7adf6bbf9800088f793b2049e137826 Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 16:46:22 +0100 Subject: [PATCH 10/11] debug decoder stuff --- src/base/decode.c | 100 ++++++++++++++++++++++++++++++++++++++- src/base/decode.h | 3 ++ src/base/decode_state.h | 13 +++++ src/coding/coding.h | 3 ++ src/coding/tac_decoder.c | 32 ++++++++++++- src/vgmstream.h | 3 ++ 6 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 src/base/decode_state.h diff --git a/src/base/decode.c b/src/base/decode.c index f1f30cac..43938b2e 100644 --- a/src/base/decode.c +++ b/src/base/decode.c @@ -6,10 +6,34 @@ #include "plugins.h" #include "sbuf.h" +#if VGM_TEST_DECODER +#include "../util/log.h" +#include "decode_state.h" + + +static void* decode_state_init() { + return calloc(1, sizeof(decode_state_t)); +} + +static void decode_state_reset(VGMSTREAM* vgmstream) { + memset(vgmstream->decode_state, 0, sizeof(decode_state_t)); +} + +// this could be part of the VGMSTREAM but for now keep separate as it simplifies +// some loop-related stuff +void* decode_init() { + return decode_state_init(); +} +#endif + + /* custom codec handling, not exactly "decode" stuff but here to simplify adding new codecs */ - void decode_free(VGMSTREAM* vgmstream) { +#if VGM_TEST_DECODER + free(vgmstream->decode_state); +#endif + if (!vgmstream->codec_data) return; @@ -127,6 +151,10 @@ void decode_free(VGMSTREAM* vgmstream) { void decode_seek(VGMSTREAM* vgmstream) { +#if VGM_TEST_DECODER + decode_state_reset(vgmstream); +#endif + if (!vgmstream->codec_data) return; @@ -228,6 +256,10 @@ void decode_seek(VGMSTREAM* vgmstream) { void decode_reset(VGMSTREAM* vgmstream) { +#if VGM_TEST_DECODER + decode_state_reset(vgmstream); +#endif + if (!vgmstream->codec_data) return; @@ -825,11 +857,74 @@ bool decode_uses_internal_offset_updates(VGMSTREAM* vgmstream) { return vgmstream->coding_type == coding_MS_IMA || vgmstream->coding_type == coding_MS_IMA_mono; } +#if VGM_TEST_DECODER +// decode frames for decoders which have their own sample buffer +static void decode_frames(sbuf_t* sbuf, VGMSTREAM* vgmstream) { + const int max_empty = 10000; + int num_empty = 0; + + decode_state_t* ds = vgmstream->decode_state; + + while (sbuf->filled < sbuf->samples) { + + // decode new frame if all was consumed + if (ds->sbuf.filled == 0) { + bool ok = false; + switch (vgmstream->coding_type) { + case coding_TAC: + ok = decode_tac_frame(vgmstream); + break; + default: + break; + } + + if (!ok) + goto decode_fail; + } + + if (ds->discard) { + // decode may signal that decoded samples need to be discarded, because of encoder delay + // (first samples of a file need to be ignored) or a loop + int current_discard = ds->discard; + if (current_discard > ds->sbuf.filled) + current_discard = ds->sbuf.filled; + + sbuf_consume(&ds->sbuf, current_discard); + + ds->discard -= current_discard; + } + else { + // copy + consume + int samples_copy = ds->sbuf.filled; + if (samples_copy > sbuf->samples - sbuf->filled) + samples_copy = sbuf->samples - sbuf->filled; + + sbuf_copy_segments(sbuf, &ds->sbuf); + sbuf_consume(&ds->sbuf, samples_copy); + + sbuf->filled += samples_copy; + } + } + + return; +decode_fail: + /* on error just put some 0 samples */ + VGM_LOG("VGMSTREAM: decode fail, missing %i samples\n", sbuf->samples - sbuf->filled); + sbuf_silence_rest(sbuf); +} +#endif + /* Decode samples into the buffer. Assume that we have written samples_filled into the * buffer already, and we have samples_to_do consecutive samples ahead of us (won't call * more than one frame if configured above to do so). * Called by layouts since they handle samples written/to_do */ void decode_vgmstream(VGMSTREAM* vgmstream, int samples_filled, int samples_to_do, sample_t* buffer) { +#if VGM_TEST_DECODER + sbuf_t sbuf_tmp = {0}; + sbuf_t* sbuf = &sbuf_tmp; + sbuf_init_s16(sbuf, buffer, samples_filled + samples_to_do, vgmstream->channels); + sbuf->filled = samples_filled; +#endif int ch; buffer += samples_filled * vgmstream->channels; /* passed externally to simplify I guess */ @@ -1566,6 +1661,9 @@ void decode_vgmstream(VGMSTREAM* vgmstream, int samples_filled, int samples_to_d } break; default: +#if VGM_TEST_DECODER + decode_frames(sbuf, vgmstream); +#endif break; } } diff --git a/src/base/decode.h b/src/base/decode.h index f959a464..4731eab4 100644 --- a/src/base/decode.h +++ b/src/base/decode.h @@ -3,6 +3,9 @@ #include "../vgmstream.h" +#if VGM_TEST_DECODER +void* decode_init(); +#endif void decode_free(VGMSTREAM* vgmstream); void decode_seek(VGMSTREAM* vgmstream); void decode_reset(VGMSTREAM* vgmstream); diff --git a/src/base/decode_state.h b/src/base/decode_state.h new file mode 100644 index 00000000..64bf7267 --- /dev/null +++ b/src/base/decode_state.h @@ -0,0 +1,13 @@ +#ifndef _DECODE_STATE_H +#define _DECODE_STATE_H + +#if VGM_TEST_DECODER +#include "sbuf.h" + +typedef struct { + int discard; + sbuf_t sbuf; +} decode_state_t; +#endif + +#endif diff --git a/src/coding/coding.h b/src/coding/coding.h index 2adb81f8..6e052438 100644 --- a/src/coding/coding.h +++ b/src/coding/coding.h @@ -372,6 +372,9 @@ typedef struct tac_codec_data tac_codec_data; tac_codec_data* init_tac(STREAMFILE* sf); void decode_tac(VGMSTREAM* vgmstream, sample_t* outbuf, int32_t samples_to_do); +#if VGM_TEST_DECODER +bool decode_tac_frame(VGMSTREAM* vgmstream); +#endif void reset_tac(tac_codec_data* data); void seek_tac(tac_codec_data* data, int32_t num_sample); void free_tac(tac_codec_data* data); diff --git a/src/coding/tac_decoder.c b/src/coding/tac_decoder.c index 5bf1f6fa..ad0c21a1 100644 --- a/src/coding/tac_decoder.c +++ b/src/coding/tac_decoder.c @@ -1,6 +1,8 @@ #include "coding.h" #include "coding_utils_samples.h" - +#if VGM_TEST_DECODER +#include "../base/decode_state.h" +#endif #include "libs/tac_lib.h" @@ -129,6 +131,34 @@ fail: s16buf_silence(&outbuf, &samples_to_do, data->sbuf.channels); } +#if VGM_TEST_DECODER +bool decode_tac_frame(VGMSTREAM* vgmstream) { + VGMSTREAMCHANNEL* stream = &vgmstream->ch[0]; + tac_codec_data* data = vgmstream->codec_data; + decode_state_t* ds = vgmstream->decode_state; + + sbuf_init_s16(&ds->sbuf, data->samples, TAC_FRAME_SAMPLES, vgmstream->channels); + + bool ok; + + ok = read_frame(data, stream->streamfile); + if (!ok) return false; + + ok = decode_frame(data); + if (!ok) return false; + + ds->sbuf.filled = TAC_FRAME_SAMPLES; //TODO call sbuf_fill(samples); + + // copy and let decoder handle + if (data->samples_discard) { + ds->discard = data->samples_discard; + data->samples_discard = 0; + } + + return true; +} +#endif + void reset_tac(tac_codec_data* data) { if (!data) return; diff --git a/src/vgmstream.h b/src/vgmstream.h index 59bd5270..10d522fc 100644 --- a/src/vgmstream.h +++ b/src/vgmstream.h @@ -242,6 +242,9 @@ typedef struct { void* tmpbuf; /* garbage buffer used for seeking/trimming */ size_t tmpbuf_size; /* for all channels (samples = tmpbuf_size / channels / sample_size) */ +#if VGM_TEST_DECODER + void* decode_state; /* for some decoders (TO-DO: to be mover around) */ +#endif } VGMSTREAM; From 1cadc040f6f2af880737d794ae22ead0fe52d023 Mon Sep 17 00:00:00 2001 From: bnnm Date: Tue, 31 Dec 2024 17:31:14 +0100 Subject: [PATCH 11/11] Allow some uncommon filenames in .txtp --- src/meta/txtp_parser.c | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/meta/txtp_parser.c b/src/meta/txtp_parser.c index d362d981..cfe92553 100644 --- a/src/meta/txtp_parser.c +++ b/src/meta/txtp_parser.c @@ -854,7 +854,7 @@ static int add_entry(txtp_header_t* txtp, char* filename, int is_default) { txtp_entry_t entry = {0}; - //;VGM_LOG("TXTP: filename=%s\n", filename); + ;VGM_LOG("TXTP: input filename=%s\n", filename); /* parse filename: file.ext#(commands) */ { @@ -864,19 +864,43 @@ static int add_entry(txtp_header_t* txtp, char* filename, int is_default) { params = filename; /* multiple commands without filename */ } else { - /* find settings start after filenames (filenames can also contain dots and #, - * so this may be fooled by certain patterns) */ - params = strchr(filename, '.'); /* first dot (may be a false positive) */ - if (!params) /* extensionless */ + // Find settings after filename (basically find extension then first #). + // Filenames may contain dots and # though, so this may be fooled by certain patterns + // (like with extensionless files with a # inside or dirs with . in the name) + + // Find first dot which is usually the extension; may be a false positive but hard to handle every case + // (can't use "last dot" because some commands allow it like '#I 1.0 20.0') + params = strchr(filename, '.'); + if (!params) // extensionless = reset to line start params = filename; - params = strchr(params, '#'); /* next should be actual settings */ + + // Skip relative path like ./../stuff/../ and maybe "01 blah... blah.adx" + while (params[1] == '.' || params[1] == '/') { + char* params_tmp = strchr(params + 1, '.'); + if (!params_tmp) //??? + break; + params = params_tmp; + } + + // Rarely filenames may be "01. blah (#blah).ext #i", where the first # is ambiguous. + // Detect the space after dot (always a track number) and search dot again. + if (params[1] == ' ') { + params = strchr(params + 1, '.'); + if (!params) /* extensionless */ + params = filename; + } + + // first # after dot should be actual .txtp settings + params = strchr(params, '#'); if (!params) params = NULL; } + ;VGM_LOG("TXTP: params=%s\n", params); parse_params(&entry, params); } + ;VGM_LOG("TXTP: output filename=%s\n", filename); clean_filename(filename); //;VGM_LOG("TXTP: clean filename='%s'\n", filename);