多频段压缩器在专业音频处理里非常常见,它把输入信号拆成低、中、高若干频段,每个频段独立做压缩处理,最后再混合回一路信号。相比单段全频压缩,这种方式能避免“低频一压、高频跟着塌”的问题。但分频处理会引入两个新麻烦:一是压缩器拉低增益后整体响度明显下降,需要做增益补偿;二是多个频段补偿后的信号叠加,峰值很容易冲破0dBFS造成削波。这篇文章围绕iOS平台的Core Audio框架,完整讲一下增益补偿、总输出增益控制和削波保护的实现思路。

一、多频段压缩器的整体链路设计
在iOS上实现多频段压缩,核心是把AudioUnit串联成一条处理链。典型做法是用AVAudioEngine或更底层的AUGraph,输入信号先经过一组分频滤波器(通常是Linkwitz-Riley类型的级联滤波器),拆分成三个频段,每个频段各自接一个压缩处理节点,最后通过混音节点求和输出。
分频点的选择很关键。常见的三段方案把分频点设在150Hz和2000Hz左右,低频段负责鼓和贝斯的能量,中频段覆盖人声主体,高频段处理镲片和空气感。Linkwitz-Riley滤波器的好处是分频点处两个频段的相位一致,叠加后不会出现频响凹陷,这在多频段压缩里是必须的,否则补偿增益的推算会失真。
需要注意,滤波器本身有处理延迟,各频段路径的延迟必须对齐,否则混合时会产生梳状滤波。用AVAudioUnitEQ做分频时,它内部已经保证了每条路径延迟一致,直接使用即可。
二、压缩后的增益补偿原理与实现
压缩器的工作逻辑是:当信号电平超过阈值时,按压缩比压低增益。比如阈值-24dB、压缩比4:1,输入-14dB时超出阈值10dB,输出只增加2.5dB,相当于被压掉了7.5dB。对整个节目信号做平均,响度会下降好几个dB,听感上就是“声音变小了、没劲了”。
增益补偿(Makeup Gain)的目的就是把这部分损失补回来。计算方法并不复杂:先算出压缩器在工作区间内的最大增益衰减量,再乘以一个补偿系数。工程上常用经验公式,补偿量等于阈值加上压缩区间一半能量的对应电平再除以压缩比的变化量。更稳妥的方式是在运行时统计:持续检测增益衰减量gainReduction的滑动平均值,把补偿增益平滑地跟上去。
下面是一段C语言风格的DSP代码,演示了基于运行时统计的补偿增益计算,可以直接放在渲染回调里:
// 单频段压缩 + 自动增益补偿
typedef struct {
float threshold; // 阈值 dB
float ratio; // 压缩比
float attack; // 启动系数
float release; // 释放系数
float env; // 包络电平
float makeup; // 补偿增益(线性值)
float avgReduction; // 平均衰减量
} CompBand;
static float dbToLinear(float db) {
return powf(10.0f, db / 20.0f);
}
static float processCompression(CompBand *c, float inSample) {
// 1. 包络检测,取绝对值做峰值近似
float absVal = fabsf(inSample);
if (absVal > c->env) {
c->env += c->attack * (absVal - c->env);
} else {
c->env += c->release * (absVal - c->env);
}
float envDb = 20.0f * log10f(c->env + 1e-8f);
// 2. 静态曲线:低于阈值不压缩
float reductionDb = 0.0f;
if (envDb > c->threshold) {
float over = envDb - c->threshold;
reductionDb = over * (1.0f - 1.0f / c->ratio);
}
// 3. 平滑统计平均衰减量,用于补偿
c->avgReduction = 0.9f * c->avgReduction + 0.1f * reductionDb;
// 补偿量取平均衰减的80%,留出余量
c->makeup = dbToLinear(c->avgReduction * 0.8f);
return inSample * dbToLinear(-reductionDb) * c->makeup;
}这段代码的关键在第三步:补偿增益不是固定值,而是跟随实际衰减量动态变化的。补偿系数取0.8而不是1.0,是因为各频段同时补偿再叠加时,峰值会往上限顶,留出20%余量能显著减轻后级limiter的压力。
三个频段各自独立计算补偿量,混合阶段直接把三路输出相加即可。每个频段的补偿量不同,这正是多频段压缩的优势:低频段压得多就补得多,高频段几乎没触发就少补。
三、总输出增益控制与削波保护
三路信号混合之后,理论上每一路都在-1dB以内,但相加后的瞬时峰值完全可能达到+3dB甚至更高,直接输出必然削波。所以混音节点后面必须再挂一个总输出增益级和限幅器。
总输出增益一般做成用户可调的参数,范围建议控制在-12dB到+6dB。它的实现很简单,就是对混合信号乘一个线性增益,但要注意用平滑插值,不能直接跳变,否则旋钮快速转动时会产生咔哒声。常用一阶平滑滤波,每个采样点向目标增益靠近10%左右。
削波保护推荐用软限幅器而不是简单clamp。硬削波(把超出的部分直接砍平)会产生大量高频谐波,听感刺耳。软限幅用平滑的传递函数过渡,失真小得多。下面是一个带lookahead的软限幅实现:
typedef struct {
float ceiling; // 输出上限,线性值,一般0.99
float threshold; // 限幅起始点,线性值,一般0.7左右
float gainSmooth; // 当前平滑增益
float targetGain; // 目标增益
float *delayBuf; // lookahead延迟缓冲
int delayLen;
int delayIdx;
} SoftLimiter;
static float processLimiter(SoftLimiter *l, float inSample) {
float absVal = fabsf(inSample);
// 1. 计算目标增益:低于threshold为1,超过则按比例压回
float tgt = 1.0f;
if (absVal > l->threshold) {
// 超过部分按平方根曲线压缩,过渡平滑
float over = (absVal - l->threshold) / (l->ceiling - l->threshold);
float shaped = l->threshold + (l->ceiling - l->threshold) * sqrtf(over);
tgt = shaped / (absVal + 1e-9f);
}
l->targetGain = tgt;
// 2. 增益平滑:向下压快,向上恢复慢
if (l->targetGain < l->gainSmooth) {
l->gainSmooth += 0.5f * (l->targetGain - l->gainSmooth); // attack 快
} else {
l->gainSmooth += 0.001f * (l->targetGain - l->gainSmooth); // release 慢
}
// 3. lookahead:信号延迟对齐增益变化,避免增益滞后导致瞬时突破
l->delayBuf[l->delayIdx] = inSample;
float delayed = l->delayBuf[(l->delayIdx + 1) % l->delayLen];
l->delayIdx = (l->delayIdx + 1) % l->delayLen;
float out = delayed * l->gainSmooth;
// 最终安全钳位,防止任何意外值
if (out > l->ceiling) out = l->ceiling;
if (out < -l->ceiling) out = -l->ceiling;
return out;
}lookahead机制值得展开说一下。限幅器的attack再快,增益下降也需要几个采样点的时间,如果峰值来了一瞬间才反应,前几个采样点还是会超。lookahead的做法是让信号先过一段延迟线(通常1到5毫秒),而增益检测用原始信号实时计算,这样增益提前下降,等信号真正到达输出端时已经被压好了。代价是整个链路多了几毫秒延迟,对播放场景完全无感。
attack快release慢的设计也有讲究。attack快保证峰值不漏,release慢保证限幅后响度回落自然,不会出现明显的“呼吸感”。release系数如果设得太快,限幅器会以可听频率抖动,形成类似振幅调制的杂音。
四、在AudioUnit渲染回调中的整合
以上三个模块最终要在AURemoteIO或AVAudioEngine的渲染回调里串起来。核心原则是所有DSP计算都不能有内存分配和锁操作,回调线程的实时性要求非常严格。结构体全部预先分配好,通过inRefCon传入。
整合后的处理顺序是:分频滤波、各频段压缩加补偿、三路求和、总增益平滑、软限幅输出。完整的渲染函数骨架如下:
static OSStatus renderCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
AppState *state = (AppState *)inRefCon;
// 先渲染上游节点(分频和压缩链已经用内部节点完成)
OSStatus status = AudioUnitRender(state->inputUnit, ioActionFlags,
inTimeStamp, inBusNumber,
inNumberFrames, ioData);
if (status != noErr) return status;
Float32 *buf = (Float32 *)ioData->mBuffers[0].mData;
for (UInt32 i = 0; i < inNumberFrames; i++) {
// 总输出增益平滑
state->outGain += 0.1f * (state->targetOutGain - state->outGain);
float sample = buf[i] * state->outGain;
// 软限幅削波保护
buf[i] = processLimiter(&state->limiter, sample);
}
return noErr;
}参数更新要特别注意线程安全。用户调节阈值或输出增益时,UI线程不能直接写DSP结构体,推荐用简单的原子变量或者三级缓冲:UI线程写参数副本,在每次渲染前用一个轻量锁交换指针,渲染过程中只读。AudioUnit自带的参数系统(AudioUnitSetParameter)已经做了线程安全处理,如果压缩逻辑封装成自定义AudioUnit,直接走参数接口最省事。
最后建议在真机上用AVAudioSession设置合理的采样率和缓冲区长度,44.1kHz下缓冲区建议256帧起步,给整条链路的DSP计算留够时间余量。调试阶段可以用MTAudioProcessingTap抓取处理前后的信号做频谱对比,确认各频段的补偿量是否符合预期,限幅器触发频率是否在合理范围内。如果limiter频繁触发,说明补偿系数给得太激进,回到第二步把0.8往下调,在响度和安全余量之间找到平衡点。
Core Audio多频段压缩器削波保护修改时间:2026-09-16 21:43:05