3

FFmpeg filters 分析:af_volumedetect

 2 years ago
source link: http://blog.tubumu.com/2021/12/03/ffmpeg-filters-af-volumedetect/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文分析 FFmpeg af_volumedetect 的实现。

二、af_volumedetect 的作用及基本原理

二、af_volumedetect 的作用是获取音频的最大音量、平均音量以及音量直方图。
它只支持 AV_SAMPLE_FMT_S16AV_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
#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# 简单实现

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;
}
}

FFmpeg filters 官网文档: volumedetect


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK