mirror of
https://github.com/vgmstream/vgmstream.git
synced 2025-01-21 08:43:40 +01:00
commit
97801db572
66
doc/TXTP.md
66
doc/TXTP.md
@ -99,7 +99,7 @@ Note that the number of channels is the sum of all layers so three 2ch layers pl
|
||||
|
||||
|
||||
### Mixed groups
|
||||
You can set "groups" to 'fold' various files into one, as layers or segments, to allow complex cases. This is an advanced settings for complex cases, so read this after understanding other features first.
|
||||
You can set "groups" to 'fold' various files into one, as layers or segments, to allow complex cases. This is an advanced setting for complex cases, so read this after understanding other features first.
|
||||
```
|
||||
# commands to make two 6ch segments with layered intro + layered loop:
|
||||
|
||||
@ -147,10 +147,11 @@ mode = layers
|
||||
```
|
||||
|
||||
`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)
|
||||
- `type`: group as S=segments or L=layers
|
||||
- `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
|
||||
- `count`: number of files in group (optional, default is all)
|
||||
- `repeat`: R=repeat group of `count` files until end (optional, default is no repeat)
|
||||
- `>file`: select file in pseudo-random groups (ignored for others)
|
||||
|
||||
Examples:
|
||||
- `L`: take all files as layers (equivalent to `mode = layers`)
|
||||
@ -161,8 +162,40 @@ Examples:
|
||||
- `1L1`: layer of one file (same)
|
||||
- `9999L`: absurd values are ignored
|
||||
|
||||
`position` may be `-` = automatic, meaning "start from position in previous `count` before current". If `repeat` is set it's ignored though (assumes first).
|
||||
```
|
||||
bgm1.adx
|
||||
bgm2.adx
|
||||
group = -L2 #layer prev 2 (will start from pos.1 = bgm1, makes group of bgm1+2 = pos.1)
|
||||
|
||||
Internally, `mode = segment/layers` are treated basically the same 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.
|
||||
bgm3.adx
|
||||
bgm4.adx
|
||||
group = -L2 #layer prev 2 (will start from pos.2 = bgm3, makes group of bgm3+4 = pos.2)
|
||||
|
||||
group = -S2 #segment prev 2 (will start from pos.1 = bgm1+2, makes group of bgm1+2 + bgm3+4)
|
||||
|
||||
# uses "previous" because "next files" often creates ambiguous cases
|
||||
# may mix groups of auto and manual positions too, but results are harder to predict
|
||||
```
|
||||
|
||||
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/should just comment files too, this is just for convenience in complex cases and testing). Files do need to exist and are parsed before being selected, and it can select groups too.
|
||||
```
|
||||
bgm1.adx
|
||||
bgm2.adx
|
||||
bgm3.adx
|
||||
group = -R3>1 #first file, change to >2 for second
|
||||
```
|
||||
```
|
||||
bgm1a.adx
|
||||
bgm1b.adx
|
||||
group = -S2
|
||||
bgm2a.adx
|
||||
bgm2b.adx
|
||||
group = -S2
|
||||
group = -R2>2 #select either group >1 or >2
|
||||
```
|
||||
|
||||
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
|
||||
mainB_2ch.at3
|
||||
@ -369,6 +402,18 @@ Similarly other games don't use loop points, but rather repeat/loops the song in
|
||||
bgm01.vag #t3:20
|
||||
```
|
||||
|
||||
You can combine file trims and begin removes (see above) for weirder combos:
|
||||
|
||||
**Cave Story 3D (3DS)**
|
||||
```
|
||||
# loop has intro2 (bridge) + body, so we want intro1 + body + intro2 + body ...
|
||||
wanpaku_ending_intro.lwav # intro1
|
||||
wanpaku_ending_loop.lwav #r 141048 # remove intro2 and leave body (same samples as intro1)
|
||||
wanpaku_ending_loop.lwav #t 141048 # plays up to intro2
|
||||
|
||||
loop_start_segment = 2 # loops back to body
|
||||
```
|
||||
|
||||
If you need to remove very few samples (like 1) to get smooth transitions it may be a bug in vgmstream, consider reporting.
|
||||
|
||||
|
||||
@ -396,9 +441,9 @@ Note that a few codecs may not work with arbitrary loop values since they weren'
|
||||
|
||||
|
||||
### Force sample rate
|
||||
**`#h(sample rate)`**: changes sample rate to selected value (within some limits).
|
||||
**`#h(sample rate)`**: changes sample rate to selected value, changing play speed.
|
||||
|
||||
Needed for a few games set a sample rate value in the header but actually play with other (applying some of pitch or just forcing it), resulting in wrong play speed if not changed.
|
||||
Needed for a few games set a sample rate value in the header but actually play with other (applying some of pitch or just forcing it), resulting in wrong play speed if not changed. Higher rates will sound faster, and lower rates slower.
|
||||
|
||||
**Super Paper Mario (Wii)**
|
||||
```
|
||||
@ -485,7 +530,7 @@ Mixing must be supported by the plugin, otherwise it's ignored (there is a negli
|
||||
Manually setting values gets old, so TXTP supports a bunch of simple macros. They automate some of the above commands (analyzing the file), and may be combined, so order still matters.
|
||||
- `volume N (channels)`: sets volume V to selected channels
|
||||
- `track (channels)`: makes a file of selected channels
|
||||
- `layer-v N (channels)`: mixes selected channels to N channels with default volume (for layered vocals)
|
||||
- `layer-v N (channels)`: mixes selected channels to N channels with default volume (for layered vocals). If N is 0 (or ommited), automatically sets highest channel count among all layers.
|
||||
- `layer-b N (channels)`: same, but adjusts volume depending on layers (for layered bgm)
|
||||
- `layer-e N (channels)`: same, but adjusts volume equally for all layers (for generic downmixing)
|
||||
- `remix N (channels)`: same, but mixes selected channels to N channels properly adjusting volume (for layered bgm)
|
||||
@ -524,6 +569,13 @@ mode = layers
|
||||
commands = #@layer-v 2
|
||||
```
|
||||
```
|
||||
# downmix 4ch bgm + 4ch vocals to 4ch (highest among all layers), automatically
|
||||
mgr-main-4ch.wem
|
||||
mgr-vocal-4ch.wem
|
||||
mode = layers
|
||||
commands = #@layer-v
|
||||
```
|
||||
```
|
||||
# can be combined with normal mixes too for creative results
|
||||
# (add channel clone of ch1, then 50% of range)
|
||||
song#m4u,4+1#@volume 0.5 2~4
|
||||
|
121
src/formats.c
121
src/formats.c
@ -1308,7 +1308,7 @@ static const meta_info meta_info_list[] = {
|
||||
{meta_ADP_KONAMI, "Konami ADP header"},
|
||||
};
|
||||
|
||||
void get_vgmstream_coding_description(VGMSTREAM *vgmstream, char *out, size_t out_size) {
|
||||
void get_vgmstream_coding_description(VGMSTREAM* vgmstream, char* out, size_t out_size) {
|
||||
int i, list_length;
|
||||
const char *description;
|
||||
|
||||
@ -1319,7 +1319,8 @@ void get_vgmstream_coding_description(VGMSTREAM *vgmstream, char *out, size_t ou
|
||||
layered_layout_data* layout_data = vgmstream->layout_data;
|
||||
get_vgmstream_coding_description(layout_data->layers[0], out, out_size);
|
||||
return;
|
||||
} else if (vgmstream->layout_type == layout_segmented) {
|
||||
}
|
||||
else if (vgmstream->layout_type == layout_segmented) {
|
||||
segmented_layout_data* layout_data = vgmstream->layout_data;
|
||||
get_vgmstream_coding_description(layout_data->segments[0], out, out_size);
|
||||
return;
|
||||
@ -1348,7 +1349,8 @@ void get_vgmstream_coding_description(VGMSTREAM *vgmstream, char *out, size_t ou
|
||||
|
||||
strncpy(out, description, out_size);
|
||||
}
|
||||
const char * get_vgmstream_layout_name(layout_t layout_type) {
|
||||
|
||||
static const char* get_layout_name(layout_t layout_type) {
|
||||
int i, list_length;
|
||||
|
||||
list_length = sizeof(layout_info_list) / sizeof(layout_info);
|
||||
@ -1359,40 +1361,103 @@ const char * get_vgmstream_layout_name(layout_t layout_type) {
|
||||
|
||||
return NULL;
|
||||
}
|
||||
void get_vgmstream_layout_description(VGMSTREAM *vgmstream, char *out, size_t out_size) {
|
||||
char temp[256];
|
||||
VGMSTREAM* vgmstreamsub = NULL;
|
||||
const char* description;
|
||||
|
||||
description = get_vgmstream_layout_name(vgmstream->layout_type);
|
||||
static int has_sublayouts(VGMSTREAM** vgmstreams, int count) {
|
||||
int i;
|
||||
for (i = 0; i < count; i++) {
|
||||
if (vgmstreams[i]->layout_type == layout_segmented || vgmstreams[i]->layout_type == layout_layered)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Makes a mixed description, considering a segments/layers can contain segments/layers infinitely, like:
|
||||
*
|
||||
* "(L3[S2L2]S3)" "(S3[L2[S2S2]])"
|
||||
* L3 S3
|
||||
* S2 L2
|
||||
* file S2
|
||||
* file file
|
||||
* file file
|
||||
* L2 file
|
||||
* file file
|
||||
* file file
|
||||
*
|
||||
* ("mixed" is added externally)
|
||||
*/
|
||||
static int get_layout_mixed_description(VGMSTREAM* vgmstream, char* dst, int dst_size) {
|
||||
int i, count, done = 0;
|
||||
VGMSTREAM** vgmstreams = NULL;
|
||||
|
||||
if (vgmstream->layout_type == layout_layered) {
|
||||
layered_layout_data* data = vgmstream->layout_data;
|
||||
vgmstreams = data->layers;
|
||||
count = data->layer_count;
|
||||
done = snprintf(dst, dst_size, "L%i", count);
|
||||
}
|
||||
else if (vgmstream->layout_type == layout_segmented) {
|
||||
segmented_layout_data* data = vgmstream->layout_data;
|
||||
vgmstreams = data->segments;
|
||||
count = data->segment_count;
|
||||
done = snprintf(dst, dst_size, "S%i", count);
|
||||
}
|
||||
|
||||
if (!vgmstreams || done == 0 || done >= dst_size)
|
||||
return 0;
|
||||
|
||||
if (!has_sublayouts(vgmstreams, count))
|
||||
return done;
|
||||
|
||||
if (done + 1 < dst_size) {
|
||||
dst[done++] = '[';
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
done += get_layout_mixed_description(vgmstreams[i], dst + done, dst_size - done);
|
||||
}
|
||||
|
||||
if (done + 1 < dst_size) {
|
||||
dst[done++] = ']';
|
||||
}
|
||||
|
||||
return done;
|
||||
}
|
||||
|
||||
void get_vgmstream_layout_description(VGMSTREAM* vgmstream, char* out, size_t out_size) {
|
||||
const char* description;
|
||||
int mixed = 0;
|
||||
|
||||
description = get_layout_name(vgmstream->layout_type);
|
||||
if (!description) description = "INCONCEIVABLE";
|
||||
|
||||
if (vgmstream->layout_type == layout_layered) {
|
||||
vgmstreamsub = ((layered_layout_data*)vgmstream->layout_data)->layers[0];
|
||||
snprintf(temp, sizeof(temp), "%s (%i layers)", description, ((layered_layout_data*)vgmstream->layout_data)->layer_count);
|
||||
} else if (vgmstream->layout_type == layout_segmented) {
|
||||
snprintf(temp, sizeof(temp), "%s (%i segments)", description, ((segmented_layout_data*)vgmstream->layout_data)->segment_count);
|
||||
vgmstreamsub = ((segmented_layout_data*)vgmstream->layout_data)->segments[0];
|
||||
} else {
|
||||
snprintf(temp, sizeof(temp), "%s", description);
|
||||
layered_layout_data* data = vgmstream->layout_data;
|
||||
mixed = has_sublayouts(data->layers, data->layer_count);
|
||||
if (!mixed)
|
||||
snprintf(out, out_size, "%s (%i layers)", description, data->layer_count);
|
||||
}
|
||||
else if (vgmstream->layout_type == layout_segmented) {
|
||||
segmented_layout_data* data = vgmstream->layout_data;
|
||||
mixed = has_sublayouts(data->segments, data->segment_count);
|
||||
if (!mixed)
|
||||
snprintf(out, out_size, "%s (%i segments)", description, data->segment_count);
|
||||
}
|
||||
else {
|
||||
snprintf(out, out_size, "%s", description);
|
||||
}
|
||||
strncpy(out, temp, out_size);
|
||||
|
||||
/* layouts can contain layouts infinitely let's leave it at one level deep (most common) */
|
||||
/* TODO: improve this somehow */
|
||||
if (vgmstreamsub && vgmstreamsub->layout_type == layout_layered) {
|
||||
description = get_vgmstream_layout_name(vgmstreamsub->layout_type);
|
||||
snprintf(temp, sizeof(temp), " + %s (%i layers)", description, ((layered_layout_data*)vgmstreamsub->layout_data)->layer_count);
|
||||
concatn(out_size, out, temp);
|
||||
} else if (vgmstreamsub && vgmstreamsub->layout_type == layout_segmented) {
|
||||
description = get_vgmstream_layout_name(vgmstreamsub->layout_type);
|
||||
snprintf(temp, sizeof(temp), " + %s (%i segments)", description, ((segmented_layout_data*)vgmstream->layout_data)->segment_count);
|
||||
concatn(out_size, out, temp);
|
||||
if (mixed) {
|
||||
char tmp[256] = {0};
|
||||
|
||||
get_layout_mixed_description(vgmstream, tmp, sizeof(tmp) - 1);
|
||||
snprintf(out, out_size, "mixed (%s)", tmp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
void get_vgmstream_meta_description(VGMSTREAM *vgmstream, char *out, size_t out_size) {
|
||||
|
||||
void get_vgmstream_meta_description(VGMSTREAM* vgmstream, char* out, size_t out_size) {
|
||||
int i, list_length;
|
||||
const char *description;
|
||||
const char* description;
|
||||
|
||||
description = "THEY SHOULD HAVE SENT A POET";
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
#define TXTP_MIXING_MAX 512
|
||||
#define TXTP_GROUP_MODE_SEGMENTED 'S'
|
||||
#define TXTP_GROUP_MODE_LAYERED 'L'
|
||||
#define TXTP_GROUP_MODE_RANDOM 'R'
|
||||
#define TXTP_GROUP_REPEAT 'R'
|
||||
#define TXTP_POSITION_LOOPS 'L'
|
||||
|
||||
@ -99,6 +100,7 @@ typedef struct {
|
||||
char type;
|
||||
int count;
|
||||
char repeat;
|
||||
int selected;
|
||||
|
||||
txtp_entry group_settings;
|
||||
|
||||
@ -112,6 +114,7 @@ typedef struct {
|
||||
txtp_group* group;
|
||||
size_t group_count;
|
||||
size_t group_max;
|
||||
int group_pos; /* entry counter for groups */
|
||||
|
||||
VGMSTREAM** vgmstream;
|
||||
size_t vgmstream_count;
|
||||
@ -401,6 +404,50 @@ fail:
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int make_group_random(txtp_header* txtp, int position, int count, int selected) {
|
||||
VGMSTREAM* vgmstream = NULL;
|
||||
int i;
|
||||
|
||||
if (count == 1) { /* nothing to do */
|
||||
//;VGM_LOG("TXTP: ignored random of 1\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (position + count > txtp->vgmstream_count || position < 0 || count < 0) {
|
||||
VGM_LOG("TXTP: ignored random position=%i, count=%i, entries=%i\n", position, count, txtp->vgmstream_count);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/* 0=actually random for fun and testing, but undocumented since random music is kinda weird, may change anytime
|
||||
* (plus foobar caches song duration so it can get strange if randoms are too different) */
|
||||
if (selected < 0) {
|
||||
static int random_seed = 0;
|
||||
srand((unsigned)txtp + random_seed++); /* whatevs */
|
||||
selected = (rand() % count); /* 0..count-1 */
|
||||
//;VGM_LOG("TXTP: autoselected random %i\n", selected);
|
||||
}
|
||||
|
||||
if (selected < 0 || selected >= count) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* get selected and remove non-selected */
|
||||
vgmstream = txtp->vgmstream[position + selected];
|
||||
txtp->vgmstream[position + selected] = NULL;
|
||||
for (i = 0; i < count; i++) {
|
||||
close_vgmstream(txtp->vgmstream[i + position]);
|
||||
}
|
||||
|
||||
/* set new vgmstream and reorder positions */
|
||||
update_vgmstream_list(vgmstream, txtp, position, count);
|
||||
|
||||
return 1;
|
||||
fail:
|
||||
close_vgmstream(vgmstream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_groups(txtp_header* txtp) {
|
||||
int i;
|
||||
|
||||
@ -444,6 +491,10 @@ static int parse_groups(txtp_header* txtp) {
|
||||
if (!make_group_segment(txtp, pos, grp->count))
|
||||
goto fail;
|
||||
break;
|
||||
case TXTP_GROUP_MODE_RANDOM:
|
||||
if (!make_group_random(txtp, pos, grp->count, grp->selected))
|
||||
goto fail;
|
||||
break;
|
||||
default:
|
||||
goto fail;
|
||||
}
|
||||
@ -1277,10 +1328,11 @@ static void parse_params(txtp_entry* entry, char* params) {
|
||||
|
||||
nm = get_int(params, &mix.max);
|
||||
params += nm;
|
||||
if (nm == 0) continue;
|
||||
|
||||
nm = get_mask(params, &mix.mask);
|
||||
params += nm;
|
||||
if (nm > 0) { /* max is optional (auto-detects and uses max channels) */
|
||||
nm = get_mask(params, &mix.mask);
|
||||
params += nm;
|
||||
}
|
||||
|
||||
mix.mode = command[7]; /* pass letter */
|
||||
add_mixing(entry, &mix, MACRO_LAYER);
|
||||
@ -1329,14 +1381,21 @@ static void parse_params(txtp_entry* entry, char* params) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
static int add_group(txtp_header* txtp, char* line) {
|
||||
int n, m;
|
||||
txtp_group cfg = {0};
|
||||
int auto_pos = 0;
|
||||
char c;
|
||||
|
||||
/* parse group: (position)(type)(count)(repeat) #(commands) */
|
||||
//;VGM_LOG("TXTP: parse group '%s'\n", line);
|
||||
|
||||
m = sscanf(line, " %c%n", &c, &n);
|
||||
if (m == 1 && c == '-') {
|
||||
auto_pos = 1;
|
||||
line += n;
|
||||
}
|
||||
|
||||
m = sscanf(line, " %d%n", &cfg.position, &n);
|
||||
if (m == 1) {
|
||||
cfg.position--; /* externally 1=first but internally 0=first */
|
||||
@ -1355,13 +1414,41 @@ static int add_group(txtp_header* txtp, char* line) {
|
||||
|
||||
m = sscanf(line, " %c%n", &cfg.repeat, &n);
|
||||
if (m == 1 && cfg.repeat == TXTP_GROUP_REPEAT) {
|
||||
auto_pos = 0;
|
||||
line += n;
|
||||
}
|
||||
|
||||
m = sscanf(line, " >%d%n", &cfg.selected, &n);
|
||||
if (m == 1) {
|
||||
cfg.selected--; /* externally 1=first but internally 0=first */
|
||||
line += n;
|
||||
}
|
||||
|
||||
parse_params(&cfg.group_settings, line);
|
||||
|
||||
//;VGM_LOG("TXTP: parsed group %i%c%i%c\n",cfg.position+1,cfg.type,cfg.count,cfg.repeat);
|
||||
/* Groups can use "auto" position of last N files, so we need a counter that changes like this:
|
||||
* #layer of 2 (pos = 0)
|
||||
* #sequence of 2
|
||||
* bgm pos +1 > 1
|
||||
* bgm pos +1 > 2
|
||||
* group = -S2 pos -2 +1 > 1 (group is at 1 now since it "collapses" wems but becomes a position)
|
||||
* #sequence of 3
|
||||
* bgm pos +1 > 2
|
||||
* bgm pos +1 > 3
|
||||
* #sequence of 2
|
||||
* bgm pos +1 > 4
|
||||
* bgm pos +1 > 5
|
||||
* group = -S2 pos -2 +1 > 4 (groups is at 4 now since are uncollapsed wems at 2/3)
|
||||
* group = -S3 pos -3 +1 > 2
|
||||
* group = -L2 pos -2 +1 > 1
|
||||
*/
|
||||
txtp->group_pos++;
|
||||
txtp->group_pos -= cfg.count;
|
||||
if (auto_pos) {
|
||||
cfg.position = txtp->group_pos - 1; /* internally 1 = first */
|
||||
}
|
||||
|
||||
//;VGM_LOG("TXTP: parsed group %i%c%i%c, auto=%i\n",cfg.position+1,cfg.type,cfg.count,cfg.repeat, auto_pos);
|
||||
|
||||
/* add final group */
|
||||
{
|
||||
@ -1468,6 +1555,7 @@ static int add_entry(txtp_header* txtp, char* filename, int is_default) {
|
||||
add_settings(current, &entry, filename);
|
||||
|
||||
txtp->entry_count++;
|
||||
txtp->group_pos++;
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
28
src/mixing.c
28
src/mixing.c
@ -717,12 +717,40 @@ void mixing_macro_track(VGMSTREAM* vgmstream, uint32_t mask) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* get highest channel count */
|
||||
static int get_layered_max_channels(VGMSTREAM* vgmstream) {
|
||||
int i, max;
|
||||
layered_layout_data* data;
|
||||
|
||||
if (vgmstream->layout_type != layout_layered)
|
||||
return 0;
|
||||
|
||||
data = vgmstream->layout_data;
|
||||
|
||||
max = 0;
|
||||
for (i = 0; i < data->layer_count; i++) {
|
||||
int output_channels = 0;
|
||||
|
||||
mixing_info(data->layers[i], NULL, &output_channels);
|
||||
|
||||
if (max < output_channels)
|
||||
max = output_channels;
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
void mixing_macro_layer(VGMSTREAM* vgmstream, int max, uint32_t mask, char mode) {
|
||||
mixing_data *data = vgmstream->mixing_data;
|
||||
int current, ch, output_channels, selected_channels;
|
||||
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
if (max == 0) /* auto calculate */
|
||||
max = get_layered_max_channels(vgmstream);
|
||||
|
||||
if (max <= 0 || data->output_channels <= max)
|
||||
return;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user