FFmpeg 使用(三)使用ffmpeg编码音频

参考: 最简单的基于FFMPEG的音频编码器(PCM编码为AAC)

一些参数的意义:
比特率(bitRate): 指编码成AAC文件后的,每秒文件流的大小

实际是用第三方库libfdk_aac编写AAC
1.需要做交叉编译的时候将libfdk_aac库编译到FFmpeg中去 (目前还不太清楚怎么弄的,后续再研究)

AAC:Advanced Audio Coding(高级音频编码)主要为了替代mp3编码, 定义在MPEG-4的第三部分.对于音频使用.m4a扩展名存储.FFmpeg目前支持AAC-LC编码和HE-AAC(V1/V2)编码.而HE-AAC的实现是在libfdk_aac模块中.

FDK_AAC介绍:
可以理解为当前最高质量的AAC编码.编码模式分为CBR和VBR.

主要步骤
1.注册组件,初始化AVFormatContext,打开输出文件格式,
2.编码参数设置,通过AVCodecContext设置码率,采样率,声道数,采样格式,采样类型等
3.获取编码器,将编码器ID赋值给AVCodecContextcodec_id,这样,AVCodecContext编码时就知道自己要使用什么类型的编码器,然后使用int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options)为编码器上下文打开这个编码器,接下来为编码器指定frame_size的大小,一般指定1024作为一帧的大小,至此我们就把编码器部分给分配好了
同时也需要判断编码器所支持的格式是否包含AVCodecContext设置的格式,如果不包含,则需要对AVCodecContext的格式进行转换设置
4.根据AVCodecContext设置输入帧的大小,AVFrame只包含数据和数据大小等,不包含音频总时间,时间点等参数
5.将AVFrame帧数据转换成AVPacket报数据,里面包含有音频总时间,时间点等参数

主要使用的方法

av_register_all():注册FFmpeg所有编解码器。

avformat_alloc_output_context2():初始化输出码流的AVFormatContext。

avio_open():打开输出文件。

av_new_stream():创建输出码流的AVStream。

avcodec_find_encoder():查找编码器。

avcodec_open2():打开编码器。

avformat_write_header():写文件头(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

avcodec_encode_audio2():编码音频。即将AVFrame(存储PCM采样数据)编码为AVPacket(存储AAC,MP3等格式的码流数据)。

av_write_frame():将编码后的视频码流写入文件。

av_write_trailer():写文件尾(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

1.首先需要拿到格式上下文AVFormatContext

    //1.注册ffmpeg组件
    avcodec_register_all();
    //2.给_avFormatContext分配内存
    _avFormatContext = avformat_alloc_context();
  //初始化输出码流的AVFormatContext,根据传入的参数猜测编码格式 内部实际是根据猜测创建了一个AVIOContext然后赋值给了_avFormatContext->pb
int ret = avformat_alloc_output_context2(&_avFormatContext, NULL, NULL, aacFilePath);
 //4.打开输出文件,内部初始化一个URLContext和AVIOContext,根据文件路径查找合适的URLProtocol (URLProtocol可以操作文件的打开,读写,关闭)
avio_open2(&_avFormatContext->pb, aacFilePath, AVIO_FLAG_WRITE, NULL, NULL);

2.根据AVFormatContext创建输出码流AVStream,并从AVStream获取编码格式上下文AVCodecContext

// 创建一个AVStream通道,并分配内存,初始化AVStream一些数据值
_audioStream = avformat_new_stream(_avFormatContext, NULL);
//获取编码格式上下文AVCodecContext
_avCodecContext = _audioStream->codec;
_avCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
    _avCodecContext->sample_rate = audioSampleRate;
_avCodecContext->bit_rate = _publishBitRate;
_avCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;
_avCodecContext->channel_layout = _audioChannels == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
//根据通道的layout返回通道的个数
 _avCodecContext->channels = av_get_channel_layout_nb_channels(_avCodecContext->channel_layout);
    LOGI("_avCodecContext->channels is %d", _avCodecContext->channels);
    //编码类型,低频率AAC
_avCodecContext->profile = FF_PROFILE_AAC_LOW;
    //|=  按位或
 _avCodecContext->flags |= CODEC_FLAG_GLOBAL_HEADER;

3.获取编码器,根据名字寻找对应的编码器, 此处寻找的是libfdk_aac

AVCodec *codec = avcodec_find_encoder_by_name(codec_name);
    //也可以根据AVCodecID id查找编码器
//    avcodec_find_encoder(_avCodecContext->codec_id);
_avCodecContext->codec_id = codec->id;

if (codec->sample_fmts) {
        /*   判断编码器所支持的采样格式       */

        const enum AVSampleFormat *p = codec->sample_fmts;
        for (; *p != -1; p++) {
            if (*p == audioStream->codec->sample_fmt)
                break;
        }
        if (*p == -1) {
            LOGI("sample format incompatible with codec. Defaulting to a format known to work.........");
            /* sample format incompatible with codec. Defaulting to a format known to work */
            avCodecContext->sample_fmt = codec->sample_fmts[0];
        }
    }

    if (codec->supported_samplerates) { 
//判断编码器所支持的采样率,如果没有avCodecContext说设置的采样率,则取最相近的采样率(绝对值最小)
        const int *p = codec->supported_samplerates;
        int best = 0;
        int best_dist = INT_MAX;
        for (; *p; p++) {
            int dist = abs(audioStream->codec->sample_rate - *p); //取采样率最接近的类型去支持
            LOGI("* is %d", *p);
            if (dist < best_dist) {
                best_dist = dist;
                best = *p;
            }
        }
        /* best is the closest supported sample rate (same as selected if best_dist == 0) */
        avCodecContext->sample_rate = best;
    }
    if ( preferedChannels != avCodecContext->channels
            || preferedSampleRate != avCodecContext->sample_rate
            || preferedSampleFMT != avCodecContext->sample_fmt) {
        LOGI("channels is {%d, %d}", preferedChannels, audioStream->codec->channels);
        LOGI("sample_rate is {%d, %d}", preferedSampleRate, audioStream->codec->sample_rate);
        LOGI("sample_fmt is {%d, %d}", preferedSampleFMT, audioStream->codec->sample_fmt);
        LOGI("AV_SAMPLE_FMT_S16P is %d AV_SAMPLE_FMT_S16 is %d AV_SAMPLE_FMT_FLTP is %d", AV_SAMPLE_FMT_S16P, AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_FLTP);
        swrContext = swr_alloc_set_opts(NULL,
                        av_get_default_channel_layout(avCodecContext->channels),
                        (AVSampleFormat)avCodecContext->sample_fmt, avCodecContext->sample_rate,
                        av_get_default_channel_layout(preferedChannels),
                        preferedSampleFMT, preferedSampleRate,
                        0, NULL);
        if (!swrContext || swr_init(swrContext)) {
            if (swrContext)
                swr_free(&swrContext);
            return -1;
        }
    }
//编码器上下文打开这个编码器
    if (avcodec_open2(avCodecContext, codec, NULL) < 0) {
        LOGI("Couldn't open codec");
        return -2;
    }

编码中使用到的API介绍

int avcodec_fill_audio_frame(AVFrame *frame, int nb_channels, enum AVSampleFormat sample_fmt, const uint8_t *buf, int buf_size, int align);

填充AVFrame音频数据并调整指针大小。
缓冲区buf必须是一个预先分配的缓冲区,其大小足以容纳指定的样本量。 填充的AVFrame数据指针将指向此缓冲区。如果需要平面音频,则会分配AVFrame extended_data通道指针。

AVFrame *frame必须在调用函数之前设置frame-> nb_samples。 此函数填充frame-> data,frame-> extended_data,frame-> linesize [0]
int nb_channels 声道数
AVSampleFormat sample_fmt 输入的格式
const uint8_t *buf 用于帧数据的缓冲区
int buf_size 缓存数据大小
int align plane size sample alignment

4.根据AVCodecContext设置输入帧的大小,AVFrame只包含数据和数据大小等,不包含音频总时间,时间点等参数

int ret = 0;
    AVSampleFormat preferedSampleFMT = AV_SAMPLE_FMT_S16;
    int preferedChannels = audioChannels;
    int preferedSampleRate = audioSampleRate;
    input_frame = av_frame_alloc();
    if (!input_frame) {
        LOGI("Could not allocate audio frame\n");
        return -1;
    }
    input_frame->nb_samples = avCodecContext->frame_size;
    input_frame->format = preferedSampleFMT;
    input_frame->channel_layout = preferedChannels == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
    input_frame->sample_rate = preferedSampleRate;
    buffer_size = av_samples_get_buffer_size(NULL, av_get_channel_layout_nb_channels(input_frame->channel_layout),
            input_frame->nb_samples, preferedSampleFMT, 0);
    samples = (uint8_t*) av_malloc(buffer_size);
    samplesCursor = 0;
    if (!samples) {
        LOGI("Could not allocate %d bytes for samples buffer\n", buffer_size);
        return -2;
    }
    LOGI("allocate %d bytes for samples buffer\n", buffer_size);
    /* setup the data pointers in the AVFrame */
    ret = avcodec_fill_audio_frame(input_frame, av_get_channel_layout_nb_channels(input_frame->channel_layout),
            preferedSampleFMT, samples, buffer_size, 0);

5.将AVFrame帧数据转换成AVPacket报数据,里面包含有音频总时间,时间点等参数

    int ret, got_output;
    AVPacket pkt;
    av_init_packet(&pkt);
    AVFrame* encode_frame;

encode_frame = input_frame;
  
    pkt.stream_index = 0;
    pkt.duration = (int) AV_NOPTS_VALUE; //数据的时长,以所属媒体流的时间基准为单位,未知则值为默认值0
    pkt.pts = pkt.dts = 0;  //编码,所以显示时间和解码时长都可以为0
    pkt.data = samples;
    pkt.size = buffer_size;
    //根据音频编码器和音频frame数据编码成音频packt数据
    /**
     avctx:编码器的AVCodecContext。
     avpkt:编码输出的AVPacket。
     frame:编码输入的AVFrame。
     got_packet_ptr:成功编码一个AVPacket的时候设置为1。
     */
    ret = avcodec_encode_audio2(avCodecContext, &pkt, encode_frame, &got_output);

if (ret < 0) {
        LOGI("Error encoding audio frame\n");
        return;
    }
    if (got_output) {//pts dts  显示时间和解码时长,   此处pack包是编码后的时长,所以应该计算pts和dts
        if (avCodecContext->coded_frame && avCodecContext->coded_frame->pts != AV_NOPTS_VALUE)
            //函数用于time_base之间转换,av_rescale_q(a,b,c)作用相当于执行a*b/c,通过设置b,c的值,可以很方便的实现time_base之间转换
            pkt.pts = av_rescale_q(avCodecContext->coded_frame->pts, avCodecContext->time_base, audioStream->time_base);
        pkt.flags |= AV_PKT_FLAG_KEY;
        this->duration = pkt.pts * av_q2d(audioStream->time_base);
        //此函数负责交错地输出一个媒体包。如果调用者无法保证来自各个媒体流的包正确交错,则最好调用此函数输出媒体包,反之,可以调用av_write_frame以提高性能。
        int writeCode = av_interleaved_write_frame(avFormatContext, &pkt);
    }
    av_free_packet(&pkt);

格式转换

当编码器不支持当前设置的编码参数时,需要进行转换

swrContext = swr_alloc_set_opts(NULL,
                        av_get_default_channel_layout(avCodecContext->channels),
                        (AVSampleFormat)avCodecContext->sample_fmt, avCodecContext->sample_rate,
                        av_get_default_channel_layout(preferedChannels),
                        preferedSampleFMT, preferedSampleRate,
                        0, NULL);
        if (!swrContext || swr_init(swrContext)) {
            if (swrContext)
                swr_free(&swrContext);
            return -1;
        }

输入帧AVFrame转换

if(swrContext) {
        if (av_sample_fmt_is_planar(avCodecContext->sample_fmt)) {
            LOGI("Codec Context SampleFormat is Planar...");
        }
        /* 分配空间 */
        convert_data = (uint8_t**)calloc(avCodecContext->channels,
                    sizeof(*convert_data));
        av_samples_alloc(convert_data, NULL,
                avCodecContext->channels, avCodecContext->frame_size,
                avCodecContext->sample_fmt, 0);
        swrBufferSize = av_samples_get_buffer_size(NULL, avCodecContext->channels, avCodecContext->frame_size, avCodecContext->sample_fmt, 0);
        swrBuffer = (uint8_t *)av_malloc(swrBufferSize);
        LOGI("After av_malloc swrBuffer");
        /* 此时data[0],data[1]分别指向frame_buf数组起始、中间地址 */
        swrFrame = av_frame_alloc();
        if (!swrFrame) {
            LOGI("Could not allocate swrFrame frame\n");
            return -1;
        }
        swrFrame->nb_samples = avCodecContext->frame_size;
        swrFrame->format = avCodecContext->sample_fmt;
        swrFrame->channel_layout = avCodecContext->channels == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
        swrFrame->sample_rate = avCodecContext->sample_rate;
        ret = avcodec_fill_audio_frame(swrFrame, avCodecContext->channels, avCodecContext->sample_fmt, (const uint8_t*)swrBuffer, swrBufferSize, 0);
        LOGI("After avcodec_fill_audio_frame");
        if (ret < 0) {
            LOGI("avcodec_fill_audio_frame error ");
            return -1;
        }
    }



swr_convert(swrContext, convert_data, avCodecContext->frame_size,
                (const uint8_t**)input_frame->data, avCodecContext->frame_size);
        int length = avCodecContext->frame_size * av_get_bytes_per_sample(avCodecContext->sample_fmt);
        for (int k = 0; k < 2; ++k) {
            for (int j = 0; j < length; ++j) {
                swrFrame->data[k][j] = convert_data[k][j];
            }
        }

AVFrame

AVFrame:存储非压缩的数据(视频对应RGB/YUV像素数据,音频对应PCM采样数据)

AVPacket

AVPacket:存储压缩数据(视频对应H.264等码流数据,音频对应AAC/MP3等码流数据)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345