音频引擎
游戏声音系统——音频图 (source→filter→mixer→output), DSP 基础, 空间音频。
概念
音频引擎是游戏的”声音内核”。它将离散的音频资源 (wav, mp3, ogg) 转换为实时音频流。核心挑战包括: 多音源混音、实时 DSP 效果、3D 空间化、以及严格的时间预算 (音频缓冲区欠载 → 爆音, 用户立即可感知)。与图形渲染不同,音频对延迟极为敏感。
核心组件
| 组件 | 职责 | 关键技术 |
|---|---|---|
| 声源 (Source) | 解码/播放音频文件 | 流式加载, 缓冲队列 |
| DSP 效果器 | 实时信号处理: 滤波、混响、延迟 | FIR/IIR 滤波器, 卷积 |
| 混音器 (Mixer) | 将多个声源混合为单一输出流 | 增益控制, 总线和子混音 |
| 空间化 | 3D 声场模拟: 方向、距离衰减、遮挡 | HRTF, Panning |
| 输出驱动 | 将音频帧数据写入硬件 | PortAudio, OpenAL, WASAPI, ALSA |
| 音频图 | 节点连接模型: 声源 → 效果 → 总线 → 输出 | 类似 Blender Shader Nodes |
音频图
音频图 (Audio Graph) — 节点连接模型:
声源层:
Source "BGM" Source "Footstep" Source "Gunshot"
| | |
效果层: | Effect "Reverb" Effect "Echo"
| | |
总线层: +---------+-----------+---------+------------+
| |
Bus "SFX" Bus "Music"
| |
混音: +----------+----------+
|
Master Bus
|
硬件输出 (Speaker / Headphone)
DSP 基础
卷积混响 (Convolution Reverb):
录制真实空间 (如音乐厅) 的脉冲响应 (IR)
IR 是一个声音样本, 表示一个极短脉冲在该空间中的回响
将任意输入音频与 IR 做卷积 → 得到"像在那个空间中播放"的效果
output[t] = sum(input[t - k] * IR[k]) for k = 0..N
// 这是 O(N^2), 通常通过 FFT 优化为 O(N log N)
延迟 (Delay):
简单的回声效果:
output[t] = input[t] + feedback * output[t - delay_samples]
滤波 (Filter):
低通滤波 (Low-Pass): 去除高频 → 模拟隔着墙的声音 (遮挡)
高通滤波 (High-Pass): 去除低频 → 模拟小型扬声器
简单一阶 IIR 低通:
output[t] = output[t-1] + cutoff * (input[t] - output[t-1])
// cutoff 越小 → 越"闷" (更多高频被去除)
空间音频
3D 音频空间化:
听者位置: (ear_x, ear_y, ear_z), 朝向 (forward, up)
声源位置: (src_x, src_y, src_z)
衰减模型:
distance = |src - ear|
gain = 1 / (1 + rolloff * distance)
// 也可以使用更复杂的: min_distance, max_distance, curve
声相 (Panning):
根据声源和听者的相对角度, 控制左右声道音量
例如: 声源在右前方 45° → 右声道略大
HRTF (Head-Related Transfer Function):
模拟人耳、头部和肩膀对声音的滤波效应
每个方向有一对 FIR 滤波器 (左耳 + 右耳)
比简单的 panning 更真实的空间定位感
遮挡与衍射:
射线检测: 声源和听者之间是否有遮挡物
有遮挡 → 启用低通滤波器 (模拟声音穿过墙壁)
PortAudio / OpenAL 框架
PortAudio 回调模型 (拉模式):
int audio_callback(void *outputBuffer, unsigned long framesPerBuffer, ...) {
float *out = (float *)outputBuffer;
for (int i = 0; i < framesPerBuffer; i++) {
float sample = 0.0f;
// 混音所有活跃声源
for each active source:
sample += source->get_next_sample();
// 应用 Master 总线效果
sample = master_compressor(sample);
// 输出立体声
out[2*i] = sample; // 左声道
out[2*i+1] = sample; // 右声道
}
return 0; // 继续
}
Pa_OpenDefaultStream(&stream, 0, 2, paFloat32, SAMPLE_RATE,
FRAMES_PER_BUFFER, audio_callback, NULL);
Pa_StartStream(stream);