目录
一、先给 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 / 播放进度 / 暂停 |
QMediaPlayer |
|
FFmpeg 解码 → PCM → 推流 |
✅ QAudioSink |
|
低延迟语音 / 对讲 / 雷达声 |
✅ QAudioSink |
|
多设备路由 / 独占 ASIO |
❌ 直接写 WASAPI / ALSA |
QAudioSink = Qt 封装的 Push / Pull Audio Endpoint
二、第一大坑:StoppedState ≠ Pause
很多人写:
sink->stop();
// 想 resume
sink->start(); // ❌
真相
-
QAudioSink没有 resume
-
stop()= 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 还活着
-
等你喂数据
但现实是:
|
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 都有效
真相
|
stop() |
❌ 失效 |
|
StoppedState |
❌ 失效 |
|
delete sink |
✅ 自动 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 映射表
|
AV_SAMPLE_FMT_S16 |
Int16 |
|
AV_SAMPLE_FMT_S32 |
Int32 |
|
AV_SAMPLE_FMT_FLT |
Float |
|
AV_CH_LAYOUT_STEREO |
ChannelConfigStereo |
九、第八大坑: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()),后者由平台音频后端决定,可优化播放流畅性 。


