之前成功将FFmpeg集成进了iOS工程,现在借助前辈的一个小的解码项目来看看如何使用FFmpeg的API来解码一个音频文件。
FFmpeg的解码的总体过程如下:
1、创建一个AVFormatContext对象,这个结构可以理解为一个解码的上下文,从打开文件信息,文件中音频流信息,包括解码器信息等都将保存在这个结构中;
2、打开音频文件,分析音频流信息,包括分析编码格式,创建、打开编码器等;
3、前面的准备工作完毕之后,通过avcoder_decode_audio相关的接口来进行解码,最终就是将aac/mp3等等编码格式解码为原始的PCM格式的数据;解码结果正确的话,通过FFplay是可以播放的;
解码主要过程:
1、第一个阶段:创建AVFormatContext对象,FFmpeg上一个非常完善的库,对于内存的管理也是仔细,在创建AVFormatContext这类关键对象的时候,都提供了相应的接口,以保证对内存和数据的正确管理,所以AVFormat创建方式如下:
avFormatContext = avformat_alloc_context();
这个方法里面没有太多的东西,主要就是分配空间,个人认为,FFmpeg里面对对象的内存管理,抽空可以研究下,里面应该可以有很多收获;
还有一点,之前使用FFmpeg的时候,需要调用av_register_all()这个方法来注册各个组件,不过我现在使用的版本中,这个方法已经标记为废弃了,所以都是直接去初识化组件对象
2、打开音频文件,查找流信息,方法如下:
int result = avformat_open_input(&avFormatContext, filePath, NULL, NULL);
if (result != 0) {
// 打开文件出错;
} else {
// 打开文件成功;
}
avFormatContext->max_analyze_duration = 50000;
result = avformat_find_stream_info(avFormatContext, NULL);
在打开文件中,设置了avFormatContext的max_analyze_duration,这个变量是在后面的avformat_find_stream_info中使用,就是告诉FFmpeg读取多长的数据来分析当前流的信息, 理论上来说,这个值越大,分析到的信息就越多,具体过大有什么影响,目前我也不清楚,以后的学习中遇到再分析吧。
avformat_find_stream_info这个方法顾名思义,就是查询流的信息,查询的依据就是从文件中读出的packet数据,根据API的解释说,这个方法最有用就是查询那些数据头的文件格式,比如MPEG之类的,当然我现在也还不了解MPEG(咱再一步一步来)。同时,这个方法里面读到的数据都会被存入buffer中,解码的时候不会再重复读取;
然后接下来这个方法,让作为新手的我实在有点不太能理解
int stream_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if (stream_index == -1) {
// 标识没有音频流;
}
AVStream *audioStream = avFormatContext->streams[stream_index];
if (audioStream->time_base.den && audioStream->time_base.num) {
timeBase = av_q2d(audioStream->time_base);
} else if (audioStream->codec->time_base.den && audioStream->codec->time_base_num) {
timeBase = av_q2d(audioStrem->codec->time_base);
}
让我迷惑的点就是这个av_find_best_stream方法,从注释中来看,是寻找“最好”(“best”)的流,连FFMpeg自己都打了引号,而这个“best”的含义就是用户所期望的流(翻译的注释,如有错,欢迎指正),但是用户期望的流是什么样子呢?我也不知道 。常规情况下,返回的非负数就说明调用成功了,根据代码,我暂时理解为从index为stream_index的流开始读取和解析,在我使用中,返回的stream_index为0,应该就是从第0个流开始吧;
紧跟着的,对于有理数的定义还不多见 time_base的类型为AVRational,定义如下:
typedef struct AVRational {
int num; // Numerator (分子)
int den; // Denominator (分母)
} AVRational;
这个结构主要用在有小数或者分数的情况下吧,为了避免不能出除尽的误差,用这个结构来描述小数或者分数的中间状态,在最后使用的时候,通过av_q2d这个方法,获取到实际的值,如下。
static inline double av_q2d(AVRational a) {
return a.num / (double) a.den;
}
3、 解码器相关;
当分析足够的流的信息的时候,接下来就是从流信息中,来初始化相应的解码器:
avCodecContext = audioStream->codec;
avCodec *avCodec = avcodec_find_decoder(avCodecContext->codec_id);
if (!avCodec) {
// 未找到相应的解码器;
}
result = avcodec_open2(avCodecContext, avCodec, NULL);
if (result < 0) {
// 解码器打开失败;
}
解码器相关的就比较简单,就是根据分析到的codec_id来寻找相应的解码器,然后打开解码器,即可。我使用的这个版本,audioStream的codec成员被标记为了废弃,推荐使用使用audioStream->codecpar属性,同样,codec目前还能用,所以懵懂的我还是先用着codec。
至此,解码的准备工作基本结束了,大概介绍了几个关键点,接下来就可以开始解码的操作了。
4、解码操作,解码操作其实并不麻烦,简单来讲就是一个while循环,从文件中以AVPakcet读取数据,然后解码成AVFrame数据,将AVFrame数据送到播放模块或者写入文件中即可。大致过程如下:
在进行解码之前,首先要设置一个buffer大小,也就是每一次需要解码多少数据,直到文件结束,需要的数据如下:
采样率sampleRate,这个值来自于前文avFormatContext的sample_rate字段值, 所以buffer大小的计算如下:
int accompanyByteCountPerSec = accompanySampleRate * CHANNEL_PER_FRAME * BITS_PER_CHANNEL / BITS_PER_BYTE;
int accompanyPacketBufferSize = (int)((accompanyByteCountPerSec / 2) * 0.2);
以上是计算出了每一秒采样的字节数accompanyByteCountPerSec,计算的各个量的含义如下:
accompanySampleRate:采样率,即一秒采样的个数;
CHENNAL_PER_FRAME:标识一个Frame中的通道数,当前是2;
BITS_PER_CHANNEL:表示采样位数,即用多少位来描述一个样品,当前是16;
BITS_PER_BYTE:就是一个字节的位数,即 1 Byte = 8 bit ;
所以得出的结果就是1秒中的数据需要的字节数。
后面的accompanyPacketBufferSize就是每次从文件中读取的Packet的大小,使用accompanyByteCountPerSec / 2,我可以理解为每一次读出的是一个通道的数据量,但是后面的乘以0.2我就不知道什么意思了 (希望有大神指点一二)。总之,这就得到了每一次将从文件中读取的数据量;
接下来就是解码的主要过程了:
int CSJDecoder::readSamples(short *sample, int size) {
...
int sampleSize = size;
while (size > 0) {
if (audioBufferCursor < audioBufferSize) {
int audioBufferDataSize = audioBufferSize - audioBufferCursor;
int copySize = MIN(size, audioBufferDataSize);
memcpy(samples + (sampleSize - size), audioBuffer + audioBufferCursor, copySize * 2);
size -= copySize;
audioBufferCursor += copySize;
LOGI("rest size: %d", size);
} else {
int ret = readFrame();
if (ret < 0) {
break;
}
}
}
int fillSize = sampleSize - size;
if (fillSize == 0) {
return -1;
}
return fillSize;
}
以上的while循环,是解码接口外的一个循环,readFrame()方法就是解码数据的方法,稍后说这个方法,audioBufferCursor就是读取当前已解码数据的游标,所以当audioBufferCursor小于audioBufferSize的时候,将持续从已经解码数据中读取音频数据,并存入samples中,当当前已解码数据全部读取完毕后,外层将来处理这个samples中的数据,可以送到播放器播放,也可以写入文件,当前项目是写入PCM文件中(后面我会贴上项目地址)。
然后来看看当前项目中的主角,也就是这个readFrame(),看看它到底做了什么!!!
int ret = 1;
av_init_packet(&packet);
int gotFrame = 0;
int readFrameCode = -1;
while (true) {
// 从文件或者提供的流里面读取下一个packet数据;
readFrameCode = av_read_frame(avFormatContext, &packet);
if (readFrameCode >= 0) {
if (packet.stream_index == stream_index) {
// 对读出的packet进行解码;
int len = avcodec_decode_audio4(avCodecContext, pAudioFrame, &gotFrame, &packet);
if (len < 0) {
LOGI("decode audio error, skip packet!");
}
if (gotFrame) {
int numChannels = OUT_PUT_CHANNELS;
int numFrames = 0;
void *audioData;
if (swrContext) {
... // 先跳过采样格式转换的部分;
audioData = swrBuffer;
} else {
// 不需要重采样;
if (avCodecContext->sample_fmt != AV_SAMPLE_FMT_S16) {
LOGI("bucheck, audio format is invalid");
ret = -1;
break;
}
audioData = pAudioFrame->data[0];
numFrames = pAudioFrame->nb_samples;
}
if (isNeedFirstFrameCorrectFlag && position >= 0) {
float expectedPosition = position + duration;
float actualPosition = av_frame_get_best_effort_timestamp(pAudioFrame) * timeBase;
firstFrameCorrectionInSec = actualPosition - expectedPosition;
isNeedFirstFrameCorrectFlag = false;
}
duration = av_frame_get_pkt_duration(pAudioFrame) * timeBase;
position = av_frame_get_best_effort_timestamp(pAudioFrame) * timeBase - firstFrameCorrectionInSec;
if (!seek_success_read_frame_success) {
LOGI("position si %.6f", position);
actualSeekPosition = position;
seek_success_read_frame_success = true;
}
audioBufferSize = numFrames * numChannels;
audioBuffer = (short *)audioData;
audioBufferCursor = 0;
break;
}
}
} else {
ret = -1;
break;
}
}
av_free_packet(&packet);
return ret;
截取了readFrame中核心的一段代码,其实逻辑很清晰:
首先是从文件中读取数据,FFmpeg对与压缩数据(已编码数据)的抽象是AVPacket,所以使用av_read_frame()方法从文件中读取了一段数据,存放到AVPacket变量中:
readFrameCode = av_read_frame(avFormatContext, &packet);
其注释说明,它只会返回文件中存储的数据,不会验证其是否正确;操作成功时返回的packet对象是有引用计数的,所以在使用完毕之后,需要调用相应的接口释放,如下图;
// 初始化AVPacket;
av_init_packet(&packet);
// 释放AVPacket;
av_free_packet(&packet);
针对AVPacket中的内容也有说明,对于video数据来说,一个packet中包含了一个frame的数据,对于audio数据,如果frame长度已知且固定的话(PCM 或者ADPCM ),一个packet中包含数个完整的frame,而对于frame长度不固定的数据格式(MPEG等),一个packet中只包含一个frame。
然后就是解码的方法:
int len = avcodec_decode_audio4(avCodecContext, pAudioFrame, &gotFrame, &packet);
avcodec_decode_audio4就是用来解码packet中的音频数据,pAudioFrame就是存储了解码结果的数据等信息,需要注意的是,gotFrame这个参数,如果调用之后其值为0,所以当前没有解码的数据,但这种情况不能是解码出错,只有返回值小于0时,才是解码出错。
同样,在当前版本,avcodec_decode_audio4这个方法也是标记为废弃,取而代之的将是avcodec_send_packet() 和avcodec_receive_frame(),具体的用法,后面再一步一步学习。
总结
综上,一次解码的主要过程就是这样,过程看起来还是比较清晰的,但是这里面的具体的东西可太多了,再慢慢深入吧。
可以看到解码过程一些重要的结构,AVFormatContext、AVCodecContext,AVPacket、AVFrame等,然后还有几个关键的方法:avformat_open_input、avformat_find_stream_info、av_read_frame、avcodec_decode_audio4等等,里面还有很多复杂的东西可以去学习和探索的。
该项目中还有一部分关于音频转换的,将8位深度转换成16位深度,暂时省略,转换这一块可以学习之后单独介绍。
其实中间还是很多点不懂的,也可能有些不对的地方,欢迎大神们不吝赐教,也希望跟大家一起学习和探讨。
项目地址:iOS-FFmpegDecoder地址
相信去这个地址的伙伴应该知道我是怎么开始学习的了 ,也确实很感谢xiaokai大神,这本书真的可以算是手把手教学了,感谢大神!
原文链接:FFMpeg--音频解码初识_ffmpeg stream_index_键盘指板的博客-CSDN博客
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓