MLT媒体程序框架03:滤镜——loudness

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;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值