【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;
}
相关推荐
开开心心就好7 小时前
Word双击预览图片插件弥补Word功能缺失
人工智能·python·智能手机·ocr·电脑·word·音视频
Yeauty9 小时前
你那条 ffmpeg 命令,一键翻成 Rust builder 代码
开发语言·rust·ffmpeg
tedcloud12320 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
潜创微科技1 天前
8K HDMI 2.1 多源切换芯片方案,一颗芯片搞定长线不黑屏
音视频
音视频牛哥1 天前
一款优秀的视频直播软件应具备哪些核心功能?结合 SmartMediaKit 谈实时音视频系统的能力边界
音视频·视频直播·低延迟rtsp播放器·gb28181接入·rtsp服务器·低延迟rtmp播放器·鸿蒙next rtsp
AI小码1 天前
把动作「画」给视频世界模型,跨本体双向推演,李飞飞参与
大数据·人工智能·算法·ai·大模型·音视频·编程
嵌入式-老费1 天前
esp32开发与应用(边采样边播放音频)
音视频
中微极客1 天前
Veo视频生成与Gemini Agent平台集成实践
数据库·人工智能·oracle·音视频
码龙-DragonCoding1 天前
一键批量提取音频、提取视频
android·音视频·提取
阿童木写作1 天前
Temu商品图批量翻译工具免费,AI视频字幕翻译+智能抠图
人工智能·python·音视频