From df271d13f3351038f3097836b190b2ae32a16be7 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 9 Dec 2024 11:12:35 +0100 Subject: [PATCH] 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 --- meson_options.txt | 4 + spa/meson.build | 3 + spa/plugins/filter-graph/ebur128_plugin.c | 631 ++++++++++++++++++++++ spa/plugins/filter-graph/meson.build | 10 + src/daemon/filter-chain/35-ebur128.conf | 63 +++ src/modules/module-filter-chain.c | 84 ++- 6 files changed, 792 insertions(+), 3 deletions(-) create mode 100644 spa/plugins/filter-graph/ebur128_plugin.c create mode 100644 src/daemon/filter-chain/35-ebur128.conf diff --git a/meson_options.txt b/meson_options.txt index dc52fd39a..9acb8f85e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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') diff --git a/spa/meson.build b/spa/meson.build index 31edfcab7..0c96e0211 100644 --- a/spa/meson.build +++ b/spa/meson.build @@ -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 diff --git a/spa/plugins/filter-graph/ebur128_plugin.c b/spa/plugins/filter-graph/ebur128_plugin.c new file mode 100644 index 000000000..22bf5f513 --- /dev/null +++ b/spa/plugins/filter-graph/ebur128_plugin.c @@ -0,0 +1,631 @@ +#include "config.h" + +#include + +#include +#include + +#include "audio-plugin.h" +#include "audio-dsp.h" + +#include + +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; +} diff --git a/spa/plugins/filter-graph/meson.build b/spa/plugins/filter-graph/meson.build index 2c54b9c26..3d74958ee 100644 --- a/spa/plugins/filter-graph/meson.build +++ b/spa/plugins/filter-graph/meson.build @@ -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 + diff --git a/src/daemon/filter-chain/35-ebur128.conf b/src/daemon/filter-chain/35-ebur128.conf new file mode 100644 index 000000000..f12c4a9cf --- /dev/null +++ b/src/daemon/filter-chain/35-ebur128.conf @@ -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 + } + } + } +] diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index b54b5151f..642e57e22 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -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