开发环境
- FFmpeg 3.4
- window
- Visual Studio 2022
使用的开发环境可以在前面的文章中找的哦,依然是在前篇博客里面工程里面增加代码
开发过程
1. 创建SwrResample.h SwrResample.cpp 文件
SwrResample.h 的定义如下
c++
#pragma once
#include <iostream>
extern "C" {
#include <libavutil/opt.h>
#include <libavutil/channel_layout.h>
#include <libavutil/samplefmt.h>
#include <libswresample/swresample.h>
}
#define WRITE_RESAMPLE_PCM_FILE
class SwrResample
{
public:
int Init(int64_t src_ch_layout, int64_t dst_ch_layout,
int src_rate, int dst_rate,
enum AVSampleFormat src_sample_fmt, enum AVSampleFormat dst_sample_fmt,
int src_nb_samples);
int WriteInput(AVFrame* frame);
int SwrConvert();
void Close();
private:
struct SwrContext* swr_ctx;
uint8_t** src_data_;
uint8_t** dst_data_;
int src_nb_channels, dst_nb_channels;
int src_linesize, dst_linesize;
int src_nb_samples_, dst_nb_samples_;
enum AVSampleFormat dst_sample_fmt_;
enum AVSampleFormat src_sample_fmt_;
#ifdef WRITE_RESAMPLE_PCM_FILE
FILE* outdecodedswffile;
#endif
};
WRITE_RESAMPLE_PCM_FILE定义了一个宏,开启,就将重采样后的的pcm写文件
2. SwrResample的Init方法初始化
c++
int SwrResample::Init(int64_t src_ch_layout, int64_t dst_ch_layout,
int src_rate, int dst_rate,
enum AVSampleFormat src_sample_fmt, enum AVSampleFormat dst_sample_fmt,
int src_nb_samples)
{
#ifdef WRITE_RESAMPLE_PCM_FILE
outdecodedswffile = fopen("decode_resample.pcm", "wb");
if (!outdecodedswffile) {
std::cout << "open out put swr file failed";
}
#endif // WRITE_RESAMPLE_PCM_FILE
src_sample_fmt_ = src_sample_fmt;
dst_sample_fmt_ = dst_sample_fmt;
int ret;
/* create resampler context */
swr_ctx = swr_alloc();
if (!swr_ctx) {
std::cout << "Could not allocate resampler context" << std::endl;
ret = AVERROR(ENOMEM);
return ret;
}
/* set options */
av_opt_set_int(swr_ctx, "in_channel_layout", src_ch_layout, 0);
av_opt_set_int(swr_ctx, "in_sample_rate", src_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", src_sample_fmt, 0);
av_opt_set_int(swr_ctx, "out_channel_layout", dst_ch_layout, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", dst_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", dst_sample_fmt, 0);
/* initialize the resampling context */
if ((ret = swr_init(swr_ctx)) < 0) {
std::cout << "Failed to initialize the resampling context" << std::endl;
return -1;
}
//配置输入的参数
/*
* src_nb_samples: 描述一整的采样个数 比如这里就是 1024
* src_linesize: 描述一行采样字节长度
* 当是planr 结构 LLLLLRRR 的时候 比如 一帧1024个采样,32为表示。那就是 1024*4 = 4096
* 当是非palner 结构的时候 比如一帧1024采样 32位表示 双通道 1024*4*2 = 8196 要乘以通道
* src_nb_channels : 可以根据布局获得音频的通道
* ret 返回输入数据的长度 比如这里 1024 * 4 * 2 = 8196 (32bit,双声道,1024个采样)
*/
src_nb_channels = av_get_channel_layout_nb_channels(src_ch_layout);
ret = av_samples_alloc_array_and_samples(&src_data_, &src_linesize, src_nb_channels,
src_nb_samples, src_sample_fmt, 0);
if (ret < 0) {
std::cout << "Could not allocate source samples\n" << std::endl;
return -1;
}
src_nb_samples_ = src_nb_samples;
//配置输出的参数
int max_dst_nb_samples = dst_nb_samples_ =
av_rescale_rnd(src_nb_samples, dst_rate, src_rate, AV_ROUND_UP);
dst_nb_channels = av_get_channel_layout_nb_channels(dst_ch_layout);
ret = av_samples_alloc_array_and_samples(&dst_data_, &dst_linesize, dst_nb_channels,
dst_nb_samples_, dst_sample_fmt, 0);
if (ret < 0) {
std::cout << "Could not allocate destination samples" << std::endl;
return -1;
}
}
- 输入参数为源的pcm参数和转换后的参数,主要是音频通道布局类型(src_cha_layout),音频采样率(src_rate),音频采样格式(src_sample_fmt)。还有一个参数是src_nb_samples,这描述一帧数据有多少采样,对于aac这里一般是1024,这个参数只能从AVFrame里面读取
- SwrContext 首先swr_alloc() 分配空间 swr_init() 初始化输出参数
- av_get_channel_layout_nb_channels 可以通过channel_layout 获得有几个音频通道,比如双声道就能判断是两个channel
- av_rescale_rnd() 这个方法返回了这帧数据在重采样后有多少个采样,这个可能比较难理解一点。比如在48000hz采样率的数据重采样44100hz后数据采样量变少了。那么原来1024个采样也会变小
- av_samples_alloc_array_and_samples() 这个方法主要是对它的第一个参数分配空间,根据后面的参数,第一个参数,后面用于存放pcm。这里会有一些复杂,这里仔细讲解一下
arduino
int av_samples_alloc_array_and_samples(uint8_t ***audio_data, int *linesize, int nb_channels,
int nb_samples, enum AVSampleFormat sample_fmt, int align)
audio_data 外面传入的是二级指针的地址,在这个方法里面分配空间。那么它怎么怎么存放pcm数据呢,这里要分两种情况就是pcm是planar格式存放的,还是非planar
-
audio_data[0]: LLLLLLLLLLLLLL audio_data[1]: RRRRRRRRRRRRRR
-
非planar audio_data[0]: LRLRLRLRLRLRLRLRLRLRLRLRLRLR
其实从原理来audio_data[0] 都是是指向一个uint8_t *buf数组的,在planar的时候 audio_data[1]指示指向 bufer的R开头的位置
另外一个知识点就是linesize是多少,在不同pcm结构时候也是不一样的,下面是远吗的描述
ini
line_size = planar ? FFALIGN(nb_samples * sample_size, align) :
FFALIGN(nb_samples * sample_size * nb_channels, align);
- planar: linesize 举个例子就是 1024 * (32/8)
- 非planar linesize 举个例子就是 1024 * (32/8) * 2
av_sample_fmt_is_planar()方法可以获得planar的类型的,这个保存在一个数组里面,可以进入远吗分析,就比较容易理解。
3. SwrResample的WriteInput(AVFrame* frame)
把一个frame的数据写swr的src_data里面,有了上面知识,就会好理解一些了,很多范例是没有考虑planar的,这样会造成一些兼容问题。大家可以根据注释去理解
这里主要frame->data 是分开L和R存储的,frame->data[0] 是所有的LLLLL,frame->data[1] 是所有的RRR
ini
int SwrResample::WriteInput(AVFrame* frame)
{
int planar = av_sample_fmt_is_planar(src_sample_fmt_);
int data_size = av_get_bytes_per_sample(src_sample_fmt_);
if (planar)
{
//src是planar类型的话,src_data里面数据是LLLLLLLRRRRR 结构,src_data_[0] 指向全部的L,src_data_[1] 指向全部R
// src_data_ 里面其实一个长 uint8_t *buf,src_data_[0] 指向L开始的位置,src_data_[1]指向R的位置
// linesize 是 b_samples * sample_size 就是比如 48000*4
for (int ch = 0; ch < src_nb_channels; ch++) {
memcpy(src_data_[ch], frame->data[ch], data_size * frame->nb_samples);
}
}
else
{
//src是非planar类型的话,src_data里面数据是LRLRLRLR 结构,src_data_[0] 指向全部数据 没有src_data[1]
// linesize 是nb_samples * sample_size * nb_channels 比如 48000*4*2
for (int i = 0; i < frame->nb_samples; i++){
for (int ch = 0; ch < src_nb_channels; ch++)
{
//拷贝后src_data[0] 指针就移动到了尾部
memcpy(src_data_[0], frame->data[ch], data_size * frame->nb_samples);
}
}
}
return 0;
}
4. SwrResample的:SwrConvert() 执行
ini
int SwrResample::SwrConvert()
{
int ret = swr_convert(swr_ctx, dst_data_, dst_nb_samples_, (const uint8_t**)src_data_, src_nb_samples_);
if (ret < 0) {
fprintf(stderr, "Error while converting\n");
exit(1);
}
int dst_bufsize = av_samples_get_buffer_size(&dst_linesize, dst_nb_channels,
ret, dst_sample_fmt_, 1);
int planar = av_sample_fmt_is_planar(dst_sample_fmt_);
if (planar)
{
int data_size = av_get_bytes_per_sample(dst_sample_fmt_);
for (int i = 0; i < dst_nb_samples_; i++) {
for (int ch = 0; ch < dst_nb_channels; ch++)
{
fwrite(dst_data_[ch]+i*data_size, 1, data_size, outdecodedswffile);
}
}
}
else {
//非planr结构,dst_data_[0] 里面存在着全部数据
fwrite(dst_data_[0], 1, dst_bufsize, outdecodedswffile);
}
return dst_bufsize;
}
这个方法就想对比较简单,swr_convert() 就是具体执行,返回执行结果。dst_bufsize是计算得出的转换后的这一个帧数据的大小,下面就是更具planar的类型存储。我们存储pcm文件的时候是采用非planar结构的,也就是LRLRLR,方便工具读取
5. SwrResample的 close 执行
scss
void SwrResample::Close()
{
#ifdef WRITE_RESAMPLE_PCM_FILE
fclose(outdecodedswffile);
#endif
if (src_data_)
av_freep(&src_data_[0]);
av_freep(&src_data_);
if (dst_data_)
av_freep(&dst_data_[0]);
av_freep(&dst_data_);
swr_free(&swr_ctx);
}
调用,我们在上一个解码后的音频后面调用
ini
int FileDecode::DecodeAudio(AVPacket* originalPacket)
{
int ret = avcodec_send_packet(codecCtx, originalPacket);
if (ret < 0)
{
return -1;
}
AVFrame* frame = av_frame_alloc();
ret = avcodec_receive_frame(codecCtx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
return -2;
}else if (ret < 0) {
std::cout << "error decoding";
return -1;
}
int data_size = av_get_bytes_per_sample(codecCtx->sample_fmt);
if (data_size < 0) {
/* This should not occur, checking just for paranoia */
std::cout << "Failed to calculate data size\n";
return -1;
}
#ifdef WRITE_DECODED_PCM_FILE
for (int i = 0; i < frame->nb_samples; i++)
for (int ch = 0; ch < codecCtx->channels; ch++)
fwrite(frame->data[ch] + data_size * i, 1, data_size, outdecodedfile);
#endif
// 把AVFrame里面的数据拷贝到,预备的src_data里面
if (swrResample == NULL)
{
swrResample = new SwrResample();
//创建重采样信息
int src_ch_layout = codecCtx->channel_layout;
int src_rate = codecCtx->sample_rate;
enum AVSampleFormat src_sample_fmt = codecCtx->sample_fmt;
int dst_ch_layout = AV_CH_LAYOUT_STEREO;
int dst_rate = 44100;
enum AVSampleFormat dst_sample_fmt = codecCtx->sample_fmt;
//aac编码一般是这个,实际这个值只能从解码后的数据里面获取,所有这个初始化过程可以放在解码出第一帧的时候
int src_nb_samples = frame->nb_samples;
swrResample->Init(src_ch_layout, dst_ch_layout,
src_rate, dst_rate,
src_sample_fmt, dst_sample_fmt,
src_nb_samples);
}
ret = swrResample->WriteInput(frame);
int res = swrResample->SwrConvert();
av_frame_free(&frame);
return 0;
}
FileDecode 见上一个博客的描述
运行
执行后会输出一个decode_resample.pcm 文件,用Audioactity去播放验证自己的代码是否正确
两个波形图一致,且能正确播放,就说明转换成功
6 总结
设计音频转换这一块其实,理解难度还是有的,特别对于初学这,里面有很多方法,而且功能类似,大家可以读ffmpeg源代码的方式帮助理解。
其他:
- 仓库: github.com/SnailCoderG...
- 讲解视频地址: www.bilibili.com/video/BV1Wx...
- 联系我:
-
微信: p13071210551