音视频-AAC编码

采样格式

必须是16位整数PCM。

采样率

支持的采样率有(Hz): 8000、11025、12000、16000、22050、24000、32000、44100、48000、64000、88200、96000

命令行基本使用

# pcm -> aac
ffmpeg -ar 44100 -ac 2 -f s16le -i in.pcm -c:a libfdk_aac out.aac
 
# wav -> aac
ffmpeg -i in.wav -c:a libfdk_aac out.aac
PCM输入数据的参数

-ar 44100 -ac 2 -f s16le

设置音频编码器, -c:a

c表示codec(编解码器),a表示audio(音频)

等价写法
  • -codec:a
  • -acodec

需要注意的是:这个参数要写在aac文件那边,也就是属于输出参数
默认生成的aac文件是LC规格的。

Win平台下, 命令行转码

ffmpeg -ar 44100 -ac 2 -f s16le -i record_to_pcm.pcm -c:a libfdk_aac pcm_to_aac.aac

PS G:\Resource> ffmpeg -ar 44100 -ac 2 -f s16le -i record_to_pcm.pcm -c:a libfdk_aac pcm_to_aac.aac
ffmpeg version 4.3.2 Copyright (c) 2000-2021 the FFmpeg developers
  built with gcc 10.2.0 (Rev6, Built by MSYS2 project)
  configuration: --prefix=/usr/local/ffmpeg --enable-shared --disable-static --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264
  libavutil      56. 51.100 / 56. 51.100
  libavcodec     58. 91.100 / 58. 91.100
  libavformat    58. 45.100 / 58. 45.100
  libavdevice    58. 10.100 / 58. 10.100
  libavfilter     7. 85.100 /  7. 85.100
  libswscale      5.  7.100 /  5.  7.100
  libswresample   3.  7.100 /  3.  7.100
  libpostproc    55.  7.100 / 55.  7.100
[s16le @ 00000000006e9240] Estimating duration from bitrate, this may be inaccurate
Guessed Channel Layout for Input Stream #0.0 : stereo
Input #0, s16le, from 'record_to_pcm.pcm':
  Duration: 00:00:05.50, bitrate: 1411 kb/s
    Stream #0:0: Audio: pcm_s16le, 44100 Hz, stereo, s16, 1411 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (pcm_s16le (native) -> aac (libfdk_aac))
Press [q] to stop, [?] for help
Output #0, adts, to 'pcm_to_aac.aac':
  Metadata:
    encoder         : Lavf58.45.100
    Stream #0:0: Audio: aac (libfdk_aac), 44100 Hz, stereo, s16, 128 kb/s
    Metadata:
      encoder         : Lavc58.91.100 libfdk_aac
size=      87kB time=00:00:05.50 bitrate= 130.0kbits/s speed= 124x
video:0kB audio:87kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000000%

查看aac文件信息

PS G:\Resource> ffprobe .\pcm_to_aac.aac
ffprobe version 4.3.2 Copyright (c) 2007-2021 the FFmpeg developers
  built with gcc 10.2.0 (Rev6, Built by MSYS2 project)
  configuration: --prefix=/usr/local/ffmpeg --enable-shared --disable-static --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264
  libavutil      56. 51.100 / 56. 51.100
  libavcodec     58. 91.100 / 58. 91.100
  libavformat    58. 45.100 / 58. 45.100
  libavdevice    58. 10.100 / 58. 10.100
  libavfilter     7. 85.100 /  7. 85.100
  libswscale      5.  7.100 /  5.  7.100
  libswresample   3.  7.100 /  3.  7.100
  libpostproc    55.  7.100 / 55.  7.100
[aac @ 000000000078b240] Estimating duration from bitrate, this may be inaccurate
Input #0, aac, from '.\pcm_to_aac.aac':
  Duration: 00:00:05.86, bitrate: 121 kb/s
    Stream #0:0: Audio: aac (LC), 44100 Hz, stereo, fltp, 121 kb/s

pcm文件大小:970200字节
aac文件大小 : 89411字节

970200 / 89411 ≈ 10.85

压缩了10-11倍

PS : 图片内名称有误, 名字就先忽略吧, 正确的是pcm_to_aac

播放器播放



MAC平台下, 命令行转码

~/Desktop 6s ❯ ffmpeg -ar 44100 -ac 2 -f f32le -i record_to_pcm.pcm -c:a libfdk_aac out.aac
ffmpeg version 4.4.git Copyright (c) 2000-2021 the FFmpeg developers
  built with Apple clang version 12.0.0 (clang-1200.0.32.29)
  configuration: --prefix=/usr/local/ffmpeg --enable-shared --disable-static --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265
  libavutil      57.  0.100 / 57.  0.100
  libavcodec     59.  3.101 / 59.  3.101
  libavformat    59.  4.100 / 59.  4.100
  libavdevice    59.  0.100 / 59.  0.100
  libavfilter     8.  0.103 /  8.  0.103
  libswscale      6.  0.100 /  6.  0.100
  libswresample   4.  0.100 /  4.  0.100
  libpostproc    56.  0.100 / 56.  0.100
[f32le @ 0x7fbfa8c0dc00] Estimating duration from bitrate, this may be inaccurate
Guessed Channel Layout for Input Stream #0.0 : stereo
Input #0, f32le, from 'record_to_pcm.pcm':
  Duration: 00:00:06.75, bitrate: 2822 kb/s
  Stream #0:0: Audio: pcm_f32le, 44100 Hz, stereo, flt, 2822 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (pcm_f32le (native) -> aac (libfdk_aac))
Press [q] to stop, [?] for help
Output #0, adts, to 'out.aac':
  Metadata:
    encoder         : Lavf59.4.100
  Stream #0:0: Audio: aac, 44100 Hz, stereo, s16, 128 kb/s
    Metadata:
      encoder         : Lavc59.3.101 libfdk_aac
size=     107kB time=00:00:06.75 bitrate= 129.2kbits/s speed=75.9x    
video:0kB audio:107kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000000%


常用参数

设置输出比特率 -b:a

比如 -b:a 96k

ffmpeg -i in.wav -c:a libfdk_aac -b:a 96k out.aac

设置输出规格 -profile:a

描述
aac_low Low Complexity AAC (LC)
aac_he High Efficiency AAC (HE-AAC)
aac_he_v2 High Efficiency AAC version 2 (HE-AACv2)
aac_ld Low Delay AAC (LD)
aac_eld Enhanced Low Delay AAC (ELD)

一旦设置了输出规格,会自动设置一个合适的输出比特率,也可以用过-b:a自行设置输出比特率。

ffmpeg -i in.wav -c:a libfdk_aac -profile:a aac_he_v2 -b:a 32k out.aac

开启VBR模式 -vbr

如果开启了VBR模式,-b:a选项将会被忽略,但-profile:a选项仍然有效
取值范围是0 ~ 5

  • 0:默认值,关闭VBR模式,开启CBR模式(Constant Bit Rate,固定比特率)
  • 1:质量最低(但是音质仍旧很棒)
  • 5:质量最高
VBR kbps/channel Audio Object Type
1 20-32 LC、HE、HEv2
2 32-40 LC、HE、HEv2
3 48-56 LC、HE、HEv2
4 64-72 LC
5 96-112 LC
ffmpeg -i in.wav -c:a libfdk_aac -vbr 1 out.aac


AAC编码相关函数

avcodec_find_encoder_by_name
avcodec_alloc_context3
avcodec_open2
av_frame_alloc
av_frame_get_buffer
av_packet_alloc
avcodec_send_frame
avcodec_receive_packet



AAC编码逻辑

源文件 ==》AVFrame ==》 编码器 ==> AVPacket ==》输出文件


编码器 AVCodec
typedef struct AVCodec {
    ......
} AVCodec;

编码上下文 AVCodecContext
typedef struct AVCodecContext {
    ......
}
(原始)音频或视频数据 AVFrame
/**
* 此结构描述解码(原始)音频或视频数据。
 *
 * 必须使用 av_frame_alloc() 分配 AVFrame。
 * 请注意,这仅分配 AVFrame 本身,必须通过其他方式管理数据缓冲区(见下文)。
 * 必须使用 av_frame_free() 释放 AVFrame。
 *
 * AVFrame 通常分配一次,然后多次重用以保存不同的数据
 * (例如,单个 AVFrame 用于保存从解码器接收到的帧)。
 * 在这种情况下,av_frame_unref() 将释放框架持有的任何引用,
 * 并将其重置为原始干净状态,然后再重新使用。
 *
 * AVFrame 描述的数据通常通过 AVBuffer API 进行引用计数。
 * 底层缓冲区引用存储在 AVFrame.buf / AVFrame.extended_buf 中。
 * 如果设置了至少一个引用,即如果 AVFrame.buf[0] != NULL,则认为 AVFrame 被引用计数。在这种情况下,
 * 每个数据平面都必须包含在 AVFrame.buf 或 AVFrame.extended_buf 中的缓冲区之一中。
 * 可能有一个用于所有数据的缓冲区,或者每个平面有一个单独的缓冲区,或者介于两者之间。
 *
 * sizeof(AVFrame) 不是公共 ABI 的一部分,因此可能会在末尾添加新字段,并稍作改动。
 *
 * 字段可以通过 AVOptions 访问,所使用的名称字符串与可通过 AVOptions 访问的字段的 C 结构字段名称相匹配。 
 * AVFrame 的 AVClass 可以从 avcodec_get_frame_class() 获得
 */
typedef struct AVFrame {
#define AV_NUM_DATA_POINTERS 8
    ......
}

存储压缩数据结构 AVPacket
/**
 * 此结构存储压缩数据。
 * 它通常由demuxers导出,然后作为输入传递给decoders,或作为encoders的输出接收,然后传递给muxers。
 *
 * 对于视频,它通常应包含一个压缩帧。对于音频,它可能包含多个压缩帧。Encoders可以输出空包,没有压缩数据,只包含边数据(例如,在编码结束时更新一些流参数)。
 *
 * AVPacket 是 FFmpeg 中为数不多的结构体之一,其大小是公共 ABI 的一部分。因此,它可以在堆栈上分配,并且在没有 libavcodec 和 libavformat 主要碰撞的情况下不能向其中添加新字段。
 *
 * 数据所有权的语义取决于 buf 字段。
 * 如果设置,则数据包数据是动态分配的,并且无限期有效,直到调用 av_packet_unref() 将引用计数减少到 0。
 *
 * 如果 buf 字段未设置 av_packet_ref() 将复制而不是增加引用计数。
 *
 * 边数据总是用av_malloc()分配,由av_packet_ref()复制,由av_packet_unref()释放。
 *
 * @see av_packet_ref
 * @see av_packet_unref
 */
typedef struct AVPacket{
    ......
}

查找编码器 avcodec_find_encoder
/**
  * 根据解码器ID值查找匹配的编码器。
  *
  * @param id 请求编码器的 AVCodecID
  * @return 如果找到一个编码器,否则为 NULL。
  */
AVCodec *avcodec_find_encoder(enum AVCodecID id);
编码器上下文 avcodec_alloc_context3
/**
  * 分配一个 AVCodecContext 并将其字段设置为默认值。 这
  * 应使用 avcodec_free_context() 释放结果结构。
  *
  * @param codec 
  *  如果非空,分配私有数据并初始化给定编解码器的默认值。  然后使用不同的编解码器调用 avcodec_open2() 是非法的。
  *  如果为 NULL,则不会初始化特定于编解码器的默认值,这可能会导致默认设置欠佳(这主要对编码器很重要,例如 libx264)。
  *
  * @return 一个 AVCodecContext 填充默认值或失败时为 NULL。
  */
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);


代码实现 Demo

目前针对于的是对PCM源文件进行AAC编码


#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));


#define CHECK_IF_ERROR_BUF_END(ret, funcStr) \
    if (ret) { \
        ERROR_BUF(ret); \
        qDebug() << #funcStr << " error :" << errbuf; \
        goto end; \
    }



#ifdef Q_OS_WIN
    #define IN_PCM_FILEPATH "G:/Resource/record_to_pcm.pcm"
    #define OUT_AAC_FILEPATH "G:/Resource/pcm_to_aac.aac"
#else
    #define IN_PCM_FILEPATH "/Users/liliguang/Desktop/record_to_pcm.pcm"
    #define OUT_AAC_FILEPATH "/Users/liliguang/Desktop/pcm_to_aac.aac"
#endif

// 检查编码器是否支持当前编码格式
static int check_sample_fmt(const AVCodec *codec,
                            enum AVSampleFormat sample_fmt)
{
    const enum AVSampleFormat *p = codec->sample_fmts;
    while (*p != AV_SAMPLE_FMT_NONE) {
        if (*p == sample_fmt)
            return 1;
        p++;
    }
    return 0;
}
// 音频编码
// 返回负数:中途出现了错误
// 返回0:编码操作正常完成
static int encode(AVCodecContext *ctx,
                  AVFrame *frame,
                  AVPacket *pkt,
                  QFile &outFile)
{
    // 发送数据到编码器
    int ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_frame error" << errbuf;
        return ret;
    }

    // 不断从编码器中取出编码后的数据
    // while (ret >= 0)
    while (true) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            // 继续读取数据到frame,然后送到编码器
            return 0;
        } else if (ret < 0) { // 其他错误
            return ret;
        }

        // 成功从编码器拿到编码后的数据
        // 将编码后的数据写入文件
        outFile.write((char *) pkt->data, pkt->size);

        // 释放pkt内部的资源
        av_packet_unref(pkt);
    }

}

void AACEncodeThread::run() {
    qDebug() << "AACEncodeThread run ";

    // 输入输出文件
    const char *infilename;
    const char *outfilename;

    // 编码器
    const AVCodec *codec;
    // 编码器上下文
    AVCodecContext *codecCtx= NULL;
    // 源文件数据源存储结构指针
    AVFrame *frame;
    // 编码文件数据源存储结构指针
    AVPacket *pkt;

    int check_sample_fmt_Ret;
    int avcodec_open2_Ret;
    int av_frame_get_buffer_Ret;

    int infileOpen_Ret;
    int outfileOpen_Ret;

    int readFile_Ret;


    infilename = IN_PCM_FILEPATH;
    outfilename = OUT_AAC_FILEPATH;

    QFile inFile(infilename);
    QFile outFile(outfilename);


    // 打开编码器 , 因为已经编译过FFmpeg, 所以拿到的aac是 fdk-aac
    codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
    CHECK_IF_ERROR_BUF_END(!codec, "avcodec_find_encoder");

    // 创建编码器上下文
    codecCtx = avcodec_alloc_context3(codec);
    CHECK_IF_ERROR_BUF_END(!codecCtx, "avcodec_alloc_context3");

    // 设置编码器上下文参数
    codecCtx->sample_rate = 44100;
    codecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP; // planner格式
    codecCtx->channel_layout = AV_CH_LAYOUT_STEREO;
    codecCtx->channels = av_get_channel_layout_nb_channels(codecCtx->channel_layout);

    // 不同的比特率影响不同的编码大小. 
    // codecCtx->bit_rate = (44100 * 32 * codecCtx->channels);
    codecCtx->bit_rate = 64000;

    // 检查编码器支持的样本格式
    check_sample_fmt_Ret = check_sample_fmt(codec, codecCtx->sample_fmt);
    CHECK_IF_ERROR_BUF_END(!check_sample_fmt_Ret, "check_sample_fmt");

    // 打开编码器
    avcodec_open2_Ret = avcodec_open2(codecCtx,codec,nullptr);
    CHECK_IF_ERROR_BUF_END(avcodec_open2_Ret, "avcodec_open2");

    // 打开源文件
    infileOpen_Ret = !inFile.open(QFile::ReadOnly);
    CHECK_IF_ERROR_BUF_END(infileOpen_Ret, "sourceFile.open");

    // 打开源文件
    outfileOpen_Ret = !outFile.open(QFile::WriteOnly);
    CHECK_IF_ERROR_BUF_END(outfileOpen_Ret, "sourceFile.outFile");

    // 创建输出Packet
    pkt = av_packet_alloc();
    CHECK_IF_ERROR_BUF_END(!pkt, "av_packet_alloc");

    // 创建AVFrame结构体本身
    frame = av_frame_alloc();
    CHECK_IF_ERROR_BUF_END(!frame, "av_frame_alloc");

    // 设置frame必要信息
    frame->format = codecCtx->sample_fmt;//样本格式
    frame->nb_samples = codecCtx->frame_size;//每个声道的样本数量大小
    frame->channel_layout = codecCtx->channel_layout; //声道布局


    // 为音频或视频数据分配新的缓冲区。
    // 在调用此函数之前,必须在框架上设置以下字段:
    // - 格式(视频的像素格式,音频的样本格式)
    // - 视频的宽度和高度
    // - 用于音频的 nb_samples 和 channel_layout
    //
    // 所以需要先设置frame->format, frame->nb_samples , frame->channel_layout
    //
    av_frame_get_buffer_Ret = av_frame_get_buffer(frame,0);
    CHECK_IF_ERROR_BUF_END(av_frame_get_buffer_Ret < 0, "av_frame_get_buffer");


    // 编码
    // 源文件 ==> (AVFrame)输入缓冲区 ==> 编码器 ==> (AVPacket)输出缓冲区 ==> 输出文件
    while( (readFile_Ret = inFile.read((char *)frame->data[0],frame->linesize[0])) > 0 ) {
        if (readFile_Ret < frame->linesize[0] ) {
            
            int bytes = av_get_bytes_per_sample((AVSampleFormat) frame->format); //每个样本大小
            int ch = av_get_channel_layout_nb_channels(frame->channel_layout); // 通道数
            frame->nb_samples = readFile_Ret / (bytes * ch); // 样本数量 /  每个样本的总大小

        } else {
            // 编码
            CHECK_IF_ERROR_BUF_END(encode(codecCtx, frame, pkt, outFile) < 0, "encode");
        }

    }

    // 在读取最后一次, 冲刷最后一次缓冲区数据
    encode(codecCtx,nullptr,pkt,outFile);

end:

    // 关闭文件
    inFile.close();
    outFile.close();

    // 释放资源
    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_free_context(&codecCtx);
    qDebug() << "AACEncodeThread end ";
}


总结 :

AAC编码的简略逻辑 : 源文件 ==》 AVFrame ==》编码器 ==》AVPacket ==> 输出文件

细致化:

源文件

inFile.open(QFile::ReadOnly) 打开文件读取文件内容到缓冲区

AVFrame 发送处理avcodec_send_frame

AVCodec 编码器

AVPacket 接受处理avcodec_receive_packet

outFile.open(QFile::WriteOnly) 从缓冲区读取数据写入文件中去

编码完成

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

推荐阅读更多精彩内容