淡入淡出必须在音频回调中按采样级推进,抢占须在混音前实时决策;状态迁移需原子化,优先级应分层枚举实测,曲线类型须匹配听觉平滑性。

淡入淡出不能靠 sleep 或 busy-wait,必须用采样级时间推进
游戏音频是实时流式处理,std::this_thread::sleep_for 会导致卡顿、跳帧甚至崩溃。淡入淡出的本质是对每个音频样本(sample)做幅度缩放,必须在音频回调函数(如 paStreamCallback 或 ALCcontext 的混音循环)中按帧推进。常见错误是把“播放 2 秒淡入”写成定时器启动后等 2 秒再设音量——这完全脱离音频时序,实际听到的是突兀起始或静音间隙。
实操建议:
- 为每个
SoundInstance维护fade_progress(float,0.0→1.0)和fade_direction(+1.0 表示淡入,-1.0 表示淡出) - 每次音频回调传入的
buffer长度为nFrames,按帧数更新:fade_progress += fade_direction * nFrames / (sampleRate * durationSec) - 确保
fade_progress被 clamp 在 [0.0, 1.0],超出即标记为“完成淡入/淡出”,触发状态切换(如从FADING_IN→PLAYING) - 避免浮点累积误差:用整数计数器(如已处理样本数)代替 float 进度,更稳定
优先级抢占必须在混音前决策,不能依赖播放线程锁
多个声音同时请求播放时,若在主线程加锁排队、再发到音频线程,会引入不可控延迟,且无法响应“正在播放的声音突然被更高优抢占”的需求。真正有效的抢占发生在混音入口——即每帧混音前,检查当前所有活跃实例的优先级、距离、是否正在淡出,并实时剔除或衰减低优实例。
实操建议:
- 维护一个全局
std::vector,按priority+distance+age多级排序,每帧混音前重排(注意:仅当有新实例加入或优先级变更时才重排,避免每帧排序) - 设定硬性上限(如
MAX_ACTIVE_SOUNDS = 32),混音前只取前 N 个;被截断的实例立即触发onPreempted()回调,可播放提示音或记录日志 - 抢占不等于立即静音:对被抢占实例执行快速淡出(如 50ms),避免咔哒声;用指数衰减(
volume *= pow(0.001f, dt))比线性更自然 - 避免用
std::mutex保护整个列表——改用std::atomic标记“列表待刷新”,由音频线程单向读取,主线程仅写入变更队列(lock-free ring buffer)
AL / FMOD / Wwise 的优先级语义差异极大,别直接套用文档默认值
OpenAL 的 AL_PRIORITY 实际上多数驱动忽略;FMOD 的 setPriority() 只在超过通道数时生效,且受 Studio::System::setSoftwareFormat() 中 voice count 影响;Wwise 完全基于 AkEvent 和 AkAuxBus 的路由拓扑。同一套“高优=100,低优=10”的数值,在不同中间件里行为可能相反。
实操建议:
- 不要信任中间件文档里的“priority 数值说明”,一律通过实测确定有效范围:例如 FMOD 中
setPriority(255)在 64-voice 系统下可能毫无作用,而setPriority(63)才触发抢占 - 优先级应分层设计:
UI_SFX > PLAYER_FOOTSTEP > AMBIENT_WIND > BACKGROUND_MUSIC,用枚举而非裸 int,避免魔法数字 - 对 OpenAL:放弃
AL_PRIORITY,改用手动管理AL_SOURCE的AL_GAIN和 active 状态,配合自定义混音器 - 若用 Wwise,优先走
AkGameObject::SetAttenuationScalingFactor()+AkState切换,而非 raw priority
淡入淡出与抢占的组合逻辑容易引发竞态,关键状态必须原子化
典型问题:一个声音正淡出到 0.05,此时被更高优抢占,代码同时执行“淡出归零”和“抢占静音”,结果因非原子操作导致音量跳变或残留残响。更隐蔽的是,淡出完成回调里释放资源,而抢占逻辑又试图修改同一实例的 volume —— 野指针或 double-free。
实操建议:
- 为
SoundInstance定义明确状态机:STOPPED → FADING_IN → PLAYING → FADING_OUT → STOPPED,所有状态迁移通过std::atomic(或std::atomic)控制 - 淡出中被抢占?只允许从
FADING_OUT→PREEMPTED,禁止再进STOPPED;PREEMPTED状态下 volume 强制为 0,但缓冲区仍保留直到下一帧混音结束 - 所有对外接口(
play(),stop(),fadeTo())都先 CAS 状态,失败则返回 false 或丢弃请求,不抛异常 - 调试时打开
#define AUDIO_DEBUG_STATES,在混音回调开头 dump 当前所有实例的状态码和 volume,比断点更可靠
最常被绕过的点是:淡入淡出曲线类型(线性/指数/s-curve)必须与抢占衰减速率匹配,否则淡出末段会被抢占逻辑“截胡”,听起来像被掐住喉咙。别假设数学上连续就等于听觉上平滑——得用耳听,不是看 plot。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











