mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
filter-chain: add ebur128 filter
The EBU R128 filter measures the signal and generates LUFS control notifications for further processing. It also adds a plugin that can convert LUFS to a gain (based on a target LUFS). Also add an example filter-chain to enable the EBU R128 measurement and how to use the results to adjust the volume dynamically. See #2286 #222 #2210
This commit is contained in:
parent
f0f9fbb009
commit
df271d13f3
6 changed files with 792 additions and 3 deletions
|
|
@ -371,3 +371,7 @@ option('doc-sysconfdir-value',
|
|||
description : 'Sysconf data directory to show in documentation instead of the actual value.',
|
||||
type : 'string',
|
||||
value : '')
|
||||
option('ebur128',
|
||||
description: 'Enable code that depends on ebur128',
|
||||
type: 'feature',
|
||||
value: 'auto')
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@ if get_option('spa-plugins').allowed()
|
|||
lilv_lib = dependency('lilv-0', required: get_option('lv2'))
|
||||
summary({'lilv (for lv2 plugins)': lilv_lib.found()}, bool_yn: true, section: 'filter-graph')
|
||||
|
||||
ebur128_lib = dependency('libebur128', required: get_option('ebur128').enabled())
|
||||
summary({'EBUR128': ebur128_lib.found()}, bool_yn: true, section: 'filter-graph')
|
||||
|
||||
cdata.set('HAVE_SPA_PLUGINS', '1')
|
||||
subdir('plugins')
|
||||
endif
|
||||
|
|
|
|||
631
spa/plugins/filter-graph/ebur128_plugin.c
Normal file
631
spa/plugins/filter-graph/ebur128_plugin.c
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
#include "config.h"
|
||||
|
||||
#include <limits.h>
|
||||
|
||||
#include <spa/utils/json.h>
|
||||
#include <spa/support/log.h>
|
||||
|
||||
#include "audio-plugin.h"
|
||||
#include "audio-dsp.h"
|
||||
|
||||
#include <ebur128.h>
|
||||
|
||||
struct plugin {
|
||||
struct spa_handle handle;
|
||||
struct spa_fga_plugin plugin;
|
||||
|
||||
struct spa_fga_dsp *dsp;
|
||||
struct spa_log *log;
|
||||
uint32_t quantum_limit;
|
||||
};
|
||||
|
||||
enum {
|
||||
PORT_IN_FL,
|
||||
PORT_IN_FR,
|
||||
PORT_IN_FC,
|
||||
PORT_IN_UNUSED,
|
||||
PORT_IN_SL,
|
||||
PORT_IN_SR,
|
||||
PORT_IN_DUAL_MONO,
|
||||
|
||||
PORT_OUT_FL,
|
||||
PORT_OUT_FR,
|
||||
PORT_OUT_FC,
|
||||
PORT_OUT_UNUSED,
|
||||
PORT_OUT_SL,
|
||||
PORT_OUT_SR,
|
||||
PORT_OUT_DUAL_MONO,
|
||||
|
||||
PORT_OUT_MOMENTARY,
|
||||
PORT_OUT_SHORTTERM,
|
||||
PORT_OUT_GLOBAL,
|
||||
PORT_OUT_WINDOW,
|
||||
PORT_OUT_RANGE,
|
||||
PORT_OUT_PEAK,
|
||||
PORT_OUT_TRUE_PEAK,
|
||||
|
||||
PORT_MAX,
|
||||
|
||||
PORT_IN_START = PORT_IN_FL,
|
||||
PORT_OUT_START = PORT_OUT_FL,
|
||||
PORT_NOTIFY_START = PORT_OUT_MOMENTARY,
|
||||
};
|
||||
|
||||
|
||||
struct ebur128_impl {
|
||||
struct plugin *plugin;
|
||||
|
||||
struct spa_fga_dsp *dsp;
|
||||
struct spa_log *log;
|
||||
|
||||
unsigned long rate;
|
||||
float *port[PORT_MAX];
|
||||
|
||||
unsigned int max_history;
|
||||
unsigned int max_window;
|
||||
bool use_histogram;
|
||||
|
||||
ebur128_state *st[7];
|
||||
};
|
||||
|
||||
static void * ebur128_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
|
||||
unsigned long SampleRate, int index, const char *config)
|
||||
{
|
||||
struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
|
||||
struct ebur128_impl *impl;
|
||||
struct spa_json it[1];
|
||||
const char *val;
|
||||
char key[256];
|
||||
int len;
|
||||
float f;
|
||||
|
||||
impl = calloc(1, sizeof(*impl));
|
||||
if (impl == NULL) {
|
||||
errno = ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
impl->plugin = pl;
|
||||
impl->dsp = pl->dsp;
|
||||
impl->log = pl->log;
|
||||
impl->max_history = 10000;
|
||||
impl->max_window = 0;
|
||||
impl->rate = SampleRate;
|
||||
|
||||
if (config == NULL)
|
||||
return impl;
|
||||
|
||||
if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) {
|
||||
spa_log_error(pl->log, "ebur128: expected object in config");
|
||||
errno = EINVAL;
|
||||
goto error;
|
||||
}
|
||||
while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) {
|
||||
if (spa_streq(key, "max-history")) {
|
||||
if (spa_json_parse_float(val, len, &f) <= 0) {
|
||||
spa_log_error(impl->log, "ebur128:max-history requires a number");
|
||||
errno = EINVAL;
|
||||
goto error;
|
||||
}
|
||||
impl->max_history = (unsigned int) (f * 1000.0f);
|
||||
}
|
||||
else if (spa_streq(key, "max-window")) {
|
||||
if (spa_json_parse_float(val, len, &f) <= 0) {
|
||||
spa_log_error(impl->log, "ebur128:max-window requires a number");
|
||||
errno = EINVAL;
|
||||
goto error;
|
||||
}
|
||||
impl->max_window = (unsigned int) (f * 1000.0f);
|
||||
}
|
||||
else if (spa_streq(key, "use-histogram")) {
|
||||
if (spa_json_parse_bool(val, len, &impl->use_histogram) <= 0) {
|
||||
spa_log_error(impl->log, "ebur128:use-histogram requires a boolean");
|
||||
errno = EINVAL;
|
||||
goto error;
|
||||
}
|
||||
} else {
|
||||
spa_log_warn(impl->log, "ebur128: unknown key %s", key);
|
||||
}
|
||||
}
|
||||
return impl;
|
||||
error:
|
||||
free(impl);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void ebur128_run(void * Instance, unsigned long SampleCount)
|
||||
{
|
||||
struct ebur128_impl *impl = Instance;
|
||||
int i, c;
|
||||
double value;
|
||||
ebur128_state *st[7];
|
||||
|
||||
for (i = 0; i < 7; i++) {
|
||||
float *in = impl->port[PORT_IN_START + i];
|
||||
float *out = impl->port[PORT_OUT_START + i];
|
||||
|
||||
st[i] = NULL;
|
||||
if (in == NULL)
|
||||
continue;
|
||||
|
||||
st[i] = impl->st[i];
|
||||
if (st[i] != NULL)
|
||||
ebur128_add_frames_float(st[i], in, SampleCount);
|
||||
|
||||
if (out != NULL)
|
||||
memcpy(out, in, SampleCount * sizeof(float));
|
||||
}
|
||||
if (impl->port[PORT_OUT_MOMENTARY] != NULL) {
|
||||
double sum = 0.0;
|
||||
for (i = 0, c = 0; i < 7; i++) {
|
||||
if (st[i] != NULL) {
|
||||
ebur128_loudness_momentary(st[i], &value);
|
||||
sum += value;
|
||||
c++;
|
||||
}
|
||||
}
|
||||
impl->port[PORT_OUT_MOMENTARY][0] = (float) (sum / c);
|
||||
}
|
||||
if (impl->port[PORT_OUT_SHORTTERM] != NULL) {
|
||||
double sum = 0.0;
|
||||
for (i = 0, c = 0; i < 7; i++) {
|
||||
if (st[i] != NULL) {
|
||||
ebur128_loudness_shortterm(st[i], &value);
|
||||
sum += value;
|
||||
c++;
|
||||
}
|
||||
}
|
||||
impl->port[PORT_OUT_SHORTTERM][0] = (float) (sum / c);
|
||||
}
|
||||
if (impl->port[PORT_OUT_GLOBAL] != NULL) {
|
||||
ebur128_loudness_global_multiple(st, 7, &value);
|
||||
impl->port[PORT_OUT_GLOBAL][0] = (float)value;
|
||||
}
|
||||
if (impl->port[PORT_OUT_WINDOW] != NULL) {
|
||||
double sum = 0.0;
|
||||
for (i = 0, c = 0; i < 7; i++) {
|
||||
if (st[i] != NULL) {
|
||||
ebur128_loudness_window(st[i], impl->max_window, &value);
|
||||
sum += value;
|
||||
c++;
|
||||
}
|
||||
}
|
||||
impl->port[PORT_OUT_WINDOW][0] = (float) (sum / c);
|
||||
}
|
||||
if (impl->port[PORT_OUT_RANGE] != NULL) {
|
||||
ebur128_loudness_range_multiple(st, 7, &value);
|
||||
impl->port[PORT_OUT_RANGE][0] = (float)value;
|
||||
}
|
||||
if (impl->port[PORT_OUT_PEAK] != NULL) {
|
||||
double max = 0.0;
|
||||
for (i = 0; i < 7; i++) {
|
||||
if (st[i] != NULL) {
|
||||
ebur128_sample_peak(st[i], i, &value);
|
||||
max = SPA_MAX(max, value);
|
||||
}
|
||||
}
|
||||
impl->port[PORT_OUT_PEAK][0] = (float) max;
|
||||
}
|
||||
if (impl->port[PORT_OUT_TRUE_PEAK] != NULL) {
|
||||
double max = 0.0;
|
||||
for (i = 0; i < 7; i++) {
|
||||
if (st[i] != NULL) {
|
||||
ebur128_true_peak(st[i], i, &value);
|
||||
max = SPA_MAX(max, value);
|
||||
}
|
||||
}
|
||||
impl->port[PORT_OUT_TRUE_PEAK][0] = (float) max;
|
||||
}
|
||||
}
|
||||
|
||||
static void ebur128_connect_port(void * Instance, unsigned long Port,
|
||||
float * DataLocation)
|
||||
{
|
||||
struct ebur128_impl *impl = Instance;
|
||||
if (Port < PORT_MAX)
|
||||
impl->port[Port] = DataLocation;
|
||||
}
|
||||
|
||||
static void ebur128_cleanup(void * Instance)
|
||||
{
|
||||
struct ebur128_impl *impl = Instance;
|
||||
free(impl);
|
||||
}
|
||||
|
||||
static void ebur128_activate(void * Instance)
|
||||
{
|
||||
struct ebur128_impl *impl = Instance;
|
||||
int mode = 0, i;
|
||||
int modes[] = {
|
||||
EBUR128_MODE_M,
|
||||
EBUR128_MODE_S,
|
||||
EBUR128_MODE_I,
|
||||
0,
|
||||
EBUR128_MODE_LRA,
|
||||
EBUR128_MODE_SAMPLE_PEAK,
|
||||
EBUR128_MODE_TRUE_PEAK,
|
||||
};
|
||||
enum channel channels[] = {
|
||||
EBUR128_LEFT,
|
||||
EBUR128_RIGHT,
|
||||
EBUR128_CENTER,
|
||||
EBUR128_UNUSED,
|
||||
EBUR128_LEFT_SURROUND,
|
||||
EBUR128_RIGHT_SURROUND,
|
||||
EBUR128_DUAL_MONO,
|
||||
};
|
||||
|
||||
if (impl->use_histogram)
|
||||
mode |= EBUR128_MODE_HISTOGRAM;
|
||||
|
||||
/* check modes */
|
||||
for (i = 0; i < 7; i++) {
|
||||
if (impl->port[PORT_NOTIFY_START + i] != NULL)
|
||||
mode |= modes[i];
|
||||
}
|
||||
|
||||
for (i = 0; i < 7; i++) {
|
||||
impl->st[i] = ebur128_init(1, impl->rate, mode);
|
||||
if (impl->st[i]) {
|
||||
ebur128_set_channel(impl->st[i], i, channels[i]);
|
||||
ebur128_set_max_history(impl->st[i], impl->max_history);
|
||||
ebur128_set_max_window(impl->st[i], impl->max_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ebur128_deactivate(void * Instance)
|
||||
{
|
||||
struct ebur128_impl *impl = Instance;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 7; i++) {
|
||||
if (impl->st[i] != NULL)
|
||||
ebur128_destroy(&impl->st[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static struct spa_fga_port ebur128_ports[] = {
|
||||
{ .index = PORT_IN_FL,
|
||||
.name = "In FL",
|
||||
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_IN_FR,
|
||||
.name = "In FR",
|
||||
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_IN_FC,
|
||||
.name = "In FC",
|
||||
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_IN_UNUSED,
|
||||
.name = "In UNUSED",
|
||||
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_IN_SL,
|
||||
.name = "In SL",
|
||||
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_IN_SR,
|
||||
.name = "In SR",
|
||||
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_IN_DUAL_MONO,
|
||||
.name = "In DUAL MONO",
|
||||
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
|
||||
{ .index = PORT_OUT_FL,
|
||||
.name = "Out FL",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_OUT_FR,
|
||||
.name = "Out FR",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_OUT_FC,
|
||||
.name = "Out FC",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_OUT_UNUSED,
|
||||
.name = "Out UNUSED",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_OUT_SL,
|
||||
.name = "Out SL",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_OUT_SR,
|
||||
.name = "Out SR",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
{ .index = PORT_OUT_DUAL_MONO,
|
||||
.name = "Out DUAL MONO",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO,
|
||||
},
|
||||
|
||||
{ .index = PORT_OUT_MOMENTARY,
|
||||
.name = "Momentary LUFS",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
|
||||
},
|
||||
{ .index = PORT_OUT_SHORTTERM,
|
||||
.name = "Shorttem LUFS",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
|
||||
},
|
||||
{ .index = PORT_OUT_GLOBAL,
|
||||
.name = "Global LUFS",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
|
||||
},
|
||||
{ .index = PORT_OUT_WINDOW,
|
||||
.name = "Window LUFS",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
|
||||
},
|
||||
{ .index = PORT_OUT_RANGE,
|
||||
.name = "Range LU",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
|
||||
},
|
||||
{ .index = PORT_OUT_PEAK,
|
||||
.name = "Peak",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
|
||||
},
|
||||
{ .index = PORT_OUT_TRUE_PEAK,
|
||||
.name = "True Peak",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
|
||||
},
|
||||
};
|
||||
|
||||
static const struct spa_fga_descriptor ebur128_desc = {
|
||||
.name = "ebur128",
|
||||
.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
|
||||
|
||||
.ports = ebur128_ports,
|
||||
.n_ports = SPA_N_ELEMENTS(ebur128_ports),
|
||||
|
||||
.instantiate = ebur128_instantiate,
|
||||
.connect_port = ebur128_connect_port,
|
||||
.activate = ebur128_activate,
|
||||
.deactivate = ebur128_deactivate,
|
||||
.run = ebur128_run,
|
||||
.cleanup = ebur128_cleanup,
|
||||
};
|
||||
|
||||
static struct spa_fga_port lufs2gain_ports[] = {
|
||||
{ .index = 0,
|
||||
.name = "LUFS",
|
||||
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
|
||||
},
|
||||
{ .index = 1,
|
||||
.name = "Gain",
|
||||
.flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL,
|
||||
},
|
||||
{ .index = 2,
|
||||
.name = "Target LUFS",
|
||||
.flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL,
|
||||
.def = -23.0f, .min = -70.0f, .max = 0.0f
|
||||
},
|
||||
};
|
||||
|
||||
struct lufs2gain_impl {
|
||||
struct plugin *plugin;
|
||||
|
||||
struct spa_fga_dsp *dsp;
|
||||
struct spa_log *log;
|
||||
|
||||
unsigned long rate;
|
||||
float *port[3];
|
||||
};
|
||||
|
||||
static void * lufs2gain_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor,
|
||||
unsigned long SampleRate, int index, const char *config)
|
||||
{
|
||||
struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin);
|
||||
struct lufs2gain_impl *impl;
|
||||
|
||||
impl = calloc(1, sizeof(*impl));
|
||||
if (impl == NULL) {
|
||||
errno = ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
impl->plugin = pl;
|
||||
impl->dsp = pl->dsp;
|
||||
impl->log = pl->log;
|
||||
impl->rate = SampleRate;
|
||||
|
||||
return impl;
|
||||
}
|
||||
|
||||
static void lufs2gain_connect_port(void * Instance, unsigned long Port,
|
||||
float * DataLocation)
|
||||
{
|
||||
struct lufs2gain_impl *impl = Instance;
|
||||
if (Port < 3)
|
||||
impl->port[Port] = DataLocation;
|
||||
}
|
||||
|
||||
static void lufs2gain_run(void * Instance, unsigned long SampleCount)
|
||||
{
|
||||
struct lufs2gain_impl *impl = Instance;
|
||||
float *in = impl->port[0];
|
||||
float *out = impl->port[1];
|
||||
float *target = impl->port[2];
|
||||
float gain;
|
||||
|
||||
if (in == NULL || out == NULL || target == NULL)
|
||||
return;
|
||||
|
||||
if (isfinite(in[0])) {
|
||||
float gaindB = target[0] - in[0];
|
||||
gain = powf(10.0f, gaindB / 20.0f);
|
||||
} else {
|
||||
gain = 1.0f;
|
||||
}
|
||||
out[0] = gain;
|
||||
}
|
||||
|
||||
static void lufs2gain_cleanup(void * Instance)
|
||||
{
|
||||
struct lufs2gain_impl *impl = Instance;
|
||||
free(impl);
|
||||
}
|
||||
|
||||
static const struct spa_fga_descriptor lufs2gain_desc = {
|
||||
.name = "lufs2gain",
|
||||
.flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA,
|
||||
|
||||
.ports = lufs2gain_ports,
|
||||
.n_ports = SPA_N_ELEMENTS(lufs2gain_ports),
|
||||
|
||||
.instantiate = lufs2gain_instantiate,
|
||||
.connect_port = lufs2gain_connect_port,
|
||||
.run = lufs2gain_run,
|
||||
.cleanup = lufs2gain_cleanup,
|
||||
};
|
||||
|
||||
static const struct spa_fga_descriptor * ebur128_descriptor(unsigned long Index)
|
||||
{
|
||||
switch(Index) {
|
||||
case 0:
|
||||
return &ebur128_desc;
|
||||
case 1:
|
||||
return &lufs2gain_desc;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
static const struct spa_fga_descriptor *ebur128_plugin_make_desc(void *plugin, const char *name)
|
||||
{
|
||||
unsigned long i;
|
||||
for (i = 0; ;i++) {
|
||||
const struct spa_fga_descriptor *d = ebur128_descriptor(i);
|
||||
if (d == NULL)
|
||||
break;
|
||||
if (spa_streq(d->name, name))
|
||||
return d;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct spa_fga_plugin_methods impl_plugin = {
|
||||
SPA_VERSION_FGA_PLUGIN_METHODS,
|
||||
.make_desc = ebur128_plugin_make_desc,
|
||||
};
|
||||
|
||||
static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
|
||||
{
|
||||
struct plugin *impl;
|
||||
|
||||
spa_return_val_if_fail(handle != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(interface != NULL, -EINVAL);
|
||||
|
||||
impl = (struct plugin *) handle;
|
||||
|
||||
if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin))
|
||||
*interface = &impl->plugin;
|
||||
else
|
||||
return -ENOENT;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int impl_clear(struct spa_handle *handle)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t
|
||||
impl_get_size(const struct spa_handle_factory *factory,
|
||||
const struct spa_dict *params)
|
||||
{
|
||||
return sizeof(struct plugin);
|
||||
}
|
||||
|
||||
static int
|
||||
impl_init(const struct spa_handle_factory *factory,
|
||||
struct spa_handle *handle,
|
||||
const struct spa_dict *info,
|
||||
const struct spa_support *support,
|
||||
uint32_t n_support)
|
||||
{
|
||||
struct plugin *impl;
|
||||
|
||||
handle->get_interface = impl_get_interface;
|
||||
handle->clear = impl_clear;
|
||||
|
||||
impl = (struct plugin *) handle;
|
||||
|
||||
impl->plugin.iface = SPA_INTERFACE_INIT(
|
||||
SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,
|
||||
SPA_VERSION_FGA_PLUGIN,
|
||||
&impl_plugin, impl);
|
||||
|
||||
impl->quantum_limit = 8192u;
|
||||
|
||||
impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
|
||||
impl->dsp = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP);
|
||||
|
||||
for (uint32_t i = 0; info && i < info->n_items; i++) {
|
||||
const char *k = info->items[i].key;
|
||||
const char *s = info->items[i].value;
|
||||
if (spa_streq(k, "clock.quantum-limit"))
|
||||
spa_atou32(s, &impl->quantum_limit, 0);
|
||||
if (spa_streq(k, "filter.graph.audio.dsp"))
|
||||
sscanf(s, "pointer:%p", &impl->dsp);
|
||||
}
|
||||
if (impl->dsp == NULL) {
|
||||
spa_log_error(impl->log, "%p: could not find DSP functions", impl);
|
||||
return -EINVAL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct spa_interface_info impl_interfaces[] = {
|
||||
{SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,},
|
||||
};
|
||||
|
||||
static int
|
||||
impl_enum_interface_info(const struct spa_handle_factory *factory,
|
||||
const struct spa_interface_info **info,
|
||||
uint32_t *index)
|
||||
{
|
||||
spa_return_val_if_fail(factory != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(info != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(index != NULL, -EINVAL);
|
||||
|
||||
switch (*index) {
|
||||
case 0:
|
||||
*info = &impl_interfaces[*index];
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
(*index)++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static struct spa_handle_factory spa_fga_ebur128_plugin_factory = {
|
||||
SPA_VERSION_HANDLE_FACTORY,
|
||||
"filter.graph.plugin.ebur128",
|
||||
NULL,
|
||||
impl_get_size,
|
||||
impl_init,
|
||||
impl_enum_interface_info,
|
||||
};
|
||||
|
||||
SPA_EXPORT
|
||||
int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
|
||||
{
|
||||
spa_return_val_if_fail(factory != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(index != NULL, -EINVAL);
|
||||
|
||||
switch (*index) {
|
||||
case 0:
|
||||
*factory = &spa_fga_ebur128_plugin_factory;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
(*index)++;
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -105,3 +105,13 @@ spa_filter_graph_plugin_lv2 = shared_library('spa-filter-graph-plugin-lv2',
|
|||
)
|
||||
endif
|
||||
|
||||
if ebur128_lib.found()
|
||||
spa_filter_graph_plugin_ebur128 = shared_library('spa-filter-graph-plugin-ebur128',
|
||||
[ 'ebur128_plugin.c' ],
|
||||
include_directories : [configinc],
|
||||
install : true,
|
||||
install_dir : spa_plugindir / 'filter-graph',
|
||||
dependencies : [ filter_graph_dependencies, lilv_lib, ebur128_lib ]
|
||||
)
|
||||
endif
|
||||
|
||||
|
|
|
|||
63
src/daemon/filter-chain/35-ebur128.conf
Normal file
63
src/daemon/filter-chain/35-ebur128.conf
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
context.modules = [
|
||||
{ name = libpipewire-module-filter-chain
|
||||
args = {
|
||||
node.description = "EBU R128 Normalizer"
|
||||
media.name = "EBU R128 Normalizer"
|
||||
filter.graph = {
|
||||
nodes = [
|
||||
{
|
||||
name = ebur128
|
||||
type = ebur128
|
||||
label = ebur128
|
||||
}
|
||||
{
|
||||
name = lufsL
|
||||
type = ebur128
|
||||
label = lufs2gain
|
||||
control = {
|
||||
"Target LUFS" = -16.0
|
||||
}
|
||||
}
|
||||
{
|
||||
name = lufsR
|
||||
type = ebur128
|
||||
label = lufs2gain
|
||||
control = {
|
||||
"Target LUFS" = -16.0
|
||||
}
|
||||
}
|
||||
{
|
||||
name = volumeL
|
||||
type = builtin
|
||||
label = linear
|
||||
}
|
||||
{
|
||||
name = volumeR
|
||||
type = builtin
|
||||
label = linear
|
||||
}
|
||||
]
|
||||
links = [
|
||||
{ output = "ebur128:Out FL" input = "volumeL:In" }
|
||||
{ output = "ebur128:Global LUFS" input = "lufsL:LUFS" }
|
||||
{ output = "lufsL:Gain" input = "volumeL:Mult" }
|
||||
{ output = "ebur128:Out FR" input = "volumeR:In" }
|
||||
{ output = "ebur128:Global LUFS" input = "lufsR:LUFS" }
|
||||
{ output = "lufsR:Gain" input = "volumeR:Mult" }
|
||||
]
|
||||
inputs = [ "ebur128:In FL" "ebur128:In FR" ]
|
||||
outputs = [ "volumeL:Out" "volumeR:Out" ]
|
||||
}
|
||||
capture.props = {
|
||||
node.name = "effect_input.ebur128_normalize"
|
||||
audio.position = [ FL FR ]
|
||||
media.class = Audio/Sink
|
||||
}
|
||||
playback.props = {
|
||||
node.name = "effect_output.ebur128_normalize"
|
||||
audio.position = [ FL FR ]
|
||||
node.passive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -95,18 +95,18 @@ extern struct spa_handle_factory spa_filter_graph_factory;
|
|||
* Nodes describe the processing filters in the graph. Use a tool like lv2ls
|
||||
* or listplugins to get a list of available plugins, labels and the port names.
|
||||
*
|
||||
* - `type` is one of `ladspa`, `lv2`, `builtin` or `sofa`.
|
||||
* - `type` is one of `ladspa`, `lv2`, `builtin`, `sofa` or `ebur128`.
|
||||
* - `name` is the name for this node, you might need this later to refer to this node
|
||||
* and its ports when setting controls or making links.
|
||||
* - `plugin` is the type specific plugin name.
|
||||
* - For LADSPA plugins it will append `.so` to find the shared object with that
|
||||
* name in the LADSPA plugin path.
|
||||
* - For LV2, this is the plugin URI obtained with lv2ls.
|
||||
* - For builtin and sofa this is ignored
|
||||
* - For builtin, sofa and ebur128 this is ignored
|
||||
* - `label` is the type specific filter inside the plugin.
|
||||
* - For LADSPA this is the label
|
||||
* - For LV2 this is unused
|
||||
* - For builtin and sofa this is the name of the filter to use
|
||||
* - For builtin, sofa and ebur128 this is the name of the filter to use
|
||||
*
|
||||
* - `config` contains a filter specific configuration section. Some plugins need
|
||||
* this. (convolver, sofa, delay, ...)
|
||||
|
|
@ -527,6 +527,84 @@ extern struct spa_handle_factory spa_filter_graph_factory;
|
|||
* - `Radius` controls how far away the signal is as a value between 0 and 100.
|
||||
* default is 1.0.
|
||||
*
|
||||
* ## EBUR128 filter
|
||||
*
|
||||
* There is an optional EBU R128 filter available.
|
||||
*
|
||||
* ### ebur128
|
||||
*
|
||||
* The ebur128 plugin can be used to measure the loudness of a signal.
|
||||
*
|
||||
* It has 7 input ports "In FL", "In FR", "In FC", "In UNUSED", "In SL", "In SR"
|
||||
* and "In DUAL MONO", corresponding to the different input channels for EBUR128.
|
||||
* Not all ports need to be connected for this filter.
|
||||
*
|
||||
* The input signal is passed unmodified on the "Out FL", "Out FR", "Out FC",
|
||||
* "Out UNUSED", "Out SL", "Out SR" and "Out DUAL MONO" output ports.
|
||||
*
|
||||
* There are 7 output control ports that contain the measured loudness information
|
||||
* and that can be used to control the processing of the audio. Some of these ports
|
||||
* contain values in LUFS, or "Loudness Units relative to Full Scale". These are
|
||||
* negative values, closer to 0 is louder. You can use the lufs2gain plugin to
|
||||
* convert this value to again to adjust a volume (See below).
|
||||
*
|
||||
* "Momentary LUFS" contains the momentary loudness measurement with a 400ms window
|
||||
* and 75% overlap. It works mostly like an R.M.S. meter.
|
||||
*
|
||||
* "Shortterm LUFS" contains the shortterm loudness in LUFS over a 3 second window.
|
||||
*
|
||||
* "Global LUFS" contains the global integrated loudness in LUFS over the max-history
|
||||
* window.
|
||||
* "Window LUFS" contains the global integrated loudness in LUFS over the max-window
|
||||
* window.
|
||||
*
|
||||
* "Range LU" contains the loudness range (LRA) in LU units.
|
||||
*
|
||||
* "Peak" contains the peak loudness.
|
||||
*
|
||||
* "True Peak" contains the true peak loudness oversampling the signal. This can more
|
||||
* accurately reflect the peak compared to "Peak".
|
||||
*
|
||||
* The node also has an optional `config` section with extra configuration:
|
||||
*
|
||||
*\code{.unparsed}
|
||||
* filter.graph = {
|
||||
* nodes = [
|
||||
* {
|
||||
* type = ebur128
|
||||
* name = ...
|
||||
* label = ebur128
|
||||
* config = {
|
||||
* max-history = ...
|
||||
* max-window = ...
|
||||
* use-histogram = ...
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
*\endcode
|
||||
*
|
||||
* - `max-history` the maximum history to keep in (float) seconds. Default to 10.0
|
||||
*
|
||||
* - `max-window` the maximum window to keep in (float) seconds. Default to 0.0
|
||||
* You will need to set this to some value to get "Window LUFS"
|
||||
* output control values.
|
||||
*
|
||||
* - `use-histogram` uses the histogram algorithm to calculate loudness. Defaults
|
||||
* to false.
|
||||
*
|
||||
* ### lufs2gain
|
||||
*
|
||||
* The lufs2gain plugin can be used to convert LUFS control values to gain. It needs
|
||||
* a target LUFS control input to drive the conversion.
|
||||
*
|
||||
* It has 2 input control ports "LUFS" and "Target LUFS" and will produce 1 output
|
||||
* control value "Gain". This gain can be used as input for the builtin `linear`
|
||||
* node, for example, to adust the gain.
|
||||
*
|
||||
*
|
||||
* ## General options
|
||||
*
|
||||
* Options with well-known behavior. Most options can be added to the global
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue