FFMpeg--音频解码初识

之前成功将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)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

猜你喜欢

转载自blog.csdn.net/yinshipin007/article/details/131920846