项目背景介绍
本篇基于之前的一直准备工作,做好了fdk-aac编译集成到ffmpeg,以及之前用ffmpeg写好的音频pcm重采样。结合起来,在我的MediaPush项目基础上。编码aac音频数据。后面会结合视频的会h264一起做各种的视频推流技术,比如rtmp推流等。
如果有编译库的问题,或者使用qt采集音频pcm,重采样问题,可以翻看我之前的博客和视频注意,了解相关基础支持
环境
- VS 版本: Visual Studio Professional 2022 (64 位)
- QT 版本: 5.12.0
- c++ 语言
- ffmpeg3.4 window编译的动态库
- fdk-aac v0.1.6,编译后集成到ffmpeg
- 其他 x264库,是本项目的备用,前面有介绍,咱与本篇博客无关
开发过程
编写 AudioCapture.h 的属性
这个类是之前我写的使用qt采集系统音频pcm的类。现在要做编码,所以要做一些修改,在里面,我一个重采样器。还增加一了一段pcm重组的代码
c++
#pragma once
#include <QAudioInput>
#include <QIODevice>
#include <QAudio>
#include <stdio.h>
#include <QDebug>
#include <QWidget>
#include <QPaintEvent>
#include "SwrResample.h"
#define WRITE_RAW_PCM_FILE
class RenderArea : public QWidget
{
Q_OBJECT
public:
explicit RenderArea(QWidget* parent = nullptr);
void setLevel(qreal value);
protected:
void paintEvent(QPaintEvent* event) override;
private:
qreal m_level = 0;
QPixmap m_pixmap;
};
struct TFormat {
int sample_rate;
int chanel_layout;
AVSampleFormat sample_fmt;
};
class AudioCapture : public QIODevice
{
Q_OBJECT
public:
AudioCapture()
{
//QAudioFormat format;
//format.setSampleRate(44100); // 采样率
//format.setChannelCount(2); // 单声道
//format.setSampleSize(16); // 采样大小
//format.setCodec("audio/pcm");
//format.setByteOrder(QAudioFormat::LittleEndian);
//format.setSampleType(QAudioFormat::SignedInt);
}
~AudioCapture();
public:
inline void OpenWrite() { write_flag = true; }
inline void CloseWrite() { write_flag = false; }
void Start(const QAudioDeviceInfo& micInfo);
void Stop();
qint64 readData(char* data, qint64 maxlen) override
{
Q_UNUSED(data)
Q_UNUSED(maxlen)
return 0; // 不实际从设备中读取数据,因为我们处理的是输入数据
}
qint64 writeData(const char* data, qint64 len) override;
void CaculateLevel(const char* data, qint64 len);
qreal level() const { return m_level; }
TFormat& format()
{
return dst_format;
}
signals:
void aframeAvailable(const char* data, qint64 len);
void updateLevel();
private:
QAudioInput* audioInput = nullptr;
QAudioFormat m_pFormat;
quint32 m_maxAmplitude = 0;
qreal m_level = 0.0; // 0.0 <= m_level <= 1.0
SwrResample* m_pSwr = nullptr;
TFormat dst_format;
const int nb_sample = 1024; // 取1024个采样,主要是方便后面的aac编码
int nb_sample_size = 0; //nb_sample个采样对应的字节数,要计算
char* src_swr_data = nullptr;
int nb_swr_remain = 0;
char* dst_swr_data = nullptr;
bool write_flag = false;
#ifdef WRITE_RAW_PCM_FILE
FILE* out_raw_pcm_file;
#endif
};
- SwrResample 类型的对象就是pcm重采样,为什么要重采样,因为编码器是有限制的,很多的fmt格式不支持,我们在做音频的时候,我们一般也是要对数据做一次统一,这样方便后面的处理。当然尽量不要做太大的修改,以免造成音质的损失
- nb_sample 是一个固定值 1024。 acc编码一般可以采样一次编码1024个采样,所以这里后面输出到编码器的数据,每次都是1024个采样,当然我们设备采集不一定是这个长度,虽有中间有个数据重组的过程
- nb_sample_size 是nb_sample个采样对象的字节数。因为不同的channel,fmt。1024个采样,需要多少个字节存储是不同的
- src_swr_data 是个中间数据,存放我把设备采集的数据,做重组后的数据存储
- nb_swr_remain 是重组过程中,当前剩余数据,后面重组代码逻辑,才能理解这个参数
- dst_swr_data 是经过SwrResmaple重采样的数据。可以送入编码器的数据
- TFormat 实例对象 dst_format 是我自定义的结构,存放送入编码器的数据的格式,方便aac编码器的初始化
- out_raw_pcm_file 是存储设备采集的原始数据,方便验证采集出来的pcm数据是否有问题
编写 AudioCapture.cpp 的重要方法
采集器器启动方法
在原来简单采集的基础上,我们要做一些初始化,比如存储数据的空间分配。一些数据长度的计算。这些都要根据设备最终采集出来的格式去计算
c++
void AudioCapture::Start(const QAudioDeviceInfo& micInfo)
{
#ifdef WRITE_RAW_PCM_FILE
out_raw_pcm_file = fopen("capture_raw.pcm", "wb");
if (!out_raw_pcm_file) {
std::cout << "open out put ra file failed";
}
#endif // WRITE_RESAMPLE_PCM_FILE
m_pFormat = micInfo.preferredFormat();
AVSampleFormat sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_S16;
switch (m_pFormat.sampleSize()) {
case 8:
switch (m_pFormat.sampleType()) {
case QAudioFormat::UnSignedInt:
m_maxAmplitude = 255;
sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_U8;
break;
case QAudioFormat::SignedInt:
m_maxAmplitude = 127;
sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_U8;
break;
default:
break;
}
break;
case 16:
switch (m_pFormat.sampleType()) {
case QAudioFormat::UnSignedInt:
m_maxAmplitude = 65535;
break;
case QAudioFormat::SignedInt:
m_maxAmplitude = 32767;
sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_S16;
break;
default:
break;
}
break;
case 32:
switch (m_pFormat.sampleType()) {
case QAudioFormat::UnSignedInt:
m_maxAmplitude = 0xffffffff;
break;
case QAudioFormat::SignedInt:
m_maxAmplitude = 0x7fffffff;
sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_S32;
break;
case QAudioFormat::Float:
m_maxAmplitude = 0x7fffffff; // Kind of
sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_FLT;
default:
break;
}
break;
default:
break;
}
if (!micInfo.isFormatSupported(m_pFormat)) {
QMessageBox::warning(nullptr, tr("Audio Capture Error"), "format is not support");
return;
}
qDebug() << "default devicdName: " << micInfo.deviceName() << " : ";
qDebug() << "sameple: " << m_pFormat.sampleRate() << " : ";
qDebug() << "channel: " << m_pFormat.channelCount() << " : ";
qDebug() << "fmt: " << m_pFormat.sampleSize() << " : ";
qDebug() << "bytesPerFrame" << m_pFormat.bytesPerFrame();
m_pSwr = new SwrResample();
//目标和源采样数据格式
int channel_count= m_pFormat.channelCount();
int64_t src_ch_layout = (channel_count == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO);
int64_t dst_ch_layout = src_ch_layout;
int src_rate = m_pFormat.sampleRate();
int dst_rate = src_rate;
AVSampleFormat src_sample_fmt = sample_fmt;
AVSampleFormat dst_sample_fmt = AV_SAMPLE_FMT_S16;
dst_format.chanel_layout = dst_ch_layout;
dst_format.sample_fmt = dst_sample_fmt;
dst_format.sample_rate = dst_rate;
nb_sample_size = m_pFormat.channelCount() * (m_pFormat.sampleSize() / 8) * nb_sample;
src_swr_data = new char[nb_sample_size];
m_pSwr->Init(src_ch_layout, dst_ch_layout, src_rate, dst_rate, src_sample_fmt, dst_sample_fmt, nb_sample);
int dst_nb_sample_size = m_pSwr->GetDstNbSample() * (m_pFormat.sampleSize() / 8) * channel_count;
dst_swr_data = new char[dst_nb_sample_size];
audioInput = new QAudioInput(micInfo, m_pFormat);
//int64_t cbuffSize = audioInput->bufferSize();
//int64_t bufferSize = m_pFormat.bytesForDuration(1000*1000); // 将 10 毫秒转换为字节数
//audioInput->setBufferSize(bufferSize);
audioInput->start(this);
this->open(QIODevice::WriteOnly);
}
-
首先上面的
switch (m_pFormat.sampleSize()) {
里面通过qt采集器的一些格式,我们计算得到采集出的pcm会使用的格式 sample_fmt 的值。这里其实不太准确,我查过资料,好像没有很好的办法严格计算,我觉得这是qt做得不够精细的地方,自己如果对pcm理解能力够的,可以通过一些播放测试,和采集出来长度推测出采集的smaple fmt。也就是一个采样数据是多少bit来存储。在别的平台,这个可能会简单一些。在下面有些qDebug()的打印,方便去分析采集出来的原始pcm的格式
-
然后创建封装好的 SwrResample 。对里面的源格式,目标格式进行赋值
nb_sample_size的计算就是 声道数*(bit/8)* 采样数,这里指定采样数位1024 dst_nb_sample_size的计算是一样的,也是1024个采样的数据长度
采集过程方法
这里就是告诉执行采集的地方,有qt里面调用上来,会很频繁,data就是采集的数据,len就是长度
c++
qint64 AudioCapture::writeData(const char* data, qint64 len)
{
// 在这里处理音频数据,例如保存到文件、进行处理等
//qDebug() << "Received audio data. Size:" << len;
CaculateLevel(data,len);
if (write_flag)
{
if (nb_swr_remain + len < nb_sample_size)
{
memcpy(src_swr_data + nb_swr_remain, data, len);
nb_swr_remain += len;
}
else
{
int out_size = nb_swr_remain + len - nb_sample_size;
memcpy(src_swr_data+ nb_swr_remain, data, len - out_size);
#ifdef WRITE_RAW_PCM_FILE
fwrite(src_swr_data, 1, nb_sample_size, out_raw_pcm_file);
#endif
//消费数据
m_pSwr->WriteInput(src_swr_data, nb_sample_size);
int rlen = m_pSwr->SwrConvert(dst_swr_data);
emit aframeAvailable(dst_swr_data, rlen);
//重新取一个开始
nb_swr_remain = out_size;
if (out_size > 0)
{
memcpy(src_swr_data, data + (len- out_size), out_size);
}
}
}
return len;
}
-
CaculateLevel 延续上次讲过的,就是音频活动量检测程序
-
write_flag是个标志,启动采集数据的处理标志
-
接下来就是数据重组,我们采集出来的原始数据长度不重要,甚至是可变的都没有关系,我们把它转成nb_sample_size长度一段一段的。
这是怎么计算的呢,就是创建一个nb_sample_size长度的字节数组 src_swr_data。往里面放数据,当放的数据没有达到nb_sample_size长度的时候,就继续放。一直到超过或者等于nb_sample_size长度,那么这个时候,我们就消费src_swr_data数据 :
scssm_pSwr->WriteInput(src_swr_data, nb_sample_size);
当然这个时候,可能还有多余的数据,那么这个数据就继续存到src_swr_data里面
c++//重新取一个开始 nb_swr_remain = out_size; if (out_size > 0) { memcpy(src_swr_data, data + (len- out_size), out_size); }
等待下一次的调用,nb_swr_remain就是一直记录的当前src_swr_data有的数据。
过程入上图解释。
- 然后就是重采样
c++
m_pSwr->WriteInput(src_swr_data, nb_sample_size);
int rlen = m_pSwr->SwrConvert(dst_swr_data);
emit aframeAvailable(dst_swr_data, rlen);
dst_swr_data 通过信号槽发送出去
- 后面就是把未消费的数据,继续存储,下次整合使用
m_pSwr是我封装好了,之前有讲过,只是做了少许调整。这里不在做解释,可以自己下载代码了解
编写 aac编码过程
初始化编码器
在 AudioCapture采集器创建好后,就初始化编码器
c++
void MediaPushWindow::start()
{
ui.actionStart->setEnabled(false);
ui.actionStop->setEnabled(true);
ui.actionSettings->setEnabled(false);
start_flag = true;
m_mic->OpenWrite();
aacEncoder.reset(new AacEncoder());
InitAudioEncode();
c++
void MediaPushWindow::InitAudioEncode()
{
int sample_rate = m_mic->format().sample_rate;
int channel_layout = m_mic->format().chanel_layout;
AVSampleFormat smaple_fmt = m_mic->format().sample_fmt;
aacEncoder->InitEncode(sample_rate, 96000, smaple_fmt, channel_layout);
}
aacEncoder编码器使用的参数,采样率,采样格式,通道数,来自于 m_mic 里面的dst_format,这个之前说过,自己定义的格式,存储的就是重采样的目标格式。
编码器的初始化实现过程
AacEncoder.h AacEncoder.cpp里面就是aac编码的实现过程
由于是使用ffmpeg集成的方式,所以基本流程和视频编码过程差不多。下面贴一下头文件的代码
c++
#pragma once
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
}
#define WRITE_CAPTURE_AAC
class AacEncoder
{
public:
AacEncoder();
~AacEncoder();
int InitEncode(int sample_rate, int bit_rate, AVSampleFormat sample_fmt,int chanel_layout);
int Encode(const char* src_buf, int src_len, unsigned char* dst_buf);
int StopEncode();
/* check that a given sample format is supported by the encoder */
static int check_sample_fmt(const AVCodec* codec, enum AVSampleFormat sample_fmt)
{
const enum AVSampleFormat* p = codec->sample_fmts;
while (*p != AV_SAMPLE_FMT_NONE) {
if (*p == sample_fmt)
return 1;
p++;
}
return 0;
}
/* just pick the highest supported samplerate */
static int check_sample_rate(const AVCodec* codec,int sample_rate)
{
const int* p;
if (!codec->supported_samplerates)
return 0;
p = codec->supported_samplerates;
while (*p) {
if (*p == sample_rate)
return 1;
p++;
}
return 0;
}
/* select layout with the highest channel count */
static int select_channel_layout(const AVCodec* codec)
{
const uint64_t* p;
uint64_t best_ch_layout = 0;
int best_nb_channels = 0;
if (!codec->channel_layouts)
return AV_CH_LAYOUT_STEREO;
p = codec->channel_layouts;
while (*p) {
int nb_channels = av_get_channel_layout_nb_channels(*p);
if (nb_channels > best_nb_channels) {
best_ch_layout = *p;
best_nb_channels = nb_channels;
}
p++;
}
return best_ch_layout;
}
private:
AVPacket* pkt = nullptr;
AVFrame* frame = nullptr;
AVCodecContext* audioCodecCtx = nullptr;
#ifdef WRITE_CAPTURE_AAC
FILE* aac_out_file = nullptr;
#endif // WRITE_CAPTURE_YUV
};
依然是那个配方,只是多了两个方法check_sample_fmt,check_sample_rate。这是检查指定的参数ffmpeg是否支持。select_channel_layout暂时没有,备用把
- 继续看 InitEncode 的实现
ini
int AacEncoder::InitEncode(int sample_rate, int bit_rate, AVSampleFormat sample_fmt, int chanel_layout)
{
//avcodec_register_all();
av_register_all();
const AVCodec* codec = nullptr;
while ((codec = av_codec_next(codec))) {
if (codec->encode2 && codec->type == AVMediaType::AVMEDIA_TYPE_AUDIO) {
qDebug() << "Codec Name :" << codec->name;
qDebug() << "Type: " << av_get_media_type_string(codec->type);
qDebug() << "Description: " << (codec->long_name ? codec->long_name : codec->name);
qDebug() << "---";
}
}
/* find the MP2 encoder */
codec = avcodec_find_encoder_by_name("libfdk_aac");
//codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
if (!codec) {
fprintf(stderr, "Codec not found\n");
exit(1);
}
qDebug() << "codec name: " << codec->name;
qDebug() << "codec long name: " << codec->long_name;
const enum AVSampleFormat* p = codec->sample_fmts;
while (*p != AV_SAMPLE_FMT_NONE) {
qDebug() << "supoort codec fmt : " << av_get_sample_fmt_name(*p);
p++;
}
audioCodecCtx = avcodec_alloc_context3(codec);
if (!audioCodecCtx) {
fprintf(stderr, "Could not allocate audio codec context\n");
exit(1);
}
//打印看到只支持 AV_SAMPLE_FMT_S16,所以这里写死
audioCodecCtx->sample_rate = sample_rate;
audioCodecCtx->channel_layout = chanel_layout;
audioCodecCtx->channels = av_get_channel_layout_nb_channels(audioCodecCtx->channel_layout);
audioCodecCtx->sample_fmt = sample_fmt;
audioCodecCtx->bit_rate = bit_rate;
//检查是否支持fmt
if (!check_sample_fmt(codec, audioCodecCtx->sample_fmt)) {
//fprintf(stderr, "Encoder does not support sample format %s",av_get_sample_fmt_name(audioCodecCtx->sample_fmt));
qDebug() << "Encoder does not support sample format " << av_get_sample_fmt_name(audioCodecCtx->sample_fmt);
exit(1);
}
if (!check_sample_rate(codec, audioCodecCtx->sample_rate)) {
//fprintf(stderr, "Encoder does not support sample format %s",av_get_sample_fmt_name(audioCodecCtx->sample_fmt));
qDebug() << "Encoder does not support sample rate " << audioCodecCtx->sample_rate;
exit(1);
}
/* open it */
if (avcodec_open2(audioCodecCtx, codec, NULL) < 0) {
fprintf(stderr, "Could not open codec\n");
exit(1);
}
pkt = av_packet_alloc();
if (!pkt) {
fprintf(stderr, "could not allocate the packet\n");
exit(1);
}
/* frame containing input raw audio */
frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "Could not allocate audio frame\n");
exit(1);
}
frame->nb_samples = audioCodecCtx->frame_size;
frame->format = audioCodecCtx->sample_fmt;
frame->channel_layout = audioCodecCtx->channel_layout;
/* allocate the data buffers */
int ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
fprintf(stderr, "Could not allocate audio data buffers\n");
exit(1);
}
#ifdef WRITE_CAPTURE_AAC
if (!aac_out_file) {
aac_out_file = fopen("ouput.aac", "wb");
if (aac_out_file == nullptr)
{
}
}
#endif // WRITE_CAPTURE_AAC
return 0;
}
这里使用 avcodec_find_encoder_by_name 直接根据编码器名称来找到编码器,也可以通过id,但是ffmpeg一个编码,可能有多个编码器支持。我这里直接指定 了fdk-aac。这个名称,上面遍历的时候可以打印,在ffmpeg编译的是 ,里面也会支持的编码器名称。
frame 是编码前数据,也就是pcm帧。赋给对应的参数,就可以自动通过 av_frame_get_buffer分配空间。注意里面的nb_samples就是我说的1024个采样。可以理解为音频每帧就是这么多个采样。虽然音频的帧其实就是个长度的区别。长度打了,那么编码时间就长了,延迟就大了,长度小了,编码效率低了。1024刚刚好。音视频,永远是平衡。
音频编码过程
采集重组的数据通过信号槽,发送到这里,然后送入编码器Encode方法
arduino
void MediaPushWindow::recvAFrame(const char* data, qint64 len)
{
if (aacEncoder)
{
aacEncoder->Encode(data, len, nullptr);
}
}
音频编码实现过程
c++
int AacEncoder::Encode(const char* src_buf, int src_len, unsigned char* dst_buf)
{
int planar = av_sample_fmt_is_planar(audioCodecCtx->sample_fmt);
if (planar)
{
// 我编码用的非planer结构
}
else
{
memcpy(frame->data[0], src_buf, src_len);
}
int ret;
/* send the frame for encoding */
ret = avcodec_send_frame(audioCodecCtx, frame);
if (ret < 0) {
fprintf(stderr, "Error sending the frame to the encoder\n");
exit(1);
}
/* read all the available output packets (in general there may be any
* number of them */
while (ret >= 0) {
ret = avcodec_receive_packet(audioCodecCtx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return 0;
else if (ret < 0) {
fprintf(stderr, "Error encoding audio frame\n");
exit(1);
}
#ifdef WRITE_CAPTURE_AAC
fwrite(pkt->data, 1, pkt->size, aac_out_file);
#endif
av_packet_unref(pkt);
}
return 0;
}
这里事情就简单,还是我们熟悉的avcodec_send_frame avcodec_receive_packet 过程。 这里有点区别的地方,就是判断了一下 planar格式 。由于我们指定的fmt。所以我们是知道目标的planar格式的。
objectivec
enum AVSampleFormat {
AV_SAMPLE_FMT_NONE = -1,
AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
AV_SAMPLE_FMT_S16, ///< signed 16 bits
AV_SAMPLE_FMT_S32, ///< signed 32 bits
AV_SAMPLE_FMT_FLT, ///< float
AV_SAMPLE_FMT_DBL, ///< double
AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
AV_SAMPLE_FMT_FLTP, ///< float, planar
AV_SAMPLE_FMT_DBLP, ///< double, planar
AV_SAMPLE_FMT_S64, ///< signed 64 bits
AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar
AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
};
这是ffmpeg的源码的注释,一眼就可以看出来那些是palnar的结构。这里都用非planar就够,也有个好处,就是memcpy直接就可以拷贝 所有数据。不用交错的去处理。
编码出来的aac数据,目前先直接写文件
arduino
#ifdef WRITE_CAPTURE_AAC
fwrite(pkt->data, 1, pkt->size, aac_out_file);
#endif
vlc播放aac了。
总结
音频的处理,难点就是pcm的转换和重组,注意一帧的长度。还有就是palnar格式注意。其他的和视频一致
7. 其他:
-
仓库: MediaPush
-
讲解视频地址: www.bilibili.com/video/BV1Qf...
-
联系我:
- 邮箱: gu19860621@163.com
- 微信: p13071210551