FFmpeg af_volumedetect 的实现

本文分析 FFmpeg af_volumedetect 的实现。

一、af_volumedetect 的作用及基本原理

af_volumedetect 的作用是获取音频的最大音量、平均音量以及音量直方图。
它只支持 AV_SAMPLE_FMT_S16 和 AV_SAMPLE_FMT_S16P 这两种格式——如果不是当然 FFmpeg 能够自动转换。

如果只是获取最大音量,只需要返回音频采样绝对值最大的即可,如果需要返回分贝,则计算:

-log10(pow(d, 2)) * 10 // 计算 dB。 d 为峰值。

二、在调用 ffmpeg 程序时使用 af_volumedetect

ffmpeg -i input.mp3 -af "volumedetect" -vn -sn -dn -f null /dev/null

在 Windows 中使用需将 /dev/null 替换为 NUL
-vn、 -sn 和 -dn 告知 FFmpeg 忽略非音频流。能够在分析时避免不必要的操作从而更快速.

输出类似于:

[Parsed_volumedetect_0 @ 0x1328042c0] n_samples: 16815744   // 音频包含的采样数
[Parsed_volumedetect_0 @ 0x1328042c0] mean_volume: -25.4 dB // 平均音量
[Parsed_volumedetect_0 @ 0x1328042c0] max_volume: -6.6 dB   // 最大音量
[Parsed_volumedetect_0 @ 0x1328042c0] histogram_6db: 35     // 大于 -7dB 并且小于或等于 -6dB 的采样数是 35 
[Parsed_volumedetect_0 @ 0x1328042c0] histogram_7db: 2354   // 大于 -8dB 并且小于或等于 -7dB 的采样数是 2354
[Parsed_volumedetect_0 @ 0x1328042c0] histogram_8db: 4969
[Parsed_volumedetect_0 @ 0x1328042c0] histogram_9db: 8978
[Parsed_volumedetect_0 @ 0x1328042c0] histogram_10db: 35545

三、源码分析

af_volumedetect 源码位于 ffmpg/libavfilter/af_volumedetect.c 中。

分析 filter 一般从 static int filter_frame(AVFilterLink *inlink, AVFrame *in) 函数入手。

// 0x10001 65536
// 0x8000  32768

typedef struct VolDetectContext {
    /**
     * Number of samples at each PCM value.
     * histogram[0x8000 + i] is the number of samples at value i.
     * The extra element is there for symmetry.
     */
    // S16 范围是 -32768 ~ 32767,即 65536 个数。histogram 统计每个采样的数量,为了和数组的索引匹配,会将所有采样都加 32768(0x8000)。
    // histogram 是采样值与其数量的关系。
    uint64_t histogram[0x10001];
} VolDetectContext;

static int filter_frame(AVFilterLink *inlink, AVFrame *samples)
{
    AVFilterContext *ctx = inlink->dst;
    VolDetectContext *vd = ctx->priv;
    int nb_samples  = samples->nb_samples;
    int nb_channels = samples->channels;
    int nb_planes   = nb_channels;
    int plane, i;
    int16_t *pcm;

    if (!av_sample_fmt_is_planar(samples->format)) {
        nb_samples *= nb_channels;
        nb_planes = 1;
    }
    // 统计每个采样值的采样数。
    for (plane = 0; plane < nb_planes; plane++) {
        pcm = (int16_t *)samples->extended_data[plane];
        for (i = 0; i < nb_samples; i++)
            vd->histogram[pcm[i] + 0x8000]++;
    }

    return ff_filter_frame(inlink->dst->outputs[0], samples);
}

print_stats 函数用于计算并打印。

// 最小分贝 -91dB


static inline double logdb(uint64_t v)
{
    // 由于传入的 v 是 Amplitude 值加了 0x8000 再进行了平方,这里做相关逆运算。
    double d = v / (double)(0x8000 * 0x8000);
    if (!v)
        return MAX_DB;
    return -log10(d) * 10;
}

static void print_stats(AVFilterContext *ctx)
{
    VolDetectContext *vd = ctx->priv;
    int i, max_volume, shift;
    uint64_t nb_samples = 0, power = 0, nb_samples_shift = 0, sum = 0;
    uint64_t histdb[MAX_DB + 1] = { 0 };

    // 其实总的采样数 nb_samples 可以定义在 VolDetectContext 中,在 filter_frame 进行计算以避免本次循环。
    for (i = 0; i < 0x10000; i++)
        nb_samples += vd->histogram[i];
    av_log(ctx, AV_LOG_INFO, "n_samples: %"PRId64"\n", nb_samples);
    if (!nb_samples)
        return;

    /* If nb_samples > 1<<34, there is a risk of overflow in the
       multiplication or the sum: shift all histogram values to avoid that.
       The total number of samples must be recomputed to avoid rounding
       errors. */
    shift = av_log2(nb_samples >> 33);
    for (i = 0; i < 0x10000; i++) {
        nb_samples_shift += vd->histogram[i] >> shift;
        power += (i - 0x8000) * (i - 0x8000) * (vd->histogram[i] >> shift);
    }
    if (!nb_samples_shift)
        return;
    power = (power + nb_samples_shift / 2) / nb_samples_shift;
    av_assert0(power <= 0x8000 * 0x8000);
    av_log(ctx, AV_LOG_INFO, "mean_volume: %.1f dB\n", -logdb(power));

    max_volume = 0x8000;
    // 倒序搜索 histogram,第一个有采样数的是最大音量值。
    while (max_volume > 0 && !vd->histogram[0x8000 + max_volume] &&
                             !vd->histogram[0x8000 - max_volume])
        max_volume--;
    av_log(ctx, AV_LOG_INFO, "max_volume: %.1f dB\n", -logdb(max_volume * max_volume));

    // histdb: dB 直方图。用于保存 0dB ~ 91dB 的采样数。
    for (i = 0; i < 0x10000; i++)
        histdb[(int)logdb((i - 0x8000) * (i - 0x8000))] += vd->histogram[i];
    // 不输出整个直方图,并且忽略采样数为 0 的条目。
    for (i = 0; i <= MAX_DB && !histdb[i]; i++);
    for (; i <= MAX_DB && sum < nb_samples / 1000; i++) {
        av_log(ctx, AV_LOG_INFO, "histogram_%ddb: %"PRId64"\n", i, histdb[i]);
        sum += histdb[i];
    }
}

四、C# 简单实现

public class VolumeUtils
{
    private const int MAX_DB = 91;

    private static double LogdB(ulong v)
    {
        // 0x8000  32768
        double d = v / (double)(0x8000 * 0x8000);
        if (v == 0)
        {
            return MAX_DB;
        }
        //20log_10(x^0.5) = 10log_10(x)
        return -Math.Log10(d) * 10;
    }

    /// <summary>
    /// 音量检测
    /// </summary>
    /// <param name="raw">PCM 数据。S16LE 格式(-32768 ~ 32767)。</param>
    /// <param name="offset">偏移</param>
    /// <param name="length">数据长度。必须是偶数</param>
    /// <param name="maxVolume">最大音量</param>
    /// <param name="meanVolume">平均音量</param>
    /// <returns>音量从大到小的直方图(部分)</returns>
    public static List<KeyValuePair<int, ulong>> VolumeDetect(byte[] raw, int offset, int length, out double maxVolume, out double meanVolume)
    {
        // MSE: mean square energy
        // 0x10001 65536
        // 0x8000  32768

        // S16 范围是 -32768 ~ 32767,即 65536 个数。histogram 统计每个采样的数量,为了和数组的索引匹配,会将所有采样都加 32768(0x8000)。
        // histogram 是采样值与其数量的关系。
        var histogram = new ulong[0x10001];

        // 统计每个采样的数量。
        ulong nb_samples = length / sizeof(short);
        for (var i = offset; i < nb_samples; i++)
        {
            var sample = BitConverter.ToInt16(raw, i * sizeof(short));
            histogram[sample + 0x8000]++;
        }

        ulong power = 0, nb_samples_shift = 0;

        /* If nb_samples > 1<<34, there is a risk of overflow in the
        multiplication or the sum: shift all histogram values to avoid that.
        The total number of samples must be recomputed to avoid rounding
        errors. */
        int shift = (int)Math.Log(nb_samples >> 33, 2);
        for (var i = 0; i < 0x10000; i++)
        {
            nb_samples_shift += histogram[i] >> shift;
            power += (ulong)(i - 0x8000) * (ulong)(i - 0x8000) * (histogram[i] >> shift);
        }
        if (nb_samples_shift == 0) {
            maxVolume = 0;
            meanVolume = 0;
            return new List<KeyValuePair<int, ulong>>(0);
        }

        power = (power + nb_samples_shift / 2) / nb_samples_shift;

        // mean volume
        meanVolume = -LogdB(power);

        // 倒序搜索 histogram,第一个有采样数的是最大音量值。
        int max_volume = 0x8000;
        while (max_volume > 0 && histogram[0x8000 + max_volume] == 0 && histogram[0x8000 - max_volume] == 0)
            max_volume--;

        // max volume
        maxVolume = -LogdB((ulong)(max_volume * max_volume));

        // histdb: dB 直方图。用于保存 0dB ~ 91dB 的采样数。
        var histdb = new ulong[MAX_DB + 1];
        for (var i = 0; i < 0x10000; i++)
        {
            histdb[(int)LogdB((ulong)((i - 0x8000) * (i - 0x8000)))] += histogram[i];
        }

        // 不返回整个直方图,并且忽略采样数为 0 的条目。
        var histdBResult = new List<KeyValuePair<int, ulong>>();
        var idx = 0;
        var sum = 0;
        for (idx = 0; idx <= MAX_DB && histdb[idx] == 0; idx++) ;
        for (; idx <= MAX_DB && sum < nb_samples / 1000; idx++)
        {
            histdBResult.Add(new KeyValuePair<int, ulong>(idx, histdb[idx]));
            sum += histdb[idx];
        }

        return histdBResult;
    }
}

参考资料

原文链接:https://blog.tubumu.com/2021/12/03/ffmpeg-filters-af-volumedetect/

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论