EBU R.128协议
引用链接
EBU
的全称为European Broadcasting Union ,既欧洲广播联盟,为欧洲与北非各广播业者(包含广播电台与电视台)的合作组织,成立于1950年2月12日,有五十多个正式加盟国,总部位于瑞士日内瓦,目前中国是EBU的准会员。
EBU R.128
实质上是欧洲广播联盟出的一个关于响度控制的建议书,该建议书在ITU-R BS.1770标准的(国际广播联盟规定的音频节目响度及真峰值的测量算法)基础之上,对响度的被测主体、积分窗长等细节作了更加明确的定义。
响度和动态范围的定义与相关的计量单位
-
响度(Loudness)
的定义: 在声学中,响度是人对声音压力的主观感受,也是听觉的一种属性,根据这种属性,可以对声音进行排序,如从安静排列到响亮,亦或者从响亮排列至安静。响度虽是声音的物理属性,但其却与听者的生理感受,心理感受息息相关,准确来说这属于心理物理学的范畴了。 -
音量单位dBFS与dB
: 首先dBFS与dB都是分贝的意思,而分贝一般指的就是音量大小。 dBFS全称decibels relative to full scale,什么叫full scale呢?既是满刻度,具体来讲是将0作为这种满刻度单位的最大值(也就是说此类值均为负数),凡是带FS后缀的计量单位都可这样理解(如LUFS或LKFS)。dB常用来形容一个音量到目标音量的“距离”,比如需要将一个信号从-4dBFS提升至-1dBFS,那么就需要对-4dBFS做3dB的提升操作。在业界,dBFS常被简称为dB,如-4dB等等。 -
动态范围
: 用通俗不严谨的话来说,动态范围指的是最大音量与最小音量之间的“距离”值,这个“距离”值就被称为动态范围,比如一段音频中的最大音量为-2dBFS,最小音量为-8dBFS,那么它的动态范围就是6dB。站在AD转换器的角度来说,动态范围指的是信号可以达到的最大不失真电平与本底噪声的差值。 -
EBU响度单位LUFS
: LUFS的全称为Loudness units relative to full scale,既相对完整刻度的响度单位。广电行业中,LUFS是有一个目标响度值的,那就是-23LUFS,该值是广播世界的终极响度参考值!LUFS的数值越大,其响度就越大。至于为什么是-23LUFS,笔者暂未做深入研究。另外,LKFS是国际广播联盟用的单位,与LUFS差不多。 -
EBU响度单位LU
: LU的全称为Loudness units,通常1LU“等同于”1dB(仅仅是理解层面上的“相等”),常用来形容两个LUFS之间的“距离”,如-25LUFS与-18LUFS间差了7LU。综上所述,dBFS与LUFS, dB与LU,虽测量算法不同,但其含义却是相似的。
响度示值,响度范围和真峰值的介绍
瞬时响度 Momentary Loudness
: 瞬时响度指的是400MS(毫秒)内的响度,并每过100ms进行一次更新,响应相当灵敏。短期响度 Short Term Loudness
: 短期响度是提取3秒以内的信号,表达的是3S内的平均响度。综合响度 Integrated Loudness
: 综合响度指的是整段音频的平均响度。响度范围 Loudness Range
: 响度范围,经常被简称为LRA,其代表的是两个LUFS之间的“距离”,如一段音频的最小响度为-25LUFS,最大响度为-18LUFS,其间差了7LU,既代表该音频的响度范围为7LU。响度范围与动态范围表达的是一个意思,只是测量方法不一样。真峰值 True Peak
: 在说真峰值之前,先用通俗的话来说说峰值是什么,峰值就是波形振幅的波峰值(波形有波谷和波峰之分),说白了就是波形瞬态的最大电平值。那为什么要在峰值之前加个“真”字呢?原因是这样的,以前有一种峰值测量方法,只有当峰值的持续时间超过一定的时间时(通常是几毫秒),才会计算出比较准确的峰值,这种测量算法被称为准峰值(QPPM)。而EBU给出了一种新的算法,无论它的持续时间有多短,都能正确的测量出其波形的真实峰值水平,故此,称其为真峰值。那么响度表中的真峰值指示条是用来干嘛的呢?它是用来检测信号有没有过载的,一旦信号过载就会被削波造成信号失真,正因如此,EBU给出建议为真峰值不要超过-1dBTP,目的就是防止信号产生过载削波,其单位为dBTP(dB True Peak真实峰值),对应的值是dBFS值。
双遍滤镜(loudness)
主要用于音频向度的精准标准化
,其核心作用是通过两次处理(Double Pass)分析音频特征并应用目标响度参数,确保输出音频符合行业标准(如EBU R128 或 ITU-R BS.1770),同时优化动态范围控制
使用方式
One Pass: 调用滤镜分析得到result
std::string AudioAnalyze::analyzeAudio(const std::string &path,int trimIn,int trimOut) {
Producer producer(getGlobalProfile(), nullptr,path.c_str());
if(producer.get_length() <= 1 || producer.get_int("audio_index") == -1){
std::cout << "analyzeAudio no effect" << std::endl;
return "";
}
if(trimOut > 0){
producer.set_in_and_out(trimIn,trimOut);
}
Filter filter(getGlobalProfile(),"loudness");
filter.set("program",-29);
producer.attach(filter);
Consumer consumer(getGlobalProfile(), "null");
consumer.set("terminate_on_pause",0);
consumer.connect(producer);
consumer.start();
while (!consumer.is_stopped()) {
// ignore
if(filter.property_exists("results")){
const char *results = filter.get("results");
if(results != nullptr) {
std::cout << "analyzeAudio results:" << results << std::endl;
consumer.stop();
return results;
} else {
std::cout << "analyzeAudio results:" << "error: key is null" << std::endl;
return "";
}
}
}
return "";
}
Two Pass: 把result写入滤镜参数中,则会自动调整目标响度
std::shared_ptr<Filter> loudnessFilter{nullptr};
if (!results.empty()) {
loudnessFilter = std::make_shared<Filter>(getGlobalProfile(), "loudness");
loudnessFilter->set("program", configuration.aiDefaultLoudness);
loudnessFilter->set("disable", 0);
loudnessFilter->set("reload", 1);
auto length = results.length() + 1;
char *result = (char *) malloc(results.length() + 1);
memcpy((void *) result, (const void *) results.c_str(), results.length());
result[length - 1] = '\0';
loudnessFilter->set("results", result);
}
loudnessFilter->set_in_and_out(trimIn, trimOut);
childClipInfo->producer->attach(*loudnessFilter);
关键源码分析
filter_get_audio
static int filter_get_audio(mlt_frame frame,
void **buffer,
mlt_audio_format *format,
int *frequency,
int *channels,
int *samples)
{
mlt_filter filter = mlt_frame_pop_audio(frame);
mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
mlt_service_lock(MLT_FILTER_SERVICE(filter));
// Get the producer's audio
*format = mlt_audio_f32le;
mlt_frame_get_audio(frame, buffer, format, frequency, channels, samples);
char *results = mlt_properties_get(properties, "results");
if (buffer && buffer[0] && results && strcmp(results, "")) {
// Two Pass的时候走这个函数
apply(filter, frame, buffer, format, frequency, channels, samples);
} else {
// One Pass的时候走这个函数
analyze(filter, frame, buffer, format, frequency, channels, samples);
}
mlt_service_unlock(MLT_FILTER_SERVICE(filter));
return 0;
}
analyze:分析得到原音频信息
static void analyze(mlt_filter filter,
mlt_frame frame,
void **buffer,
mlt_audio_format *format,
int *frequency,
int *channels,
int *samples)
{
private_data *private = (private_data *) filter->child;
mlt_position pos = mlt_filter_get_position(filter, frame);
// If any frames are skipped, analysis data will be incomplete.
// 作用:确保音频帧按顺序处理,跳过帧会导致分析中断。
// 原因:EBU R128 要求连续分析,缺失帧会导致积分响度计算错误。
if (private->analyze && pos != private->last_position + 1) {
mlt_log_error(MLT_FILTER_SERVICE(filter), "Analysis Failed: Bad frame sequence\n");
destroy_analyze_data(filter);
}
// Analyze Audio
if (!private->analyze && pos == 0) {
init_analyze_data(filter, *channels, *frequency);
}
if (private->analyze) {
// 作用:将当前音频帧数据(buffer)输入 EBU R128 分析器,更新内部状态。
ebur128_add_frames_float(private->analyze->state, *buffer, *samples);
if (pos + 1 == mlt_filter_get_length2(filter, frame)) {
mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
double loudness = 0.0;
double range = 0.0;
double tmpPeak = 0.0;
double peak = 0.0;
int i = 0;
char result[MAX_RESULT_SIZE];
// 计算整体响度
ebur128_loudness_global(private->analyze->state, &loudness);
// 计算响度范围
ebur128_loudness_range(private->analyze->state, &range);
// 获取声道峰值
for (i = 0; i < *channels; i++) {
ebur128_sample_peak(private->analyze->state, i, &tmpPeak);
if (tmpPeak > peak) {
peak = tmpPeak;
}
}
// 格式化结果并存储
snprintf(result, MAX_RESULT_SIZE, "L: %lf\tR: %lf\tP %lf", loudness, range, peak);
result[MAX_RESULT_SIZE - 1] = '\0';
mlt_log_info(MLT_FILTER_SERVICE(filter), "Stored results: %s\n", result);
mlt_properties_set(properties, "results", result);
destroy_analyze_data(filter);
}
private->last_position = pos;
}
}
ebur128_gated_loudness: 计算 综合响度(Integrated Loudness)
这段代码是 EBU R128 综合响度(Integrated Loudness) 的核心计算逻辑,用于在音频处理中通过门限控制(Gating)剔除低能量部分,最终得到符合标准的响度值。
static int
ebur128_gated_loudness(ebur128_state** sts, size_t size, double* out) {
struct ebur128_dq_entry* it;
double gated_loudness = 0.0;
double relative_threshold = 0.0;
size_t above_thresh_counter = 0;
size_t i, j, start_index;
// 检查输入的音频状态结构体数组 sts 是否启用了综合响度模式(EBUR128_MODE_I)。若未启用,返回错误。
// 综合响度模式是 EBU R128 的核心功能,需通过 ebur128_init 初始化时设置模式标志
for (i = 0; i < size; i++) {
if (sts[i] && (sts[i]->mode & EBUR128_MODE_I) != EBUR128_MODE_I) {
return EBUR128_ERROR_INVALID_MODE;
}
}
// 作用:遍历所有音频状态实例,调用 ebur128_calc_relative_threshold 计算 相对门限
// 相对门限:基于音频块能量分布,动态确定一个阈值,剔除低于该值的部分(如静音段或背景噪声)
// 公式:相对门限 = 平均能量 × 相对门限因子(默认-10 dB,即 relative_gate_factor = 0.1)
// 引用:EBU R128 要求综合响度计算需忽略低于动态门限的音频段,以聚焦主要节目内容
for (i = 0; i < size; i++) {
if (!sts[i]) {
continue;
}
ebur128_calc_relative_threshold(sts[i], &above_thresh_counter,
&relative_threshold);
}
if (!above_thresh_counter) {
*out = -HUGE_VAL;
return EBUR128_SUCCESS;
}
relative_threshold /= (double) above_thresh_counter;
relative_threshold *= relative_gate_factor;
// 作用:根据相对门限值,确定直方图中高于门限的起始索引 start_index。
// 直方图优化:若启用直方图模式(use_histogram),直接遍历预计算的能量区间,跳过低能量部分。
// 未启用直方图:则需遍历块链表(block_list),逐个检查能量是否高于门限。
// 引用:直方图加速了能量统计,是 EBU R128 实现高效计算的关键优化
above_thresh_counter = 0;
if (relative_threshold < histogram_energy_boundaries[0]) {
start_index = 0;
} else {
start_index = find_histogram_index(relative_threshold);
if (relative_threshold > histogram_energies[start_index]) {
++start_index;
}
}
// 作用:统计所有高于门限的音频块能量总和 gated_loudness 及其数量 above_thresh_counter。
// 直方图模式:通过预计算的能量值 histogram_energies 和直方图计数快速累加。
// 块链表模式:遍历链表,筛选符合门限的块并累加能量。
// 引用:EBU R128 要求忽略低于门限的音频段,例如静音或低电平背景声
for (i = 0; i < size; i++) {
if (!sts[i]) {
continue;
}
if (sts[i]->d->use_histogram) {
for (j = start_index; j < 1000; ++j) {
gated_loudness +=
sts[i]->d->block_energy_histogram[j] * histogram_energies[j];
above_thresh_counter += sts[i]->d->block_energy_histogram[j];
}
} else {
STAILQ_FOREACH(it, &sts[i]->d->block_list, entries) {
if (it->z >= relative_threshold) {
++above_thresh_counter;
gated_loudness += it->z;
}
}
}
}
if (!above_thresh_counter) {
*out = -HUGE_VAL;
return EBUR128_SUCCESS;
}
// 作用:
// 平均能量:将累积的能量总和除以有效块数量,得到平均能量。
// 能量转响度:调用 ebur128_energy_to_loudness 将能量转换为 LUFS(Loudness Units Full Scale)。
// 公式:LUFS = 10 × log₁₀(平均能量) - 绝对偏移值(如-0.691 dB,用于校准滤波器响应)
// 引用:LUFS 是 EBU R128 的标准化响度单位,综合响度目标值为 -23 LUFS(广播领域)
gated_loudness /= (double) above_thresh_counter;
*out = ebur128_energy_to_loudness(gated_loudness);
return EBUR128_SUCCESS;
}
ebur128_loudness_range_multiple:计算响度范围(Loudness Range, LRA)
用于衡量音频节目中响度的动态变化
计算多个音频流的综合响度范围(LRA),即节目中最响的 95% 部分与最安静的 10% 部分之间的响度差(单位:LU)
输入:
sts:多个 ebur128_state 状态对象的数组(支持多路音频流合并计算)。
size:数组长度。
输出:
out:计算得到的 LRA 值。
返回值:错误码(成功为 EBUR128_SUCCESS)。
int ebur128_loudness_range_multiple(ebur128_state** sts,
size_t size,
double* out) {
size_t i, j;
struct ebur128_dq_entry* it;
double* stl_vector;
size_t stl_size;
double* stl_relgated;
size_t stl_relgated_size;
double stl_power, stl_integrated;
/* High and low percentile energy */
double h_en, l_en;
int use_histogram = 0;
// 模式校验
// 必须启用 LRA 模式:所有状态对象需通过 EBUR128_MODE_LRA 初始化。
// 直方图模式一致性:所有状态对象需统一启用或禁用直方图(EBUR128_MODE_HISTOGRAM)。
for (i = 0; i < size; ++i) {
if (sts[i]) {
if ((sts[i]->mode & EBUR128_MODE_LRA) != EBUR128_MODE_LRA) {
return EBUR128_ERROR_INVALID_MODE;
}
if (i == 0 && sts[i]->mode & EBUR128_MODE_HISTOGRAM) {
use_histogram = 1;
} else if (use_histogram != !!(sts[i]->mode & EBUR128_MODE_HISTOGRAM)) {
return EBUR128_ERROR_INVALID_MODE;
}
}
}
// 直方图模式计算
if (use_histogram) {
unsigned long hist[1000] = { 0 };
size_t percentile_low, percentile_high;
size_t index;
stl_size = 0;
stl_power = 0.0;
for (i = 0; i < size; ++i) {
if (!sts[i]) {
continue;
}
for (j = 0; j < 1000; ++j) {
// 直方图累加:合并所有音频流的短期能量直方图(3秒窗口)。
hist[j] += sts[i]->d->short_term_block_energy_histogram[j];
stl_size += sts[i]->d->short_term_block_energy_histogram[j];
stl_power += sts[i]->d->short_term_block_energy_histogram[j] *
histogram_energies[j];
}
}
if (!stl_size) {
*out = 0.0;
return EBUR128_SUCCESS;
}
//门限处理:排除低于平均能量 -20 dB 的块(minus_twenty_decibels = 0.01)。
// 平均能量
stl_power /= stl_size;
// // 应用-20 dB门限
stl_integrated = minus_twenty_decibels * stl_power;
if (stl_integrated < histogram_energy_boundaries[0]) {
index = 0;
} else {
// 找到门限对应的直方图索引(直方图优化:跳过低于门限的区间,减少计算量。)
index = find_histogram_index(stl_integrated);
if (stl_integrated > histogram_energies[index]) {
++index;
}
}
stl_size = 0;
for (j = index; j < 1000; ++j) {
stl_size += hist[j];
}
if (!stl_size) {
*out = 0.0;
return EBUR128_SUCCESS;
}
// 遍历直方图:累加直方图计数,找到对应百分位的能量值。
// 10% 低百分位
percentile_low = (size_t) ((stl_size - 1) * 0.1 + 0.5);
// 95% 高百分位
percentile_high = (size_t) ((stl_size - 1) * 0.95 + 0.5);
stl_size = 0;
j = index;
while (stl_size <= percentile_low) {
stl_size += hist[j++];
}
l_en = histogram_energies[j - 1];
while (stl_size <= percentile_high) {
stl_size += hist[j++];
}
h_en = histogram_energies[j - 1];
// 计算 LRA(将能量值转换为响度(LU),计算高低百分位的差值
*out = ebur128_energy_to_loudness(h_en) - ebur128_energy_to_loudness(l_en);
return EBUR128_SUCCESS;
}
// 非直方图模式计算同理
stl_size = 0;
for (i = 0; i < size; ++i) {
if (!sts[i]) {
continue;
}
STAILQ_FOREACH(it, &sts[i]->d->short_term_block_list, entries) {
++stl_size;
}
}
if (!stl_size) {
*out = 0.0;
return EBUR128_SUCCESS;
}
stl_vector = (double*) malloc(stl_size * sizeof(double));
if (!stl_vector) {
return EBUR128_ERROR_NOMEM;
}
j = 0;
for (i = 0; i < size; ++i) {
if (!sts[i]) {
continue;
}
STAILQ_FOREACH(it, &sts[i]->d->short_term_block_list, entries) {
stl_vector[j] = it->z;
++j;
}
}
qsort(stl_vector, stl_size, sizeof(double), ebur128_double_cmp);
stl_power = 0.0;
for (i = 0; i < stl_size; ++i) {
stl_power += stl_vector[i];
}
stl_power /= (double) stl_size;
stl_integrated = minus_twenty_decibels * stl_power;
stl_relgated = stl_vector;
stl_relgated_size = stl_size;
while (stl_relgated_size > 0 && *stl_relgated < stl_integrated) {
++stl_relgated;
--stl_relgated_size;
}
if (stl_relgated_size) {
h_en = stl_relgated[(size_t) ((stl_relgated_size - 1) * 0.95 + 0.5)];
l_en = stl_relgated[(size_t) ((stl_relgated_size - 1) * 0.1 + 0.5)];
free(stl_vector);
*out = ebur128_energy_to_loudness(h_en) - ebur128_energy_to_loudness(l_en);
} else {
free(stl_vector);
*out = 0.0;
}
return EBUR128_SUCCESS;
}
ebur128_sample_peak:获取指定声道采样峰值(Sample Peak)
作用:获取音频流中指定声道的采样峰值电平(单位:dBFS)。
采样峰值(Sample Peak):音频信号在数字域中的最大绝对值,未经插值处理,直接来自采样点。
用途:检测音频是否削波(超过 0 dBFS),确保符合广播或流媒体的响度标准。
int ebur128_sample_peak(ebur128_state* st,
unsigned int channel_number,
double* out) {
if ((st->mode & EBUR128_MODE_SAMPLE_PEAK) != EBUR128_MODE_SAMPLE_PEAK) {
return EBUR128_ERROR_INVALID_MODE;
}
if (channel_number >= st->channels) {
return EBUR128_ERROR_INVALID_CHANNEL_INDEX;
}
*out = st->d->sample_peak[channel_number];
return EBUR128_SUCCESS;
}
apply
核心目标:将音频的响度调整到用户设定的目标值(如 EBU R128 规定的 -23 LUFS)。
输入依赖:需要预先通过分析阶段获取当前音频的响度(in_loudness)、响度范围(in_range)和峰值(in_peak)。
操作:计算增益系数并应用到音频数据,使其响度匹配目标值。
static void apply(mlt_filter filter,
mlt_frame frame,
void **buffer,
mlt_audio_format *format,
int *frequency,
int *channels,
int *samples)
{
mlt_properties properties = MLT_FILTER_PROPERTIES(filter);
char *results = mlt_properties_get(properties, "results");
double in_loudness;
double in_range;
double in_peak;
int scan_return = sscanf(results, "L: %lf\tR: %lf\tP %lf", &in_loudness, &in_range, &in_peak);
if (scan_return != 3) {
mlt_log_error(MLT_FILTER_SERVICE(filter), "Unable to load results: %s\n", results);
} else {
// 计算增益系数
// 增益公式是固定的
double target_db = mlt_properties_get_double(properties, "program");
double delta_db = target_db - in_loudness;
double coeff = delta_db > -90.0 ? pow(10.0, delta_db / 20.0) : 0.0;
float *p = *buffer;
int count = *samples * *channels;
// 线性增益:将每个音频样本乘以计算出的增益系数。
// 声道处理:遍历所有声道和样本,统一应用增益。
for (int i = 0; i < count; i++) {
p[i] = p[i] * coeff;
}
}
}