【FFmpeg实战】手撕音频转码:WAV转AAC的全链路解析与C++实现

在音视频开发中,音频转码是一个永远绕不开的经典课题。很多初学者在刚接触 FFmpeg 时,往往会被眼花缭乱的结构体(AVFrameAVPacket)和错综复杂的内存管理搞得晕头转向。

今天,我们将抛开繁杂的解封装外壳,直击底层数据流转的灵魂。本文将带你手写一段 WAV 转 AAC 的 C++ 代码,不仅给你最终的源码,更会为你剖析为什么要这么写:从重采样的数学逻辑,到编码器的"硬性规矩",再到时间戳(PTS)的"铺砖理论"。

一、 核心概念扫盲:转码到底在转什么?

WAV 转 AAC,不仅仅是改个后缀名那么简单。本质上,我们要跨越两道鸿沟:

1. 格式的鸿沟:为什么需要重采样(Resampling)?

WAV 文件里存放的通常是原始的 PCM 数据(一般为 16位整型 S16 )。但是,AAC 编码器是个"挑食"的精密仪器,为了进行复杂的心理声学模型运算,它硬性要求输入的数据必须是浮点平面格式(FLTP) 。 因此,我们必须在中间加一台"重采样器(SwrContext)",把 S16 的粗粮加工成 FLTP 的细粮。

2. 大小的鸿沟:nb_samplesframe_size 的恩怨
  • c->frame_size(编码器的规矩) :AAC 编码器规定,每次必须喂给它恰好 1024 个采样点,少一个报错,多一个装不下。

  • frame->nb_samples(数据的现实):代表我们当前数据帧里实际装了多少个采样点。 转码的核心艺术,就是想方设法让"现实"对齐"规矩"。

二、 数据流转的"加工厂"模型

在看代码之前,请把整个程序想象成一条流水线:

  1. 进货 (fread):从 WAV 文件中切下一块 4096 字节的"生肉"(1024样本 × 2声道 × 2字节位深)。

  2. 加工 (swr_convert) :把生肉丢进重采样器,吐出 1024 个高精度的 FLTP 样本,装进 AVFrame 的盘子里。

  3. 打标签 (PTS) :音频是连续的河流。第一帧贴上 0 秒的标签,第二帧必须贴上 1024 刻度的标签,严丝合缝,绝不能重叠或断层。

  4. 压缩 (avcodec_send_frame):把盘子丢给 AAC 编码器进行极限瘦身。

  5. 出货 (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;
}
相关推荐
lq12332102 小时前
视频AI超分辨率转换器Topaz Video Pro 1.3.1
音视频
EasyDSS13 小时前
智能会议管理系统/私有化视频会议平台EasyDSS私有化部署构建企业级私域视频全场景解决方案
人工智能·音视频
潜创微科技16 小时前
切换器芯片主要型号有哪些:潜创微高清音视频领域主流型号与应用场景全解析
音视频
KevinCyao17 小时前
106视频短信接口怎么发?支持高清影音下发的106视频短信服务商
音视频
潜创微科技--高清音视频芯片方案开发18 小时前
2026年切换器方案服务商市场格局分析与主流品牌选型指南
音视频·硬件工程
潜创微科技--高清音视频芯片方案开发18 小时前
2026年高清音视频切换器方案选型分
音视频
二等饼干~za89866820 小时前
源码可控:云罗 GEO 源头工厂,开源搭建 + 二次开发全链路解决方案
服务器·开发语言·开源·php·音视频·ai-native
feasibility.21 小时前
OpenClaw+LibTV视频生成实测(含安装+配置+分析):ai生成工作流很规范,但画面在“打架“
人工智能·aigc·音视频·内容运营·短剧·openclaw·libtv
深念Y1 天前
FFmpeg 480p 转码失败但 1080p/720p 正常的坑
ffmpeg·音视频·转码·流媒体·分辨率·hls·m3u8