mirror of
https://github.com/vgmstream/vgmstream.git
synced 2024-11-24 23:10:10 +01:00
Add TXTP loop anchors to simplify segment loops and multi-loop groups
This commit is contained in:
parent
48a32e6631
commit
0df5bccd2e
87
doc/TXTP.md
87
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_BEGIN.VAG
|
||||||
BGM01_LOOPED.VAG
|
BGM01_LOOPED.VAG
|
||||||
|
|
||||||
# segments must define loops
|
# segments may define loops
|
||||||
loop_start_segment = 2 # 2nd file start
|
loop_start_segment = 2 # 2nd file start
|
||||||
loop_end_segment = 2 # optional, default is last
|
loop_end_segment = 2 # optional, default is last
|
||||||
mode = segments # optional, default is segments
|
mode = segments # optional, default is segments
|
||||||
@ -51,6 +51,7 @@ BGM01_LOOPED.VAG
|
|||||||
# (only for multiple segments, to repeat a single file use #E)
|
# (only for multiple segments, to repeat a single file use #E)
|
||||||
loop_mode = auto
|
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:
|
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
|
# 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)`:
|
`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)
|
- `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
|
- `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
|
# 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.
|
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
|
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
|
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.
|
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
|
mainA_2ch.at3
|
||||||
@ -233,30 +260,7 @@ mode = segments
|
|||||||
loop_start_segment = 3 #refers to final group at position 2
|
loop_start_segment = 3 #refers to final group at position 2
|
||||||
loop_mode = keep
|
loop_mode = keep
|
||||||
```
|
```
|
||||||
|
Also see loop anchors to handle looping in some cases.
|
||||||
|
|
||||||
### 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".
|
|
||||||
|
|
||||||
|
|
||||||
## TXTP COMMANDS
|
## 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).
|
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
|
### Macros
|
||||||
**`#@(macro name and parameters)`**: adds a new macro
|
**`#@(macro name and parameters)`**: adds a new macro
|
||||||
|
|
||||||
|
@ -89,6 +89,9 @@ typedef struct {
|
|||||||
int32_t loop_start_sample;
|
int32_t loop_start_sample;
|
||||||
double loop_end_second;
|
double loop_end_second;
|
||||||
int32_t loop_end_sample;
|
int32_t loop_end_sample;
|
||||||
|
/* flags */
|
||||||
|
int loop_anchor_start;
|
||||||
|
int loop_anchor_end;
|
||||||
|
|
||||||
int trim_set;
|
int trim_set;
|
||||||
double trim_second;
|
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++) {
|
for (i = position + count; i < txtp->vgmstream_count; i++) {
|
||||||
//;VGM_LOG("TXTP: copy %i to %i\n", i, i + 1 - count);
|
//;VGM_LOG("TXTP: copy %i to %i\n", i, i + 1 - count);
|
||||||
txtp->vgmstream[i + 1 - count] = txtp->vgmstream[i];
|
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 */
|
/* 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);
|
//;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) {
|
static int make_group_segment(txtp_header* txtp, int is_group, int position, int count) {
|
||||||
VGMSTREAM* vgmstream = NULL;
|
VGMSTREAM* vgmstream = NULL;
|
||||||
segmented_layout_data *data_s = NULL;
|
segmented_layout_data *data_s = NULL;
|
||||||
int i, loop_flag = 0;
|
int i, loop_flag = 0;
|
||||||
|
int loop_start = 0, loop_end = 0;
|
||||||
|
|
||||||
|
|
||||||
/* allowed for actual groups (not final "mode"), otherwise skip to optimize */
|
/* 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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* loop settings only make sense if this group becomes final vgmstream */
|
|
||||||
if (position == 0 && txtp->vgmstream_count == count) {
|
/* set loops with "anchors" (this allows loop config inside groups, not just in the final group,
|
||||||
if (txtp->loop_start_segment && !txtp->loop_end_segment) {
|
* which is sometimes useful when paired with random/selectable groups or loop times) */
|
||||||
txtp->loop_end_segment = count;
|
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 */
|
else if (txtp->is_loop_auto) { /* auto set to last segment */
|
||||||
txtp->loop_start_segment = count;
|
loop_start = count;
|
||||||
txtp->loop_end_segment = 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;
|
goto fail;
|
||||||
|
|
||||||
/* build the layout VGMSTREAM */
|
/* 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;
|
if (!vgmstream) goto fail;
|
||||||
|
|
||||||
/* custom meta name if all parts don't match */
|
/* 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) {
|
if (loop_flag && txtp->is_loop_keep) {
|
||||||
int32_t current_samples = 0;
|
int32_t current_samples = 0;
|
||||||
for (i = 0; i < count; i++) {
|
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;
|
vgmstream->loop_start_sample = current_samples + data_s->segments[i]->loop_start_sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
current_samples += data_s->segments[i]->num_samples;
|
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;
|
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) */
|
/* group may also have settings (like downmixing) */
|
||||||
apply_settings(txtp->vgmstream[grp->position], &grp->group_settings);
|
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?) */
|
/* 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->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) {
|
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);
|
//;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
|
//todo cleanup
|
||||||
/* macros */
|
/* macros */
|
||||||
else if (strcmp(command,"@volume") == 0) {
|
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) {
|
static int add_entry(txtp_header* txtp, char* filename, int is_default) {
|
||||||
int i;
|
int i;
|
||||||
txtp_entry entry = {0};
|
txtp_entry entry = {0};
|
||||||
|
Loading…
Reference in New Issue
Block a user