跳到主要内容FFmpeg C++ 音频混音器实现 | 极客日志C++算法
FFmpeg C++ 音频混音器实现
介绍基于 FFmpeg 的 C++ 音频混音器实现。支持多路异构 PCM 输入,通过 filter graph 构建 amix 混音链路,配合 aformat 统一输出格式。重点讲解了空包冲刷机制(EOF 处理)、帧大小与样本数对齐原理,以及 aformat 触发自动重采样的机制。代码包含完整的类定义、滤镜图配置及主函数流程,解决了多格式音频同步混音的关键问题。
ArchDesign1 浏览 核心功能分析
支持多路异构音频输入
每个输入可以有不同的采样率(samplerate)、声道数(channels)、位深(bitsPerSample)、采样格式(AVSampleFormat,如 S16、FLT 等)。示例中添加了两个输入:输入 0 为 48kHz, stereo, 32-bit float (AV_SAMPLE_FMT_FLT);输入 1 为 48kHz, stereo, 16-bit int (AV_SAMPLE_FMT_S16)。
注意:虽然输入参数允许异构,但 FFmpeg 的 amix 滤镜要求所有输入具有相同的采样率和声道布局。若输入不一致,avfilter_graph_config() 会失败。
代码未做重采样或格式转换,实际运行时若输入格式不兼容(如采样率不同),会初始化失败。
统一输出格式
输出可指定目标格式(示例中为 96kHz, stereo, 16-bit int)。使用 aformat 滤镜强制将混音后的结果转为目标格式。这是合理的,因为 amix 输出的格式通常与输入之一相同,不一定符合用户期望。
使用 FFmpeg filter graph 构建混音流水线
构建的滤镜图如下:
[abuffer0] \
[abuffer1] --> [amix] --> [aformat] --> [abuffersink]
/
[abufferN]
每个输入对应一个 abuffer(音频源缓冲区),所有 abuffer 连接到 amix(混音器),amix 输出连接到 aformat(格式转换),最终输出到 abuffersink(供程序读取)。
混音策略可控
通过 init('longest') 等参数控制混音时长策略:
- longest:以最长输入为准(默认)
- shortest:以最短输入结束
- first:以第一个输入结束
- dropout_transition=0:输入流结束后立即停止贡献音量(无淡出)
帧级输入/输出接口
- addFrame(index, buf, size):向指定输入通道送入原始 PCM 数据。若 buf == nullptr,表示该流结束(发送 EOF)。
- getFrame(outBuf, maxOutBufSize):从混音器拉取一帧混合后的 PCM 数据,内部调用 av_buffersink_get_frame(),返回实际写入字节数,或 -1 表示无数据/错误。
注意:getFrame 可能返回 0 字节(当缓冲区不足一帧时),但当前代码未处理这种情况,仅在 ret < 0 时退出循环。
资源管理与线程安全
- 使用 std::mutex 保证多线程安全(虽然示例是单线程)
- 使用 std::shared_ptr 和 RAII 管理 AudioInfo
- 析构函数自动调用 exit() 释放 FFmpeg 资源
- 正确调用 avfilter_free 和 avfilter_graph_free
示例 main() 函数流程
- 创建 AudioMixer
- 添加两个不同格式的输入流(float32 & int16)
- 设置输出为 96kHz / S16
- 初始化混音器(策略:longest)
- 循环读取两个 PCM 文件,分别送入混音器
- 当某文件读完,发送 NULL 帧表示结束
- 不断调用 getFrame 获取混音结果并写入 output.pcm
- 最后清理资源
代码分析
类定义
class AudioMixer {
public:
AudioMixer();
~();
;
;
;
;
;
;
};
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
virtual
AudioMixer
int addAudioInput(uint32_t index, uint32_t samplerate, uint32_t channels, uint32_t bitsPerSample, AVSampleFormat format)
int addAudioOutput(const uint32_t samplerate, const uint32_t channels, const uint32_t bitsPerSample, const AVSampleFormat format)
int init(const char* duration = "longest")
int exit()
int addFrame(uint32_t index, uint8_t* inBuf, uint32_t size)
int getFrame(uint8_t* outBuf, uint32_t maxOutBufSize)
private:
struct AudioInfo {
AudioInfo() { filterCtx = nullptr; }
uint32_t samplerate;
uint32_t channels;
uint32_t bitsPerSample;
AVSampleFormat format;
std::string name;
AVFilterContext *filterCtx;
};
bool initialized_ = false;
std::mutex mutex_;
std::map<uint32_t, AudioInfo> audio_input_info_;
std::shared_ptr<AudioInfo> audio_output_info_;
std::shared_ptr<AudioInfo> audio_mix_info_;
std::shared_ptr<AudioInfo> audio_sink_info_;
AVFilterGraph *filter_graph_ = nullptr;
实现关键逻辑
AudioMixer::AudioMixer():initialized_(false),filter_graph_(nullptr),audio_output_info_(nullptr){
audio_mix_info_.reset(new AudioInfo);
audio_mix_info_->name = "amix";
audio_sink_info_.reset(new AudioInfo);
audio_sink_info_->name = "sink";
}
int AudioMixer::addAudioInput(uint32_t index,...){
std::lock_guard<std::mutex> locker(mutex_);
if(initialized_) return -1;
if(audio_input_info_.find(index) != audio_input_info_.end()) return -1;
auto& filterInfo = audio_input_info_[index];
filterInfo.samplerate = samplerate;
filterInfo.channels = channels;
filterInfo.bitsPerSample = bitsPerSample;
filterInfo.format = format;
filterInfo.name = "input" + to_string(index);
return 0;
}
int AudioMixer::init(const char* duration){
std::lock_guard<std::mutex> locker(mutex_);
if(initialized_ || audio_input_info_.size()==0) return -1;
filter_graph_ = avfilter_graph_alloc();
if(!filter_graph_) return -1;
}
char args[512]={0};
const AVFilter *amix = avfilter_get_by_name("amix");
audio_mix_info_->filterCtx = avfilter_graph_alloc_filter(filter_graph_, amix,"amix");
snprintf(args,sizeof(args),"inputs=%d:duration=%s:dropout_transition=0", audio_input_info_.size(), duration);
if(avfilter_init_str(...)!=0) return -1;
const AVFilter *abuffersink = avfilter_get_by_name("abuffersink");
audio_sink_info_->filterCtx = avfilter_graph_alloc_filter(...,"sink");
if(avfilter_init_str(...,nullptr)!=0) return -1;
问题 1:为什么在文件读完后要调用 addFrame(index, NULL, 0)?不调用会有什么后果?
这是'空包冲刷'(Flush with NULL frame)机制,用于通知 FFmpeg 滤镜图某一路输入流已结束。
av_buffersrc_add_frame(ctx, NULL) 向 abuffer 滤镜发送一个 EOF(End-of-Stream)信号。
这会触发下游滤镜(如 amix)进入'尾部处理'状态。
若不调用的后果:
amix 会一直等待该路新数据(即使文件已读完),永不认为流结束。
即使另一路还在送数据,amix 可能因内部缓冲策略延迟输出。
最严重时:程序卡死在 getFrame 循环中,无法输出剩余混音数据,导致输出文件截断或无声结尾。
类比:就像网络通信中必须发送 FIN 包来关闭连接,否则对方会一直等待。
因此,每个输入流结束时必须且仅需发送一次 NULL 帧,你的代码中通过 file1_finish 标志正确实现了这一点。
问题 2:两个输入 PCM 的帧大小不同(8192 vs 4096 字节),为何还能正确混音?会不会导致声道错位或失步?
不会错位或失步,因为两者的音频样本数量是相同的,只是存储格式不同。
计算样本数:
输入 0(FLT, 32-bit):8192 字节 ÷ (4 字节/样本 × 2 声道) = 1024 样本
输入 1(S16, 16-bit):4096 字节 ÷ (2 字节/样本 × 2 声道) = 1024 样本
FFmpeg 内部处理:
abuffer 滤镜根据你注册的 format / samplerate / channels 正确解析帧内容。
amix 混音时以样本(sample)为单位对齐,而非字节。
因此,每调用一次 addFrame,两路都贡献了 1024 个样本,时间轴完全对齐。
风险点:如果误设 PCM1_FRAME_SIZE = 4096(与 S16 相同),则 FLT 流每次只送 512 样本,导致快进式混音(音调变高、节奏加快)。
结论:只要 FRAME_SIZE 与 bitsPerSample × channels 匹配,就能保证样本对齐。
问题 3:输出采样率是 96kHz,但输入是 48kHz,重采样是如何发生的?是否需要显式添加 aresample 滤镜?
重采样是自动发生的,无需显式添加 aresample 滤镜,这是 FFmpeg 滤镜图的智能连接机制。
机制说明:
amix 输出格式继承自输入(48kHz, stereo)。
aformat=sample_rates=96000 要求下游必须是 96kHz。
当 avfilter_graph_config() 被调用时,FFmpeg 自动在 amix 和 aformat 之间插入 aresample 滤镜,完成 48k → 96k 重采样。
av_log_set_level(AV_LOG_DEBUG);
avfilter_graph_dump(filter_graph_,NULL);
- 空包冲刷:必须发送 NULL 帧通知流结束,否则混音器卡住
- 帧大小差异:关键是样本数一致,FFmpeg 按样本对齐,非字节
- 重采样机制:aformat 触发自动插入 aresample,无需手动添加
代码实现
audiomixer.cpp
#include"audiomixer.h"
AudioMixer::AudioMixer():initialized_(false),filter_graph_(nullptr),audio_output_info_(nullptr){
audio_mix_info_.reset(new AudioInfo);
audio_mix_info_->name = "amix";
audio_sink_info_.reset(new AudioInfo);
audio_sink_info_->name = "sink";
}
AudioMixer::~AudioMixer(){
if(initialized_){exit();}
}
int AudioMixer::addAudioInput(uint32_t index,uint32_t samplerate,uint32_t channels,uint32_t bitsPerSample, AVSampleFormat format){
std::lock_guard<std::mutex>locker(mutex_);
if(initialized_){return -1;}
if(audio_input_info_.find(index)!= audio_input_info_.end()){return -1;
auto& filterInfo = audio_input_info_[index];
filterInfo.samplerate = samplerate;
filterInfo.channels = channels;
filterInfo.bitsPerSample = bitsPerSample;
filterInfo.format = format;
filterInfo.name = std::string("input")+ std::to_string(index);
return 0;
}
int AudioMixer::addAudioOutput(constuint32_t samplerate,constuint32_t channels,constuint32_t bitsPerSample,const AVSampleFormat format){
std::lock_guard<std::mutex>locker(mutex_);
if(initialized_){return -1;}
audio_output_info_.reset(new AudioInfo);
audio_output_info_->samplerate = samplerate;
audio_output_info_->channels = channels;
audio_output_info_->bitsPerSample = bitsPerSample;
audio_output_info_->format = format;
audio_output_info_->name = "output";
return 0;
}
int AudioMixer::init(constchar*duration){
std::lock_guard<std::mutex>locker(mutex_);
if(initialized_){return -1;}
if(audio_input_info_.size()==0){return -1;}
filter_graph_ =avfilter_graph_alloc();
if(filter_graph_ ==nullptr){return -1;}
char args[512]={0};
const AVFilter *amix =avfilter_get_by_name("amix");
audio_mix_info_->filterCtx =avfilter_graph_alloc_filter(filter_graph_, amix,"amix");
snprintf(args,sizeof(args),"inputs=%d:duration=%s:dropout_transition=0", audio_input_info_.size(), duration);
if(avfilter_init_str(audio_mix_info_->filterCtx, args)!=0){
printf("[AudioMixer] avfilter_init_str(amix) failed.\n");
return -1;
}
const AVFilter *abuffersink =avfilter_get_by_name("abuffersink");
audio_sink_info_->filterCtx =avfilter_graph_alloc_filter(filter_graph_, abuffersink,"sink");
if(avfilter_init_str(audio_sink_info_->filterCtx,nullptr)!=0){
printf("[AudioMixer] avfilter_init_str(abuffersink) failed.\n");
return -1;
}
for(auto& iter : audio_input_info_){
const AVFilter *abuffer =avfilter_get_by_name("abuffer");
snprintf(args,sizeof(args),"sample_rate=%d:sample_fmt=%s:channel_layout=0x%I64x", iter.second.samplerate,av_get_sample_fmt_name(iter.second.format),av_get_default_channel_layout(iter.second.channels));
printf("[AudioMixer] input(%d) args: %s\n", iter.first, args);
iter.second.filterCtx =avfilter_graph_alloc_filter(filter_graph_, abuffer, audio_output_info_->name.c_str());
if(avfilter_init_str(iter.second.filterCtx, args)!=0){
printf("[AudioMixer] avfilter_init_str(abuffer) failed.\n");
return -1;
}
if(avfilter_link(iter.second.filterCtx,0, audio_mix_info_->filterCtx, iter.first)!=0){
printf("[AudioMixer] avfilter_link(abuffer(%d), amix) failed.", iter.first);
return -1;
}
}
if(audio_output_info_ !=nullptr){
const AVFilter *aformat =avfilter_get_by_name("aformat");
snprintf(args,sizeof(args),"sample_rates=%d:sample_fmts=%s:channel_layouts=0x%I64x", audio_output_info_->samplerate,av_get_sample_fmt_name(audio_output_info_->format),av_get_default_channel_layout(audio_output_info_->channels));
printf("[AudioMixer] output args: %s\n", args);
audio_output_info_->filterCtx =avfilter_graph_alloc_filter(filter_graph_, aformat,"aformat");
if(avfilter_init_str(audio_output_info_->filterCtx, args)!=0){
printf("[AudioMixer] avfilter_init_str(aformat) failed. %s\n", args);
return -1;
}
if(avfilter_link(audio_mix_info_->filterCtx,0, audio_output_info_->filterCtx,0)!=0){
printf("[AudioMixer] avfilter_link(amix, aformat) failed.\n");
return -1;
}
if(avfilter_link(audio_output_info_->filterCtx,0, audio_sink_info_->filterCtx,0)!=0){
printf("[AudioMixer] avfilter_link(aformat, abuffersink) failed.\n");
return -1;
}
}
if(avfilter_graph_config(filter_graph_,NULL)<0){
printf("[AudioMixer] avfilter_graph_config() failed.\n");
return -1;
}
initialized_ =true;
return 0;
}
int AudioMixer::exit(){
std::lock_guard<std::mutex>locker(mutex_);
if(initialized_){
for(auto iter : audio_input_info_){
if(iter.second.filterCtx !=nullptr){
avfilter_free(iter.second.filterCtx);
}
}
audio_input_info_.clear();
if(audio_output_info_ && audio_output_info_->filterCtx){
avfilter_free(audio_output_info_->filterCtx);
audio_output_info_->filterCtx =nullptr;
}
if(audio_mix_info_->filterCtx){
avfilter_free(audio_mix_info_->filterCtx);
audio_mix_info_->filterCtx =nullptr;
}
if(audio_sink_info_->filterCtx){
avfilter_free(audio_sink_info_->filterCtx);
audio_sink_info_->filterCtx =nullptr;
}
avfilter_graph_free(&filter_graph_);
filter_graph_ =nullptr;
initialized_ =false;
}
return 0;
}
int AudioMixer::addFrame(uint32_t index,uint8_t*inBuf,uint32_t size){
std::lock_guard<std::mutex>locker(mutex_);
if(!initialized_){return -1;}
auto iter = audio_input_info_.find(index);
if(iter == audio_input_info_.end()){return -1;}
if(inBuf && size >0){
std::shared_ptr<AVFrame>avFrame(av_frame_alloc(),[](AVFrame *ptr){av_frame_free(&ptr);});
avFrame->sample_rate = iter->second.samplerate;
avFrame->format = iter->second.format;
avFrame->channel_layout =av_get_default_channel_layout(iter->second.channels);
avFrame->nb_samples = size *8/ iter->second.bitsPerSample / iter->second.channels;
av_frame_get_buffer(avFrame.get(),1);
memcpy(avFrame->extended_data[0], inBuf, size);
if(av_buffersrc_add_frame(iter->second.filterCtx, avFrame.get())!=0){return -1;}
}else{
if(av_buffersrc_add_frame(iter->second.filterCtx,NULL)!=0){return -1;}
}
return 0;
}
int AudioMixer::getFrame(uint8_t*outBuf,uint32_t maxOutBufSize){
std::lock_guard<std::mutex>locker(mutex_);
if(!initialized_){return -1;}
std::shared_ptr<AVFrame>avFrame(av_frame_alloc(),[](AVFrame *ptr){av_frame_free(&ptr);});
int ret =av_buffersink_get_frame(audio_sink_info_->filterCtx, avFrame.get());
if(ret <0){
return -1;
}
int size =av_samples_get_buffer_size(NULL, avFrame->channels, avFrame->nb_samples,(AVSampleFormat)avFrame->format,1);
if(size >(int)maxOutBufSize){return 0;}
memcpy(outBuf, avFrame->extended_data[0], size);
return size;
}
audiomixer.h
#ifndef AUDIOMIXER_H
#define AUDIOMIXER_H
#include<map>
#include<mutex>
#include<cstdio>
#include<cstdint>
#include<string>
#include<memory>
extern"C"{
#include<libavcodec/avcodec.h>
#include<libavformat/avformat.h>
#include<libavfilter/buffersink.h>
#include<libavfilter/buffersrc.h>
#include<libavutil/opt.h>
}
class AudioMixer{
public:
AudioMixer();
virtual~AudioMixer();
int addAudioInput(uint32_t index,uint32_t samplerate,uint32_t channels,uint32_t bitsPerSample, AVSampleFormat format);
int addAudioOutput(constuint32_t samplerate,constuint32_t channels,constuint32_t bitsPerSample,const AVSampleFormat format);
int init(constchar*duration ="longest");
int exit();
int addFrame(uint32_t index,uint8_t*inBuf,uint32_t size);
int getFrame(uint8_t*outBuf,uint32_t maxOutBufSize);
private:
struct AudioInfo{
AudioInfo(){ filterCtx =nullptr;}
uint32_t samplerate;
uint32_t channels;
uint32_t bitsPerSample;
AVSampleFormat format;
std::string name;
AVFilterContext *filterCtx;
};
bool initialized_ =false;
std::mutex mutex_;
std::map<uint32_t, AudioInfo> audio_input_info_;
std::shared_ptr<AudioInfo> audio_output_info_;
std::shared_ptr<AudioInfo> audio_mix_info_;
std::shared_ptr<AudioInfo> audio_sink_info_;
AVFilterGraph *filter_graph_ =nullptr;
};
#endif
main.cpp
#include"audiomixer.h"
#define PCM1_FRAME_SIZE (4096*2)
#define PCM2_FRAME_SIZE (4096)
#define PCM_OUT_FRAME_SIZE (40000)
int main(int argc,char**argv){
AudioMixer amix;
amix.addAudioInput(0,48000,2,32, AV_SAMPLE_FMT_FLT);
amix.addAudioInput(1,48000,2,16, AV_SAMPLE_FMT_S16);
amix.addAudioOutput(96000,2,16, AV_SAMPLE_FMT_S16);
if(amix.init("longest")<0){return -1;}
int len1 =0, len2 =0;
uint8_t buf1[PCM1_FRAME_SIZE];
uint8_t buf2[PCM2_FRAME_SIZE];
FILE *file1 =fopen("48000_2_f32le.pcm","rb");
if(!file1){printf("fopen 48000_2_f32le.pcm failed\n");return -1;}
FILE *file2 =fopen("48000_2_s16le.pcm","rb");
if(!file2){printf("fopen 48000_2_s16le.pcm failed\n");return -1;}
FILE* file_out =fopen("output.pcm","wb");
if(!file_out){printf("fopen output.pcm failed\n");return -1;}
uint8_t out_buf[PCM_OUT_FRAME_SIZE];
uint32_t out_size =0;
int file1_finish =0;
int file2_finish =0;
while(1){
len1 =fread(buf1,1, PCM1_FRAME_SIZE, file1);
len2 =fread(buf2,1, PCM2_FRAME_SIZE, file2);
if(len1 >0|| len2 >0){
if(len1 >0){
if(amix.addFrame(0, buf1, len1)<0){
printf("amix.addFrame(0, buf1, len1) failed\n");
break;
}
}else{
if(file1_finish ==0){
file1_finish =1;
if(amix.addFrame(0,NULL,0)<0){
printf("amix.addFrame(0, buf1, len1) failed\n");
}
}
}
if(len2 >0){
if(amix.addFrame(1, buf2, len2)<0){
printf("amix.addFrame(1, buf2, len2) failed\n");
break;
}
}else{
if(file2_finish ==0){
file2_finish =1;
if(amix.addFrame(1,NULL,0)<0){
printf("amix.addFrame(1, buf2, len2) failed\n");
}
}
}
int ret =0;
while((ret = amix.getFrame(out_buf,10240))>=0){
out_size += ret;
if(out_size %(1024*1024)==0)printf("mix audio: %d, out_size:%u\n", ret, out_size);
fwrite(out_buf,1, ret, file_out);
}
}else{
printf("two file finish\n");
break;
}
}
printf("end, out_size:%u\n", out_size);
amix.exit();
if(file_out)fclose(file_out);
if(file1)fclose(file1);
if(file2)fclose(file2);
getchar();
return 0;
}