From 996b13fac4810efc35ff988f523f0c88a3b57ec9 Mon Sep 17 00:00:00 2001 From: Paul B Mahol Date: Fri, 26 Nov 2021 14:23:16 +0100 Subject: [PATCH] avfilter: add audio dynamic equalizer filter --- Changelog | 1 + doc/filters.texi | 76 +++++++ libavfilter/Makefile | 1 + libavfilter/af_adynamicequalizer.c | 315 +++++++++++++++++++++++++++++ libavfilter/allfilters.c | 1 + libavfilter/version.h | 2 +- 6 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 libavfilter/af_adynamicequalizer.c diff --git a/Changelog b/Changelog index 13b22f6149..5c6adc0fa7 100644 --- a/Changelog +++ b/Changelog @@ -40,6 +40,7 @@ version : - adynamicsmooth audio filter - libplacebo filter - vflip_vulkan, hflip_vulkan and flip_vulkan filters +- adynamicequalizer audio filter version 4.4: diff --git a/doc/filters.texi b/doc/filters.texi index 8eff460cd9..cffec8168c 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -843,6 +843,82 @@ Compute derivative/integral of audio stream. Applying both filters one after another produces original audio. +@section adynamicequalizer + +Apply dynamic equalization to input audio stream. + +A description of the accepted options follows. + +@table @option +@item threshold +Set the detection threshold used to trigger equalization. +Threshold detection is using bandpass filter. +Default value is 0. Allowed range is from 0 to 100. + +@item dfrequency +Set the detection frequency in Hz used for bandpass filter used to trigger equalization. +Default value is 1000 Hz. Allowed range is between 2 and 1000000 Hz. + +@item dqfactor +Set the detection resonance factor for bandpass filter used to trigger equalization. +Default value is 1. Allowed range is from 0.001 to 1000. + +@item tfrequency +Set the target frequency of equalization filter. +Default value is 1000 Hz. Allowed range is between 2 and 1000000 Hz. + +@item tqfactor +Set the target resonance factor for target equalization filter. +Default value is 1. Allowed range is from 0.001 to 1000. + +@item attack +Set the amount of milliseconds the signal from detection has to rise above +the detection threshold before equalization starts. +Default is 20. Allowed range is between 1 and 2000. + +@item release +Set the amount of milliseconds the signal from detection has to fall below the +detection threshold before equalization ends. +Default is 200. Allowed range is between 1 and 2000. + +@item knee +Curve the sharp knee around the detection threshold to calculate +equalization gain more softly. +Default is 1. Allowed range is between 0 and 8. + +@item ratio +Set the ratio by which the equalization gain is raised. +Default is 1. Allowed range is between 1 and 20. + +@item makeup +Set the makeup offset in dB by which the equalization gain is raised. +Default is 0. Allowed range is between 0 and 30. + +@item range +Set the max allowed cut/boost amount in dB. Default is 0. +Allowed range is from 0 to 200. + +@item slew +Set the slew factor. Default is 1. Allowed range is from 1 to 200. + +@item mode +Set the mode of filter operation, can be one of the following: + +@table @samp +@item listen +Output only isolated bandpass signal. +@item cut +Cut frequencies above detection threshold. +@item boost +Boost frequencies bellow detection threshold. +@end table +Default mode is @samp{cut}. +@end table + +@subsection Commands + +This filter supports the all above options as @ref{commands}. + @section adynamicsmooth Apply dynamic smoothing to input audio stream. diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 8744cc3c63..2fe495df28 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -44,6 +44,7 @@ OBJS-$(CONFIG_ADECORRELATE_FILTER) += af_adecorrelate.o OBJS-$(CONFIG_ADELAY_FILTER) += af_adelay.o OBJS-$(CONFIG_ADENORM_FILTER) += af_adenorm.o OBJS-$(CONFIG_ADERIVATIVE_FILTER) += af_aderivative.o +OBJS-$(CONFIG_ADYNAMICEQUALIZER_FILTER) += af_adynamicequalizer.o OBJS-$(CONFIG_ADYNAMICSMOOTH_FILTER) += af_adynamicsmooth.o OBJS-$(CONFIG_AECHO_FILTER) += af_aecho.o OBJS-$(CONFIG_AEMPHASIS_FILTER) += af_aemphasis.o diff --git a/libavfilter/af_adynamicequalizer.c b/libavfilter/af_adynamicequalizer.c new file mode 100644 index 0000000000..f377a5db3d --- /dev/null +++ b/libavfilter/af_adynamicequalizer.c @@ -0,0 +1,315 @@ +/* + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include + +#include "libavutil/opt.h" +#include "avfilter.h" +#include "audio.h" +#include "formats.h" +#include "hermite.h" + +typedef struct AudioDynamicEqualizerContext { + const AVClass *class; + + double threshold; + double dfrequency; + double dqfactor; + double tfrequency; + double tqfactor; + double ratio; + double range; + double makeup; + double knee; + double slew; + double attack; + double release; + double attack_coef; + double release_coef; + int mode; + + AVFrame *state; +} AudioDynamicEqualizerContext; + +static int config_input(AVFilterLink *inlink) +{ + AVFilterContext *ctx = inlink->dst; + AudioDynamicEqualizerContext *s = ctx->priv; + + s->state = ff_get_audio_buffer(inlink, 8); + if (!s->state) + return AVERROR(ENOMEM); + + return 0; +} + +static double get_svf(double in, double *m, double *a, double *b) +{ + const double v0 = in; + const double v3 = v0 - b[1]; + const double v1 = a[0] * b[0] + a[1] * v3; + const double v2 = b[1] + a[1] * b[0] + a[2] * v3; + + b[0] = 2. * v1 - b[0]; + b[1] = 2. * v2 - b[1]; + + return m[0] * v0 + m[1] * v1 + m[2] * v2; +} + +static inline double from_dB(double x) +{ + return exp(0.05 * x * M_LN10); +} + +static inline double to_dB(double x) +{ + return 20. * log10(x); +} + +static inline double sqr(double x) +{ + return x * x; +} + +static double get_gain(double in, double srate, double makeup, + double aattack, double iratio, double knee, double range, + double thresdb, double slewfactor, double *state, + double attack_coeff, double release_coeff, double nc) +{ + double width = (6. * knee) + 0.01; + double cdb = 0.; + double Lgain = 1.; + double Lxg, Lxl, Lyg, Lyl, Ly1; + double checkwidth = 0.; + double slewwidth = 1.8; + int attslew = 0; + + Lyg = 0.; + Lxg = to_dB(fabs(in) + DBL_EPSILON); + + Lyg = Lxg + (iratio - 1.) * sqr(Lxg - thresdb + width * .5) / (2. * width); + + checkwidth = 2. * fabs(Lxg - thresdb); + if (2. * (Lxg - thresdb) < -width) { + Lyg = Lxg; + } else if (checkwidth <= width) { + Lyg = thresdb + (Lxg - thresdb) * iratio; + if (checkwidth <= slewwidth) { + if (Lyg >= state[2]) + attslew = 1; + } + } else if (2. * (Lxg-thresdb) > width) { + Lyg = thresdb + (Lxg - thresdb) * iratio; + } + + attack_coeff = attslew ? aattack : attack_coeff; + + Lxl = Lxg - Lyg; + + Ly1 = fmaxf(Lxl, release_coeff * state[1] +(1. - release_coeff) * Lxl); + Lyl = attack_coeff * state[0] + (1. - attack_coeff) * Ly1; + + cdb = -Lyl; + Lgain = from_dB(nc * fmin(cdb - makeup, range)); + + state[0] = Lyl; + state[1] = Ly1; + state[2] = Lyg; + + return Lgain; +} + +typedef struct ThreadData { + AVFrame *in, *out; +} ThreadData; + +static int filter_channels(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs) +{ + AudioDynamicEqualizerContext *s = ctx->priv; + ThreadData *td = arg; + AVFrame *in = td->in; + AVFrame *out = td->out; + const double sample_rate = in->sample_rate; + const double makeup = s->makeup; + const double iratio = 1. / s->ratio; + const double range = s->range; + const double dfrequency = fmin(s->dfrequency, sample_rate * 0.5); + const double tfrequency = fmin(s->tfrequency, sample_rate * 0.5); + const double threshold = log(s->threshold + DBL_EPSILON); + const double release = s->release_coef; + const double attack = s->attack_coef; + const double dqfactor = s->dqfactor; + const double tqfactor = s->tqfactor; + const double fg = tan(M_PI * tfrequency / sample_rate); + const double dg = tan(M_PI * dfrequency / sample_rate); + const int start = (in->channels * jobnr) / nb_jobs; + const int end = (in->channels * (jobnr+1)) / nb_jobs; + const int mode = s->mode; + const double knee = s->knee; + const double slew = s->slew; + const double aattack = exp(-1000. / ((s->attack + 2.0 * (slew - 1.)) * sample_rate)); + const double nc = mode == 0 ? 1. : -1.; + double da[3], dm[3]; + + { + double k = 1. / dqfactor; + + da[0] = 1. / (1. + dg * (dg + k)); + da[1] = dg * da[0]; + da[2] = dg * da[1]; + + dm[0] = 0.; + dm[1] = 1.; + dm[2] = 0.; + } + + for (int ch = start; ch < end; ch++) { + const double *src = (const double *)in->extended_data[ch]; + double *dst = (double *)out->extended_data[ch]; + double *state = (double *)s->state->extended_data[ch]; + + for (int n = 0; n < out->nb_samples; n++) { + double detect, gain, v, listen; + double fa[3], fm[3]; + + detect = listen = get_svf(src[n], dm, da, state); + detect = fabs(detect); + + gain = get_gain(detect, sample_rate, makeup, + aattack, iratio, knee, range, threshold, slew, + &state[4], attack, release, nc); + + { + double k = 1. / (tqfactor * gain); + + fa[0] = 1. / (1. + fg * (fg + k)); + fa[1] = fg * fa[0]; + fa[2] = fg * fa[1]; + + fm[0] = 1.; + fm[1] = k * (gain * gain - 1.); + fm[2] = 0.; + } + + v = get_svf(src[n], fm, fa, &state[2]); + v = mode == -1 ? listen : v; + dst[n] = ctx->is_disabled ? src[n] : v; + } + } + + return 0; +} + +static double get_coef(double x, double sr) +{ + return exp(-1000. / (x * sr)); +} + +static int filter_frame(AVFilterLink *inlink, AVFrame *in) +{ + AVFilterContext *ctx = inlink->dst; + AVFilterLink *outlink = ctx->outputs[0]; + AudioDynamicEqualizerContext *s = ctx->priv; + ThreadData td; + AVFrame *out; + + if (av_frame_is_writable(in)) { + out = in; + } else { + out = ff_get_audio_buffer(outlink, in->nb_samples); + if (!out) { + av_frame_free(&in); + return AVERROR(ENOMEM); + } + av_frame_copy_props(out, in); + } + + s->attack_coef = get_coef(s->attack, in->sample_rate); + s->release_coef = get_coef(s->release, in->sample_rate); + + td.in = in; + td.out = out; + ff_filter_execute(ctx, filter_channels, &td, NULL, + FFMIN(outlink->channels, ff_filter_get_nb_threads(ctx))); + + if (out != in) + av_frame_free(&in); + return ff_filter_frame(outlink, out); +} + +static av_cold void uninit(AVFilterContext *ctx) +{ + AudioDynamicEqualizerContext *s = ctx->priv; + + av_frame_free(&s->state); +} + +#define OFFSET(x) offsetof(AudioDynamicEqualizerContext, x) +#define FLAGS AV_OPT_FLAG_AUDIO_PARAM|AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_RUNTIME_PARAM + +static const AVOption adynamicequalizer_options[] = { + { "threshold", "set detection threshold", OFFSET(threshold), AV_OPT_TYPE_DOUBLE, {.dbl=0}, 0, 100, FLAGS }, + { "dfrequency", "set detection frequency", OFFSET(dfrequency), AV_OPT_TYPE_DOUBLE, {.dbl=1000}, 2, 1000000, FLAGS }, + { "dqfactor", "set detection Q factor", OFFSET(dqfactor), AV_OPT_TYPE_DOUBLE, {.dbl=1}, 0.001, 1000, FLAGS }, + { "tfrequency", "set target frequency", OFFSET(tfrequency), AV_OPT_TYPE_DOUBLE, {.dbl=1000}, 2, 1000000, FLAGS }, + { "tqfactor", "set target Q factor", OFFSET(tqfactor), AV_OPT_TYPE_DOUBLE, {.dbl=1}, 0.001, 1000, FLAGS }, + { "attack", "set attack duration", OFFSET(attack), AV_OPT_TYPE_DOUBLE, {.dbl=20}, 1, 2000, FLAGS }, + { "release", "set release duration", OFFSET(release), AV_OPT_TYPE_DOUBLE, {.dbl=200}, 1, 2000, FLAGS }, + { "knee", "set knee factor", OFFSET(knee), AV_OPT_TYPE_DOUBLE, {.dbl=1}, 0, 8, FLAGS }, + { "ratio", "set ratio factor", OFFSET(ratio), AV_OPT_TYPE_DOUBLE, {.dbl=1}, 1, 20, FLAGS }, + { "makeup", "set makeup gain", OFFSET(makeup), AV_OPT_TYPE_DOUBLE, {.dbl=0}, 0, 30, FLAGS }, + { "range", "set max gain", OFFSET(range), AV_OPT_TYPE_DOUBLE, {.dbl=0}, 0, 200, FLAGS }, + { "slew", "set slew factor", OFFSET(slew), AV_OPT_TYPE_DOUBLE, {.dbl=1}, 1, 200, FLAGS }, + { "mode", "set mode", OFFSET(mode), AV_OPT_TYPE_INT, {.i64=0}, -1, 1, FLAGS, "mode" }, + { "listen", 0, 0, AV_OPT_TYPE_CONST, {.i64=-1}, 0, 0, FLAGS, "mode" }, + { "cut", 0, 0, AV_OPT_TYPE_CONST, {.i64=0}, 0, 0, FLAGS, "mode" }, + { "boost", 0, 0, AV_OPT_TYPE_CONST, {.i64=1}, 0, 0, FLAGS, "mode" }, + { NULL } +}; + +AVFILTER_DEFINE_CLASS(adynamicequalizer); + +static const AVFilterPad inputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_AUDIO, + .filter_frame = filter_frame, + .config_props = config_input, + }, +}; + +static const AVFilterPad outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_AUDIO, + }, +}; + +const AVFilter ff_af_adynamicequalizer = { + .name = "adynamicequalizer", + .description = NULL_IF_CONFIG_SMALL("Apply Dynamic Equalization of input audio."), + .priv_size = sizeof(AudioDynamicEqualizerContext), + .priv_class = &adynamicequalizer_class, + .uninit = uninit, + FILTER_INPUTS(inputs), + FILTER_OUTPUTS(outputs), + FILTER_SINGLE_SAMPLEFMT(AV_SAMPLE_FMT_DBLP), + .flags = AVFILTER_FLAG_SUPPORT_TIMELINE_INTERNAL | + AVFILTER_FLAG_SLICE_THREADS, + .process_command = ff_filter_process_command, +}; diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index 9e16b4e71e..ec57a2c49c 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -37,6 +37,7 @@ extern const AVFilter ff_af_adecorrelate; extern const AVFilter ff_af_adelay; extern const AVFilter ff_af_adenorm; extern const AVFilter ff_af_aderivative; +extern const AVFilter ff_af_adynamicequalizer; extern const AVFilter ff_af_adynamicsmooth; extern const AVFilter ff_af_aecho; extern const AVFilter ff_af_aemphasis; diff --git a/libavfilter/version.h b/libavfilter/version.h index 0247fb4f9a..0253c911be 100644 --- a/libavfilter/version.h +++ b/libavfilter/version.h @@ -30,7 +30,7 @@ #include "libavutil/version.h" #define LIBAVFILTER_VERSION_MAJOR 8 -#define LIBAVFILTER_VERSION_MINOR 19 +#define LIBAVFILTER_VERSION_MINOR 20 #define LIBAVFILTER_VERSION_MICRO 100