Add TXTP loop anchors to simplify segment loops and multi-loop groups

This commit is contained in:
bnnm 2020-11-01 22:26:52 +01:00
parent 48a32e6631
commit 0df5bccd2e
2 changed files with 132 additions and 35 deletions

View File

@ -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

View File

@ -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};