欢迎光临
我们一直在努力

说一说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 / 播放进度 / 暂停

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 还活着

  • 等你喂数据

但现实是:

平台

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 状态

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 映射表

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()),后者由平台音频后端决定,可优化播放流畅性 。‌‌

赞(0)
未经允许不得转载:171主机测评 » 说一说Qt6 的 QAudioSink:我用它踩完坑后的「避雷白皮书」
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址