From f3b7b93d190c52b8909b4fa13063a5c82e88661d Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 21 Nov 2020 15:51:43 +0100 Subject: [PATCH 01/12] Add .mwa extension [Fatal Frame (Xbox)] --- src/formats.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/formats.c b/src/formats.c index 66e6c408..1690858d 100644 --- a/src/formats.c +++ b/src/formats.c @@ -333,6 +333,7 @@ static const char* extension_list[] = { "musc", "musx", "mvb", //txth/reserved [Porsche Challenge (PS1)] + "mwa", //txth/reserved [Fatal Frame (Xbox)] "mwv", "mxst", "myspd", From 42e4a11564d310ed47e292232b50162d18da23aa Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 21 Nov 2020 15:54:07 +0100 Subject: [PATCH 02/12] Fix some .xwb+xsb name issues [LocoCycle (X360)] --- src/meta/xwb.c | 1468 ++++++++++++++++++++++---------------------- src/meta/xwb_xsb.h | 115 ++-- 2 files changed, 799 insertions(+), 784 deletions(-) diff --git a/src/meta/xwb.c b/src/meta/xwb.c index 7d951aaf..2eb63586 100644 --- a/src/meta/xwb.c +++ b/src/meta/xwb.c @@ -1,734 +1,734 @@ -#include "meta.h" -#include "../coding/coding.h" -#include -#include "xwb_xsb.h" - -/* most info from XWBtool, xactwb.h, xact2wb.h and xact3wb.h */ - -#define WAVEBANK_FLAGS_COMPACT 0x00020000 // Bank uses compact format -#define WAVEBANKENTRY_FLAGS_IGNORELOOP 0x00000008 // Used internally when the loop region can't be used (no idea...) - -/* the x.x version is just to make it clearer, MS only classifies XACT as 1/2/3 */ -#define XACT1_0_MAX 1 /* Project Gotham Racing 2 (v1), Silent Hill 4 (v1) */ -#define XACT1_1_MAX 3 /* Unreal Championship (v2), The King of Fighters 2003 (v3) */ -#define XACT2_0_MAX 34 /* Dead or Alive 4 (v17), Kameo (v23), Table Tennis (v34) */ // v35/36/37 too? -#define XACT2_1_MAX 38 /* Prey (v38) */ // v39 too? -#define XACT2_2_MAX 41 /* Blue Dragon (v40) */ -#define XACT3_0_MAX 46 /* Ninja Blade (t43 v42), Persona 4 Ultimax NESSICA (t45 v43) */ -#define XACT_TECHLAND 0x10000 /* Sniper Ghost Warrior, Nail'd (PS3/X360), equivalent to XACT3_0 */ -#define XACT_CRACKDOWN 0x87 /* Crackdown 1, equivalent to XACT2_2 */ - -static const int wma_avg_bps_index[7] = { - 12000, 24000, 4000, 6000, 8000, 20000, 2500 -}; -static const int wma_block_align_index[17] = { - 929, 1487, 1280, 2230, 8917, 8192, 4459, 5945, 2304, 1536, 1485, 1008, 2731, 4096, 6827, 5462, 1280 -}; - - -typedef enum { PCM, XBOX_ADPCM, MS_ADPCM, XMA1, XMA2, WMA, XWMA, ATRAC3, OGG, DSP, ATRAC9_RIFF } xact_codec; -typedef struct { - int little_endian; - int version; - - /* segments */ - off_t base_offset; - size_t base_size; - off_t entry_offset; - size_t entry_size; - off_t names_offset; - size_t names_size; - size_t names_entry_size; - off_t extra_offset; - size_t extra_size; - off_t data_offset; - size_t data_size; - - off_t stream_offset; - size_t stream_size; - - uint32_t base_flags; - size_t entry_elem_size; - size_t entry_alignment; - int total_subsongs; - - uint32_t entry_flags; - uint32_t format; - int tag; - int channels; - int sample_rate; - int block_align; - int bits_per_sample; - xact_codec codec; - - int loop_flag; - uint32_t num_samples; - uint32_t loop_start; - uint32_t loop_end; - uint32_t loop_start_sample; - uint32_t loop_end_sample; - - char wavebank_name[64+1]; - - int is_crackdown; - int fix_xma_num_samples; - int fix_xma_loop_samples; -} xwb_header; - -static void get_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf); - - -/* XWB - XACT Wave Bank (Microsoft SDK format for XBOX/XBOX360/Windows) */ -VGMSTREAM* init_vgmstream_xwb(STREAMFILE* sf) { - VGMSTREAM* vgmstream = NULL; - off_t start_offset, offset, suboffset; - xwb_header xwb = {0}; - int target_subsong = sf->stream_index; - uint32_t (*read_u32)(off_t,STREAMFILE*) = NULL; - int32_t (*read_s32)(off_t,STREAMFILE*) = NULL; - - - /* checks */ - /* .xwb: standard - * .xna: Touhou Makukasai ~ Fantasy Danmaku Festival (PC) - * (extensionless): Ikaruga (X360/PC), Grabbed by the Ghoulies (Xbox) */ - if (!check_extensions(sf,"xwb,xna,")) - goto fail; - if ((read_u32be(0x00,sf) != 0x57424E44) && /* "WBND" (LE) */ - (read_u32be(0x00,sf) != 0x444E4257)) /* "DNBW" (BE) */ - goto fail; - - xwb.little_endian = read_u32be(0x00,sf) == 0x57424E44; /* WBND */ - if (xwb.little_endian) { - read_u32 = read_u32le; - read_s32 = read_s32le; - } else { - read_u32 = read_u32be; - read_s32 = read_s32be; - } - - - /* read main header (WAVEBANKHEADER) */ - xwb.version = read_u32(0x04, sf); /* XACT3: 0x04=tool version, 0x08=header version */ - - /* Crackdown 1 (X360), essentially XACT2 but may have split header in some cases, compact entries change */ - if (xwb.version == XACT_CRACKDOWN) { - xwb.version = XACT2_2_MAX; - xwb.is_crackdown = 1; - } - - /* read segment offsets (SEGIDX) */ - if (xwb.version <= XACT1_0_MAX) { - xwb.total_subsongs = read_s32(0x0c, sf); - read_string(xwb.wavebank_name,0x10+1, 0x10, sf); /* null-terminated */ - xwb.base_offset = 0; - xwb.base_size = 0; - xwb.entry_offset = 0x50; - xwb.entry_elem_size = 0x14; - xwb.entry_size = xwb.entry_elem_size * xwb.total_subsongs; - xwb.data_offset = xwb.entry_offset + xwb.entry_size; - xwb.data_size = get_streamfile_size(sf) - xwb.data_offset; - - xwb.names_offset = 0; - xwb.names_size = 0; - xwb.names_entry_size= 0; - xwb.extra_offset = 0; - xwb.extra_size = 0; - } - else { - offset = xwb.version <= XACT2_2_MAX ? 0x08 : 0x0c; - xwb.base_offset = read_s32(offset+0x00, sf);//BANKDATA - xwb.base_size = read_s32(offset+0x04, sf); - xwb.entry_offset= read_s32(offset+0x08, sf);//ENTRYMETADATA - xwb.entry_size = read_s32(offset+0x0c, sf); - - /* read extra segments (values can be 0 == no segment) */ - if (xwb.version <= XACT1_1_MAX) { - xwb.names_offset = read_s32(offset+0x10, sf);//ENTRYNAMES - xwb.names_size = read_s32(offset+0x14, sf); - xwb.names_entry_size= 0x40; - xwb.extra_offset = 0; - xwb.extra_size = 0; - suboffset = 0x04*2; - } - else if (xwb.version <= XACT2_1_MAX) { - xwb.names_offset = read_s32(offset+0x10, sf);//ENTRYNAMES - xwb.names_size = read_s32(offset+0x14, sf); - xwb.names_entry_size= 0x40; - xwb.extra_offset = read_s32(offset+0x18, sf);//EXTRA - xwb.extra_size = read_s32(offset+0x1c, sf); - suboffset = 0x04*2 + 0x04*2; - } else { - xwb.extra_offset = read_s32(offset+0x10, sf);//SEEKTABLES - xwb.extra_size = read_s32(offset+0x14, sf); - xwb.names_offset = read_s32(offset+0x18, sf);//ENTRYNAMES - xwb.names_size = read_s32(offset+0x1c, sf); - xwb.names_entry_size= 0x40; - suboffset = 0x04*2 + 0x04*2; - } - - xwb.data_offset = read_s32(offset+0x10+suboffset, sf);//ENTRYWAVEDATA - xwb.data_size = read_s32(offset+0x14+suboffset, sf); - - /* for Techland's XWB with no data */ - if (xwb.base_offset == 0) goto fail; - - /* read base entry (WAVEBANKDATA) */ - offset = xwb.base_offset; - xwb.base_flags = read_u32(offset+0x00, sf); - xwb.total_subsongs = read_s32(offset+0x04, sf); - read_string(xwb.wavebank_name,0x40+1, offset+0x08, sf); /* null-terminated */ - suboffset = 0x08 + (xwb.version <= XACT1_1_MAX ? 0x10 : 0x40); - xwb.entry_elem_size = read_s32(offset+suboffset+0x00, sf); - /* suboff+0x04: meta name entry size */ - xwb.entry_alignment = read_s32(offset+suboffset+0x08, sf); /* usually 1 dvd sector */ - xwb.format = read_s32(offset+suboffset+0x0c, sf); /* compact mode only */ - /* suboff+0x10: build time 64b (XACT2/3) */ - } - - //;VGM_LOG("XWB: wavebank name='%s'\n", xwb.wavebank_name); - - if (target_subsong == 0) target_subsong = 1; /* auto: default to 1 */ - if (target_subsong < 0 || target_subsong > xwb.total_subsongs || xwb.total_subsongs < 1) goto fail; - - - /* read stream entry (WAVEBANKENTRY) */ - offset = xwb.entry_offset + (target_subsong-1) * xwb.entry_elem_size; - - - if ((xwb.base_flags & WAVEBANK_FLAGS_COMPACT) && xwb.is_crackdown) { - /* mutant compact (w/ entry_elem_size=0x08) [Crackdown (X360)] */ - uint32_t entry, size_sectors, sector_offset; - - entry = read_u32(offset+0x00, sf); - size_sectors = ((entry >> 19) & 0x1FFF); /* 13b, exact size in sectors */ - sector_offset = (entry & 0x7FFFF); /* 19b, offset within data in sectors */ - xwb.stream_size = size_sectors * xwb.entry_alignment; - xwb.num_samples = read_u32(offset+0x04, sf); - - xwb.stream_offset = xwb.data_offset + sector_offset * xwb.entry_alignment; - } - else if (xwb.base_flags & WAVEBANK_FLAGS_COMPACT) { - /* compact entry [NFL Fever 2004 demo from Amped 2 (Xbox)] */ - uint32_t entry, size_deviation, sector_offset; - off_t next_stream_offset; - - entry = read_u32(offset+0x00, sf); - size_deviation = ((entry >> 21) & 0x7FF); /* 11b, padding data for sector alignment in bytes*/ - sector_offset = (entry & 0x1FFFFF); /* 21b, offset within data in sectors */ - - xwb.stream_offset = xwb.data_offset + sector_offset * xwb.entry_alignment; - - /* find size using next offset */ - if (target_subsong < xwb.total_subsongs) { - uint32_t next_entry = read_u32(offset + xwb.entry_elem_size, sf); - next_stream_offset = xwb.data_offset + (next_entry & 0x1FFFFF) * xwb.entry_alignment; - } - else { /* for last entry (or first, when subsongs = 1) */ - next_stream_offset = xwb.data_offset + xwb.data_size; - } - xwb.stream_size = next_stream_offset - xwb.stream_offset - size_deviation; - } - else if (xwb.version <= XACT1_0_MAX) { - xwb.format = read_u32(offset+0x00, sf); - xwb.stream_offset = xwb.data_offset + read_u32(offset+0x04, sf); - xwb.stream_size = read_u32(offset+0x08, sf); - - xwb.loop_start = read_u32(offset+0x0c, sf); - xwb.loop_end = read_u32(offset+0x10, sf);//length - } - else { - uint32_t entry_info = read_u32(offset+0x00, sf); - if (xwb.version <= XACT1_1_MAX) { - xwb.entry_flags = entry_info; - } else { - xwb.entry_flags = (entry_info) & 0xF; /*4b*/ - xwb.num_samples = (entry_info >> 4) & 0x0FFFFFFF; /*28b*/ - } - xwb.format = read_u32(offset+0x04, sf); - xwb.stream_offset = xwb.data_offset + read_u32(offset+0x08, sf); - xwb.stream_size = read_u32(offset+0x0c, sf); - - if (xwb.version <= XACT2_1_MAX) { /* LoopRegion (bytes) */ - xwb.loop_start = read_u32(offset+0x10, sf); - xwb.loop_end = read_u32(offset+0x14, sf);//length (LoopRegion) or offset (XMALoopRegion in late XACT2) - } else { /* LoopRegion (samples) */ - xwb.loop_start_sample = read_u32(offset+0x10, sf); - xwb.loop_end_sample = read_u32(offset+0x14, sf) + xwb.loop_start_sample; - } - } - - - /* parse format */ - if (xwb.version <= XACT1_0_MAX) { - xwb.bits_per_sample = (xwb.format >> 31) & 0x1; /*1b*/ - xwb.sample_rate = (xwb.format >> 4) & 0x7FFFFFF; /*27b*/ - xwb.channels = (xwb.format >> 1) & 0x7; /*3b*/ - xwb.tag = (xwb.format) & 0x1; /*1b*/ - } - else if (xwb.version <= XACT1_1_MAX) { - xwb.bits_per_sample = (xwb.format >> 31) & 0x1; /*1b*/ - xwb.sample_rate = (xwb.format >> 5) & 0x3FFFFFF; /*26b*/ - xwb.channels = (xwb.format >> 2) & 0x7; /*3b*/ - xwb.tag = (xwb.format) & 0x3; /*2b*/ - } - else if (xwb.version <= XACT2_0_MAX) { - xwb.bits_per_sample = (xwb.format >> 31) & 0x1; /*1b*/ - xwb.block_align = (xwb.format >> 24) & 0xFF; /*8b*/ - xwb.sample_rate = (xwb.format >> 4) & 0x7FFFF; /*19b*/ - xwb.channels = (xwb.format >> 1) & 0x7; /*3b*/ - xwb.tag = (xwb.format) & 0x1; /*1b*/ - } - else { - xwb.bits_per_sample = (xwb.format >> 31) & 0x1; /*1b*/ - xwb.block_align = (xwb.format >> 23) & 0xFF; /*8b*/ - xwb.sample_rate = (xwb.format >> 5) & 0x3FFFF; /*18b*/ - xwb.channels = (xwb.format >> 2) & 0x7; /*3b*/ - xwb.tag = (xwb.format) & 0x3; /*2b*/ - } - - /* standardize tag to codec */ - if (xwb.version <= XACT1_0_MAX) { - switch(xwb.tag){ - case 0: xwb.codec = PCM; break; - case 1: xwb.codec = XBOX_ADPCM; break; - default: goto fail; - } - } - else if (xwb.version <= XACT1_1_MAX) { - switch(xwb.tag){ - case 0: xwb.codec = PCM; break; - case 1: xwb.codec = XBOX_ADPCM; break; - case 2: xwb.codec = WMA; break; - case 3: xwb.codec = OGG; break; /* extension */ - default: goto fail; - } - } - else if (xwb.version <= XACT2_2_MAX) { - switch(xwb.tag) { - case 0: xwb.codec = PCM; break; - /* Table Tennis (v34): XMA1, Prey (v38): XMA2, v35/36/37: ? */ - case 1: xwb.codec = xwb.version <= XACT2_0_MAX ? XMA1 : XMA2; break; - case 2: xwb.codec = MS_ADPCM; break; - default: goto fail; - } - } - else { - switch(xwb.tag) { - case 0: xwb.codec = PCM; break; - case 1: xwb.codec = XMA2; break; - case 2: xwb.codec = MS_ADPCM; break; - case 3: xwb.codec = XWMA; break; - default: goto fail; - } - } - - - /* format hijacks from creative devs, using non-official codecs */ - if (xwb.version == XACT_TECHLAND && xwb.codec == XMA2 /* XACT_TECHLAND used in their X360 games too */ - && (xwb.block_align == 0x60 || xwb.block_align == 0x98 || xwb.block_align == 0xc0) ) { /* standard ATRAC3 blocks sizes */ - /* Techland ATRAC3 [Nail'd (PS3), Sniper: Ghost Warrior (PS3)] */ - xwb.codec = ATRAC3; - - /* num samples uses a modified entry_info format (maybe skip samples + samples? sfx use the standard format) - * ignore for now and just calc max samples */ - xwb.num_samples = atrac3_bytes_to_samples(xwb.stream_size, xwb.block_align * xwb.channels); - } - else if (xwb.codec == OGG) { - /* Oddworld: Stranger's Wrath (iOS/Android) */ - xwb.num_samples = xwb.stream_size / (2 * xwb.channels); /* uncompressed bytes */ - xwb.stream_size = xwb.loop_end; - xwb.loop_start = 0; - xwb.loop_end = 0; - } - else if (xwb.version == XACT3_0_MAX && xwb.codec == XMA2 - && (xwb.bits_per_sample == 0x00 || xwb.bits_per_sample == 0x01) /* bps=0+ba=2 in mono? (Blossom Tales) */ - && (xwb.block_align == 0x02 || xwb.block_align == 0x04) - && read_u32le(xwb.stream_offset + 0x08, sf) == xwb.sample_rate /* DSP header */ - && read_u16le(xwb.stream_offset + 0x0e, sf) == 0 - && read_u32le(xwb.stream_offset + 0x18, sf) == 2 - /*&& xwb.data_size == 0x55951c1c*/) { /* some kind of id in Stardew Valley? */ - /* Stardew Valley (Switch), Skulls of the Shogun (Switch): full interleaved DSPs (including headers) */ - xwb.codec = DSP; - } - else if (xwb.version == XACT3_0_MAX && xwb.codec == XMA2 - && xwb.bits_per_sample == 0x01 && xwb.block_align == 0x04 - && xwb.data_size == 0x4e0a1000) { /* some kind of id? */ - /* Stardew Valley (Vita), standard RIFF with ATRAC9 */ - xwb.codec = ATRAC9_RIFF; - } - - - /* test loop after the above fixes */ - xwb.loop_flag = (xwb.loop_end > 0 || xwb.loop_end_sample > xwb.loop_start) - && !(xwb.entry_flags & WAVEBANKENTRY_FLAGS_IGNORELOOP); - - /* Oddworld OGG the data_size value is size of uncompressed bytes instead; DSP uses some id/config as value */ - if (xwb.codec != OGG && xwb.codec != DSP && xwb.codec != ATRAC9_RIFF) { - /* some low-q rips don't remove padding, relax validation a bit */ - if (xwb.data_offset + xwb.stream_size > get_streamfile_size(sf)) - goto fail; - //if (xwb.data_offset + xwb.data_size > get_streamfile_size(sf)) /* badly split */ - // goto fail; - } - - - /* fix samples */ - if (xwb.version <= XACT2_2_MAX && xwb.codec == PCM) { - int bits_per_sample = xwb.bits_per_sample == 0 ? 8 : 16; - xwb.num_samples = pcm_bytes_to_samples(xwb.stream_size, xwb.channels, bits_per_sample); - if (xwb.loop_flag) { - xwb.loop_start_sample = pcm_bytes_to_samples(xwb.loop_start, xwb.channels, bits_per_sample); - xwb.loop_end_sample = pcm_bytes_to_samples(xwb.loop_start + xwb.loop_end, xwb.channels, bits_per_sample); - } - } - else if (xwb.version <= XACT1_1_MAX && xwb.codec == XBOX_ADPCM) { - xwb.block_align = 0x24 * xwb.channels; /* not really needed... */ - xwb.num_samples = xbox_ima_bytes_to_samples(xwb.stream_size, xwb.channels); - if (xwb.loop_flag) { - xwb.loop_start_sample = xbox_ima_bytes_to_samples(xwb.loop_start, xwb.channels); - xwb.loop_end_sample = xbox_ima_bytes_to_samples(xwb.loop_start + xwb.loop_end, xwb.channels); - } - } - else if (xwb.version <= XACT2_2_MAX && xwb.codec == MS_ADPCM && xwb.loop_flag) { - int block_size = (xwb.block_align + 22) * xwb.channels; /*22=CONVERSION_OFFSET (?)*/ - - xwb.loop_start_sample = msadpcm_bytes_to_samples(xwb.loop_start, block_size, xwb.channels); - xwb.loop_end_sample = msadpcm_bytes_to_samples(xwb.loop_start + xwb.loop_end, block_size, xwb.channels); - } - else if (xwb.version <= XACT2_1_MAX && (xwb.codec == XMA1 || xwb.codec == XMA2) && xwb.loop_flag) { - /* v38: byte offset, v40+: sample offset, v39: ? */ - /* need to manually find sample offsets, thanks to Microsoft's dumb headers */ - ms_sample_data msd = {0}; - - msd.xma_version = xwb.codec == XMA1 ? 1 : 2; - msd.channels = xwb.channels; - msd.data_offset = xwb.stream_offset; - msd.data_size = xwb.stream_size; - msd.loop_flag = xwb.loop_flag; - msd.loop_start_b = xwb.loop_start; /* bit offset in the stream */ - msd.loop_end_b = (xwb.loop_end >> 4); /*28b */ - /* XACT adds +1 to the subframe, but this means 0 can't be used? */ - msd.loop_end_subframe = ((xwb.loop_end >> 2) & 0x3) + 1; /* 2b */ - msd.loop_start_subframe = ((xwb.loop_end >> 0) & 0x3) + 1; /* 2b */ - - xma_get_samples(&msd, sf); - xwb.loop_start_sample = msd.loop_start_sample; - xwb.loop_end_sample = msd.loop_end_sample; - - /* if provided, xwb.num_samples is equal to msd.num_samples after proper adjustments (+ 128 - start_skip - end_skip) */ - xwb.fix_xma_loop_samples = 1; - xwb.fix_xma_num_samples = 0; - - /* for XWB v22 (and below?) this seems normal [Project Gotham Racing (X360)] */ - if (xwb.num_samples == 0) { - xwb.num_samples = msd.num_samples; - xwb.fix_xma_num_samples = 1; - } - } - else if ((xwb.codec == XMA1 || xwb.codec == XMA2) && xwb.loop_flag) { - /* unlike prev versions, xwb.num_samples is the full size without adjustments */ - xwb.fix_xma_loop_samples = 1; - xwb.fix_xma_num_samples = 1; - - /* Crackdown does use xwb.num_samples after adjustments (but not loops) */ - if (xwb.is_crackdown) { - xwb.fix_xma_num_samples = 0; - } - } - - - /* build the VGMSTREAM */ - vgmstream = allocate_vgmstream(xwb.channels,xwb.loop_flag); - if (!vgmstream) goto fail; - - vgmstream->sample_rate = xwb.sample_rate; - vgmstream->num_samples = xwb.num_samples; - vgmstream->loop_start_sample = xwb.loop_start_sample; - vgmstream->loop_end_sample = xwb.loop_end_sample; - vgmstream->num_streams = xwb.total_subsongs; - vgmstream->stream_size = xwb.stream_size; - vgmstream->meta_type = meta_XWB; - get_name(vgmstream->stream_name,STREAM_NAME_SIZE, target_subsong, &xwb, sf); - - switch(xwb.codec) { - case PCM: /* Unreal Championship (Xbox)[PCM8], KOF2003 (Xbox)[PCM16LE], Otomedius (X360)[PCM16BE] */ - vgmstream->coding_type = xwb.bits_per_sample == 0 ? coding_PCM8_U : - (xwb.little_endian ? coding_PCM16LE : coding_PCM16BE); - vgmstream->layout_type = xwb.channels > 1 ? layout_interleave : layout_none; - vgmstream->interleave_block_size = xwb.bits_per_sample == 0 ? 0x01 : 0x02; - break; - - case XBOX_ADPCM: /* Silent Hill 4 (Xbox) */ - vgmstream->coding_type = coding_XBOX_IMA; - vgmstream->layout_type = layout_none; - break; - - case MS_ADPCM: /* Persona 4 Ultimax (AC) */ - vgmstream->coding_type = coding_MSADPCM; - vgmstream->layout_type = layout_none; - vgmstream->frame_size = (xwb.block_align + 22) * xwb.channels; /*22=CONVERSION_OFFSET (?)*/ - break; - -#ifdef VGM_USE_FFMPEG - case XMA1: { /* Kameo (X360) */ - uint8_t buf[0x100]; - int bytes; - - bytes = ffmpeg_make_riff_xma1(buf,0x100, vgmstream->num_samples, xwb.stream_size, vgmstream->channels, vgmstream->sample_rate, 0); - vgmstream->codec_data = init_ffmpeg_header_offset(sf, buf,bytes, xwb.stream_offset,xwb.stream_size); - if (!vgmstream->codec_data) goto fail; - vgmstream->coding_type = coding_FFmpeg; - vgmstream->layout_type = layout_none; - - xma_fix_raw_samples(vgmstream, sf, xwb.stream_offset,xwb.stream_size, 0, xwb.fix_xma_num_samples,xwb.fix_xma_loop_samples); - - /* this fixes some XMA1, perhaps the above isn't reading end_skip correctly (doesn't happen for all files though) */ - if (vgmstream->loop_flag && - vgmstream->loop_end_sample > vgmstream->num_samples) { - VGM_LOG("XWB: fix XMA1 looping\n"); - vgmstream->loop_end_sample = vgmstream->num_samples; - } - break; - } - - case XMA2: { /* Blue Dragon (X360) */ - uint8_t buf[0x100]; - int bytes, block_size, block_count; - - block_size = 0x10000; /* XACT default */ - block_count = xwb.stream_size / block_size + (xwb.stream_size % block_size ? 1 : 0); - - bytes = ffmpeg_make_riff_xma2(buf,0x100, vgmstream->num_samples, xwb.stream_size, vgmstream->channels, vgmstream->sample_rate, block_count, block_size); - vgmstream->codec_data = init_ffmpeg_header_offset(sf, buf,bytes, xwb.stream_offset,xwb.stream_size); - if (!vgmstream->codec_data) goto fail; - vgmstream->coding_type = coding_FFmpeg; - vgmstream->layout_type = layout_none; - - xma_fix_raw_samples(vgmstream, sf, xwb.stream_offset,xwb.stream_size, 0, xwb.fix_xma_num_samples,xwb.fix_xma_loop_samples); - break; - } - - case WMA: { /* WMAudio1 (WMA v2): Prince of Persia 2 port (Xbox) */ - ffmpeg_codec_data *ffmpeg_data = NULL; - - ffmpeg_data = init_ffmpeg_offset(sf, xwb.stream_offset,xwb.stream_size); - if ( !ffmpeg_data ) goto fail; - vgmstream->codec_data = ffmpeg_data; - vgmstream->coding_type = coding_FFmpeg; - vgmstream->layout_type = layout_none; - - /* no wma_bytes_to_samples, this should be ok */ - if (!vgmstream->num_samples) - vgmstream->num_samples = (int32_t)ffmpeg_data->totalSamples; - break; - } - - case XWMA: { /* WMAudio2 (WMA v2): BlazBlue (X360), WMAudio3 (WMA Pro): Bullet Witch (PC) voices */ - uint8_t buf[0x100]; - int bytes, bps_index, block_align, block_index, avg_bps, wma_codec; - - bps_index = (xwb.block_align >> 5); /* upper 3b bytes-per-second index (docs say 2b+6b but are wrong) */ - block_index = (xwb.block_align) & 0x1F; /*lower 5b block alignment index */ - if (bps_index >= 7) goto fail; - if (block_index >= 17) goto fail; - - avg_bps = wma_avg_bps_index[bps_index]; - block_align = wma_block_align_index[block_index]; - wma_codec = xwb.bits_per_sample ? 0x162 : 0x161; /* 0=WMAudio2, 1=WMAudio3 */ - - bytes = ffmpeg_make_riff_xwma(buf,0x100, wma_codec, xwb.stream_size, vgmstream->channels, vgmstream->sample_rate, avg_bps, block_align); - vgmstream->codec_data = init_ffmpeg_header_offset(sf, buf,bytes, xwb.stream_offset,xwb.stream_size); - if (!vgmstream->codec_data) goto fail; - vgmstream->coding_type = coding_FFmpeg; - vgmstream->layout_type = layout_none; - break; - } - - case ATRAC3: { /* Techland PS3 extension [Sniper Ghost Warrior (PS3)] */ - int block_align, encoder_delay; - - block_align = xwb.block_align * vgmstream->channels; - encoder_delay = 1024; /* assumed */ - vgmstream->num_samples -= encoder_delay; - - vgmstream->codec_data = init_ffmpeg_atrac3_raw(sf, xwb.stream_offset,xwb.stream_size, vgmstream->num_samples,vgmstream->channels,vgmstream->sample_rate, block_align, encoder_delay); - if (!vgmstream->codec_data) goto fail; - vgmstream->coding_type = coding_FFmpeg; - vgmstream->layout_type = layout_none; - break; - } -#endif -#ifdef VGM_USE_VORBIS - case OGG: { /* Oddworld: Strangers Wrath (iOS/Android) extension */ - vgmstream->codec_data = init_ogg_vorbis(sf, xwb.stream_offset, xwb.stream_size, NULL); - if (!vgmstream->codec_data) goto fail; - vgmstream->coding_type = coding_OGG_VORBIS; - vgmstream->layout_type = layout_none; - break; - } -#endif - - case DSP: { /* Stardew Valley (Switch) extension */ - vgmstream->coding_type = coding_NGC_DSP; - vgmstream->layout_type = layout_interleave; - vgmstream->interleave_block_size = xwb.stream_size / xwb.channels; - - dsp_read_coefs(vgmstream,sf,xwb.stream_offset + 0x1c,vgmstream->interleave_block_size,!xwb.little_endian); - dsp_read_hist (vgmstream,sf,xwb.stream_offset + 0x3c,vgmstream->interleave_block_size,!xwb.little_endian); - xwb.stream_offset += 0x60; /* skip DSP header */ - break; - } - -#ifdef VGM_USE_ATRAC9 - case ATRAC9_RIFF: { /* Stardew Valley (Vita) extension */ - VGMSTREAM *temp_vgmstream = NULL; - STREAMFILE* temp_sf = NULL; - - /* standard RIFF, use subfile (seems doesn't use xwb loops) */ - VGM_ASSERT(xwb.loop_flag, "XWB: RIFF ATRAC9 loop flag found\n"); - - temp_sf = setup_subfile_streamfile(sf, xwb.stream_offset,xwb.stream_size, "at9"); - if (!temp_sf) goto fail; - - temp_vgmstream = init_vgmstream_riff(temp_sf); - close_streamfile(temp_sf); - if (!temp_vgmstream) goto fail; - - temp_vgmstream->num_streams = vgmstream->num_streams; - temp_vgmstream->stream_size = vgmstream->stream_size; - temp_vgmstream->meta_type = vgmstream->meta_type; - strcpy(temp_vgmstream->stream_name, vgmstream->stream_name); - - close_vgmstream(vgmstream); - return temp_vgmstream; - } -#endif - - default: - goto fail; - } - - - start_offset = xwb.stream_offset; - - if ( !vgmstream_open_stream(vgmstream,sf,start_offset) ) - goto fail; - return vgmstream; - -fail: - close_vgmstream(vgmstream); - return NULL; -} - -/* ****************************************************************************** */ - -static int get_xwb_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf) { - size_t read; - - if (!xwb->names_offset || !xwb->names_size || xwb->names_entry_size > maxsize) - goto fail; - - read = read_string(buf,xwb->names_entry_size, xwb->names_offset + xwb->names_entry_size*(target_subsong-1),sf); - if (read == 0) goto fail; - - return 1; - -fail: - return 0; -} - -static int get_xsb_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf) { - xsb_header xsb = {0}; - - xsb.selected_stream = target_subsong - 1; - if (!parse_xsb(&xsb, sf, xwb->wavebank_name)) - goto fail; - - if ((xwb->version <= XACT1_1_MAX && xsb.version > XSB_XACT1_2_MAX) || - (xwb->version <= XACT2_2_MAX && xsb.version > XSB_XACT2_MAX)) { - VGM_LOG("XSB: mismatched XACT versions: xsb v%i vs xwb v%i\n", xsb.version, xwb->version); - goto fail; - } - - //;VGM_LOG("XSB: name found=%i at %lx\n", xsb.parse_found, xsb.name_offset); - if (!xsb.name_len || xsb.name[0] == '\0') - goto fail; - - strncpy(buf,xsb.name,maxsize); - buf[maxsize-1] = '\0'; - return 1; -fail: - return 0; -} - -static int get_wbh_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf) { - int selected_stream = target_subsong - 1; - int version, name_count; - off_t offset, name_number; - - if (read_u32be(0x00, sf) != 0x57424844) /* "WBHD" */ - goto fail; - version = read_u32le(0x04, sf); - if (version != 1) - goto fail; - name_count = read_u32le(0x08, sf); - - if (selected_stream > name_count) - goto fail; - - /* next table: - * - 0x00: wave id? (ordered from 0 to N) - * - 0x04: always 0 */ - offset = 0x10 + 0x08 * name_count; - - name_number = 0; - while (offset < get_streamfile_size(sf)) { - size_t name_len = read_string(buf, maxsize, offset, sf) + 1; - - if (name_len == 0) - goto fail; - if (name_number == selected_stream) - break; - - name_number++; - offset += name_len; - } - - return 1; -fail: - return 0; -} - -static void get_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf_xwb) { - STREAMFILE* sf_name = NULL; - int name_found; - - /* try to get the stream name in the .xwb, though they are very rarely included */ - name_found = get_xwb_name(buf, maxsize, target_subsong, xwb, sf_xwb); - if (name_found) return; - - /* try again in a companion files */ - - if (xwb->version == 1) { - /* .wbh, a simple name container */ - sf_name = open_streamfile_by_ext(sf_xwb, "wbh"); - if (!sf_name) return; /* rarely found [Pac-Man World 2 (Xbox)] */ - - name_found = get_wbh_name(buf, maxsize, target_subsong, xwb, sf_name); - close_streamfile(sf_name); - } - else { - /* .xsb, a comically complex cue format */ - sf_name = open_xsb_filename_pair(sf_xwb); - if (!sf_name) return; /* not all xwb have xsb though */ - - name_found = get_xsb_name(buf, maxsize, target_subsong, xwb, sf_name); - close_streamfile(sf_name); - } - - - if (!name_found) { - buf[0] = '\0'; - } -} +#include "meta.h" +#include "../coding/coding.h" +#include +#include "xwb_xsb.h" + +/* most info from XWBtool, xactwb.h, xact2wb.h and xact3wb.h */ + +#define WAVEBANK_FLAGS_COMPACT 0x00020000 // Bank uses compact format +#define WAVEBANKENTRY_FLAGS_IGNORELOOP 0x00000008 // Used internally when the loop region can't be used (no idea...) + +/* the x.x version is just to make it clearer, MS only classifies XACT as 1/2/3 */ +#define XACT1_0_MAX 1 /* Project Gotham Racing 2 (v1), Silent Hill 4 (v1), Shin Megami Tensei NINE (v1) */ +#define XACT1_1_MAX 3 /* Unreal Championship (v2), The King of Fighters 2003 (v3) */ +#define XACT2_0_MAX 34 /* Dead or Alive 4 (v17), Kameo (v23), Table Tennis (v34) */ // v35/36/37 too? +#define XACT2_1_MAX 38 /* Prey (v38) */ // v39 too? +#define XACT2_2_MAX 41 /* Blue Dragon (v40) */ +#define XACT3_0_MAX 46 /* Ninja Blade (t43 v42), Persona 4 Ultimax NESSICA (t45 v43) */ +#define XACT_TECHLAND 0x10000 /* Sniper Ghost Warrior, Nail'd (PS3/X360), equivalent to XACT3_0 */ +#define XACT_CRACKDOWN 0x87 /* Crackdown 1, equivalent to XACT2_2 */ + +static const int wma_avg_bps_index[7] = { + 12000, 24000, 4000, 6000, 8000, 20000, 2500 +}; +static const int wma_block_align_index[17] = { + 929, 1487, 1280, 2230, 8917, 8192, 4459, 5945, 2304, 1536, 1485, 1008, 2731, 4096, 6827, 5462, 1280 +}; + + +typedef enum { PCM, XBOX_ADPCM, MS_ADPCM, XMA1, XMA2, WMA, XWMA, ATRAC3, OGG, DSP, ATRAC9_RIFF } xact_codec; +typedef struct { + int little_endian; + int version; + + /* segments */ + off_t base_offset; + size_t base_size; + off_t entry_offset; + size_t entry_size; + off_t names_offset; + size_t names_size; + size_t names_entry_size; + off_t extra_offset; + size_t extra_size; + off_t data_offset; + size_t data_size; + + off_t stream_offset; + size_t stream_size; + + uint32_t base_flags; + size_t entry_elem_size; + size_t entry_alignment; + int total_subsongs; + + uint32_t entry_flags; + uint32_t format; + int tag; + int channels; + int sample_rate; + int block_align; + int bits_per_sample; + xact_codec codec; + + int loop_flag; + uint32_t num_samples; + uint32_t loop_start; + uint32_t loop_end; + uint32_t loop_start_sample; + uint32_t loop_end_sample; + + char wavebank_name[64+1]; + + int is_crackdown; + int fix_xma_num_samples; + int fix_xma_loop_samples; +} xwb_header; + +static void get_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf); + + +/* XWB - XACT Wave Bank (Microsoft SDK format for XBOX/XBOX360/Windows) */ +VGMSTREAM* init_vgmstream_xwb(STREAMFILE* sf) { + VGMSTREAM* vgmstream = NULL; + off_t start_offset, offset, suboffset; + xwb_header xwb = {0}; + int target_subsong = sf->stream_index; + uint32_t (*read_u32)(off_t,STREAMFILE*) = NULL; + int32_t (*read_s32)(off_t,STREAMFILE*) = NULL; + + + /* checks */ + /* .xwb: standard + * .xna: Touhou Makukasai ~ Fantasy Danmaku Festival (PC) + * (extensionless): Ikaruga (X360/PC), Grabbed by the Ghoulies (Xbox) */ + if (!check_extensions(sf,"xwb,xna,")) + goto fail; + if ((read_u32be(0x00,sf) != 0x57424E44) && /* "WBND" (LE) */ + (read_u32be(0x00,sf) != 0x444E4257)) /* "DNBW" (BE) */ + goto fail; + + xwb.little_endian = read_u32be(0x00,sf) == 0x57424E44; /* WBND */ + if (xwb.little_endian) { + read_u32 = read_u32le; + read_s32 = read_s32le; + } else { + read_u32 = read_u32be; + read_s32 = read_s32be; + } + + + /* read main header (WAVEBANKHEADER) */ + xwb.version = read_u32(0x04, sf); /* XACT3: 0x04=tool version, 0x08=header version */ + + /* Crackdown 1 (X360), essentially XACT2 but may have split header in some cases, compact entries change */ + if (xwb.version == XACT_CRACKDOWN) { + xwb.version = XACT2_2_MAX; + xwb.is_crackdown = 1; + } + + /* read segment offsets (SEGIDX) */ + if (xwb.version <= XACT1_0_MAX) { + xwb.total_subsongs = read_s32(0x0c, sf); + read_string(xwb.wavebank_name,0x10+1, 0x10, sf); /* null-terminated */ + xwb.base_offset = 0; + xwb.base_size = 0; + xwb.entry_offset = 0x50; + xwb.entry_elem_size = 0x14; + xwb.entry_size = xwb.entry_elem_size * xwb.total_subsongs; + xwb.data_offset = xwb.entry_offset + xwb.entry_size; + xwb.data_size = get_streamfile_size(sf) - xwb.data_offset; + + xwb.names_offset = 0; + xwb.names_size = 0; + xwb.names_entry_size= 0; + xwb.extra_offset = 0; + xwb.extra_size = 0; + } + else { + offset = xwb.version <= XACT2_2_MAX ? 0x08 : 0x0c; + xwb.base_offset = read_s32(offset+0x00, sf);//BANKDATA + xwb.base_size = read_s32(offset+0x04, sf); + xwb.entry_offset= read_s32(offset+0x08, sf);//ENTRYMETADATA + xwb.entry_size = read_s32(offset+0x0c, sf); + + /* read extra segments (values can be 0 == no segment) */ + if (xwb.version <= XACT1_1_MAX) { + xwb.names_offset = read_s32(offset+0x10, sf);//ENTRYNAMES + xwb.names_size = read_s32(offset+0x14, sf); + xwb.names_entry_size= 0x40; + xwb.extra_offset = 0; + xwb.extra_size = 0; + suboffset = 0x04*2; + } + else if (xwb.version <= XACT2_1_MAX) { + xwb.names_offset = read_s32(offset+0x10, sf);//ENTRYNAMES + xwb.names_size = read_s32(offset+0x14, sf); + xwb.names_entry_size= 0x40; + xwb.extra_offset = read_s32(offset+0x18, sf);//EXTRA + xwb.extra_size = read_s32(offset+0x1c, sf); + suboffset = 0x04*2 + 0x04*2; + } else { + xwb.extra_offset = read_s32(offset+0x10, sf);//SEEKTABLES + xwb.extra_size = read_s32(offset+0x14, sf); + xwb.names_offset = read_s32(offset+0x18, sf);//ENTRYNAMES + xwb.names_size = read_s32(offset+0x1c, sf); + xwb.names_entry_size= 0x40; + suboffset = 0x04*2 + 0x04*2; + } + + xwb.data_offset = read_s32(offset+0x10+suboffset, sf);//ENTRYWAVEDATA + xwb.data_size = read_s32(offset+0x14+suboffset, sf); + + /* for Techland's XWB with no data */ + if (xwb.base_offset == 0) goto fail; + + /* read base entry (WAVEBANKDATA) */ + offset = xwb.base_offset; + xwb.base_flags = read_u32(offset+0x00, sf); + xwb.total_subsongs = read_s32(offset+0x04, sf); + read_string(xwb.wavebank_name,0x40+1, offset+0x08, sf); /* null-terminated */ + suboffset = 0x08 + (xwb.version <= XACT1_1_MAX ? 0x10 : 0x40); + xwb.entry_elem_size = read_s32(offset+suboffset+0x00, sf); + /* suboff+0x04: meta name entry size */ + xwb.entry_alignment = read_s32(offset+suboffset+0x08, sf); /* usually 1 dvd sector */ + xwb.format = read_s32(offset+suboffset+0x0c, sf); /* compact mode only */ + /* suboff+0x10: build time 64b (XACT2/3) */ + } + + //;VGM_LOG("XWB: wavebank name='%s'\n", xwb.wavebank_name); + + if (target_subsong == 0) target_subsong = 1; /* auto: default to 1 */ + if (target_subsong < 0 || target_subsong > xwb.total_subsongs || xwb.total_subsongs < 1) goto fail; + + + /* read stream entry (WAVEBANKENTRY) */ + offset = xwb.entry_offset + (target_subsong-1) * xwb.entry_elem_size; + + + if ((xwb.base_flags & WAVEBANK_FLAGS_COMPACT) && xwb.is_crackdown) { + /* mutant compact (w/ entry_elem_size=0x08) [Crackdown (X360)] */ + uint32_t entry, size_sectors, sector_offset; + + entry = read_u32(offset+0x00, sf); + size_sectors = ((entry >> 19) & 0x1FFF); /* 13b, exact size in sectors */ + sector_offset = (entry & 0x7FFFF); /* 19b, offset within data in sectors */ + xwb.stream_size = size_sectors * xwb.entry_alignment; + xwb.num_samples = read_u32(offset+0x04, sf); + + xwb.stream_offset = xwb.data_offset + sector_offset * xwb.entry_alignment; + } + else if (xwb.base_flags & WAVEBANK_FLAGS_COMPACT) { + /* compact entry [NFL Fever 2004 demo from Amped 2 (Xbox)] */ + uint32_t entry, size_deviation, sector_offset; + off_t next_stream_offset; + + entry = read_u32(offset+0x00, sf); + size_deviation = ((entry >> 21) & 0x7FF); /* 11b, padding data for sector alignment in bytes*/ + sector_offset = (entry & 0x1FFFFF); /* 21b, offset within data in sectors */ + + xwb.stream_offset = xwb.data_offset + sector_offset * xwb.entry_alignment; + + /* find size using next offset */ + if (target_subsong < xwb.total_subsongs) { + uint32_t next_entry = read_u32(offset + xwb.entry_elem_size, sf); + next_stream_offset = xwb.data_offset + (next_entry & 0x1FFFFF) * xwb.entry_alignment; + } + else { /* for last entry (or first, when subsongs = 1) */ + next_stream_offset = xwb.data_offset + xwb.data_size; + } + xwb.stream_size = next_stream_offset - xwb.stream_offset - size_deviation; + } + else if (xwb.version <= XACT1_0_MAX) { + xwb.format = read_u32(offset+0x00, sf); + xwb.stream_offset = xwb.data_offset + read_u32(offset+0x04, sf); + xwb.stream_size = read_u32(offset+0x08, sf); + + xwb.loop_start = read_u32(offset+0x0c, sf); + xwb.loop_end = read_u32(offset+0x10, sf);//length + } + else { + uint32_t entry_info = read_u32(offset+0x00, sf); + if (xwb.version <= XACT1_1_MAX) { + xwb.entry_flags = entry_info; + } else { + xwb.entry_flags = (entry_info) & 0xF; /*4b*/ + xwb.num_samples = (entry_info >> 4) & 0x0FFFFFFF; /*28b*/ + } + xwb.format = read_u32(offset+0x04, sf); + xwb.stream_offset = xwb.data_offset + read_u32(offset+0x08, sf); + xwb.stream_size = read_u32(offset+0x0c, sf); + + if (xwb.version <= XACT2_1_MAX) { /* LoopRegion (bytes) */ + xwb.loop_start = read_u32(offset+0x10, sf); + xwb.loop_end = read_u32(offset+0x14, sf);//length (LoopRegion) or offset (XMALoopRegion in late XACT2) + } else { /* LoopRegion (samples) */ + xwb.loop_start_sample = read_u32(offset+0x10, sf); + xwb.loop_end_sample = read_u32(offset+0x14, sf) + xwb.loop_start_sample; + } + } + + + /* parse format */ + if (xwb.version <= XACT1_0_MAX) { + xwb.bits_per_sample = (xwb.format >> 31) & 0x1; /*1b*/ + xwb.sample_rate = (xwb.format >> 4) & 0x7FFFFFF; /*27b*/ + xwb.channels = (xwb.format >> 1) & 0x7; /*3b*/ + xwb.tag = (xwb.format) & 0x1; /*1b*/ + } + else if (xwb.version <= XACT1_1_MAX) { + xwb.bits_per_sample = (xwb.format >> 31) & 0x1; /*1b*/ + xwb.sample_rate = (xwb.format >> 5) & 0x3FFFFFF; /*26b*/ + xwb.channels = (xwb.format >> 2) & 0x7; /*3b*/ + xwb.tag = (xwb.format) & 0x3; /*2b*/ + } + else if (xwb.version <= XACT2_0_MAX) { + xwb.bits_per_sample = (xwb.format >> 31) & 0x1; /*1b*/ + xwb.block_align = (xwb.format >> 24) & 0xFF; /*8b*/ + xwb.sample_rate = (xwb.format >> 4) & 0x7FFFF; /*19b*/ + xwb.channels = (xwb.format >> 1) & 0x7; /*3b*/ + xwb.tag = (xwb.format) & 0x1; /*1b*/ + } + else { + xwb.bits_per_sample = (xwb.format >> 31) & 0x1; /*1b*/ + xwb.block_align = (xwb.format >> 23) & 0xFF; /*8b*/ + xwb.sample_rate = (xwb.format >> 5) & 0x3FFFF; /*18b*/ + xwb.channels = (xwb.format >> 2) & 0x7; /*3b*/ + xwb.tag = (xwb.format) & 0x3; /*2b*/ + } + + /* standardize tag to codec */ + if (xwb.version <= XACT1_0_MAX) { + switch(xwb.tag){ + case 0: xwb.codec = PCM; break; + case 1: xwb.codec = XBOX_ADPCM; break; + default: goto fail; + } + } + else if (xwb.version <= XACT1_1_MAX) { + switch(xwb.tag){ + case 0: xwb.codec = PCM; break; + case 1: xwb.codec = XBOX_ADPCM; break; + case 2: xwb.codec = WMA; break; + case 3: xwb.codec = OGG; break; /* extension */ + default: goto fail; + } + } + else if (xwb.version <= XACT2_2_MAX) { + switch(xwb.tag) { + case 0: xwb.codec = PCM; break; + /* Table Tennis (v34): XMA1, Prey (v38): XMA2, v35/36/37: ? */ + case 1: xwb.codec = xwb.version <= XACT2_0_MAX ? XMA1 : XMA2; break; + case 2: xwb.codec = MS_ADPCM; break; + default: goto fail; + } + } + else { + switch(xwb.tag) { + case 0: xwb.codec = PCM; break; + case 1: xwb.codec = XMA2; break; + case 2: xwb.codec = MS_ADPCM; break; + case 3: xwb.codec = XWMA; break; + default: goto fail; + } + } + + + /* format hijacks from creative devs, using non-official codecs */ + if (xwb.version == XACT_TECHLAND && xwb.codec == XMA2 /* XACT_TECHLAND used in their X360 games too */ + && (xwb.block_align == 0x60 || xwb.block_align == 0x98 || xwb.block_align == 0xc0) ) { /* standard ATRAC3 blocks sizes */ + /* Techland ATRAC3 [Nail'd (PS3), Sniper: Ghost Warrior (PS3)] */ + xwb.codec = ATRAC3; + + /* num samples uses a modified entry_info format (maybe skip samples + samples? sfx use the standard format) + * ignore for now and just calc max samples */ + xwb.num_samples = atrac3_bytes_to_samples(xwb.stream_size, xwb.block_align * xwb.channels); + } + else if (xwb.codec == OGG) { + /* Oddworld: Stranger's Wrath (iOS/Android) */ + xwb.num_samples = xwb.stream_size / (2 * xwb.channels); /* uncompressed bytes */ + xwb.stream_size = xwb.loop_end; + xwb.loop_start = 0; + xwb.loop_end = 0; + } + else if (xwb.version == XACT3_0_MAX && xwb.codec == XMA2 + && (xwb.bits_per_sample == 0x00 || xwb.bits_per_sample == 0x01) /* bps=0+ba=2 in mono? (Blossom Tales) */ + && (xwb.block_align == 0x02 || xwb.block_align == 0x04) + && read_u32le(xwb.stream_offset + 0x08, sf) == xwb.sample_rate /* DSP header */ + && read_u16le(xwb.stream_offset + 0x0e, sf) == 0 + && read_u32le(xwb.stream_offset + 0x18, sf) == 2 + /*&& xwb.data_size == 0x55951c1c*/) { /* some kind of id in Stardew Valley? */ + /* Stardew Valley (Switch), Skulls of the Shogun (Switch): full interleaved DSPs (including headers) */ + xwb.codec = DSP; + } + else if (xwb.version == XACT3_0_MAX && xwb.codec == XMA2 + && xwb.bits_per_sample == 0x01 && xwb.block_align == 0x04 + && xwb.data_size == 0x4e0a1000) { /* some kind of id? */ + /* Stardew Valley (Vita), standard RIFF with ATRAC9 */ + xwb.codec = ATRAC9_RIFF; + } + + + /* test loop after the above fixes */ + xwb.loop_flag = (xwb.loop_end > 0 || xwb.loop_end_sample > xwb.loop_start) + && !(xwb.entry_flags & WAVEBANKENTRY_FLAGS_IGNORELOOP); + + /* Oddworld OGG the data_size value is size of uncompressed bytes instead; DSP uses some id/config as value */ + if (xwb.codec != OGG && xwb.codec != DSP && xwb.codec != ATRAC9_RIFF) { + /* some low-q rips don't remove padding, relax validation a bit */ + if (xwb.data_offset + xwb.stream_size > get_streamfile_size(sf)) + goto fail; + //if (xwb.data_offset + xwb.data_size > get_streamfile_size(sf)) /* badly split */ + // goto fail; + } + + + /* fix samples */ + if (xwb.version <= XACT2_2_MAX && xwb.codec == PCM) { + int bits_per_sample = xwb.bits_per_sample == 0 ? 8 : 16; + xwb.num_samples = pcm_bytes_to_samples(xwb.stream_size, xwb.channels, bits_per_sample); + if (xwb.loop_flag) { + xwb.loop_start_sample = pcm_bytes_to_samples(xwb.loop_start, xwb.channels, bits_per_sample); + xwb.loop_end_sample = pcm_bytes_to_samples(xwb.loop_start + xwb.loop_end, xwb.channels, bits_per_sample); + } + } + else if (xwb.version <= XACT1_1_MAX && xwb.codec == XBOX_ADPCM) { + xwb.block_align = 0x24 * xwb.channels; /* not really needed... */ + xwb.num_samples = xbox_ima_bytes_to_samples(xwb.stream_size, xwb.channels); + if (xwb.loop_flag) { + xwb.loop_start_sample = xbox_ima_bytes_to_samples(xwb.loop_start, xwb.channels); + xwb.loop_end_sample = xbox_ima_bytes_to_samples(xwb.loop_start + xwb.loop_end, xwb.channels); + } + } + else if (xwb.version <= XACT2_2_MAX && xwb.codec == MS_ADPCM && xwb.loop_flag) { + int block_size = (xwb.block_align + 22) * xwb.channels; /*22=CONVERSION_OFFSET (?)*/ + + xwb.loop_start_sample = msadpcm_bytes_to_samples(xwb.loop_start, block_size, xwb.channels); + xwb.loop_end_sample = msadpcm_bytes_to_samples(xwb.loop_start + xwb.loop_end, block_size, xwb.channels); + } + else if (xwb.version <= XACT2_1_MAX && (xwb.codec == XMA1 || xwb.codec == XMA2) && xwb.loop_flag) { + /* v38: byte offset, v40+: sample offset, v39: ? */ + /* need to manually find sample offsets, thanks to Microsoft's dumb headers */ + ms_sample_data msd = {0}; + + msd.xma_version = xwb.codec == XMA1 ? 1 : 2; + msd.channels = xwb.channels; + msd.data_offset = xwb.stream_offset; + msd.data_size = xwb.stream_size; + msd.loop_flag = xwb.loop_flag; + msd.loop_start_b = xwb.loop_start; /* bit offset in the stream */ + msd.loop_end_b = (xwb.loop_end >> 4); /*28b */ + /* XACT adds +1 to the subframe, but this means 0 can't be used? */ + msd.loop_end_subframe = ((xwb.loop_end >> 2) & 0x3) + 1; /* 2b */ + msd.loop_start_subframe = ((xwb.loop_end >> 0) & 0x3) + 1; /* 2b */ + + xma_get_samples(&msd, sf); + xwb.loop_start_sample = msd.loop_start_sample; + xwb.loop_end_sample = msd.loop_end_sample; + + /* if provided, xwb.num_samples is equal to msd.num_samples after proper adjustments (+ 128 - start_skip - end_skip) */ + xwb.fix_xma_loop_samples = 1; + xwb.fix_xma_num_samples = 0; + + /* for XWB v22 (and below?) this seems normal [Project Gotham Racing (X360)] */ + if (xwb.num_samples == 0) { + xwb.num_samples = msd.num_samples; + xwb.fix_xma_num_samples = 1; + } + } + else if ((xwb.codec == XMA1 || xwb.codec == XMA2) && xwb.loop_flag) { + /* unlike prev versions, xwb.num_samples is the full size without adjustments */ + xwb.fix_xma_loop_samples = 1; + xwb.fix_xma_num_samples = 1; + + /* Crackdown does use xwb.num_samples after adjustments (but not loops) */ + if (xwb.is_crackdown) { + xwb.fix_xma_num_samples = 0; + } + } + + + /* build the VGMSTREAM */ + vgmstream = allocate_vgmstream(xwb.channels,xwb.loop_flag); + if (!vgmstream) goto fail; + + vgmstream->sample_rate = xwb.sample_rate; + vgmstream->num_samples = xwb.num_samples; + vgmstream->loop_start_sample = xwb.loop_start_sample; + vgmstream->loop_end_sample = xwb.loop_end_sample; + vgmstream->num_streams = xwb.total_subsongs; + vgmstream->stream_size = xwb.stream_size; + vgmstream->meta_type = meta_XWB; + get_name(vgmstream->stream_name,STREAM_NAME_SIZE, target_subsong, &xwb, sf); + + switch(xwb.codec) { + case PCM: /* Unreal Championship (Xbox)[PCM8], KOF2003 (Xbox)[PCM16LE], Otomedius (X360)[PCM16BE] */ + vgmstream->coding_type = xwb.bits_per_sample == 0 ? coding_PCM8_U : + (xwb.little_endian ? coding_PCM16LE : coding_PCM16BE); + vgmstream->layout_type = xwb.channels > 1 ? layout_interleave : layout_none; + vgmstream->interleave_block_size = xwb.bits_per_sample == 0 ? 0x01 : 0x02; + break; + + case XBOX_ADPCM: /* Silent Hill 4 (Xbox) */ + vgmstream->coding_type = coding_XBOX_IMA; + vgmstream->layout_type = layout_none; + break; + + case MS_ADPCM: /* Persona 4 Ultimax (AC) */ + vgmstream->coding_type = coding_MSADPCM; + vgmstream->layout_type = layout_none; + vgmstream->frame_size = (xwb.block_align + 22) * xwb.channels; /*22=CONVERSION_OFFSET (?)*/ + break; + +#ifdef VGM_USE_FFMPEG + case XMA1: { /* Kameo (X360), Table Tennis (X360) */ + uint8_t buf[0x100]; + int bytes; + + bytes = ffmpeg_make_riff_xma1(buf, sizeof(buf), vgmstream->num_samples, xwb.stream_size, vgmstream->channels, vgmstream->sample_rate, 0); + vgmstream->codec_data = init_ffmpeg_header_offset(sf, buf, bytes, xwb.stream_offset,xwb.stream_size); + if (!vgmstream->codec_data) goto fail; + vgmstream->coding_type = coding_FFmpeg; + vgmstream->layout_type = layout_none; + + xma_fix_raw_samples(vgmstream, sf, xwb.stream_offset,xwb.stream_size, 0, xwb.fix_xma_num_samples,xwb.fix_xma_loop_samples); + + /* this fixes some XMA1, perhaps the above isn't reading end_skip correctly (doesn't happen for all files though) */ + if (vgmstream->loop_flag && + vgmstream->loop_end_sample > vgmstream->num_samples) { + VGM_LOG("XWB: fix XMA1 looping\n"); + vgmstream->loop_end_sample = vgmstream->num_samples; + } + break; + } + + case XMA2: { /* Blue Dragon (X360) */ + uint8_t buf[0x100]; + int bytes, block_size, block_count; + + block_size = 0x10000; /* XACT default */ + block_count = xwb.stream_size / block_size + (xwb.stream_size % block_size ? 1 : 0); + + bytes = ffmpeg_make_riff_xma2(buf, sizeof(buf), vgmstream->num_samples, xwb.stream_size, vgmstream->channels, vgmstream->sample_rate, block_count, block_size); + vgmstream->codec_data = init_ffmpeg_header_offset(sf, buf, bytes, xwb.stream_offset,xwb.stream_size); + if (!vgmstream->codec_data) goto fail; + vgmstream->coding_type = coding_FFmpeg; + vgmstream->layout_type = layout_none; + + xma_fix_raw_samples(vgmstream, sf, xwb.stream_offset,xwb.stream_size, 0, xwb.fix_xma_num_samples,xwb.fix_xma_loop_samples); + break; + } + + case WMA: { /* WMAudio1 (WMA v2): Prince of Persia 2 port (Xbox) */ + ffmpeg_codec_data *ffmpeg_data = NULL; + + ffmpeg_data = init_ffmpeg_offset(sf, xwb.stream_offset,xwb.stream_size); + if ( !ffmpeg_data ) goto fail; + vgmstream->codec_data = ffmpeg_data; + vgmstream->coding_type = coding_FFmpeg; + vgmstream->layout_type = layout_none; + + /* no wma_bytes_to_samples, this should be ok */ + if (!vgmstream->num_samples) + vgmstream->num_samples = (int32_t)ffmpeg_data->totalSamples; + break; + } + + case XWMA: { /* WMAudio2 (WMA v2): BlazBlue (X360), WMAudio3 (WMA Pro): Bullet Witch (PC) voices */ + uint8_t buf[0x100]; + int bytes, bps_index, block_align, block_index, avg_bps, wma_codec; + + bps_index = (xwb.block_align >> 5); /* upper 3b bytes-per-second index (docs say 2b+6b but are wrong) */ + block_index = (xwb.block_align) & 0x1F; /*lower 5b block alignment index */ + if (bps_index >= 7) goto fail; + if (block_index >= 17) goto fail; + + avg_bps = wma_avg_bps_index[bps_index]; + block_align = wma_block_align_index[block_index]; + wma_codec = xwb.bits_per_sample ? 0x162 : 0x161; /* 0=WMAudio2, 1=WMAudio3 */ + + bytes = ffmpeg_make_riff_xwma(buf, sizeof(buf), wma_codec, xwb.stream_size, vgmstream->channels, vgmstream->sample_rate, avg_bps, block_align); + vgmstream->codec_data = init_ffmpeg_header_offset(sf, buf, bytes, xwb.stream_offset,xwb.stream_size); + if (!vgmstream->codec_data) goto fail; + vgmstream->coding_type = coding_FFmpeg; + vgmstream->layout_type = layout_none; + break; + } + + case ATRAC3: { /* Techland PS3 extension [Sniper Ghost Warrior (PS3)] */ + int block_align, encoder_delay; + + block_align = xwb.block_align * vgmstream->channels; + encoder_delay = 1024; /* assumed */ + vgmstream->num_samples -= encoder_delay; + + vgmstream->codec_data = init_ffmpeg_atrac3_raw(sf, xwb.stream_offset,xwb.stream_size, vgmstream->num_samples,vgmstream->channels,vgmstream->sample_rate, block_align, encoder_delay); + if (!vgmstream->codec_data) goto fail; + vgmstream->coding_type = coding_FFmpeg; + vgmstream->layout_type = layout_none; + break; + } +#endif +#ifdef VGM_USE_VORBIS + case OGG: { /* Oddworld: Strangers Wrath (iOS/Android) extension */ + vgmstream->codec_data = init_ogg_vorbis(sf, xwb.stream_offset, xwb.stream_size, NULL); + if (!vgmstream->codec_data) goto fail; + vgmstream->coding_type = coding_OGG_VORBIS; + vgmstream->layout_type = layout_none; + break; + } +#endif + + case DSP: { /* Stardew Valley (Switch) extension */ + vgmstream->coding_type = coding_NGC_DSP; + vgmstream->layout_type = layout_interleave; + vgmstream->interleave_block_size = xwb.stream_size / xwb.channels; + + dsp_read_coefs(vgmstream,sf,xwb.stream_offset + 0x1c,vgmstream->interleave_block_size,!xwb.little_endian); + dsp_read_hist (vgmstream,sf,xwb.stream_offset + 0x3c,vgmstream->interleave_block_size,!xwb.little_endian); + xwb.stream_offset += 0x60; /* skip DSP header */ + break; + } + +#ifdef VGM_USE_ATRAC9 + case ATRAC9_RIFF: { /* Stardew Valley (Vita) extension */ + VGMSTREAM *temp_vgmstream = NULL; + STREAMFILE* temp_sf = NULL; + + /* standard RIFF, use subfile (seems doesn't use xwb loops) */ + VGM_ASSERT(xwb.loop_flag, "XWB: RIFF ATRAC9 loop flag found\n"); + + temp_sf = setup_subfile_streamfile(sf, xwb.stream_offset,xwb.stream_size, "at9"); + if (!temp_sf) goto fail; + + temp_vgmstream = init_vgmstream_riff(temp_sf); + close_streamfile(temp_sf); + if (!temp_vgmstream) goto fail; + + temp_vgmstream->num_streams = vgmstream->num_streams; + temp_vgmstream->stream_size = vgmstream->stream_size; + temp_vgmstream->meta_type = vgmstream->meta_type; + strcpy(temp_vgmstream->stream_name, vgmstream->stream_name); + + close_vgmstream(vgmstream); + return temp_vgmstream; + } +#endif + + default: + goto fail; + } + + + start_offset = xwb.stream_offset; + + if ( !vgmstream_open_stream(vgmstream,sf,start_offset) ) + goto fail; + return vgmstream; + +fail: + close_vgmstream(vgmstream); + return NULL; +} + +/* ****************************************************************************** */ + +static int get_xwb_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf) { + size_t read; + + if (!xwb->names_offset || !xwb->names_size || xwb->names_entry_size > maxsize) + goto fail; + + read = read_string(buf,xwb->names_entry_size, xwb->names_offset + xwb->names_entry_size*(target_subsong-1),sf); + if (read == 0) goto fail; + + return 1; + +fail: + return 0; +} + +static int get_xsb_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf) { + xsb_header xsb = {0}; + + xsb.selected_stream = target_subsong - 1; + if (!parse_xsb(&xsb, sf, xwb->wavebank_name)) + goto fail; + + if ((xwb->version <= XACT1_1_MAX && xsb.version > XSB_XACT1_2_MAX) || + (xwb->version <= XACT2_2_MAX && xsb.version > XSB_XACT2_2_MAX)) { + VGM_LOG("XSB: mismatched XACT versions: xsb v%i vs xwb v%i\n", xsb.version, xwb->version); + goto fail; + } + + //;VGM_LOG("XSB: name found=%i at %lx\n", xsb.parse_found, xsb.name_offset); + if (!xsb.name_len || xsb.name[0] == '\0') + goto fail; + + strncpy(buf,xsb.name,maxsize); + buf[maxsize-1] = '\0'; + return 1; +fail: + return 0; +} + +static int get_wbh_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf) { + int selected_stream = target_subsong - 1; + int version, name_count; + off_t offset, name_number; + + if (read_u32be(0x00, sf) != 0x57424844) /* "WBHD" */ + goto fail; + version = read_u32le(0x04, sf); + if (version != 1) + goto fail; + name_count = read_u32le(0x08, sf); + + if (selected_stream > name_count) + goto fail; + + /* next table: + * - 0x00: wave id? (ordered from 0 to N) + * - 0x04: always 0 */ + offset = 0x10 + 0x08 * name_count; + + name_number = 0; + while (offset < get_streamfile_size(sf)) { + size_t name_len = read_string(buf, maxsize, offset, sf) + 1; + + if (name_len == 0) + goto fail; + if (name_number == selected_stream) + break; + + name_number++; + offset += name_len; + } + + return 1; +fail: + return 0; +} + +static void get_name(char* buf, size_t maxsize, int target_subsong, xwb_header* xwb, STREAMFILE* sf_xwb) { + STREAMFILE* sf_name = NULL; + int name_found; + + /* try to get the stream name in the .xwb, though they are very rarely included */ + name_found = get_xwb_name(buf, maxsize, target_subsong, xwb, sf_xwb); + if (name_found) return; + + /* try again in a companion files */ + + if (xwb->version == 1) { + /* .wbh, a simple name container */ + sf_name = open_streamfile_by_ext(sf_xwb, "wbh"); + if (!sf_name) return; /* rarely found [Pac-Man World 2 (Xbox)] */ + + name_found = get_wbh_name(buf, maxsize, target_subsong, xwb, sf_name); + close_streamfile(sf_name); + } + else { + /* .xsb, a comically complex cue format */ + sf_name = open_xsb_filename_pair(sf_xwb); + if (!sf_name) return; /* not all xwb have xsb though */ + + name_found = get_xsb_name(buf, maxsize, target_subsong, xwb, sf_name); + close_streamfile(sf_name); + } + + + if (!name_found) { + buf[0] = '\0'; + } +} diff --git a/src/meta/xwb_xsb.h b/src/meta/xwb_xsb.h index b68e3059..e2b8b5f1 100644 --- a/src/meta/xwb_xsb.h +++ b/src/meta/xwb_xsb.h @@ -5,7 +5,9 @@ #define XSB_XACT1_0_MAX 5 /* Unreal Championship (Xbox) */ #define XSB_XACT1_1_MAX 8 /* Die Hard: Vendetta (Xbox) */ #define XSB_XACT1_2_MAX 11 /* other Xbox games */ -#define XSB_XACT2_MAX 41 /* other PC/X360 games */ +#define XSB_XACT2_0_MAX 34 /* Table Tennis (v34) */ +//#define XSB_XACT2_1_MAX 38 /* Prey (v38) */ // v39 too? +#define XSB_XACT2_2_MAX 41 /* other PC/X360 games */ typedef struct { @@ -44,10 +46,10 @@ typedef struct { static void xsb_check_stream(xsb_header *xsb, int stream_index, int wavebank_index, off_t name_offset, STREAMFILE *sf) { if (xsb->parse_done) return; - //;VGM_LOG("XSB old: found stream=%i vs %i, wavebank=%i vs %i, name_offset=%lx\n", stream_index, xsb->selected_stream, wavebank_index, xsb->selected_wavebank, name_offset); + //;VGM_LOG("XSB: found stream=%i vs %i, wavebank=%i vs %i, name_offset=%lx\n", stream_index, xsb->selected_stream, wavebank_index, xsb->selected_wavebank, name_offset); if (stream_index < 0 || stream_index > 0xFFF || wavebank_index < 0 || wavebank_index > xsb->wavebanks_count) { - VGM_LOG("XSB old: bad stream=%i, wavebank=%i\n", stream_index, wavebank_index); + VGM_LOG("XSB: bad stream=%i, wavebank=%i\n", stream_index, wavebank_index); return; } @@ -277,7 +279,7 @@ static int parse_xsb_old_cues(xsb_header *xsb, STREAMFILE *sf) { /* 0x0c: some low value or flag? */ /* 0x0e: some index? */ /* 0x10: 4 fields? (-1 or 7) */ - //;VGM_LOG("XSB old index %i at %lx: flags=%x, entry=%i, name_offset=%lx\n", i, offset, flags, cue_entry, name_offset); + //;VGM_LOG("XSB old index %i at %lx: entry=%i, name_offset=%lx\n", i, offset, cue_entry, name_offset); if (cue_entry < 0) { jump_offset = read_s32(offset + 0x08, sf); @@ -315,12 +317,12 @@ static int parse_xsb_clip(xsb_header *xsb, off_t offset, off_t name_offset, STRE uint32_t flags; int stream_index, wavebank_index; - int i, t, track_count, event_count; + int i, t, track_count, event_count, size; event_count = read_s8(offset + 0x00, sf); - //;VGM_LOG("XSB clip at %lx\n", offset); + //;VGM_LOG("XSB clip at %lx, events=%i\n", offset, event_count); offset += 0x01; for (i = 0; i < event_count; i++) { @@ -333,16 +335,26 @@ static int parse_xsb_clip(xsb_header *xsb, off_t offset, off_t name_offset, STRE switch (flags & 0x1F) { /* event ID */ case 0x01: /* playwave event */ - /* 00(1): unknown */ - /* 01(1): flags */ - stream_index = read_s16(offset + 0x02, sf); - wavebank_index = read_s8 (offset + 0x04, sf); - /* 05(1): loop count */ - /* 06(2): pan angle */ - /* 08(2): pan arc */ + if (xsb->version <= XSB_XACT2_0_MAX) { /* v34 (Table Tennis) */ + /* 00(1): unknown */ + stream_index = read_s16(offset + 0x01, sf); + wavebank_index = read_s8 (offset + 0x03, sf); + /* 04(1): loop count? */ + size = 0x05; + } + else { /* v40 (Blue Dragon) */ + /* 00(1): unknown */ + /* 01(1): flags */ + stream_index = read_s16(offset + 0x02, sf); + wavebank_index = read_s8 (offset + 0x04, sf); + /* 05(1): loop count */ + /* 06(2): pan angle */ + /* 08(2): pan arc */ + size = 0x0a; + } //;VGM_LOG("XSB clip event 1 at %lx: stream=%i, wavebank=%i\n", offset, stream_index, wavebank_index); - offset += 0x0a; + offset += size; xsb_check_stream(xsb, stream_index, wavebank_index, name_offset, sf); if (xsb->parse_done) return 1; @@ -354,11 +366,11 @@ static int parse_xsb_clip(xsb_header *xsb, off_t offset, off_t name_offset, STRE /* 02(1): loop count */ /* 03(2): pan angle */ /* 05(2): pan arc */ - track_count = read_s16(offset + 0x07, sf); - /* 09(1): flags? */ - /* 0a(5): unknown */ + /* 07(2): flags? */ + track_count = read_s16(offset + 0x09, sf); /* MonoGame reads at 0x07, but this looks correct [LocoCycle (X360)-v46] */ + /* 0b(4): unknown */ - //;VGM_LOG("XSB clip event 3 at %lx\n", offset); + //;VGM_LOG("XSB clip event 3 at %lx, tracks=%i\n", offset, track_count); offset += 0x0F; for (t = 0; t < track_count; t++) { @@ -389,13 +401,13 @@ static int parse_xsb_clip(xsb_header *xsb, off_t offset, off_t name_offset, STRE /* 0f(1): max volume */ /* 10(4): min frequency */ /* 14(4): max frequency */ - /* 18(1): min Q */ - /* 19(1): max Q */ - /* 1a(1): unknown */ - /* 1b(1): variation flags */ + /* 18(4): min Q */ + /* 1c(4): max Q */ + /* 20(1): unknown */ + /* 21(1): variation flags */ //;VGM_LOG("XSB clip event 4 at %lx: stream=%i, wavebank=%i\n", offset, stream_index, wavebank_index); - offset += 0x1c; + offset += 0x22; xsb_check_stream(xsb, stream_index, wavebank_index, name_offset, sf); if (xsb->parse_done) return 1; @@ -413,16 +425,17 @@ static int parse_xsb_clip(xsb_header *xsb, off_t offset, off_t name_offset, STRE /* 0c(1): max volume */ /* 0d(4): min frequency */ /* 11(4): max frequency */ - /* 15(1): min Q */ - /* 16(1): max Q */ - /* 17(1): unknown */ - /* 18(1): variation flags */ - track_count = read_s16(offset + 0x19, sf); - /* 1a(1): flags 2 */ - /* 1b(5): unknown 2 */ + /* 15(4): min Q */ + /* 19(4): max Q */ + /* 1d(1): unknown */ + /* 1e(1): variation flags? */ + /* 1f(1): unknown 2 */ + /* 20(1): variation flags? */ + track_count = read_s16(offset + 0x21, sf); /* MonoGame reads at 0x1f, but this looks correct [LocoCycle (X360)-v46] */ + /* 23(4): unknown 3 (-1?) */ - //;VGM_LOG("XSB clip event 6 at %lx\n", offset); - offset += 0x20; + //;VGM_LOG("XSB clip event 6 at %lx, tracks=%i\n", offset, track_count); + offset += 0x27; for (t = 0; t < track_count; t++) { stream_index = read_s16(offset + 0x00, sf); @@ -529,6 +542,7 @@ static int parse_xsb_sound(xsb_header *xsb, off_t offset, off_t name_offset, STR } } + //;VGM_LOG("XSB sound end\n"); return 0; } @@ -542,11 +556,15 @@ static int parse_xsb_variation(xsb_header *xsb, off_t offset, off_t name_offset, int i, variation_count; - variation_count = read_s16(offset + 0x00, sf); - flags = read_u16(offset + 0x02, sf); + /* MonoGame reads count first, but this looks correct [LocoCycle (X360)-v46] */ + flags = read_u16(offset + 0x00, sf); + variation_count = read_s16(offset + 0x02, sf); + /* 0x04(1): unknown */ + /* 0x05(2): unknown */ + /* 0x07(1): unknown */ - //;VGM_LOG("XSB variation at %lx\n", offset); - offset += 0x04; + //;VGM_LOG("XSB variation at %lx, count=%i\n", offset, variation_count); + offset += 0x08; for (i = 0; i < variation_count; i++) { off_t sound_offset; @@ -558,7 +576,7 @@ static int parse_xsb_variation(xsb_header *xsb, off_t offset, off_t name_offset, /* 03(1): weight min */ /* 04(1): weight max */ - //;VGM_LOG("XSB variation: type 0 with stream=%i, wavebank=%i\n", stream_index, wavebank_index); + //;VGM_LOG("XSB variation: type 0 at %lx with stream=%i, wavebank=%i\n", offset, stream_index, wavebank_index); offset += 0x05; xsb_check_stream(xsb, stream_index, wavebank_index, name_offset, sf); @@ -570,7 +588,7 @@ static int parse_xsb_variation(xsb_header *xsb, off_t offset, off_t name_offset, /* 04(1): weight min */ /* 05(1): weight max */ - //;VGM_LOG("XSB variation: type 1\n"); + //;VGM_LOG("XSB variation: type 1 at %lx\n", offset); offset += 0x06; parse_xsb_sound(xsb, sound_offset, name_offset, sf); @@ -583,7 +601,7 @@ static int parse_xsb_variation(xsb_header *xsb, off_t offset, off_t name_offset, /* 08(4): weight max */ /* 0c(4): flags */ - //;VGM_LOG("XSB variation: type 3\n"); + //;VGM_LOG("XSB variation: type 3 at %lx\n", offset); offset += 0x10; parse_xsb_sound(xsb, sound_offset, name_offset, sf); @@ -594,7 +612,7 @@ static int parse_xsb_variation(xsb_header *xsb, off_t offset, off_t name_offset, stream_index = read_s16(offset + 0x00, sf); wavebank_index = read_s8(offset + 0x02, sf); - //;VGM_LOG("XSB variation: type 4 with stream=%i, wavebank=%i\n", stream_index, wavebank_index); + //;VGM_LOG("XSB variation: type 4 at %lx with stream=%i, wavebank=%i\n", offset, stream_index, wavebank_index); offset += 0x03; xsb_check_stream(xsb, stream_index, wavebank_index, name_offset, sf); @@ -612,7 +630,7 @@ static int parse_xsb_variation(xsb_header *xsb, off_t offset, off_t name_offset, /* 03(1): unknown */ offset += 0x04; - + //;VGM_LOG("XSB variation end\n"); return 1; fail: return 0; @@ -620,7 +638,7 @@ fail: static int parse_xsb_cues(xsb_header *xsb, STREAMFILE *sf) { - int32_t (*read_s32)(off_t,STREAMFILE*) = xsb->big_endian ? read_s32be : read_s32le; + int32_t (*read_s32)(off_t,STREAMFILE*) = xsb->big_endian ? read_s32be : read_s32le; uint8_t flags; off_t offset, name_offset, sound_offset; @@ -767,7 +785,7 @@ static int parse_xsb(xsb_header *xsb, STREAMFILE *sf, char *xwb_wavebank_name) { xsb->index_size = 0x14; xsb->entry_size = 0x14; } - else if (xsb->version <= XSB_XACT2_MAX) { + else if (xsb->version <= XSB_XACT2_2_MAX) { /* 06(2): crc */ /* 08(1): platform? (3=X360) */ xsb->simple_cues_count = read_s16(0x09, sf); @@ -821,12 +839,9 @@ static int parse_xsb(xsb_header *xsb, STREAMFILE *sf, char *xwb_wavebank_name) { } //;VGM_LOG("XSB header: version=%i\n", xsb->version); - //;VGM_LOG("XSB header: count: simple=%i, complex=%i, wavebanks=%i, sounds=%i\n", - // xsb->simple_cues_count, xsb->complex_cues_count, xsb->wavebanks_count, xsb->sounds_count); - //;VGM_LOG("XSB header: offset: simple=%lx, complex=%lx, wavebanks=%lx, sounds=%lx\n", - // xsb->simple_cues_offset, xsb->complex_cues_offset, xsb->wavebanks_offset, xsb->sounds_offset); - //;VGM_LOG("XSB header: names: cues=%lx, size=%x, hash=%lx\n", - // xsb->cue_names_offset, xsb->cue_names_size, xsb->nameoffsets_offset); + //;VGM_LOG("XSB header: count: simple=%i, complex=%i, wavebanks=%i, sounds=%i\n", xsb->simple_cues_count, xsb->complex_cues_count, xsb->wavebanks_count, xsb->sounds_count); + //;VGM_LOG("XSB header: offset: simple=%lx, complex=%lx, wavebanks=%lx, sounds=%lx\n", xsb->simple_cues_offset, xsb->complex_cues_offset, xsb->wavebanks_offset, xsb->sounds_offset); + //;VGM_LOG("XSB header: names: cues=%lx, size=%x, hash=%lx\n", xsb->cue_names_offset, xsb->cue_names_size, xsb->nameoffsets_offset); if (xsb->version > XSB_XACT1_2_MAX && xsb->cue_names_size <= 0) { VGM_LOG("XSB: no names found\n"); @@ -845,7 +860,7 @@ static int parse_xsb(xsb_header *xsb, STREAMFILE *sf, char *xwb_wavebank_name) { offset = xsb->wavebanks_offset; for (i = 0; i < xsb->wavebanks_count; i++) { read_string(xsb_wavebank_name,xsb->wavebanks_name_size, offset, sf); - //;VGM_LOG("XSB wavebanks: bank %i=%s\n", i, wavebank_name); + //;VGM_LOG("XSB wavebanks: bank %i\n", i); //, wavebank_name if (strcasecmp(xsb_wavebank_name, xwb_wavebank_name)==0) { //;VGM_LOG("XSB banks: current xwb is wavebank %i=%s\n", i, xsb_wavebank_name); xsb->selected_wavebank = i; From 30a495335cdceefeda789b71181577a03dc122b4 Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 21 Nov 2020 15:55:06 +0100 Subject: [PATCH 03/12] Enable Wwise OPUS [Assassin's Creed Valhalla (PC)] --- src/meta/wwise.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/meta/wwise.c b/src/meta/wwise.c index 15a9307d..f893be23 100644 --- a/src/meta/wwise.c +++ b/src/meta/wwise.c @@ -484,12 +484,11 @@ VGMSTREAM* init_vgmstream_wwise(STREAMFILE* sf) { break; } -#if 0 // disabled until more files/tests case OPUSWW: { /* updated Opus [Assassin's Creed Valhalla (PC)] */ int skip, table_count; if (ww.block_align != 0 || ww.bits_per_sample != 0) goto fail; - if (!ww.seek_offset)) goto fail; + if (!ww.seek_offset) goto fail; /* extra: size 0x10 */ /* 0x12: samples per frame */ @@ -512,7 +511,6 @@ VGMSTREAM* init_vgmstream_wwise(STREAMFILE* sf) { vgmstream->layout_type = layout_none; break; } -#endif #endif case HEVAG: /* PSV */ @@ -781,13 +779,13 @@ static int parse_wwise(STREAMFILE* sf, wwise_header* ww) { case 0x0166: ww->codec = XMA2; break; /* fmt-chunk XMA */ case 0xAAC0: ww->codec = AAC; break; case 0xFFF0: ww->codec = DSP; break; - case 0xFFFB: ww->codec = HEVAG; break; + case 0xFFFB: ww->codec = HEVAG; break; /* "VAG" */ case 0xFFFC: ww->codec = ATRAC9; break; case 0xFFFE: ww->codec = PCM; break; /* "PCM for Wwise Authoring" */ case 0xFFFF: ww->codec = VORBIS; break; case 0x3039: ww->codec = OPUSNX; break; /* renamed from "OPUS" on Wwise 2018.1 */ case 0x3040: ww->codec = OPUS; break; - case 0x3041: ww->codec = OPUSWW; break; /* added on Wwise 2019.2.3, presumably replaces OPUS */ + case 0x3041: ww->codec = OPUSWW; break; /* "OPUS_WEM", added on Wwise 2019.2.3, replaces OPUS */ case 0x8311: ww->codec = PTADPCM; break; /* added on Wwise 2019.1, replaces IMA */ default: goto fail; From 388007c355f1e45d97cf00d916ed22cdc1c0298f Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 21 Nov 2020 15:55:46 +0100 Subject: [PATCH 04/12] Fix some .sad [Luminous Arc (DS)] --- src/meta/sadl.c | 69 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/src/meta/sadl.c b/src/meta/sadl.c index 98132b78..78cc0654 100644 --- a/src/meta/sadl.c +++ b/src/meta/sadl.c @@ -1,59 +1,92 @@ #include "meta.h" +#include "../coding/coding.h" + /* sadl - from DS games with Procyon Studio audio driver [Professor Layton (DS), Soma Bringer (DS)] */ VGMSTREAM* init_vgmstream_sadl(STREAMFILE* sf) { VGMSTREAM* vgmstream = NULL; - int channel_count, loop_flag; + int channels, loop_flag; off_t start_offset; + uint8_t flags; + uint32_t loop_start, data_size; /* checks */ if (!check_extensions(sf, "sad")) goto fail; - if (read_32bitBE(0x00,sf) != 0x7361646c) /* "sadl" */ - goto fail; - if (read_32bitLE(0x40,sf) != get_streamfile_size(sf)) + if (read_u32be(0x00,sf) != 0x7361646c) /* "sadl" */ goto fail; + /* 04: null */ + /* 08: data size, or null in later files */ + /* 0c: version? (x0410=Luminous Arc, 0x0411=Layton, 0x0415=rest) */ + /* 0e: file id (for .sad packed in .spd) */ + /* 14: name related? */ + /* 20: short filename (may be null or nor match full filename) */ + + /* 30: flags? (0/1/2) */ + loop_flag = read_u8(0x31,sf); + channels = read_u8(0x32,sf); + flags = read_u8(0x33,sf); + /* 34: flags? */ + /* 38: flags? */ + /* 3c: null? */ + data_size = read_u32le(0x40,sf); //? + start_offset = read_u32le(0x48,sf); /* usually 0x100, 0xc0 in LA */ + /* 4c: start offset again or 0x40 in LA */ + /* 50: size or samples? */ + loop_start = read_u32le(0x54,sf); //? + /* others: sizes/samples/flags? */ + + data_size -= start_offset; + loop_start -= start_offset; + - loop_flag = read_8bit(0x31,sf); - channel_count = read_8bit(0x32,sf); - start_offset = 0x100; - /* build the VGMSTREAM */ - vgmstream = allocate_vgmstream(channel_count,loop_flag); + vgmstream = allocate_vgmstream(channels, loop_flag); if (!vgmstream) goto fail; - switch (read_8bit(0x33,sf) & 6) { + vgmstream->meta_type = meta_SADL; + + switch (flags & 6) { /* possibly > 1? (0/1/2) */ case 4: vgmstream->sample_rate = 32728; break; - case 2: + case 2: /* Layton */ + case 0: /* Luminous Arc (DS) */ vgmstream->sample_rate = 16364; break; default: goto fail; } - vgmstream->meta_type = meta_SADL; - vgmstream->layout_type = layout_interleave; vgmstream->interleave_block_size = 0x10; - switch(read_8bit(0x33,sf) & 0xf0) { + switch(flags & 0xf0) { /* possibly >> 6? (0/1/2) */ + case 0x00: /* Luminous Arc (DS) (non-int IMA? all files are mono though) */ case 0x70: /* Ni no Kuni (DS), Professor Layton and the Curious Village (DS), Soma Bringer (DS) */ vgmstream->coding_type = coding_IMA_int; - vgmstream->num_samples = (read_32bitLE(0x40,sf)-start_offset)/channel_count*2; - vgmstream->loop_start_sample = (read_32bitLE(0x54,sf)-start_offset)/channel_count*2; + vgmstream->num_samples = ima_bytes_to_samples(data_size, channels); + vgmstream->loop_start_sample = ima_bytes_to_samples(loop_start, channels); vgmstream->loop_end_sample = vgmstream->num_samples; + + { + int i; + for (i = 0; i < channels; i++) { + vgmstream->ch[i].adpcm_history1_32 = read_s16le(0x80 + i*0x04 + 0x00, sf); + vgmstream->ch[i].adpcm_step_index = read_s16le(0x80 + i*0x04 + 0x02, sf); + } + } break; + //TODO: Luminous Arc 2 uses a variation of this, but value 0x70 case 0xb0: /* Soma Bringer (DS), Rekishi Taisen Gettenka (DS) */ vgmstream->coding_type = coding_NDS_PROCYON; - vgmstream->num_samples = (read_32bitLE(0x40,sf)-start_offset)/channel_count/16*30; - vgmstream->loop_start_sample = (read_32bitLE(0x54,sf)-start_offset)/channel_count/16*30; + vgmstream->num_samples = data_size / channels / 16 * 30; + vgmstream->loop_start_sample = loop_start / channels / 16 *30; vgmstream->loop_end_sample = vgmstream->num_samples; break; From 1c48be52d29a83ef000a6f1856d5a9a252cc7409 Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 21 Nov 2020 15:59:52 +0100 Subject: [PATCH 05/12] Tweak layer-v mixing in some cases and improve performance --- src/mixing.c | 238 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 213 insertions(+), 25 deletions(-) diff --git a/src/mixing.c b/src/mixing.c index 43957e8f..a88c36ef 100644 --- a/src/mixing.c +++ b/src/mixing.c @@ -56,6 +56,7 @@ typedef enum { MIX_SWAP, MIX_ADD, + MIX_ADD_COPY, MIX_VOLUME, MIX_LIMIT, MIX_UPMIX, @@ -89,28 +90,41 @@ typedef struct { size_t mixing_size; /* mixing max */ mix_command_data mixing_chain[VGMSTREAM_MAX_MIXING]; /* effects to apply (could be alloc'ed but to simplify...) */ float* mixbuf; /* internal mixing buffer */ + + /* fades only apply at some points, other mixes are active */ + int has_non_fade; + int has_fade; } mixing_data; /* ******************************************************************* */ -static int is_active(mixing_data *data, int32_t current_start, int32_t current_end) { +static int is_fade_active(mixing_data *data, int32_t current_start, int32_t current_end) { int i; - int32_t fade_start, fade_end; for (i = 0; i < data->mixing_count; i++) { mix_command_data *mix = &data->mixing_chain[i]; + int32_t fade_start, fade_end; + float vol_start = mix->vol_start; if (mix->command != MIX_FADE) - return 1; /* has non-fades = active */ + continue; /* check is current range falls within a fade * (assuming fades were already optimized on add) */ - fade_start = mix->time_pre < 0 ? 0 : mix->time_pre; + if (mix->time_pre < 0 && vol_start == 1.0) { + fade_start = mix->time_start; /* ignore unused */ + } + else { + fade_start = mix->time_pre < 0 ? 0 : mix->time_pre; + } fade_end = mix->time_post < 0 ? INT_MAX : mix->time_post; - - if (current_start < fade_end && current_end > fade_start) + + //;VGM_LOG("MIX: fade test, tp=%i, te=%i, cs=%i, ce=%i\n", mix->time_pre, mix->time_post, current_start, current_end); + if (current_start < fade_end && current_end > fade_start) { + //;VGM_LOG("MIX: fade active, cs=%i < fe=%i and ce=%i > fs=%i\n", current_start, fade_end, current_end, fade_start); return 1; + } } return 0; @@ -249,7 +263,7 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream) mixing_data *data = vgmstream->mixing_data; int ch, s, m, ok; - int32_t current_pos, current_subpos; + int32_t current_subpos = 0; float temp_f, temp_min, temp_max, cur_vol = 0.0f; float *temp_mixbuf; sample_t *temp_outbuf; @@ -261,18 +275,21 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream) if (!data || !data->mixing_on || data->mixing_count == 0) return; - /* try to skip if no ops apply (for example if fade set but does nothing yet) */ - current_pos = get_current_pos(vgmstream, sample_count); - if (!is_active(data, current_pos, current_pos + sample_count)) - return; + /* try to skip if no fades apply (set but does nothing yet) + only has fades */ + if (data->has_fade) { + int32_t current_pos = get_current_pos(vgmstream, sample_count); + //;VGM_LOG("MIX: fade test %i, %i\n", data->has_non_fade, is_fade_active(data, current_pos, current_pos + sample_count)); + if (!data->has_non_fade && !is_fade_active(data, current_pos, current_pos + sample_count)) + return; + //;VGM_LOG("MIX: fade pos=%i\n", current_pos); + current_subpos = current_pos; + } /* use advancing buffer pointers to simplify logic */ temp_mixbuf = data->mixbuf; temp_outbuf = outbuf; - current_subpos = current_pos; - /* apply mixes in order per channel */ for (s = 0; s < sample_count; s++) { /* reset after new sample 'step'*/ @@ -307,6 +324,10 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream) stpbuf[mix->ch_dst] = stpbuf[mix->ch_dst] + stpbuf[mix->ch_src] * mix->vol; break; + case MIX_ADD_COPY: + stpbuf[mix->ch_dst] = stpbuf[mix->ch_dst] + stpbuf[mix->ch_src]; + break; + case MIX_VOLUME: if (mix->ch_dst < 0) { for (ch = 0; ch < step_channels; ch++) { @@ -384,7 +405,8 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream) temp_outbuf += vgmstream->channels; } - /* copy resulting mix to output */ + /* copy resulting mix to output + * (you'd think using a int32 temp buf would be faster but somehow it's slower?) */ for (s = 0; s < sample_count * data->output_channels; s++) { /* when casting float to int, value is simply truncated: * - (int)1.7 = 1, (int)-1.7 = -1 @@ -457,6 +479,14 @@ static int add_mixing(VGMSTREAM* vgmstream, mix_command_data *mix) { data->mixing_chain[data->mixing_count] = *mix; /* memcpy */ data->mixing_count++; + + if (mix->command == MIX_FADE) { + data->has_fade = 1; + } + else { + data->has_non_fade = 1; + } + //;VGM_LOG("MIX: total %i\n", data->mixing_count); return 1; } @@ -485,7 +515,7 @@ void mixing_push_add(VGMSTREAM* vgmstream, int ch_dst, int ch_src, double volume if (ch_dst < 0 || ch_src < 0) return; if (!data || ch_dst >= data->output_channels || ch_src >= data->output_channels) return; - mix.command = MIX_ADD; //if (volume == 1.0) MIX_ADD_COPY /* could simplify */ + mix.command = (volume == 1.0) ? MIX_ADD_COPY : MIX_ADD; mix.ch_dst = ch_dst; mix.ch_src = ch_src; mix.vol = volume; @@ -679,6 +709,10 @@ void mixing_push_fade(VGMSTREAM* vgmstream, int ch_dst, double vol_start, double /* ******************************************************************* */ +#define MIX_MACRO_VOCALS 'v' +#define MIX_MACRO_EQUAL 'e' +#define MIX_MACRO_BGM 'b' + void mixing_macro_volume(VGMSTREAM* vgmstream, double volume, uint32_t mask) { mixing_data *data = vgmstream->mixing_data; int ch; @@ -741,6 +775,153 @@ static int get_layered_max_channels(VGMSTREAM* vgmstream) { return max; } +static int is_layered_auto(VGMSTREAM* vgmstream, int max, char mode) { + int i; + mixing_data *data = vgmstream->mixing_data; + layered_layout_data* l_data; + + + if (vgmstream->layout_type != layout_layered) + return 0; + + /* no channels set and only vocals for now */ + if (max > 0 || mode != MIX_MACRO_VOCALS) + return 0; + + /* no channel down/upmixing (cannot guess output) */ + for (i = 0; i < data->mixing_count; i++) { + mix_command_t mix = data->mixing_chain[i].command; + if (mix == MIX_UPMIX || mix == MIX_DOWNMIX || mix == MIX_KILLMIX) /*mix == MIX_SWAP || ??? */ + return 0; + } + + /* only previsible cases */ + l_data = vgmstream->layout_data; + for (i = 0; i < l_data->layer_count; i++) { + int output_channels = 0; + + mixing_info(l_data->layers[i], NULL, &output_channels); + + if (output_channels > 8) + return 0; + } + + return 1; +} + + +/* special layering, where channels are respected (so Ls only go to Ls), also more optimized */ +static void mixing_macro_layer_auto(VGMSTREAM* vgmstream, int max, char mode) { + layered_layout_data* ldata = vgmstream->layout_data; + int i, ch; + int target_layer = 0, target_chs = 0, ch_max, target_ch = 0, target_silence = 0; + int ch_num; + + /* With N layers like: (ch1 ch2) (ch1 ch2 ch3 ch4) (ch1 ch2), output is normally 2+4+2=8ch. + * We want to find highest layer (ch1..4) = 4ch, add other channels to it and drop them */ + + /* find target "main" channels (will be first most of the time) */ + ch_num = 0; + ch_max = 0; + for (i = 0; i < ldata->layer_count; i++) { + int layer_chs = 0; + + mixing_info(ldata->layers[i], NULL, &layer_chs); + + if (ch_max < layer_chs || (ch_max == layer_chs && target_silence)) { + target_ch = ch_num; + target_chs = layer_chs; + target_layer = i; + ch_max = layer_chs; + /* avoid using silence as main if possible for minor optimization */ + target_silence = (ldata->layers[i]->coding_type == coding_SILENCE); + } + + ch_num += layer_chs; + } + + /* all silences? */ + if (!target_chs) { + target_ch = 0; + target_chs = 0; + target_layer = 0; + mixing_info(ldata->layers[0], NULL, &target_chs); + } + + /* add other channels to target (assumes standard channel mapping to simplify) + * most of the time all layers will have same number of channels though */ + ch_num = 0; + for (i = 0; i < ldata->layer_count; i++) { + int layer_chs = 0; + + if (target_layer == i) { + ch_num += target_chs; + continue; + } + + mixing_info(ldata->layers[i], NULL, &layer_chs); + + if (ldata->layers[i]->coding_type == coding_SILENCE) { + ch_num += layer_chs; + continue; /* unlikely but sometimes in Wwise */ + } + + if (layer_chs == target_chs) { + /* 1:1 mapping */ + for (ch = 0; ch < layer_chs; ch++) { + mixing_push_add(vgmstream, target_ch + ch, ch_num + ch, 1.0); + } + } + else { + const double vol_sqrt = 1 / sqrt(2); + + /* extra mixing for better sound in some cases (assumes layer_chs is lower than target_chs) */ + switch(layer_chs) { + case 1: + mixing_push_add(vgmstream, target_ch + 0, ch_num + 0, vol_sqrt); + mixing_push_add(vgmstream, target_ch + 1, ch_num + 0, vol_sqrt); + break; + case 2: + mixing_push_add(vgmstream, target_ch + 0, ch_num + 0, 1.0); + mixing_push_add(vgmstream, target_ch + 1, ch_num + 1, 1.0); + break; + default: /* less common */ + //TODO add other mixes, depends on target_chs + mapping (ex. 4.0 to 5.0 != 5.1, 2.1 xiph to 5.1 != 5.1 xiph) + for (ch = 0; ch < layer_chs; ch++) { + mixing_push_add(vgmstream, target_ch + ch, ch_num + ch, 1.0); + } + break; + } + } + + ch_num += layer_chs; + } + + /* drop non-target channels */ + ch_num = 0; + for (i = 0; i < ldata->layer_count; i++) { + + if (i < target_layer) { /* least common, hopefully (slower to drop chs 1 by 1) */ + int layer_chs = 0; + mixing_info(ldata->layers[i], NULL, &layer_chs); + + for (ch = 0; ch < layer_chs; ch++) { + mixing_push_downmix(vgmstream, ch_num); //+ ch + } + + //ch_num += layer_chs; /* dropped channels change this */ + } + else if (i == target_layer) { + ch_num += target_chs; + } + else { /* most common, hopefully (faster) */ + mixing_push_killmix(vgmstream, ch_num); + break; + } + } +} + + void mixing_macro_layer(VGMSTREAM* vgmstream, int max, uint32_t mask, char mode) { mixing_data *data = vgmstream->mixing_data; int current, ch, output_channels, selected_channels; @@ -748,6 +929,13 @@ void mixing_macro_layer(VGMSTREAM* vgmstream, int max, uint32_t mask, char mode) if (!data) return; + if (is_layered_auto(vgmstream, max, mode)) { + //;VGM_LOG("MIX: auto layer mode\n"); + mixing_macro_layer_auto(vgmstream, max, mode); + return; + } + //;VGM_LOG("MIX: regular layer mode\n"); + if (max == 0) /* auto calculate */ max = get_layered_max_channels(vgmstream); @@ -781,10 +969,10 @@ void mixing_macro_layer(VGMSTREAM* vgmstream, int max, uint32_t mask, char mode) if (!((mask >> ch) & 1)) continue; - /* mode 'v': same volume for all layers (for layered vocals) */ - /* mode 'b': volume adjusted depending on layers (for layered bgm) */ - /* mode 'e': volume adjusted equally for all layers (for generic downmixing) */ - if (mode == 'b' && ch < max) { + /* MIX_MACRO_VOCALS: same volume for all layers (for layered vocals) */ + /* MIX_MACRO_EQUAL: volume adjusted equally for all layers (for generic downmixing) */ + /* MIX_MACRO_BGM: volume adjusted depending on layers (for layered bgm) */ + if (mode == MIX_MACRO_BGM && ch < max) { /* reduce a bit main channels (see below) */ int channel_mixes = selected_channels / max; if (current < selected_channels % (channel_mixes * max)) /* may be simplified? */ @@ -795,7 +983,7 @@ void mixing_macro_layer(VGMSTREAM* vgmstream, int max, uint32_t mask, char mode) volume = 1 / sqrt(channel_mixes); } - if ((mode == 'b' && ch >= max) || (mode == 'e')) { + if ((mode == MIX_MACRO_BGM && ch >= max) || (mode == MIX_MACRO_EQUAL)) { /* find how many will be mixed in current channel (earlier channels receive more * mixes than later ones, ex: selected 8ch + max 3ch: ch0=0+3+6, ch1=1+4+7, ch2=2+5) */ int channel_mixes = selected_channels / max; @@ -909,13 +1097,13 @@ void mixing_macro_crosslayer(VGMSTREAM* vgmstream, int max, char mode) { vgmstream->config.config_set = 1; } - /* mode 'v': constant volume - * mode 'e': sets fades to successively lower/equalize volume per loop for each layer + /* MIX_MACRO_VOCALS: constant volume + * MIX_MACRO_EQUAL: sets fades to successively lower/equalize volume per loop for each layer * (to keep final volume constant-ish), ex. 3 layers/loops, 2 max: * - layer0 (ch0+1): loop0 --[1.0]--, loop1 )=1.0..0.7, loop2 )=0.7..0.5, loop3 --[0.5/end]-- * - layer1 (ch2+3): loop0 --[0.0]--, loop1 (=0.0..0.7, loop2 )=0.7..0.5, loop3 --[0.5/end]-- * - layer2 (ch4+5): loop0 --[0.0]--, loop1 ---[0.0]--, loop2 (=0.0..0.5, loop3 --[0.5/end]-- - * mode 'b': similar but 1st layer (main) has higher/delayed volume: + * MIX_MACRO_BGM: similar but 1st layer (main) has higher/delayed volume: * - layer0 (ch0+1): loop0 --[1.0]--, loop1 )=1.0..1.0, loop2 )=1.0..0.7, loop3 --[0.7/end]-- */ for (loop = 1; loop < layer_num; loop++) { @@ -927,7 +1115,7 @@ void mixing_macro_crosslayer(VGMSTREAM* vgmstream, int max, char mode) { change_pos = loop_pre + loop_samples * loop; change_time = 10.0 * vgmstream->sample_rate; /* in secs */ - if (mode == 'e') { + if (mode == MIX_MACRO_EQUAL) { volume1 = 1 / sqrt(loop + 0); volume2 = 1 / sqrt(loop + 1); } @@ -936,7 +1124,7 @@ void mixing_macro_crosslayer(VGMSTREAM* vgmstream, int max, char mode) { for (layer = 0; layer < layer_num; layer++) { char type; - if (mode == 'b') { + if (mode == MIX_MACRO_BGM) { if (layer == 0) { volume1 = 1 / sqrt(loop - 1 <= 0 ? 1 : loop - 1); volume2 = 1 / sqrt(loop + 0); From f6a87861fcd29e048781fce2dc35a7f712d534cc Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 21 Nov 2020 15:59:58 +0100 Subject: [PATCH 06/12] Doc --- README.md | 18 ++++++++++++++++++ doc/TXTH.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3652b241..18772586 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,24 @@ simpler to make and cleaner: for example create a text file named `bgm01-loop.tx and inside write `bgm01.mp3 #I 10.0 90.0`. Open the `.txtp` to play the `.mp3` looping from 10 to 90 seconds. +#### OS case sensitiveness +When using OS with case sensitive filesystem (mainly Linux), a known issue with +companion files is that vgmstream generally tries to find them using lowercase +extension. + +This means that if the developer used uppercase instead (e.g. `bgm.ABK`+`bgm.AST`) +loading will fail. It's technically complex to fix this, so for the time being +the only option is renaming the companion extension to lowercase. + +A particularly nasty variation of that is that some formats load files by full +name (e.g. `STREAM.SS0`), but sometimes the actual filename is in other case +(`Stream.ss0`), and some files could even point to that with another case. You +could try adding *symlinks* in various upper/lower/mixed cases to handle this. +Currently there isn't any way to know what exact name is needed (other than +hex-editting), though only a few formats do this, mainly *Ubisoft* banks. + +Regular formats without companion files should work fine in upper/lowercase. + ### Decryption keys Certain formats have encrypted data, and need a key to decrypt. vgmstream will try to find the correct key from a list, but it can be provided by diff --git a/doc/TXTH.md b/doc/TXTH.md index b3e28d32..77e83d78 100644 --- a/doc/TXTH.md +++ b/doc/TXTH.md @@ -440,9 +440,13 @@ While you can put anything in the values, this feature is meant to be used to st #### BASE OFFSET MODIFIER -You can set a default offset that affects next `@(offset)` reads making them `@(offset + base_offset)`, for cleaner parsing (particularly interesting when combined with the `name_table`). +You can set a default offset that affects next `@(offset)` reads making them `@(offset + base_offset)`, for cleaner parsing. + +This is particularly interesting when combined with offsets to some long value. For example instead of `channels = @0x714` you could set `base_offset = 0x710, channels = @0x04`. Or values from the `name_table`, like `base_offset = name_value, channels = @0x04`. + +It also allows parsing formats that set offsets to another offset, by "chaining" `base_offset`. With `base_offset = @0x10` (pointing to `0x40`) then `base_offset = @0x20`, it reads value at `0x60`. Set to 0 when you want to disable/reset the chain: `base_offset = @0x10` then `base_offset = 0` then `base_offset = @0x20` reads value at `0x20` + -For example instead of `channels = @0x714` you could set `base_offset = 0x710, channels = @0x04`. Set to 0 when you want to disable it. ``` base_offset = (value) ``` @@ -1052,3 +1056,43 @@ loop_flag = auto #@0x10 is an absolute offset to another table, that shouldn't be affected by subsong_spacing name_offset_absolute = @0x10 + 0x270 ``` + +#### Fatal Frame (Xbox) .mwa.txth +``` +#00: MWAV +#04: flags? +#08: subsongs +#0c: data size +#10: null +#14: sizes offset +#18: offsets table +#1c: offset to tables? +#20: header offset + +subsong_count = @0x08 + +# size table +subsong_spacing = 0 +base_offset = 0 +base_offset = @0x14 +subsong_spacing = 0x04 +data_size = @0x00 + +# offset table +subsong_spacing = 0 +base_offset = 0 +base_offset = @0x18 +subsong_spacing = 0x04 +start_offset = @0x00 + +# header (standard "fmt") +subsong_spacing = 0 +base_offset = 0 +base_offset = @0x20 +channels = @0x02$2 +sample_rate = @0x04 + +codec = XBOX +num_samples = data_size +#todo: there are dummy entries +``` From 4a9c56c976ff0953021d1b7aba2731d6d1f59cf8 Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 21 Nov 2020 17:53:48 +0100 Subject: [PATCH 07/12] Fix segfault when using pad end in some cases --- src/render.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/render.c b/src/render.c index 308f32f4..04908ebf 100644 --- a/src/render.c +++ b/src/render.c @@ -361,7 +361,7 @@ static int render_pad_begin(VGMSTREAM* vgmstream, sample_t* buf, int samples_to_ return to_do; } -static int render_fade(VGMSTREAM* vgmstream, sample_t* buf, int samples_done) { +static int render_fade(VGMSTREAM* vgmstream, sample_t* buf, int samples_left) { play_state_t* ps = &vgmstream->pstate; //play_config_t* pc = &vgmstream->config; @@ -376,7 +376,7 @@ static int render_fade(VGMSTREAM* vgmstream, sample_t* buf, int samples_done) { int32_t to_do = ps->fade_left; if (ps->play_position < ps->fade_start) { - start = samples_done - (ps->play_position + samples_done - ps->fade_start); + start = samples_left - (ps->play_position + samples_left - ps->fade_start); fade_pos = 0; } else { @@ -384,8 +384,8 @@ static int render_fade(VGMSTREAM* vgmstream, sample_t* buf, int samples_done) { fade_pos = ps->play_position - ps->fade_start; } - if (to_do > samples_done - start) - to_do = samples_done - start; + if (to_do > samples_left - start) + to_do = samples_left - start; //TODO: use delta fadedness to improve performance? for (s = start; s < start + to_do; s++, fade_pos++) { @@ -398,27 +398,31 @@ static int render_fade(VGMSTREAM* vgmstream, sample_t* buf, int samples_done) { ps->fade_left -= to_do; /* next samples after fade end would be pad end/silence, so we can just memset */ - memset(buf + (start + to_do) * channels, 0, (samples_done - to_do - start) * sizeof(sample_t) * channels); - - return samples_done; + memset(buf + (start + to_do) * channels, 0, (samples_left - to_do - start) * sizeof(sample_t) * channels); + return samples_left; //start + to_do; } } -static int render_pad_end(VGMSTREAM* vgmstream, sample_t* buf, int samples_done) { +static int render_pad_end(VGMSTREAM* vgmstream, sample_t* buf, int samples_left) { play_state_t* ps = &vgmstream->pstate; int channels = vgmstream->pstate.output_channels; int start = 0; + int32_t to_do = samples_left; - /* since anything beyond pad end is silence no need to check end */ + /* pad end works like fades, where part of buf done it may be valid data and part padding (silent), + * so needs positions (since anything beyond pad end start is silence no need to check end) */ if (ps->play_position < ps->pad_end_start) { - start = samples_done - (ps->play_position + samples_done - ps->pad_end_start); + start = samples_left - (ps->play_position + samples_left - ps->pad_end_start); } else { start = 0; } - memset(buf + (start * channels), 0, (samples_done - start) * channels * sizeof(sample_t)); - return samples_done; + if (to_do > samples_left - start) + to_do = samples_left - start; + + memset(buf + (start * channels), 0, to_do * sizeof(sample_t) * channels); + return samples_left; //start + to_do; } @@ -455,7 +459,7 @@ int render_vgmstream(sample_t* buf, int32_t sample_count, VGMSTREAM* vgmstream) /* end padding (done before to avoid decoding if possible, samples_to_do becomes 0) */ if (!vgmstream->config.play_forever /* && ps->pad_end_left */ - && ps->play_position + samples_done >= ps->pad_end_start + && ps->play_position + samples_to_do >= ps->pad_end_start && samples_to_do) { done = render_pad_end(vgmstream, tmpbuf, samples_to_do); samples_done += done; From 2e24208622fa4c2b9cc8b2f0bb36e761ad75fd38 Mon Sep 17 00:00:00 2001 From: bnnm Date: Sun, 22 Nov 2020 18:57:15 +0100 Subject: [PATCH 08/12] Fix some pad end issues with segmented layout --- src/render.c | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/render.c b/src/render.c index 04908ebf..8e3cec27 100644 --- a/src/render.c +++ b/src/render.c @@ -399,30 +399,32 @@ static int render_fade(VGMSTREAM* vgmstream, sample_t* buf, int samples_left) { /* next samples after fade end would be pad end/silence, so we can just memset */ memset(buf + (start + to_do) * channels, 0, (samples_left - to_do - start) * sizeof(sample_t) * channels); - return samples_left; //start + to_do; + return start + to_do; } } static int render_pad_end(VGMSTREAM* vgmstream, sample_t* buf, int samples_left) { play_state_t* ps = &vgmstream->pstate; int channels = vgmstream->pstate.output_channels; - int start = 0; - int32_t to_do = samples_left; + int skip = 0; + int32_t to_do; - /* pad end works like fades, where part of buf done it may be valid data and part padding (silent), - * so needs positions (since anything beyond pad end start is silence no need to check end) */ + /* pad end works like fades, where part of buf samples and part padding (silent), + * calc exact totals (beyond pad end normally is silence, except with segmented layout) */ if (ps->play_position < ps->pad_end_start) { - start = samples_left - (ps->play_position + samples_left - ps->pad_end_start); + skip = ps->pad_end_start - ps->play_position; + to_do = ps->pad_end_duration; } else { - start = 0; + skip = 0; + to_do = (ps->pad_end_start + ps->pad_end_duration) - ps->play_position; } - if (to_do > samples_left - start) - to_do = samples_left - start; + if (to_do > samples_left - skip) + to_do = samples_left - skip; - memset(buf + (start * channels), 0, to_do * sizeof(sample_t) * channels); - return samples_left; //start + to_do; + memset(buf + (skip * channels), 0, to_do * sizeof(sample_t) * channels); + return skip + to_do; } @@ -457,9 +459,9 @@ int render_vgmstream(sample_t* buf, int32_t sample_count, VGMSTREAM* vgmstream) tmpbuf += done * vgmstream->pstate.output_channels; /* as if mixed */ } - /* end padding (done before to avoid decoding if possible, samples_to_do becomes 0) */ - if (!vgmstream->config.play_forever /* && ps->pad_end_left */ - && ps->play_position + samples_to_do >= ps->pad_end_start + /* end padding (before to avoid decoding if possible, but must be inside pad region) */ + if (!vgmstream->config.play_forever + && ps->play_position /*+ samples_to_do*/ >= ps->pad_end_start && samples_to_do) { done = render_pad_end(vgmstream, tmpbuf, samples_to_do); samples_done += done; From 450281dafda922ce3a780f9f36165cc27a73b58b Mon Sep 17 00:00:00 2001 From: bnnm Date: Sun, 22 Nov 2020 19:00:01 +0100 Subject: [PATCH 09/12] Allow segments of different number of channels --- doc/TXTP.md | 116 +++++++++++++++++++++-------------------- src/layout/segmented.c | 50 ++++++++++++++---- src/meta/aax.c | 2 +- src/meta/mus_acm.c | 2 +- src/meta/ubi_bao.c | 2 +- src/meta/ubi_sb.c | 2 +- src/vgmstream.h | 8 ++- 7 files changed, 109 insertions(+), 73 deletions(-) diff --git a/doc/TXTP.md b/doc/TXTP.md index ce1cb228..e84fa1b5 100644 --- a/doc/TXTP.md +++ b/doc/TXTP.md @@ -72,7 +72,7 @@ loop_start_segment = 2 loop_end_segment = 3 loop_mode = keep # loops in 2nd file's loop_start to 3rd file's loop_end ``` -Mixing sample rates is ok (uses max) but channel number must be equal for all files. You can use mixing (explained later) to join segments of different channels though. +Mixing sample rates is ok (uses max). Different number of channels is allowed, but you may need to use mixing (explained later) to improve results. 4ch + 2ch will sound ok, but 1ch + 2ch would need some upmixing first. ### Layers mode @@ -242,15 +242,16 @@ group = L #h44100 commands = #h48000 #overwrites ``` -Segments and layer settings and rules still apply when making groups, so you can't group segments of files with different total channels. To do it you could use commands to "downmix" the group first: +Segments and layer settings and rules still apply when making groups, so you may need to adjust groups a bit with commands: ``` # this doesn't need to be grouped intro_2ch.at3 -# this is grouped into a single 4ch file, then downmixed to stereo +# this is grouped into a single 4ch file, then auto-downmixed to stereo +# (without downmixing may sound a bit strange since channels from mainB wouldn't mix with intro) mainA_2ch.at3 mainB_2ch.at3 -group = 2L2 #@layer-v 2 +group = -L2 #@layer-v # finally resulting layers are played as segments (2ch, 2ch) # (could set a group = S and ommit mode here, too) @@ -489,6 +490,39 @@ Use this feature responsibly, though. If you find a format that should loop usin Note that a few codecs may not work with arbitrary loop values since they weren't tested with loops. Misaligned loops will cause audible "clicks" at loop point too. +### Loop anchors +**`#a`** (loop start segment), **`#A`** (loop end segment): mark looping parts in segmented layout. + +For segmented layout normally you set loop points using `loop_start_segment` and `loop_end_segment`. It's clean in simpler cases but can be a hassle when lots of files exist. To simplify those cases you can set "loop anchors": +``` +bgm01.adx +bgm02.adx #a ##defines loop start +``` +``` +bgm01.adx +bgm02.adx #a ##defines loop start +bgm03.adx +bgm04.adx #A ##defines loop end +bgm05.adx +``` +You can also use `#@loop` to set loop start. + +This setting also works in groups, which allows loops when using multiple segmented groups (not possible with `loop_start/end_segment`). +``` + bgm01.adx + bgm02.adx #a + group -S2 #l 2.0 + bgm01.adx + bgm02.adx #a + bgm03.adx + group -S2 #l 3.0 +group -S2 +#could use R groups to select one sub-groups that loops +# (loop_start_segment doesn't make sense for both segments) +``` +Loop anchors have priority over `loop_start_segment`, and are ignored in layered layouts. + + ### Force sample rate **`#h(sample rate)`**: changes sample rate to selected value, changing play speed. @@ -566,44 +600,11 @@ song#m1-3,2-4,3D # - drop channel 1 then 2 (now 1) song#m1d,1d ``` -Proper mixing requires some basic knowledge though, it's further explained later. Order matters and operations are applied sequentially, for extra flexibility at the cost of complexity and user-friendliness, and may result in surprising mixes. Try to stick to macros and simple combos, using later examples as a base. +Proper mixing requires some basic knowledge though, it's further explained later. Order matters and operations are applied sequentially, for extra flexibility at the cost of complexity and user-friendliness, and may result in surprising mixes. Typical mixing operations are provided as *macros* (see below), so try to stick to macros and simple combos, using later examples as a base. This can be applied to individual layers and segments, but normally you want to use `commands` to apply mixing to the resulting file (see examples). Per-segment mixing should be reserved to specific up/downmixings. -Mixing must be supported by the plugin, otherwise it's ignored (there is a negligible performance penalty per mix operation though). - - -### Loop anchors -**`#a`** (loop start segment), **`#A`** (loop end segment): mark looping parts in segmented layout. - -For segmented layout normally you set loop points using `loop_start_segment` and `loop_end_segment`. It's clean in simpler cases but can be a hassle when lots of files exist. To simplify those cases you can set "loop anchors": -``` -bgm01.adx -bgm02.adx #a ##defines loop start -``` -``` -bgm01.adx -bgm02.adx #a ##defines loop start -bgm03.adx -bgm04.adx #A ##defines loop end -bgm05.adx -``` -You can also use `#@loop` to set loop start. - -This setting also works in groups, which allows loops when using multiple segmented groups (not possible with `loop_start/end_segment`). -``` - bgm01.adx - bgm02.adx #a - group -S2 #l 2.0 - bgm01.adx - bgm02.adx #a - bgm03.adx - group -S2 #l 3.0 -group -S2 -#could use R groups to select one sub-groups that loops -# (loop_start_segment doesn't make sense for both segments) -``` -Loop anchors have priority over `loop_start_segment`, and are ignored in layered layouts. +Mixing must be supported by the plugin, otherwise it's ignored (there is a negligible performance penalty per mix operation though, though having *a lot* will add up). ### Macros @@ -613,10 +614,10 @@ Manually setting values gets old, so TXTP supports a bunch of simple macros. The - `volume N (channels)`: sets volume V to selected channels. N.N = percent or NdB = decibels. - `1.0` or `0dB` = base volume, `2.0` or `6dB` = double volume, `0.5` or `-6dB` = half volume - `#v N` also works -- `track (channels)`: makes a file of selected channels -- `layer-v N (channels)`: mixes selected channels to N channels with default volume (for layered vocals). If N is 0 (or ommited), automatically sets highest channel count among all layers. -- `layer-b N (channels)`: same, but adjusts volume depending on layers (for layered bgm) -- `layer-e N (channels)`: same, but adjusts volume equally for all layers (for generic downmixing) +- `track (channels)`: makes a file of selected channels (drops others) +- `layer-v (N) (channels)`: for layered files, mixes selected channels to N channels with default volume (for layered vocals). If N is ommited (or 0), automatically sets highest channel count among all layers plus does some extra optimizations for (hopefully) better sounding results. May be applied to global commands or group config. +- `layer-e (N) (channels)`: same, but adjusts volume equally for all layers (for generic downmixing) +- `layer-b (N) (channels)`: same, but adjusts volume focusing on "main" layer (for layered bgm) - `remix N (channels)`: same, but mixes selected channels to N channels properly adjusting volume (for layered bgm) - `crosstrack N`: crossfades between Nch tracks after every loop (loop count is adjusted as needed) - `crosslayer-v/b/e N`: crossfades Nch layers to the main track after every loop (loop count is adjusted as needed) @@ -731,7 +732,7 @@ Here is a rough look of how TXTP parses files, so you get a better idea of what' subdir name/bgm bank.fsb#s2#C1,2 subdir name/bgm bank.fsb #s2 #C1,2 #comment ``` -All defined files must exist and be parseable by vgmstream, and general config like `mode` must make sense (not `mde = layers` or `mode = laye`). +All defined files must exist and be parseable by vgmstream, and general config like `mode` must make sense (not `mde = layers` or `mode = laye`). *subdir* may even be relative paths like `../file.adx`, provided your OS supports that. Commands may add spaces as needed, but try to keep it simple. They *must* start with `#(command)`, as `#(space)(anything)` is a comment. Commands without corresponding file are ignored too (seen as comments), while incorrect commands are ignored and skip to next, though the parser may try to make something usable of them (this may be change anytime without warning): ``` @@ -796,7 +797,7 @@ You can also add spaces before files/commands, mainly to improve readability whe #segment x2 song1 song2 - group = 1S2 #E + group = -S2 #E ``` Repeated commands overwrite previous setting, except comma-separated commands that are additive: @@ -844,7 +845,7 @@ All this means there is no simple, standard way to mix, so you must experiment a ### Mixing examples -TXTP has a few macros that help you handle most simpler cases (like `#C 1 2`, `#@layer-v 2`), that you should use when possible, but below is a full explanation of manual mixing (macros just automate these options using some standard formulas). +TXTP has a few macros that help you handle most simpler cases (like `#C 1 2`, `#@layer-v`), that you should use when possible, but below is a full explanation of manual mixing (macros just automate these options using some standard formulas). ``` # boost volume of all channels by 30% song#m0*1.3 @@ -915,19 +916,22 @@ ffxiii2-eclipse.scd#m5u,6u,5+1,6+2#@crosstrack 2 Segment/layer downmixing is allowed but try to keep it simple, some mixes accomplish the same things but are a bit strange. ``` -# mix one stereo segment with a mono segment +# mix one stereo segment with a mono segment upmixed to stereo intro-stereo.hca -loop-mono.hca#m2u - -# this makes mono file -intro-stereo.hca#m2u -loop-stereo.hca#m2u - -# but you normally should do this instead as it's more natural +loop-mono.hca#m2u,2+1 +``` +``` +# this makes mono file from each stereo song +intro-stereo.hca#m2d +loop-stereo.hca#m2d +``` +``` +# but you normally should do it on the final result as it's more natural intro-stereo.hca loop-stereo.hca -commands = #m2u - +commands = #m2d +``` +``` # fading segments work rather unexpectedly # fades out 1 minute into the _segment_ (could be 2 minutes into the resulting file) segment1.hca#m0{0:10+10.0 diff --git a/src/layout/segmented.c b/src/layout/segmented.c index fe0422a8..a8baf530 100644 --- a/src/layout/segmented.c +++ b/src/layout/segmented.c @@ -7,6 +7,7 @@ #define VGMSTREAM_MAX_SEGMENTS 1024 #define VGMSTREAM_SEGMENT_SAMPLE_BUFFER 8192 +static inline void copy_samples(sample_t* outbuf, segmented_layout_data* data, int current_channels, int32_t samples_to_do, int32_t samples_written); /* Decodes samples for segmented streams. * Chains together sequential vgmstreams, for data divided into separate sections or files @@ -15,9 +16,10 @@ void render_vgmstream_segmented(sample_t* outbuf, int32_t sample_count, VGMSTREA int samples_written = 0, samples_this_block; segmented_layout_data* data = vgmstream->layout_data; int use_internal_buffer = 0; + int current_channels = 0; /* normally uses outbuf directly (faster?) but could need internal buffer if downmixing */ - if (vgmstream->channels != data->input_channels) { + if (vgmstream->channels != data->input_channels || data->mixed_channels) { use_internal_buffer = 1; } @@ -27,6 +29,7 @@ void render_vgmstream_segmented(sample_t* outbuf, int32_t sample_count, VGMSTREA } samples_this_block = vgmstream_get_samples(data->segments[data->current_segment]); + mixing_info(data->segments[data->current_segment], NULL, ¤t_channels); while (samples_written < sample_count) { int samples_to_do; @@ -34,6 +37,7 @@ void render_vgmstream_segmented(sample_t* outbuf, int32_t sample_count, VGMSTREA if (vgmstream->loop_flag && vgmstream_do_loop(vgmstream)) { /* handle looping (loop_layout has been called below, changes segments/state) */ samples_this_block = vgmstream_get_samples(data->segments[data->current_segment]); + mixing_info(data->segments[data->current_segment], NULL, ¤t_channels); continue; } @@ -50,6 +54,7 @@ void render_vgmstream_segmented(sample_t* outbuf, int32_t sample_count, VGMSTREA reset_vgmstream(data->segments[data->current_segment]); samples_this_block = vgmstream_get_samples(data->segments[data->current_segment]); + mixing_info(data->segments[data->current_segment], NULL, ¤t_channels); vgmstream->samples_into_block = 0; continue; } @@ -73,10 +78,7 @@ void render_vgmstream_segmented(sample_t* outbuf, int32_t sample_count, VGMSTREA data->segments[data->current_segment]); if (use_internal_buffer) { - int s; - for (s = 0; s < samples_to_do * data->output_channels; s++) { - outbuf[samples_written * data->output_channels + s] = data->buffer[s]; - } + copy_samples(outbuf, data, current_channels, samples_to_do, samples_written); } samples_written += samples_to_do; @@ -89,6 +91,30 @@ decode_fail: memset(outbuf + samples_written * data->output_channels, 0, (sample_count - samples_written) * data->output_channels * sizeof(sample_t)); } +static inline void copy_samples(sample_t* outbuf, segmented_layout_data* data, int current_channels, int32_t samples_to_do, int32_t samples_written) { + int ch_out = data->output_channels; + int ch_in = current_channels; + int pos = samples_written * ch_out; + int s; + if (ch_in == ch_out) { /* most common and probably faster */ + for (s = 0; s < samples_to_do * ch_out; s++) { + outbuf[pos + s] = data->buffer[s]; + } + } + else { + int ch; + for (s = 0; s < samples_to_do; s++) { + for (ch = 0; ch < ch_in; ch++) { + outbuf[pos + s*ch_out + ch] = data->buffer[s*ch_in + ch]; + } + for (ch = ch_in; ch < ch_out; ch++) { + outbuf[pos + s*ch_out + ch] = 0; + } + } + } +} + + void loop_layout_segmented(VGMSTREAM* vgmstream, int32_t loop_sample) { int segment, total_samples; segmented_layout_data* data = vgmstream->layout_data; @@ -139,7 +165,7 @@ fail: } int setup_layout_segmented(segmented_layout_data* data) { - int i, max_input_channels = 0, max_output_channels = 0; + int i, max_input_channels = 0, max_output_channels = 0, mixed_channels = 0; sample_t *outbuf_re = NULL; @@ -170,8 +196,8 @@ int setup_layout_segmented(segmented_layout_data* data) { } } - /* different segments may have different input channels, though output should be - * the same for all (ex. 2ch + 1ch segments, but 2ch segment is downmixed to 1ch) */ + /* different segments may have different input or output channels, we + * need to know maxs to properly handle */ mixing_info(data->segments[i], &segment_input_channels, &segment_output_channels); if (max_input_channels < segment_input_channels) max_input_channels = segment_input_channels; @@ -183,11 +209,12 @@ int setup_layout_segmented(segmented_layout_data* data) { mixing_info(data->segments[i-1], NULL, &prev_output_channels); if (segment_output_channels != prev_output_channels) { - VGM_LOG("SEGMENTED: segment %i has wrong channels %i vs prev channels %i\n", i, segment_output_channels, prev_output_channels); - goto fail; + mixed_channels = 1; + //VGM_LOG("SEGMENTED: segment %i has wrong channels %i vs prev channels %i\n", i, segment_output_channels, prev_output_channels); + //goto fail; } - /* a bit weird, but no matter */ + /* a bit weird, but no matter (should resample) */ if (data->segments[i]->sample_rate != data->segments[i-1]->sample_rate) { VGM_LOG("SEGMENTED: segment %i has different sample rate\n", i); } @@ -214,6 +241,7 @@ int setup_layout_segmented(segmented_layout_data* data) { data->input_channels = max_input_channels; data->output_channels = max_output_channels; + data->mixed_channels = mixed_channels; return 1; fail: diff --git a/src/meta/aax.c b/src/meta/aax.c index 32959889..b8041009 100644 --- a/src/meta/aax.c +++ b/src/meta/aax.c @@ -105,7 +105,7 @@ VGMSTREAM * init_vgmstream_aax(STREAMFILE *streamFile) { } } - channel_count = data->segments[0]->channels; + channel_count = data->output_channels; /* build the VGMSTREAM */ diff --git a/src/meta/mus_acm.c b/src/meta/mus_acm.c index 963c6723..03c50233 100644 --- a/src/meta/mus_acm.c +++ b/src/meta/mus_acm.c @@ -78,7 +78,7 @@ VGMSTREAM * init_vgmstream_mus_acm(STREAMFILE *streamFile) { goto fail; - channel_count = data->segments[0]->channels; + channel_count = data->output_channels; /* build the VGMSTREAM */ diff --git a/src/meta/ubi_bao.c b/src/meta/ubi_bao.c index d21102be..052fc772 100644 --- a/src/meta/ubi_bao.c +++ b/src/meta/ubi_bao.c @@ -570,7 +570,7 @@ static VGMSTREAM* init_vgmstream_ubi_bao_sequence(ubi_bao_header* bao, STREAMFIL /* build the base VGMSTREAM */ - vgmstream = allocate_vgmstream(data->segments[0]->channels, !bao->sequence_single); + vgmstream = allocate_vgmstream(data->output_channels, !bao->sequence_single); if (!vgmstream) goto fail; vgmstream->meta_type = meta_UBI_BAO; diff --git a/src/meta/ubi_sb.c b/src/meta/ubi_sb.c index 92e96985..8115405d 100644 --- a/src/meta/ubi_sb.c +++ b/src/meta/ubi_sb.c @@ -1477,7 +1477,7 @@ static VGMSTREAM* init_vgmstream_ubi_sb_sequence(ubi_sb_header* sb, STREAMFILE* goto fail; /* build the base VGMSTREAM */ - vgmstream = allocate_vgmstream(data->segments[0]->channels, !sb->sequence_single); + vgmstream = allocate_vgmstream(data->output_channels, !sb->sequence_single); if (!vgmstream) goto fail; vgmstream->meta_type = meta_UBI_SB; diff --git a/src/vgmstream.h b/src/vgmstream.h index 7f80fdfd..2403be28 100644 --- a/src/vgmstream.h +++ b/src/vgmstream.h @@ -767,16 +767,19 @@ typedef enum { } speaker_t; /* typical mappings that metas may use to set channel_layout (but plugin must actually use it) - * (in order, so 3ch file could be mapped to FL FR FC or FL FR LFE but not LFE FL FR) */ + * (in order, so 3ch file could be mapped to FL FR FC or FL FR LFE but not LFE FL FR) + * not too sure about names but no clear standards */ typedef enum { mapping_MONO = speaker_FC, mapping_STEREO = speaker_FL | speaker_FR, mapping_2POINT1 = speaker_FL | speaker_FR | speaker_LFE, - mapping_2POINT1_xiph = speaker_FL | speaker_FR | speaker_FC, + mapping_2POINT1_xiph = speaker_FL | speaker_FR | speaker_FC, /* aka 3STEREO? */ mapping_QUAD = speaker_FL | speaker_FR | speaker_BL | speaker_BR, mapping_QUAD_surround = speaker_FL | speaker_FR | speaker_FC | speaker_BC, + mapping_QUAD_side = speaker_FL | speaker_FR | speaker_SL | speaker_SR, mapping_5POINT0 = speaker_FL | speaker_FR | speaker_LFE | speaker_BL | speaker_BR, mapping_5POINT0_xiph = speaker_FL | speaker_FR | speaker_FC | speaker_BL | speaker_BR, + mapping_5POINT0_surround = speaker_FL | speaker_FR | speaker_FC | speaker_SL | speaker_SR, mapping_5POINT1 = speaker_FL | speaker_FR | speaker_FC | speaker_LFE | speaker_BL | speaker_BR, mapping_5POINT1_surround = speaker_FL | speaker_FR | speaker_FC | speaker_LFE | speaker_SL | speaker_SR, mapping_7POINT0 = speaker_FL | speaker_FR | speaker_FC | speaker_LFE | speaker_BC | speaker_FLC | speaker_FRC, @@ -998,6 +1001,7 @@ typedef struct { sample_t* buffer; int input_channels; /* internal buffer channels */ int output_channels; /* resulting channels (after mixing, if applied) */ + int mixed_channels; /* segments have different number of channels */ } segmented_layout_data; /* for files made of "parallel" layers, one per group of channels (using a complete sub-VGMSTREAM) */ From be8eeb22d44766ebb808f53584bdbb08e7667ded Mon Sep 17 00:00:00 2001 From: bnnm Date: Sun, 22 Nov 2020 19:00:55 +0100 Subject: [PATCH 10/12] Fix multichannel Wwise Opus --- src/coding/coding.h | 2 +- src/coding/ffmpeg_decoder_custom_opus.c | 10 ++-- src/meta/wwise.c | 63 +++++++++++++++++++++---- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/coding/coding.h b/src/coding/coding.h index 82619ec6..6ddaf784 100644 --- a/src/coding/coding.h +++ b/src/coding/coding.h @@ -551,7 +551,7 @@ ffmpeg_codec_data* init_ffmpeg_ue4_opus(STREAMFILE* sf, off_t start_offset, size ffmpeg_codec_data* init_ffmpeg_ea_opus(STREAMFILE* sf, off_t start_offset, size_t data_size, int channels, int skip, int sample_rate); ffmpeg_codec_data* init_ffmpeg_x_opus(STREAMFILE* sf, off_t table_offset, int table_count, off_t data_offset, size_t data_size, int channels, int skip); ffmpeg_codec_data* init_ffmpeg_fsb_opus(STREAMFILE* sf, off_t start_offset, size_t data_size, int channels, int skip, int sample_rate); -ffmpeg_codec_data* init_ffmpeg_wwise_opus(STREAMFILE* sf, off_t table_offset, int table_count, off_t data_offset, size_t data_size, int channels, int skip); +ffmpeg_codec_data* init_ffmpeg_wwise_opus(STREAMFILE* sf, off_t data_offset, size_t data_size, opus_config* cfg); size_t switch_opus_get_samples(off_t offset, size_t stream_size, STREAMFILE* sf); diff --git a/src/coding/ffmpeg_decoder_custom_opus.c b/src/coding/ffmpeg_decoder_custom_opus.c index 2d47a60b..b143998d 100644 --- a/src/coding/ffmpeg_decoder_custom_opus.c +++ b/src/coding/ffmpeg_decoder_custom_opus.c @@ -490,11 +490,11 @@ static size_t make_opus_header(uint8_t* buf, int buf_size, opus_config *cfg) { if (mapping_family > 0) { int i; - /* internal mono/stereo streams (N mono/stereo streams form M channels) */ + /* internal mono/stereo streams (N mono/stereo streams that make M channels) */ put_u8(buf+0x13, cfg->stream_count); - /* joint stereo streams (rest would be mono, so 6ch can be 2ch+2ch+1ch+1ch = 2 coupled */ + /* joint stereo streams (rest would be mono, so 6ch can be 2ch+2ch+1ch+1ch = 2 coupled in 4 streams */ put_u8(buf+0x14, cfg->coupled_count); - /* mapping bits per channel? */ + /* mapping per channel (order of channels, ex: 0x000104050203) */ for (i = 0; i < cfg->channels; i++) { put_u8(buf+0x15+i, cfg->channel_mapping[i]); } @@ -753,8 +753,8 @@ ffmpeg_codec_data* init_ffmpeg_x_opus(STREAMFILE* sf, off_t table_offset, int ta ffmpeg_codec_data* init_ffmpeg_fsb_opus(STREAMFILE* sf, off_t start_offset, size_t data_size, int channels, int skip, int sample_rate) { return init_ffmpeg_custom_opus(sf, start_offset, data_size, channels, skip, sample_rate, OPUS_FSB); } -ffmpeg_codec_data* init_ffmpeg_wwise_opus(STREAMFILE* sf, off_t table_offset, int table_count, off_t data_offset, size_t data_size, int channels, int skip) { - return init_ffmpeg_custom_table_opus(sf, table_offset, table_count, data_offset, data_size, channels, skip, 0, OPUS_WWISE); +ffmpeg_codec_data* init_ffmpeg_wwise_opus(STREAMFILE* sf, off_t data_offset, size_t data_size, opus_config* cfg) { + return init_ffmpeg_custom_opus_config(sf, data_offset, data_size, cfg, OPUS_WWISE); } static opus_type_t get_ue4opus_version(STREAMFILE* sf, off_t offset) { diff --git a/src/meta/wwise.c b/src/meta/wwise.c index f893be23..f2390368 100644 --- a/src/meta/wwise.c +++ b/src/meta/wwise.c @@ -40,6 +40,7 @@ typedef struct { int block_align; int average_bps; int bits_per_sample; + uint8_t channel_type; uint32_t channel_layout; size_t extra_size; @@ -462,7 +463,7 @@ VGMSTREAM* init_vgmstream_wwise(STREAMFILE* sf) { break; } - case OPUS: { /* alt to Vorbis [Girl Cafe Gun (Mobile)] */ + case OPUS: { /* fully standard Ogg Opus [Girl Cafe Gun (Mobile)] */ if (ww.block_align != 0 || ww.bits_per_sample != 0) goto fail; /* extra: size 0x12 */ @@ -484,18 +485,26 @@ VGMSTREAM* init_vgmstream_wwise(STREAMFILE* sf) { break; } - case OPUSWW: { /* updated Opus [Assassin's Creed Valhalla (PC)] */ - int skip, table_count; - + case OPUSWW: { /* updated Opus [Assassin's Creed Valhalla (PC)] */ + int mapping; + opus_config cfg = {0}; + if (ww.block_align != 0 || ww.bits_per_sample != 0) goto fail; if (!ww.seek_offset) goto fail; - /* extra: size 0x10 */ + cfg.channels = ww.channels; + cfg.table_offset = ww.seek_offset; + + /* extra: size 0x10 (though last 2 fields are beyond, AK plz) */ /* 0x12: samples per frame */ vgmstream->num_samples = read_s32(ww.fmt_offset + 0x18, sf); - table_count = read_u32(ww.fmt_offset + 0x1c, sf); /* same as seek size / 2 */ - skip = read_u16(ww.fmt_offset + 0x20, sf); - /* 0x22: 1? (though extra size is declared as 0x10 so this is outsize, AK plz */ + cfg.table_count = read_u32(ww.fmt_offset + 0x1c, sf); /* same as seek size / 2 */ + cfg.skip = read_u16(ww.fmt_offset + 0x20, sf); + /* 0x22: codec version */ + mapping = read_u8(ww.fmt_offset + 0x23, sf); + + if (read_u8(ww.fmt_offset + 0x22, sf) != 1) + goto fail; /* OPUS is VBR so this is very approximate percent, meh */ if (ww.truncated) { @@ -504,8 +513,42 @@ VGMSTREAM* init_vgmstream_wwise(STREAMFILE* sf) { ww.data_size = ww.file_size - start_offset; } + /* AK does some wonky implicit config for multichannel */ + if (mapping == 1 && ww.channel_type == 1) { /* only allowed values ATM, set when >2ch */ + static const int8_t mapping_matrix[8][8] = { + { 0, 0, 0, 0, 0, 0, 0, 0, }, + { 0, 1, 0, 0, 0, 0, 0, 0, }, + { 0, 2, 1, 0, 0, 0, 0, 0, }, + { 0, 1, 2, 3, 0, 0, 0, 0, }, + { 0, 4, 1, 2, 3, 0, 0, 0, }, + { 0, 4, 1, 2, 3, 5, 0, 0, }, + { 0, 6, 1, 2, 3, 4, 5, 0, }, + { 0, 6, 1, 2, 3, 4, 5, 7, }, + }; + int i; + + /* find coupled OPUS streams (internal streams using 2ch) */ + switch(ww.channel_layout) { + case mapping_7POINT1_surround: cfg.coupled_count = 3; break; /* 2ch+2ch+2ch+1ch+1ch, 5 streams */ + case mapping_5POINT1_surround: /* 2ch+2ch+1ch+1ch, 4 streams */ + case mapping_QUAD_side: cfg.coupled_count = 2; break; /* 2ch+2ch, 2 streams */ + case mapping_2POINT1_xiph: /* 2ch+1ch, 2 streams */ + case mapping_STEREO: cfg.coupled_count = 1; break; /* 2ch, 1 stream */ + default: cfg.coupled_count = 0; break; /* 1ch, 1 stream */ + //TODO: AK OPUS doesn't seem to handles others mappings, though AK's .h imply they exist (uses 0 coupleds?) + } + + /* total number internal OPUS streams (should be >0) */ + cfg.stream_count = ww.channels - cfg.coupled_count; + + /* channel assignments */ + for (i = 0; i < ww.channels; i++) { + cfg.channel_mapping[i] = mapping_matrix[ww.channels - 1][i]; + } + } + /* Wwise Opus saves all frame sizes in the seek table */ - vgmstream->codec_data = init_ffmpeg_wwise_opus(sf, ww.seek_offset, table_count, ww.data_offset, ww.data_size, ww.channels, skip); + vgmstream->codec_data = init_ffmpeg_wwise_opus(sf, ww.data_offset, ww.data_size, &cfg); if (!vgmstream->codec_data) goto fail; vgmstream->coding_type = coding_FFmpeg; vgmstream->layout_type = layout_none; @@ -648,6 +691,7 @@ static int parse_wwise(STREAMFILE* sf, wwise_header* ww) { * - HEVAG: very off * - XMA2: exact file size * - some RIFX have LE size + * Value is ignored by AK's parser (set to -1). * (later we'll validate "data" which fortunately is correct) */ if (read_u32(0x04,sf) + 0x04 + 0x04 != ww->file_size) { @@ -743,6 +787,7 @@ static int parse_wwise(STREAMFILE* sf, wwise_header* ww) { * - 4b: eConfigType (0=none, 1=standard, 2=ambisonic) * - 19b: uChannelMask */ if ((ww->channel_layout & 0xFF) == ww->channels) { + ww->channel_type = (ww->channel_layout >> 8) & 0x0F; ww->channel_layout = (ww->channel_layout >> 12); } } From 2f516d4e2992d4169a9dec92c6b248fcb444b00c Mon Sep 17 00:00:00 2001 From: bnnm Date: Sun, 22 Nov 2020 19:01:17 +0100 Subject: [PATCH 11/12] Add RIFF .ima [Baja: Edge of Control (PS3/X360)] --- src/meta/riff.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/meta/riff.c b/src/meta/riff.c index 9b488c2d..6428bbf7 100644 --- a/src/meta/riff.c +++ b/src/meta/riff.c @@ -347,8 +347,9 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { * .at9: standard ATRAC9 * .saf: Whacked! (Xbox) * .mwv: Level-5 games [Dragon Quest VIII (PS2), Rogue Galaxy (PS2)] + * .ima: Baja: Edge of Control (PS3/X360) */ - if ( check_extensions(sf, "wav,lwav,xwav,da,dax,cd,med,snd,adx,adp,xss,xsew,adpcm,adw,wd,,sbv,wvx,str,at3,rws,aud,at9,saf") ) { + if ( check_extensions(sf, "wav,lwav,xwav,da,dax,cd,med,snd,adx,adp,xss,xsew,adpcm,adw,wd,,sbv,wvx,str,at3,rws,aud,at9,saf,ima") ) { ; } else if ( check_extensions(sf, "mwv") ) { From 3b80e4c813b5f1772daa5ee5d82a8c3035be4895 Mon Sep 17 00:00:00 2001 From: bnnm Date: Sun, 22 Nov 2020 20:14:05 +0100 Subject: [PATCH 12/12] Improve seeking speed in layered/segmented layout in some cases --- src/layout/layered.c | 12 ++++++++++++ src/layout/layout.h | 6 ++++-- src/layout/segmented.c | 18 +++++++++++------- src/render.c | 33 +++++++++++++++++++++++++++------ 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/layout/layered.c b/src/layout/layered.c index 25f6e212..60c24f2a 100644 --- a/src/layout/layered.c +++ b/src/layout/layered.c @@ -76,6 +76,18 @@ decode_fail: } +void seek_layout_layered(VGMSTREAM* vgmstream, int32_t seek_sample) { + int layer; + layered_layout_data* data = vgmstream->layout_data; + + for (layer = 0; layer < data->layer_count; layer++) { + seek_vgmstream(data->layers[layer], seek_sample); + } + + vgmstream->current_sample = seek_sample; + vgmstream->samples_into_block = seek_sample; +} + void loop_layout_layered(VGMSTREAM* vgmstream, int32_t loop_sample) { int layer; layered_layout_data* data = vgmstream->layout_data; diff --git a/src/layout/layout.h b/src/layout/layout.h index db4beef0..0a1ae872 100644 --- a/src/layout/layout.h +++ b/src/layout/layout.h @@ -59,7 +59,8 @@ segmented_layout_data* init_layout_segmented(int segment_count); int setup_layout_segmented(segmented_layout_data* data); void free_layout_segmented(segmented_layout_data* data); void reset_layout_segmented(segmented_layout_data* data); -void loop_layout_segmented(VGMSTREAM* vgmstream, int32_t seek_sample); +void seek_layout_segmented(VGMSTREAM* vgmstream, int32_t seek_sample); +void loop_layout_segmented(VGMSTREAM* vgmstream, int32_t loop_sample); VGMSTREAM *allocate_segmented_vgmstream(segmented_layout_data* data, int loop_flag, int loop_start_segment, int loop_end_segment); void render_vgmstream_layered(sample_t* buffer, int32_t sample_count, VGMSTREAM* vgmstream); @@ -67,7 +68,8 @@ layered_layout_data* init_layout_layered(int layer_count); int setup_layout_layered(layered_layout_data* data); void free_layout_layered(layered_layout_data* data); void reset_layout_layered(layered_layout_data* data); -void loop_layout_layered(VGMSTREAM* vgmstream, int32_t seek_sample); +void seek_layout_layered(VGMSTREAM* vgmstream, int32_t seek_sample); +void loop_layout_layered(VGMSTREAM* vgmstream, int32_t loop_sample); VGMSTREAM *allocate_layered_vgmstream(layered_layout_data* data); #endif diff --git a/src/layout/segmented.c b/src/layout/segmented.c index a8baf530..566b2958 100644 --- a/src/layout/segmented.c +++ b/src/layout/segmented.c @@ -115,7 +115,7 @@ static inline void copy_samples(sample_t* outbuf, segmented_layout_data* data, i } -void loop_layout_segmented(VGMSTREAM* vgmstream, int32_t loop_sample) { +void seek_layout_segmented(VGMSTREAM* vgmstream, int32_t seek_sample) { int segment, total_samples; segmented_layout_data* data = vgmstream->layout_data; @@ -124,13 +124,13 @@ void loop_layout_segmented(VGMSTREAM* vgmstream, int32_t loop_sample) { while (total_samples < vgmstream->num_samples) { int32_t segment_samples = vgmstream_get_samples(data->segments[segment]); - /* find if loop falls within segment's samples */ - if (loop_sample >= total_samples && loop_sample < total_samples + segment_samples) { - int32_t loop_relative = loop_sample - total_samples; + /* find if sample falls within segment's samples */ + if (seek_sample >= total_samples && seek_sample < total_samples + segment_samples) { + int32_t seek_relative = seek_sample - total_samples; - seek_vgmstream(data->segments[segment], loop_relative); + seek_vgmstream(data->segments[segment], seek_relative); data->current_segment = segment; - vgmstream->samples_into_block = loop_relative; + vgmstream->samples_into_block = seek_relative; break; } total_samples += segment_samples; @@ -138,10 +138,14 @@ void loop_layout_segmented(VGMSTREAM* vgmstream, int32_t loop_sample) { } if (segment == data->segment_count) { - VGM_LOG("SEGMENTED: can't find loop segment\n"); + VGM_LOG("SEGMENTED: can't find seek segment\n"); } } +void loop_layout_segmented(VGMSTREAM* vgmstream, int32_t loop_sample) { + loop_layout_segmented(vgmstream, loop_sample); +} + segmented_layout_data* init_layout_segmented(int segment_count) { segmented_layout_data* data = NULL; diff --git a/src/render.c b/src/render.c index 8e3cec27..bed5d073 100644 --- a/src/render.c +++ b/src/render.c @@ -548,6 +548,33 @@ void seek_vgmstream(VGMSTREAM* vgmstream, int32_t seek_sample) { int is_looped = vgmstream->loop_flag || vgmstream->loop_target > 0; /* loop target disabled loop flag during decode */ + /* cleanup */ + if (seek_sample < 0) + seek_sample = 0; + /* play forever can seek past max */ + if (vgmstream->config_enabled && seek_sample > ps->play_duration && !play_forever) + seek_sample = ps->play_duration; + + + /* optimize as layouts can seek faster internally */ + if (vgmstream->layout_type == layout_segmented) { + seek_layout_segmented(vgmstream, seek_sample); + + if (vgmstream->config_enabled) { + vgmstream->pstate.play_position = seek_sample; + } + return; + } + else if (vgmstream->layout_type == layout_layered) { + seek_layout_layered(vgmstream, seek_sample); + + if (vgmstream->config_enabled) { + vgmstream->pstate.play_position = seek_sample; + } + return; + } + + /* will decode and loop until seek sample, but slower */ //todo apply same loop logic as below, or pretend we have play_forever + settings? if (!vgmstream->config_enabled) { @@ -580,12 +607,6 @@ void seek_vgmstream(VGMSTREAM* vgmstream, int32_t seek_sample) { * | pad-begin | body-begin | body-loop0 | body-loop1 | body-loop2 | fade | pad-end + beyond) * 0 5s (-3s) 25s 95s 165s 235s 245s Ns */ - - if (seek_sample < 0) - seek_sample = 0; - if (seek_sample > ps->play_duration && !play_forever) /* play forever can seek to any loop */ - seek_sample = ps->play_duration; - //;VGM_LOG("SEEK: seek sample=%i, is_looped=%i\n", seek_sample, is_looped); /* start/pad-begin: consume pad samples */