Fix some TXTP mixing bugs and add loop option in fades

This commit is contained in:
bnnm 2019-09-14 16:48:11 +02:00
parent 4fac5e7d3e
commit 4d7f5a3eb8
3 changed files with 227 additions and 122 deletions

View File

@ -305,17 +305,23 @@ Possible operations:
- `Nu`: upmix (insert) N ('pushing' all following channels forward) - `Nu`: upmix (insert) N ('pushing' all following channels forward)
- `Nd`: downmix (remove) N ('pulling' all following channels backward) - `Nd`: downmix (remove) N ('pulling' all following channels backward)
- `ND`: downmix (remove) N and all following channels - `ND`: downmix (remove) N and all following channels
- `N(type)(time-start)+(time-length)`: defines a fade - `N(type)(position)(time-start)+(time-length)`: defines a fade
* `type` can be `{` = fade-in, `}` = fade-out, `(` = crossfade-in, `)` = crossfade-out * `type` can be `{` = fade-in, `}` = fade-out, `(` = crossfade-in, `)` = crossfade-out
* crossfades are better tuned to use when changing between tracks * crossfades are better tuned to use when changing between tracks
* `(position)` pre-adjusts `(time-start)` to start after certain time (optional)
* using multiple fades in the same channel will cancel previous fades * using multiple fades in the same channel will cancel previous fades
* may only cancel when fade is after previous one * may only cancel when fade is after previous one
* `}` then `{` makes sense, but `}` then `}` will make funny volume bumps * `}` then `{` or `{` then `}` makes sense, but `}` then `}` will make funny volume bumps
* example: `1{0:10+0:5, 1}0:30+0:5` fades-in at 10 seconds, then fades-out at 30 seconds
- `N^(volume-start)~(volume-end)=(shape)@(time-pre)~(time-start)+(time-length)~(time-last)`: defines full fade envelope - `N^(volume-start)~(volume-end)=(shape)@(time-pre)~(time-start)+(time-length)~(time-last)`: defines full fade envelope
* full definition of the above to allow precise volume changes over time * full definition of a fade envelope to allow precise volume changes over time
* not necessarily fades, as you could set length 0 for volume "bumps", or make volumes 1.0~0.5 * not necessarily fades, as you could set length 0 for volume "bumps" like `1.0~0.5`
* pre/post may be -1 to set "file start" and "file end", cancelled by next fade
* `(shape)` can be `{` = fade, `(` = crossfade, other values are reserved for internal testing and may change anytime * `(shape)` can be `{` = fade, `(` = crossfade, other values are reserved for internal testing and may change anytime
* `(time-start)`+`(time-length)` make `(time-end)`
* between `(time-pre)` and `(time-start)` song uses `(volume-start)`
* between `(time-start)` and `(time-end)` song gradually changes `(volume-start)` to `(volume-end)` (depending on `(shape)`)
* between `(time-end)` and `(time-post)` song uses `(volume-end)`
* `time-pre/post` may be -1 to set "file start" and "file end", cancelled by next fade
Considering: Considering:
- `N` and `M` are channels (*current* value after previous operators are applied) - `N` and `M` are channels (*current* value after previous operators are applied)
@ -324,6 +330,8 @@ Considering:
- may use `x` instead of `*` and `_` instead of `:` (for mini-TXTP) - may use `x` instead of `*` and `_` instead of `:` (for mini-TXTP)
- `(volume)` is a `N.N` decimal value where 1.0 is 100% base volume - `(volume)` is a `N.N` decimal value where 1.0 is 100% base volume
- negative volume inverts the waveform (for weird effects) - negative volume inverts the waveform (for weird effects)
- `(position)` can be `N.NL` or `NL` = N.N loops
- if loop start is 1000 and loop end 5000, `0.0L` = 1000 samples, `1.0L` = 5000 samples, `2.0L` = 9000 samples, etc
- `(time)` can be `N:NN(.n)` (minutes:seconds), `N.N` (seconds) or `N` (samples) - `(time)` can be `N:NN(.n)` (minutes:seconds), `N.N` (seconds) or `N` (samples)
- represents the file's global play time, so it may be set after N loops - represents the file's global play time, so it may be set after N loops
- beware of `10.0` (ten seconds) vs `10` (ten samples) - beware of `10.0` (ten seconds) vs `10` (ten samples)
@ -449,7 +457,7 @@ TXTP may even reference other TXTP, or files that require TXTH, for extra comple
bgm bank#s2#c1,2 bgm bank#s2#c1,2
``` ```
You may add spaces as needed (but try to keep it simple and don't go overboard), though commands *must* start with `#(command)` (`#(space)(anything)` is a comment). Commands without corresponding file are ignored too (seen as comments too), while incorrect commands are ignored and skip to next, though the parser may try to make something usable of them (this may be change anytime without warning): You may add spaces as needed (but try to keep it simple and don't go overboard), though commands *must* start with `#(command)` (`#(space)(anything)` is a comment). Commands without corresponding file are ignored too (seen as comments), while incorrect commands are ignored and skip to next, though the parser may try to make something usable of them (this may be change anytime without warning):
``` ```
# those are all equivalent # those are all equivalent
song#s2#c1,2 song#s2#c1,2
@ -510,17 +518,20 @@ Repeated commands overwrite previous setting, except comma-separated commands th
``` ```
# overwrites, equivalent to #s2 # overwrites, equivalent to #s2
song#s1#s2 song#s1#s2
```
```
# adds, equivalent to #m1-2,3-4,5-6 # adds, equivalent to #m1-2,3-4,5-6
song#m1-2#m3-4 song#m1-2#m3-4
commands = #m5-6 commands = #m5-6
# also added to song
commands = #l 3.0
``` ```
The parser is fairly simplistic and lax, and may be erratic with edge cases or behave unexpectedly due to unforeseen use-cases and bugs. As filenames may contain spaces or #, certain name patterns could fool it too. Keep in mind this while making .txtp files. The parser is fairly simplistic and lax, and may be erratic with edge cases or behave unexpectedly due to unforeseen use-cases and bugs. As filenames may contain spaces or #, certain name patterns could fool it too. Keep in mind this while making .txtp files.
## MINI-TXTP ## MINI-TXTP
To simplify TXTP creation, if the .txtp doesn't set a name inside its filename is used directly including config. Note that extension must be included (since vgmstream needs a full filename). You can set `commands` inside the .txtp too: To simplify TXTP creation, if the .txtp doesn't set a name inside then its filename is used directly, including config. Note that extension must be included (since vgmstream needs a full filename). You can set `commands` inside the .txtp too:
- *bgm.sxd2#12.txtp*: plays subsong 12 - *bgm.sxd2#12.txtp*: plays subsong 12
- *bgm.sxd2#12.txtp*, , inside has `commands = #@volume 0.5`: plays subsong 12 at half volume - *bgm.sxd2#12.txtp*, , inside has `commands = #@volume 0.5`: plays subsong 12 at half volume
- *bgm.sxd2.txtp*, , inside has `commands = #12 #@volume 0.5`: plays subsong 12 at half volume - *bgm.sxd2.txtp*, , inside has `commands = #12 #@volume 0.5`: plays subsong 12 at half volume
@ -563,7 +574,7 @@ song#m1+3,2+4,3D
song#m1+3*0.7,2+4*0.7,3D song#m1+3*0.7,2+4*0.7,3D
# downmix 4ch layers to stereo with equal adjusted volume (common layer mixdown) # downmix 4ch layers to stereo with equal adjusted volume (common layer mixdown)
song#m0*0.7,m1*0.7,1+3*0.7,2+4*0.7,3D song#m0*0.7,1*0.7,1+3*0.7,2+4*0.7,3D
# downmix stereo to mono (ignored if file is 1ch) # downmix stereo to mono (ignored if file is 1ch)
zelda-cdi.xa#m1d zelda-cdi.xa#m1d
@ -591,21 +602,21 @@ song#m1-2,2*0.5
# fade-in ch3+4 percussion track layer into main track, downmix to stereo # fade-in ch3+4 percussion track layer into main track, downmix to stereo
# (may be split in multiple lines, no difference) # (may be split in multiple lines, no difference)
okami-ryoshima_coast.aix#l2 okami-ryoshima_coast.aix#l2
commands = #m3(1:10~0:05 # loop happens after ~1:10 commands = #m3(1:10+0:05 # loop happens after ~1:10
commands = #m4(1:10~0:05 # ch3/4 are percussion tracks commands = #m4(1:10+0:05 # ch3/4 are percussion tracks
commands = #m1+3*0.707,2+4*0.707 # ch3/4 always mixed but silent until 1:10 commands = #m1+3*0.707,2+4*0.707 # ch3/4 always mixed but silent until 1:10
commands = #m3D # remove channels after all mixing commands = #m3D # remove channels after all mixing
# same but fade-out percussion after second loop # same but fade-out percussion after second loop
okami-ryoshima_coast.aix#l3 okami-ryoshima_coast.aix#l3
commands = #m3(1:10~0:05,3)2:20~0:05 commands = #m3(1:10+0:05,3)2:20+0:05
commands = #m4(1:10~0:05,4)2:20~0:05 commands = #m4(1:10+0:05,4)2:20+0:05
commands = #m1+3*0.707,2+4*0.707,3D commands = #m1+3*0.707,2+4*0.707,3D
# crossfade exploration and combat sections after loop # crossfade exploration and combat sections after loop
ffxiii-2~eclipse.aix ffxiii-2~eclipse.aix
commands = #m1)1:50~0:10,m2)1:50~0:10 commands = #m1)1:50+0:10,2)1:50+0:10
commands = #m3(1:50~0:10,m4(1:50~0:10 commands = #m3(1:50+0:10,4(1:50+0:10
commands = #m1+3,2+4,3D # won't play at the same time, no volume needed commands = #m1+3,2+4,3D # won't play at the same time, no volume needed
# ghetto voice removal (invert channel + other channel removes duplicated parts, and vocals are often layered) # ghetto voice removal (invert channel + other channel removes duplicated parts, and vocals are often layered)

View File

@ -5,10 +5,11 @@
#define TXTP_LINE_MAX 1024 #define TXTP_LINE_MAX 1024
#define TXTP_MIXING_MAX 128 #define TXTP_MIXING_MAX 512
#define TXTP_GROUP_MODE_SEGMENTED 'S' #define TXTP_GROUP_MODE_SEGMENTED 'S'
#define TXTP_GROUP_MODE_LAYERED 'L' #define TXTP_GROUP_MODE_LAYERED 'L'
#define TXTP_GROUP_REPEAT 'R' #define TXTP_GROUP_REPEAT 'R'
#define TXTP_POSITION_LOOPS 'L'
/* mixing info */ /* mixing info */
typedef enum { typedef enum {
@ -50,6 +51,8 @@ typedef struct {
double time_start; double time_start;
double time_end; double time_end;
double time_post; double time_post;
double position;
char position_type;
/* macros */ /* macros */
int max; int max;
@ -69,8 +72,11 @@ typedef struct {
int mixing_count; int mixing_count;
txtp_mix_data mixing[TXTP_MIXING_MAX]; txtp_mix_data mixing[TXTP_MIXING_MAX];
int config_loop_count_set;
double config_loop_count; double config_loop_count;
int config_fade_time_set;
double config_fade_time; double config_fade_time;
int config_fade_delay_set;
double config_fade_delay; double config_fade_delay;
int config_ignore_loop; int config_ignore_loop;
int config_force_loop; int config_force_loop;
@ -420,16 +426,21 @@ fail:
static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current) { static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current) {
vgmstream->config_loop_count = current->config_loop_count; if (current->config_loop_count_set)
vgmstream->config_fade_time = current->config_fade_time; vgmstream->config_loop_count = current->config_loop_count;
vgmstream->config_fade_delay = current->config_fade_delay; if (current->config_fade_time_set)
vgmstream->config_ignore_loop = current->config_ignore_loop; vgmstream->config_fade_time = current->config_fade_time;
vgmstream->config_force_loop = current->config_force_loop; if (current->config_fade_delay_set)
vgmstream->config_ignore_fade = current->config_ignore_fade; vgmstream->config_fade_delay = current->config_fade_delay;
if (current->config_ignore_loop)
vgmstream->config_ignore_loop = current->config_ignore_loop;
if (current->config_force_loop)
vgmstream->config_force_loop = current->config_force_loop;
if (current->config_ignore_fade)
vgmstream->config_ignore_fade = current->config_ignore_fade;
if (current->sample_rate > 0) { if (current->sample_rate > 0)
vgmstream->sample_rate = current->sample_rate; vgmstream->sample_rate = current->sample_rate;
}
if (current->loop_install) { if (current->loop_install) {
if (current->loop_start_second > 0 || current->loop_end_second > 0) { if (current->loop_start_second > 0 || current->loop_end_second > 0) {
@ -462,7 +473,7 @@ static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current) {
/* copy mixing list (should be done last as some mixes depend on config) */ /* copy mixing list (should be done last as some mixes depend on config) */
if (current->mixing_count > 0) { if (current->mixing_count > 0) {
int m; int m, position_samples;
for (m = 0; m < current->mixing_count; m++) { for (m = 0; m < current->mixing_count; m++) {
txtp_mix_data mix = current->mixing[m]; txtp_mix_data mix = current->mixing[m];
@ -489,6 +500,19 @@ static void apply_config(VGMSTREAM *vgmstream, txtp_entry *current) {
if (mix.time_pre < 0.0) mix.sample_pre = -1; if (mix.time_pre < 0.0) mix.sample_pre = -1;
if (mix.time_post < 0.0) mix.sample_post = -1; if (mix.time_post < 0.0) mix.sample_post = -1;
if (mix.position_type == TXTP_POSITION_LOOPS && vgmstream->loop_flag) {
int loop_pre = vgmstream->loop_start_sample;
int loop_samples = (vgmstream->loop_end_sample - vgmstream->loop_start_sample);
VGM_LOG("ls=%i + %i * %f\n", loop_pre, loop_samples, mix.position);
position_samples = loop_pre + loop_samples * mix.position;
if (mix.sample_pre >= 0) mix.sample_pre += position_samples;
mix.sample_start += position_samples;
mix.sample_end += position_samples;
if (mix.sample_post >= 0) mix.sample_post += position_samples;
}
mixing_push_fade(vgmstream, mix.ch_dst, mix.vol_start, mix.vol_end, mix.shape, mixing_push_fade(vgmstream, mix.ch_dst, mix.vol_start, mix.vol_end, mix.shape,
mix.sample_pre, mix.sample_start, mix.sample_end, mix.sample_post); mix.sample_pre, mix.sample_start, mix.sample_end, mix.sample_post);
break; break;
@ -545,14 +569,17 @@ static void clean_filename(char * filename) {
* - %n: special match (not counted in return value), chars consumed until that point (can appear and be set multiple times) * - %n: special match (not counted in return value), chars consumed until that point (can appear and be set multiple times)
*/ */
static int get_double(const char * config, double *value) { static int get_double(const char * config, double *value, int *is_set) {
int n, m; int n, m;
double temp; double temp;
if (is_set) *is_set = 0;
m = sscanf(config, " %lf%n", &temp,&n); m = sscanf(config, " %lf%n", &temp,&n);
if (m != 1 || temp < 0) if (m != 1 || temp < 0)
return 0; return 0;
if (is_set) *is_set = 1;
*value = temp; *value = temp;
return n; return n;
} }
@ -569,6 +596,26 @@ static int get_int(const char * config, int *value) {
return n; return n;
} }
static int get_position(const char * config, double *value_f, char *value_type) {
int n,m;
double temp_f;
char temp_c;
/* test if format is position: N.n(type) */
m = sscanf(config, " %lf%c%n", &temp_f,&temp_c,&n);
if (m != 2 || temp_f < 0.0)
return 0;
/* test accepted chars as it will capture anything */
if (temp_c != TXTP_POSITION_LOOPS)
return 0;
VGM_LOG("found position: %f, %c\n", temp_f, temp_c);
*value_f = temp_f;
*value_type = temp_c;
return n;
}
static int get_time(const char * config, double *value_f, int32_t *value_i) { static int get_time(const char * config, double *value_f, int32_t *value_i) {
int n,m; int n,m;
int temp_i1, temp_i2; int temp_i1, temp_i2;
@ -752,6 +799,11 @@ static int get_fade(const char * config, txtp_mix_data *mix, int *out_n) {
mix->time_pre = -1.0; mix->time_pre = -1.0;
mix->sample_pre = -1; mix->sample_pre = -1;
n = get_position(config, &mix->position, &mix->position_type);
//if (n == 0) goto fail; /* optional */
config += n;
tn += n;
n = get_time(config, &mix->time_start, &mix->sample_start); n = get_time(config, &mix->time_start, &mix->sample_start);
if (n == 0) goto fail; if (n == 0) goto fail;
config += n; config += n;
@ -785,8 +837,8 @@ void add_mixing(txtp_entry* cfg, txtp_mix_data* mix, txtp_mix_t command) {
return; return;
} }
/* parsers reads ch1 = first, but for mixing code ch0 = first /* parser reads ch1 = first, but for mixing code ch0 = first
* (if parser reads ch0 here it'll become -1 with special meaning in code) */ * (if parser reads ch0 here it'll become -1 with meaning of "all channels" in mixing code) */
mix->ch_dst--; mix->ch_dst--;
mix->ch_src--; mix->ch_src--;
mix->command = command; mix->command = command;
@ -797,14 +849,19 @@ 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) { static void add_config(txtp_entry* current, txtp_entry* cfg, const char* filename) {
/* don't memcopy to allow list additions and ignore values not set,
* as current can be "default" config */
//*current = *cfg;
if (filename) if (filename)
strcpy(current->filename, filename); strcpy(current->filename, filename);
current->subsong = cfg->subsong; if (cfg->subsong)
current->subsong = cfg->subsong;
current->channel_mask = cfg->channel_mask; if (cfg->channel_mask)
current->channel_mask = cfg->channel_mask;
//*current = *cfg; /* don't memcopy to allow list additions */ //todo save list first then memcpy
if (cfg->mixing_count > 0) { if (cfg->mixing_count > 0) {
int i; int i;
@ -814,20 +871,40 @@ static void add_config(txtp_entry* current, txtp_entry* cfg, const char* filenam
} }
} }
current->config_loop_count = cfg->config_loop_count; if (cfg->config_loop_count_set) {
current->config_fade_time = cfg->config_fade_time; current->config_loop_count_set = cfg->config_loop_count_set;
current->config_fade_delay = cfg->config_fade_delay; current->config_loop_count = cfg->config_loop_count;
current->config_ignore_loop = cfg->config_ignore_loop; }
current->config_force_loop = cfg->config_force_loop; if (cfg->config_fade_time_set) {
current->config_ignore_fade = cfg->config_ignore_fade; current->config_fade_time_set = cfg->config_fade_time_set;
current->config_fade_time = cfg->config_fade_time;
}
if (cfg->config_fade_delay_set) {
current->config_fade_delay_set = cfg->config_fade_delay_set;
current->config_fade_delay = cfg->config_fade_delay;
}
if (cfg->config_ignore_loop) {
current->config_ignore_loop = cfg->config_ignore_loop;
}
if (cfg->config_force_loop) {
current->config_force_loop = cfg->config_force_loop;
}
if (cfg->config_ignore_fade) {
current->config_ignore_fade = cfg->config_ignore_fade;
}
current->sample_rate = cfg->sample_rate; if (cfg->sample_rate > 0) {
current->loop_install = cfg->loop_install; current->sample_rate = cfg->sample_rate;
current->loop_end_max = cfg->loop_end_max; }
current->loop_start_sample = cfg->loop_start_sample;
current->loop_start_second = cfg->loop_start_second; if (cfg->loop_install) {
current->loop_end_sample = cfg->loop_end_sample; current->loop_install = cfg->loop_install;
current->loop_end_second = cfg->loop_end_second; current->loop_end_max = cfg->loop_end_max;
current->loop_start_sample = cfg->loop_start_sample;
current->loop_start_second = cfg->loop_start_second;
current->loop_end_sample = cfg->loop_end_sample;
current->loop_end_second = cfg->loop_end_second;
}
} }
static void parse_config(txtp_entry *cfg, char *config) { static void parse_config(txtp_entry *cfg, char *config) {
@ -982,15 +1059,15 @@ static void parse_config(txtp_entry *cfg, char *config) {
//;VGM_LOG("TXTP: ignore_fade=%i\n", cfg->config_ignore_fade); //;VGM_LOG("TXTP: ignore_fade=%i\n", cfg->config_ignore_fade);
} }
else if (strcmp(command,"l") == 0) { else if (strcmp(command,"l") == 0) {
config += get_double(config, &cfg->config_loop_count); config += get_double(config, &cfg->config_loop_count, &cfg->config_loop_count_set);
//;VGM_LOG("TXTP: loop_count=%f\n", cfg->config_loop_count); //;VGM_LOG("TXTP: loop_count=%f\n", cfg->config_loop_count);
} }
else if (strcmp(command,"f") == 0) { else if (strcmp(command,"f") == 0) {
config += get_double(config, &cfg->config_fade_time); config += get_double(config, &cfg->config_fade_time, &cfg->config_fade_time_set);
//;VGM_LOG("TXTP: fade_time=%f\n", cfg->config_fade_time); //;VGM_LOG("TXTP: fade_time=%f\n", cfg->config_fade_time);
} }
else if (strcmp(command,"d") == 0) { else if (strcmp(command,"d") == 0) {
config += get_double(config, &cfg->config_fade_delay); config += get_double(config, &cfg->config_fade_delay, &cfg->config_fade_delay_set);
//;VGM_LOG("TXTP: fade_delay %f\n", cfg->config_fade_delay); //;VGM_LOG("TXTP: fade_delay %f\n", cfg->config_fade_delay);
} }
else if (strcmp(command,"h") == 0) { else if (strcmp(command,"h") == 0) {
@ -1018,7 +1095,7 @@ static void parse_config(txtp_entry *cfg, char *config) {
else if (strcmp(command,"@volume") == 0) { else if (strcmp(command,"@volume") == 0) {
txtp_mix_data mix = {0}; txtp_mix_data mix = {0};
nm = get_double(config, &mix.vol); nm = get_double(config, &mix.vol, NULL);
config += nm; config += nm;
if (nm == 0) continue; if (nm == 0) continue;

View File

@ -48,7 +48,8 @@
* (and maybe improve memory cache?), though maybe it should call one function per operation. * (and maybe improve memory cache?), though maybe it should call one function per operation.
*/ */
#define VGMSTREAM_MAX_MIXING 128 #define VGMSTREAM_MAX_MIXING 512
#define MIXING_PI 3.14159265358979323846f
/* mixing info */ /* mixing info */
@ -115,24 +116,72 @@ static int is_active(mixing_data *data, int32_t current_start, int32_t current_e
return 0; return 0;
} }
static int32_t get_current_pos(VGMSTREAM* vgmstream) { static int32_t get_current_pos(VGMSTREAM* vgmstream, int32_t sample_count) {
int32_t current_pos; int32_t current_pos;
if (vgmstream->loop_flag && vgmstream->current_sample > vgmstream->loop_start_sample) { if (vgmstream->loop_flag && vgmstream->loop_count > 0) {
int loop_pre = vgmstream->loop_start_sample; int loop_pre = vgmstream->loop_start_sample; /* samples before looping */
int loop_into = vgmstream->current_sample - vgmstream->loop_start_sample; int loop_into = (vgmstream->current_sample - vgmstream->loop_start_sample); /* samples after loop */
int loop_samples = vgmstream->loop_end_sample - vgmstream->loop_start_sample; int loop_samples = (vgmstream->loop_end_sample - vgmstream->loop_start_sample); /* looped section */
current_pos = loop_pre + loop_into + loop_samples*vgmstream->loop_count;
current_pos = loop_pre + (loop_samples * vgmstream->loop_count) + loop_into - sample_count;
} }
else { else {
current_pos = vgmstream->current_sample; current_pos = (vgmstream->current_sample - sample_count);
} }
return current_pos; return current_pos;
} }
static float get_fade_gain_curve(char shape, float index) {
float gain;
/* don't bother doing calcs near 0.0/1.0 */
if (index <= 0.0001f || index >= 0.9999f) {
return index;
}
//todo optimizations: interleave calcs, maybe use cosf, powf, etc? (with extra defines)
/* (curve math mostly from SoX/FFmpeg) */
switch(shape) {
/* 2.5f in L/E 'pow' is the attenuation factor, where 5.0 (100db) is common but a bit fast
* (alt calculations with 'exp' from FFmpeg use (factor)*ln(0.1) = -NN.N... */
case 'E': /* exponential (for fade-outs, closer to natural decay of sound) */
//gain = pow(0.1f, (1.0f - index) * 2.5f);
gain = exp(-5.75646273248511f * (1.0f - index));
break;
case 'L': /* logarithmic (inverse of the above, maybe for crossfades) */
//gain = 1 - pow(0.1f, (index) * 2.5f);
gain = 1 - exp(-5.75646273248511f * (index));
break;
case 'H': /* raised sine wave or cosine wave (for more musical crossfades) */
gain = (1.0f - cos(index * MIXING_PI)) / 2.0f;
break;
case 'Q': /* quarter of sine wave (for musical fades) */
gain = sin(index * MIXING_PI / 2.0f);
break;
case 'p': /* parabola (maybe for crossfades) */
gain = 1.0f - sqrt(1.0f - index);
break;
case 'P': /* inverted parabola (maybe for fades) */
gain = (1.0f - (1.0f - index) * (1.0f - index));
break;
case 'T': /* triangular/linear (simpler/sharper fades) */
default:
gain = index;
break;
}
return gain;
}
static int get_fade_gain(mix_command_data *mix, float *out_cur_vol, int32_t current_subpos) { static int get_fade_gain(mix_command_data *mix, float *out_cur_vol, int32_t current_subpos) {
//todo optimizations: interleave calcs, maybe use cosf, powf, etc?
float cur_vol = 0.0f; float cur_vol = 0.0f;
if ((current_subpos >= mix->time_pre || mix->time_pre < 0) && current_subpos < mix->time_start) { if ((current_subpos >= mix->time_pre || mix->time_pre < 0) && current_subpos < mix->time_start) {
@ -173,41 +222,7 @@ static int get_fade_gain(mix_command_data *mix, float *out_cur_vol, int32_t curr
* curves are complementary (exponential fade-in ~= logarithmic fade-out); the following * curves are complementary (exponential fade-in ~= logarithmic fade-out); the following
* are described taking fade-in = normal. * are described taking fade-in = normal.
*/ */
gain = get_fade_gain_curve(mix->shape, index);
/* (curve math mostly from SoX/FFmpeg) */
switch(mix->shape) {
/* 2.5f in L/E 'pow' is the attenuation factor, where 5.0 (100db) is common but a bit fast
* (alt calculations with 'exp' from FFmpeg use (factor)*ln(0.1) = -NN.N... */
case 'E': /* exponential (for fade-outs, closer to natural decay of sound) */
//gain = pow(0.1f, (1.0f - index) * 2.5f);
gain = exp(-5.75646273248511f * (1.0f - index));
break;
case 'L': /* logarithmic (inverse of the above, maybe for crossfades) */
//gain = 1 - pow(0.1f, (index) * 2.5f);
gain = 1 - exp(-5.75646273248511f * (index));
break;
case 'H': /* raised sine wave or cosine wave (for more musical crossfades) */
gain = (1.0f - cos(index * M_PI )) / 2.0f;
break;
case 'Q': /* quarter of sine wave (for musical fades) */
gain = sin(index * M_PI / 2.0f);
break;
case 'p': /* parabola (maybe for crossfades) */
gain = 1.0f - sqrt(1.0f - index);
break;
case 'P': /* inverted parabola (maybe for fades) */
gain = (1.0f - (1.0f - index) * (1.0f - index));
break;
case 'T': /* triangular/linear (simpler/sharper fades) */
default:
gain = index;
break;
}
if (mix->vol_start < mix->vol_end) { /* fade in */ if (mix->vol_start < mix->vol_end) { /* fade in */
cur_vol = mix->vol_start + range_vol * gain; cur_vol = mix->vol_start + range_vol * gain;
@ -216,6 +231,7 @@ static int get_fade_gain(mix_command_data *mix, float *out_cur_vol, int32_t curr
} }
} }
else { else {
/* fade is outside reach */
goto fail; goto fail;
} }
@ -230,20 +246,19 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream)
int ch, s, m, ok; int ch, s, m, ok;
int32_t current_pos, current_subpos; int32_t current_pos, current_subpos;
float temp_f, temp_min, temp_max, cur_vol; float temp_f, temp_min, temp_max, cur_vol = 0.0f;
float *temp_mixbuf; float *temp_mixbuf;
sample_t *temp_outbuf; sample_t *temp_outbuf;
const float limiter_max = 32767.0f; const float limiter_max = 32767.0f;
const float limiter_min = -32768.0f; const float limiter_min = -32768.0f;
/* no support or not need to apply */ /* no support or not need to apply */
if (!data || !data->mixing_on || data->mixing_count == 0) if (!data || !data->mixing_on || data->mixing_count == 0)
return; return;
/* try to skip if no ops apply (for example if fade set but does nothing yet) */ /* try to skip if no ops apply (for example if fade set but does nothing yet) */
current_pos = get_current_pos(vgmstream); current_pos = get_current_pos(vgmstream, sample_count);
if (!is_active(data, current_pos, current_pos + sample_count)) if (!is_active(data, current_pos, current_pos + sample_count))
return; return;
@ -252,6 +267,8 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream)
temp_mixbuf = data->mixbuf; temp_mixbuf = data->mixbuf;
temp_outbuf = outbuf; temp_outbuf = outbuf;
current_subpos = current_pos;
/* apply mixes in order per channel */ /* apply mixes in order per channel */
for (s = 0; s < sample_count; s++) { for (s = 0; s < sample_count; s++) {
/* reset after new sample 'step'*/ /* reset after new sample 'step'*/
@ -263,7 +280,7 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream)
} }
for (m = 0; m < data->mixing_count; m++) { for (m = 0; m < data->mixing_count; m++) {
mix_command_data mix = data->mixing_chain[m]; mix_command_data *mix = &data->mixing_chain[m];
/* mixing ops are designed to apply in order, all channels per 1 sample 'step'. Since some ops change /* mixing ops are designed to apply in order, all channels per 1 sample 'step'. Since some ops change
* total channels, channel number meaning varies as ops move them around, ex: * total channels, channel number meaning varies as ops move them around, ex:
@ -274,34 +291,34 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream)
* - 2ch w/ "1-2,1d" = ch1<>ch2, ch1(drop and move ch2(old ch1) to ch1) = ch1 * - 2ch w/ "1-2,1d" = ch1<>ch2, ch1(drop and move ch2(old ch1) to ch1) = ch1
* - 2ch w/ "1d,1-2" = ch1(drop and pull rest), ch1(do nothing, ch2 doesn't exist now) = ch2 * - 2ch w/ "1d,1-2" = ch1(drop and pull rest), ch1(do nothing, ch2 doesn't exist now) = ch2
*/ */
switch(mix.command) { switch(mix->command) {
case MIX_SWAP: case MIX_SWAP:
temp_f = stpbuf[mix.ch_dst]; temp_f = stpbuf[mix->ch_dst];
stpbuf[mix.ch_dst] = stpbuf[mix.ch_src]; stpbuf[mix->ch_dst] = stpbuf[mix->ch_src];
stpbuf[mix.ch_src] = temp_f; stpbuf[mix->ch_src] = temp_f;
break; break;
case MIX_ADD: case MIX_ADD:
stpbuf[mix.ch_dst] = stpbuf[mix.ch_dst] + stpbuf[mix.ch_src] * mix.vol; stpbuf[mix->ch_dst] = stpbuf[mix->ch_dst] + stpbuf[mix->ch_src] * mix->vol;
break; break;
case MIX_VOLUME: case MIX_VOLUME:
if (mix.ch_dst < 0) { if (mix->ch_dst < 0) {
for (ch = 0; ch < step_channels; ch++) { for (ch = 0; ch < step_channels; ch++) {
stpbuf[ch] = stpbuf[ch] * mix.vol; stpbuf[ch] = stpbuf[ch] * mix->vol;
} }
} }
else { else {
stpbuf[mix.ch_dst] = stpbuf[mix.ch_dst] * mix.vol; stpbuf[mix->ch_dst] = stpbuf[mix->ch_dst] * mix->vol;
} }
break; break;
case MIX_LIMIT: case MIX_LIMIT:
temp_max = limiter_max * mix.vol; temp_max = limiter_max * mix->vol;
temp_min = limiter_min * mix.vol; temp_min = limiter_min * mix->vol;
if (mix.ch_dst < 0) { if (mix->ch_dst < 0) {
for (ch = 0; ch < step_channels; ch++) { for (ch = 0; ch < step_channels; ch++) {
if (stpbuf[ch] > temp_max) if (stpbuf[ch] > temp_max)
stpbuf[ch] = temp_max; stpbuf[ch] = temp_max;
@ -310,47 +327,45 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream)
} }
} }
else { else {
if (stpbuf[mix.ch_dst] > temp_max) if (stpbuf[mix->ch_dst] > temp_max)
stpbuf[mix.ch_dst] = temp_max; stpbuf[mix->ch_dst] = temp_max;
else if (stpbuf[mix.ch_dst] < temp_min) else if (stpbuf[mix->ch_dst] < temp_min)
stpbuf[mix.ch_dst] = temp_min; stpbuf[mix->ch_dst] = temp_min;
} }
break; break;
case MIX_UPMIX: case MIX_UPMIX:
step_channels += 1; step_channels += 1;
for (ch = step_channels - 1; ch > mix.ch_dst; ch--) { for (ch = step_channels - 1; ch > mix->ch_dst; ch--) {
stpbuf[ch] = stpbuf[ch-1]; /* 'push' channels forward (or pull backwards) */ stpbuf[ch] = stpbuf[ch-1]; /* 'push' channels forward (or pull backwards) */
} }
stpbuf[mix.ch_dst] = 0; /* inserted as silent */ stpbuf[mix->ch_dst] = 0; /* inserted as silent */
break; break;
case MIX_DOWNMIX: case MIX_DOWNMIX:
step_channels -= 1; step_channels -= 1;
for (ch = mix.ch_dst; ch < step_channels; ch++) { for (ch = mix->ch_dst; ch < step_channels; ch++) {
stpbuf[ch] = stpbuf[ch+1]; /* 'pull' channels back */ stpbuf[ch] = stpbuf[ch+1]; /* 'pull' channels back */
} }
break; break;
case MIX_KILLMIX: case MIX_KILLMIX:
step_channels = mix.ch_dst; /* clamp channels */ step_channels = mix->ch_dst; /* clamp channels */
break; break;
case MIX_FADE: case MIX_FADE:
current_subpos = current_pos + s; ok = get_fade_gain(mix, &cur_vol, current_subpos);
ok = get_fade_gain(&mix, &cur_vol, current_subpos);
if (!ok) { if (!ok) {
break; break; /* fade doesn't apply right now */
} }
if (mix.ch_dst < 0) { if (mix->ch_dst < 0) {
for (ch = 0; ch < step_channels; ch++) { for (ch = 0; ch < step_channels; ch++) {
stpbuf[ch] = stpbuf[ch] * cur_vol; stpbuf[ch] = stpbuf[ch] * cur_vol;
} }
} }
else { else {
stpbuf[mix.ch_dst] = stpbuf[mix.ch_dst] * cur_vol; stpbuf[mix->ch_dst] = stpbuf[mix->ch_dst] * cur_vol;
} }
break; break;
@ -359,6 +374,8 @@ void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream)
} }
} }
current_subpos++;
temp_mixbuf += step_channels; temp_mixbuf += step_channels;
temp_outbuf += vgmstream->channels; temp_outbuf += vgmstream->channels;
} }