在音视频开发中,音频转码是一个永远绕不开的经典课题。很多初学者在刚接触 FFmpeg 时,往往会被眼花缭乱的结构体(AVFrame、AVPacket)和错综复杂的内存管理搞得晕头转向。
今天,我们将抛开繁杂的解封装外壳,直击底层数据流转的灵魂。本文将带你手写一段 WAV 转 AAC 的 C++ 代码,不仅给你最终的源码,更会为你剖析为什么要这么写:从重采样的数学逻辑,到编码器的"硬性规矩",再到时间戳(PTS)的"铺砖理论"。
一、 核心概念扫盲:转码到底在转什么?
WAV 转 AAC,不仅仅是改个后缀名那么简单。本质上,我们要跨越两道鸿沟:
1. 格式的鸿沟:为什么需要重采样(Resampling)?
WAV 文件里存放的通常是原始的 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 替代。)
cpp
#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;
}