音频混音原理(MIXer)

音频混音原理(MIXer)

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

一、基础原理

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

模拟波形叠加:

混音输出波形= 波形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 浮点域线性叠加

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 动态调整增益

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 电平。
2. 算: 根据声音大小决定需要衰减多少(查增益曲线)。

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


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

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

Attack Handling阶段特殊处理

• 如果检测到电平突然升高(scaling_factors[0] > scaling_factors[1],即需要快速衰减以防止削波),第一个子帧会使用非线性插值(幂函数,见文件顶部的 InterpolateFirstSubframe)。
• 原因: 线性插值在攻击阶段可能不够快,导致初始样本仍然削波。幂函数插值能更迅速地降低增益,优先保证不削波,尽管这可能稍微牺牲一点固定增益的有效性。
4. 做: 将增益应用到每个样本,并确保不溢出。

• 即使经过限幅,由于浮点精度或极端情况,结果仍可能略微超出 int16 的范围(-1.0 到 1.0 对应的浮点值)。
• SafeClamp 确保最终输出严格限制在 [-1.0, 1.0] 范围内,防止后续转换为 int16 时发生溢出。