From patchwork Thu May 31 20:05:46 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Paul B Mahol X-Patchwork-Id: 9184 Delivered-To: ffmpegpatchwork@gmail.com Received: by 2002:a02:11c:0:0:0:0:0 with SMTP id c28-v6csp58200jad; Thu, 31 May 2018 13:14:30 -0700 (PDT) X-Google-Smtp-Source: ADUXVKJmO2+f7trvFoeQnC6TsvOlVVB51L7RJ6HGziPWuUmD3QY0uI/t8qxzerPnO+/mfLDEuYEa X-Received: by 2002:a1c:168c:: with SMTP id 134-v6mr805492wmw.53.1527797670726; Thu, 31 May 2018 13:14:30 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1527797670; cv=none; d=google.com; s=arc-20160816; b=K0mhgsqy70YNbGqvoMecjXx9rT0WROYzm4LQSQFo6tdZLS/pdQXg1UauMHMyR4yFGW 5Gqvp9JKI1FYlVReAYVCoqYQm+uOI//Yzk05oo5n3j7Id0XqTq4nElrx2oK5SQOZz4tp GzmoNif/GJc+BJaOSAmWv8kOsJTVH7vFFAgM1cX1cmpVtuKrUIiDHuuaXXfQvl8fXsi4 4y5XMi0QakynqaPrHNJyNE2P4+QnL8UqZuX0n9cjqL8Lp9+Z2ePhuzmoZiJswIO96wj6 DwS+D2Mu9omZVAJ2LuEPh62ZBVlCRnNB2+R8mJmchgT/3OGBRm7NuwnedKCLBvngI7pr RZKw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=sender:errors-to:content-transfer-encoding:mime-version:reply-to :list-subscribe:list-help:list-post:list-archive:list-unsubscribe :list-id:precedence:subject:message-id:date:to:from:dkim-signature :delivered-to:arc-authentication-results; bh=1cX70tZd0o5JWLRKU37iXX5HRZT4EGcXJrMt/IeEK1Y=; b=VedL2PcYB9NVP0D61bEtMrrhjKQ9YfoUdvPw0+BEpxD1hp6DFz1lAwohShjKySEJ2C AKwDYe00yuK1YkHk6o0mfjvg1t8Io9gF3EOXUp2Enmd8y8RA6ZaOywVP++66GJxoa5QP LDAF1YnYtbNdRqadtJm7H+7utzpndju5ExPksi46R/K8MY0DFBZUvSC1NWx5GXWe5ToY 2OuHiQH/eGl+lAGoX5aG1PtQEv2GNeHSGg/NYZVf1PzwU1jc4yFXn4seD2jqFcjjvVQW fX5DhcOOJrFVLJsBwGdaztG5svnprQaIPdXlpJf2mBsxY2LNnauEzTVghwS8lKOm0zLu miSw== ARC-Authentication-Results: i=1; mx.google.com; dkim=neutral (body hash did not verify) header.i=@gmail.com header.s=20161025 header.b=pZ68aFyW; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org; dmarc=fail (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Return-Path: Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org. [79.124.17.100]) by mx.google.com with ESMTP id g203-v6si173611wmd.78.2018.05.31.13.14.29; Thu, 31 May 2018 13:14:30 -0700 (PDT) Received-SPF: pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) client-ip=79.124.17.100; Authentication-Results: mx.google.com; dkim=neutral (body hash did not verify) header.i=@gmail.com header.s=20161025 header.b=pZ68aFyW; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org; dmarc=fail (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id 3E19768A2DD; Thu, 31 May 2018 23:13:43 +0300 (EEST) X-Original-To: ffmpeg-devel@ffmpeg.org Delivered-To: ffmpeg-devel@ffmpeg.org Received: from mail-wm0-f68.google.com (mail-wm0-f68.google.com [74.125.82.68]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id 4CBA6689DE7 for ; Thu, 31 May 2018 23:13:36 +0300 (EEST) Received: by mail-wm0-f68.google.com with SMTP id r15-v6so28474776wmc.1 for ; Thu, 31 May 2018 13:14:21 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:subject:date:message-id; bh=/6Qqc1Z/VTi3vPm7AQBH5PUn6XV2sROoO6Qd2Os1vM0=; b=pZ68aFyWH7MgdDlfopsYaMifxYbVEHyHwCrZhooQO3IpGbNq4aZC7Q7E0hzEQouSuM gIPvzGbwOPGL5DvbJLRWeLSL+Pcey3N6b31uWlOA4qSQb7mwXdAemmJCf0H7cps4EBr0 EEkozF25fcCLPHLkqafXYlusNX4SShkEmlelJIwXCaVAVSdHZMxocf3vMXalWecbnAKX W6VXvJFbbFAcT5GGN0XzU1xHxZJ6hPU6kCYci327hz9PuncgVMevAj0FvEgKOKfFRS00 e19JAqvxXN0yM50tsVqDcDC5CIAWHR1m3/A2zNwdpduqwLVz4cYIb/YZMptSuMrcEdom 1BxQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:subject:date:message-id; bh=/6Qqc1Z/VTi3vPm7AQBH5PUn6XV2sROoO6Qd2Os1vM0=; b=mcL8No5d868e1y25FUfNIvJu4mNTsVRjQ7mR2OyQtNz/6T/FrQe4zI6SKP9SnT1O9R ZxRyMLN+LRrpFEgGBQluAfILJdexty9D25yvGog+kjuB8lUMjftVD7sqD3AjHATsMimN 5V4E9cHJKLvVhmMs1NF27RITnrQKFJc2MWawEQZQzBkjaglxqzxiHXkzKGrUvo0hmxRY 9nVwCbgXzMnkguriMrlhQfAsc0g+vxVQsH9JH5H1QGWM73Y0NMt3tKIC+V1RevRw8xuq 8LDD2YDt8QPZO7Vo9euMwWTDWiC5xGuV+qpJ0u/Ol2g0YgliIFQqUV/h7qkj8RhuHR0g Sebg== X-Gm-Message-State: ALKqPwfVG1N2Ugs4CZOgSgm3ZZ51yV2dN/IseOgvRNSs9uR9xsrGVAsp t6LsLK4IwehhcxWiXAvU0L/91w== X-Received: by 2002:a1c:ce:: with SMTP id 197-v6mr753808wma.118.1527797160259; Thu, 31 May 2018 13:06:00 -0700 (PDT) Received: from localhost.localdomain ([94.250.174.60]) by smtp.gmail.com with ESMTPSA id q194-v6sm268786wmd.26.2018.05.31.13.05.59 for (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Thu, 31 May 2018 13:05:59 -0700 (PDT) From: Paul B Mahol To: ffmpeg-devel@ffmpeg.org Date: Thu, 31 May 2018 22:05:46 +0200 Message-Id: <20180531200546.23288-1-onemda@gmail.com> X-Mailer: git-send-email 2.11.0 Subject: [FFmpeg-devel] [PATCH] avfilter: add crossover filter X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.20 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: FFmpeg development discussions and patches MIME-Version: 1.0 Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" Signed-off-by: Paul B Mahol --- doc/filters.texi | 17 +++ libavfilter/Makefile | 1 + libavfilter/af_crossover.c | 343 +++++++++++++++++++++++++++++++++++++++++++++ libavfilter/allfilters.c | 1 + 4 files changed, 362 insertions(+) create mode 100644 libavfilter/af_crossover.c diff --git a/doc/filters.texi b/doc/filters.texi index fb131670c7..e3ade147ee 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -2565,6 +2565,23 @@ Set input gain. Default is 0.9. Set output gain. Default is 1. @end table +@section crossover +Split audio stream into several bands. + +This filter splits audio stream into two or more frequency ranges. +Summing all streams back will give flat output. + +The filter accepts the following options: + +@table @option +@item split +Set split frequencies. Those must be positive and increasing. + +@item order +Set filter order, can be @var{2nd}, @var{4th} or @var{8th}. +Default is @var{4th}. +@end table + @section crystalizer Simple algorithm to expand audio dynamic range. diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 3201cbeacf..4d82d6597d 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -89,6 +89,7 @@ OBJS-$(CONFIG_CHORUS_FILTER) += af_chorus.o generate_wave_table. OBJS-$(CONFIG_COMPAND_FILTER) += af_compand.o OBJS-$(CONFIG_COMPENSATIONDELAY_FILTER) += af_compensationdelay.o OBJS-$(CONFIG_CROSSFEED_FILTER) += af_crossfeed.o +OBJS-$(CONFIG_CROSSOVER_FILTER) += af_crossover.o OBJS-$(CONFIG_CRYSTALIZER_FILTER) += af_crystalizer.o OBJS-$(CONFIG_DCSHIFT_FILTER) += af_dcshift.o OBJS-$(CONFIG_DRMETER_FILTER) += af_drmeter.o diff --git a/libavfilter/af_crossover.c b/libavfilter/af_crossover.c new file mode 100644 index 0000000000..59f8352c2b --- /dev/null +++ b/libavfilter/af_crossover.c @@ -0,0 +1,343 @@ +/* + * 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 + */ + +/** + * @file + * Crossover filter + * + * Split an audio stream into several bands. + */ + +#include "libavutil/attributes.h" +#include "libavutil/avstring.h" +#include "libavutil/channel_layout.h" +#include "libavutil/internal.h" +#include "libavutil/opt.h" + +#include "audio.h" +#include "avfilter.h" +#include "formats.h" +#include "internal.h" + +#define MAX_SPLITS 8 +#define MAX_BANDS MAX_SPLITS + 1 + +typedef struct BiquadContext { + double a0, a1, a2; + double b1, b2; + double i1, i2; + double o1, o2; +} BiquadContext; + +typedef struct CrossoverChannel { + BiquadContext lp[MAX_BANDS][4]; + BiquadContext hp[MAX_BANDS][4]; +} CrossoverChannel; + +typedef struct CrossoverContext { + const AVClass *class; + + char *splits_str; + int order; + + int filter_count; + int nb_splits; + float *splits; + + CrossoverChannel *xover; +} CrossoverContext; + +#define OFFSET(x) offsetof(CrossoverContext, x) +#define AF AV_OPT_FLAG_AUDIO_PARAM | AV_OPT_FLAG_FILTERING_PARAM + +static const AVOption crossover_options[] = { + { "split", "set split frequencies", OFFSET(splits_str), AV_OPT_TYPE_STRING, {.str="500"}, 0, 0, AF }, + { "order", "set order", OFFSET(order), AV_OPT_TYPE_INT, {.i64=1}, 0, 2, AF, "m" }, + { "2nd", "2nd order", 0, AV_OPT_TYPE_CONST, {.i64=0}, 0, 0, AF, "m" }, + { "4th", "4th order", 0, AV_OPT_TYPE_CONST, {.i64=1}, 0, 0, AF, "m" }, + { "8th", "8th order", 0, AV_OPT_TYPE_CONST, {.i64=2}, 0, 0, AF, "m" }, + { NULL } +}; + +AVFILTER_DEFINE_CLASS(crossover); + +static av_cold int init(AVFilterContext *ctx) +{ + CrossoverContext *s = ctx->priv; + char *p, *arg, *saveptr = NULL; + int i, ret = 0; + + s->splits = av_calloc(MAX_SPLITS, sizeof(*s->splits)); + if (!s->splits) + return AVERROR(ENOMEM); + + p = s->splits_str; + for (i = 0; i < MAX_SPLITS; i++) { + float freq; + + if (!(arg = av_strtok(p, " |", &saveptr))) + break; + + p = NULL; + + ret = sscanf(arg, "%f", &freq); + + if (freq <= 0) { + av_log(ctx, AV_LOG_ERROR, "Frequency %f must be positive number.\n", freq); + return AVERROR(EINVAL); + } + + if (i > 0 && freq <= s->splits[i-1]) { + av_log(ctx, AV_LOG_ERROR, "Frequency %f must be in increasing order.\n", freq); + return AVERROR(EINVAL); + } + + s->splits[i] = freq; + } + + s->nb_splits = i; + + for (i = 0; i <= s->nb_splits; i++) { + AVFilterPad pad = { 0 }; + char *name; + + pad.type = AVMEDIA_TYPE_AUDIO; + name = av_asprintf("out%d", ctx->nb_outputs); + if (!name) + return AVERROR(ENOMEM); + pad.name = name; + + if ((ret = ff_insert_outpad(ctx, i, &pad)) < 0) { + av_freep(&pad.name); + return ret; + } + } + + return ret; +} + +static void set_lp(BiquadContext *b, float fc, float q, float sr) +{ + double omega = (2.0 * M_PI * fc / sr); + double sn = sin(omega); + double cs = cos(omega); + double alpha = (sn / (2 * q)); + double inv = (1.0 / (1.0 + alpha)); + + b->a2 = b->a0 = (inv * (1.0 - cs) * 0.5); + b->a1 = b->a0 + b->a0; + b->b1 = -2. * cs * inv; + b->b2 = (1. - alpha) * inv; +} + +static void set_hp(BiquadContext *b, float fc, float q, float sr) +{ + double omega = 2 * M_PI * fc / sr; + double sn = sin(omega); + double cs = cos(omega); + double alpha = sn / (2 * q); + double inv = 1.0 / (1.0 + alpha); + + b->a0 = inv * (1. + cs) / 2.; + b->a1 = -2. * b->a0; + b->a2 = b->a0; + b->b1 = -2. * cs * inv; + b->b2 = (1. - alpha) * inv; +} + +static int config_input(AVFilterLink *inlink) +{ + AVFilterContext *ctx = inlink->dst; + CrossoverContext *s = ctx->priv; + int ch, band, sample_rate = inlink->sample_rate; + double q; + + s->xover = av_calloc(inlink->channels, sizeof(*s->xover)); + if (!s->xover) + return AVERROR(ENOMEM); + + switch (s->order) { + case 0: + q = 0.5; + s->filter_count = 1; + break; + case 1: + q = M_SQRT1_2; + s->filter_count = 2; + break; + case 2: + q = 0.54; + s->filter_count = 4; + break; + } + + for (ch = 0; ch < inlink->channels; ch++) { + for (band = 0; band <= s->nb_splits; band++) { + set_lp(&s->xover[ch].lp[band][0], s->splits[band], q, sample_rate); + set_hp(&s->xover[ch].hp[band][0], s->splits[band], q, sample_rate); + + if (s->order > 1) { + set_lp(&s->xover[ch].lp[band][1], s->splits[band], 1.34, sample_rate); + set_hp(&s->xover[ch].hp[band][1], s->splits[band], 1.34, sample_rate); + set_lp(&s->xover[ch].lp[band][2], s->splits[band], q, sample_rate); + set_hp(&s->xover[ch].hp[band][2], s->splits[band], q, sample_rate); + set_lp(&s->xover[ch].lp[band][3], s->splits[band], 1.34, sample_rate); + set_hp(&s->xover[ch].hp[band][3], s->splits[band], 1.34, sample_rate); + } else { + set_lp(&s->xover[ch].lp[band][1], s->splits[band], q, sample_rate); + set_hp(&s->xover[ch].hp[band][1], s->splits[band], q, sample_rate); + } + } + } + + return 0; +} + +static int query_formats(AVFilterContext *ctx) +{ + AVFilterFormats *formats; + AVFilterChannelLayouts *layouts; + static const enum AVSampleFormat sample_fmts[] = { + AV_SAMPLE_FMT_DBLP, + AV_SAMPLE_FMT_NONE + }; + int ret; + + layouts = ff_all_channel_counts(); + if (!layouts) + return AVERROR(ENOMEM); + ret = ff_set_common_channel_layouts(ctx, layouts); + if (ret < 0) + return ret; + + formats = ff_make_format_list(sample_fmts); + if (!formats) + return AVERROR(ENOMEM); + ret = ff_set_common_formats(ctx, formats); + if (ret < 0) + return ret; + + formats = ff_all_samplerates(); + if (!formats) + return AVERROR(ENOMEM); + return ff_set_common_samplerates(ctx, formats); +} + +static double biquad_process(BiquadContext *b, double in) +{ + double out = in * b->a0 + b->i1 * b->a1 + b->i2 * b->a2 - b->o1 * b->b1 - b->o2 * b->b2; + + b->i2 = b->i1; + b->o2 = b->o1; + b->i1 = in; + b->o1 = out; + + return out; +} + +static int filter_frame(AVFilterLink *inlink, AVFrame *in) +{ + AVFilterContext *ctx = inlink->dst; + CrossoverContext *s = ctx->priv; + AVFrame *frames[MAX_BANDS] = { NULL }; + int i, f, ch, band, ret = 0; + + for (i = 0; i < ctx->nb_outputs; i++) { + frames[i] = ff_get_audio_buffer(ctx->outputs[i], in->nb_samples); + + if (!frames[i]) { + ret = AVERROR(ENOMEM); + break; + } + + frames[i]->pts = in->pts; + } + + if (ret < 0) + goto fail; + + for (ch = 0; ch < inlink->channels; ch++) { + const double *src = (const double *)in->extended_data[ch]; + CrossoverChannel *xover = &s->xover[ch]; + + for (band = 0; band < ctx->nb_outputs; band++) { + double *dst = (double *)frames[band]->extended_data[ch]; + + for (i = 0; i < in->nb_samples; i++) { + dst[i] = src[i]; + + for (f = 0; f < s->filter_count; f++) { + if (band + 1 < ctx->nb_outputs) { + BiquadContext *lp = &xover->lp[band][f]; + dst[i] = biquad_process(lp, dst[i]); + } + + if (band - 1 >= 0) { + BiquadContext *hp = &xover->hp[band - 1][f]; + dst[i] = biquad_process(hp, dst[i]); + } + } + } + } + } + + for (i = 0; i < ctx->nb_outputs; i++) { + ret = ff_filter_frame(ctx->outputs[i], frames[i]); + if (ret < 0) + break; + } + +fail: + av_frame_free(&in); + + return ret; +} + +static av_cold void uninit(AVFilterContext *ctx) +{ + CrossoverContext *s = ctx->priv; + int i; + + av_freep(&s->splits); + + for (i = 0; i < ctx->nb_outputs; i++) + av_freep(&ctx->output_pads[i].name); +} + +static const AVFilterPad inputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_AUDIO, + .filter_frame = filter_frame, + .config_props = config_input, + }, + { NULL } +}; + +AVFilter ff_af_crossover = { + .name = "crossover", + .description = NULL_IF_CONFIG_SMALL("Split audio into per-bands streams."), + .priv_size = sizeof(CrossoverContext), + .priv_class = &crossover_class, + .init = init, + .uninit = uninit, + .query_formats = query_formats, + .inputs = inputs, + .outputs = NULL, + .flags = AVFILTER_FLAG_DYNAMIC_OUTPUTS, +}; diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index b44093d21b..45c9cf334c 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -82,6 +82,7 @@ extern AVFilter ff_af_chorus; extern AVFilter ff_af_compand; extern AVFilter ff_af_compensationdelay; extern AVFilter ff_af_crossfeed; +extern AVFilter ff_af_crossover; extern AVFilter ff_af_crystalizer; extern AVFilter ff_af_dcshift; extern AVFilter ff_af_drmeter;