FFmpeg开发笔记(七):ffmpeg解码音频保存为PCM并使用软件播放
若该文为原创文章,未经允许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/108799279
各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究
上一篇:《FFmpeg开发笔记(六):ffmpeg解码视频并使用SDL同步时间显示播放》
下一篇:敬请期待
前言
本篇解码音频,包括从mp3等文件中抽取音频流的pcm,从视频文件中抽取音频流的pcm。
本文章篇幅相对较长,码字作图不易,请各位读者且行且珍惜。
音频基础知识
音频的几个关键因素请查看:《SDL开发笔记(二):音频基础介绍、使用SDL播放音频》
Demo
导入原始文件,设置好数据类型、升到、采样率
软件下载地址
CSDN:https://download.csdn.net/download/qq21497936/12888731
QQ群:1047134658(点击“文件”搜索“audacity”,群内与博文同步更新)
FFmpeg解码音频
ffmpeg解码音频流程
ffmpeg解码音频转码基本流程如下:
步骤一:注册:
使用ffmpeg对应的库,都需要进行注册,可以注册子项也可以注册全部。
步骤二:打开文件:
打开文件,根据文件名信息获取对应的ffmpeg全局上下文。
步骤三:探测流信息:
一定要探测流信息,拿到流编码的编码格式,不探测流信息则其流编码器拿到的编码类型可能为空,后续进行数据转换的时候就无法知晓原始格式,导致错误。
步骤四:查找对应的解码器
依据流的格式查找解码器,软解码还是硬解码是在此处决定的,但是特别注意是否支持硬件,需要自己查找本地的硬件解码器对应的标识,并查询其是否支持。普遍操作是,枚举支持文件后缀解码的所有解码器进行查找,查找到了就是可以硬解了(此处,不做过多的讨论,对应硬解码后续会有文章进行进一步研究)。
(注意:解码时查找解码器,编码时查找编码器,两者函数不同,不要弄错了,否则后续能打开但是数据是错的)
步骤五:打开解码器
打开获取到的解码器。
步骤六:申请重采样结构体
此处特别注意,基本上解码的数据都是pcm格式,pcm格式也分很多种,若8位整形,无符号8为整形,32位浮点,带P和不带P的,带P的数据真存储为LRLRLRLR不带P的为LLLLRRRR,还有单通道、双通道和多通道,通道又涉及到了声道的定位枚举,所以pcm原始数据也多种多样,对齐进行重弄采样使其输出的pcm格式参数特点一致。
步骤七:重采样初始化
重采样结构体设置好后,需要设置生效。
步骤八:解封装获取其中一个数据包。
数据包是封装在容器中的一个数据包。
步骤九:分组数据包送往解码器
拿取封装的一个packet后,判断packet数据的类型进行送往解码器解码。
步骤十:从解码器缓存中获取解码后的数据
一个包可能存在多组数据,老的api获取的是第一个,新的api分开后,可以循环获取,直至获取不到跳转“步骤十二”
步骤十一:样本点重采样
使用冲残阳函数结合转换结构体对编码的数据进行转换,拿到重采样后的音频原始数据。
步骤十二:自行处理
拿到了原始数据自行处理。
继续执行“步骤八”,若步骤八获取不到数据则执行“步骤十二”
步骤十三:释放QAVPacket
此处要单独列出是因为,其实很多网上和开发者的代码:
在进入循环解码前进行了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解码相关变量”。
SwrContext
重采样的结构体,最关键的是几个参数,输入的采样频率、通道布局、数据格式,输出的采样频率、通道布局、数据格式。
ffmpeg解码音频流程相关函数原型
与视频解码通用函数原型请参照博文《FFmpeg开发笔记(四):ffmpeg解码的基本流程详解》中的"ffmpeg解码相关函数原型。
swr_alloc_set_opts
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);
分配并设置重采样的结构体上下文。
- 参数一:输入需要设置的重采样结构体,如果为空,则会由此函数内部进行分配。
- 参数二:输出的通道布局(转换后的)
- 参数三:输出的样本格式(转换后的)
带P和不带P,关系到了AVFrame中的data的数据排列,不带P,则是LRLRLRLRLR排列,带P则是LLLLLRRRRR排列,若是双通道则带P则意味着data[0]全是L,data[1]全是R(注意:这是采样点不是字节),PCM播放器播放的文件需要的是LRLRLRLR的。 - 参数四:输出的采样率(转换后的)
- 参数五:输入的通道布局(转换前的)
- 参数六:输入的样本格式(转换前的)
- 参数七:输入的采样率(转换前的)
- 参数八:日志等级,忽略直接0
- 参数九:日志,忽略直接0
swr_init
int swr_init(struct SwrContext *s);
初始化采样器,使采样器生效。
swr_free
void swr_free(struct SwrContext **s);
释放给定的SwrContext并将指针设置为NULL。
ffmpeg3之后的新解码api解码函数原型
avcodec_send_packet:ffmpeg3新增解码发送数据包给解码器
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
将原始分组数据发送给解码器。
在内部,此调用将复制相关的AVCodeContext字段,这些字段可以影响每个数据包的解码,并在实际解码数据包时应用这些字段。(例如AVCodeContext.skip_frame,这可能会指示解码器丢弃使用此函数发送的数据包所包含的帧。)
这个函数可以理解为ffmpeg为多线程准备的,将解码数据帧包送入编码器理解为一个线程,将从编码器获取解码后的数据理解为一个线程。
- 参数一:编解码器上下文
- 参数二:avpkt输入的AVPacket。通常,这将是一个单一的视频帧,或几个完整的音频帧。数据包的所有权归调用者所有,解码器不会写入数据包。解码器可以创建对分组数据的引用(如果分组没有被引用计数,则复制它)。与旧的API不同,数据包总是被完全消耗掉,如果它包含多个帧(例如某些音频编解码器),则需要在发送新数据包之前多次调用avcodec_receive_frame()。它可以是NULL(或者数据设置为NULL且大小设置为0的AVPacket);在这种情况下,它被认为是一个刷新包,它发出流结束的信号。发送第一个刷新包将返回成功。后续的是不必要的,将返回AVERROR ou EOF。如果解码器仍有帧缓冲,它将在发送刷新包后返回它们。
avcodec_receive_frame:ffmpeg3新增解码从解码器获取解码后的帧
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
从解码器返回解码输出数据。这个函数可以理解为ffmpeg为多线程准备的,将解码数据帧包送入编码器理解为一个线程,将从编码器获取解码后的数据理解为一个线程。
- 参数一:编解码器上下文
- 参数二:这将被设置为参考计数的视频或音频解码器分配的帧(取决于解码器类型)。请注意,函数在执行任何其他操作之前总是调用av_frame_unref(frame),自己释放frame,只有最后一帧不释放。
Demo源码
解码音频不带重采样版本v1.3.0
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)";
}
}
解码音频重采样版本v1.3.1
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、v1.3.1
对应工程模板v1.3.0:增加解码音频裸存pcmDemo
对应工程模板v1.3.1:增加解码音频重采样存pcmDemo
入坑
入坑一:v1.3.0输出的pcm文件音频播放声音变了
原因
存文件存错了,入坑一;字节交错错误,单条音轨是好的,双轨存入文件,使用pcm的软件播放,则默认是LRLRLRLR的方式(采样点交错)。
分析音频文件如下:
解决
入坑二:v1.3.1输出的pcm文件音频播放声音过快
原因
通道布局与通道数据的枚举值是不同的,需要转换
解决
入坑三:v1.3.1输出的pcm文件音频降低采样率出现滴答的声音
原因
重采样之后,采样率不同了,那么对应的时间分片的数据包是相同的,那么很明显,采样率低了,则数据应该减少,时间是一样长的,问题就处在转换函数需要计算一次采样率变了之后的实际采样点,关系到其输出的音频采样点数据,否则长了还好说,短了的话,存入更多就是错误数据,自然就出现声音不对。
解决
入坑四:v1.3.1输出的pcm文件较短
原因
解码mp4封装时,获取到的第一个AVFrame的nb_samples不同,第一帧尾32,本想做动态分布,结果踩坑.
解决
在最前面开辟认为的最大缓存空间,如下:
上一篇:《FFmpeg开发笔记(六):ffmpeg解码视频并使用SDL同步时间显示播放》
下一篇:敬请期待
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/108799279
FFmpeg开发笔记(七):ffmpeg解码音频保存为PCM并使用软件播放的更多相关文章
- FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)
若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...
- FFmpeg开发笔记(四):ffmpeg解码的基本流程详解
若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...
- FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放
前言 ffmpeg播放rtsp网络流和摄像头流. Demo 使用ffmpeg播放局域网rtsp1080p海康摄像头:延迟0.2s,存在马赛克 使用ffmpeg播放网络rtsp文件流 ...
- FFmpeg开发笔记(三):ffmpeg介绍、windows编译以及开发环境搭建
前言 本篇章是对之前windows环境的补充,之前windows的是无需进行编译的,此篇使用源码进行编译,版本就使用3.4.8. FFmpeg简介 FFmpeg是领先的多媒体框架,能够解码 ...
- FFmpeg开发笔记(十):ffmpeg在ubuntu上的交叉编译移植到海思HI35xx平台
FFmpeg和SDL开发专栏(点击传送门) 上一篇:<FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放>下一篇:敬请期待 前言 将ffmpeg移植到海思H ...
- FFmpeg开发笔记(一)搭建Linux系统的开发环境
对于初学者来说,如何搭建FFmpeg的开发环境是个不小的拦路虎,因为FFmpeg用到了许多第三方开发包,所以要先编译这些第三方源码,之后才能给FFmpeg集成编译好的第三方库.不过考虑到刚开始仅仅调用 ...
- ffmpeg学习笔记-初识ffmpeg
ffmpeg用来对音视频进行处理,那么在使用ffmpeg前就需要ffmpeg有一个大概的了解,这里使用雷神的ppt素材进行整理,以便于复习 音视频基础知识 视频播放器的原理 播放视频的流程大致如下: ...
- iOS陆哥开发笔记(七) (AVFoundation简单介绍)
在AVFoundation框架中AVAudioRecorder类专门处理录音操作,支持多种音频格式. 以下是经常使用的属性和方法: 属性 说明 @property(readonly, getter=i ...
- FFmpeg开发笔记(二)搭建Windows系统的开发环境
由于Linux系统比较专业,个人电脑很少安装Linux,反而大都安装Windows系统,因此提高了FFmpeg的学习门槛,毕竟在Windows系统搭建FFmpeg的开发环境还是比较麻烦的.不过若有已经 ...
- netty权威指南学习笔记七——编解码技术之GoogleProtobuf
首先我们来看一下protobuf的优点: 谷歌长期使用成熟度高: 跨语言支持多种语言如:C++,java,Python: 编码后消息更小,更利于存储传输: 编解码性能高: 支持不同协议版本的兼容性: ...
随机推荐
- [转帖]官网:Nacos的授权验证
https://nacos.io/zh-cn/docs/v2/guide/user/auth.html 注意 Nacos是一个内部微服务组件,需要在可信的内部网络中运行,不可暴露在公网环境,防止带来安 ...
- [转帖]Springboot容器化镜像设置堆内存大小
参考资料:Best Practices: Java Memory Arguments for Containers - DZone Java 有三种方式设置最大堆内存大小: 1. -Xmx 2. -X ...
- 文心一言 VS 讯飞星火 VS chatgpt (186)-- 算法导论14.1 3题
三.用go语言,写出OS-SELECT的非递归版本. 文心一言: 下面是使用Go语言编写的OS-SELECT的非递归版本: func OS_SELECT(T *Node, k int) *Node { ...
- 我们开源了一个轻量的 Web IDE UI 框架
我们开源了一个轻量的 Web IDE UI 框架 Molecule 一个轻量的 Web IDE UI 框架 简介 Molecule 是一个受 VS Code 启发,使用 React.js 构建的 We ...
- 让你彻底理解Typescript中静态成员和抽象方法
1.Typescript中static详解 静态成员:在类中通过 static 修饰的属性或者方法 那么就是静态的属性静态方法 也称之为:静态成员(静态成员包含静态属性和静态方法) 静态成员在使用的时 ...
- 如何处理开发环境没有问题,线上环境有问题这个bug
解决思路 首先确认开发环境有没有这个问题: 如果没有这个问题: 将你的地址切换为线上的环境,看看线上环境有没有这个问题: 如果切换为线上环境有这个问题,就可以调试了: 如果切换为线上环境没有这个问题, ...
- 原生js拖拽元素(onmouseup不能够触发的原因)
我们经常会遇见拖拽某一个元素的场景,拖拽也是很常用的: 这次拖拽遇见一个问题,有时在拖拽的时候吗,鼠标松开,元素仍然可以拖拽: 经过查阅资料,发现: 会触发H5原生的拖拽事件.并且不会监听到onmou ...
- 【解决一个小问题】macbook m2 上交叉编译 gozstd
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 已知 zstd 是一个优秀的压缩库,gozstd封装了这个 ...
- Go 匿名函数与闭包
Go 匿名函数与闭包 匿名函数和闭包是一些编程语言中的重要概念,它们在Go语言中也有重要的应用.让我们来详细介绍这两个概念,并提供示例代码来帮助理解. 目录 Go 匿名函数与闭包 一.匿名函数(Ano ...
- 应用实践:Paddle分类模型大集成者[PaddleHub、Finetune、prompt]
相关文章: Paddlenlp之UIE模型实战实体抽取任务[打车数据.快递单] Paddlenlp之UIE分类模型[以情感倾向分析新闻分类为例]含智能标注方案) 项目连接: 应用实践:分类模型大集成者 ...