若该文为原创文章,未经容许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:http://www.javashuo.com/article/p-wxwjppoc-mo.html
本文章博客地址:http://www.javashuo.com/article/p-gmllrnso-mo.html
各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么本身研究web
红胖子(红模仿)的博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结合等等)持续更新中…(点击传送门)api
上一篇:《FFmpeg开发笔记(六):ffmpeg解码视频并使用SDL同步时间显示播放》
下一篇:《FFmpeg开发笔记(八):ffmpeg解码音频并使用SDL同步音频播放》缓存
本篇解码音频,包括从mp3等文件中抽取音频流的pcm,从视频文件中抽取音频流的pcm。
本文章篇幅相对较长,码字做图不易,请各位读者且行且珍惜。多线程
音频的几个关键因素请查看:《SDL开发笔记(二):音频基础介绍、使用SDL播放音频》ide
导入原始文件,设置好数据类型、声道、采样率
svg
CSDN:https://download.csdn.net/download/qq21497936/12888731
QQ群:1047134658(点击“文件”搜索“audacity”,群内与博文同步更新)函数
ffmpeg解码音频转码基本流程以下:
布局
使用ffmpeg对应的库,都须要进行注册,能够注册子项也能够注册所有。ui
打开文件,根据文件名信息获取对应的ffmpeg全局上下文。编码
必定要探测流信息,拿到流编码的编码格式,不探测流信息则其流编码器拿到的编码类型可能为空,后续进行数据转换的时候就没法知晓原始格式,致使错误。
依据流的格式查找解码器,软解码仍是硬解码是在此处决定的,可是特别注意是否支持硬件,须要本身查找本地的硬件解码器对应的标识,并查询其是否支持。广泛操做是,枚举支持文件后缀解码的全部解码器进行查找,查找到了就是能够硬解了(此处,不作过多的讨论,对应硬解码后续会有文章进行进一步研究)。
(注意:解码时查找解码器,编码时查找编码器,二者函数不一样,不要弄错了,不然后续能打开可是数据是错的)
打开获取到的解码器。
此处特别注意,基本上解码的数据都是pcm格式,pcm格式也分不少种,若8位整形,无符号8为整形,32位浮点,带P和不带P的,不带P的数据真存储为LRLRLRLR,带P的为LLLLRRRR,还有单通道、双通道和多通道,通道又涉及到了声道的定位枚举,因此pcm原始数据也多种多样,对齐进行重弄采样使其输出的pcm格式参数特色一致。
重采样结构体设置好后,须要设置生效。
数据包是封装在容器中的一个数据包。获取不到数据则跳转“步骤十四”
拿取封装的一个packet后,判断packet数据的类型进行送往解码器解码。
一个包可能存在多组数据,老的api获取的是第一个,新的api分开后,能够循环获取,直至获取不到跳转“步骤十三”
使用冲残阳函数结合转换结构体对编码的数据进行转换,拿到重采样后的音频原始数据。
拿到了原始数据自行处理。
循环解码跳转“步骤八”,若步骤八获取不到数据则执行“步骤十四”
此处要单独列出是由于,其实不少网上和开发者的代码:
在进入循环解码前进行了av_new_packet,循环中未av_free_packet,形成内存溢出;
在进入循环解码前进行了av_new_packet,循环中进行av_free_pakcet,那么一次new对应无数次free,在编码器上是不符合先后一一对应规范的。
查看源代码,其实能够发现av_read_frame时,自动进行了av_new_packet(),那么其实对于packet,只须要进行一次av_packet_alloc()便可,解码完后av_free_packet。
执行完后,返回执行“步骤八:获取一帧packet”,一次循环结束。
所有解码完成后,按照申请顺序,反向依次进行对应资源的释放。
关闭以前打开的解码/编码器。
关闭文件上下文后,要对以前申请的变量按照申请的顺序,依次释放。
与视频解码通用变量请参照博文《FFmpeg开发笔记(四):ffmpeg解码的基本流程详解》中的“ffmpeg解码相关变量”。
重采样的结构体,最关键的是几个参数,输入的采样频率、通道布局、数据格式,输出的采样频率、通道布局、数据格式。
与视频解码通用函数原型请参照博文《FFmpeg开发笔记(四):ffmpeg解码的基本流程详解》中的“ffmpeg解码相关函数原型”。
struct SwrContext *swr_alloc_set_opts(struct SwrContext *s, int64_t out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate, int64_t in_ch_layout, enum AVSampleFormat in_sample_fmt, int in_sample_rate, int log_offset, void *log_ctx);
分配并设置重采样的结构体上下文。
int swr_init(struct SwrContext *s);
初始化采样器,使采样器生效。
void swr_free(struct SwrContext **s);
释放给定的SwrContext并将指针设置为NULL。
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
将原始分组数据发送给解码器。
在内部,此调用将复制相关的AVCodeContext字段,这些字段能够影响每一个数据包的解码,并在实际解码数据包时应用这些字段。(例如AVCodeContext.skip_frame,这可能会指示解码器丢弃使用此函数发送的数据包所包含的帧。)
这个函数能够理解为ffmpeg为多线程准备的,将解码数据帧包送入编码器理解为一个线程,将从编码器获取解码后的数据理解为一个线程。
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
从解码器返回解码输出数据。这个函数能够理解为ffmpeg为多线程准备的,将解码数据帧包送入编码器理解为一个线程,将从编码器获取解码后的数据理解为一个线程。
void FFmpegManager::testDecodeAudio() { QString fileName = "test/1.avi"; // QString fileName = "test/1.mp4"; // QString fileName = "E:/testFile2/1.mp3"; QString outFileName = "E:/1.pcm"; // ffmpeg相关变量预先定义与分配 AVFormatContext *pAVFormatContext = 0; // ffmpeg的全局上下文,全部ffmpeg操做都须要 AVCodecContext *pAVCodecContext = 0; // ffmpeg编码上下文 AVCodec *pAVCodec = 0; // ffmpeg编码器 AVPacket *pAVPacket = 0; // ffmpag单帧数据包 AVFrame *pAVFrame = 0; // ffmpeg单帧缓存 QFile file(outFileName); // Qt文件操做 int ret = 0; // 函数执行结果 int audioIndex = -1; // 音频流所在的序号 int numBytes = 0; pAVFormatContext = avformat_alloc_context(); // 分配 pAVPacket = av_packet_alloc(); // 分配 pAVFrame = av_frame_alloc(); // 分配 if(!pAVFormatContext || !pAVPacket || !pAVFrame) { LOG << "Failed to alloc"; goto END; } // 步骤一:注册全部容器和编解码器(也能够只注册一类,如注册容器、注册编码器等) av_register_all(); // 步骤二:打开文件(ffmpeg成功则返回0) LOG << "文件:" << fileName << ",是否存在:" << QFile::exists(fileName); // ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), pAVInputFormat, 0); ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), 0, 0); if(ret) { LOG << "Failed"; goto END; } // 步骤三:探测流媒体信息 ret = avformat_find_stream_info(pAVFormatContext, 0); if(ret < 0) { LOG << "Failed to avformat_find_stream_info(pAVCodecContext, 0)"; goto END; } LOG << "视频文件包含流信息的数量:" << pAVFormatContext->nb_streams; // 步骤四:提取流信息,提取视频信息 for(int index = 0; index < pAVFormatContext->nb_streams; index++) { pAVCodecContext = pAVFormatContext->streams[index]->codec; switch (pAVCodecContext->codec_type) { case AVMEDIA_TYPE_UNKNOWN: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_UNKNOWN"; break; case AVMEDIA_TYPE_VIDEO: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_VIDEO"; break; case AVMEDIA_TYPE_AUDIO: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_AUDIO"; audioIndex = index; break; case AVMEDIA_TYPE_DATA: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_DATA"; break; case AVMEDIA_TYPE_SUBTITLE: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_SUBTITLE"; break; case AVMEDIA_TYPE_ATTACHMENT: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_ATTACHMENT"; break; case AVMEDIA_TYPE_NB: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_NB"; break; default: break; } // 已经找打视频品流 if(audioIndex != -1) { break; } } if(audioIndex == -1 || !pAVCodecContext) { LOG << "Failed to find video stream"; goto END; } // 步骤五:对找到的音频流寻解码器 pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id); if(!pAVCodec) { LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):" << pAVCodecContext->codec_id; goto END; } #if 0 pAVCodecContext = avcodec_alloc_context3(pAVCodec); // 填充CodecContext信息 if (avcodec_parameters_to_context(pAVCodecContext, pAVFormatContext->streams[audioIndex]->codecpar) < 0) { printf("Failed to copy codec parameters to decoder context!\n"); goto END; } #endif // 步骤六:打开解码器 ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL); if(ret) { LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)"; goto END; } // 打印 LOG << "解码器名称:" <<pAVCodec->name << "通道数:" << pAVCodecContext->channels << "采样率:" << pAVCodecContext->sample_rate << "采样格式:" << pAVCodecContext->sample_fmt; file.open(QIODevice::WriteOnly | QIODevice::Truncate); // 步骤七:读取一帧数据的数据包 while(av_read_frame(pAVFormatContext, pAVPacket) >= 0) { if(pAVPacket->stream_index == audioIndex) { // 步骤八:将封装包发往解码器 ret = avcodec_send_packet(pAVCodecContext, pAVPacket); if(ret) { LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret; break; } // 步骤九:从解码器循环拿取数据帧 while(!avcodec_receive_frame(pAVCodecContext, pAVFrame)) { // for(int index = 0; index < pAVFrame->linesize[0]; index++) // { // 入坑一;字节交错错误,单条音轨是好的,双轨存入文件,使用pcm的软件播放,则默认是LRLRLRLR的方式(采样点交错) // file.write((const char *)(pAVFrame->data[0] + index), 1); // file.write((const char *)(pAVFrame->data[1] + index), 1); // } // 入坑一;字节交错错误,单条音轨是好的,双轨存入文件,使用pcm的软件播放,则默认是LRLRLRLR的方式(采样点交错) // file.write((const char *)(pAVFrame->data[0], pAVFrame->linesize[0]); // file.write((const char *)(pAVFrame->data[1], pAVFrame->linesize[0]); // 输出为2, S16P格式是2字节 numBytes = av_get_bytes_per_sample(pAVCodecContext->sample_fmt); // LOG << "numBytes =" << numBytes; /* P表示Planar(平面),其数据格式排列方式为 (特别记住,该处是以点nb_samples采样点来交错,不是以字节交错): LLLLLLRRRRRRLLLLLLRRRRRRLLLLLLRRRRRRL...(每一个LLLLLLRRRRRR为一个音频帧) 而不带P的数据格式(即交错排列)排列方式为: LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRL...(每一个LR为一个音频样本) */ // 使用命令行提取pcm ffmpeg.exe -i 1.mp3 -f s16le -ar 44100 -ac 2 -acodec pcm_s16le D:/2.pcm for (int index = 0; index < pAVFrame->nb_samples; index++) { for (int channel = 0; channel < pAVCodecContext->channels; channel++) // 交错的方式写入, 大部分float的格式输出 { file.write((char *)pAVFrame->data[channel] + numBytes * index, numBytes); } } av_free_packet(pAVPacket); } } } file.close(); END: LOG << "释放回收资源"; if(pAVFrame) { av_frame_free(&pAVFrame); pAVFrame = 0; LOG << "av_frame_free(pAVFrame)"; } if(pAVPacket) { av_free_packet(pAVPacket); pAVPacket = 0; LOG << "av_free_packet(pAVPacket)"; } if(pAVCodecContext) { avcodec_close(pAVCodecContext); pAVCodecContext = 0; LOG << "avcodec_close(pAVCodecContext);"; } if(pAVFormatContext) { avformat_close_input(&pAVFormatContext); avformat_free_context(pAVFormatContext); pAVFormatContext = 0; LOG << "avformat_free_context(pAVFormatContext)"; } }
void FFmpegManager::testDecodeAudioForPcm() { // QString fileName = "test/1.avi"; QString fileName = "E:/testFile/3.mp4"; // QString fileName = "E:/testFile2/1.mp3"; QString outFileName = "D:/1.pcm"; AVFormatContext *pAVFormatContext = 0; // ffmpeg的全局上下文,全部ffmpeg操做都须要 AVCodecContext *pAVCodecContext = 0; // ffmpeg编码上下文 AVCodec *pAVCodec = 0; // ffmpeg编码器 AVPacket *pAVPacket = 0; // ffmpag单帧数据包 AVFrame *pAVFrame = 0; // ffmpeg单帧缓存 SwrContext *pSwrContext = 0; // ffmpeg音频转码 QFile file(outFileName); // Qt文件操做 int ret = 0; // 函数执行结果 int audioIndex = -1; // 音频流所在的序号 int numBytes = 0; uint8_t * outData[2] = {0}; int dstNbSamples = 0; // 解码目标的采样率 int outChannel = 0; // 重采样后输出的通道 AVSampleFormat outFormat = AV_SAMPLE_FMT_NONE; // 重采样后输出的格式 int outSampleRate = 0; // 重采样后输出的采样率 pAVFormatContext = avformat_alloc_context(); // 分配 pAVPacket = av_packet_alloc(); // 分配 pAVFrame = av_frame_alloc(); // 分配 if(!pAVFormatContext || !pAVPacket || !pAVFrame) { LOG << "Failed to alloc"; goto END; } // 步骤一:注册全部容器和编解码器(也能够只注册一类,如注册容器、注册编码器等) av_register_all(); // 步骤二:打开文件(ffmpeg成功则返回0) LOG << "文件:" << fileName << ",是否存在:" << QFile::exists(fileName); // ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), pAVInputFormat, 0); ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), 0, 0); if(ret) { LOG << "Failed"; goto END; } // 步骤三:探测流媒体信息 ret = avformat_find_stream_info(pAVFormatContext, 0); if(ret < 0) { LOG << "Failed to avformat_find_stream_info(pAVCodecContext, 0)"; goto END; } LOG << "视频文件包含流信息的数量:" << pAVFormatContext->nb_streams; // 步骤四:提取流信息,提取视频信息 for(int index = 0; index < pAVFormatContext->nb_streams; index++) { pAVCodecContext = pAVFormatContext->streams[index]->codec; switch (pAVCodecContext->codec_type) { case AVMEDIA_TYPE_UNKNOWN: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_UNKNOWN"; break; case AVMEDIA_TYPE_VIDEO: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_VIDEO"; break; case AVMEDIA_TYPE_AUDIO: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_AUDIO"; audioIndex = index; break; case AVMEDIA_TYPE_DATA: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_DATA"; break; case AVMEDIA_TYPE_SUBTITLE: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_SUBTITLE"; break; case AVMEDIA_TYPE_ATTACHMENT: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_ATTACHMENT"; break; case AVMEDIA_TYPE_NB: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_NB"; break; default: break; } // 已经找打视频品流 if(audioIndex != -1) { break; } } if(audioIndex == -1 || !pAVCodecContext) { LOG << "Failed to find video stream"; goto END; } // 步骤五:对找到的音频流寻解码器 pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id); if(!pAVCodec) { LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):" << pAVCodecContext->codec_id; goto END; } #if 0 pAVCodecContext = avcodec_alloc_context3(pAVCodec); // 填充CodecContext信息 if (avcodec_parameters_to_context(pAVCodecContext, pAVFormatContext->streams[audioIndex]->codecpar) < 0) { printf("Failed to copy codec parameters to decoder context!\n"); goto END; } #endif // 步骤六:打开解码器 ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL); if(ret) { LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)"; goto END; } // 打印 LOG << "解码器名称:" <<pAVCodec->name << endl << "通道数:" << pAVCodecContext->channels << endl << "通道布局:" << av_get_default_channel_layout(pAVCodecContext->channels) << endl << "采样率:" << pAVCodecContext->sample_rate << endl << "采样格式:" << pAVCodecContext->sample_fmt; #if 1 outChannel = 2; outSampleRate = 44100; outFormat = AV_SAMPLE_FMT_S16P; #endif #if 0 outChannel = 2; outSampleRate = 48000; outFormat = AV_SAMPLE_FMT_FLTP; #endif LOG << "to" << endl << "通道数:" << outChannel << endl << "通道布局:" << av_get_default_channel_layout(outChannel) << endl << "采样率:" << outSampleRate << endl << "采样格式:" << outFormat; // 步骤七:获取音频转码器并设置采样参数初始化 // 入坑二:通道布局与通道数据的枚举值是不一样的,须要转换 pSwrContext = swr_alloc_set_opts(0, // 输入为空,则会分配 av_get_default_channel_layout(outChannel), outFormat, // 输出的采样频率 outSampleRate, // 输出的格式 av_get_default_channel_layout(pAVCodecContext->channels), pAVCodecContext->sample_fmt, // 输入的格式 pAVCodecContext->sample_rate, // 输入的采样率 0, 0); ret = swr_init(pSwrContext); if(ret < 0) { LOG << "Failed to swr_init(pSwrContext);"; goto END; } file.open(QIODevice::WriteOnly | QIODevice::Truncate); outData[0] = (uint8_t *)av_malloc(1152 * 8); outData[1] = (uint8_t *)av_malloc(1152 * 8); // 步骤七:读取一帧数据的数据包 while(av_read_frame(pAVFormatContext, pAVPacket) >= 0) { if(pAVPacket->stream_index == audioIndex) { // 步骤八:将封装包发往解码器 ret = avcodec_send_packet(pAVCodecContext, pAVPacket); if(ret) { LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret; break; } // 步骤九:从解码器循环拿取数据帧 while(!avcodec_receive_frame(pAVCodecContext, pAVFrame)) { // nb_samples并非每一个包都相同,碰见过第一个包为47,第二个包开始为1152的 // LOG << pAVFrame->nb_samples; // 步骤十:获取每一个采样点的字节大小 numBytes = av_get_bytes_per_sample(outFormat); // 步骤十一:修改采样率参数后,须要从新获取采样点的样本个数 dstNbSamples = av_rescale_rnd(pAVFrame->nb_samples, outSampleRate, pAVCodecContext->sample_rate, AV_ROUND_ZERO); // 步骤十二:重采样 swr_convert(pSwrContext, outData, dstNbSamples, (const uint8_t **)pAVFrame->data, pAVFrame->nb_samples); // 第一次显示 static bool show = true; if(show) { LOG << numBytes << pAVFrame->nb_samples << "to" << dstNbSamples; show = false; } // 步骤十四:使用LRLRLRLRLRL(采样点为单位,采样点有几个字节,交替存储到文件,可以使用pcm播放器播放) for (int index = 0; index < dstNbSamples; index++) { for (int channel = 0; channel < pAVCodecContext->channels; channel++) // 交错的方式写入, 大部分float的格式输出 { // 用于原始文件jinxin跟对比 // file.write((char *)pAVFrame->data[channel] + numBytes * index, numBytes); file.write((char *)outData[channel] + numBytes * index, numBytes); } } av_free_packet(pAVPacket); } } } file.close(); END: LOG << "释放回收资源"; if(outData[0] && outData[1]) { av_free(outData[0]); av_free(outData[1]); outData[0] = 0; outData[1] = 0; LOG << "av_free(outData[0])"; LOG << "av_free(outData[1])"; } if(pSwrContext) { swr_free(&pSwrContext); pSwrContext = 0; } if(pAVFrame) { av_frame_free(&pAVFrame); pAVFrame = 0; LOG << "av_frame_free(pAVFrame)"; } if(pAVPacket) { av_free_packet(pAVPacket); pAVPacket = 0; LOG << "av_free_packet(pAVPacket)"; } if(pAVCodecContext) { avcodec_close(pAVCodecContext); pAVCodecContext = 0; LOG << "avcodec_close(pAVCodecContext);"; } if(pAVFormatContext) { avformat_close_input(&pAVFormatContext); avformat_free_context(pAVFormatContext); pAVFormatContext = 0; LOG << "avformat_free_context(pAVFormatContext)"; } }
对应工程模板v1.3.0:增长解码音频裸存pcmDemo
对应工程模板v1.3.1:增长解码音频重采样存pcmDemo
存文件存错了,入坑一;字节交错错误,单条音轨是好的,双轨存入文件,使用pcm的软件播放,则默认是LRLRLRLR的方式(采样点交错)。
分析音频文件以下:
通道布局与通道数据的枚举值是不一样的,须要转换
重采样以后,采样率不一样了,那么对应的时间分片的数据包是相同的,那么很明显,采样率低了,则数据应该减小,时间是同样长的,问题就处在转换函数须要计算一次采样率变了以后的实际采样点,关系到其输出的音频采样点数据,不然长了还好说,短了的话,存入更多就是错误数据,天然就出现声音不对。
解码mp4封装时,获取到的第一个AVFrame的nb_samples不一样,第一帧尾32,本想作动态分布,结果踩坑.
在最前面开辟认为的最大缓存空间,以下:
上一篇:《FFmpeg开发笔记(六):ffmpeg解码视频并使用SDL同步时间显示播放》
下一篇:《FFmpeg开发笔记(八):ffmpeg解码音频并使用SDL同步音频播放》
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:http://www.javashuo.com/article/p-wxwjppoc-mo.html
本文章博客地址:http://www.javashuo.com/article/p-gmllrnso-mo.html