FFmpeg filters 分析:af_silencedetect

語言: CN / TW / HK

本文分析 FFmpeg af_silencedetect 的實現。

二、af_silencedetect 的作用及基本原理

二、af_silencedetect 的作用是獲取音訊的最大音量、平均音量以及音量直方圖。

它只支援 AV_SAMPLE_FMT_S16AV_SAMPLE_FMT_S32AV_SAMPLE_FMT_FLTAV_SAMPLE_FMT_DBL 這四種格式——如果不是當然 FFmpeg 能夠自動轉換。

多大音量認為是靜音由引數 noise 確定,預設是 -60dB0.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_s32silencedetect_s16 由巨集定義:

#define SILENCE_DETECT(name, type)                                               \
static void silencedetect_##name(SilenceDetectContext *s, AVFrame *insamples,    \
                                 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}}}";
        }
    }
}

注意:由於本人需要,對於多聲道本方法也只檢測第一個聲道。在多個聲道音量不是交錯的情況下有助於提升效率。