channelmix: make up/downmix levels configurable

Add channelmix.center-level, channelmix.surround-level and
channelmix.lfe-level to control how center, surround and LFE is
up/downmixed.

Fixes #5198
This commit is contained in:
Wim Taymans 2026-03-30 17:50:48 +02:00
parent 03f894bab0
commit 18c97222c4
12 changed files with 119 additions and 25 deletions

View file

@ -80,6 +80,9 @@ stream.properties = {
#channelmix.fc-cutoff = 12000.0
#channelmix.rear-delay = 12.0
#channelmix.stereo-widen = 0.0
#channelmix.center-level = 0.707106781
#channelmix.surround-level = 0.707106781
#channelmix.lfe-level = 0.5
#channelmix.hilbert-taps = 0
#dither.noise = 0
#dither.method = none # rectangular, triangular, triangular-hf, wannamaker3, shaped5

View file

@ -783,6 +783,15 @@ more to the center speaker and leaves the ambient sound in the stereo channels.
This is only active when up-mix is enabled and a Front Center channel is mixed.
\endparblock
@PAR@ node-prop channelmix.center-level = 0.707106781
The level of the center channel when up/downmixing.
@PAR@ node-prop channelmix.surround-level = 0.707106781
The level of the surround channels when up/downmixing.
@PAR@ node-prop channelmix.lfe-level = 0.5
The level of the LFE channel when up/downmixing.
@PAR@ node-prop channelmix.hilbert-taps = 0
\parblock
This option will apply a 90 degree phase shift to the rear channels to improve specialization.

View file

@ -93,6 +93,9 @@ stream.properties = {
#channelmix.fc-cutoff = 12000.0
#channelmix.rear-delay = 12.0
#channelmix.stereo-widen = 0.0
#channelmix.center-level = 0.707106781
#channelmix.surround-level = 0.707106781
#channelmix.lfe-level = 0.5
#channelmix.hilbert-taps = 0
#dither.noise = 0
#dither.method = none # rectangular, triangular, triangular-hf, wannamaker3, shaped5

View file

@ -724,6 +724,34 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 19:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("channelmix.center-level"),
SPA_PROP_INFO_description, SPA_POD_String("Center up/downmix level"),
SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
this->mix.center_level, 0.0, 10.0),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 20:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("channelmix.surround-level"),
SPA_PROP_INFO_description, SPA_POD_String("Surround up/downmix level"),
SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
this->mix.surround_level, 0.0, 10.0),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 21:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("channelmix.lfe-level"),
SPA_PROP_INFO_description, SPA_POD_String("LFE up/downmix level"),
SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(
this->mix.lfe_level, 0.0, 10.0),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 22:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("channelmix.hilbert-taps"),
@ -732,7 +760,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
this->mix.hilbert_taps, 0, MAX_TAPS),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 20:
case 23:
spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
spa_pod_builder_add(b,
SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix-method"),
@ -751,14 +779,14 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
spa_pod_builder_pop(b, &f[1]);
*param = spa_pod_builder_pop(b, &f[0]);
break;
case 21:
case 24:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_rate),
SPA_PROP_INFO_description, SPA_POD_String("Rate scaler"),
SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Double(p->rate, 0.0, 10.0));
break;
case 22:
case 25:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_quality),
@ -767,7 +795,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->resample_quality, 0, 14),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 23:
case 26:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("resample.disable"),
@ -775,7 +803,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->resample_disabled),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 24:
case 27:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("dither.noise"),
@ -783,7 +811,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(this->dir[1].conv.noise_bits, 0, 16),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 25:
case 28:
spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id);
spa_pod_builder_add(b,
SPA_PROP_INFO_name, SPA_POD_String("dither.method"),
@ -801,7 +829,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
spa_pod_builder_pop(b, &f[1]);
*param = spa_pod_builder_pop(b, &f[0]);
break;
case 26:
case 29:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("debug.wav-path"),
@ -809,7 +837,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
SPA_PROP_INFO_type, SPA_POD_String(p->wav_path),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 27:
case 30:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("channelmix.lock-volumes"),
@ -817,7 +845,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->lock_volumes),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 28:
case 31:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph.disable"),
@ -825,7 +853,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->filter_graph_disabled),
SPA_PROP_INFO_params, SPA_POD_Bool(true));
break;
case 29:
case 32:
*param = spa_pod_builder_add_object(b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph.N"),
@ -836,7 +864,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index,
default:
if (this->filter_graph[0] && this->filter_graph[0]->graph) {
return spa_filter_graph_enum_prop_info(this->filter_graph[0]->graph,
index - 30, b, param);
index - 33, b, param);
}
return 0;
}
@ -903,6 +931,12 @@ static int node_param_props(struct impl *this, uint32_t id, uint32_t index,
spa_pod_builder_float(b, this->mix.rear_delay);
spa_pod_builder_string(b, "channelmix.stereo-widen");
spa_pod_builder_float(b, this->mix.widen);
spa_pod_builder_string(b, "channelmix.center-level");
spa_pod_builder_float(b, this->mix.center_level);
spa_pod_builder_string(b, "channelmix.surround-level");
spa_pod_builder_float(b, this->mix.surround_level);
spa_pod_builder_string(b, "channelmix.lfe-level");
spa_pod_builder_float(b, this->mix.lfe_level);
spa_pod_builder_string(b, "channelmix.hilbert-taps");
spa_pod_builder_int(b, this->mix.hilbert_taps);
spa_pod_builder_string(b, "channelmix.upmix-method");
@ -1490,6 +1524,12 @@ static int audioconvert_set_param(struct impl *this, const char *k, const char *
spa_atof(s, &this->mix.rear_delay);
else if (spa_streq(k, "channelmix.stereo-widen"))
spa_atof(s, &this->mix.widen);
else if (spa_streq(k, "channelmix.center-level"))
spa_atof(s, &this->mix.center_level);
else if (spa_streq(k, "channelmix.surround-level"))
spa_atof(s, &this->mix.surround_level);
else if (spa_streq(k, "channelmix.lfe-level"))
spa_atof(s, &this->mix.lfe_level);
else if (spa_streq(k, "channelmix.hilbert-taps"))
spa_atou32(s, &this->mix.hilbert_taps, 0);
else if (spa_streq(k, "channelmix.upmix-method"))
@ -4320,13 +4360,7 @@ impl_init(const struct spa_handle_factory *factory,
this->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
this->rate_limit.burst = 1;
this->mix.options = CHANNELMIX_OPTION_UPMIX | CHANNELMIX_OPTION_MIX_LFE;
this->mix.upmix = CHANNELMIX_UPMIX_NONE;
this->mix.log = this->log;
this->mix.lfe_cutoff = 0.0f;
this->mix.fc_cutoff = 0.0f;
this->mix.rear_delay = 0.0f;
this->mix.widen = 0.0f;
channelmix_reset(&this->mix);
for (i = 0; info && i < info->n_items; i++) {
const char *k = info->items[i].key;

View file

@ -198,9 +198,9 @@ static int make_matrix(struct channelmix *mix)
uint32_t dst_chan = mix->dst_chan;
uint64_t unassigned, keep;
uint32_t i, j, ic, jc, matrix_encoding = MATRIX_NORMAL;
float clev = SQRT1_2;
float slev = SQRT1_2;
float llev = 0.5f;
float clev = mix->center_level;
float slev = mix->surround_level;
float llev = mix->lfe_level;
float maxsum = 0.0f;
bool filter_fc = false, filter_lfe = false, matched = false, normalize;
#define _MATRIX(s,d) matrix[_CH(s)][_CH(d)]
@ -874,6 +874,21 @@ static void impl_channelmix_free(struct channelmix *mix)
mix->process = NULL;
}
void channelmix_reset(struct channelmix *mix)
{
spa_zero(*mix);
mix->options = CHANNELMIX_DEFAULT_OPTIONS;
mix->upmix = CHANNELMIX_DEFAULT_UPMIX;
mix->lfe_cutoff = CHANNELMIX_DEFAULT_LFE_CUTOFF;
mix->fc_cutoff = CHANNELMIX_DEFAULT_FC_CUTOFF;
mix->rear_delay = CHANNELMIX_DEFAULT_REAR_DELAY;
mix->center_level = CHANNELMIX_DEFAULT_CENTER_LEVEL;
mix->surround_level = CHANNELMIX_DEFAULT_SURROUND_LEVEL;
mix->lfe_level = CHANNELMIX_DEFAULT_LFE_LEVEL;
mix->widen = CHANNELMIX_DEFAULT_WIDEN;
mix->hilbert_taps = CHANNELMIX_DEFAULT_HILBERT_TAPS;
}
int channelmix_init(struct channelmix *mix)
{
const struct channelmix_info *info;

View file

@ -28,6 +28,17 @@
#define CHANNELMIX_OPS_MAX_ALIGN 16
#define CHANNELMIX_DEFAULT_OPTIONS (CHANNELMIX_OPTION_UPMIX | CHANNELMIX_OPTION_MIX_LFE)
#define CHANNELMIX_DEFAULT_UPMIX CHANNELMIX_UPMIX_NONE
#define CHANNELMIX_DEFAULT_LFE_CUTOFF 0.0f
#define CHANNELMIX_DEFAULT_FC_CUTOFF 0.0f
#define CHANNELMIX_DEFAULT_REAR_DELAY 0.0f
#define CHANNELMIX_DEFAULT_CENTER_LEVEL 0.707106781f
#define CHANNELMIX_DEFAULT_SURROUND_LEVEL 0.707106781f
#define CHANNELMIX_DEFAULT_LFE_LEVEL 0.5f
#define CHANNELMIX_DEFAULT_WIDEN 0.0f
#define CHANNELMIX_DEFAULT_HILBERT_TAPS 0
struct channelmix {
uint32_t src_chan;
uint32_t dst_chan;
@ -60,6 +71,9 @@ struct channelmix {
float fc_cutoff; /* in Hz, 0 is disabled */
float rear_delay; /* in ms, 0 is disabled */
float widen; /* stereo widen. 0 is disabled */
float center_level; /* center down/upmix level, sqrt(1/2) */
float lfe_level; /* lfe down/upmix level, 1/2 */
float surround_level; /* surround down/upmix level, sqrt(1/2) */
uint32_t hilbert_taps; /* to phase shift, 0 disabled */
struct lr4 lr4[MAX_CHANNELS];
@ -80,6 +94,7 @@ struct channelmix {
void *data;
};
void channelmix_reset(struct channelmix *mix);
int channelmix_init(struct channelmix *mix);
static const struct channelmix_upmix_info {

View file

@ -54,7 +54,7 @@ static int setup_context(struct context *ctx)
size_t size;
int res;
struct spa_support support[1];
struct spa_dict_item items[6];
struct spa_dict_item items[9];
const struct spa_handle_factory *factory;
void *iface;
@ -76,10 +76,13 @@ static int setup_context(struct context *ctx)
items[3] = SPA_DICT_ITEM_INIT("channelmix.lfe-cutoff", "150");
items[4] = SPA_DICT_ITEM_INIT("channelmix.fc-cutoff", "12000");
items[5] = SPA_DICT_ITEM_INIT("channelmix.rear-delay", "12.0");
items[6] = SPA_DICT_ITEM_INIT("channelmix.center-level", "0.707106781");
items[7] = SPA_DICT_ITEM_INIT("channelmix.surround-level", "0.707106781");
items[8] = SPA_DICT_ITEM_INIT("channelmix.lfe-level", "0.5");
res = spa_handle_factory_init(factory,
ctx->convert_handle,
&SPA_DICT_INIT(items, 6),
&SPA_DICT_INIT(items, 9),
support, 1);
spa_assert_se(res >= 0);

View file

@ -45,7 +45,7 @@ static void test_mix(uint32_t src_chan, uint32_t src_mask, uint32_t dst_chan, ui
spa_log_debug(&logger.log, "start %d->%d (%08x -> %08x)", src_chan, dst_chan, src_mask, dst_mask);
spa_zero(mix);
channelmix_reset(&mix);
mix.options = options;
mix.src_chan = src_chan;
mix.dst_chan = dst_chan;
@ -340,7 +340,7 @@ static void test_n_m_impl(void)
src[i] = src_data[i];
}
spa_zero(mix);
channelmix_reset(&mix);
mix.src_chan = 16;
mix.dst_chan = 12;
mix.log = &logger.log;

View file

@ -101,6 +101,9 @@ stream.properties = {
#channelmix.lfe-cutoff = 150
#channelmix.fc-cutoff = 12000
#channelmix.rear-delay = 12.0
#channelmix.center-level = 0.707106781
#channelmix.surround-level = 0.707106781
#channelmix.lfe-level = 0.5
#channelmix.stereo-widen = 0.0
#channelmix.hilbert-taps = 0
#dither.noise = 0

View file

@ -332,6 +332,9 @@ context.objects = [
#channelmix.fc-cutoff = 12000
#channelmix.rear-delay = 12.0
#channelmix.stereo-widen = 0.0
#channelmix.center-level = 0.707106781
#channelmix.surround-level = 0.707106781
#channelmix.lfe-level = 0.5
#channelmix.hilbert-taps = 0
#channelmix.disable = false
#dither.noise = 0

View file

@ -60,6 +60,9 @@ stream.properties = {
#channelmix.fc-cutoff = 6000
#channelmix.rear-delay = 12.0
#channelmix.stereo-widen = 0.1
#channelmix.center-level = 0.707106781
#channelmix.surround-level = 0.707106781
#channelmix.lfe-level = 0.5
#channelmix.hilbert-taps = 0
}

View file

@ -88,6 +88,9 @@ stream.properties = {
#channelmix.fc-cutoff = 12000
#channelmix.rear-delay = 12.0
#channelmix.stereo-widen = 0.0
#channelmix.center-level = 0.707106781
#channelmix.surround-level = 0.707106781
#channelmix.lfe-level = 0.5
#channelmix.hilbert-taps = 0
#dither.noise = 0
}