说一说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 / 播放进度 / 暂停QMediaPlayerFFmpeg 解码 → PCM → 推流✅QAudioSink​低延迟语音 / 对讲 / 雷达声✅QAudioSink​多设备路由 / 独占 ASIO❌ 直接写 WASAPI / ALSAQAudioSink 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一会儿直接 StoppedPulseAudio立刻 UnderrunALSA卡住不出声正确用法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最隐蔽 bugif (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 msLinux1~2 秒PulseAudio 笑死低延迟必写QAudioSink *sink new QAudioSink(fmt); sink-setBufferSize(1024 * 4); // 经验值 dev sink-start();公式bufferSize ≈ samplesPerFrame × channels × bytes × 2七、第六大坑stateChanged 里别同步 delete sinkconnect(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 映射表FFmpegQt6AV_SAMPLE_FMT_S16Int16AV_SAMPLE_FMT_S32Int32AV_SAMPLE_FMT_FLTFloatAV_CH_LAYOUT_STEREOChannelConfigStereo九、第八大坑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()后者由平台音频后端决定可优化播放流畅性 。‌‌