音频引擎

游戏声音系统——音频图 (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);