vgmstream/src/base/mixing.c
2023-07-30 23:47:50 +02:00

364 lines
13 KiB
C

#include "../vgmstream.h"
#include "../util/channel_mappings.h"
#include "mixing.h"
#include "mixing_priv.h"
#include "mixing_fades.h"
#include "plugins.h"
#include <math.h>
#include <limits.h>
/**
* Mixing lets vgmstream modify the resulting sample buffer before final output.
* This can be implemented in a number of ways but it's done like it is considering
* overall simplicity in coding, usage and performance (main complexity is allowing
* down/upmixing). Code is mostly independent with some hooks in the main vgmstream
* code.
*
* It works using two buffers:
* - outbuf: plugin's pcm16 buffer, at least input_channels*sample_count
* - mixbuf: internal's pcmfloat buffer, at least mixing_channels*sample_count
* outbuf starts with decoded samples of vgmstream->channel size. This unsures that
* if no mixing is done (most common case) we can skip copying samples between buffers.
* Resulting outbuf after mixing has samples for ->output_channels (plus garbage).
* - output_channels is the resulting total channels (that may be less/more/equal)
* - input_channels is normally ->channels or ->output_channels when it's higher
*
* First, a meta (ex. TXTP) or plugin may add mixing commands through the API,
* validated so non-sensical mixes are ignored (to ensure mixing code doesn't
* have to recheck every time). Then, before starting to decode mixing must be
* manually activated, because plugins need to be ready for possibly different
* input/output channels. API could be improved but this way we can avoid having
* to update all plugins, while allowing internal setup and layer/segment mixing
* (may change in the future for simpler usage).
*
* Then after decoding normally, vgmstream applies mixing internally:
* - detect if mixing is active and needs to be done at this point (some effects
* like fades only apply after certain time) and skip otherwise.
* - copy outbuf to mixbuf, as using a float buffer to increase accuracy (most ops
* apply float volumes) and slightly improve performance (avoids doing
* int16-to-float casts per mix, as it's not free)
* - apply all mixes on mixbuf
* - copy mixbuf to outbuf
* segmented/layered layouts handle mixing on their own.
*
* Mixing is tuned for most common case (no mix except fade-out at the end) and is
* fast enough but not super-optimized yet, there is some penalty the more effects
* are applied. Maybe could add extra sub-ops to avoid ifs and dumb values (volume=0.0
* could simply use a clear op), only use mixbuf if necessary (swap can be done without
* mixbuf if it goes first) or add function pointer indexes but isn't too important.
* Operations are applied once per "step" with 1 sample from all channels to simplify code
* (and maybe improve memory cache?), though maybe it should call one function per operation.
*/
/* ******************************************************************* */
static void sbuf_copy_f32_to_s16(int16_t* buf_s16, float* buf_f32, int samples, int channels) {
for (int s = 0; s < samples * channels; s++) {
/* when casting float to int, value is simply truncated:
* - (int)1.7 = 1, (int)-1.7 = -1
* alts for more accurate rounding could be:
* - (int)floor(f)
* - (int)(f < 0 ? f - 0.5f : f + 0.5f)
* - (((int) (f1 + 32768.5)) - 32768)
* - etc
* but since +-1 isn't really audible we'll just cast as it's the fastest
*/
buf_s16[s] = clamp16( (int32_t)buf_f32[s] );
}
}
void mix_vgmstream(sample_t *outbuf, int32_t sample_count, VGMSTREAM* vgmstream) {
mixing_data *data = vgmstream->mixing_data;
int ch, s, m, ok;
int32_t current_subpos = 0;
float temp_f, temp_min, temp_max, cur_vol = 0.0f;
float *temp_mixbuf;
sample_t *temp_outbuf;
const float limiter_max = 32767.0f;
const float limiter_min = -32768.0f;
/* no support or not need to apply */
if (!data || !data->mixing_on || data->mixing_count == 0)
return;
/* try to skip if no fades apply (set but does nothing yet) + only has fades */
if (data->has_fade) {
int32_t current_pos = get_current_pos(vgmstream, sample_count);
//;VGM_LOG("MIX: fade test %i, %i\n", data->has_non_fade, is_fade_active(data, current_pos, current_pos + sample_count));
if (!data->has_non_fade && !is_fade_active(data, current_pos, current_pos + sample_count))
return;
//;VGM_LOG("MIX: fade pos=%i\n", current_pos);
current_subpos = current_pos;
}
/* use advancing buffer pointers to simplify logic */
temp_mixbuf = data->mixbuf; /* you'd think using a int32 temp buf would be faster but somehow it's slower? */
temp_outbuf = outbuf;
/* 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:
* - 4ch w/ "1-2,2+3" = ch1<>ch3, ch2(old ch1)+ch3 = 4ch: ch2 ch1+ch3 ch3 ch4
* - 4ch w/ "2+3,1-2" = ch2+ch3, ch1<>ch2(modified) = 4ch: ch2+ch3 ch1 ch3 ch4
* - 2ch w/ "1+2,1u" = ch1+ch2, ch1(add and push rest) = 3ch: ch1' ch1+ch2 ch2
* - 2ch w/ "1u,1+2" = ch1(add and push rest) = 3ch: ch1'+ch1 ch1 ch2
* - 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
*/
/* apply mixes in order per channel */
for (s = 0; s < sample_count; s++) {
/* reset after new sample 'step'*/
float *stpbuf = temp_mixbuf;
int step_channels = vgmstream->channels;
for (ch = 0; ch < step_channels; ch++) {
stpbuf[ch] = temp_outbuf[ch]; /* copy current 'lane' */
}
for (m = 0; m < data->mixing_count; m++) {
mix_command_data *mix = &data->mixing_chain[m];
switch(mix->command) {
case MIX_SWAP:
temp_f = stpbuf[mix->ch_dst];
stpbuf[mix->ch_dst] = stpbuf[mix->ch_src];
stpbuf[mix->ch_src] = temp_f;
break;
case MIX_ADD:
stpbuf[mix->ch_dst] = stpbuf[mix->ch_dst] + stpbuf[mix->ch_src] * mix->vol;
break;
case MIX_ADD_COPY:
stpbuf[mix->ch_dst] = stpbuf[mix->ch_dst] + stpbuf[mix->ch_src];
break;
case MIX_VOLUME:
if (mix->ch_dst < 0) {
for (ch = 0; ch < step_channels; ch++) {
stpbuf[ch] = stpbuf[ch] * mix->vol;
}
}
else {
stpbuf[mix->ch_dst] = stpbuf[mix->ch_dst] * mix->vol;
}
break;
case MIX_LIMIT:
temp_max = limiter_max * mix->vol;
temp_min = limiter_min * mix->vol;
if (mix->ch_dst < 0) {
for (ch = 0; ch < step_channels; ch++) {
if (stpbuf[ch] > temp_max)
stpbuf[ch] = temp_max;
else if (stpbuf[ch] < temp_min)
stpbuf[ch] = temp_min;
}
}
else {
if (stpbuf[mix->ch_dst] > temp_max)
stpbuf[mix->ch_dst] = temp_max;
else if (stpbuf[mix->ch_dst] < temp_min)
stpbuf[mix->ch_dst] = temp_min;
}
break;
case MIX_UPMIX:
step_channels += 1;
for (ch = step_channels - 1; ch > mix->ch_dst; ch--) {
stpbuf[ch] = stpbuf[ch-1]; /* 'push' channels forward (or pull backwards) */
}
stpbuf[mix->ch_dst] = 0; /* inserted as silent */
break;
case MIX_DOWNMIX:
step_channels -= 1;
for (ch = mix->ch_dst; ch < step_channels; ch++) {
stpbuf[ch] = stpbuf[ch+1]; /* 'pull' channels back */
}
break;
case MIX_KILLMIX:
step_channels = mix->ch_dst; /* clamp channels */
break;
case MIX_FADE:
ok = get_fade_gain(mix, &cur_vol, current_subpos);
if (!ok) {
break; /* fade doesn't apply right now */
}
if (mix->ch_dst < 0) {
for (ch = 0; ch < step_channels; ch++) {
stpbuf[ch] = stpbuf[ch] * cur_vol;
}
}
else {
stpbuf[mix->ch_dst] = stpbuf[mix->ch_dst] * cur_vol;
}
break;
default:
break;
}
}
current_subpos++;
temp_mixbuf += step_channels;
temp_outbuf += vgmstream->channels;
}
/* copy resulting temp mix to output */
sbuf_copy_f32_to_s16(outbuf, data->mixbuf, sample_count, data->output_channels);
}
/* ******************************************************************* */
void mixing_init(VGMSTREAM* vgmstream) {
mixing_data *data = calloc(1, sizeof(mixing_data));
if (!data) goto fail;
data->mixing_size = VGMSTREAM_MAX_MIXING; /* fixed array for now */
data->mixing_channels = vgmstream->channels;
data->output_channels = vgmstream->channels;
vgmstream->mixing_data = data;
return;
fail:
free(data);
return;
}
void mixing_close(VGMSTREAM* vgmstream) {
mixing_data *data = NULL;
if (!vgmstream) return;
data = vgmstream->mixing_data;
if (!data) return;
free(data->mixbuf);
free(data);
}
void mixing_update_channel(VGMSTREAM* vgmstream) {
mixing_data *data = vgmstream->mixing_data;
if (!data) return;
/* lame hack for dual stereo, but dual stereo is pretty hack-ish to begin with */
data->mixing_channels++;
data->output_channels++;
}
/* ******************************************************************* */
static int fix_layered_channel_layout(VGMSTREAM* vgmstream) {
int i;
mixing_data* data = vgmstream->mixing_data;
layered_layout_data* layout_data;
uint32_t prev_cl;
if (vgmstream->channel_layout || vgmstream->layout_type != layout_layered)
return 0;
layout_data = vgmstream->layout_data;
/* mainly layer-v (in cases of layers-within-layers should cascade) */
if (data->output_channels != layout_data->layers[0]->channels)
return 0;
/* check all layers share layout (implicitly works as a channel check, if not 0) */
prev_cl = layout_data->layers[0]->channel_layout;
if (prev_cl == 0)
return 0;
for (i = 1; i < layout_data->layer_count; i++) {
uint32_t layer_cl = layout_data->layers[i]->channel_layout;
if (prev_cl != layer_cl)
return 0;
prev_cl = layer_cl;
}
vgmstream->channel_layout = prev_cl;
return 1;
}
/* channel layout + down/upmixing = ?, salvage what we can */
static void fix_channel_layout(VGMSTREAM* vgmstream) {
mixing_data* data = vgmstream->mixing_data;
if (fix_layered_channel_layout(vgmstream))
goto done;
/* segments should share channel layout automatically */
/* a bit wonky but eh... */
if (vgmstream->channel_layout && vgmstream->channels != data->output_channels) {
vgmstream->channel_layout = 0;
}
done:
((VGMSTREAM*)vgmstream->start_vgmstream)->channel_layout = vgmstream->channel_layout;
}
void mixing_setup(VGMSTREAM* vgmstream, int32_t max_sample_count) {
mixing_data *data = vgmstream->mixing_data;
float *mixbuf_re = NULL;
if (!data) goto fail;
/* special value to not actually enable anything (used to query values) */
if (max_sample_count <= 0)
goto fail;
/* create or alter internal buffer */
mixbuf_re = realloc(data->mixbuf, max_sample_count*data->mixing_channels*sizeof(float));
if (!mixbuf_re) goto fail;
data->mixbuf = mixbuf_re;
data->mixing_on = 1;
fix_channel_layout(vgmstream);
/* since data exists on its own memory and pointer is already set
* there is no need to propagate to start_vgmstream */
/* segments/layers are independant from external buffers and may always mix */
return;
fail:
return;
}
void mixing_info(VGMSTREAM* vgmstream, int* p_input_channels, int* p_output_channels) {
mixing_data *data = vgmstream->mixing_data;
int input_channels, output_channels;
if (!data) goto fail;
output_channels = data->output_channels;
if (data->output_channels > vgmstream->channels)
input_channels = data->output_channels;
else
input_channels = vgmstream->channels;
if (p_input_channels) *p_input_channels = input_channels;
if (p_output_channels) *p_output_channels = output_channels;
//;VGM_LOG("MIX: channels %i, in=%i, out=%i, mix=%i\n", vgmstream->channels, input_channels, output_channels, data->mixing_channels);
return;
fail:
if (p_input_channels) *p_input_channels = vgmstream->channels;
if (p_output_channels) *p_output_channels = vgmstream->channels;
return;
}