mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-11-04 13:30:12 -05:00
filter-chain: add parametric EQ builtin plugin
add param_eq which can take an EQ file or a config list of biquad filters. It is potentially more efficient to run this than a chain of biquads.
This commit is contained in:
parent
ddbe135a3b
commit
ab20cc5f28
2 changed files with 307 additions and 1 deletions
|
|
@ -233,6 +233,75 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
|
||||||
* }
|
* }
|
||||||
*\endcode
|
*\endcode
|
||||||
*
|
*
|
||||||
|
* ### Parametric EQ
|
||||||
|
*
|
||||||
|
* The parametric EQ chains a number of biquads together. It is more efficient than
|
||||||
|
* specifying a number of chained biquads and it can also load configuration from a
|
||||||
|
* file.
|
||||||
|
*
|
||||||
|
*\code{.unparsed}
|
||||||
|
* filter.graph = {
|
||||||
|
* nodes = [
|
||||||
|
* {
|
||||||
|
* type = builtin
|
||||||
|
* name = ...
|
||||||
|
* label = param_eq
|
||||||
|
* config = {
|
||||||
|
* filename = "..."
|
||||||
|
* filters = [
|
||||||
|
* { type = ..., freq = ..., gain = ..., q = ... },
|
||||||
|
* { type = ..., freq = ..., gain = ..., q = ... },
|
||||||
|
* ....
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*\endcode
|
||||||
|
*
|
||||||
|
* Either a `filename` or a `filters` array can be specified.
|
||||||
|
*
|
||||||
|
* The `filename` must point to a parametric equalizer configuration
|
||||||
|
* generated from the AutoEQ project or Squiglink. Both the projects allow
|
||||||
|
* equalizing headphones or an in-ear monitor to a target curve.
|
||||||
|
*
|
||||||
|
* A popular example of the above being EQ'ing to the Harman target curve
|
||||||
|
* or EQ'ing one headphone/IEM to another.
|
||||||
|
*
|
||||||
|
* For AutoEQ, see https://github.com/jaakkopasanen/AutoEq.
|
||||||
|
* For SquigLink, see https://squig.link/.
|
||||||
|
*
|
||||||
|
* Parametric equalizer configuration generated from AutoEQ or Squiglink looks
|
||||||
|
* like below.
|
||||||
|
*
|
||||||
|
* \code{.unparsed}
|
||||||
|
* Preamp: -6.8 dB
|
||||||
|
* Filter 1: ON PK Fc 21 Hz Gain 6.7 dB Q 1.100
|
||||||
|
* Filter 2: ON PK Fc 85 Hz Gain 6.9 dB Q 3.000
|
||||||
|
* Filter 3: ON PK Fc 110 Hz Gain -2.6 dB Q 2.700
|
||||||
|
* Filter 4: ON PK Fc 210 Hz Gain 5.9 dB Q 2.100
|
||||||
|
* Filter 5: ON PK Fc 710 Hz Gain -1.0 dB Q 0.600
|
||||||
|
* Filter 6: ON PK Fc 1600 Hz Gain 2.3 dB Q 2.700
|
||||||
|
* \endcode
|
||||||
|
*
|
||||||
|
* Fc, Gain and Q specify the frequency, gain and Q factor respectively.
|
||||||
|
* The fourth column can be one of PK, LSC or HSC specifying peaking, low
|
||||||
|
* shelf and high shelf filter respectively. More often than not only peaking
|
||||||
|
* filters are involved.
|
||||||
|
*
|
||||||
|
* The `filters` can contain an array of filter specification object with the following
|
||||||
|
* keys:
|
||||||
|
*
|
||||||
|
* `type` specifies the filter type, choose one from the available biquad labels.
|
||||||
|
* `freq` is the frequency passed to the biquad.
|
||||||
|
* `gain` is the gain passed to the biquad.
|
||||||
|
* `q` is the Q passed to the biquad.
|
||||||
|
*
|
||||||
|
* This makes it possible to also use the param eq without a file and with all the
|
||||||
|
* available biquads.
|
||||||
|
*
|
||||||
* ### Convolver
|
* ### Convolver
|
||||||
*
|
*
|
||||||
* The convolver can be used to apply an impulse response to a signal. It is usually used
|
* The convolver can be used to apply an impulse response to a signal. It is usually used
|
||||||
|
|
|
||||||
|
|
@ -633,7 +633,7 @@ static const struct fc_descriptor bq_raw_desc = {
|
||||||
/** convolve */
|
/** convolve */
|
||||||
struct convolver_impl {
|
struct convolver_impl {
|
||||||
unsigned long rate;
|
unsigned long rate;
|
||||||
float *port[64];
|
float *port[2];
|
||||||
|
|
||||||
struct convolver *conv;
|
struct convolver *conv;
|
||||||
};
|
};
|
||||||
|
|
@ -1668,6 +1668,241 @@ static const struct fc_descriptor sine_desc = {
|
||||||
.cleanup = builtin_cleanup,
|
.cleanup = builtin_cleanup,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#define PARAM_EQ_NUM_PORTS 2
|
||||||
|
static struct fc_port param_eq_ports[] = {
|
||||||
|
{ .index = 0,
|
||||||
|
.name = "Out",
|
||||||
|
.flags = FC_PORT_OUTPUT | FC_PORT_AUDIO,
|
||||||
|
},
|
||||||
|
{ .index = 1,
|
||||||
|
.name = "In",
|
||||||
|
.flags = FC_PORT_INPUT | FC_PORT_AUDIO,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#define PARAM_EQ_MAX 128
|
||||||
|
struct param_eq_impl {
|
||||||
|
unsigned long rate;
|
||||||
|
float *port[2];
|
||||||
|
|
||||||
|
uint32_t n_bq;
|
||||||
|
struct biquad bq[PARAM_EQ_MAX];
|
||||||
|
};
|
||||||
|
|
||||||
|
static int load_eq_bands(struct param_eq_impl *impl, const char *filename)
|
||||||
|
{
|
||||||
|
FILE *f = NULL;
|
||||||
|
char *line = NULL;
|
||||||
|
ssize_t nread;
|
||||||
|
size_t linelen, n = 0;
|
||||||
|
uint32_t freq;
|
||||||
|
char filter_type[4];
|
||||||
|
char filter[4];
|
||||||
|
char q[7], gain[7];
|
||||||
|
float vg, vq;
|
||||||
|
int res = 0;
|
||||||
|
|
||||||
|
if ((f = fopen(filename, "r")) == NULL) {
|
||||||
|
res = -errno;
|
||||||
|
pw_log_error("failed to open param_eq file '%s': %m", filename);
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Read the Preamp gain line.
|
||||||
|
* Example: Preamp: -6.8 dB
|
||||||
|
*
|
||||||
|
* When a pre-amp gain is required, which is usually the case when
|
||||||
|
* applying EQ, we need to modify the first EQ band to apply a
|
||||||
|
* bq_highshelf filter at frequency 0 Hz with the provided negative
|
||||||
|
* gain.
|
||||||
|
*
|
||||||
|
* Pre-amp gain is always negative to offset the effect of possible
|
||||||
|
* clipping introduced by the amplification resulting from EQ.
|
||||||
|
*/
|
||||||
|
nread = getline(&line, &linelen, f);
|
||||||
|
if (nread != -1 && sscanf(line, "%*s %6s %*s", gain) == 1) {
|
||||||
|
if (spa_json_parse_float(gain, strlen(gain), &vg))
|
||||||
|
biquad_set(&impl->bq[impl->n_bq++], BQ_HIGHSHELF, 0.0f, 1.0f, vg);
|
||||||
|
}
|
||||||
|
/* Read the filter bands */
|
||||||
|
while ((nread = getline(&line, &linelen, f)) != -1) {
|
||||||
|
if (n == PARAM_EQ_MAX) {
|
||||||
|
res = -ENOSPC;
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* On field widths:
|
||||||
|
* - filter can be ON or OFF
|
||||||
|
* - filter type can be PK, LSC, HSC
|
||||||
|
* - freq can be at most 5 decimal digits
|
||||||
|
* - gain can be -xy.z
|
||||||
|
* - Q can be x.y00
|
||||||
|
*
|
||||||
|
* Use a field width of 6 for gain and Q to account for any
|
||||||
|
* possible zeros.
|
||||||
|
*/
|
||||||
|
if (sscanf(line, "%*s %*d: %3s %3s %*s %5d %*s %*s %6s %*s %*c %6s",
|
||||||
|
filter, filter_type, &freq, gain, q) == 5) {
|
||||||
|
if (strcmp(filter, "ON") == 0) {
|
||||||
|
int type;
|
||||||
|
|
||||||
|
if (spa_streq(filter_type, "PK"))
|
||||||
|
type = BQ_PEAKING;
|
||||||
|
else if (spa_streq(filter_type, "LSC"))
|
||||||
|
type = BQ_LOWSHELF;
|
||||||
|
else if (spa_streq(filter_type, "HSC"))
|
||||||
|
type = BQ_HIGHSHELF;
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (spa_json_parse_float(gain, strlen(gain), &vg) &&
|
||||||
|
spa_json_parse_float(q, strlen(q), &vq))
|
||||||
|
biquad_set(&impl->bq[impl->n_bq++], type, freq * 2.0f / impl->rate, vq, vg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit:
|
||||||
|
if (f)
|
||||||
|
fclose(f);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* {
|
||||||
|
* filename = "...",
|
||||||
|
* filters = [
|
||||||
|
* { type=bq_peaking freq=21 gain=6.7 q=1.100 }
|
||||||
|
* { type=bq_peaking freq=85 gain=6.9 q=3.000 }
|
||||||
|
* { type=bq_peaking freq=110 gain=-2.6 q=2.700 }
|
||||||
|
* { type=bq_peaking freq=210 gain=5.9 q=2.100 }
|
||||||
|
* { type=bq_peaking freq=710 gain=-1.0 q=0.600 }
|
||||||
|
* { type=bq_peaking freq=1600 gain=2.3 q=2.700 }
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
|
||||||
|
static void *param_eq_instantiate(const struct fc_descriptor * Descriptor,
|
||||||
|
unsigned long SampleRate, int index, const char *config)
|
||||||
|
{
|
||||||
|
struct spa_json it[3];
|
||||||
|
const char *val;
|
||||||
|
char key[256], filename[PATH_MAX];
|
||||||
|
char type_str[17];
|
||||||
|
int len, res;
|
||||||
|
struct param_eq_impl *impl;
|
||||||
|
|
||||||
|
if (config == NULL) {
|
||||||
|
pw_log_error("param_eq: requires a config section");
|
||||||
|
errno = EINVAL;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) {
|
||||||
|
pw_log_error("param_eq: config must be an object");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl = calloc(1, sizeof(*impl));
|
||||||
|
if (impl == NULL)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
impl->rate = SampleRate;
|
||||||
|
|
||||||
|
while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
|
||||||
|
if (spa_streq(key, "filename")) {
|
||||||
|
if (spa_json_parse_stringn(val, len, filename, sizeof(filename)) <= 0) {
|
||||||
|
pw_log_error("param_eq: filename requires a string");
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
res = load_eq_bands(impl, filename);
|
||||||
|
if (res < 0) {
|
||||||
|
pw_log_error("failed to parse param_eq configuration from %s", filename);
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (spa_streq(key, "filters")) {
|
||||||
|
if (!spa_json_is_array(val, len)) {
|
||||||
|
pw_log_error("param_eq:filters require an array");
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
spa_json_enter(&it[0], &it[1]);
|
||||||
|
while (spa_json_enter_object(&it[1], &it[2]) > 0) {
|
||||||
|
float freq = 0.0f, gain = 0.0f, q = 1.0f;
|
||||||
|
int type = BQ_NONE;
|
||||||
|
|
||||||
|
while ((len = spa_json_object_next(&it[2], key, sizeof(key), &val)) > 0) {
|
||||||
|
if (spa_streq(key, "type")) {
|
||||||
|
if (spa_json_parse_stringn(val, len, type_str, sizeof(type_str)) <= 0) {
|
||||||
|
pw_log_error("param_eq:type requires a string");
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
type = bq_type_from_name(type_str);
|
||||||
|
}
|
||||||
|
else if (spa_streq(key, "freq")) {
|
||||||
|
if (spa_json_parse_float(val, len, &freq) <= 0) {
|
||||||
|
pw_log_error("param_eq:rate requires a number");
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (spa_streq(key, "q")) {
|
||||||
|
if (spa_json_parse_float(val, len, &q) <= 0) {
|
||||||
|
pw_log_error("param_eq:q requires a float");
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (spa_streq(key, "gain")) {
|
||||||
|
if (spa_json_parse_float(val, len, &gain) <= 0) {
|
||||||
|
pw_log_error("param_eq:gain requires a float");
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pw_log_warn("param_eq: ignoring filter key: '%s'", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
biquad_set(&impl->bq[impl->n_bq++], type, freq * 2 / impl->rate, q, gain);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pw_log_warn("delay: ignoring config key: '%s'", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pw_log_info("loaded %d biquads", impl->n_bq);
|
||||||
|
return impl;
|
||||||
|
error:
|
||||||
|
free(impl);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void param_eq_connect_port(void * Instance, unsigned long Port,
|
||||||
|
float * DataLocation)
|
||||||
|
{
|
||||||
|
struct param_eq_impl *impl = Instance;
|
||||||
|
impl->port[Port] = DataLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void param_eq_run(void * Instance, unsigned long SampleCount)
|
||||||
|
{
|
||||||
|
struct param_eq_impl *impl = Instance;
|
||||||
|
float *in = impl->port[1];
|
||||||
|
float *out = impl->port[0];
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < impl->n_bq; i++) {
|
||||||
|
dsp_ops_biquad_run(dsp_ops, &impl->bq[i], out, in, SampleCount);
|
||||||
|
in = out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct fc_descriptor param_eq_desc = {
|
||||||
|
.name = "param_eq",
|
||||||
|
|
||||||
|
.n_ports = PARAM_EQ_NUM_PORTS,
|
||||||
|
.ports = param_eq_ports,
|
||||||
|
|
||||||
|
.instantiate = param_eq_instantiate,
|
||||||
|
.connect_port = param_eq_connect_port,
|
||||||
|
.run = param_eq_run,
|
||||||
|
.cleanup = free,
|
||||||
|
};
|
||||||
|
|
||||||
static const struct fc_descriptor * builtin_descriptor(unsigned long Index)
|
static const struct fc_descriptor * builtin_descriptor(unsigned long Index)
|
||||||
{
|
{
|
||||||
switch(Index) {
|
switch(Index) {
|
||||||
|
|
@ -1713,6 +1948,8 @@ static const struct fc_descriptor * builtin_descriptor(unsigned long Index)
|
||||||
return &mult_desc;
|
return &mult_desc;
|
||||||
case 20:
|
case 20:
|
||||||
return &sine_desc;
|
return &sine_desc;
|
||||||
|
case 21:
|
||||||
|
return ¶m_eq_desc;
|
||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue