ffmpeg项目巨大,本文针对ffmpeg学习(3)编码、解码的流程介绍中编码部分,完成以下内容:

(1) 实现对音频采样数据pcm进行MP3/AAC编码、保存; (2) 音频格式转码,保存封装

关于解码的流程主要流程如下

FFmpegFrameRecorder 录制MP4默认编码格式 ffmpeg mp3编码_音视频编码

流程图中使用的函数,可以通过其名称了解其用处,网上参考较多,也可以直接看api文档。

其中关于结构体AVFrame和AVPackt的介绍参看文章:

ffmpeg学习 结构体分析AVFrameffmpeg学习 结构体分析AVPackffmpeg学习 音频采样数据PCM

ffmpeg_audio_encoder示例代码

和视频编码类似,对于音频pcm数据,根据编码器需送入固定数量的采样数据,编码后保存。由于编码的裸流音频数据文件可能无法播放(AAC不能直接播放),需要进行额外处理或直接封装,关于文件封装格式,参考博客ffmpeg学习(1)背景知识-基础知识ffmpeg学习(4)协议解析、封装解析

(1)MP3编码保存

MP3编码的每一个帧数据,由帧头、附加信息和声音数据,播放器都能从任何帧开始进行解码播放

#include <stdio.h>

#ifdef __cplusplus  
extern "C" {
#endif  

#include "libavcodec/avcodec.h"
#include "libavutil/opt.h"

#ifdef __cplusplus  
}
#endif 

static void encode(AVCodecContext *enc_ctx, const AVFrame *frame, AVPacket *pkt, FILE *outfile)
{
    int ret;

    /* send the frame to the encoder */
    //if(frame)
    //    printf("Send frame %lld\n", frame->pts);

    ret = avcodec_send_frame(enc_ctx, frame);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error sending a frame for encoding\n");
        exit(1);
    }

    while(ret >= 0) {
        ret = avcodec_receive_packet(enc_ctx, pkt);
        if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return;
        }
        else if(ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Error during encoding\n");
            exit(1);
        }

        //write to file
        printf("Write packet %lld (size = %d)\n", pkt->pts, pkt->size);
        fwrite(pkt->data, 1, pkt->size, outfile);

        av_packet_unref(pkt);
    }
}

int main()
{
    // 输入音频文件信息
    int in_sample_rates = 8000;
    uint64_t in_sample_layout = AV_CH_LAYOUT_STEREO;
    AVSampleFormat in_sample_format = AV_SAMPLE_FMT_S16;

    //const char *in_file_name = "../files/Titanic_48000_s16_stero.pcm";
    const char *in_file_name = "../files/Titanic_8000_s16_stero.pcm";

    // 输出编码流文件信息(和输入相同)
    int out_sample_rates = 8000;
    uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;

    //AVCodecID codec_id = AV_CODEC_ID_AAC;
    //AVSampleFormat out_sample_format = AV_SAMPLE_FMT_FLTP;  // aac 固定使用FLTP 
    //const char *out_file_name = "Titanic_out.aac";

    AVCodecID codec_id = AV_CODEC_ID_MP3;
    AVSampleFormat out_sample_format = AV_SAMPLE_FMT_S16P;  // mp3 固定使用S16p 
    const char *out_file_name = "Titanic_out.mp3";

    // 输入输出文件
    FILE *in_file, *out_file;
    in_file = fopen(in_file_name, "rb");
    if(!in_file) {
        fprintf(stderr, "Can not open file %s\n", in_file_name);
        return 0;
    }
    out_file = fopen(out_file_name, "wb");
    if(!out_file) {
        fprintf(stderr, "Can not open file %s\n", out_file_name);
        return 0;
    }

    // video encoder
    const AVCodec *encodec = avcodec_find_encoder(codec_id);
    if(!encodec) {
        av_log(NULL, AV_LOG_ERROR, "Codec '%s' not found\n", avcodec_get_name(codec_id));
        return 0;
    }

    // video encoder contex
    AVCodecContext *encoder_ctx = avcodec_alloc_context3(encodec);
    if(!encoder_ctx) {
        av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context\n");
        return 0;
    }

    // encoder parameters
    encoder_ctx->sample_rate = out_sample_rates;
    encoder_ctx->sample_fmt = out_sample_format;
    encoder_ctx->channel_layout = out_ch_layout;

    // open condec
    int ret = avcodec_open2(encoder_ctx, encodec, NULL);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Could not open codec\n");
        avcodec_free_context(&encoder_ctx);
        return 0;
    }


    // allocate variables
    AVFrame *frame = av_frame_alloc();
    //av_samples_alloc(frame->data, frame->linesize, 
    //                 encoder_ctx->channels, encoder_ctx->frame_size, encoder_ctx->sample_fmt, 1);
    frame->channel_layout = encoder_ctx->channel_layout;
    frame->nb_samples = encoder_ctx->frame_size;
    frame->format = encoder_ctx->sample_fmt;
    av_frame_get_buffer(frame, 1);


    AVPacket *pkt = av_packet_alloc();

    // start encoder
    int64_t frame_cnt = 1;
    while(!feof(in_file)) {

        for(int i = 0; i < frame->nb_samples; i++) {
            int16_t intens[2];
            fread(intens, sizeof(int16_t) * 2, 1, in_file);

            if(codec_id == AV_CODEC_ID_AAC) {
                // s16 => fltp  aac
                *((float*)(frame->data[0]) + i) = intens[0] / 32768.;
                *((float*)(frame->data[1]) + i) = intens[1] / 32768.;
            }
            else if(codec_id == AV_CODEC_ID_MP3) {
                // s16 => s16p  mp3
                *((int16_t*)(frame->data[0]) + i) = intens[0];
                *((int16_t*)(frame->data[1]) + i) = intens[1];
            }
        }

        frame->pts = frame_cnt++;

        encode(encoder_ctx, frame, pkt, out_file);
    }

    // flush
    encode(encoder_ctx, NULL, pkt, out_file);

    fclose(in_file);
    fclose(out_file);

    avcodec_free_context(&encoder_ctx);
    av_frame_free(&frame);
    av_packet_free(&pkt);
}

以上示例将输入Titanic_8000_s16_stero.pcm 进行MP3编码并保存为裸流文件,可以直接进行播放。使用MediaInfo查看信息如下。

FFmpegFrameRecorder 录制MP4默认编码格式 ffmpeg mp3编码_ffmpeg_02

(2)AAC编码保存

调整代码,使用AAC编码,运行上述代码运行,会提示一些警告信息,如

[aac @ 0000016010a81280] Too many bits 16384.000000 > 12288 per frame requested, clamping to max
[aac @ 0000016010a81280] Qavg: 53900.766

需要在编码器参数上额外设置编码码率,

if(codec_id == AV_CODEC_ID_AAC)
        encoder_ctx->bit_rate = 64000; // 1024/1152 * sizeof(float) * chns * 8bit= 65536/71728

前面提到,AAC编码保存的裸流文件,不能直接播放。使用MediaInfo查看信息如下

FFmpegFrameRecorder 录制MP4默认编码格式 ffmpeg mp3编码_音频编码_03

不能直接看到这个文件的任何相关信息,将该文件导入QQ音乐中提示“添加文件出错,强检查文件”。这是因为保存的aac文件首先非封装文件,其次裸流文件中每一帧不包含编码信息,播放器解码器不能正确解析并播放。为每一帧AAC数据添加的编码信息专业术语叫adts头。

根据编码信息,生成adts头数据的函数

static char* adts_gen(const int packetLen)
{
    static char packet[7];

    int profile = 2; // AAC LC AAC级别
    int freqIdx = 11; // 采样频率: 8-16k ,11 8k,  4 - 44.1k, 3 - 48k
    int chanCfg = 2; // CPE 频道数

    // fill in ADTS data
    packet[0] = 0xFF;
    packet[1] = 0xF1;
    packet[2] = (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
    packet[3] = (((chanCfg & 3) << 6) + (packetLen >> 11));
    packet[4] = ((packetLen & 0x7FF) >> 3);
    packet[5] = (((packetLen & 7) << 5) + 0x1F);
    packet[6] = 0xFC;

    return packet;
}

在每一帧aac数据保存前,先保存adts数据

//write to file
        printf("Write packet %lld (size = %d)\n", pkt->pts, pkt->size);

        if(enc_ctx->codec_id == AV_CODEC_ID_AAC) {
            fwrite(adts_gen(pkt->size), 7, 1, outfile);
        }
        fwrite(pkt->data, 1, pkt->size, outfile);

经过以上处理,重新保存aac文件,使用mediaInfo查看文件变大。

FFmpegFrameRecorder 录制MP4默认编码格式 ffmpeg mp3编码_音视频编码_04

编码帧数为376,大小为 (376*7)/1024 = 2.57KB,接近增加的379-376=3KBd大小。此时的acc裸流文件,可以加入到QQ音乐并正常播放。

FFmpegFrameRecorder 录制MP4默认编码格式 ffmpeg mp3编码_aac_05

ffmpeg_audio_encoder 保存为封装文件

在前面的示例基础上,修改保存为AAC和MP3封装文件。

#include <stdio.h>

#ifdef __cplusplus  
extern "C" {
#endif  

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"

#ifdef __cplusplus  
}
#endif 

static void encode(AVCodecContext *enc_ctx, AVFormatContext* fmt_ctx, const AVFrame *frame, AVPacket *pkt)
{
    int ret;

    /* send the frame to the encoder */
    //if(frame)
    //    printf("Send frame %lld\n", frame->pts);

    ret = avcodec_send_frame(enc_ctx, frame);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error sending a frame for encoding\n");
        exit(1);
    }

    while(ret >= 0) {
        ret = avcodec_receive_packet(enc_ctx, pkt);
        if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return;
        }
        else if(ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Error during encoding\n");
            exit(1);
        }

        //write to file
        printf("Write packet %lld (size = %d)\n", pkt->pts, pkt->size);

        ret = av_write_frame(fmt_ctx, pkt);
        if(ret < 0)
            break;

        av_packet_unref(pkt);
    }
}


int main()
{
    // 输入音频文件信息
    int in_sample_rates = 8000;
    uint64_t in_sample_layout = AV_CH_LAYOUT_STEREO;
    AVSampleFormat in_sample_format = AV_SAMPLE_FMT_S16;

    //const char *in_file_name = "../files/Titanic_48000_s16_stero.pcm";
    const char *in_file_name = "../files/Titanic_8000_s16_stero.pcm";

    // 输出编码流文件信息(和输入相同,否则pcm数据需要转换)
    int out_sample_rates = 8000;
    uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;

     //AVCodecID codec_id = AV_CODEC_ID_AAC;
     //AVSampleFormat out_sample_format = AV_SAMPLE_FMT_FLTP;  // aac 固定使用FLTP 
     //const char *out_file_name = "Titanic_out.aac";

    AVCodecID codec_id = AV_CODEC_ID_MP3;
    AVSampleFormat out_sample_format = AV_SAMPLE_FMT_S16P;  // mp3 固定使用S16p 
    const char *out_file_name = "Titanic_out.mp3";

     输入输出文件
    FILE *in_file = fopen(in_file_name, "rb");
    if(!in_file) {
        fprintf(stderr, "Can not open file %s\n", in_file_name);
        return 0;
    }


    //AVFormatContext *fmt_ctx = avformat_alloc_context();
    //fmt_ctx->oformat = av_guess_format(NULL, out_file_name, NULL);
    AVFormatContext *fmt_ctx;
    int ret = avformat_alloc_output_context2(&fmt_ctx, NULL, NULL, out_file_name);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "avformat_alloc_output_context2 failed.\n");
        return 0;
    }

    if(avio_open(&fmt_ctx->pb, out_file_name, AVIO_FLAG_READ_WRITE) < 0) {
        av_log(NULL, AV_LOG_ERROR, "avio_open failed.\n");
        return 0;
    }

    // 输出流
    AVStream *out_stream = avformat_new_stream(fmt_ctx, NULL);

     video encoder
    codec_id = fmt_ctx->oformat->audio_codec;
    const AVCodec *encodec = avcodec_find_encoder(codec_id);
    if(!encodec) {
        av_log(NULL, AV_LOG_ERROR, "Codec '%s' not found\n", avcodec_get_name(codec_id));
        return 0;
    }

    // video encoder contex
    //AVCodecContext *encoder_ctx = out_stream->codec; //废弃的方法
    //encoder_ctx->sample_rate = out_sample_rates;
    //encoder_ctx->sample_fmt = out_sample_format;
    //encoder_ctx->channel_layout = out_ch_layout;

    // video encoder contex
    AVCodecContext *encoder_ctx = avcodec_alloc_context3(encodec);
    if(!encoder_ctx) {
        av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context\n");
        return 0;
    }
    // encoder parameters
    encoder_ctx->sample_rate = out_sample_rates;
    encoder_ctx->sample_fmt = out_sample_format;
    encoder_ctx->channel_layout = out_ch_layout;
    if(codec_id == AV_CODEC_ID_AAC)
        encoder_ctx->bit_rate = 64000; // 1024/1152 * sizeof(float) * chns * 8bit= 65536/71728

    // open condec
    ret = avcodec_open2(encoder_ctx, encodec, NULL);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Could not open codec\n");
        avcodec_free_context(&encoder_ctx);
        return 0;
    }

     新方法,配置输出codec上下文参数(在打开编码器后)
    avcodec_parameters_from_context(out_stream->codecpar, encoder_ctx);

    av_dump_format(fmt_ctx, 0, out_file_name, 1);

    //Write Header
    avformat_write_header(fmt_ctx, NULL);


    // allocate variables
    AVFrame *frame = av_frame_alloc();
    //av_samples_alloc(frame->data, frame->linesize, 
    //                 encoder_ctx->channels, encoder_ctx->frame_size, encoder_ctx->sample_fmt, 1);
    frame->channel_layout = encoder_ctx->channel_layout;
    frame->nb_samples = encoder_ctx->frame_size;
    frame->format = encoder_ctx->sample_fmt;
    av_frame_get_buffer(frame, 1);


    AVPacket *pkt = av_packet_alloc();

    // start encoder
    int64_t frame_cnt = 1;
    while(!feof(in_file)) {

        for(int i = 0; i < frame->nb_samples; i++) {
            int16_t intens[2];
            fread(intens, sizeof(int16_t) * 2, 1, in_file);

            if(codec_id == AV_CODEC_ID_AAC) {
                // s16 => fltp  aac
                *((float*)(frame->data[0]) + i) = intens[0] / 32768.;
                *((float*)(frame->data[1]) + i) = intens[1] / 32768.;
            }
            else if(codec_id == AV_CODEC_ID_MP3) {
                // s16 => s16p  mp3
                *((int16_t*)(frame->data[0]) + i) = intens[0];
                *((int16_t*)(frame->data[1]) + i) = intens[1];
            }
        }

        frame->pts = frame_cnt++;

        encode(encoder_ctx, fmt_ctx, frame, pkt);
    }

    // flush
    encode(encoder_ctx, fmt_ctx, NULL, pkt);

    //Write Trailer
    av_write_trailer(fmt_ctx);

    fclose(in_file);

    avcodec_free_context(&encoder_ctx);
    av_frame_free(&frame);
    av_packet_free(&pkt);

    avio_close(fmt_ctx->pb);
    avformat_free_context(fmt_ctx);
}

先查看aac文件,不同意上面保存的裸流文件无法查看文件详细信息,这里以封装格式保存能看到详细的文件信息,例如文件格式ADTS,声道、码率、采样率等。

FFmpegFrameRecorder 录制MP4默认编码格式 ffmpeg mp3编码_aac_06