文章目录
-
-
- 核心功能分析
- [示例 main() 函数流程](#示例 main() 函数流程)
- 代码分析
- 实现关键逻辑
- [问题1: 为什么在文件读完后要调用 addFrame(index, NULL, 0)?不调用会有什么后果?](#问题1: 为什么在文件读完后要调用 addFrame(index, NULL, 0)?不调用会有什么后果?)
- [问题2: 两个输入 PCM 的帧大小不同(8192 vs 4096 字节),为何还能正确混音?会不会导致声道错位或失步?](#问题2: 两个输入 PCM 的帧大小不同(8192 vs 4096 字节),为何还能正确混音?会不会导致声道错位或失步?)
- [问题3: 输出采样率是 96kHz,但输入是 48kHz,重采样是如何发生的?是否需要显式添加 aresample 滤镜?](#问题3: 输出采样率是 96kHz,但输入是 48kHz,重采样是如何发生的?是否需要显式添加 aresample 滤镜?)
- code
-
本文实现了一个 基于 FFmpeg 的音频混音器(AudioMixer),支持多个不同格式的 PCM 音频输入流
核心功能分析
支持多路异构音频输入
每个输入可以有不同的采样率(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
- 最后清理资源
代码分析
类定义
cpp
class AudioMixer
{
public:
AudioMixer();
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);
内部结构体
cpp
private:
struct AudioInfo
{
AudioInfo()
{
filterCtx = nullptr;
}
uint32_t samplerate;
uint32_t channels;
uint32_t bitsPerSample;
AVSampleFormat format;
std::string name;
AVFilterContext *filterCtx;
};
成员变量
cpp
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;
};
实现关键逻辑
构造函数
cpp
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";
}
输入流注册
cpp
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;
}
混音器初始化
cpp
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;
AMIX 滤镜配置
cpp
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;
输出缓冲区配置
cpp
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 重采样。
验证方法(调试时):
c
// 在 init() 最后打印滤镜图
av_log_set_level(AV_LOG_DEBUG);
avfilter_graph_dump(filter_graph_, NULL); // 输出到 stderr
总结:
空包冲刷 : 必须发送 NULL 帧通知流结束,否则混音器卡住
帧大小差异 : 关键是样本数一致,FFmpeg 按样本对齐,非字节
重采样机制: aformat 触发自动插入 aresample,无需手动添加
code
audiomixer.cpp
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; // 已经存在则返回-1
}
// 初始化一个input 可以有多个输入
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(const uint32_t samplerate, const uint32_t channels,
const uint32_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;
}
/*
inputs
The number of inputs. If unspecified, it defaults to 2.//输入的数量,如果没有指明,默认为2.
duration
How to determine the end-of-stream.//决定了流的结束
longest
The duration of the longest input. (default)//最长输入的持续时间
shortest
The duration of the shortest input.//最短输入的持续时间
first
The duration of the first input.//第一个输入的持续时间 比如直播,mic为主的时候 first,
dropout_transition
The transition time, in seconds, for volume renormalization when an input stream ends.
The default value is 2 seconds.
*/
/**
* @brief 初始化
* @param duration longest最长输入时间,shortest最短,first第一个输入持续的时间
* @return
*/
int AudioMixer::init(const char *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(); // 创建avfilter_graph
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");
/*inputs=输入流数量, duration=决定流的结束,
* dropout_transition= 输入流结束时,容量重整时间,
* (longest最长输入时间,shortest最短,first第一个输入持续的时间))*/
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;
}
// iter.first 是input index
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)
{
// printf("ret = %d, %d\n", ret, AVERROR(EAGAIN));
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
cpp
#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(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;
};
#endif // AUDIOMIXER_H
main.c
cpp
#include "audiomixer.h"
//ffmpeg -i buweishui_1m.mp3 -i huiguniang.mp3 -filter_complex amix=inputs=2:duration=longest:dropout_transition=3 out.mp3 -y
#define PCM1_FRAME_SIZE (4096*2) // 需要和格式匹配 4*1024*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); // 48000_2_f32le.pcm
amix.addAudioInput(1, 48000, 2, 16, AV_SAMPLE_FMT_S16); // 48000_2_s16le.pcm
// 输出流
amix.addAudioOutput(96000, 2, 16, AV_SAMPLE_FMT_S16);
// init之前,要先添加输入源和输出源
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; //amix.getFrame(out_buf, 10240);
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;
}