From 976e1f3efe7430285056e7b5d278e74790eece94 Mon Sep 17 00:00:00 2001 From: bnnm Date: Sat, 24 Nov 2018 01:45:04 +0100 Subject: [PATCH] Add TXTH dual file/bigfile/subsong/multiplier handling - Can loading a separate header_file or body_file for dual files - Can load from .txth directly while setting header_file/body_file for bigfiles - Can set value_multiplier for use with sector and similar values that must be multiplied - Can set subsong_count and subsong_offset for subsong headers suboffset --- doc/TXTH.md | 190 +++++++++++++++++++++------- src/formats.c | 1 + src/meta/txth.c | 320 ++++++++++++++++++++++++++++++++++-------------- 3 files changed, 374 insertions(+), 137 deletions(-) diff --git a/doc/TXTH.md b/doc/TXTH.md index 7a88a8d3..84ba6854 100644 --- a/doc/TXTH.md +++ b/doc/TXTH.md @@ -2,7 +2,7 @@ TXTH is a simple text file that uses text commands to simulate a header for files unsupported by vgmstream, mainly headerless audio. -When an unsupported file is loaded, vgmstream tries to find a TXTH header in the same dir, in this order: +When an unsupported file is loaded (for instance "bgm01.snd"), vgmstream tries to find a TXTH header in the same dir, in this order: - (filename.ext).txth - .(ext).txth - .txth @@ -31,16 +31,16 @@ A text file with the above commands must be saved as ".vag.txth" or ".txth", not ``` ###################################################### -# comments start with #, can be inline # The file is made lines of "key = value" describing a header. -# Spaces are optional: key=value, key= value, and so on are all ok. +# Comments start with #, can be inlined. +# Spaces are optional: key=value, key = value, and so on are all ok. # The parser is fairly simple and may be buggy or unexpected in some cases. # The order of keys is variable but some things won't work if others aren't defined # (ex. bytes-to-samples may not work without channels or interleave). # Common values: # - (number): constant number in dec/hex. -# Examples: 44100, 40, 0x40 (dec=64) +# Examples: 44100, 40, 0x40 (decimal=64) # - (offset): format is @(number)[:LE|BE][$1|2|3|4] # * @(number): value at offset (required) # * :LE|BE: value is little/big endian (optional, defaults to LE) @@ -52,73 +52,76 @@ A text file with the above commands must be saved as ".vag.txth" or ".txth", not # Accepted codec strings: # - PSX PlayStation ADPCM # - XBOX Xbox IMA ADPCM -# - NGC_DTK Nintendo ADP/DTK ADPCM -# - PCM16BE PCM RAW 16bit big endian -# - PCM16LE PCM RAW 16bit little endian -# - PCM8 PCM RAW 8bit +# - NGC_DTK|DTK Nintendo ADP/DTK ADPCM +# - PCM16BE PCM 16-bit big endian +# - PCM16LE PCM 16-bit little endian +# - PCM8 PCM 8-bit # - SDX2 Squareroot-delta-exact 8-bit DPCM (3DO games) # - DVI_IMA DVI IMA ADPCM -# - MPEG MPEG Audio Layer File (MP1/2/3) +# - MPEG MPEG Audio Layer file (MP1/2/3) # - IMA IMA ADPCM # - AICA Yamaha AICA ADPCM (Dreamcast) # - MSADPCM Microsoft ADPCM (Windows) -# - NGC_DSP Nintendo GameCube ADPCM +# - NGC_DSP|DSP Nintendo GameCube ADPCM # - PCM8_U_int PCM RAW 8bit unsigned (interleaved) # - PSX_bf PlayStation ADPCM with bad flags # - MS_IMA Microsoft IMA ADPCM -# - PCM8_U PCM RAW 8bit unsigned +# - PCM8_U PCM 8-bit unsigned # - APPLE_IMA4 Apple Quicktime IMA ADPCM -# - ATRAC3 raw ATRAC3 -# - ATRAC3PLUS raw ATRAC3PLUS -# - XMA1 raw XMA1 -# - XMA2 raw XMA2 -# - FFMPEG any headered FFmpeg format +# - ATRAC3 Sony ATRAC3 +# - ATRAC3PLUS Sony ATRAC3plus +# - XMA1 Microsoft XMA1 +# - XMA2 Microsoft XMA2 +# - FFMPEG Any headered FFmpeg format +# - AC3 AC3/SPDIF codec = (codec string) - -# Varies with codec: + +# Codec variations [OPTIONAL, depends on codec] # - NGC_DSP: 0=normal interleave, 1=byte interleave, 2=no interleave # - ATRAC3: 0=autodetect joint stereo, 1=force joint stereo, 2=force normal stereo # - XMA1|XMA2: 0=dual multichannel (2ch xN), 1=single multichannel (1ch xN) # - XBOX: 0=standard (mono or stereo interleave), 1=force mono interleave mode # - others: ignored codec_mode = (number) - + +# Multiplies any next read values [OPTIONAL] +# Will change to "(key) = (number)|(offset) * value_multiplier", +# useful if a size is given in 0x800 sectors or such. +# Should be set to 0 when done using, as it affects ANY value. +value_multiplier = (number)|(offset) + # Interleave or block size [REQUIRED/OPTIONAL, depends on codec] -# For mono/interleaved codecs it's the amount of data between channels. -# For codecs with variable-sized frames (MSADPCM, MS-IMA, ATRAC3/plus) -# it's the block size (size of a single frame). +# - half_size: sets interleave as data_size / channels +# For mono/interleaved codecs it's the amount of data between channels, +# and for codecs with variable-sized frames (MSADPCM, MS-IMA, ATRAC3/plus) +# means block size (size of a single frame). # Interleave 0 means "stereo mode" for some codecs (IMA, AICA, etc). -interleave = (number)|(offset) +interleave = (number)|(offset)|half_size # Validate that id_value matches value at id_offset [OPTIONAL] # Can be redefined several times, it's checked whenever a new id_offset is found. id_value = (number)|(offset) id_offset = (number)|(offset) - + # Number of channels [REQUIRED] channels = (number)|(offset) - + # Music frequency in hz [REQUIRED] sample_rate = (number)|(offset) - + # Data start [OPTIONAL, default to 0] start_offset = (number)|(offset) - + # Variable that can be used in sample values [OPTIONAL] -# Defaults to (file_size - start_offset), re-calculated when start_offset is set. +# Defaults to (file_size - start_offset), re-calculated when start_offset +# is set (won't recalculate if data_size is set then start_offset changes). data_size = (number)|(offset) - - + # Modifies the meaning of sample fields when set *before* them [OPTIONAL, defaults to samples] # - samples: exact sample # - bytes: automatically converts bytes/offset to samples # - blocks: same as bytes, but value is given in blocks/frames # Value is internally converted from blocks to bytes first: bytes = (value * interleave*channels) -# It's possible to re-define values multiple times: -# * samples_type=bytes ... num_samples=@0x10 -# * samples_type=sample ... loop_end_sample=@0x14 -# Sometimes "bytes" values are given for a single channel only. In that case you can temporally set 1 channel -# * channels=1 ... sample_type=bytes ... num_samples=@0x10 ... channels=2 # Some codecs can't convert bytes-to-samples at the moment: MPEG/FFMPEG # For XMA1/2 bytes does special parsing, with loop values being bit offsets within data. sample_type = samples|bytes|blocks @@ -128,29 +131,124 @@ num_samples = (number)|(offset)|data_size loop_start_sample = (number)|(offset) loop_end_sample = (number)|(offset)|data_size -# For XMA1/2 + sample_type=bytes it means loop subregion, if read after loop values. -# For other codecs its added to loop start/end, if read before loop values -# (rarely a format may have rough loop offset/bytes, then a loop adjust in samples). -loop_adjust = (number)|(offset) - # Force loop, on (>0) or off (0), as loop start/end may be defined but not used [OPTIONAL] +# - auto: tries to autodetect loop points for PS-ADPCM data, which may include loop flags. # By default it loops when loop_end_sample is defined -# auto tries to autodetect loop points for PS-ADPCM data, which may include loop flags. loop_flag = (number)|(offset)|auto -# beginning samples to skip (encoder delay), for codecs that need them (ATRAC3/XMA/etc) +# Loop start/end modifier [OPTIONAL] +# For XMA1/2 + sample_type=bytes it means loop subregion, if read after loop values. +# For other codecs its added to loop start/end, if read before loop values +# (a format may rarely have rough loop offset/bytes, then a loop adjust in samples). +loop_adjust = (number)|(offset) + +# Beginning samples to skip (encoder delay) [OPTIONAL] +# Only some codecs use them (ATRAC3/ATRAC3PLUS/XMA/FFMPEG/AC3) skip_samples = (number)|(offset) -# DSP coefficients [REQUIRED for NGC_DSP] -# Coefs start +# DSP decoding coefficients [REQUIRED for NGC_DSP] +# These coefs are a list of 8*2 16-bit values per channel, starting from offset. coef_offset = (number)|(offset) -# offset separation per channel, usually 0x20 -# - Example: channel N coefs are read at coef_offset + coef_spacing * N +# Offset separation per channel, usually 0x20 (16 values * 2 bytes) +# Channel N coefs are read at coef_offset + coef_spacing * N coef_spacing = (number)|(offset) # Format, usually BE; with (offset): 0=LE, >0=BE coef_endianness = BE|LE|(offset) # Split/normal coefs [NOT IMPLEMENTED YET] #coef_mode = (number)|(offset) + +# Change header/body to external files [OPTIONAL] +# TXTH commands are done on a "header", and decoding on "body". +# When loading an unsupported file it becomes the "base" file +# that loads the .txth, and is both header and body. +# +# You can alter those, mainly for files that split header and body +# in separate files (load base file and txth sets header on another file). +# It's also possible to load the .txth directly with a set body, as a sort of +# "reverse TXTH" (useful with bigfiles, as you could have one .txth per song). +# +# Allowed values: +# - (filename): open any file, subdirs also work (dir/filename) +# - *.(extension): opens with same name as the "base" file plus another extension +# - null: unloads file and goes back to defaults (body/header = base file). +header_file = (filename)|*.(extension)|null +body_file = (filename)|*.(extension)|null + +# Subsongs [OPTIONAL] +# Sets the number of subsongs in the file, adjusting reads per subsong N: +# "value = @(offset) + subsong_offset*N". Mainly for bigfiles with consecutive +# headers per subsong, set subsong_offset to 0 when done as it affects any reads. +# The current subsong number is handled externally by plugins or TXTP. +subsong_count = (number)|(offset) +subsong_offset = (number)|(offset) +``` + +## Usages + +### Temporary values +Most commands are evaluated and calculated immediatedly, every time they are found. This is by design, as it can be used to adjust and trick for certain calculations. + +For example, normally you are given a data_size in bytes, that can be used to calculate num_samples for all channels. +``` +channels = 2 +sample_type = bytes +num_samples = @0x10 #calculated from data_size +``` + +But sometimes this size is for a single channel only (even though the file may be stereo). You can set temporally change the channel number to force a correct calculation. +``` +channels = 1 #not the actual number of channels +sample_type = bytes +num_samples = @0x10 #calculated from channel_size +channels = 2 #change once calculations are done +``` + +### Redefining values +Some commands alter the function of all next commands and can be redefined as needed: +``` +samples_type = bytes +num_samples = @0x10 + +samples_type = sample +loop_end_sample = @0x14 +``` + +### External files +When setting external files all commands are done on the "header" file, but with some creativity you can read in multiple files. +``` +body_file = bgm01.bdy +header_file = bgm01.hdr +channels = @0x10 #base info in bgm01.hdr +header_file = bgm01.bdy +coef_offset = 0x00 #DSP coefs in bgm01.bdy +``` +Note that DSP coefs are special in that aren't read immediately, and will use *last* header_file set. + + +### Resetting values +Values may need to be reset (to 0 or other sensible value) when done. Subsong example: +``` +subsong_count = 5 +subsong_offset = 0x20 # there are 5 subsong headers, 0x20 each +channel_count = @0x10 # reads channels at 0x10+0x20*subsong +# 1st subsong: 0x10+0x20*0: 0x10 +# 2nd subsong: 0x10+0x20*1: 0x30 +# 2nd subsong: 0x10+0x20*2: 0x50 +# ... +start_offset = @0x14 # reads offset within data at 0x14+0x20*subsong + +subsong_offset = 0 # reset value +sample_rate = 0x04 # sample rate is the same for all subsongs +# Nth subsong ch: 0x04+0x00*N: 0x08 +``` + +### Multipliers +Sometimes header values are in "sectors" or similar concepts (typical in DVD games), and need to be adjusted to a real value. +``` +value_multiplier = 0x800 # offsets are in DVD sector size +start_offset = @0x10 # 2*0x800, for example +value_multiplier = 0 # next values don't need to be multiplied +start_offset = @0x14 ``` diff --git a/src/formats.c b/src/formats.c index c049147b..d6f1e798 100644 --- a/src/formats.c +++ b/src/formats.c @@ -392,6 +392,7 @@ static const char* extension_list[] = { "trj", "trm", "tun", + "txth", "txtp", "tydsp", diff --git a/src/meta/txth.c b/src/meta/txth.c index b938d6fd..3cdd7170 100644 --- a/src/meta/txth.c +++ b/src/meta/txth.c @@ -35,6 +35,7 @@ typedef enum { typedef struct { txth_type codec; uint32_t codec_mode; + uint32_t value_multiplier; uint32_t interleave; uint32_t id_value; @@ -66,12 +67,29 @@ typedef struct { int num_samples_data_size; + int target_subsong; + uint32_t subsong_count; + uint32_t subsong_offset; + + /* original STREAMFILE and its type (may be an unsupported "base" file or a .txth) */ + STREAMFILE *streamFile; + int streamfile_is_txth; + + /* configurable STREAMFILEs and if we opened it (thus must close it later) */ + STREAMFILE *streamText; + STREAMFILE *streamHead; + STREAMFILE *streamBody; + int streamtext_opened; + int streamhead_opened; + int streambody_opened; + } txth_header; + static STREAMFILE * open_txth(STREAMFILE * streamFile); -static int parse_txth(STREAMFILE * streamFile, STREAMFILE * streamText, txth_header * txth); -static int parse_keyval(STREAMFILE * streamFile, STREAMFILE * streamText, txth_header * txth, const char * key, const char * val); -static int parse_num(STREAMFILE * streamFile, const char * val, uint32_t * out_value); +static int parse_txth(txth_header * txth); +static int parse_keyval(STREAMFILE * streamFile, txth_header * txth, const char * key, char * val); +static int parse_num(STREAMFILE * streamFile, txth_header * txth, const char * val, uint32_t * out_value); static int get_bytes_to_samples(txth_header * txth, uint32_t bytes); @@ -79,23 +97,42 @@ static int get_bytes_to_samples(txth_header * txth, uint32_t bytes); * Similar to GENH, but with a single separate .txth file in the dir and text-based. */ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { VGMSTREAM * vgmstream = NULL; - STREAMFILE * streamText = NULL; txth_header txth = {0}; coding_t coding; int i, j; - /* reject .txth as the CLI can open and decode with itself */ - if (check_extensions(streamFile, "txth")) - goto fail; + /* accept .txth (should set body_file or will fail later) */ + if (check_extensions(streamFile, "txth")) { + txth.streamFile = streamFile; + txth.streamfile_is_txth = 1; - /* no need for ID or ext checks -- if a .TXTH exists all is good - * (player still needs to accept the streamfile's ext, so at worst rename to .vgmstream) */ - streamText = open_txth(streamFile); - if (!streamText) goto fail; + txth.streamText = streamFile; + txth.streamHead = NULL; + txth.streamBody = NULL; + txth.streamtext_opened = 0; + txth.streamhead_opened = 0; + txth.streambody_opened = 0; + } + else { + /* accept base file (no need for ID or ext checks --if a companion .TXTH exists all is good) + * (player still needs to accept the streamfile's ext, so at worst rename to .vgmstream) */ + STREAMFILE * streamText = open_txth(streamFile); + if (!streamText) goto fail; + + txth.streamFile = streamFile; + txth.streamfile_is_txth = 0; + + txth.streamText = streamText; + txth.streamHead = streamFile; + txth.streamBody = streamFile; + txth.streamtext_opened = 1; + txth.streamhead_opened = 0; + txth.streambody_opened = 0; + } /* process the text file */ - if (!parse_txth(streamFile, streamText, &txth)) + if (!parse_txth(&txth)) goto fail; @@ -135,8 +172,8 @@ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { /* try to autodetect PS-ADPCM loop data */ if (txth.loop_flag_auto && coding == coding_PSX) { - size_t data_size = get_streamfile_size(streamFile) - txth.start_offset; - txth.loop_flag = ps_find_loop_offsets(streamFile, txth.start_offset, data_size, txth.channels, txth.interleave, + size_t data_size = get_streamfile_size(txth.streamBody) - txth.start_offset; + txth.loop_flag = ps_find_loop_offsets(txth.streamBody, txth.start_offset, data_size, txth.channels, txth.interleave, (int32_t*)&txth.loop_start_sample, (int32_t*)&txth.loop_end_sample); } @@ -149,6 +186,10 @@ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { vgmstream->num_samples = txth.num_samples; vgmstream->loop_start_sample = txth.loop_start_sample; vgmstream->loop_end_sample = txth.loop_end_sample; + if (txth.subsong_count) { + vgmstream->num_streams = txth.subsong_count; + vgmstream->stream_size = txth.data_size; + } /* codec specific (taken from GENH with minimal changes) */ switch (coding) { @@ -262,15 +303,15 @@ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { /* normal/split coefs */ if (txth.coef_mode == 0) { for (j=0;j<16;j++) { - vgmstream->ch[i].adpcm_coef[j] = read_16bit(txth.coef_offset + i*txth.coef_spacing + j*2,streamFile); + vgmstream->ch[i].adpcm_coef[j] = read_16bit(txth.coef_offset + i*txth.coef_spacing + j*2,txth.streamHead); } } else { goto fail; //IDK what is this /* for (j=0;j<8;j++) { - vgmstream->ch[i].adpcm_coef[j*2]=read_16bit(coef[i]+j*2,streamFile); - vgmstream->ch[i].adpcm_coef[j*2+1]=read_16bit(coef_splitted[i]+j*2,streamFile); + vgmstream->ch[i].adpcm_coef[j*2]=read_16bit(coef[i]+j*2,txth.streamHead); + vgmstream->ch[i].adpcm_coef[j*2+1]=read_16bit(coef_splitted[i]+j*2,txth.streamHead); } */ } @@ -280,7 +321,7 @@ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { #ifdef VGM_USE_MPEG case coding_MPEG_layer3: vgmstream->layout_type = layout_none; - vgmstream->codec_data = init_mpeg(streamFile, txth.start_offset, &coding, vgmstream->channels); + vgmstream->codec_data = init_mpeg(txth.streamBody, txth.start_offset, &coding, vgmstream->channels); if (!vgmstream->codec_data) goto fail; break; @@ -291,7 +332,7 @@ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { if (txth.codec == FFMPEG || txth.codec == AC3) { /* default FFmpeg */ - ffmpeg_data = init_ffmpeg_offset(streamFile, txth.start_offset,txth.data_size); + ffmpeg_data = init_ffmpeg_offset(txth.streamBody, txth.start_offset,txth.data_size); if ( !ffmpeg_data ) goto fail; if (vgmstream->num_samples == 0) @@ -336,7 +377,7 @@ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { goto fail; } - ffmpeg_data = init_ffmpeg_header_offset(streamFile, buf,bytes, txth.start_offset,txth.data_size); + ffmpeg_data = init_ffmpeg_header_offset(txth.streamBody, buf,bytes, txth.start_offset,txth.data_size); if ( !ffmpeg_data ) goto fail; } @@ -344,7 +385,7 @@ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { vgmstream->layout_type = layout_none; if (txth.codec == XMA1 || txth.codec == XMA2) { - xma_fix_raw_samples(vgmstream, streamFile, txth.start_offset,txth.data_size, 0, 0,0); + xma_fix_raw_samples(vgmstream, txth.streamBody, txth.start_offset,txth.data_size, 0, 0,0); } else if (txth.skip_samples_set) { /* force encoder delay */ ffmpeg_set_skip_samples(ffmpeg_data, txth.skip_samples); } @@ -373,14 +414,13 @@ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { msd.loop_end_subframe = txth.loop_adjust >> 4; /* upper 4b: subframe where the loop ends, 0..3 */ } - xma_get_samples(&msd, streamFile); + xma_get_samples(&msd, txth.streamBody); vgmstream->num_samples = msd.num_samples; if (txth.sample_type==1) { vgmstream->loop_start_sample = msd.loop_start_sample; vgmstream->loop_end_sample = msd.loop_end_sample; } - //skip_samples = msd.skip_samples; //todo add skip samples } #endif @@ -389,14 +429,18 @@ VGMSTREAM * init_vgmstream_txth(STREAMFILE *streamFile) { vgmstream->allow_dual_stereo = 1; - if ( !vgmstream_open_stream(vgmstream,streamFile,txth.start_offset) ) + if ( !vgmstream_open_stream(vgmstream,txth.streamBody,txth.start_offset) ) goto fail; - if (streamText) close_streamfile(streamText); + if (txth.streamtext_opened) close_streamfile(txth.streamText); + if (txth.streamhead_opened) close_streamfile(txth.streamHead); + if (txth.streambody_opened) close_streamfile(txth.streamBody); return vgmstream; fail: - if (streamText) close_streamfile(streamText); + if (txth.streamtext_opened) close_streamfile(txth.streamText); + if (txth.streamhead_opened) close_streamfile(txth.streamHead); + if (txth.streambody_opened) close_streamfile(txth.streamBody); close_vgmstream(vgmstream); return NULL; } @@ -434,15 +478,25 @@ static STREAMFILE * open_txth(STREAMFILE * streamFile) { /* Simple text parser of "key = value" lines. * The code is meh and error handling not exactly the best. */ -static int parse_txth(STREAMFILE * streamFile, STREAMFILE * streamText, txth_header * txth) { +static int parse_txth(txth_header * txth) { off_t txt_offset = 0x00; - off_t file_size = get_streamfile_size(streamText); + off_t file_size = get_streamfile_size(txth->streamText); + + /* setup txth defaults */ + if (txth->streamBody) + txth->data_size = get_streamfile_size(txth->streamBody); + txth->target_subsong = txth->streamFile->stream_index; + if (txth->target_subsong == 0) txth->target_subsong = 1; - txth->data_size = get_streamfile_size(streamFile); /* for later use */ /* skip BOM if needed */ - if ((uint16_t)read_16bitLE(0x00, streamText) == 0xFFFE || (uint16_t)read_16bitLE(0x00, streamText) == 0xFEFF) + if ((uint16_t)read_16bitLE(0x00, txth->streamText) == 0xFFFE || + (uint16_t)read_16bitLE(0x00, txth->streamText) == 0xFEFF) { txt_offset = 0x02; + } + else if (((uint32_t)read_32bitBE(0x00, txth->streamText) & 0xFFFFFF00) == 0xEFBBBF00) { + txt_offset = 0x03; + } /* read lines */ while (txt_offset < file_size) { @@ -450,7 +504,7 @@ static int parse_txth(STREAMFILE * streamFile, STREAMFILE * streamText, txth_hea char key[TXT_LINE_MAX] = {0}, val[TXT_LINE_MAX] = {0}; /* at least as big as a line to avoid overflows (I hope) */ int ok, bytes_read, line_done; - bytes_read = get_streamfile_text_line(TXT_LINE_MAX,line, txt_offset,streamText, &line_done); + bytes_read = get_streamfile_text_line(TXT_LINE_MAX,line, txt_offset,txth->streamText, &line_done); if (!line_done) goto fail; txt_offset += bytes_read; @@ -460,49 +514,58 @@ static int parse_txth(STREAMFILE * streamFile, STREAMFILE * streamText, txth_hea if (ok != 2) /* ignore line if no key=val (comment or garbage) */ continue; - if (!parse_keyval(streamFile, streamText, txth, key, val)) /* read key/val */ + if (!parse_keyval(txth->streamFile, txth, key, val)) /* read key/val */ goto fail; } if (!txth->loop_flag_set) txth->loop_flag = txth->loop_end_sample && txth->loop_end_sample != 0xFFFFFFFF; + if (!txth->streamBody) + goto fail; + return 1; fail: return 0; } -static int parse_keyval(STREAMFILE * streamFile, STREAMFILE * streamText, txth_header * txth, const char * key, const char * val) { +static int parse_keyval(STREAMFILE * streamFile_, txth_header * txth, const char * key, char * val) { + //;VGM_LOG("TXTH: key=%s, val=%s\n", key, val); if (0==strcmp(key,"codec")) { - if (0==strcmp(val,"PSX")) txth->codec = PSX; - else if (0==strcmp(val,"XBOX")) txth->codec = XBOX; - else if (0==strcmp(val,"NGC_DTK")) txth->codec = NGC_DTK; - else if (0==strcmp(val,"PCM16BE")) txth->codec = PCM16BE; - else if (0==strcmp(val,"PCM16LE")) txth->codec = PCM16LE; - else if (0==strcmp(val,"PCM8")) txth->codec = PCM8; - else if (0==strcmp(val,"SDX2")) txth->codec = SDX2; - else if (0==strcmp(val,"DVI_IMA")) txth->codec = DVI_IMA; - else if (0==strcmp(val,"MPEG")) txth->codec = MPEG; - else if (0==strcmp(val,"IMA")) txth->codec = IMA; - else if (0==strcmp(val,"AICA")) txth->codec = AICA; - else if (0==strcmp(val,"MSADPCM")) txth->codec = MSADPCM; - else if (0==strcmp(val,"NGC_DSP")) txth->codec = NGC_DSP; - else if (0==strcmp(val,"PCM8_U_int")) txth->codec = PCM8_U_int; - else if (0==strcmp(val,"PSX_bf")) txth->codec = PSX_bf; - else if (0==strcmp(val,"MS_IMA")) txth->codec = MS_IMA; - else if (0==strcmp(val,"PCM8_U")) txth->codec = PCM8_U; - else if (0==strcmp(val,"APPLE_IMA4")) txth->codec = APPLE_IMA4; - else if (0==strcmp(val,"ATRAC3")) txth->codec = ATRAC3; - else if (0==strcmp(val,"ATRAC3PLUS")) txth->codec = ATRAC3PLUS; - else if (0==strcmp(val,"XMA1")) txth->codec = XMA1; - else if (0==strcmp(val,"XMA2")) txth->codec = XMA2; - else if (0==strcmp(val,"FFMPEG")) txth->codec = FFMPEG; - else if (0==strcmp(val,"AC3")) txth->codec = AC3; + if (0==strcmp(val,"PSX")) txth->codec = PSX; + else if (0==strcmp(val,"XBOX")) txth->codec = XBOX; + else if (0==strcmp(val,"NGC_DTK")) txth->codec = NGC_DTK; + else if (0==strcmp(val,"DTK")) txth->codec = NGC_DTK; + else if (0==strcmp(val,"PCM16BE")) txth->codec = PCM16BE; + else if (0==strcmp(val,"PCM16LE")) txth->codec = PCM16LE; + else if (0==strcmp(val,"PCM8")) txth->codec = PCM8; + else if (0==strcmp(val,"SDX2")) txth->codec = SDX2; + else if (0==strcmp(val,"DVI_IMA")) txth->codec = DVI_IMA; + else if (0==strcmp(val,"MPEG")) txth->codec = MPEG; + else if (0==strcmp(val,"IMA")) txth->codec = IMA; + else if (0==strcmp(val,"AICA")) txth->codec = AICA; + else if (0==strcmp(val,"MSADPCM")) txth->codec = MSADPCM; + else if (0==strcmp(val,"NGC_DSP")) txth->codec = NGC_DSP; + else if (0==strcmp(val,"DSP")) txth->codec = NGC_DSP; + else if (0==strcmp(val,"PCM8_U_int")) txth->codec = PCM8_U_int; + else if (0==strcmp(val,"PSX_bf")) txth->codec = PSX_bf; + else if (0==strcmp(val,"MS_IMA")) txth->codec = MS_IMA; + else if (0==strcmp(val,"PCM8_U")) txth->codec = PCM8_U; + else if (0==strcmp(val,"APPLE_IMA4")) txth->codec = APPLE_IMA4; + else if (0==strcmp(val,"ATRAC3")) txth->codec = ATRAC3; + else if (0==strcmp(val,"ATRAC3PLUS")) txth->codec = ATRAC3PLUS; + else if (0==strcmp(val,"XMA1")) txth->codec = XMA1; + else if (0==strcmp(val,"XMA2")) txth->codec = XMA2; + else if (0==strcmp(val,"FFMPEG")) txth->codec = FFMPEG; + else if (0==strcmp(val,"AC3")) txth->codec = AC3; else goto fail; } else if (0==strcmp(key,"codec_mode")) { - if (!parse_num(streamFile,val, &txth->codec_mode)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->codec_mode)) goto fail; + } + else if (0==strcmp(key,"value_multiplier")) { + if (!parse_num(txth->streamHead,txth,val, &txth->value_multiplier)) goto fail; } else if (0==strcmp(key,"interleave")) { if (0==strcmp(val,"half_size")) { @@ -510,30 +573,32 @@ static int parse_keyval(STREAMFILE * streamFile, STREAMFILE * streamText, txth_h txth->interleave = txth->data_size / txth->channels; } else { - if (!parse_num(streamFile,val, &txth->interleave)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->interleave)) goto fail; } } else if (0==strcmp(key,"id_value")) { - if (!parse_num(streamFile,val, &txth->id_value)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->id_value)) goto fail; } else if (0==strcmp(key,"id_offset")) { - if (!parse_num(streamFile,val, &txth->id_offset)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->id_offset)) goto fail; if (txth->id_value != txth->id_offset) /* evaluate current ID */ goto fail; } else if (0==strcmp(key,"channels")) { - if (!parse_num(streamFile,val, &txth->channels)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->channels)) goto fail; } else if (0==strcmp(key,"sample_rate")) { - if (!parse_num(streamFile,val, &txth->sample_rate)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->sample_rate)) goto fail; } else if (0==strcmp(key,"start_offset")) { - if (!parse_num(streamFile,val, &txth->start_offset)) goto fail; - if (!txth->data_size_set) - txth->data_size = get_streamfile_size(streamFile) - txth->start_offset; /* re-evaluate */ + if (!parse_num(txth->streamHead,txth,val, &txth->start_offset)) goto fail; + if (!txth->data_size_set) { + txth->data_size = !txth->streamBody ? 0 : + get_streamfile_size(txth->streamBody) - txth->start_offset; /* re-evaluate */ + } } else if (0==strcmp(key,"data_size")) { - if (!parse_num(streamFile,val, &txth->data_size)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->data_size)) goto fail; txth->data_size_set = 1; } else if (0==strcmp(key,"sample_type")) { @@ -548,7 +613,7 @@ static int parse_keyval(STREAMFILE * streamFile, STREAMFILE * streamText, txth_h txth->num_samples_data_size = 1; } else { - if (!parse_num(streamFile,val, &txth->num_samples)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->num_samples)) goto fail; if (txth->sample_type==1) txth->num_samples = get_bytes_to_samples(txth, txth->num_samples); if (txth->sample_type==2) @@ -556,7 +621,7 @@ static int parse_keyval(STREAMFILE * streamFile, STREAMFILE * streamText, txth_h } } else if (0==strcmp(key,"loop_start_sample")) { - if (!parse_num(streamFile,val, &txth->loop_start_sample)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->loop_start_sample)) goto fail; if (txth->sample_type==1) txth->loop_start_sample = get_bytes_to_samples(txth, txth->loop_start_sample); if (txth->sample_type==2) @@ -569,7 +634,7 @@ static int parse_keyval(STREAMFILE * streamFile, STREAMFILE * streamText, txth_h txth->loop_end_sample = get_bytes_to_samples(txth, txth->data_size); } else { - if (!parse_num(streamFile,val, &txth->loop_end_sample)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->loop_end_sample)) goto fail; if (txth->sample_type==1) txth->loop_end_sample = get_bytes_to_samples(txth, txth->loop_end_sample); if (txth->sample_type==2) @@ -579,7 +644,7 @@ static int parse_keyval(STREAMFILE * streamFile, STREAMFILE * streamText, txth_h txth->loop_end_sample += txth->loop_adjust; } else if (0==strcmp(key,"skip_samples")) { - if (!parse_num(streamFile,val, &txth->skip_samples)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->skip_samples)) goto fail; txth->skip_samples_set = 1; if (txth->sample_type==1) txth->skip_samples = get_bytes_to_samples(txth, txth->skip_samples); @@ -587,7 +652,7 @@ static int parse_keyval(STREAMFILE * streamFile, STREAMFILE * streamText, txth_h txth->skip_samples = get_bytes_to_samples(txth, txth->skip_samples * (txth->interleave*txth->channels)); } else if (0==strcmp(key,"loop_adjust")) { - if (!parse_num(streamFile,val, &txth->loop_adjust)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->loop_adjust)) goto fail; if (txth->sample_type==1) txth->loop_adjust = get_bytes_to_samples(txth, txth->loop_adjust); if (txth->sample_type==2) @@ -598,28 +663,87 @@ static int parse_keyval(STREAMFILE * streamFile, STREAMFILE * streamText, txth_h txth->loop_flag_auto = 1; } else { - if (!parse_num(streamFile,val, &txth->loop_flag)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->loop_flag)) goto fail; txth->loop_flag_set = 1; } } else if (0==strcmp(key,"coef_offset")) { - if (!parse_num(streamFile,val, &txth->coef_offset)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->coef_offset)) goto fail; } else if (0==strcmp(key,"coef_spacing")) { - if (!parse_num(streamFile,val, &txth->coef_spacing)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->coef_spacing)) goto fail; } else if (0==strcmp(key,"coef_endianness")) { if (val[0]=='B' && val[1]=='E') txth->coef_big_endian = 1; else if (val[0]=='L' && val[1]=='E') txth->coef_big_endian = 0; - else if (!parse_num(streamFile,val, &txth->coef_big_endian)) goto fail; + else if (!parse_num(txth->streamHead,txth,val, &txth->coef_big_endian)) goto fail; } else if (0==strcmp(key,"coef_mode")) { - if (!parse_num(streamFile,val, &txth->coef_mode)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->coef_mode)) goto fail; } else if (0==strcmp(key,"psx_loops")) { - if (!parse_num(streamFile,val, &txth->coef_mode)) goto fail; + if (!parse_num(txth->streamHead,txth,val, &txth->coef_mode)) goto fail; + } + else if (0==strcmp(key,"subsong_count")) { + if (!parse_num(txth->streamHead,txth,val, &txth->subsong_count)) goto fail; + } + else if (0==strcmp(key,"subsong_offset")) { + if (!parse_num(txth->streamHead,txth,val, &txth->subsong_offset)) goto fail; + } + else if (0==strcmp(key,"header_file")) { + if (txth->streamhead_opened) { + close_streamfile(txth->streamHead); + txth->streamHead = NULL; + txth->streamhead_opened = 0; + } + + if (0==strcmp(val,"null")) { /* reset */ + if (!txth->streamfile_is_txth) { + txth->streamHead = txth->streamFile; + } + } + else if (val[0]=='*' && val[1]=='.') { /* basename + extension */ + txth->streamHead = open_streamfile_by_ext(txth->streamFile, (val+2)); + if (!txth->streamHead) goto fail; + txth->streamhead_opened = 1; + } + else { /* open file */ + fix_dir_separators(val); /* clean paths */ + + txth->streamHead = open_streamfile_by_filename(txth->streamFile, val); + if (!txth->streamHead) goto fail; + txth->streamhead_opened = 1; + } + } + else if (0==strcmp(key,"body_file")) { + if (txth->streambody_opened) { + close_streamfile(txth->streamBody); + txth->streamBody = NULL; + txth->streambody_opened = 0; + } + + if (0==strcmp(val,"null")) { /* reset */ + if (!txth->streamfile_is_txth) { + txth->streamBody = txth->streamFile; + } + } + else if (val[0]=='*' && val[1]=='.') { /* basename + extension */ + txth->streamBody = open_streamfile_by_ext(txth->streamFile, (val+2)); + if (!txth->streamBody) goto fail; + txth->streambody_opened = 1; + } + else { /* open file */ + fix_dir_separators(val); /* clean paths */ + + txth->streamBody = open_streamfile_by_filename(txth->streamFile, val); + if (!txth->streamBody) goto fail; + txth->streambody_opened = 1; + } + + txth->data_size = !txth->streamBody ? 0 : + get_streamfile_size(txth->streamBody) - txth->start_offset; /* re-evaluate */ } else { VGM_LOG("TXTH: unknown key=%s, val=%s\n", key,val); @@ -631,27 +755,34 @@ fail: return 0; } -static int parse_num(STREAMFILE * streamFile, const char * val, uint32_t * out_value) { +static int parse_num(STREAMFILE * streamFile, txth_header * txth, const char * val, uint32_t * out_value) { + /* out_value can be these values, save before modifying */ + uint32_t value_multiplier = txth->value_multiplier; + uint32_t subsong_offset = txth->subsong_offset; if (val[0] == '@') { /* offset */ - uint32_t off = 0; + uint32_t offset = 0; char ed1 = 'L', ed2 = 'E'; int size = 4; int big_endian = 0; int hex = (val[1]=='0' && val[2]=='x'); + /* can happen when loading .txth and not setting body/head */ + if (!streamFile) + goto fail; + /* read exactly N fields in the expected format */ if (strchr(val,':') && strchr(val,'$')) { - if (sscanf(val, hex ? "@%x:%c%c$%i" : "@%u:%c%c$%i", &off, &ed1,&ed2, &size) != 4) goto fail; + if (sscanf(val, hex ? "@%x:%c%c$%i" : "@%u:%c%c$%i", &offset, &ed1,&ed2, &size) != 4) goto fail; } else if (strchr(val,':')) { - if (sscanf(val, hex ? "@%x:%c%c" : "@%u:%c%c", &off, &ed1,&ed2) != 3) goto fail; + if (sscanf(val, hex ? "@%x:%c%c" : "@%u:%c%c", &offset, &ed1,&ed2) != 3) goto fail; } else if (strchr(val,'$')) { - if (sscanf(val, hex ? "@%x$%i" : "@%u$%i", &off, &size) != 2) goto fail; + if (sscanf(val, hex ? "@%x$%i" : "@%u$%i", &offset, &size) != 2) goto fail; } else { - if (sscanf(val, hex ? "@%x" : "@%u", &off) != 1) goto fail; + if (sscanf(val, hex ? "@%x" : "@%u", &offset) != 1) goto fail; } - if (off < 0 || off > get_streamfile_size(streamFile)) + if (offset < 0 || offset > get_streamfile_size(streamFile)) goto fail; if (ed1 == 'B' && ed2 == 'E') @@ -659,21 +790,28 @@ static int parse_num(STREAMFILE * streamFile, const char * val, uint32_t * out_v else if (!(ed1 == 'L' && ed2 == 'E')) goto fail; + if (subsong_offset) + offset = offset + subsong_offset * (txth->target_subsong - 1); + switch(size) { - case 1: *out_value = read_8bit(off,streamFile); break; - case 2: *out_value = big_endian ? (uint16_t)read_16bitBE(off,streamFile) : (uint16_t)read_16bitLE(off,streamFile); break; - case 3: *out_value = (big_endian ? (uint32_t)read_32bitBE(off,streamFile) : (uint32_t)read_32bitLE(off,streamFile)) & 0x00FFFFFF; break; - case 4: *out_value = big_endian ? (uint32_t)read_32bitBE(off,streamFile) : (uint32_t)read_32bitLE(off,streamFile); break; + case 1: *out_value = read_8bit(offset,streamFile); break; + case 2: *out_value = big_endian ? (uint16_t)read_16bitBE(offset,streamFile) : (uint16_t)read_16bitLE(offset,streamFile); break; + case 3: *out_value = (big_endian ? (uint32_t)read_32bitBE(offset,streamFile) : (uint32_t)read_32bitLE(offset,streamFile)) & 0x00FFFFFF; break; + case 4: *out_value = big_endian ? (uint32_t)read_32bitBE(offset,streamFile) : (uint32_t)read_32bitLE(offset,streamFile); break; default: goto fail; } } else { /* constant */ int hex = (val[0]=='0' && val[1]=='x'); - if (sscanf(val, hex ? "%x" : "%u", out_value)!=1) goto fail; + if (sscanf(val, hex ? "%x" : "%u", out_value)!=1) + goto fail; } - //VGM_LOG("TXTH: value=%s, read %u\n", val, *out_value); + if (value_multiplier) + *out_value = (*out_value) * value_multiplier; + + //;VGM_LOG("TXTH: val=%s, read %u (0x%x)\n", val, *out_value, *out_value); return 1; fail: return 0;