PCM与音量详解
PCM 缓冲写到满幅,系统音量条却只有一半------耳朵听到的响度仍可能不大。常见误会包括:「采样值 1 就是最大音量」「int16 顶到 ±32767 就一定最响」「用户把音量开到 100% 会让 PCM 超过满幅」。根因是 数字振幅(0 dBFS) 、播放链路上的 gain 、人耳主观响度 是三层不同概念。
速览
- PCM 存的是 采样振幅 ;满幅 = 0 dBFS (该位深允许的最大值),再大会 削波。
- int16 满幅是 ±32767 / ±32768 ;float 满幅是 ±1.0 ;整数格式里的
1几乎等于静音。- 用户/App 音量在 PCM 之后 乘 gain,不改变文件里的满幅定义。
- 音量条多用 对数 / dB 映射 ,不是滑块位置线性对应响度;普通滑条 不按频谱做等响度分析。
text
位深与满幅 → 播放链路 gain → 音量条感知映射 → 工程 headroom
目录
一、数字域
- [1. PCM 与 0 dBFS](#1. PCM 与 0 dBFS)
- [2. 位深与满幅对照](#2. 位深与满幅对照)
- [3. int16 PCM 混音防溢出与削波](#3. int16 PCM 混音防溢出与削波)
二、从 PCM 到耳朵
- [4. 播放链路](#4. 播放链路)
- [5. 软件音量与硬件音量](#5. 软件音量与硬件音量)
三、音量条与听感
- [6. 音量条为什么不是线性?dB 与感知响度](#6. 音量条为什么不是线性?dB 与感知响度)
- [7. 等响度与「真·响度」](#7. 等响度与「真·响度」)
四、工程实践
- [8. 峰值与混音建议](#8. 峰值与混音建议)
- [9. 增益与淡入淡出代码](#9. 增益与淡入淡出代码)
1. PCM 与 0 dBFS
PCM(Pulse Code Modulation) 用离散采样表示声波:每个采样是一个 振幅数值,不是「音量百分比」。
| 概念 | 含义 |
|---|---|
| 满幅 | 采样达到当前格式允许的最大正负值 |
| 0 dBFS | 数字满幅的参考点(Full Scale);再增大只会削波,不会更「合法」 |
| 削波 Clipping | 数值超出范围被截断,波形失真 |
音量在数字域里首先对应 振幅有多大;至于听起来响不响,还取决于波形形状、时长、后续 gain 和人耳特性。
2. 位深与满幅对照
「最大音量」必须由位深(及有符号/浮点约定)决定。
| 格式 | 典型存储 | 数值范围(满幅) | 1 的含义 |
|---|---|---|---|
| 16-bit 有符号 | int16_t,CD/WAV 常见 |
-32768 ~ +32767 | 极弱信号,接近静音 |
| 24-bit 有符号 | int32 低 24 位等 |
约 ±8 388 607 | 同样远非满幅 |
| 32-bit float | float,DAW/游戏引擎常见 |
约定满幅 -1.0 ~ +1.0 | 1.0 = 正峰值满幅 |
| 16-bit 无符号(少见) | 0~65535 | 非标准,与主流 API 不一致 | 需单独约定 |
2.1 常见误解
| 说法 | 对错 |
|---|---|
| 「PCM 里写 1 就是最大音量」 | 错 (除非明确是 float PCM 的 1.0f) |
| 「int16 的 1 和 float 的 1 一样响」 | 错(量级完全不同) |
| 「超过满幅会更响」 | 错(只会 clipping 或内部限幅) |
2.2 float PCM 补充
0.5≈ 幅度减半,约 -6 dB(相对 1.0)。- 部分混音管线在 中间缓冲 里允许短暂 > 1.0 (给多轨叠加留 headroom),送 DAC 或写回文件前仍会 限幅/归一化。
- 不代表 可以把素材故意写到
2.0、4.0当「更大声」------超满幅只会削波或被平台压回,与 int16 顶死 ±32767 是同一类错误。 - 不要与「用户音量滑条」混淆:滑条只缩放,不重新定义 0 dBFS。
3. int16 PCM 混音防溢出与削波
对有符号 16-bit PCM(Windows WAV、ALSA、CoreAudio、WASAPI 等常见路径):
- INT16_MAX = +32767 、INT16_MIN = -32768 对应 0 dBFS 数字天花板。
- 满幅正弦波在 +32767 ↔ -32768 之间交替;再叠加能量只会失真。
3.1 满幅 ≠ 主观「最响」
| 波形 | 峰值都是 32767 时 |
|---|---|
| 持续满幅方波 | 听起来很响 |
| 单个窄脉冲 | 峰值高,听感可能并不响 |
这里的「最大」指 振幅上限,不是 dB SPL 声压级。
3.2 混音必须防溢出
两路 int16 直接相加会超出范围:
c
int32_t tmp = (int32_t)a + (int32_t)b;
if (tmp > 32767) tmp = 32767;
if (tmp < -32768) tmp = -32768;
out[i] = (int16_t)tmp;
先在 更宽整数或 float 里累加,再 clamp 回 int16,是基本做法。
4. 播放链路
INT16_MAX 是数字域上限,不是耳朵听到的最终响度。数据大致经过:
PCM 缓冲
int16 ±32767
软件音量
App / 系统 / 播放器 gain
DAC 数模转换
硬件音量
功放 / 耳机 / 音箱
声压 → 主观响度
用户音量调节发生在 PCM 之后 (对即将播放的采样流乘 gain,或控制模拟增益),不会把文件里的采样改成超过 ±32767。
模拟域
数字域
文件/缓冲满幅
0 dBFS
× gain
≤ 1 常见
DAC
放大器
5. 软件音量与硬件音量
| 类型 | 作用位置 | 本质 | 是否改 PCM 文件 |
|---|---|---|---|
| 软件音量 | 系统/App/播放器 | sample_out = sample_in × gain |
否(只改播放流) |
| 硬件音量 | DAC 之后、功放/耳机 | 模拟放大 | 否 |
5.1 关键结论
| 命题 | 说明 |
|---|---|
| 满幅定义不变 | PCM 仍是 ±32767;0 dBFS 含义不变 |
| 音量调小 | 有效输出振幅变小(如 gain=0.5 → 约 ±16384 送进 DAC) |
| 音量调到 100% | 不会让 PCM 数值「超过」满幅 |
| 写到满幅 ≠ 一定最大声 | 系统音量可能很低;设备还有限幅/响度管理 |
5.2 常见误区
「缓冲已经 ±32767,用户一定听到最大声音」------不成立,系统音量、耳机电位器、平台响度策略都会再缩放。
5.3 平台差异(实测为准)
| 现象 | 说明 |
|---|---|
| Windows float 播放路径 | 部分 App / 驱动在送 DAC 前会对接近 1.0 的样本再做衰减或限幅;UI 显示「100%」时,缓冲峰值常见 0.9 左右 而非字面 1.0f |
| 移动端响度管理 | iOS / Android 可能对流媒体、通话等流类型做 额外增益或压缩 |
| 蓝牙设备 | 耳机端往往还有独立音量刻度,与 PC 滑条叠加 |
平台策略会变,以示波器/峰值表 + 实机试听 校准,不要假设「代码里乘 1.0 = 系统绝对最大」。
6. 音量条为什么不是线性?dB 与感知响度
音量条底层仍是 gain ,但 UI 刻度通常按 人耳近似对数感知 映射,而不是:
text
滑块 50% → gain = 0.5 // 假想线性:高音区几乎听不出变化
6.1 dB 与 gain
功率/幅度常用:
text
gain = 10^(dB / 20) // 幅度
dB = 20 × log10(gain)
许多系统让滑块位置对应 dB 衰减 或 指数曲线 (如 gain = position² / position³),使主观上「每格差不多响」。
| 滑块(示意) | gain(约) | 相对满幅 dB |
|---|---|---|
| 100% | 1.0 | 0 dB |
| 70% | ~0.5 | ~-6 dB |
| 50% | ~0.25 | ~-12 dB |
| 10% | ~0.01 | ~-40 dB |
滑块位置
对数 / 指数 / dB 映射
gain
× 每个 PCM 采样
6.2 与 PCM 数值的关系
- 音量条 不重新定义 0 dBFS。
- 它只在播放时 等比例缩放 已有 PCM。
7. 等响度与「真·响度」
人耳对不同频率敏感度不同(Fletcher--Munson / ISO 226 等响度曲线 ):中频(约 2~5 kHz)最敏感;小音量时低频往往显得更「薄」。
| 机制 | 是否普通音量条自带 |
|---|---|
| 对 PCM 乘 gain | 是 |
| 滑块 dB/指数映射 | 是(各平台实现不同) |
| 按频谱实时等响度补偿 | 否(多为可选 DSP / Hi-Fi) |
| 按节目内容测响度(BS.1770 等) | 否(广播/流媒体标准化另论) |
普通音量条不知道 当前播放的是 60 Hz 还是 1 kHz,只做 整体幅度缩放;不会按 ISO 226 逐频分析。
等响度补偿(Loudness Compensation) :音量小时提升低/高频------属于 额外音效,不是滑块本身。
广播/流媒体响度(如 EBU R128、ReplayGain) :对 节目整体 做测量与归一化,与系统「音量键」是不同层级。
8. 峰值与混音建议
| 场景 | 建议 |
|---|---|
| 音乐 / 音效素材 | 峰值留 headroom (如峰值约 -1~-3 dBFS),避免顶满 ±32767 |
| 多轨混音 | 宽位宽累加 + clamp;主 bus 再限幅 |
| 游戏 / 交互音频 | Master volume 与分类 gain 分开,预留余量 |
| 录音 | 防止输入链 clipping |
| 播放器 | 用户 gain 与素材峰值分开管理 |
「数字顶满」只保证 没用满量化范围 ;听感响度仍受 素材 RMS/频谱、后续 gain、设备 影响。
工程准则 :素材峰值宜 ≤ -3 dBFS (留 headroom);多轨 Master 不要顶满 ;UI 音量用 dB / 指数映射,与素材峰值、0 dBFS 分开管理。
9. 增益与淡入淡出代码
9.1 应用 gain(float 中间量,再写回 int16)
c
void apply_gain_int16(const int16_t* in, int16_t* out, size_t n, float gain)
{
for (size_t i = 0; i < n; ++i) {
float s = (float)in[i] * gain;
if (s > 32767.f) s = 32767.f;
if (s < -32768.f) s = -32768.f;
out[i] = (int16_t)s;
}
}
9.2 滑块位置 → gain(平方律示例,感知更均匀)
c
// position ∈ [0, 1],0 为静音,1 为满刻度
float slider_to_gain(float position)
{
if (position <= 0.f) return 0.f;
return position * position; // 也可试 position^3 或查 dB 表
}
9.3 线性淡入(帧计数)
c
void fade_in_int16(int16_t* buf, size_t n, size_t fade_samples)
{
if (fade_samples == 0) return;
for (size_t i = 0; i < n && i < fade_samples; ++i) {
float g = (float)(i + 1) / (float)fade_samples;
float s = (float)buf[i] * g;
buf[i] = (int16_t)s;
}
}
播放线程里可将 素材峰值 × 分类 gain × 主音量 gain 分层,避免一路顶死 0 dBFS。
一句话 :0 dBFS 由位深决定(int16 是 ±32767,float 是 ±1.0);播放音量 由后续 gain 决定;音量条 用 对数型映射 贴近听感,但不改变满幅定义。写代码时把三层分开,素材留 headroom、Master 不顶满,就不会再把「采样值 1」当成最大音量。