今天我们的实战内容是将音频解码成PCM,并将PCM重采样成特定的采样率,然后输出到本地文件进行播放。
所谓重采样,一句话总结就是改变音频的三元素,也就是通过重采样改变音频的采样率、采样格式或者声道数。
例如音频A是采样率48000hz、采样格式为f32le、声道数为1,通过重采样可以将音频A的采样率变更为采样率44100hz、采样格式为s16le、声道数为2等。
一般进行重采样有两个原因,一是播放设备需要,二是音频合并、或编码器等需要。
例如有些声音设备只能播放44100hz的采样率、16位采样格式的音频数据,因此如果音频不是这些格式的,就需要进行重采样才能正常播放了。
例如FFmpeg默认的AAC编码器输入的PCM格式为:AV_SAMPLE_FMT_FLTP
,如果需要使用FFMpeg默认的AAC编码器则需要进行重采样了。又比有些需要进行混音的业务需求,需要保证PCM三要素相同才能进行正常混音。
在重采样的过程中我们要坚守一个原则就是音频经过重采样后它的播放时间是不变的,如果一个10s的音频经过重采样后变成了15,那肯定就是不行的。
影响音频播放时长的因素是每帧的采样数和采样率,下面举一个例子简单介绍下音频播放时长的问题:
假如现有mp3,它的采样率是采样率48000,mp3每帧采样点数是1152,那么每帧mp3的播放时长就是 1152/48000,每一个采样点的播放时长就是1/48000。
假如现有mp3,它的采样率是采样率44100,aac每帧采样点数是1024,那么每帧aac的播放时长就是 1024/44100,每个采样点的播放时长就是1/44100。
从上面的例子中我们可以看出,对于采样率不同的两个音频,不可能1帧mp3转换出1帧aac,它们的比例不是1:1的,对于上面的例子,那么1帧mp3能重采样出多少个aac的采样点呢?
以时间不变为基础,可以有这样的一个公式:
1152 / 48000 = 目标采样点数 / 44100
也就是说:目标采样点数 = 1152 * 44100 / 48000
这条公式可以用FFmpeg中的函数av_rescale_rnd
来实现…
有了计算公式,下面我们说说FFmpeg重采样的步骤:
1、分配SwrContext并配置音频输出输出参数
这里可以直接使用函数swr_alloc_set_opts
实现,也可以使用swr_alloc、av_opt_set_channel_layout、av_opt_set_int、av_opt_set_sample_fmt
等组合函数分步实现,
2、初始化SwrContext
分配好SwrContext 后,通过函数swr_init
进行重采样上下文初始化。
3、swr_convert重采样
FFmpeg真正进行重采样的函数是swr_convert
。它的返回值就是重采样输出的点数。使用FFmpeg进行重采样时内部是有缓存的,而内部缓存了多少个采样点,可以用函数swr_get_delay
获取。
也就是说调用函数swr_convert
时你传递进去的第三个参数表示你希望输出的采样点数,但是函数swr_convert
的返回值才是真正输出的采样点数,这个返回值一定是小于或等于你希望输出的采样点数。
下面是完整代码:
#ifndef AUDIO_TARGET_SAMPLE
#define AUDIO_TARGET_SAMPLE 48000
#endif
#include
extern "C" {
#include "libavformat/avformat.h"
#include
#include
#include
#include
#include
}
class AudioResample {
public:
// 将PCM数据重采样
void decode_audio_resample(const char *media_path, const char *pcm_path) {
avFormatContext = avformat_alloc_context();
int ret = avformat_open_input(&avFormatContext, media_path, nullptr, nullptr);
if (ret < 0) {
std::cout << "输入打开失败" << std::endl;
return;
}
// 寻找视频流
int audio_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
if (audio_index < 0) {
std::cout << "没有可用的音频流" << std::endl;
return;
}
// 配置解码相关
const AVCodec *avCodec = avcodec_find_decoder(avFormatContext->streams[audio_index]->codecpar->codec_id);
avCodecContext = avcodec_alloc_context3(avCodec);
avcodec_parameters_to_context(avCodecContext, avFormatContext->streams[audio_index]->codecpar);
ret = avcodec_open2(avCodecContext, avCodec, nullptr);
if (ret < 0) {
std::cout << "解码器打开失败" << std::endl;
return;
}
// 分配包和帧数据结构
avPacket = av_packet_alloc();
avFrame = av_frame_alloc();
// 打开yuv输出文件
pcm_out = fopen(pcm_path, "wb");
// 读取数据解码
while (true) {
ret = av_read_frame(avFormatContext, avPacket);
if (ret < 0) {
std::cout << "音频包读取完毕" << std::endl;
break;
} else {
if (avPacket->stream_index == audio_index) {
// 只处理音频包
ret = avcodec_send_packet(avCodecContext, avPacket);
if (ret < 0) {
std::cout << "发送解码包失败" << std::endl;
return;
}
while (true) {
ret = avcodec_receive_frame(avCodecContext, avFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
std::cout << "获取解码数据失败" << std::endl;
return;
} else {
std::cout << "重采样解码数据" << std::endl;
resample();
}
}
}
}
av_packet_unref(avPacket);
}
}
~AudioResample() {
// todo 释放资源
}
private:
AVFormatContext *avFormatContext = nullptr;
AVCodecContext *avCodecContext = nullptr;
AVPacket *avPacket = nullptr;
AVFrame *avFrame = nullptr;
FILE *pcm_out = nullptr;
SwrContext *swrContext = nullptr;
AVFrame *out_frame = nullptr;
int64_t max_dst_nb_samples;
/**
* 重采样并输出到文件
*/
void resample() {
if (nullptr == swrContext) {
/**
* 以下可以使用 swr_alloc、av_opt_set_channel_layout、av_opt_set_int、av_opt_set_sample_fmt
* 等API设置,更加灵活
*/
swrContext = swr_alloc_set_opts(nullptr, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_FLTP, AUDIO_TARGET_SAMPLE,
avFrame->channel_layout, static_cast(avFrame->format),
avFrame->sample_rate, 0, nullptr);
swr_init(swrContext);
}
// 进行音频重采样
int src_nb_sample = avFrame->nb_samples;
// 为了保持从采样后 dst_nb_samples / dest_sample = src_nb_sample / src_sample_rate
max_dst_nb_samples = av_rescale_rnd(src_nb_sample, AUDIO_TARGET_SAMPLE, avFrame->sample_rate, AV_ROUND_UP);
// 从采样器中会缓存一部分,获取缓存的长度
int64_t delay = swr_get_delay(swrContext, avFrame->sample_rate);
int64_t dst_nb_samples = av_rescale_rnd(delay + avFrame->nb_samples, AUDIO_TARGET_SAMPLE, avFrame->sample_rate,
AV_ROUND_UP);
if(nullptr == out_frame){
init_out_frame(dst_nb_samples);
}
if (dst_nb_samples > max_dst_nb_samples) {
// 需要重新分配buffer
std::cout << "需要重新分配buffer" << std::endl;
init_out_frame(dst_nb_samples);
max_dst_nb_samples = dst_nb_samples;
}
// 重采样
int ret = swr_convert(swrContext, out_frame->data, dst_nb_samples,
const_cast(avFrame->data), avFrame->nb_samples);
if(ret < 0){
std::cout << "重采样失败" << std::endl;
} else{
// 每帧音频数据量的大小
int data_size = av_get_bytes_per_sample(static_cast(out_frame->format));
std::cout << "重采样成功:" << ret << "----dst_nb_samples:" << dst_nb_samples << "---data_size:" << data_size << std::endl;
// 交错模式保持写入
// 注意不要用 i < out_frame->nb_samples, 因为重采样出来的点数不一定就是out_frame->nb_samples
for (int i = 0; i < ret; i++) {
for (int ch = 0; ch < out_frame->channels; ch++) {
// 需要储存为pack模式
fwrite(out_frame->data[ch] + data_size * i, 1, data_size, pcm_out);
}
}
}
}
void init_out_frame(int64_t dst_nb_samples){
av_frame_free(&out_frame);
out_frame = av_frame_alloc();
out_frame->sample_rate = AUDIO_TARGET_SAMPLE;
out_frame->format = AV_SAMPLE_FMT_FLTP;
out_frame->channel_layout = AV_CH_LAYOUT_STEREO;
out_frame->nb_samples = dst_nb_samples;
// 分配buffer
av_frame_get_buffer(out_frame,0);
av_frame_make_writable(out_frame);
}
};
使用ffplay播放以下重采样后的PCM文件是否正常,播放命令是:
// -ar 表示采样率
// -ac 表示音频通道数
// -f 表示 pcm 格式,sample_fmts + le(小端)或者 be(大端) f32le表示的是 AV_SAMPLE_FMT_FLTP 的小端模式
// sample_fmts可以通过ffplay -sample_fmts来查询
// -i 表示输入文件,这里就是 pcm 文件
ffplay -ar 44100 -ac 2 -f f32le -i pcm文件路径
FFmpeg连载1-开发环境搭建
FFmpeg连载2-分离视频和音频
FFmpeg连载3-视频解码
FFmpeg连载4-音频解码
FFmpeg连载5-音视频编码