当前位置: 首页 > news >正文

Qt + FFmpeg 实战:将音视频文件解码为 PCM 数据

前言

在音视频开发中,PCM 是最常见的原始音频数据格式之一。无论是做波形显示、音频播放、音频分析、语音识别前处理,还是后续编码转码,很多场景都需要先把压缩音频解码成 PCM。

本文基于一个 Qt + FFmpeg 项目,介绍如何使用 FFmpeg 打开音视频文件,找到其中的音频流,解码音频帧,并统一转换成s16interleaved PCM 数据,最后使用 Qt 的QByteArray保存。

本文对应项目中的核心文件:

audiodecoder.h/.cpp // 负责音频解码为 PCM mediaanalyzer.h/.cpp // QML 控制入口,调用 AudioDecoder ffmpegutils.h/.cpp // FFmpeg 公共辅助函数

1. PCM 是什么

PCM,全称 Pulse-Code Modulation,脉冲编码调制。它是未经压缩的原始音频采样数据。

常见 PCM 参数包括:

参数含义
sample rate采样率,例如 44100 Hz、48000 Hz
channels通道数,例如 1 表示单声道,2 表示双声道
sample format采样格式,例如 s16、flt、s32
interleaved多通道是否交错存储

例如双声道s16interleaved PCM 的数据排列方式是:

L0 R0 L1 R1 L2 R2 ...

其中每个采样点是 16 bit,也就是 2 字节。

在本文项目中,为了让输出格式稳定,解码后统一转换为:

AV_SAMPLE_FMT_S16 interleaved 原始采样率 原始通道布局

并保存到:

QByteArray m_pcmData;

2. 解码流程总览

使用 FFmpeg 解码音频,核心流程如下:

打开输入文件 -> 读取媒体流信息 -> 查找最佳音频流 -> 创建并打开解码器 -> 创建重采样上下文 SwrContext -> 循环读取 AVPacket -> 发送 packet 给解码器 -> 接收 AVFrame -> 使用 swr_convert 转换为 s16 PCM -> 追加到 QByteArray -> 刷新解码器 -> 释放资源

项目中将这部分封装在AudioDecoder类里:

classAudioDecoder{public:booldecodeToPcm(constQString&filePath,QByteArray*pcm,QVariantMap*pcmInfo,QString*errorText)const;};

这个接口保持得比较简单:

  • filePath:输入音视频文件路径
  • pcm:输出 PCM 原始数据
  • pcmInfo:输出 PCM 参数,例如格式、采样率、通道数、大小
  • errorText:失败原因

3. 打开文件并读取流信息

第一步是使用avformat_open_input打开输入文件:

AVFormatContext*formatContext=nullptr;intret=avformat_open_input(&formatContext,filePath.toUtf8().constData(),nullptr,nullptr);if(ret<0){if(errorText)*errorText=FFmpegUtils::errorString(ret);returnfalse;}

AVFormatContext表示整个媒体容器,比如 MP4、MKV、MP3、WAV 等。它负责管理文件格式、媒体流、时长、码率等封装层信息。

打开文件后,还需要调用:

ret=avformat_find_stream_info(formatContext,nullptr);

这一步会让 FFmpeg 尽可能读取并分析媒体流信息。没有这一步,后续可能拿不到完整的 codec 参数、时长、流数量等信息。

4. 查找最佳音频流

一个视频文件里可能有多个流:

  • 视频流
  • 音频流
  • 字幕流
  • 附件流

我们要解码 PCM,只关心音频流。项目中使用 FFmpeg 的av_find_best_stream

constAVCodec*decoder=nullptr;constintaudioStreamIndex=av_find_best_stream(formatContext,AVMEDIA_TYPE_AUDIO,-1,-1,&decoder,0);if(audioStreamIndex<0){if(errorText)*errorText=QStringLiteral("未找到可解码的音频流");returnfalse;}

这个函数会自动帮我们选择一个最合适的音频流,并返回对应的解码器。

拿到流索引后,可以取得AVStream

AVStream*audioStream=formatContext->streams[audioStreamIndex];

5. 创建并打开解码器

FFmpeg 中,AVStream::codecpar保存的是编码参数,例如 codec id、采样率、通道数等。但真正解码需要AVCodecContext

项目中的做法是:

AVCodecContext*codecContext=avcodec_alloc_context3(decoder);if(!codecContext){if(errorText)*errorText=QStringLiteral("无法创建解码上下文");returnfalse;}ret=avcodec_parameters_to_context(codecContext,audioStream->codecpar);if(ret<0){if(errorText)*errorText=FFmpegUtils::errorString(ret);returnfalse;}ret=avcodec_open2(codecContext,decoder,nullptr);if(ret<0){if(errorText)*errorText=FFmpegUtils::errorString(ret);returnfalse;}

这里分三步:

  1. 根据解码器创建AVCodecContext
  2. 将流参数复制到解码上下文
  3. 打开解码器

打开成功后,codecContext中就包含了解码时需要的输入采样格式、采样率、通道布局等信息。

6. 为什么需要重采样

不同音频文件的采样格式可能不一样,例如:

s16 s32 flt fltp dblp

其中带p的格式是 planar 格式,例如fltp。planar 多通道数据不是交错排列,而是分通道存储:

LLLL... RRRR...

为了让上层 Qt 侧处理更简单,项目里统一转换为:

constAVSampleFormat outputSampleFormat=AV_SAMPLE_FMT_S16;constintoutputSampleRate=codecContext->sample_rate>0?codecContext->sample_rate:44100;

也就是说:

  • 输出采样格式固定为s16
  • 输出采样率优先沿用原始音频采样率
  • 输出通道布局沿用原始通道布局

7. 创建 SwrContext

FFmpeg 的libswresample用于音频重采样和格式转换。项目使用swr_alloc_set_opts2创建上下文:

AVChannelLayout inputLayout;AVChannelLayout outputLayout;memset(&inputLayout,0,sizeof(inputLayout));memset(&outputLayout,0,sizeof(outputLayout));if(codecContext->ch_layout.nb_channels>0)av_channel_layout_copy(&inputLayout,&codecContext->ch_layout);elseav_channel_layout_default(&inputLayout,2);av_channel_layout_copy(&outputLayout,&inputLayout);ret=swr_alloc_set_opts2(&swrContext,&outputLayout,outputSampleFormat,outputSampleRate,&inputLayout,codecContext->sample_fmt,codecContext->sample_rate,0,nullptr);

这里的输入参数来自解码器:

codecContext->sample_fmt codecContext->sample_rate inputLayout

输出参数由我们统一指定:

AV_SAMPLE_FMT_S16 outputSampleRate outputLayout

创建后还要初始化:

ret=swr_init(swrContext);if(ret<0){if(errorText)*errorText=FFmpegUtils::errorString(ret);returnfalse;}

8. 读取 Packet 并发送给解码器

FFmpeg 新版解码 API 使用 send/receive 模式:

avcodec_send_packet() avcodec_receive_frame()

项目中的读取循环:

while((ret=av_read_frame(formatContext,packet))>=0){if(packet->stream_index==audioStreamIndex){ret=avcodec_send_packet(codecContext,packet);av_packet_unref(packet);if(ret<0){if(errorText)*errorText=FFmpegUtils::errorString(ret);returnfalse;}if(!receiveFrames())returnfalse;}else{av_packet_unref(packet);}}

注意这里必须判断:

packet->stream_index==audioStreamIndex

因为一个媒体文件中可能包含视频包、字幕包等。只有音频流的 packet 才能送进音频解码器。

每个 packet 用完后都需要:

av_packet_unref(packet);

这样可以释放 packet 内部引用的缓存。

9. 接收 Frame 并转换成 PCM

receiveFrames是项目中定义的 lambda,负责从解码器中取出所有可用的音频帧:

autoreceiveFrames=[&]()->bool{while(true){constintframeRet=avcodec_receive_frame(codecContext,frame);if(frameRet==AVERROR(EAGAIN)||frameRet==AVERROR_EOF)returntrue;if(frameRet<0){if(errorText)*errorText=FFmpegUtils::errorString(frameRet);returnfalse;}// convert frame to PCMav_frame_unref(frame);}};

AVERROR(EAGAIN)表示当前解码器需要更多 packet 才能继续输出 frame,不是错误。

拿到AVFrame后,需要计算输出采样数:

constint64_tdelay=swr_get_delay(swrContext,codecContext->sample_rate);constint64_toutSamples64=av_rescale_rnd(delay+frame->nb_samples,outputSampleRate,codecContext->sample_rate,AV_ROUND_UP);

然后计算输出缓存大小:

constintoutSamples=static_cast<int>(outSamples64);constintbytesPerSample=av_get_bytes_per_sample(outputSampleFormat);constintchannels=outputLayout.nb_channels;constintbufferSize=av_samples_get_buffer_size(nullptr,channels,outSamples,outputSampleFormat,1);

项目中使用QByteArray分配输出缓存:

QByteArray converted;converted.resize(bufferSize);uint8_t*outputData[1]={reinterpret_cast<uint8_t*>(converted.data())};

执行格式转换:

constuint8_t**inputData=const_cast<constuint8_t**>(frame->extended_data);constintconvertedSamples=swr_convert(swrContext,outputData,outSamples,inputData,frame->nb_samples);

转换完成后,截断到真实使用的字节数:

constintusedBytes=convertedSamples*channels*bytesPerSample;converted.truncate(usedBytes);decodedPcm.append(converted);

最终,所有解码出的 PCM 数据都追加到:

QByteArray decodedPcm;

10. 刷新解码器

读取文件结束后,解码器内部可能还有缓存帧没有输出。因此需要发送空 packet 来 flush:

ret=avcodec_send_packet(codecContext,nullptr);if(ret>=0&&!receiveFrames())returnfalse;

这一步很重要,否则文件尾部的少量音频帧可能丢失。

11. 返回 PCM 信息

解码成功后,项目把 PCM 参数写入QVariantMap

QVariantMap resultInfo;resultInfo.insert(QStringLiteral("format"),QStringLiteral("s16"));resultInfo.insert(QStringLiteral("sampleRate"),outputSampleRate);resultInfo.insert(QStringLiteral("channels"),outputLayout.nb_channels);resultInfo.insert(QStringLiteral("channelLayout"),FFmpegUtils::channelLayoutName(outputLayout));resultInfo.insert(QStringLiteral("bitsPerSample"),16);resultInfo.insert(QStringLiteral("size"),decodedPcm.size());resultInfo.insert(QStringLiteral("sizeText"),FFmpegUtils::formatBytes(decodedPcm.size()));

这样 QML 侧不仅能拿到 PCM 数据大小,还能展示 PCM 的格式、采样率和通道布局。

控制层MediaAnalyzer调用解码器后,会把 PCM 信息合并到媒体信息里:

QVariantMap pcmInfo;QByteArray pcm;constboolok=m_audioDecoder.decodeToPcm(filePath,&pcm,&pcmInfo,&errorText);if(ok){QVariantMap info=m_mediaInfo;info.insert(QStringLiteral("pcm"),pcmInfo);setMediaInfo(info);setPcmData(pcm);}

这里的setPcmData(pcm)内部保存到:

QByteArray m_pcmData;

12. 资源释放

FFmpeg 是 C API,资源需要手动释放。项目中统一封装了释放函数:

voidreleaseDecoderResources(AVFormatContext**formatContext,AVCodecContext**codecContext,SwrContext**swrContext,AVPacket**packet,AVFrame**frame,AVChannelLayout*inputLayout,AVChannelLayout*outputLayout){if(packet&&*packet)av_packet_free(packet);if(frame&&*frame)av_frame_free(frame);if(swrContext)swr_free(swrContext);if(inputLayout)av_channel_layout_uninit(inputLayout);if(outputLayout)av_channel_layout_uninit(outputLayout);if(codecContext)avcodec_free_context(codecContext);if(formatContext)avformat_close_input(formatContext);}

释放顺序并不需要绝对固定,但需要保证所有成功创建的对象最终都被释放。

尤其是 FFmpeg 6 使用AVChannelLayout后,通道布局如果通过av_channel_layout_copy创建,也需要调用:

av_channel_layout_uninit(&layout);

13. QML 调用方式

项目中没有让 QML 直接接触AudioDecoder,而是通过控制类MediaAnalyzer暴露接口:

Q_INVOKABLEbooldecodeCurrentToPcm();Q_INVOKABLEbooldecodeToPcm(constQUrl&fileUrl);Q_PROPERTY(intpcmSize READ pcmSize NOTIFY pcmDataChanged)

QML 侧按钮调用:

ActionButton { text: "解码 PCM" accent: true enabled: !mediaAnalyzer.busy && mediaAnalyzer.currentFile !== "" onClicked: mediaAnalyzer.decodeCurrentToPcm() }

展示 PCM 状态:

Text { text: mediaAnalyzer.pcmSize > 0 ? mediaAnalyzer.pcmSummary() : "PCM 未生成" }

这种设计有一个好处:QML 只负责交互和展示,FFmpeg 相关逻辑全部留在 C++ 层,后续维护更清晰。

14. 常见问题

14.1 为什么只解码音频流

PCM 是音频原始数据格式。音视频文件中的视频流解码后通常是 YUV、RGB 等图像帧,不是 PCM。

因此本文实现的是:

音视频文件中的音频流 -> PCM

如果文件只有视频流没有音频流,解码器会返回:

未找到可解码的音频流

14.2 为什么输出 s16

因为s16是很多音频播放和处理流程都支持的基础格式。统一成s16后,上层处理会简单很多。

如果后续需要浮点 PCM,可以把输出格式改成:

AV_SAMPLE_FMT_FLT

同时修改bitsPerSample、buffer size 和显示信息即可。

14.3 大文件会不会占用大量内存

会。当前实现会把完整 PCM 追加到一个QByteArray中:

decodedPcm.append(converted);

这适合短音频、工具演示、分析入口。如果要处理超长音频,建议改成流式输出:

  • 分块写入.pcm文件
  • 分块送给播放器
  • 分块送给波形分析模块
  • 通过 signal 分批返回数据

14.4 如何保存 PCM 到文件

可以在MediaAnalyzer中增加接口:

Q_INVOKABLEboolsavePcm(constQUrl&fileUrl);

保存时直接写出m_pcmData

QFilefile(path);if(!file.open(QIODevice::WriteOnly))returnfalse;file.write(m_pcmData);file.close();

保存出的裸 PCM 需要播放器知道采样率、通道数和格式才能正确播放。

15. 小结

本文实现了一个简洁的 Qt + FFmpeg PCM 解码流程:

AVFormatContext 打开文件 AVStream 查找音频流 AVCodecContext 打开解码器 AVPacket 输入压缩数据 AVFrame 接收解码帧 SwrContext 转换为 s16 PCM QByteArray 保存结果

核心类职责清晰:

AudioDecoder // 只负责解码 MediaAnalyzer // 只负责和 QML 交互 FFmpegUtils // 公共辅助函数

这种结构适合继续扩展更多 FFmpeg 功能,比如导出 WAV、音频裁剪、重采样参数配置、视频抽帧、转码等。

http://www.jsqmd.com/news/986432/

相关文章:

  • 2026六安工伤律师事务所推荐排行 权威评测与选择攻略 - 极欧测评
  • 还在一个个打开PSD找素材?教你一招,文件夹里秒看设计稿内容
  • BGP Peer Group保姆级配置指南:用华为/思科设备5分钟搞定邻居批量管理
  • 天津实体门店黄金回收 专业资质齐全 本地老牌商家靠谱不踩坑 - 奢侈品回收评测
  • 12.linux笔记:线程
  • 【资源下载】一款免费驱动,告别付费
  • 2026年6月指路牌灯箱厂家志科推荐指南 - 多才菠萝
  • 湖北孝感青少年封闭管教中心|孩子叛逆/网瘾/厌学/夜不归宿怎么教育|心理特教团队重塑阳光少年 - 辛云教育资讯
  • 靠谱工业冷水机怎么挑?从资质、技术到工况全维度解析 - 信息热点
  • 2026年合肥医药卫生学校怎么报名?招生条件是什么? - cc江江
  • MySQL查看数据库编码、数据表编码、排序规则(乱码问题彻底解决)
  • 从零搭建企业网:手把手教你用eNSP模拟千人校园网络规划(附拓扑与配置)
  • CAD图纸怎么转换为PDF格式?如何将CAD直接导出为PDF?4个方式轻松搞定!
  • Linux系统编程-线程、互斥锁与多线程模块的封装
  • 2026常州闲置名牌包包变现,8家回收机构横向测评,到手价排行公示 - 生活测评君
  • 告别熬夜凑论文!paperxie 课程论文 AI 写作,一键解锁高效出稿新方式
  • 避坑指南:VS Code verilog-format插件配置最常见的3个错误(及正确设置方法)
  • 2026年重庆市健身塑形训练营哪家好 重庆SGO封闭式健身训练营 联系电话:19122466397 - 速递信息
  • 配电网通信技术全解析:架构方案与应用
  • .NET 领域驱动设计:用户角色更新如何从应用服务落地到领域实体(代码拆解)
  • 华为交换机开启snmp
  • CANoe测试工程师必看:CAPL全局变量在多个Simulation Node里到底怎么用?
  • 全球供应链风险管控视角:解读一体化关务系统的核心价值 - Discorery
  • 避坑指南:MMSegmentation训练自定义数据集时,这些配置项千万别乱改(基于UperNet消融实验)
  • 优利德数字示波器代理商怎么选?价格最低≠最划算,这篇说透了 - 品牌推荐大师
  • 手把手教你快速判断搬家公司是否靠谱,为什么北京利康鸿运值得信赖? - 资讯纵览
  • N100软路由(一) 知己知彼--搞懂你家网络到底在干什么
  • 2026 昌邑厨卫屋面地下室漏水瓷砖空鼓测评:吉修匠 99.8 分五星榜首 - 吉修匠
  • 【Hermes Agent 进阶教程】彻底解决本地大模型/慢速 API 的请求超时问题
  • 别再只知EMD了!VMD、SSA、ITD算法选型指南:从原理到场景的深度解析