【FFmpeg实战】手撕音频转码:WAV转AAC的全链路解析与C++实现
在音视频开发中音频转码是一个永远绕不开的经典课题。很多初学者在刚接触 FFmpeg 时往往会被眼花缭乱的结构体AVFrame、AVPacket和错综复杂的内存管理搞得晕头转向。今天我们将抛开繁杂的解封装外壳直击底层数据流转的灵魂。本文将带你手写一段WAV 转 AAC的 C 代码不仅给你最终的源码更会为你剖析为什么要这么写从重采样的数学逻辑到编码器的“硬性规矩”再到时间戳PTS的“铺砖理论”。一、 核心概念扫盲转码到底在转什么WAV 转 AAC不仅仅是改个后缀名那么简单。本质上我们要跨越两道鸿沟1. 格式的鸿沟为什么需要重采样ResamplingWAV 文件里存放的通常是原始的 PCM 数据一般为16位整型 S16。但是AAC 编码器是个“挑食”的精密仪器为了进行复杂的心理声学模型运算它硬性要求输入的数据必须是浮点平面格式FLTP。 因此我们必须在中间加一台“重采样器SwrContext”把 S16 的粗粮加工成 FLTP 的细粮。2. 大小的鸿沟nb_samples与frame_size的恩怨c-frame_size编码器的规矩AAC 编码器规定每次必须喂给它恰好1024个采样点少一个报错多一个装不下。frame-nb_samples数据的现实代表我们当前数据帧里实际装了多少个采样点。 转码的核心艺术就是想方设法让“现实”对齐“规矩”。二、 数据流转的“加工厂”模型在看代码之前请把整个程序想象成一条流水线进货 (fread)从 WAV 文件中切下一块 4096 字节的“生肉”1024样本 × 2声道 × 2字节位深。加工 (swr_convert)把生肉丢进重采样器吐出 1024 个高精度的 FLTP 样本装进AVFrame的盘子里。打标签 (PTS)音频是连续的河流。第一帧贴上0秒的标签第二帧必须贴上1024刻度的标签严丝合缝绝不能重叠或断层。压缩 (avcodec_send_frame)把盘子丢给 AAC 编码器进行极限瘦身。出货 (avcodec_receive_packet)在一个while循环里死死守住出货口把吐出的AVPacket写入到 MP4/AAC 容器中。三、 完整 C 实战代码这份代码避开了初学者最容易踩的几个大坑内存泄漏、时间戳丢失、结尾爆音是一份非常扎实的工业级代码雏形。(注意为了直观展示数据体积换算本例使用 C 语言标准fread读取原始 WAV PCM数据。在实际复杂项目中建议使用avformat_open_input替代。)#define _CRT_SECURE_NO_WARNINGS #include iostream extern C { #include libavformat/avformat.h #include libavcodec/avcodec.h #include libswresample/swresample.h } int main() { const char* infile E:/videos/16.wav; const char* outfile E:/videos/out.aac; // // 1. 初始化编码器 (AAC) // const AVCodec* codec avcodec_find_encoder(AV_CODEC_ID_AAC); AVCodecContext* c avcodec_alloc_context3(codec); c-bit_rate 128000; c-sample_rate 44100; c-sample_fmt AV_SAMPLE_FMT_FLTP; av_channel_layout_default(c-ch_layout, 2); c-time_base { 1, c-sample_rate }; // 极其重要设置时间基准为 1/44100 秒 avcodec_open2(c, codec, NULL); // // 2. 初始化封装器 (Muxer) // AVFormatContext* oc NULL; avformat_alloc_output_context2(oc, NULL, NULL, outfile); AVStream* st avformat_new_stream(oc, NULL); avcodec_parameters_from_context(st-codecpar, c); avio_open(oc-pb, outfile, AVIO_FLAG_WRITE); avformat_write_header(oc, NULL); // // 3. 初始化重采样器 (S16 - FLTP) // AVChannelLayout in_ch_layout; av_channel_layout_default(in_ch_layout, 2); SwrContext* actx NULL; swr_alloc_set_opts2(actx, c-ch_layout, c-sample_fmt, c-sample_rate, in_ch_layout, AV_SAMPLE_FMT_S16, 44100, 0, NULL); swr_init(actx); // // 4. 准备数据载体 (Frame Packet) // AVFrame* frame av_frame_alloc(); frame-format AV_SAMPLE_FMT_FLTP; av_channel_layout_default(frame-ch_layout, 2); frame-nb_samples c-frame_size; // AAC 规定必须是 1024 av_frame_get_buffer(frame, 0); // 严格遵循先定参数再分配内存 AVPacket* pkt av_packet_alloc(); // // 5. 核心流转大循环 // // 计算每次需要读取的裸字节数: 1024样本 * 2字节(16位) * 2声道 4096 字节 int readSize frame-nb_samples * 2 * 2; char *pcm new char[readSize]; FILE* fp fopen(infile, rb); int64_t pts_counter 0; // 时间戳累加器 for (;;) { int bytes_read fread(pcm, 1, readSize, fp); if (bytes_read 0) break; // 计算本次实际读到的样本数 (防止文件末尾越界) int in_samples bytes_read / 4; const uint8_t* data[1]; data[0] (uint8_t*)pcm; // 【加工】重采样 int out_samples swr_convert(actx, frame-data, frame-nb_samples, data, in_samples); if (out_samples 0) break; // 【打标签】设置 PTS 并累加 frame-pts pts_counter; pts_counter out_samples; // 【投喂】送去编码 if (avcodec_send_frame(c, frame) 0) return -1; // 【接货】循环接收编码后的 AAC 包 while (avcodec_receive_packet(c, pkt) 0) { // 转换时间戳并写文件 av_packet_rescale_ts(pkt, c-time_base, st-time_base); pkt-stream_index st-index; av_interleaved_write_frame(oc, pkt); av_packet_unref(pkt); // 务必清空篮子 } } // // 6. 收尾打扫 (极其重要的 Flush 操作) // avcodec_send_frame(c, NULL); // 发送空帧逼编码器吐出存货 while (avcodec_receive_packet(c, pkt) 0) { av_packet_rescale_ts(pkt, c-time_base, st-time_base); pkt-stream_index st-index; av_interleaved_write_frame(oc, pkt); av_packet_unref(pkt); } av_write_trailer(oc); // 写入 MP4/AAC 尾部信息 // // 7. 释放资源 // fclose(fp); delete[] pcm; swr_free(actx); av_frame_free(frame); av_packet_free(pkt); avcodec_free_context(c); if (oc !(oc-oformat-flags AVFMT_NOFILE)) { avio_closep(oc-pb); // 先关 IO } avformat_free_context(oc); // 再关 Context std::cout WAV to AAC 转换完美结束 std::endl; return 0; }