说一说Qt6 的 QAudioSink:我用它踩完坑后的「避雷白皮书」
目录
一、先给 QAudioSink 一个定位
二、第一大坑:StoppedState ≠ Pause
真相
正确认知
三、第二大坑:IdleState 是“临终关怀”
你看到的状态机通常是:
IdleState 是什么?
但现实是:
正确用法
四、第三大坑:start() 返回的 QIODevice* 生命周期
错误认知
真相
最隐蔽 bug
五、第四大坑:bytesFree() 不是你想的那样
问题
推荐策略
六、第五大坑:bufferSize 不设置 = 玄学延迟
低延迟必写
七、第六大坑:stateChanged 里别同步 delete sink
正确姿势
八、第七大坑:Qt6 的 QAudioFormat 和 FFmpeg 对不齐
Qt5 老写法
FFmpeg → Qt6 映射表
九、第八大坑:Underrun 不重连,用户以为“播完了”
正确逻辑
十、一个使用示例
十一、总结
觉得有用,就请您帮忙点赞转发收藏吧,您的鼓励是我创作的动力,多谢看官。
由于能力水平有限,文中的错误或不严谨的地方在所难免,还请批评指正。
QAudioSink 是 Qt 6 多媒体模块(Qt Multimedia)中用于将音频数据发送到输出设备(如扬声器、耳机)的核心类。它取代了 Qt 5 中的
QAudioOutput,提供了更底层、更灵活的音频播放控制接口 。QAudioSink 是 Qt6 里音频输出最底层、最“裸”的接口,性能很好,但几乎不帮你兜底。
用得爽的人:自己写解码器 / 实时流。
用哭的人:以为它是 QMediaPlayer 的轻量版。
这篇文章只讲一件事:
我用 QAudioSink 在 Linux / Windows / macOS 上踩过的坑,按严重程度排序。
一、先给 QAudioSink 一个定位
如果你想要 | 用啥 |
|---|---|
MP3 / WAV / 播放进度 / 暂停 |
|
FFmpeg 解码 → PCM → 推流 | ✅QAudioSink |
低延迟语音 / 对讲 / 雷达声 | ✅QAudioSink |
多设备路由 / 独占 ASIO | ❌ 直接写 WASAPI / ALSA |
QAudioSink = Qt 封装的 Push / Pull Audio Endpoint
二、第一大坑:StoppedState ≠ Pause
很多人写:
sink->stop(); // 想 resume sink->start(); // ❌真相
QAudioSink没有 resumestop()= backend 关闭(Pulse / WASAPI / CoreAudio 全关)StoppedState 后返回的
QIODevice*已经被 Qt delete 了
正确认知
QAudioSink 是一次性会话对象
正确重启姿势:
delete sink; sink = new QAudioSink(fmt); dev = sink->start();官方没明说,但源码里就是这么回事。
三、第二大坑:IdleState 是“临终关怀”
你看到的状态机通常是:
ActiveState → IdleState → StoppedState (UnderrunError)IdleState 是什么?
内部 ring buffer 空了
backend 还活着
等你喂数据
但现实是:
平台 | Idle 命运 |
|---|---|
Windows WASAPI | 一会儿直接 Stopped |
PulseAudio | 立刻 Underrun |
ALSA | 卡住不出声 |
正确用法
Idle = 立刻补静音
void onStateChanged(QAudio::State s) { if (s == QAudio::IdleState) { QByteArray silence(512, 0); dev->write(silence); } }我现在的规则:
IdleState 不当正常状态,只当预警
四、第三大坑:start() 返回的 QIODevice* 生命周期
QIODevice *dev = sink->start();错误认知
以为是 Qt 给你 new 的普通 device
以为 sink 析构前 dev 都有效
真相
操作 | dev 状态 |
|---|---|
| ❌ 失效 |
| ❌ 失效 |
| ✅ 自动 delete |
最隐蔽 bug
if (sink->state() == QAudio::IdleState) dev->write(data); // dev 是野指针(如果曾经 Stopped)正确写法:
if (sink && sink->state() != QAudio::StoppedState && dev) dev->write(data);五、第四大坑:bytesFree() 不是你想的那样
很多人写:
if (sink->bytesFree() > pcm.size()) dev->write(pcm);问题
Idle 时
bytesFree()很大backend 实际已经卡死
Windows 下 write 返回 0
推荐策略
场景 | 策略 |
|---|---|
实时流 | 自己 FIFO,尽量写 |
解码器 | 写满 bufferSize 的 1/2 就停 |
Idle | 无视 bytesFree,直接补静音 |
六、第五大坑:bufferSize 不设置 = 玄学延迟
QAudioSink sink(fmt); sink.start();默认 bufferSize:
平台 | 默认 |
|---|---|
Windows | ~200~500 ms |
Linux | 1~2 秒(PulseAudio 笑死) |
低延迟必写
QAudioSink *sink = new QAudioSink(fmt); sink->setBufferSize(1024 * 4); // 经验值 dev = sink->start();公式:
bufferSize ≈ samplesPerFrame × channels × bytes × 2七、第六大坑:stateChanged 里别同步 delete sink
connect(sink, &QAudioSink::stateChanged, this, [](QAudio::State s){ if (s == QAudio::StoppedState) delete sink; // ❌ 栈回溯炸 });正确姿势
QMetaObject::invokeMethod(this, [this]{ restartSink(); }, Qt::QueuedConnection);或者:
QTimer::singleShot(0, this, &MyClass::restartSink);八、第七大坑:Qt6 的 QAudioFormat 和 FFmpeg 对不齐
Qt6 新坑(很多人从 Qt5 迁上来):
QAudioFormat fmt; /* enum SampleFormat : quint16 { Unknown, UInt8, Int16, Int32, Float, NSampleFormats }; */ fmt.setSampleFormat(QAudioFormat::Int16); // ❗不是 setSampleType fmt.setChannelConfig(QAudioFormat::ChannelConfigStereo);Qt5 老写法
fmt.setSampleType(QAudioFormat::SignedInt); // Qt6 没了 fmt.setChannelCount(2);FFmpeg → Qt6 映射表
FFmpeg | Qt6 |
|---|---|
|
|
|
|
|
|
|
|
九、第八大坑:Underrun 不重连,用户以为“播完了”
if (sink->error() == QAudio::UnderrunError) qDebug() << "完了?";正确逻辑
if (sink->state() == QAudio::StoppedState && sink->error() == QAudio::UnderrunError) { // 不是 EOF,是 backend 踢人 restartSink(); }十、一个使用示例
class AudioOut : public QObject { QAudioSink *sink{}; QIODevice *dev{}; QAudioFormat fmt; public: void start() { sink = new QAudioSink(fmt); sink->setBufferSize(4096); dev = sink->start(); connect(sink, &QAudioSink::stateChanged, this, &AudioOut::onState); } void push(const QByteArray &pcm) { if (!dev || sink->state() == QAudio::StoppedState) return; dev->write(pcm); } private: void onState(QAudio::State s) { if (s == QAudio::IdleState && dev) dev->write(QByteArray(512, 0)); if (s == QAudio::StoppedState) QTimer::singleShot(0, this, &AudioOut::start); } };十一、总结
两种播放模式:
- QIODevice 模式:适用于应用线程,通过
start(QIODevice*)从文件或网络流中读取数据播放,适合播放 PCM 文件或网络音频流 。- Callback 模式(Qt 6.11+):适用于音频线程,通过
start(Callback)直接写入音频缓冲区,实现低延迟播放,常用于实时音频生成或处理 。状态管理:
QAudioSink 具有四种状态:Active(播放中)、Suspended(暂停)、Stopped(停止)、Idle(缓冲区空)。状态变化通过stateChanged()信号通知 。格式与设备配置:
创建时需指定QAudioFormat(采样率、通道数、样本格式)和可选的QAudioDevice。若格式不被后端支持,需检查error()返回值 。音量与缓冲控制:
支持运行时调整音量(setVolume())和缓冲区大小(setBufferSize()),后者由平台音频后端决定,可优化播放流畅性 。
