diff --git a/doc/TXTP.md b/doc/TXTP.md index 7e8763a9..01002b13 100644 --- a/doc/TXTP.md +++ b/doc/TXTP.md @@ -27,6 +27,8 @@ loop_end_segment = 2 # optional, default is last # select subsong 12 bigfiles/bgm.sxd2#12 #relative paths are ok too for TXTP +#bigfiles/bgm.sxd2#s12 # "sN" is al alt for subsong + # single files loop normally by default # if loop segment is defined it forces a full loop (0..num_samples) #loop_start_segment = 1 @@ -44,6 +46,7 @@ amb_fx.sb0#121 #notice "#" works as config or comment loop_start_segment = 3 ``` + ### Channel mask for channel subsongs/layers - __Final Fantasy XIII-2__: _music_Home_01.ps3.txtp_ ``` @@ -59,6 +62,7 @@ music_Home.ps3.scd#c3,4 # song still has 4 channels, just mutes some ``` + ### Multilayered songs TXTP "layers" play songs with channels/parts divided into files as one @@ -93,22 +97,61 @@ file1.ext#m2-3 # "FL BL FR BR" to "FL FR BL BR" file2.ext#m2-3,4-5,4-6 # ogg "FL CN FR BL BR SB" to wav "FL FR CN SB BL BR" ``` + +### Custom play settings +Those setting should override player's defaults if set (except "loop forever"). They are equivalent to some test.exe options. + +- __God Hand (PS2)__: _boss2_3ningumi_ver6.txtp_ (each line is a separate TXTP) +´´´ +# set number of loops +boss2_3ningumi_ver6.adx#l3 + +# set fade time (in seconds) +boss2_3ningumi_ver6.adx#f10.5 + +# set fade delay (in seconds) +boss2_3ningumi_ver6.adx#d0.5 + +# ignore and disable loops +boss2_3ningumi_ver6.adx#i + +# don't fade out and instead play the song ending after +boss2_3ningumi_ver6.adx#F # this song has a nice stop + +# force full loops from end-to-end +boss2_3ningumi_ver6.adx#E + +# settings can be combined +boss2_3ningumi_ver6.adx#l2#F # 2 loops + ending + +# settings can be combined +boss2_3ningumi_ver6.adx#l1.5#d1#f5 + +# boss2_3ningumi_ver6.adx#l1.0#F # this is equivalent to #i +´´´´ + +For segments and layers the first file defines looping options. + + ### Force plugin extensions vgmstream supports a few common extensions that confuse plugins, like .wav/ogg/aac/opus/etc, so for them those extensions are disabled and are expected to be renamed to .lwav/logg/laac/lopus/etc. TXTP can make plugins play those disabled extensions, since it calls files directly by filename. Combined with TXTH, this can also be used for extensions that aren't normally accepted by vgmstream. + ### TXTP combos TXTP may even reference other TXTP, or files that require TXTH, for extra complex cases. Each file defined in TXTP is internally parsed like it was a completely separate file, so there is a bunch of valid ways to mix them. ## Mini TXTP -To simplify TXTP creation, if the .txtp is empty (0 bytes) its filename is used directly as a command. +To simplify TXTP creation, if the .txtp is empty (0 bytes) its filename is used directly as a command. Note that extension is also included (since vgmstream needs a full filename). - _bgm.sxd2#12.txtp_: plays subsong 12 - _Ryoshima Coast 1 & 2.aix#c1,2.txtp_: channel mask +- _boss2_3ningumi_ver6.adx#l2#F.txtp_: loop twice then play song end file normally - etc + ## Other examples _Join "segments" (intro+body):_ diff --git a/src/meta/txtp.c b/src/meta/txtp.c index 032cf3c3..a020c0d4 100644 --- a/src/meta/txtp.c +++ b/src/meta/txtp.c @@ -11,6 +11,13 @@ typedef struct { uint32_t channel_mask; int channel_mappings_on; int channel_mappings[32]; + + double config_loop_count; + double config_fade_time; + double config_fade_delay; + int config_ignore_loop; + int config_force_loop; + int config_ignore_fade; } txtp_entry; typedef struct { @@ -26,9 +33,10 @@ typedef struct { static txtp_header* parse_txtp(STREAMFILE* streamFile); static void clean_txtp(txtp_header* txtp); +static void set_config(VGMSTREAM *vgmstream, txtp_entry *current); -/* TXTP - an artificial playlist-like format to play segmented files with config */ +/* TXTP - an artificial playlist-like format to play files with segments/layers/config */ VGMSTREAM * init_vgmstream_txtp(STREAMFILE *streamFile) { VGMSTREAM * vgmstream = NULL; txtp_header* txtp = NULL; @@ -184,6 +192,9 @@ VGMSTREAM * init_vgmstream_txtp(STREAMFILE *streamFile) { } + /* loop settings apply to the resulting vgmstream, so config based on first entry */ + set_config(vgmstream, &txtp->entry[0]); + clean_txtp(txtp); return vgmstream; @@ -195,13 +206,30 @@ fail: return NULL; } +static void set_config(VGMSTREAM *vgmstream, txtp_entry *current) { + vgmstream->config_loop_count = current->config_loop_count; + vgmstream->config_fade_time = current->config_fade_time; + vgmstream->config_fade_delay = current->config_fade_delay; + vgmstream->config_ignore_loop = current->config_ignore_loop; + vgmstream->config_force_loop = current->config_force_loop; + vgmstream->config_ignore_fade = current->config_ignore_fade; +} + +/* ********************************** */ + +static void get_double(const char * config, double *value) { + int n; + if (sscanf(config, "%lf%n", value,&n) != 1) { + *value = 0; + } +} static int add_filename(txtp_header * txtp, char *filename) { - int i; - uint32_t channel_mask = 0; - int channel_mappings_on = 0; - int channel_mappings[32] = {0}; + int i, n; + txtp_entry cfg = {0}; size_t range_start, range_end; + const char separator = '#'; + //;VGM_LOG("TXTP: filename=%s\n", filename); @@ -209,33 +237,37 @@ static int add_filename(txtp_header * txtp, char *filename) { { char *config; - /* position in base extension */ - config = strrchr(filename,'.'); - if (!config) /* needed...? */ + /* find config start (filenames and config can contain multiple dots and #, + * so this may be fooled by certain patterns of . and #) */ + config = strchr(filename, '.'); /* first dot (may be a false positive) */ + if (!config) /* extensionless */ + config = filename; + config = strchr(config,separator); /* next should be config (hopefully right after extension) */ + if (!config) /* no config */ config = filename; range_start = 0; range_end = 1; do { /* get config pointer but remove config from filename */ - config = strchr(config, '#'); + config = strchr(config, separator); if (!config) continue; + //;VGM_LOG("TXTP: config=%s\n", config); config[0] = '\0'; config++; if (config[0] == 'c') { - /* channel mask */ - /* - file.ext#c1,2 = play channels 1,2 */ - int n, ch; + /* channel mask: file.ext#c1,2 = play channels 1,2 and mutes rest */ + int ch; config++; - channel_mask = 0; + cfg.channel_mask = 0; while (sscanf(config, "%d%n", &ch,&n) == 1) { if (ch > 0 && ch <= 32) - channel_mask |= (1 << (ch-1)); + cfg.channel_mask |= (1 << (ch-1)); config += n; if (config[0]== ',' || config[0]== '-') /* "-" for PowerShell, may have problems with "," */ @@ -245,12 +277,11 @@ static int add_filename(txtp_header * txtp, char *filename) { }; } else if (config[0] == 'm') { - /* channel mappings */ - /* - file.ext#m1-2,3-4 = swaps channels 1<>2 and 3<>4 */ - int n, ch_from = 0, ch_to = 0; + /* channel mappings: file.ext#m1-2,3-4 = swaps channels 1<>2 and 3<>4 */ + int ch_from = 0, ch_to = 0; config++; - channel_mappings_on = 1; + cfg.channel_mappings_on = 1; while (config[0] != '\0') { if (sscanf(config, "%d%n", &ch_from, &n) != 1) @@ -270,21 +301,16 @@ static int add_filename(txtp_header * txtp, char *filename) { break; if (ch_from > 0 && ch_from <= 32 && ch_to > 0 && ch_to <= 32) { - channel_mappings[ch_from-1] = ch_to-1; + cfg.channel_mappings[ch_from-1] = ch_to-1; } } } else if (config[0] == 's' || (config[0] >= '0' && config[0] <= '9')) { - /* subsongs */ - /* - file.ext#2 = play subsong 2 */ - /* - file.ext#s3 = play subsong 3 */ - /* - file.ext#2~10 = play subsong range */ - + /* subsongs: file.ext#s2 = play subsong 2, file.ext#2~10 = play subsong range */ int subsong_start = 0, subsong_end = 0; if (config[0]== 's') config++; - if (sscanf(config, "%d~%d", &subsong_start, &subsong_end) == 2) { if (subsong_start > 0 && subsong_end > 0) { range_start = subsong_start-1; @@ -301,9 +327,36 @@ static int add_filename(txtp_header * txtp, char *filename) { config = NULL; /* wrong config, ignore */ } } + else if (config[0] == 'i') { + config++; + cfg.config_ignore_loop = 1; + } + else if (config[0] == 'E') { + config++; + cfg.config_force_loop = 1; + } + else if (config[0] == 'F') { + config++; + cfg.config_ignore_fade = 1; + } + else if (config[0] == 'l') { + config++; + get_double(config, &cfg.config_loop_count); + } + else if (config[0] == 'f') { + config++; + get_double(config, &cfg.config_fade_time); + } + else if (config[0] == 'd') { + config++; + get_double(config, &cfg.config_fade_delay); + } + else if (config[0] == ' ') { + continue; /* likely a comment, find next # */ + } else { - VGM_LOG("TXTP: unknown command\n"); - goto fail; + //;VGM_LOG("TXTP: unknown command '%c'\n", config[0]); + break; /* also possibly a comment too */ } } while (config != NULL); @@ -327,6 +380,8 @@ static int add_filename(txtp_header * txtp, char *filename) { /* add filesnames */ for (i = range_start; i < range_end; i++){ + txtp_entry *current; + /* resize in steps if not enough */ if (txtp->entry_count+1 > txtp->entry_max) { txtp_entry *temp_entry; @@ -338,20 +393,29 @@ static int add_filename(txtp_header * txtp, char *filename) { } /* new entry */ - memset(&txtp->entry[txtp->entry_count],0, sizeof(txtp_entry)); + current = &txtp->entry[txtp->entry_count]; + memset(current,0, sizeof(txtp_entry)); + strcpy(current->filename, filename); - strcpy(txtp->entry[txtp->entry_count].filename, filename); + current->subsong = (i+1); - txtp->entry[txtp->entry_count].channel_mask = channel_mask; + current->channel_mask = cfg.channel_mask; - if (channel_mappings_on) { + if (cfg.channel_mappings_on) { int ch; - txtp->entry[txtp->entry_count].channel_mappings_on = channel_mappings_on; + current->channel_mappings_on = cfg.channel_mappings_on; for (ch = 0; ch < 32; ch++) { - txtp->entry[txtp->entry_count].channel_mappings[ch] = channel_mappings[ch]; + current->channel_mappings[ch] = cfg.channel_mappings[ch]; } } - txtp->entry[txtp->entry_count].subsong = (i+1); + + current->config_loop_count = cfg.config_loop_count; + current->config_fade_time = cfg.config_fade_time; + current->config_fade_delay = cfg.config_fade_delay; + current->config_ignore_loop = cfg.config_ignore_loop; + current->config_force_loop = cfg.config_force_loop; + current->config_ignore_fade = cfg.config_ignore_fade; + txtp->entry_count++; } diff --git a/src/vgmstream.c b/src/vgmstream.c index 2119077f..059a29a0 100644 --- a/src/vgmstream.c +++ b/src/vgmstream.c @@ -897,20 +897,19 @@ void close_vgmstream(VGMSTREAM * vgmstream) { /* calculate samples based on player's config */ int32_t get_vgmstream_play_samples(double looptimes, double fadeseconds, double fadedelayseconds, VGMSTREAM * vgmstream) { if (vgmstream->loop_flag) { - if (fadeseconds < 0) { /* a bit hack-y to avoid signature change */ + if (vgmstream->loop_target == (int)looptimes) { /* set externally, as this function is info-only */ /* Continue playing the file normally after looping, instead of fading. * Most files cut abruply after the loop, but some do have proper endings. * With looptimes = 1 this option should give the same output vs loop disabled */ int loop_count = (int)looptimes; /* no half loops allowed */ - //vgmstream->loop_target = loop_count; /* handled externally, as this is into-only */ return vgmstream->loop_start_sample + (vgmstream->loop_end_sample - vgmstream->loop_start_sample) * loop_count + (vgmstream->num_samples - vgmstream->loop_end_sample); } else { - return (int32_t)(vgmstream->loop_start_sample + return vgmstream->loop_start_sample + (vgmstream->loop_end_sample - vgmstream->loop_start_sample) * looptimes - + (fadedelayseconds + fadeseconds) * vgmstream->sample_rate); + + (fadedelayseconds + fadeseconds) * vgmstream->sample_rate; } } else { @@ -952,6 +951,22 @@ void vgmstream_force_loop(VGMSTREAM* vgmstream, int loop_flag, int loop_start_sa /* segmented layout only works (ATM) with exact/header loop, full loop or no loop */ } +void vgmstream_set_loop_target(VGMSTREAM* vgmstream, int loop_target) { + if (!vgmstream) return; + + vgmstream->loop_target = loop_target; /* loop count must be rounded (int) as otherwise target is meaningless */ + + /* propagate changes to layouts that need them */ + if (vgmstream->layout_type == layout_layered) { + int i; + layered_layout_data *data = vgmstream->layout_data; + for (i = 0; i < data->layer_count; i++) { + vgmstream_set_loop_target(data->layers[i], loop_target); + } + } +} + + /* Decode data into sample buffer */ void render_vgmstream(sample * buffer, int32_t sample_count, VGMSTREAM * vgmstream) { switch (vgmstream->layout_type) { diff --git a/src/vgmstream.h b/src/vgmstream.h index 92701228..8e996e42 100644 --- a/src/vgmstream.h +++ b/src/vgmstream.h @@ -762,34 +762,42 @@ typedef struct { /* main vgmstream info */ typedef struct { /* basics */ - int32_t num_samples; /* the actual number of samples in this stream */ - int32_t sample_rate; /* sample rate in Hz */ - int channels; /* number of channels */ - coding_t coding_type; /* type of encoding */ - layout_t layout_type; /* type of layout for data */ - meta_t meta_type; /* how we know the metadata */ - - /* subsongs */ - int num_streams; /* for multi-stream formats (0=not set/one stream, 1=one stream) */ - int stream_index; /* selected stream (also 1-based) */ - char stream_name[STREAM_NAME_SIZE]; /* name of the current stream (info), if the file stores it and it's filled */ - size_t stream_size; /* info to properly calculate bitrate in case of subsongs */ - /* config */ - int allow_dual_stereo; /* search for dual stereo (file_L.ext + file_R.ext = single stereo file) */ - uint32_t channel_mask; /* to silence crossfading subsongs/layers */ - int channel_mappings_on; /* channel mappings are active */ - int channel_mappings[32]; /* swap channel "i" with "[i]" */ - double config_loops; /* appropriate number of loops (config request for players) */ - int config_nofade; /* continue normally after target loop count (config request for players) */ + int32_t num_samples; /* the actual max number of samples */ + int32_t sample_rate; /* sample rate in Hz */ + int channels; /* number of channels */ + coding_t coding_type; /* type of encoding */ + layout_t layout_type; /* type of layout */ + meta_t meta_type; /* type of metadata */ /* looping */ - int loop_flag; /* is this stream looped? */ - int32_t loop_start_sample; /* first sample of the loop (included in the loop) */ - int32_t loop_end_sample; /* last sample of the loop (not included in the loop) */ + int loop_flag; /* is this stream looped? */ + int32_t loop_start_sample; /* first sample of the loop (included in the loop) */ + int32_t loop_end_sample; /* last sample of the loop (not included in the loop) */ /* layouts/block */ - size_t interleave_block_size; /* interleave, or block/frame size (depending on the codec) */ - size_t interleave_last_block_size; /* smaller interleave for last block */ + size_t interleave_block_size; /* interleave, or block/frame size (depending on the codec) */ + size_t interleave_last_block_size; /* smaller interleave for last block */ + + /* subsongs */ + int num_streams; /* for multi-stream formats (0=not set/one stream, 1=one stream) */ + int stream_index; /* selected subsong (also 1-based) */ + size_t stream_size; /* info to properly calculate bitrate in case of subsongs */ + char stream_name[STREAM_NAME_SIZE]; /* name of the current stream (info), if the file stores it and it's filled */ + + /* config */ + int allow_dual_stereo; /* search for dual stereo (file_L.ext + file_R.ext = single stereo file) */ + uint32_t channel_mask; /* to silence crossfading subsongs/layers */ + int channel_mappings_on; /* channel mappings are active */ + int channel_mappings[32]; /* swap channel "i" with "[i]" */ + /* config requests, players must read and honor these values */ + /* (ideally internally would work as a player, but for now player must do it manually) */ + double config_loop_count; + double config_fade_time; + double config_fade_delay; + int config_ignore_loop; + int config_force_loop; + int config_ignore_fade; + /* channel state */ VGMSTREAMCHANNEL * ch; /* pointer to array of channels */ @@ -814,13 +822,12 @@ typedef struct { /* loop state */ int hit_loop; /* have we seen the loop yet? */ - /* counters for "loop + play end of the stream instead of fading" (not used/needed otherwise) */ - int loop_count; /* number of complete loops (1=looped once) */ - int loop_target; /* max loops before continuing with the stream end */ + int loop_count; /* counter of complete loops (1=looped once) */ + int loop_target; /* max loops before continuing with the stream end (loops forever if not set) */ /* decoder specific */ int codec_endian; /* little/big endian marker; name is left vague but usually means big endian */ - int codec_config; /* flags for codecs or layouts with minor variations; meaning is up to the user */ + int codec_config; /* flags for codecs or layouts with minor variations; meaning is up to the codec */ int32_t ws_output_size; /* WS ADPCM: output bytes for this block */ @@ -1286,13 +1293,17 @@ void describe_vgmstream(VGMSTREAM * vgmstream, char * desc, int length); /* Return the average bitrate in bps of all unique files contained within this stream. */ int get_vgmstream_average_bitrate(VGMSTREAM * vgmstream); -/* List supported formats and return elements in the list, for plugins that need to know. */ +/* List supported formats and return elements in the list, for plugins that need to know. + * The list disables some common formats that may conflict (.wav, .ogg, etc). */ const char ** vgmstream_get_formats(size_t * size); /* Force enable/disable internal looping. Should be done before playing anything, * and not all codecs support arbitrary loop values ATM. */ void vgmstream_force_loop(VGMSTREAM* vgmstream, int loop_flag, int loop_start_sample, int loop_end_sample); +/* Set number of max loops to do, then play up to stream end (for songs with proper endings) */ +void vgmstream_set_loop_target(VGMSTREAM* vgmstream, int loop_target); + /* -------------------------------------------------------------------------*/ /* vgmstream "private" API */ /* -------------------------------------------------------------------------*/