去年夏天,南方一家做在线检测设备的公司(下称 B 厂)停了一条产线。
停机的原因不是设备坏了。是——它没响。
他们的设备有一个很朴素的设计:操作员站在工位上,眼睛盯着工件,不看屏幕。设备判断NG,就"滴"一声;连续NG,就"滴滴滴"急促起来;设备本身故障,是另一种更低的"嘟嘟"声。三种声音,三种动作,一线工人靠耳朵就能分辨。
那天晚上,一批产品流过去了,报警音晚了八秒才响。
事后追查,那台机器用的是QMediaPlayer 播一段wav。play()到真正出声,冷启动要300到500毫秒;那天机器刚开机,磁盘还在忙,拖到了八秒。
"它没响"这三个字,比"它坏了"难查一百倍。坏了有日志、有堆栈、有蓝屏码。没响,什么都没有。
后来我们介入这件事,一开始以为是个小问题——播放一个提示音,QSound::play()一行代码的事。结果从接手到方案落地,整整两周。而这两周里,我们几乎把Qt的音频栈从头到尾趟了一遍。
这篇文章就是那两周的记录。

几乎所有做桌面软件的人,第一次加提示音都是这么写的:
QSound::play(":/sounds/ding.wav");一行,编译通过,运行有声,测试同学点了个赞,需求关闭。你甚至会觉得这块有点过于简单,简单到不值得写进设计文档。
然后半年之内,这些反馈会一个个找上来:
"声音太小了。"
"声音太大了,戴耳机差点聋。"
"有时候响,有时候不响。"
"连点三下,只听到一声。"
"新装的那台机器没声音。"
"升级到Qt6,这行编译不过了。"
"客户说这个提示音太廉价,像二十年前的软件。"
每一条都不致命。每一条都不好意思拒绝。每一条都得改。而它们其实来自同一个根因:你在用"播放一个文件"的方式,做"生成一个声音"的事。
这里得先摊开讲。很多人一开始就走错了路,后面全是补丁。
第一条,QSound。 最省事,也最脆。它在Qt6里已经不在QtMultimedia 里了,得额外拉Qt5Compat 模块——项目一升级,第一行报错就是它。而且它同一时刻只能播一个声音,音量控制几乎没有,播什么全看文件本身。
第二条,QSoundEffect。 延迟低,是"音效"这个场景的正路。但它只吃未压缩的WAV——你给个mp3,它不响,而且不一定告诉你为什么,你就对着代码怀疑人生。多个实例同时播放还会抢音频设备,一个响了另一个哑了。首次播放前不调 setLoaded,第一声永远是延迟的。
第三条,QMediaPlayer。 功能最全,延迟最大。它内部要走完整的媒体管线、解码器初始化,冷启动几百毫秒是常态。用它做报警音,等于没有报警音——B 厂那八秒,就是这条路给的。
第四条,QAudioSink(Qt6)/ QAudioOutput(Qt5)。 它不管你要播什么,它只问你要PCM数据。你自己算出每一个采样点,喂给它,它负责送到声卡。
提示音要满足三个条件:快、可控、能叠加。前三条路各自缺一条,只有第四条全都有。
也就是说,你要的不是"播放器",是"合成器"。

这一点比技术更关键,但我们是在踩了两个月之后才想明白的。
B厂原本有12个wav文件,对应12种状态。产品经理提了个需求:把严重程度分成三级,每级再分"开始"和"恢复",一下从12个变成20多个。找录音师重新录,报价两万;改一次需求,重录一次。
而且文件这个东西,成本从来不在录的时候,在管的时候:
采样率不统一,有的44.1k有的48k,混着播音调都不一样
音量没归一化,有的响有的轻
打包漏一个,装到现场才发现
装在中文路径下,某些环境静默失败
安装包白白多出几兆,就为了几个"叮"
可你想想,"叮"这个声音,本质上是什么?
它只是几个参数:基频多少、持续多久、包络怎么走、加多少谐波、节奏是几个音。就这些。
把它当文件,它是死的;把它当参数,它是活的。
改成参数之后,产品经理说"这个音再亮一点",你调一个数字,立刻能听。说"急促一点",你把间隔从200ms改成80ms。不需要录音师,不需要重新打包,不需要发版。
这条路不难走通,难在好听、稳定、不出事。我们踩过的坑大概是这样几类:
第一,纯正弦不像提示音。 你在示波器上算一个880Hz的正弦波,播出来是"嘀——"的测试音,冷冰冰的像医疗仪器自检。真正像"叮"的声音,得有谐波,而且谐波要衰减得比基频快,才有那种敲击之后的余韵。
第二,首尾会爆音。这是最容易被忽略的一条。波形在非零的位置突然开始或结束,扬声器振膜会瞬间跳变,你听到的是"啪"。每响一次"啪"一次,听久了极其难受。解法很土但必须做:几毫秒的淡入、十几毫秒的淡出。
第三,叠加会削波。两个音量都是1.0的声音同时响,加起来是2.0,超出int16的范围就会回绕,变成刺耳的电流噪音。多音混合必须预留 headroom,或者做软限幅。
第四,延迟和爆音是一对矛盾。缓冲区设大了,延迟高;设小了,数据供不上(underrun),又是爆音。20到50毫秒是实践中的甜点,但这跟机器性能、驱动、电源策略都有关系。
第五,设备是会变的。用户插蓝牙耳机、接HDMI显示器、拔USB声卡,音频设备就换了。如果你的sink不重建,从此以后就是静音——而且不报错。
第六,有些机器根本没有声卡。服务器、远程桌面、CI环境,QMediaDevices返回空列表。你的代码如果直接取第一个设备,就是空指针崩溃。一个提示音,把整个软件干掉了。
核心的生成逻辑其实不长,大概是这个形状:

有了这段,把它塞进一个自己写的QIODevice::readData(),再挂到QAudioSink上,一声"叮"就出来了。真正花时间的是外面那一圈:混音、限幅、设备热插拔、无声卡降级、以及"为什么这台机器上听起来不一样"。
三层,从里到外:
第一层,把提示音变成配方。 一个提示音就是一段配置:基频、时长、包络类型、谐波权重、节奏序列、增益。运行时生成PCM,一个音频文件都不带。改音色改配置,发版都不用。
第二层,自己写一个混音器。 全局只开一个QAudioSink,内部维护若干路音源,每次readData时把所有活跃音源按各自的位置相加,做headroom和软限幅。这样三个报警同时来,是三个声音叠在一起,而不是后一个把前一个掐死。
第三层,把设备和生命周期管起来。 监听设备变化,sink该重建就重建;没有可用设备就静默降级,绝不崩;所有输出做一次响度归一化,保证换台机器音量不会突然翻倍。
B厂最后把二十多种提示音全换成了配置表。产品经理坐在我们旁边调频点,调完直接听,一下午定完。报警响应从三四百毫秒压到三十毫秒以内,操作员终于能在工件流过去之前抬头。
如果你正在被提示音折磨,按这个顺序查,能省你不少时间:
1. 先确认你走的是哪条路。 用了QMediaPlayer播提示音的,延迟问题基本无解,换路比调优快。
2. 第一声总是慢半拍?QSoundEffect没有预热,setLoaded(true) 之后等isLoaded() 再播。
3. 只响一声,后面的被吞了? 说明你复用了同一个实例,或者多个实例在抢设备。要么排队列,要么自己做混音。
4. 每一声都有"啪"? 缺淡入淡出,五毫秒就能解决。
5. 同时响多个就变噪音? 削波了,把每个音源的增益压到0.2左右再混。
6. 换台机器音量差很多? 没做响度归一化,别指望系统音量一致。
7. 在某些机器上直接崩? 检查QMediaDevices::defaultAudioOutput() 是否为空——服务器和远程桌面上它真的会是空的。
写这个不是为了证明我们多懂音频。是因为提示音这东西,在大多数团队里都排不进优先级,直到它在客户现场哑了一次。
如果你在做类似的事——工业软件、设备上位机、桌面客户端,需要报警音、状态音、语音播报——欢迎带着场景来找我们聊。
不用带着合同来。你可以先说清楚三件事:声音要表达什么、允许多大延迟、跑在什么机器上。这三个答案基本就能定方案,十分钟的事。
我们在做的事,说白了就是:让国产工业软件,在这种没人愿意细看的地方,也别掉链子。