首页/文章/ 详情

一声"叮",我们做了两周 | Qt里合成一个提示音,比你以为的难在哪

42分钟前浏览0

一、凌晨两点,产线停了

去年夏天,南方一家做在线检测设备的公司(下称 B 厂)停了一条产线。

停机的原因不是设备坏了。是——它没响。

他们的设备有一个很朴素的设计:操作员站在工位上,眼睛盯着工件,不看屏幕。设备判断NG,就"滴"一声;连续NG,就"滴滴滴"急促起来;设备本身故障,是另一种更低的"嘟嘟"声。三种声音,三种动作,一线工人靠耳朵就能分辨。

那天晚上,一批产品流过去了,报警音晚了八秒才响。

事后追查,那台机器用的是QMediaPlayer 播一段wav。play()到真正出声,冷启动要300到500毫秒;那天机器刚开机,磁盘还在忙,拖到了八秒。

"它没响"这三个字,比"它坏了"难查一百倍。坏了有日志、有堆栈、有蓝屏码。没响,什么都没有。

后来我们介入这件事,一开始以为是个小问题——播放一个提示音,QSound::play()一行代码的事。结果从接手到方案落地,整整两周。而这两周里,我们几乎把Qt的音频栈从头到尾趟了一遍。

这篇文章就是那两周的记录。

二、提示音这件事,看起来只有一行

几乎所有做桌面软件的人,第一次加提示音都是这么写的:

QSound::play(":/sounds/ding.wav");一行,编译通过,运行有声,测试同学点了个赞,需求关闭。你甚至会觉得这块有点过于简单,简单到不值得写进设计文档。

然后半年之内,这些反馈会一个个找上来:

  •  "声音太小了。"

  • "声音太大了,戴耳机差点聋。"

  • "有时候响,有时候不响。"

  • "连点三下,只听到一声。"

  • "新装的那台机器没声音。"

  • "升级到Qt6,这行编译不过了。"

  • "客户说这个提示音太廉价,像二十年前的软件。"

每一条都不致命。每一条都不好意思拒绝。每一条都得改。而它们其实来自同一个根因:你在用"播放一个文件"的方式,做"生成一个声音"的事。

三、Qt里放个声音,其实有四条路

这里得先摊开讲。很多人一开始就走错了路,后面全是补丁。

第一条,QSound。 最省事,也最脆。它在Qt6里已经不在QtMultimedia 里了,得额外拉Qt5Compat 模块——项目一升级,第一行报错就是它。而且它同一时刻只能播一个声音,音量控制几乎没有,播什么全看文件本身。

第二条,QSoundEffect。 延迟低,是"音效"这个场景的正路。但它只吃未压缩的WAV——你给个mp3,它不响,而且不一定告诉你为什么,你就对着代码怀疑人生。多个实例同时播放还会抢音频设备,一个响了另一个哑了。首次播放前不调 setLoaded,第一声永远是延迟的。

第三条,QMediaPlayer。 功能最全,延迟最大。它内部要走完整的媒体管线、解码器初始化,冷启动几百毫秒是常态。用它做报警音,等于没有报警音——B 厂那八秒,就是这条路给的。

第四条,QAudioSink(Qt6)/ QAudioOutput(Qt5)。 它不管你要播什么,它只问你要PCM数据。你自己算出每一个采样点,喂给它,它负责送到声卡。

提示音要满足三个条件:快、可控、能叠加。前三条路各自缺一条,只有第四条全都有。

也就是说,你要的不是"播放器",是"合成器"。

探索QT软件开发的架构设计:从设计理念到实践应用-CSDN博客

四、更深一层:提示音不该是文件

这一点比技术更关键,但我们是在踩了两个月之后才想明白的。

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() 是否为空——服务器和远程桌面上它真的会是空的。

八、如果你也在跟声音较劲

写这个不是为了证明我们多懂音频。是因为提示音这东西,在大多数团队里都排不进优先级,直到它在客户现场哑了一次。

如果你在做类似的事——工业软件、设备上位机、桌面客户端,需要报警音、状态音、语音播报——欢迎带着场景来找我们聊。

不用带着合同来。你可以先说清楚三件事:声音要表达什么、允许多大延迟、跑在什么机器上。这三个答案基本就能定方案,十分钟的事。

我们在做的事,说白了就是:让国产工业软件,在这种没人愿意细看的地方,也别掉链子。


来源:IFD优飞迪
电源UM数字孪生控制
著作权归作者所有,欢迎分享,未经许可,不得转载
首次发布时间:2026-10-26
最近编辑:42分钟前
优飞迪科技
赋能新仿真,创优新设计
获赞 326粉丝 351文章 547课程 4
点赞
收藏
作者推荐
未登录
还没有评论
课程
培训
服务
行家
VIP会员 学习计划 福利任务
下载APP
联系我们
帮助与反馈