FFmpeg af_silencedetect 的实现

本文分析 FFmpeg af_silencedetect 的实现。

一、af_silencedetect 的作用及基本原理

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

多大音量认为是静音由参数 noise 确定,默认是 -60dB 或 0.001;多长的连续时长认为是静音由参数 duration 确定,默认是 2 秒。参数 mono 为非 0 表示各个声道分别检测,默认是合并在一起检测。

合并在一起检测:比如认为 2 秒连续无声(或小声)认为是静音,那么其中一个声道达标,另一个声道在该时段内不达标也不认为是静音。

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

使用默认参数:

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

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

输出类似于:

[silencedetect @ 0x137f044d0] silence_start: 0 0x    
[silencedetect @ 0x137f044d0] silence_end: 4.0214 | silence_duration: 4.0214
[silencedetect @ 0x137f044d0] silence_start: 8.08879
[silencedetect @ 0x137f044d0] silence_end: 15.1732 | silence_duration: 7.08437
[silencedetect @ 0x137f044d0] silence_start: 64.6201

各个声道分别检测:

ffmpeg -i 0.mp3 -af "silencedetect=mono=1" -vn -sn -dn -f null /dev/null

输出类似于:

[silencedetect @ 0x152704190] channel: 0 | silence_start: 0
[silencedetect @ 0x152704190] channel: 1 | silence_start: 0
[silencedetect @ 0x152704190] channel: 0 | silence_end: 4.0214 | silence_duration: 4.0214
[silencedetect @ 0x152704190] channel: 1 | silence_end: 4.0214 | silence_duration: 4.0214
[silencedetect @ 0x152704190] channel: 0 | silence_start: 8.08879
[silencedetect @ 0x152704190] channel: 1 | silence_start: 8.08879
[silencedetect @ 0x152704190] channel: 0 | silence_end: 15.1732 | silence_duration: 7.08437
[silencedetect @ 0x152704190] channel: 1 | silence_end: 15.1732 | silence_duration: 7.08437
[silencedetect @ 0x152704190] channel: 0 | silence_start: 64.6201
[silencedetect @ 0x152704190] channel: 1 | silence_start: 64.6201
[silencedetect @ 0x152704190] channel: 0 | silence_end: 68.664 | silence_duration: 4.04385
[silencedetect @ 0x152704190] channel: 1 | silence_end: 68.664 | silence_duration: 4.04385

三、源码分析

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

分析 filter 一般从 static int filter_frame(AVFilterLink *inlink, AVFrame *in) 函数入手。不过由于要支持多种采样格式,需要在 static int config_input(AVFilterLink *inlink) 根据采样格式设置检测函数。

static int config_input(AVFilterLink *inlink)
{
    AVFilterContext *ctx = inlink->dst;
    SilenceDetectContext *s = ctx->priv;
    int c;

    s->channels = inlink->channels;
    // 调用的参数 duration 单位是秒,s->duration 的单位是微妙。下面将其转换为采样数。
    // 比如 44100 的 2 秒音频,采样数就是 44100 * 2 = 88200。
    s->duration = av_rescale(s->duration, inlink->sample_rate, AV_TIME_BASE);
    // 独立声道数。如果 mono 参数不为 0 则取音频的声道数,否则固定为 1 。
    // 实际上因为音频格式是交错模式,如果 mono 为 0,不管多少声道都当成单声道处理。
    s->independent_channels = s->mono ? s->channels : 1;
    // nb_null_samples 用于在检测过程中记录检测到的采样数。考虑到独立声道检测的情况所以定义为数组。下一次检测前会将其各个元素重置为 0 。
    s->nb_null_samples = av_mallocz_array(sizeof(*s->nb_null_samples), s->independent_channels);
    if (!s->nb_null_samples)
        return AVERROR(ENOMEM);
    // start 用于在检测过程中记录检测到的第一个采样所在索引。考虑到独立声道检测的情况所以定义为数组。下一次检测前会将其重置为 INT64_MIN 。
    s->start = av_malloc_array(sizeof(*s->start), s->independent_channels);
    if (!s->start)
        return AVERROR(ENOMEM);
    for (c = 0; c < s->independent_channels; c++)
        s->start[c] = INT64_MIN; // 使用魔术值(magic value) INT64_MIN 表示尚未检测到第一个符合条件的采样。

    // 根据音频的输入格式选择合适的静音检测函数。
    switch (inlink->format) {
    case AV_SAMPLE_FMT_DBL: s->silencedetect = silencedetect_dbl; break;
    case AV_SAMPLE_FMT_FLT: s->silencedetect = silencedetect_flt; break;
    case AV_SAMPLE_FMT_S32:
        s->noise *= INT32_MAX;
        s->silencedetect = silencedetect_s32;
        break;
    case AV_SAMPLE_FMT_S16:
        s->noise *= INT16_MAX;
        s->silencedetect = silencedetect_s16;
        break;
    }

    return 0;
}

nb_null_samples 用于累加达标的采样数,通过

silencedetect_dblsilencedetect_fltsilencedetect_s32 和 silencedetect_s16 由宏定义:


static void silencedetect_#
                                 int nb_samples, int64_t nb_samples_notify,      \
                                 AVRational time_base)                           \
{                                                                                \
    const type *p = (const type *)insamples->data[0];                            \
    const type noise = s->noise;                                                 \
    int i;                                                                       \
    
    // 遍历每一个采样进行检测                                                        \
    for (i = 0; i < nb_samples; i++, p++)                                        \
        update(s, insamples, *p < noise && *p > -noise, i,                       \
               nb_samples_notify, time_base);                                    \
}

SILENCE_DETECT(dbl, double)
SILENCE_DETECT(flt, float)
SILENCE_DETECT(s32, int32_t)
SILENCE_DETECT(s16, int16_t)

update 用于检测每一个采样:

static av_always_inline void update(SilenceDetectContext *s, AVFrame *insamples,
                                    int is_silence, int current_sample, int64_t nb_samples_notify,
                                    AVRational time_base)
{
    // 因为是音频交错模式,对于多声道各自检测,根据采样所在索引就能得出该采样属于哪个声道。
    int channel = current_sample % s->independent_channels;
    // 如果当前采样符合静音条件。
    if (is_silence) {
        if (s->start[channel] == INT64_MIN) { // 如果尚未开始
            s->nb_null_samples[channel]++;
            // 如果检测到足够多个采样则可以计算 `s->start[channel]` 并输出 `silence_start` 。
            if (s->nb_null_samples[channel] >= nb_samples_notify) {
                s->start[channel] = insamples->pts + av_rescale_q(current_sample / s->channels + 1 - nb_samples_notify * s->independent_channels / s->channels,
                        (AVRational){ 1, s->last_sample_rate }, time_base);
                set_meta(insamples, s->mono ? channel + 1 : 0, "silence_start",
                        av_ts2timestr(s->start[channel], &time_base));
                if (s->mono)
                    av_log(s, AV_LOG_INFO, "channel: %d | ", channel);
                av_log(s, AV_LOG_INFO, "silence_start: %s\n",
                        av_ts2timestr(s->start[channel], &time_base));
            }
        }
    } else {
        // 如果该采样不符合条件,判断之前的采样属于静音段,则表示该静音段结束了。输出 `silence_end` 和 `silence_duration`。
        if (s->start[channel] > INT64_MIN) {
            int64_t end_pts = insamples ? insamples->pts + av_rescale_q(current_sample / s->channels,
                    (AVRational){ 1, s->last_sample_rate }, time_base)
                    : s->frame_end;
            int64_t duration_ts = end_pts - s->start[channel];
            if (insamples) {
                set_meta(insamples, s->mono ? channel + 1 : 0, "silence_end",
                        av_ts2timestr(end_pts, &time_base));
                set_meta(insamples, s->mono ? channel + 1 : 0, "silence_duration",
                        av_ts2timestr(duration_ts, &time_base));
            }
            if (s->mono)
                av_log(s, AV_LOG_INFO, "channel: %d | ", channel);
            av_log(s, AV_LOG_INFO, "silence_end: %s | silence_duration: %s\n",
                    av_ts2timestr(end_pts, &time_base),
                    av_ts2timestr(duration_ts, &time_base));
        }

        // 重置辅助变量。
        s->nb_null_samples[channel] = 0;
        s->start[channel] = INT64_MIN;
    }
}

四、C# 简单实现

public class VolumeUtils
{
    /// <summary>
    /// 静音检测
    /// </summary>
    /// <param name="raw">PCM 数据。支持 S16LE 格式,单/双声道。</param>
    /// <param name="offset">数据偏移</param>
    /// <param name="length">数据长度</param>
    /// <param name="blockAlign">块对其长度。因为只检测第一声道,需该值来跳过数据。</param>
    /// <param name="sampleRate">采样率。配合 minDuration 使用。</param>
    /// <param name="noise">声量。取值范围:0 ~ 1。</param>
    /// <param name="minDuration">最小时长。 配合 sampleRate 使用。</param>
    /// <param name="detectMax">最多检测出多少段后终止。 0 表示检测全部段。</param>
    /// <returns>静音段集合</returns>
    public static List<SilencePeriod> SilenceDetect(byte[] raw, 
    int offset, 
    int length, 
    int blockAlign, 
    double sampleRate, 
    double noise, 
    double minDuration, 
    int detectMax = 0)
    {
        var result = new List<SilencePeriod>();

        noise = noise * Int16.MaxValue;
        var numberOfSamplesNotify = (int)(minDuration * sampleRate);

        var numberOfSilenceSamples = 0;
        var startSample = Int32.MinValue;

        for (var i = offset; i < length; i += blockAlign)
        {
            var sample = BitConverter.ToInt16(raw, i);
            var isSilence = sample < noise && sample > -noise;
            if (isSilence)
            {
                numberOfSilenceSamples++;
                if (startSample == Int32.MinValue)
                {
                    // 开始
                    startSample = i / blockAlign;
                }
            }
            else
            {
                if (startSample != Int32.MinValue && numberOfSilenceSamples >= numberOfSamplesNotify)
                {
                    // 结束
                    var silencePeriod = new SilencePeriod
                    {
                        Start = startSample * blockAlign,
                        Length = numberOfSilenceSamples * blockAlign,
                        Duration = numberOfSilenceSamples / sampleRate
                    };
                    silencePeriod.StartTS = (double)silencePeriod.Start / blockAlign / sampleRate;
                    result.Add(silencePeriod);

                    if(detectMax > 0 && result.Count == detectMax)
                    {
                        return result;
                    }
                }
                numberOfSilenceSamples = 0;
                startSample = Int32.MinValue;
            }
        }

        return result;
    }

    public class SilencePeriod
    {
        public int Start { get; set; }

        public int Length { get; set; }

        public double StartTS { get; set; }

        public double Duration { get; set; }

        public override string ToString()
        {
            return $"{{Start={Start},Length={Length},StartTS={StartTS:0.000},Duration={Duration:0.000}}}";
        }
    }
}

由于本人需要,对于多声道本方法也只检测第一个声道。在多个声道音量不是交错的情况下有助于提升效率。

参考资料

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

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

(0)

相关推荐

发表回复

登录后才能评论