Merge pull request #389 from bnnm/txtp-groups-aif

txtp groups aif
This commit is contained in:
Christopher Snowhill 2019-04-09 03:43:47 -07:00 committed by GitHub
commit c585f3f502
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 800 additions and 426 deletions

View File

@ -131,7 +131,8 @@ like foobar or Winamp don't react well to that, they may be renamed to make
them playable through vgmstream.
- .aac to .laac (tri-Ace games)
- .ac3 to .lac3 (standard AC3)
- .aif to .aiffl or .aifcl (standard Mac AIF)
- .aif to .laif or .aiffl or .aifcl (standard Mac AIF, Asobo AIF, Ogg)
- .aiff/aifc to .aiffl/aifcl (standard Mac AIF)
- .asf to .lasf (EA games, Argonaut ASF)
- .flac to .lflac (standard FLAC)
- .mp2 to .lmp2 (standard MP2)

View File

@ -79,9 +79,9 @@ def find_files(dir, pattern, recursive):
def make_cmd(cfg, fname_in, fname_out, target_subsong):
if (cfg.test_dupes):
cmd = "{} -s {} -i -o {} {}".format(cfg.cli, target_subsong, fname_out, fname_in)
cmd = "{} -s {} -i -o \"{}\" \"{}\"".format(cfg.cli, target_subsong, fname_out, fname_in)
else:
cmd = "{} -s {} -m -i -o {} {}".format(cfg.cli, target_subsong, fname_out, fname_in)
cmd = "{} -s {} -m -i -o \"{}\" \"{}\"".format(cfg.cli, target_subsong, fname_out, fname_in)
return cmd
class LogHelper(object):

View File

@ -18,6 +18,9 @@ You can set commands to alter how files play (described later). Having a single
```
# set "subsong" command for single file inside subdir
sounds/file#12
# will be ignored as none make sense here and is treated as "single" mode
#mode = layers/segments/mixed
```
@ -33,6 +36,7 @@ BGM01_LOOPED.VAG
# segments must define loops
loop_start_segment = 2 # 2nd file start
loop_end_segment = 2 # optional, default is last
mode = segments # optional, default is segments
```
If your loop segment has proper loops you want to keep, you can use:
@ -77,6 +81,87 @@ mode = layers
```
Note that the number of channels is the sum of all layers, so three 2ch layers play as a 6ch file. If all layers share loop points they are automatically kept.
### Mixed groups
You can set "groups" to 'fold' various files into one, as layers or segments, to allow complex cases:
```
# commands to make two 6ch segments with layered intro + layered loop:
# - set introA+B+C as layer (this group becomes position 1, and loopA_2ch position 2)
introA_2ch.at3 #position 1
introB_2ch.at3
introC_2ch.at3
group = 1L3
# - set loopA+B+C as layer (this group becomes position 2)
loopA_2ch.at3 #position 4
loopB_2ch.at3
loopC_2ch.at3
group = 2L3
# - play both as segments (this step is optional if using mode = segments)
group = S2
# - set loop start loopA+B+C (new position 2, not original position 4)
loop_start_segment = 2
# optional, to avoid "segments" default (for debugging)
mode = mixed
```
From TXTP's perspective, it starts with N separate files and every command joins some files that are treated as a single new file, so positions are reassigned. End result will be a single "file" that may contain groups within groups. It's pretty flexible so you can express similar things in various ways:
```
# commands to make a 6ch with segmented intro + loop:
introA_2ch.at3
mainA_2ch.at3
introB_2ch.at3
mainB_2ch.at3
introC_2ch.at3
mainC_2ch.at3
# - group intro/main pairs as segments, starting from 1 and repeating for A/B/C
group = S2R
# - play all as layer (can't set loop_start_segment in this case)
mode = layers
# you could also set: group = L and mode = mixed, same thing
```
`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
- `count`: number of files in group (optional, default is all)
- `repeat`: R=repeat group of `count` files until end (optional, default is no repeat)
Examples:
- `L`: take all files as layers (equivalent to `mode = layers`)
- `S`: take all files as segments (equivalent to `mode = segments`)
- `3L2`: layer 2 files starting from file 3
- `2L3R`: group every 3 files from position 2 as layers
- `1S1`: segment of one file (useless thus ignored)
- `1L1`: layer of one file (same)
- `9999L`: absurd values are ignored
Segments and layer settings and rules still apply, so you can't make segments of files with different total channels. To do it you can use commands to "downmix" the group, as well as giving it some config (explained later):
```
# this doesn't need to be grouped
intro_2ch.at3
# this is grouped into a single 4ch file, then downmixed to stereo
mainA_2ch.at3
mainB_2ch.at3
group = 2L2 #@layer-v 2
# finally resulting layers are played as segments (2ch, 2ch)
# (could set a group = S and ommit S here, too)
mode = segments
# if the last group joins all as segments you can use loop_start
loop_start_segment = 3 #refers to final group at position 2
loop_mode = keep
```
## TXTP COMMANDS
You can set file commands by adding multiple `#(command)` after the name. `# (anything)` is considered a comment and ignored, as well as any command not understood.
@ -91,9 +176,7 @@ bgm.sxd2#12
#bgm.sxd2#s12 # "sN" is 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
# single files loop normally by default (see below to force looping)
```
### Play segmented subsong ranges as one
@ -554,6 +637,19 @@ segment2.hca#m0}1:00+10.0
# it would work ok it they were layers, but still, better to use commands with the resulting file
```
Combine with groups or extra complex cases:
```
BGM_SUMMON_0001_02-Intro.hca # 2ch file
BGM_SUMMON_0001_02-Intro2.hca # 2ch file
BGM_SUMMON_0001_02.hca
BGM_SUMMON_0001_02-Vocal.hca
group = 3L2 #@layer-v 2 # layer Main+Vocal as 4ch then downmix to 2ch
loop_start_segment = 3 #refers to new group at position 3
loop_mode = keep
```
Note how order subtly affects end results:
```
# after silencing channel 1 mixing is meaningless

View File

@ -43,6 +43,7 @@ static const char* extension_list[] = {
"ahv",
"ai",
//"aif", //common
"aif-Loop",
"aifc", //common?
"aifcl", //fake extension for .aif???
//"aiff", //common
@ -212,6 +213,9 @@ static const char* extension_list[] = {
"l",
"laac", //fake extension for .aac (tri-Ace)
"laif", //fake extension for .aif (various)
"laiff", //fake extension for .aiff
"laifc", //fake extension for .aifc
"lac3", //fake extension for .ac3, FFmpeg/not parsed
"lasf", //fake extension for .asf (various)
"leg",

View File

@ -25,7 +25,8 @@ void render_vgmstream_flat(sample_t * buffer, int32_t sample_count, VGMSTREAM *
samples_to_do = sample_count - samples_written;
if (samples_to_do == 0) {
VGM_LOG("layout_flat: wrong samples_to_do found\n");
VGM_LOG("layout_flat: wrong samples_to_do 0 found\n"); /* could happen when calling render at EOF? */
//VGM_LOG("layout_flat: tb=%i sib=%i, spf=%i\n", samples_this_block, vgmstream->samples_into_block, samples_per_frame);
memset(buffer + samples_written*vgmstream->channels, 0, (sample_count - samples_written) * vgmstream->channels * sizeof(sample_t));
break;
}

View File

@ -87,11 +87,15 @@ int setup_layout_layered(layered_layout_data* data) {
for (i = 0; i < data->layer_count; i++) {
int layer_input_channels, layer_output_channels;
if (!data->layers[i])
if (data->layers[i] == NULL) {
VGM_LOG("layered: no vgmstream in %i\n", i);
goto fail;
}
if (data->layers[i]->num_samples <= 0)
if (data->layers[i]->num_samples <= 0) {
VGM_LOG("layered: no samples in %i\n", i);
goto fail;
}
/* different layers may have different input/output channels */
mixing_info(data->layers[i], &layer_input_channels, &layer_output_channels);
@ -103,12 +107,12 @@ int setup_layout_layered(layered_layout_data* data) {
if (i > 0) {
/* a bit weird, but no matter */
if (data->layers[i]->sample_rate != data->layers[i-1]->sample_rate) {
VGM_LOG("layered layout: layer %i has different sample rate\n", i);
VGM_LOG("layered: layer %i has different sample rate\n", i);
}
/* also weird */
if (data->layers[i]->coding_type != data->layers[i-1]->coding_type) {
VGM_LOG("layered layout: layer %i has different coding type\n", i);
VGM_LOG("layered: layer %i has different coding type\n", i);
}
}

View File

@ -135,15 +135,21 @@ int setup_layout_segmented(segmented_layout_data* data) {
for (i = 0; i < data->segment_count; i++) {
int segment_input_channels, segment_output_channels;
if (!data->segments[i])
if (data->segments[i] == NULL) {
VGM_LOG("segmented: no vgmstream in segment %i\n", i);
goto fail;
}
if (data->segments[i]->num_samples <= 0)
if (data->segments[i]->num_samples <= 0) {
VGM_LOG("segmented: no samples in segment %i\n", i);
goto fail;
}
/* disable so that looping is controlled by render_vgmstream_segmented */
if (data->segments[i]->loop_flag != 0) {
VGM_LOG("segmented layout: segment %i is looped\n", i);
VGM_LOG("segmented: segment %i is looped\n", i);
data->segments[i]->loop_flag = 0;
}
@ -159,12 +165,14 @@ int setup_layout_segmented(segmented_layout_data* data) {
int prev_output_channels;
mixing_info(data->segments[i-1], NULL, &prev_output_channels);
if (segment_output_channels != prev_output_channels)
if (segment_output_channels != prev_output_channels) {
VGM_LOG("segmented: segment %i has wrong channels %i vs prev channels %i\n", i, segment_output_channels, prev_output_channels);
goto fail;
}
/* a bit weird, but no matter */
if (data->segments[i]->sample_rate != data->segments[i-1]->sample_rate) {
VGM_LOG("segmented layout: segment %i has different sample rate\n", i);
VGM_LOG("segmented: segment %i has different sample rate\n", i);
}
/* perfectly acceptable */

View File

@ -10,8 +10,8 @@ VGMSTREAM * init_vgmstream_aif_asobo(STREAMFILE *streamFile) {
/* checks */
/* aif: standard, aiffl: for plugins? */
if ( !check_extensions(streamFile,"aif,aiffl") )
/* aif: standard, .laif/aiffl: for plugins */
if ( !check_extensions(streamFile,"aif,laif,aiffl") )
goto fail;
if ((uint16_t)read_16bitLE(0x00,streamFile) != 0x69) /* Xbox codec */
goto fail;

View File

@ -67,20 +67,21 @@ VGMSTREAM * init_vgmstream_aifc(STREAMFILE *streamFile) {
/* checks */
/* .aif: common (AIFF or AIFC), .aiff: common AIFF, .aifc: common AIFC
* .laif/laifc/laiff: for plugins
* .aifcl/aiffl: for plugins?
* .cbd2: M2 games
* .bgm: Super Street Fighter II Turbo (3DO)
* .acm: Crusader - No Remorse (SAT)
* .adp: Sonic Jam (SAT)
* .ai: Dragon Force (SAT)
* .aifcl/aiffl: for plugins? */
if (check_extensions(streamFile, "aif")) {
* .ai: Dragon Force (SAT) */
if (check_extensions(streamFile, "aif,laif")) {
is_aifc_ext = 1;
is_aiff_ext = 1;
}
else if (check_extensions(streamFile, "aifc,aifcl,afc,cbd2,bgm")) {
else if (check_extensions(streamFile, "aifc,laifc,aifcl,afc,cbd2,bgm")) {
is_aifc_ext = 1;
}
else if (check_extensions(streamFile, "aiff,acm,adp,ai,aiffl")) {
else if (check_extensions(streamFile, "aiff,laiff,acm,adp,ai,aiffl")) {
is_aiff_ext = 1;
}
else {

View File

@ -299,7 +299,8 @@ static int parse_awc_header(STREAMFILE* streamFile, awc_header* awc) {
if ((awc->num_samples && !(awc->num_samples >= num_samples - 10 && awc->num_samples <= num_samples + 10)) ||
(awc->sample_rate && awc->sample_rate != sample_rate) ||
(awc->codec && awc->codec != codec)) {
VGM_LOG("AWC: found header diffs between channels\n");
VGM_LOG("AWC: found header diffs in channel %i, ns=%i vs %i, sr=%i vs %i, c=%i vs %i\n",
ch, awc->num_samples, num_samples, awc->sample_rate, sample_rate, awc->codec, codec);
goto fail;
}

View File

@ -310,8 +310,9 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
* .adx: KID [Remember11 (PC)]
* .rof: The Rhythm of Fighters (Mobile)
* .acm: Planescape Torment Enhanced Edition (PC)
* .sod: Zone 4 (PC) */
if (check_extensions(streamFile,"ogg,logg,adx,rof,acm,sod")) {
* .sod: Zone 4 (PC)
* .aif/laif/aif-Loop: Psychonauts (PC) raw extractions (named) */
if (check_extensions(streamFile,"ogg,logg,adx,rof,acm,sod,aif,laif,aif-Loop")) {
is_ogg = 1;
} else if (check_extensions(streamFile,"um3")) {
is_um3 = 1;

View File

@ -6,6 +6,9 @@
#define TXTP_LINE_MAX 1024
#define TXTP_MIXING_MAX 128
#define TXTP_GROUP_MODE_SEGMENTED 'S'
#define TXTP_GROUP_MODE_LAYERED 'L'
#define TXTP_GROUP_REPEAT 'R'
/* mixing info */
typedef enum {
@ -56,7 +59,11 @@ typedef struct {
typedef struct {
char filename[TXTP_LINE_MAX];
int range_start;
int range_end;
int subsong;
uint32_t channel_mask;
int mixing_count;
txtp_mix_data mixing[TXTP_MIXING_MAX];
@ -78,32 +85,53 @@ typedef struct {
} txtp_entry;
typedef struct {
int position;
char type;
int count;
char repeat;
txtp_entry group_config;
} txtp_group;
typedef struct {
txtp_entry *entry;
size_t entry_count;
size_t entry_max;
txtp_group *group;
size_t group_count;
size_t group_max;
VGMSTREAM* *vgmstream;
size_t vgmstream_count;
uint32_t loop_start_segment;
uint32_t loop_end_segment;
int is_loop_keep;
txtp_entry default_entry;
int default_entry_set;
size_t is_layered;
int is_loop_keep;
int is_segmented;
int is_layered;
} txtp_header;
static txtp_header* parse_txtp(STREAMFILE* streamFile);
static void clean_txtp(txtp_header* txtp);
static void clean_txtp(txtp_header* txtp, int fail);
static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current);
void add_mixing(txtp_entry* cfg, txtp_mix_data* mix, txtp_mix_t command);
static int make_group_segment(txtp_header* txtp, int from, int count);
static int make_group_layer(txtp_header* txtp, int from, int count);
/* TXTP - an artificial playlist-like format to play files with segments/layers/config */
VGMSTREAM * init_vgmstream_txtp(STREAMFILE *streamFile) {
VGMSTREAM * vgmstream = NULL;
VGMSTREAM *vgmstream = NULL;
txtp_header* txtp = NULL;
segmented_layout_data *data_s = NULL;
layered_layout_data * data_l = NULL;
int i;
@ -111,97 +139,173 @@ VGMSTREAM * init_vgmstream_txtp(STREAMFILE *streamFile) {
if (!check_extensions(streamFile, "txtp"))
goto fail;
/* read .txtp text file to get segments */
/* read .txtp with all files and config */
txtp = parse_txtp(streamFile);
if (!txtp) goto fail;
/* post-process */
{
if (txtp->entry_count == 0)
goto fail;
txtp->vgmstream = calloc(txtp->entry_count, sizeof(VGMSTREAM*));
if (!txtp->vgmstream) goto fail;
if (txtp->entry_count == 1 && !txtp->loop_start_segment) {
/* single file */
STREAMFILE* temp_streamFile = open_streamfile_by_filename(streamFile, txtp->entry[0].filename);
if (!temp_streamFile) goto fail;
temp_streamFile->stream_index = txtp->entry[0].subsong;
vgmstream = init_vgmstream_from_STREAMFILE(temp_streamFile);
close_streamfile(temp_streamFile);
if (!vgmstream) goto fail;
apply_config(vgmstream, &txtp->entry[0]);
txtp->vgmstream_count = txtp->entry_count;
}
else if (txtp->is_layered) {
/* layered multi file */
/* init layout */
data_l = init_layout_layered(txtp->entry_count);
if (!data_l) goto fail;
/* open each layer subfile */
for (i = 0; i < data_l->layer_count; i++) {
/* open all entry files first as they'll be modified by modes */
for (i = 0; i < txtp->vgmstream_count; i++) {
STREAMFILE* temp_streamFile = open_streamfile_by_filename(streamFile, txtp->entry[i].filename);
if (!temp_streamFile) goto fail;
if (!temp_streamFile) {
VGM_LOG("TXTP: cannot open streamfile for %s\n", txtp->entry[i].filename);
goto fail;
}
temp_streamFile->stream_index = txtp->entry[i].subsong;
data_l->layers[i] = init_vgmstream_from_STREAMFILE(temp_streamFile);
txtp->vgmstream[i] = init_vgmstream_from_STREAMFILE(temp_streamFile);
close_streamfile(temp_streamFile);
if (!data_l->layers[i]) goto fail;
apply_config(data_l->layers[i], &txtp->entry[i]);
}
/* setup layered VGMSTREAMs */
if (!setup_layout_layered(data_l))
if (!txtp->vgmstream[i]) {
VGM_LOG("TXTP: cannot open vgmstream for %s\n", txtp->entry[i].filename);
goto fail;
/* build the layered VGMSTREAM */
vgmstream = allocate_layered_vgmstream(data_l);
if (!vgmstream) goto fail;
/* custom meta name if all parts don't match */
for (i = 0; i < data_l->layer_count; i++) {
if (vgmstream->meta_type != data_l->layers[i]->meta_type) {
vgmstream->meta_type = meta_TXTP;
break;
}
apply_config(txtp->vgmstream[i], &txtp->entry[i]);
}
/* group files as needed */
for (i = 0; i < txtp->group_count; i++) {
txtp_group *grp = &txtp->group[i];
int pos, groups;
//;VGM_LOG("TXTP: apply group %i%c%i%c\n",txtp->group[i].position,txtp->group[i].type,txtp->group[i].count,txtp->group[i].repeat);
/* special meaning of "all files" */
if (grp->position < 0 || grp->position >= txtp->vgmstream_count)
grp->position = 0;
if (grp->count <= 0)
grp->count = txtp->vgmstream_count - grp->position;
/* repeats N groups (trailing files are not grouped) */
if (grp->repeat == TXTP_GROUP_REPEAT) {
groups = ((txtp->vgmstream_count - grp->position) / grp->count);
}
else {
/* segmented multi file */
int loop_flag;
/* init layout */
data_s = init_layout_segmented(txtp->entry_count);
if (!data_s) goto fail;
/* open each segment subfile */
for (i = 0; i < data_s->segment_count; i++) {
STREAMFILE* temp_streamFile = open_streamfile_by_filename(streamFile, txtp->entry[i].filename);
if (!temp_streamFile) goto fail;
/* subsongs ranges also work for files without subsongs (as to repeat the same file), not sure if bug or feature */
temp_streamFile->stream_index = txtp->entry[i].subsong;
data_s->segments[i] = init_vgmstream_from_STREAMFILE(temp_streamFile);
close_streamfile(temp_streamFile);
if (!data_s->segments[i]) goto fail;
apply_config(data_s->segments[i], &txtp->entry[i]);
groups = 1;
}
/* setup segmented VGMSTREAMs */
/* as groups are compacted position goes 1 by 1 */
for (pos = grp->position; pos < grp->position + groups; pos++) {
//;VGM_LOG("TXTP: group=%i, count=%i, groups=%i\n", pos, grp->count, groups);
switch(grp->type) {
case TXTP_GROUP_MODE_LAYERED:
if (!make_group_layer(txtp, pos, grp->count))
goto fail;
break;
case TXTP_GROUP_MODE_SEGMENTED:
if (!make_group_segment(txtp, pos, grp->count))
goto fail;
break;
default:
goto fail;
}
}
/* group may also have config (like downmixing) */
apply_config(txtp->vgmstream[grp->position], &grp->group_config);
}
/* final grouping (should be integrated with the above?) */
if (txtp->is_layered) {
if (!make_group_layer(txtp, 0, txtp->vgmstream_count))
goto fail;
}
if (txtp->is_segmented) {
if (!make_group_segment(txtp, 0, txtp->vgmstream_count))
goto fail;
}
/* may happen if using mixed mode but some files weren't grouped */
if (txtp->vgmstream_count != 1) {
VGM_LOG("TXTP: wrong final vgmstream count %i\n", txtp->vgmstream_count);
goto fail;
}
/* apply default config to the resulting file */
if (txtp->default_entry_set) {
apply_config(txtp->vgmstream[0], &txtp->default_entry);
}
vgmstream = txtp->vgmstream[0];
clean_txtp(txtp, 0);
return vgmstream;
fail:
clean_txtp(txtp, 1);
return NULL;
}
static void update_vgmstream_list(VGMSTREAM* vgmstream, txtp_header* txtp, int position, int count) {
int i;
//;VGM_LOG("TXTP: compact position=%i count=%i, vgmstreams=%i\n", position, count, txtp->vgmstream_count);
/* sets and compacts vgmstream list pulling back all following entries */
txtp->vgmstream[position] = vgmstream;
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];
}
/* list can only become smaller, no need to alloc/free/etc */
txtp->vgmstream_count = txtp->vgmstream_count + 1 - count;
//;VGM_LOG("TXTP: compact vgmstreams=%i\n", txtp->vgmstream_count);
}
static int make_group_segment(txtp_header* txtp, int position, int count) {
VGMSTREAM * vgmstream = NULL;
segmented_layout_data *data_s = NULL;
int i, loop_flag = 0;
if (count == 1) { /* nothing to do */
//;VGM_LOG("TXTP: ignored segments of 1\n");
return 1;
}
if (position + count > txtp->vgmstream_count || position < 0 || count < 0) {
VGM_LOG("TXTP: ignored segment position=%i, count=%i, entries=%i\n", position, count, txtp->vgmstream_count);
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;
loop_flag = (txtp->loop_start_segment > 0 && txtp->loop_start_segment <= count);
}
/* init layout */
data_s = init_layout_segmented(count);
if (!data_s) goto fail;
/* copy each subfile */
for (i = 0; i < count; i++) {
data_s->segments[i] = txtp->vgmstream[i + position];
txtp->vgmstream[i + position] = NULL; /* will be freed by layout */
}
/* setup VGMSTREAMs */
if (!setup_layout_segmented(data_s))
goto fail;
/* get looping and samples */
if (txtp->loop_start_segment && !txtp->loop_end_segment)
txtp->loop_end_segment = txtp->entry_count;
loop_flag = (txtp->loop_start_segment > 0 && txtp->loop_start_segment <= txtp->entry_count);
/* build the VGMSTREAM */
/* build the layout VGMSTREAM */
vgmstream = allocate_segmented_vgmstream(data_s,loop_flag, txtp->loop_start_segment - 1, txtp->loop_end_segment - 1);
if (!vgmstream) goto fail;
@ -228,28 +332,75 @@ VGMSTREAM * init_vgmstream_txtp(STREAMFILE *streamFile) {
}
}
}
}
/* apply default config to the resulting file */
if (txtp->default_entry_set) {
apply_config(vgmstream, &txtp->default_entry);
}
clean_txtp(txtp);
return vgmstream;
/* set new vgmstream and reorder positions */
update_vgmstream_list(vgmstream, txtp, position, count);
return 1;
fail:
clean_txtp(txtp);
close_vgmstream(vgmstream);
if (!vgmstream) {
if (!vgmstream)
free_layout_segmented(data_s);
free_layout_layered(data_l);
}
return NULL;
return 0;
}
static int make_group_layer(txtp_header* txtp, int position, int count) {
VGMSTREAM * vgmstream = NULL;
layered_layout_data * data_l = NULL;
int i;
if (count == 1) { /* nothing to do */
//;VGM_LOG("TXTP: ignored layer of 1\n");
return 1;
}
if (position + count > txtp->vgmstream_count || position < 0 || count < 0) {
VGM_LOG("TXTP: ignored layer position=%i, count=%i, entries=%i\n", position, count, txtp->vgmstream_count);
return 1;
}
/* init layout */
data_l = init_layout_layered(count);
if (!data_l) goto fail;
/* copy each subfile */
for (i = 0; i < count; i++) {
data_l->layers[i] = txtp->vgmstream[i + position];
txtp->vgmstream[i + position] = NULL; /* will be freed by layout */
}
/* setup VGMSTREAMs */
if (!setup_layout_layered(data_l))
goto fail;
/* build the layout VGMSTREAM */
vgmstream = allocate_layered_vgmstream(data_l);
if (!vgmstream) goto fail;
/* custom meta name if all parts don't match */
for (i = 0; i < count; i++) {
if (vgmstream->meta_type != data_l->layers[i]->meta_type) {
vgmstream->meta_type = meta_TXTP;
break;
}
}
/* set new vgmstream and reorder positions */
update_vgmstream_list(vgmstream, txtp, position, count);
return 1;
fail:
close_vgmstream(vgmstream);
if (!vgmstream)
free_layout_layered(data_l);
return 0;
}
static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current) {
vgmstream->config_loop_count = current->config_loop_count;
@ -628,6 +779,7 @@ void add_mixing(txtp_entry* cfg, txtp_mix_data* mix, txtp_mix_t command) {
static void add_config(txtp_entry* current, txtp_entry* cfg, const char* filename) {
if (filename)
strcpy(current->filename, filename);
current->subsong = cfg->subsong;
@ -660,35 +812,13 @@ static void add_config(txtp_entry* current, txtp_entry* cfg, const char* filenam
current->loop_end_second = cfg->loop_end_second;
}
static int add_filename(txtp_header * txtp, char *filename, int is_default) {
int i, n, nc, nm, mc;
txtp_entry cfg = {0};
size_t range_start, range_end;
static void parse_config(txtp_entry *cfg, char *config) {
/* parse config: #(commands) */
int n, nc, nm, mc;
char command[TXTP_LINE_MAX] = {0};
//;VGM_LOG("TXTP: filename=%s\n", filename);
/* parse config: file.ext#(commands) */
{
char *config;
if (is_default) {
config = filename; /* multiple commands without filename */
}
else {
/* 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, '#'); /* next should be config */
if (!config) /* no config */
config = filename; //todo if no config just exit?
}
range_start = 0;
range_end = 1;
cfg->range_start = 0;
cfg->range_end = 1;
while (config != NULL) {
/* position in next #(command) */
@ -710,8 +840,8 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
if (strcmp(command,"c") == 0) {
/* channel mask: file.ext#c1,2 = play channels 1,2 and mutes rest */
config += get_mask(config, &cfg.channel_mask);
//;VGM_LOG("TXTP: channel_mask ");{int i; for (i=0;i<16;i++)VGM_LOG("%i ",(cfg.channel_mask>>i)&1);}VGM_LOG("\n");
config += get_mask(config, &cfg->channel_mask);
//;VGM_LOG("TXTP: channel_mask ");{int i; for (i=0;i<16;i++)VGM_LOG("%i ",(cfg->channel_mask>>i)&1);}VGM_LOG("\n");
}
else if (strcmp(command,"m") == 0) {
/* channel mixing: file.ext#m(sub-command),(sub-command),etc */
@ -730,7 +860,7 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
if (sscanf(config, " %d - %d%n", &mix.ch_dst, &mix.ch_src, &n) == 2 && n != 0) {
//;VGM_LOG("TXTP: mix %i-%i\n", mix.ch_dst, mix.ch_src);
add_mixing(&cfg, &mix, MIX_SWAP); /* N-M: swaps M with N */
add_mixing(cfg, &mix, MIX_SWAP); /* N-M: swaps M with N */
config += n;
continue;
}
@ -738,14 +868,14 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
if ((sscanf(config, " %d + %d * %lf%n", &mix.ch_dst, &mix.ch_src, &mix.vol, &n) == 3 && n != 0) ||
(sscanf(config, " %d + %d x %lf%n", &mix.ch_dst, &mix.ch_src, &mix.vol, &n) == 3 && n != 0)) {
//;VGM_LOG("TXTP: mix %i+%i*%f\n", mix.ch_dst, mix.ch_src, mix.vol);
add_mixing(&cfg, &mix, MIX_ADD_VOLUME); /* N+M*V: mixes M*volume to N */
add_mixing(cfg, &mix, MIX_ADD_VOLUME); /* N+M*V: mixes M*volume to N */
config += n;
continue;
}
if (sscanf(config, " %d + %d%n", &mix.ch_dst, &mix.ch_src, &n) == 2 && n != 0) {
//;VGM_LOG("TXTP: mix %i+%i\n", mix.ch_dst, mix.ch_src);
add_mixing(&cfg, &mix, MIX_ADD); /* N+M: mixes M to N */
add_mixing(cfg, &mix, MIX_ADD); /* N+M: mixes M to N */
config += n;
continue;
}
@ -753,35 +883,35 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
if ((sscanf(config, " %d * %lf%n", &mix.ch_dst, &mix.vol, &n) == 2 && n != 0) ||
(sscanf(config, " %d x %lf%n", &mix.ch_dst, &mix.vol, &n) == 2 && n != 0)) {
//;VGM_LOG("TXTP: mix %i*%f\n", mix.ch_dst, mix.vol);
add_mixing(&cfg, &mix, MIX_VOLUME); /* N*V: changes volume of N */
add_mixing(cfg, &mix, MIX_VOLUME); /* N*V: changes volume of N */
config += n;
continue;
}
if ((sscanf(config, " %d = %lf%n", &mix.ch_dst, &mix.vol, &n) == 2 && n != 0)) {
//;VGM_LOG("TXTP: mix %i=%f\n", mix.ch_dst, mix.vol);
add_mixing(&cfg, &mix, MIX_LIMIT); /* N=V: limits volume of N */
add_mixing(cfg, &mix, MIX_LIMIT); /* N=V: limits volume of N */
config += n;
continue;
}
if (sscanf(config, " %d%c%n", &mix.ch_dst, &cmd, &n) == 2 && n != 0 && cmd == 'D') {
//;VGM_LOG("TXTP: mix %iD\n", mix.ch_dst);
add_mixing(&cfg, &mix, MIX_KILLMIX); /* ND: downmix N and all following channels */
add_mixing(cfg, &mix, MIX_KILLMIX); /* ND: downmix N and all following channels */
config += n;
continue;
}
if (sscanf(config, " %d%c%n", &mix.ch_dst, &cmd, &n) == 2 && n != 0 && cmd == 'd') {
//;VGM_LOG("TXTP: mix %id\n", mix.ch_dst);
add_mixing(&cfg, &mix, MIX_DOWNMIX);/* Nd: downmix N only */
add_mixing(cfg, &mix, MIX_DOWNMIX);/* Nd: downmix N only */
config += n;
continue;
}
if (sscanf(config, " %d%c%n", &mix.ch_dst, &cmd, &n) == 2 && n != 0 && cmd == 'u') {
//;VGM_LOG("TXTP: mix %iu\n", mix.ch_dst);
add_mixing(&cfg, &mix, MIX_UPMIX); /* Nu: upmix N */
add_mixing(cfg, &mix, MIX_UPMIX); /* Nu: upmix N */
config += n;
continue;
}
@ -790,7 +920,7 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
//;VGM_LOG("TXTP: fade %d^%f~%f=%c@%f~%f+%f~%f\n",
// mix.ch_dst, mix.vol_start, mix.vol_end, mix.shape,
// mix.time_pre, mix.time_start, mix.time_end, mix.time_post);
add_mixing(&cfg, &mix, MIX_FADE); /* N^V1~V2@T1~T2+T3~T4: fades volumes between positions */
add_mixing(cfg, &mix, MIX_FADE); /* N^V1~V2@T1~T2+T3~T4: fades volumes between positions */
config += n;
continue;
}
@ -805,15 +935,15 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
//todo also advance config?
if (sscanf(config, " %d ~ %d", &subsong_start, &subsong_end) == 2) {
if (subsong_start > 0 && subsong_end > 0) {
range_start = subsong_start-1;
range_end = subsong_end;
cfg->range_start = subsong_start-1;
cfg->range_end = subsong_end;
}
//;VGM_LOG("TXTP: subsong range %i~%i\n", range_start, range_end);
}
else if (sscanf(config, " %d", &subsong_start) == 1) {
if (subsong_start > 0) {
range_start = subsong_start-1;
range_end = subsong_start;
cfg->range_start = subsong_start-1;
cfg->range_end = subsong_start;
}
//;VGM_LOG("TXTP: subsong single %i-%i\n", range_start, range_end);
}
@ -822,49 +952,49 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
}
}
else if (strcmp(command,"i") == 0) {
config += get_bool(config, &cfg.config_ignore_loop);
//;VGM_LOG("TXTP: ignore_loop=%i\n", cfg.config_ignore_loop);
config += get_bool(config, &cfg->config_ignore_loop);
//;VGM_LOG("TXTP: ignore_loop=%i\n", cfg->config_ignore_loop);
}
else if (strcmp(command,"E") == 0) {
config += get_bool(config, &cfg.config_force_loop);
//;VGM_LOG("TXTP: force_loop=%i\n", cfg.config_force_loop);
config += get_bool(config, &cfg->config_force_loop);
//;VGM_LOG("TXTP: force_loop=%i\n", cfg->config_force_loop);
}
else if (strcmp(command,"F") == 0) {
config += get_bool(config, &cfg.config_ignore_fade);
//;VGM_LOG("TXTP: ignore_fade=%i\n", cfg.config_ignore_fade);
config += get_bool(config, &cfg->config_ignore_fade);
//;VGM_LOG("TXTP: ignore_fade=%i\n", cfg->config_ignore_fade);
}
else if (strcmp(command,"l") == 0) {
config += get_double(config, &cfg.config_loop_count);
//;VGM_LOG("TXTP: loop_count=%f\n", cfg.config_loop_count);
config += get_double(config, &cfg->config_loop_count);
//;VGM_LOG("TXTP: loop_count=%f\n", cfg->config_loop_count);
}
else if (strcmp(command,"f") == 0) {
config += get_double(config, &cfg.config_fade_time);
//;VGM_LOG("TXTP: fade_time=%f\n", cfg.config_fade_time);
config += get_double(config, &cfg->config_fade_time);
//;VGM_LOG("TXTP: fade_time=%f\n", cfg->config_fade_time);
}
else if (strcmp(command,"d") == 0) {
config += get_double(config, &cfg.config_fade_delay);
//;VGM_LOG("TXTP: fade_delay %f\n", cfg.config_fade_delay);
config += get_double(config, &cfg->config_fade_delay);
//;VGM_LOG("TXTP: fade_delay %f\n", cfg->config_fade_delay);
}
else if (strcmp(command,"h") == 0) {
config += get_int(config, &cfg.sample_rate);
//;VGM_LOG("TXTP: sample_rate %i\n", cfg.sample_rate);
config += get_int(config, &cfg->sample_rate);
//;VGM_LOG("TXTP: sample_rate %i\n", cfg->sample_rate);
}
else if (strcmp(command,"I") == 0) {
n = get_time(config, &cfg.loop_start_second, &cfg.loop_start_sample);
n = get_time(config, &cfg->loop_start_second, &cfg->loop_start_sample);
if (n > 0) { /* first value must exist */
config += n;
n = get_time(config, &cfg.loop_end_second, &cfg.loop_end_sample);
n = get_time(config, &cfg->loop_end_second, &cfg->loop_end_sample);
if (n == 0) { /* second value is optional */
cfg.loop_end_max = 1;
cfg->loop_end_max = 1;
}
config += n;
cfg.loop_install = 1;
cfg->loop_install = 1;
}
//;VGM_LOG("TXTP: loop_install %i (max=%i): %i %i / %f %f\n", cfg.loop_install, cfg.loop_end_max,
// cfg.loop_start_sample, cfg.loop_end_sample, cfg.loop_start_second, cfg.loop_end_second);
//;VGM_LOG("TXTP: loop_install %i (max=%i): %i %i / %f %f\n", cfg->loop_install, cfg->loop_end_max,
// cfg->loop_start_sample, cfg->loop_end_sample, cfg->loop_start_second, cfg->loop_end_second);
}
//todo cleanup
else if (strcmp(command,"@volume") == 0) {
@ -878,7 +1008,7 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
nm = get_mask(config, &mix.mask);
config += nm;
add_mixing(&cfg, &mix, MACRO_VOLUME);
add_mixing(cfg, &mix, MACRO_VOLUME);
}
else if (strcmp(command,"@track") == 0 ||
strcmp(command,"C") == 0 ) {
@ -888,7 +1018,7 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
config += nm;
if (nm == 0) continue;
add_mixing(&cfg, &mix, MACRO_TRACK);
add_mixing(cfg, &mix, MACRO_TRACK);
}
else if (strcmp(command,"@layer-v") == 0 ||
strcmp(command,"@layer-b") == 0 ||
@ -903,7 +1033,7 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
config += nm;
mix.mode = command[7]; /* pass letter */
add_mixing(&cfg, &mix, MACRO_LAYER);
add_mixing(cfg, &mix, MACRO_LAYER);
}
else if (strcmp(command,"@crosslayer-v") == 0 ||
strcmp(command,"@crosslayer-b") == 0 ||
@ -923,7 +1053,7 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
config += nm;
if (nm == 0) continue;
add_mixing(&cfg, &mix, type);
add_mixing(cfg, &mix, type);
}
else if (config[nc] == ' ') {
//;VGM_LOG("TXTP: comment\n");
@ -936,6 +1066,93 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
break;
}
}
}
static int add_group(txtp_header * txtp, char *line) {
int n, m;
txtp_group cfg = {0};
/* parse group: (position)(type)(count)(repeat) #(commands) */
//;VGM_LOG("TXTP: parse group '%s'\n", line);
m = sscanf(line, " %d%n", &cfg.position, &n);
if (m == 1) {
cfg.position--; /* externally 1=first but internally 0=first */
line += n;
}
m = sscanf(line, " %c%n", &cfg.type, &n);
if (m == 1) {
line += n;
}
m = sscanf(line, " %d%n", &cfg.count, &n);
if (m == 1) {
line += n;
}
m = sscanf(line, " %c%n", &cfg.repeat, &n);
if (m == 1 && cfg.repeat == TXTP_GROUP_REPEAT) {
line += n;
}
parse_config(&cfg.group_config, line);
//;VGM_LOG("TXTP: parsed group %i%c%i%c\n",cfg.position+1,cfg.type,cfg.count,cfg.repeat);
/* add final group */
{
/* resize in steps if not enough */
if (txtp->group_count+1 > txtp->group_max) {
txtp_group *temp_group;
txtp->group_max += 5;
temp_group = realloc(txtp->group, sizeof(txtp_group) * txtp->group_max);
if (!temp_group) goto fail;
txtp->group = temp_group;
}
/* new group */
txtp->group[txtp->group_count] = cfg; /* memcpy */
txtp->group_count++;
}
return 1;
fail:
return 0;
}
static int add_entry(txtp_header * txtp, char *filename, int is_default) {
int i;
txtp_entry cfg = {0};
//;VGM_LOG("TXTP: filename=%s\n", filename);
/* parse filename: file.ext#(commands) */
{
char *config;
if (is_default) {
config = filename; /* multiple commands without filename */
}
else {
/* 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, '#'); /* next should be config */
if (!config) /* no config */
config = NULL;
}
parse_config(&cfg, config);
}
@ -945,12 +1162,12 @@ static int add_filename(txtp_header * txtp, char *filename, int is_default) {
/* config that applies to all files */
if (is_default) {
txtp->default_entry_set = 1;
add_config(&txtp->default_entry, &cfg, filename);
add_config(&txtp->default_entry, &cfg, NULL);
return 1;
}
/* add filenames */
for (i = range_start; i < range_end; i++){
/* add final entry */
for (i = cfg.range_start; i < cfg.range_end; i++){
txtp_entry *current;
/* resize in steps if not enough */
@ -978,6 +1195,21 @@ fail:
return 0;
}
/* ************************************************************************ */
static int is_substring(const char * val, const char * cmp) {
int n;
char subval[TXTP_LINE_MAX] = {0};
/* read string without trailing spaces or comments/commands */
if (sscanf(val, " %s%n[^ #\t\r\n]%n", subval, &n, &n) != 1)
return 0;
if (0 != strcmp(subval,cmp))
return 0;
return n;
}
static int parse_num(const char * val, uint32_t * out_value) {
int hex = (val[0]=='0' && val[1]=='x');
if (sscanf(val, hex ? "%x" : "%u", out_value) != 1)
@ -991,6 +1223,7 @@ fail:
static int parse_keyval(txtp_header * txtp, const char * key, const char * val) {
//;VGM_LOG("TXTP: key=val '%s'='%s'\n", key,val);
if (0==strcmp(key,"loop_start_segment")) {
if (!parse_num(val, &txtp->loop_start_segment)) goto fail;
}
@ -998,10 +1231,16 @@ static int parse_keyval(txtp_header * txtp, const char * key, const char * val)
if (!parse_num(val, &txtp->loop_end_segment)) goto fail;
}
else if (0==strcmp(key,"mode")) {
if (0==strcmp(val,"layers")) {
if (is_substring(val,"layers")) {
txtp->is_segmented = 0;
txtp->is_layered = 1;
}
else if (0==strcmp(val,"segments")) {
else if (is_substring(val,"segments")) {
txtp->is_segmented = 1;
txtp->is_layered = 0;
}
else if (is_substring(val,"mixed")) {
txtp->is_segmented = 0;
txtp->is_layered = 0;
}
else {
@ -1009,7 +1248,7 @@ static int parse_keyval(txtp_header * txtp, const char * key, const char * val)
}
}
else if (0==strcmp(key,"loop_mode")) {
if (0==strcmp(val,"keep")) {
if (is_substring(val,"keep")) {
txtp->is_loop_keep = 1;
}
else {
@ -1019,7 +1258,13 @@ static int parse_keyval(txtp_header * txtp, const char * key, const char * val)
else if (0==strcmp(key,"commands")) {
char val2[TXTP_LINE_MAX];
strcpy(val2, val); /* copy since val is modified here but probably not important */
if (!add_filename(txtp, val2, 1)) goto fail;
if (!add_entry(txtp, val2, 1)) goto fail;
}
else if (0==strcmp(key,"group")) {
char val2[TXTP_LINE_MAX];
strcpy(val2, val); /* copy since val is modified here but probably not important */
if (!add_group(txtp, val2)) goto fail;
}
else {
goto fail;
@ -1040,6 +1285,9 @@ static txtp_header* parse_txtp(STREAMFILE* streamFile) {
txtp = calloc(1,sizeof(txtp_header));
if (!txtp) goto fail;
/* defaults */
txtp->is_segmented = 1;
/* empty file: use filename with config (ex. "song.ext#3.txtp") */
if (get_streamfile_size(streamFile) == 0) {
@ -1052,7 +1300,7 @@ static txtp_header* parse_txtp(STREAMFILE* streamFile) {
if (!ext) goto fail; /* ??? */
ext[0] = '\0';
if (!add_filename(txtp, filename, 0))
if (!add_entry(txtp, filename, 0))
goto fail;
return txtp;
@ -1063,7 +1311,7 @@ static txtp_header* parse_txtp(STREAMFILE* streamFile) {
if ((uint16_t)read_16bitLE(0x00, streamFile) == 0xFFFE || (uint16_t)read_16bitLE(0x00, streamFile) == 0xFEFF)
txt_offset = 0x02;
/* read lines */
/* normal file: read and parse lines */
while (txt_offset < file_size) {
char line[TXTP_LINE_MAX] = {0};
char key[TXTP_LINE_MAX] = {0}, val[TXTP_LINE_MAX] = {0}; /* at least as big as a line to avoid overflows (I hope) */
@ -1077,17 +1325,11 @@ static txtp_header* parse_txtp(STREAMFILE* streamFile) {
/* get key/val (ignores lead/trail spaces, # may be commands or comments) */
ok = sscanf(line, " %[^ \t#=] = %[^\t\r\n] ", key,val);
if (ok == 2) { /* no key=val */
if (val[0] != '#') {
/* val is not command, re-parse skipping comments and trailing spaces */
ok = sscanf(line, " %[^ \t#=] = %[^ #\t\r\n] ", key,val);
}
if (ok == 2) {
if (ok == 2) { /* key=val */
if (!parse_keyval(txtp, key, val)) /* read key/val */
goto fail;
continue;
}
}
/* must be a filename (only remove spaces from start/end, as filenames con contain mid spaces/#/etc) */
ok = sscanf(line, " %[^\t\r\n] ", filename);
@ -1097,21 +1339,32 @@ static txtp_header* parse_txtp(STREAMFILE* streamFile) {
continue; /* simple comment */
/* filename with config */
if (!add_filename(txtp, filename, 0))
if (!add_entry(txtp, filename, 0))
goto fail;
}
return txtp;
fail:
clean_txtp(txtp);
clean_txtp(txtp, 1);
return NULL;
}
static void clean_txtp(txtp_header* txtp) {
static void clean_txtp(txtp_header* txtp, int fail) {
int i, start;
if (!txtp)
return;
/* returns first vgmstream on success so it's not closed */
start = fail ? 0 : 1;
for (i = start; i < txtp->vgmstream_count; i++) {
close_vgmstream(txtp->vgmstream[i]);
}
free(txtp->vgmstream);
free(txtp->group);
free(txtp->entry);
free(txtp);
}

View File

@ -548,12 +548,13 @@ void mixing_push_killmix(VGMSTREAM* vgmstream, int ch_dst) {
mix_command_data mix = {0};
int ok;
if (ch_dst <= 1) return; /* can't kill from 1 */
if (ch_dst <= 0) return; /* can't kill from first channel */
if (!data || ch_dst >= data->output_channels) return;
mix.command = MIX_KILLMIX;
mix.ch_dst = ch_dst;
//;VGM_LOG("MIX: killmix %i\n", ch_dst);
ok = add_mixing(vgmstream, &mix);
if (ok) {
data->output_channels = ch_dst; /* clamp channels */

View File

@ -2640,7 +2640,9 @@ static void try_dual_file_stereo(VGMSTREAM * opened_vgmstream, STREAMFILE *strea
mixing_update_channel(opened_vgmstream); /* notify of new channel hacked-in */
}
return;
fail:
close_vgmstream(new_vgmstream);
return;
}

View File

@ -10,7 +10,7 @@ enum { PATH_LIMIT = 32768 };
enum { STREAM_NAME_SIZE = 255 };
enum { VGMSTREAM_MAX_CHANNELS = 64 };
enum { VGMSTREAM_MIN_SAMPLE_RATE = 300 }; /* 300 is Wwise min */
enum { VGMSTREAM_MAX_SAMPLE_RATE = 128000 };
enum { VGMSTREAM_MAX_SAMPLE_RATE = 192000 }; /* found in some FSB5 */
enum { VGMSTREAM_MAX_SUBSONGS = 65535 };
#include "streamfile.h"

View File

@ -1731,6 +1731,7 @@ int ext_output_channels = 0;
static void *winampGetExtendedRead_open_common(in_char *fn, int *size, int *bps, int *nch, int *srate)
{
VGMSTREAM *ext_vgmstream = NULL;
in_char filename[PATH_LIMIT];
int stream_index = 0;
@ -1739,7 +1740,7 @@ static void *winampGetExtendedRead_open_common(in_char *fn, int *size, int *bps,
parse_fn_int(fn, wa_L("$s"), &stream_index);
/* open the stream */
VGMSTREAM *ext_vgmstream = init_vgmstream_winamp(filename, stream_index);
ext_vgmstream = init_vgmstream_winamp(filename, stream_index);
if (!ext_vgmstream) {
return NULL;
}