Alby's blog

世上没有巧合,只有巧合的假象。

0%

FFmpeg filters 分析:af_volumedetect

一、概述

本文分析 FFmpeg af_volumedetect 的实现。

二、af_volumedetect 的作用及基本原理

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

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

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

如果要计算平均音量,

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

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

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

输出类似于:

1
2
3
4
5
6
7
8
[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) 函数入手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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 函数用于计算并打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

// 最小分贝 -91dB
#define MAX_DB 91

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# 简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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;
}
}

参考资料