音频混音原理

混音本质就是多路独立音频信号,经过音量、声像、均衡、压缩、效果器处理后,合并为单 / 双 / 多声道输出信号,解决声音冲突、塑造空间层次、平衡响度,最终得到清晰好听的成品音频。

一、基础原理

混音就是两个信号的音频叠加,比如两路混音就是两个信号叠加,可以通过模拟信号叠加和数字信号叠加实现。

模拟波形叠加:

混音输出波形= 波形A+波形B。 波形叠加存在一个问题, 同向波形相加会音量变大,异向会相互抵消。

数字音频叠加

混音输出采样= 音频1采样+音频2采样。在数字音频中,所有采样点数值直接相加,会超过设备最大电平就会削波失真(刺耳破音),因此混音首要目标就是控制总电平不超限。

综上可以看出,不论是模拟波形直接叠加和数字音频直接叠加,都存在一定的问题,只要解决了相关的问题,就可以实现混音。

如果是专业研究,可以研究这两个方向,我作为程序员,暂时只考虑数字混音的实现。因此本文主要以数字混音,通过CPU的浮点计算进行实现。

二、数字混音

数字混音需要把所有的音频转换为PCM 离散采样数据,由 CPU/FPGA 执行浮点数学运算,完成增益、声像、滤波、动态处理、信号路由、多轨求和,最终输出混合后的 PCM 流。

2.1 前置条件

2.1.1 PCM格式要求

混音的音频文件,必须采样率一样,这样才能保证单位时间点的数组长度一样。采样位数和声道尽量一致,如果不一致,只能以最大的作为输出参数。

采样率,必须一样方便计算

**采样位数:**不一样,也可以进行混音,但是计算会非常麻烦,建议转为一样的采样位数后再混音。

声道数:不一样,也可以进行混音,也是计算要进行处理,建议同声道数的混音。

本文章的前置条件全部为 一样,才进行混音,不考虑特殊场景。

2.1.2 核心算法

设同一时刻,轨道 1、轨道2......轨道n的 采样值增益分别为

单轨缩放公式:(增益)

立体声(平衡系数

,

多轨道求和为:( 浮点域线性叠加)

,

三、以webrtc的为例

源码在webrtc的frame_combiner.cc文件中

3.1 浮点域线性叠加

cpp 复制代码
void MixToFloatFrame(const std::vector<AudioFrame*>& mix_list,
                     size_t samples_per_channel,
                     size_t number_of_channels,
                     MixingBuffer* mixing_buffer) {
  RTC_DCHECK_LE(samples_per_channel, FrameCombiner::kMaximumChannelSize);
  RTC_DCHECK_LE(number_of_channels, FrameCombiner::kMaximumNumberOfChannels);
  // Clear the mixing buffer.
  for (auto& one_channel_buffer : *mixing_buffer) {
    std::fill(one_channel_buffer.begin(), one_channel_buffer.end(), 0.f);
  }

  // Convert to FloatS16 and mix.
  for (size_t i = 0; i < mix_list.size(); ++i) {
    const AudioFrame* const frame = mix_list[i];
    for (size_t j = 0; j < std::min(number_of_channels,
                                    FrameCombiner::kMaximumNumberOfChannels);
         ++j) {
      for (size_t k = 0; k < std::min(samples_per_channel,
                                      FrameCombiner::kMaximumChannelSize);
           ++k) {
        (*mixing_buffer)[j][k] += frame->data()[number_of_channels * k + j];
      }
    }
  }
}

1,采用浮点运算避免溢出

• 如果在 int16 域直接相加,两个 20000 相加就会变成 40000,超过 32767 导致溢出(Wrap-around),产生巨大的爆音。

• 使用 float 累加,可以容纳非常大的中间值(float 有巨大的动态范围)。

2,为限幅度做准备

• 累加后的浮点数据可能远远超过 int16 的范围。

• 后续的 RunLimiter 函数会分析这个浮点缓冲区的峰值。如果峰值过高,它会动态调整增益(Gain),平滑地降低整体音量,而不是硬截断(Hard Clipping)。

3.2 限幅控制(防止削波/Clipping)

限幅控制有两种方式,一种是动态调整增益,一种是采用固定的值进行 控制。

3.2.1 动态调整增益

cpp 复制代码
void Limiter::Process(AudioFrameView<float> signal) {
  const auto level_estimate = level_estimator_.ComputeLevel(signal);

  RTC_DCHECK_EQ(level_estimate.size() + 1, scaling_factors_.size());
  scaling_factors_[0] = last_scaling_factor_;
  std::transform(level_estimate.begin(), level_estimate.end(),
                 scaling_factors_.begin() + 1, [this](float x) {
                   return interp_gain_curve_.LookUpGainToApply(x);
                 });

  const size_t samples_per_channel = signal.samples_per_channel();
  RTC_DCHECK_LE(samples_per_channel, kMaximalNumberOfSamplesPerChannel);

  auto per_sample_scaling_factors = rtc::ArrayView<float>(
      &per_sample_scaling_factors_[0], samples_per_channel);
  ComputePerSampleSubframeFactors(scaling_factors_, samples_per_channel,
                                  per_sample_scaling_factors);
  ScaleSamples(per_sample_scaling_factors, signal);

  last_scaling_factor_ = scaling_factors_.back();

  // Dump data for debug.
  apm_data_dumper_->DumpRaw("agc2_gain_curve_applier_scaling_factors",
                            samples_per_channel,
                            per_sample_scaling_factors_.data());
}

webrtc的实现为:

  1. 看: 估计当前声音有多大。

level_estimator进行音频估计,level_estimator_(电平估计器)将音频帧划分为多个子帧(Sub-frames,通常为 4 个),并计算每个子帧的峰值电平或 RMS 电平。

  1. 算: 根据声音大小决定需要衰减多少(查增益曲线)。

scaling_factors_ 计算子帧增益系数,对于当前帧的每个子帧电平 x,调用 interp_gain_curve_.LookUpGainToApply(x) 查找映射表

  1. 平滑: 在样本级别平滑过渡增益,特别是快速响应突发的大音量(Attack)。

ComputePerSampleSubframeFactors 计算逐样本增益插值,我们不能突然在子帧边界跳变增益(例如前 1/4 帧增益为 1.0,后 3/4 突然变为 0.5),这会产生严重的失真。我们需要为每一个样本计算一个平滑过渡的增益值。

Attack Handling阶段特殊处理

• 如果检测到电平突然升高(scaling_factors0 > scaling_factors1,即需要快速衰减以防止削波),第一个子帧会使用非线性插值(幂函数,见文件顶部的 InterpolateFirstSubframe)。

• 原因: 线性插值在攻击阶段可能不够快,导致初始样本仍然削波。幂函数插值能更迅速地降低增益,优先保证不削波,尽管这可能稍微牺牲一点固定增益的有效性。

  1. 做: 将增益应用到每个样本,并确保不溢出。

• 即使经过限幅,由于浮点精度或极端情况,结果仍可能略微超出 int16 的范围(-1.0 到 1.0 对应的浮点值)。

• SafeClamp 确保最终输出严格限制在 -1.0, 1.0 范围内,防止后续转换为 int16 时发生溢出。