1. 为什么需要音频重采样?
最近在做一个实时音频采集项目时,遇到了一个典型问题:我的麦克风采集到的音频采样率是48000Hz,但下游的AAC编码器只支持44100Hz。这就好比你想把美式插头(48000Hz)插到中式插座(44100Hz)里,直接硬塞肯定不行,必须得用个转换器(重采样)。
在实际操作中,我发现直接使用FFmpeg的swr_convert函数进行简单转换会出现两个致命问题:一是播放时有明显的"滋滋"电流声,二是音频速度明显变快。后来查资料才知道,这是因为采样率转换时没有处理好48000和44100这两个数字的关系。就像齿轮传动,48齿的齿轮直接带动44齿的齿轮,转速肯定会出问题。
2. 重采样核心参数设置
2.1 采样点数的黄金比例
经过多次踩坑,我发现关键在于理解48000和44100的比例关系。这两个采样率的最小公倍数是2116800,换算下来就是480:441的比例。也就是说,每480个48000Hz的采样点,应该对应转换为441个44100Hz的采样点。
// 正确设置示例 int src_nb_samples = 480; // 输入采样点数 int dst_nb_samples = 441; // 输出采样点数 int count = swr_convert(swr_ctx, dst_data, dst_nb_samples, (const uint8_t **)src_data, src_nb_samples);这里有个坑要注意:dst_nb_samples应该设置得比理论值稍大一些。因为实际转换时可能会有微小误差,比如有时候输出是440个点,有时候是441个点。我一般会多预留10%的缓冲空间。
2.2 缓冲区管理技巧
直接转换后的数据还不能立即送给编码器,因为AAC编码器要求每次输入必须是1024个采样点。这时候就需要用到FFmpeg的av_audio_fifo缓冲队列:
// 创建FIFO缓冲区 AVAudioFifo *fifo = av_audio_fifo_alloc(AV_SAMPLE_FMT_S16, 2, 1); // 写入重采样后的数据 av_audio_fifo_write(fifo, (void **)dst_data, count); // 当积累够1024个采样点时取出编码 if(av_audio_fifo_size(fifo) >= 1024) { av_audio_fifo_read(fifo, (void **)encode_data, 1024); // 进行编码... }3. 实时处理中的边界情况
3.1 停止录制时的数据冲刷
在实时处理中最容易忽略的就是停止录制时的数据冲刷。这时候三个地方可能还有残留数据:
- 原始采集缓冲区(未重采样)
- 重采样后的缓冲区
- 编码器内部的缓冲区
我的处理流程是这样的:
- 先把原始缓冲区数据全部重采样
- 将重采样数据写入FIFO
- 从FIFO中取出剩余数据编码
- 最后送一帧空数据(NULL)给编码器,强制它输出缓存的最后数据
// 冲刷编码器的技巧 AVPacket *null_pkt = NULL; avcodec_send_frame(codec_ctx, NULL); while(avcodec_receive_packet(codec_ctx, pkt) >= 0) { // 处理最后的编码数据 }3.2 电流声问题排查
遇到电流声时,我花了整整两天时间排查。最终发现是写入文件时直接用了dst_linesize导致的。正确的做法是先计算实际需要的缓冲区大小:
int buf_size = av_samples_get_buffer_size(&dst_linesize, 2, dst_nb_samples, AV_SAMPLE_FMT_S16, 1); fwrite(dst_data[0], 1, buf_size, outfile);这个坑特别隐蔽,因为直接写dst_linesize有时候也能正常工作,但偶尔就会出现电流声。后来看FFmpeg源码才知道,linesize可能包含对齐用的填充数据。
4. 性能优化实践
4.1 内存预分配策略
在实时音频处理中,频繁的内存分配会严重影响性能。我的优化方案是:
- 预先分配足够大的输入/输出缓冲区
- 重复使用AVPacket和AVFrame
- 使用环形缓冲区减少拷贝
// 预分配缓冲区示例 AVFrame *frame = av_frame_alloc(); frame->format = AV_SAMPLE_FMT_S16; frame->channels = 2; frame->channel_layout = AV_CH_LAYOUT_STEREO; frame->nb_samples = 1024; // 按最大需求分配 av_frame_get_buffer(frame, 0);4.2 多线程处理架构
对于高并发的实时场景,我设计了这样的处理流水线:
- 采集线程:专门负责从设备读取数据
- 重采样线程:处理格式转换
- 编码线程:负责最终编码
- 写入线程:管理文件IO
各线程之间通过无锁队列交换数据,实测在i5处理器上可以轻松处理8路音频同时录制。
5. 完整代码结构示例
下面是我的项目核心代码框架,已经过大量实际验证:
// 初始化阶段 swr_ctx = swr_alloc_set_opts(NULL, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 48000, 0, NULL); // 主处理循环 while(running) { // 1. 采集数据 ret = av_read_frame(fmt_ctx, pkt); // 2. 写入原始FIFO av_audio_fifo_write(raw_fifo, (void **)pkt->data, pkt->size/4); // 3. 达到480个采样点时重采样 if(av_audio_fifo_size(raw_fifo) >= 480) { av_audio_fifo_read(raw_fifo, (void **)src_data, 480); int count = swr_convert(swr_ctx, dst_data, 441, (const uint8_t **)src_data, 480); // 4. 写入重采样FIFO av_audio_fifo_write(resampled_fifo, (void **)dst_data, count); } // 5. 达到1024个采样点时编码 if(av_audio_fifo_size(resampled_fifo) >= 1024) { av_audio_fifo_read(resampled_fifo, (void **)frame->data, 1024); encode_frame(frame); } av_packet_unref(pkt); }这个框架在多个直播项目中稳定运行,24小时不间断工作也没有出现内存泄漏或音频不同步的问题。关键点在于各个缓冲区的size管理要精确,特别是要注意swr_convert返回的实际采样点数可能和预期有细微差别。