diff --git a/doc/TXTP.md b/doc/TXTP.md index 1d1cb6d2..3bc714f0 100644 --- a/doc/TXTP.md +++ b/doc/TXTP.md @@ -37,7 +37,7 @@ Some games clumsily loop audio by using multiple full file "segments", so you ca BGM01_BEGIN.VAG BGM01_LOOPED.VAG -# segments must define loops +# segments may define loops loop_start_segment = 2 # 2nd file start loop_end_segment = 2 # optional, default is last mode = segments # optional, default is segments @@ -51,6 +51,7 @@ BGM01_LOOPED.VAG # (only for multiple segments, to repeat a single file use #E) loop_mode = auto ``` +Another way to set looping is using "loop anchors", that are meant to simplify more complex .txtp (explained later). If your loop segment has proper loops you want to keep, you can use: @@ -158,6 +159,7 @@ mode = layers # you could also set: group = L and mode = mixed, same thing ``` +### Group definition `group` can go anywhere in the .txtp, as many times as needed (groups are read and kept in an list that is applied in order at the end). Format is `(position)(type)(count)(repeat)`: - `position`: file start (optional, default is 1 = first, or set `-` for auto from prev N files) - `type`: group as `S`=segments, `L`=layers, or `R`=pseudo-random @@ -190,6 +192,7 @@ group = -S2 #segment prev 2 (will start from pos.1 = bgm1+2, makes group of bgm # may mix groups of auto and manual positions too, but results are harder to predict ``` +### Pseudo-random groups Group `R` is meant to help with games that randomly select a file in a group. You can set with `>N` which file will be selected. This way you can quickly edit the TXTP and change the file (you could just comment files too, this is just for convenience in complex cases and testing). You can also set `>-`, meaning "play all", basically turning `R` into `S`. Files do need to exist and are parsed before being selected, and it can select groups too. ``` bgm1.adx @@ -207,6 +210,30 @@ group = -R3>1 #first file, change to >2 for second group = -R2>2 #select either group >1 or >2 ``` +### Silent files +You can put `?.` in an entry to make a silent (non-existing) file. By default takes channels and sample rate of nearby files, can be combined with regular commands to configure. +``` +intro.adx +?.silence #b 3.0 # 3 seconds of silence +loop.adx +``` + +It also doubles as a quick "silence this file" while keeping the same structure, for complex cases. The `.` can actually be anywhere after `?`, but must appear before commands to function correctly. +``` + layer1a.adx + ?layer1b.adx + group = -L2 + + ?layer2a.adx + layer2b.adx + group = -L2 + +group = -S2 +``` + +Most of the time you can do the same with `#p`/`#P` padding commands or `#@volume 0.0`. This is mainly for complex engines that combine silent entries in twisted ways. You can't silence `group` with `?group` though since they aren't considered "entries". + +### Other considerations Internally, `mode = segment/layers` are treated basically as a (default, at the end) group. You can apply commands to the resulting group (rather than the individual files) too. `commands` would be applied to this final group. ``` mainA_2ch.at3 @@ -233,30 +260,7 @@ mode = segments loop_start_segment = 3 #refers to final group at position 2 loop_mode = keep ``` - - -### Silent files -You can put `?.` in an entry to make a silent (non-existing) file. By default takes channels and sample rate of nearby files, can be combined with regular commands to configure. -``` -intro.adx -?.silence #b 3.0 # 3 seconds of silence -loop.adx -``` - -It also doubles as a quick "silence this file" while keeping the same structure, for complex cases. The `.` can actually be anywhere after `?`, but must appear before commands to function correctly. -``` - layer1a.adx - ?layer1b.adx - group = -L2 - - ?layer2a.adx - layer2b.adx - group = -L2 - -group = -S2 -``` - -Most of the time you can do the same with `#p`/`#P` padding commands or `#@volume 0.0`. This is mainly for complex engines that combine silent entries in twisted ways. You can't silence `group` with `?group` though since they aren't considered "entries". +Also see loop anchors to handle looping in some cases. ## TXTP COMMANDS @@ -569,6 +573,39 @@ This can be applied to individual layers and segments, but normally you want to 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. + + ### Macros **`#@(macro name and parameters)`**: adds a new macro diff --git a/src/meta/txtp.c b/src/meta/txtp.c index bb7d2af7..9a98e881 100644 --- a/src/meta/txtp.c +++ b/src/meta/txtp.c @@ -89,6 +89,9 @@ typedef struct { int32_t loop_start_sample; double loop_end_second; int32_t loop_end_sample; + /* flags */ + int loop_anchor_start; + int loop_anchor_end; int trim_set; double trim_second; @@ -312,6 +315,7 @@ static void update_vgmstream_list(VGMSTREAM* vgmstream, txtp_header* txtp, int p for (i = position + count; i < txtp->vgmstream_count; i++) { //;VGM_LOG("TXTP: copy %i to %i\n", i, i + 1 - count); txtp->vgmstream[i + 1 - count] = txtp->vgmstream[i]; + txtp->entry[i + 1 - count] = txtp->entry[i]; /* memcpy old settings for other groups */ } /* list can only become smaller, no need to alloc/free/etc */ @@ -319,10 +323,38 @@ static void update_vgmstream_list(VGMSTREAM* vgmstream, txtp_header* txtp, int p //;VGM_LOG("TXTP: compact vgmstreams=%i\n", txtp->vgmstream_count); } +static int find_loop_anchors(txtp_header* txtp, int position, int count, int* p_loop_start, int* p_loop_end) { + int loop_start = 0, loop_end = 0; + int i, j; + + //;VGM_LOG("TXTP: find loop anchors from %i to %i\n", position, count); + + for (i = position, j = 0; i < position + count; i++, j++) { + if (txtp->entry[i].loop_anchor_start) { + loop_start = j + 1; /* logic elsewhere also uses +1 */ + } + if (txtp->entry[i].loop_anchor_end) { + loop_end = j + 1; + } + } + + if (loop_start) { + if (!loop_end) + loop_end = count; + *p_loop_start = loop_start; + *p_loop_end = loop_end; + //;VGM_LOG("TXTP: loop anchors %i, %i\n", loop_start, loop_end); + return 1; + } + + return 0; +} + static int make_group_segment(txtp_header* txtp, int is_group, int position, int count) { VGMSTREAM* vgmstream = NULL; segmented_layout_data *data_s = NULL; int i, loop_flag = 0; + int loop_start = 0, loop_end = 0; /* allowed for actual groups (not final "mode"), otherwise skip to optimize */ @@ -336,16 +368,25 @@ static int make_group_segment(txtp_header* txtp, int is_group, int position, int return 1; } - /* loop settings only make sense if this group becomes final vgmstream */ - if (position == 0 && txtp->vgmstream_count == count) { - if (txtp->loop_start_segment && !txtp->loop_end_segment) { - txtp->loop_end_segment = count; + + /* set loops with "anchors" (this allows loop config inside groups, not just in the final group, + * which is sometimes useful when paired with random/selectable groups or loop times) */ + if (find_loop_anchors(txtp, position, count, &loop_start, &loop_end)) { + loop_flag = (loop_start > 0 && loop_start <= count); + } + /* loop segment settings only make sense if this group becomes final vgmstream */ + else if (position == 0 && txtp->vgmstream_count == count) { + loop_start = txtp->loop_start_segment; + loop_end = txtp->loop_end_segment; + + if (loop_start && !loop_end) { + loop_end = count; } else if (txtp->is_loop_auto) { /* auto set to last segment */ - txtp->loop_start_segment = count; - txtp->loop_end_segment = count; + loop_start = count; + loop_end = count; } - loop_flag = (txtp->loop_start_segment > 0 && txtp->loop_start_segment <= count); + loop_flag = (loop_start > 0 && loop_start <= count); } @@ -364,7 +405,7 @@ static int make_group_segment(txtp_header* txtp, int is_group, int position, int goto fail; /* build the layout VGMSTREAM */ - vgmstream = allocate_segmented_vgmstream(data_s,loop_flag, txtp->loop_start_segment - 1, txtp->loop_end_segment - 1); + vgmstream = allocate_segmented_vgmstream(data_s, loop_flag, loop_start - 1, loop_end - 1); if (!vgmstream) goto fail; /* custom meta name if all parts don't match */ @@ -379,13 +420,13 @@ static int make_group_segment(txtp_header* txtp, int is_group, int position, int if (loop_flag && txtp->is_loop_keep) { int32_t current_samples = 0; for (i = 0; i < count; i++) { - if (txtp->loop_start_segment == i+1 /*&& data_s->segments[i]->loop_start_sample*/) { + if (loop_start == i+1 /*&& data_s->segments[i]->loop_start_sample*/) { vgmstream->loop_start_sample = current_samples + data_s->segments[i]->loop_start_sample; } current_samples += data_s->segments[i]->num_samples; - if (txtp->loop_end_segment == i+1 && data_s->segments[i]->loop_end_sample) { + if (loop_end == i+1 && data_s->segments[i]->loop_end_sample) { vgmstream->loop_end_sample = current_samples - data_s->segments[i]->num_samples + data_s->segments[i]->loop_end_sample; } } @@ -568,6 +609,7 @@ static int parse_groups(txtp_header* txtp) { /* group may also have settings (like downmixing) */ apply_settings(txtp->vgmstream[grp->position], &grp->group_settings); + txtp->entry[grp->position] = grp->group_settings; /* memcpy old settings for subgroups */ } /* final tweaks (should be integrated with the above?) */ @@ -1171,6 +1213,14 @@ static void add_settings(txtp_entry* current, txtp_entry* entry, const char* fil current->mixing_count++; } } + + current->loop_anchor_start = entry->loop_anchor_start; + current->loop_anchor_end = entry->loop_anchor_end; +} + +//TODO use +static inline int is_match(const char* str1, const char* str2) { + return strcmp(str1, str2) == 0; } static void parse_params(txtp_entry* entry, char* params) { @@ -1401,6 +1451,15 @@ static void parse_params(txtp_entry* entry, char* params) { //;VGM_LOG("TXTP: trim %i - %f / %i\n", entry->trim_set, entry->trim_second, entry->trim_sample); } + else if (is_match(command,"a") || is_match(command,"@loop")) { + entry->loop_anchor_start = 1; + //;VGM_LOG("TXTP: anchor start set\n"); + } + else if (is_match(command,"A") || is_match(command,"@LOOP")) { + entry->loop_anchor_end = 1; + //;VGM_LOG("TXTP: anchor end set\n"); + } + //todo cleanup /* macros */ else if (strcmp(command,"@volume") == 0) { @@ -1606,6 +1665,7 @@ static void clean_filename(char* filename) { } +//TODO see if entry can be set to &default/&entry[entry_count] to avoid add_settings static int add_entry(txtp_header* txtp, char* filename, int is_default) { int i; txtp_entry entry = {0};