0.前言
PCM(Pulse Code Modulation,脉冲编码调制)音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准数字音频数据。
描述 PCM 数据的 6 个参数(参考https://www.jianshu.com/p/fd43c1c82945):
- Sample Rate : 采样频率。如8kHz(电话)、44.1kHz(CD)、48kHz(DVD)。
- Sample Size : 量化位数。通常该值为16-bit。
- Number of Channels : 通道个数。常见的音频有立体声(stereo)和单声道(mono)两种类型,立体声包含左声道和右声道。另外还有环绕立体声等其它不太常用的类型。
- Sign : 表示样本数据是否是有符号位,比如用一字节表示的样本数据,有符号的话表示范围为-128 ~ 127,无符号是0 ~ 255。
- Byte Ordering : 字节序。字节序是little-endian还是big-endian。通常均为little-endian。
- Integer Or Floating Point : 整形或浮点型。大多数格式的PCM样本数据使用整形表示,而在一些对精度要求高的应用方面,使用浮点类型表示PCM样本数据。
本文通过重采样的方式,将音频文件中的数据转为指定参数格式的 PCM 数据。FFmpeg 中重采样的功能由 libswresample 提供,该模块提供了高度优化的转换音频的采样频率、声道格式或采样格式的功能。如果不转换直接读取文件的 PCM 数据,因为格式比较多,处理起来也挺麻烦,重采样之后便于进一步的处理,如绘制波形等。最终效果:
参考示例:ffmpeg-4.2.4\doc\examples\resampling_audio.c
参考博客:https://www.jianshu.com/p/bf5e54f553a4
参考博客:https://segmentfault.com/a/1190000025145553
本文代码链接(不带FFmpeg库):https://github.com/gongjianbo/MyTestCode/tree/master/Qt/GetAudioInfo
工程CSDN下载:https://download.csdn.net/download/gongjianbo1992/13188021
1.主要接口
swr_alloc_set_opts
该函数相当于 swr_alloc 加上 swr_alloc_set_opts ,即初始化并设置 SwrContext 参数。对于输入参数,取 AVCodecContext 输入解码器上下文的参数就行了。对于输出参数,可以自己制定,达到编码格式转换的目的。
/**
* @param s 已有的重采样上下文对象, 或者填 NULL
* @param out_ch_layout 输出通道布局 (AV_CH_LAYOUT_*)
* @param out_sample_fmt 输出采样格式,如16位有符号数 (AV_SAMPLE_FMT_*).
* @param out_sample_rate 输出采样率 (frequency in Hz)
* @param in_ch_layout 输入通道布局 (AV_CH_LAYOUT_*)
* @param in_sample_fmt 输入采样格式 (AV_SAMPLE_FMT_*).
* @param in_sample_rate 输入采样率 (frequency in Hz)
* @param log_offset 日志等级
* @param log_ctx parent logging context, can be NULL
*
* @return 返回重采样上下文对象,如果为 NULL 则失败
*/
struct SwrContext *swr_alloc_set_opts(struct SwrContext *s,
int64_t out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate,
int64_t in_ch_layout, enum AVSampleFormat in_sample_fmt, int in_sample_rate,
int log_offset, void *log_ctx);
swr_convert
该函数执行重采样转换。在转换之前,还要通过 av_read_frame+avcodec_send_packet+avcodec_receive_frame 解码原数据,解码完成之后,输入参数的缓冲区就填 AVFrame 的 data,采样数就填 AVFrame 的 nb_samples。
主要需要关注的是输出参数,缓冲区是一个指针数组,如果是 planar 存储形式,则左右声道会分别给两个数组写数据,如 [0]=LLLL [1]=RRRR;如果是 packed 存储形式,则只使用数组 [0],双声道时左右声道交错写,[0]=LRLRLRLR。那么缓冲区该多大呢?可以使用 av_rescale_rnd 或者 swr_get_out_samples 获取大致的转换后采样数,一般比实际的大一点,再乘上通道数和采样点位宽就得到了需要的缓冲区大小。如果输入的 nb_samles 采样数大于了输出的 nb_samplse 采样数,则 SwrContext 中会有缓存。如果有缓存,swr_get_out_samples 第二个参数填零可以取缓存数据大小,swr_convert 最后两个参数填0可以获取缓存数据。
/**
* @param s 有效的重采样上下文对象
* @param out 输出缓冲区,如果是压缩音频,只需设置第一个缓冲区
* @param out_count 输出采样数
* @param in 输入缓冲区
* @param in_count 输入采样数
*
* @return 每个通道的采样数,NULL 则错误
*/
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
const uint8_t **in , int in_count);
2.主要代码
因为我封装了两个类文件(EasyAudioContext 和 EasyAudioDecoder),所以代码比较长,完整代码请看文首,贴出来主要是便于我以后在线阅读。
(这个 Demo 也比较简单,主要就是调用 FFmpeg 的相关接口)
#ifndef EASYAUDIOCONTEXT_H
#define EASYAUDIOCONTEXT_H
#include <QString>
#include <QSharedPointer>
//在头文件导入只是偷个懒
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libswresample/swresample.h>
#include <libavutil/frame.h>
#include <libavutil/mem.h>
}
/**
* @brief 存储音频格式等信息
*/
struct EasyAudioInfo
{
bool valid=false;
//下面为音频文件相关信息
QString filepath;
QString filename;
QString encode;
int sampleRate = 0;
int channels = 0;
int sampleBit = 0;
qint64 duration = 0;
qint64 bitRate = 0;
qint64 size = 0;
QString type;
};
/**
* @brief 存储输入或者输出用的参数
*/
struct EasyAudioParameter
{
//通道数
int channels=1;
//采样存储格式,对应枚举AVSampleFormat,AV_SAMPLE_FMT_S16=1
AVSampleFormat sampleFormat=AV_SAMPLE_FMT_S16;
//采样率
int sampleRate=16000;
};
/**
* @brief 管理音频上下文,也可用来获取音频格式等信息
* @author 龚建波
* @date 2020-11-20
* @details
* 去掉了拷贝和赋值,需要作为参数传递时使用智能指针管理
* (为什么用 NULL 不用 nullptr,为了和 C 保持一致)
*
* 内存管理参考:
* https://www.jianshu.com/p/9f45d283d904
* https://blog.csdn.net/leixiaohua1020/article/details/41181155
*
* 获取音频信息参考:
* https://blog.csdn.net/zhoubotong2012/article/details/79340722
* https://blog.csdn.net/luotuo44/article/details/54981809
*
*/
class EasyAudioContext
{
private:
//判断解析状态,只有Success才表示成功
enum EasyState{
None //无效的
,Success //解析成功
,NoFile //文件不存在
,FormatOpenError //打开文件失败
,FindStreamError //读取流信息失败
,NoAudio //未找到音频流
,CodecFindDecoderError //未找到解码器
,CodecAllocContextError //分配解码上下文失败
,ParameterError //填充解码上下文失败
,CodecOpenError //打开解码器失败
};
public:
//根据文件创建上下文对象
explicit EasyAudioContext(const QString &filepath);
//去掉了拷贝和赋值,使用智能指针管理
EasyAudioContext(const EasyAudioContext &other)=delete;
EasyAudioContext &operator =(const EasyAudioContext &other)=delete;
EasyAudioContext(EasyAudioContext &&other)=delete;
EasyAudioContext &operator =(EasyAudioContext &&other)=delete;
//析构中释放资源
~EasyAudioContext();
//是否为有效的上下文
bool isValid() const;
//获取该上下文音频格式等信息
EasyAudioInfo getAudioInfo() const;
//获取该上下文参数信息
EasyAudioParameter getAudioParameter() const;
private:
//根据文件初始化上下文
void init(const QString &filepath);
//释放资源
void free();
private:
//源文件路径
QString srcpath;
//该上下文是否有效,默认无效
EasyState status=None;
//格式化I/O上下文
AVFormatContext *formatCtx = NULL;
//解码器
AVCodec *codec = NULL;
//解码器上下文
AVCodecContext *codecCtx = NULL;
//音频流index
int audioStreamIndex = -1;
//在友元中访问私有变量用
friend class EasyAudioDecoder;
};
#endif // EASYAUDIOCONTEXT_H
#include "EasyAudioContext.h"
#include <QFileInfo>
#include <QDebug>
//忘了删除,头文件已经加了
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include "libswresample/swresample.h"
#include <libavutil/frame.h>
#include <libavutil/mem.h>
}
EasyAudioContext::EasyAudioContext(const QString &filepath)
{
init(filepath);
}
EasyAudioContext::~EasyAudioContext()
{
free();
}
bool EasyAudioContext::isValid() const
{
return (status==EasyState::Success);
}
EasyAudioInfo EasyAudioContext::getAudioInfo() const
{
EasyAudioInfo info;
//把需要的格式信息copy过来
info.valid = isValid();
info.filepath = srcpath;
QFileInfo f_info(srcpath);
info.filename = f_info.fileName();
info.size = f_info.size();
if(!isValid())
return info;
info.encode = codec->name;
info.sampleRate = codecCtx->sample_rate; //hz
info.channels = codecCtx->channels;
info.sampleBit = (av_get_bytes_per_sample(codecCtx->sample_fmt)<<3); //byte
info.duration = formatCtx->duration/(AV_TIME_BASE/1000.0); //ms
info.bitRate = formatCtx->bit_rate; //bps
info.type = formatCtx->iformat->name;
return info;
}
EasyAudioParameter EasyAudioContext::getAudioParameter() const
{
EasyAudioParameter param;
if(!isValid())
return param;
param.channels=codecCtx->channels;
param.sampleFormat=codecCtx->sample_fmt;
param.sampleRate=codecCtx->sample_rate;
return param;
}
void EasyAudioContext::init(const QString &filepath)
{
srcpath=filepath;
if(!QFileInfo::exists(filepath)){
status=EasyState::NoFile;
return;
}
//ffmpeg默认用的utf8编码,这里转换下
QByteArray temp=filepath.toUtf8();
const char *path=temp.constData();
//const char *filepath="D:/Download/12.wav";
//打开输入流并读取头
//流要使用avformat_close_input关闭
//成功时返回=0
const int result=avformat_open_input(&formatCtx, path, NULL, NULL);
if (result!=0||formatCtx==NULL){
status=EasyState::FormatOpenError;
return;
}
//读取文件获取流信息,把它存入AVFormatContext中
//正常时返回>=0
if (avformat_find_stream_info(formatCtx, NULL) < 0) {
status=EasyState::FindStreamError;
return;
}
//qDebug()<<"filepath"<<filepath;
//duration/AV_TIME_BASE单位为秒
//qDebug()<<"duration"<<formatCtx->duration/(AV_TIME_BASE/1000.0)<<"ms";
//文件格式,如wav
//qDebug()<<"format"<<formatCtx->iformat->name<<":"<<formatCtx->iformat->long_name;
//qDebug()<<"bitrate"<<formatCtx->bit_rate<<"bps";
//qDebug()<<"n stream"<<formatCtx->nb_streams;
status=EasyState::NoAudio;
for (unsigned int i = 0; i < formatCtx->nb_streams; i++)
{
#if 1
//AVStream是存储每一个视频/音频流信息的结构体
AVStream *in_stream = formatCtx->streams[i];
//类型为音频
if(in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
audioStreamIndex = i;
//查找具有匹配编解码器ID的已注册解码器
//失败返回NULL
codec = avcodec_find_decoder(in_stream->codecpar->codec_id);
if(codec==NULL){
status=EasyState::CodecFindDecoderError;
return;
}
//分配AVCodecContext并将其字段设置为默认值
//需要使用avcodec_free_context释放生成的对象
//如果失败,则返回默认填充或者 NULL
codecCtx = avcodec_alloc_context3(codec);
if(codecCtx==NULL){
status=EasyState::CodecAllocContextError;
return;
}
//根据编码器填充上下文参数
//事实上codecpar包含了大部分解码器相关的信息,这里是直接从AVCodecParameters复制到AVCodecContext
//成功时返回值>=0
if(avcodec_parameters_to_context(codecCtx, in_stream->codecpar)<0){
status=EasyState::ParameterError;
return;
}
//某些AVCodecContext字段的访问器,已弃用
//av_codec_set_pkt_timebase(codec_ctx, in_stream->time_base);
//没有此句会出现:Could not update timestamps for skipped samples
codecCtx->pkt_timebase = formatCtx->streams[i]->time_base;
//打开解码器(将解码器和解码器上下文进行关联)
//使用给定的AVCodec初始化AVCodecContext
//在之前必须使用avcodec_alloc_context3()分配上下文
//成功时返回值=0
if(avcodec_open2(codecCtx, codec, nullptr)!=0){
status=EasyState::CodecOpenError;
return;
}
//采样率
//qDebug()<<"sample rate"<<codecCtx->sample_rate;
//通道数
//qDebug()<<"channels"<<codecCtx->channels;
//采样深度
//in_stream->codec->sample_fmt枚举AVSampleFormat表示数据存储格式,如16位无符号
//av_get_bytes_per_sample返回AVSampleFormat对应的字节大小
//qDebug()<<"sample bit"<<codecCtx->sample_fmt<<":"<<(av_get_bytes_per_sample(codecCtx->sample_fmt)<<3);
//编码,如pcm
//qDebug()<<"codec name"<<codec->name<<":"<<codec->long_name;
status=EasyState::Success;
return;
}
#else
//新的版本这种获取方式已弃用
AVStream *in_stream = fmt_ctx->streams[i];
AVCodecContext *avctx=in_stream->codec;
if (avctx->codec_type == AVMEDIA_TYPE_VIDEO){
//视频信息略
}else if (avctx->codec_type == AVMEDIA_TYPE_AUDIO){
//音频信息
qDebug()<<"sample rate"<<in_stream->codec->sample_rate;
qDebug()<<"channels"<<in_stream->codec->channels;
//in_stream->codec->sample_fmt枚举AVSampleFormat表示数据存储格式,如16位无符号
//av_get_bytes_per_sample返回AVSampleFormat对应的字节大小
qDebug()<<"sample bit"<<in_stream->codec->sample_fmt<<":"<<(av_get_bytes_per_sample(in_stream->codec->sample_fmt)<<3);
AVCodec *codec=avcodec_find_decoder(avctx->codec_id);
if(codec==NULL){
return;
}
qDebug()<<"codec name"<<codec->name<<":"<<codec->long_name;
return;
}
#endif
}
}
void EasyAudioContext::free()
{
if(codecCtx){
//不要直接使用avcodec_close,而是用avcodec_free_context
//把codec相关的其他东西一并释放
avcodec_free_context(&codecCtx);
}
if(formatCtx){
//avformat_close_input内部其实已经调用了avformat_free_context
avformat_close_input(&formatCtx);
//avformat_free_context(formatCtx);
}
codec=NULL;
codecCtx=NULL;
formatCtx=NULL;
}
#ifndef EASYAUDIODECODER_H
#define EASYAUDIODECODER_H
#include <functional>
#include "EasyAudioContext.h"
/**
* @brief wav文件头结构体
* @author 龚建波
* @date 2020-11-12
* @details
* wav头是不定长格式,不过这里用的比较简单的格式
* (数值以小端存储,不过pc一般是小端存储,暂不特殊处理)
* 参照:https://www.cnblogs.com/ranson7zop/p/7657874.html
* 参照:https://www.cnblogs.com/Ph-one/p/6839892.html
*/
struct EasyWavHead
{
char riffFlag[4]; //文档标识,大写"RIFF"
//从下一个字段首地址开始到文件末尾的总字节数。
//该字段的数值加 8 为当前文件的实际长度。
unsigned int riffSize; //数据长度
char waveFlag[4]; //文件格式标识,大写"WAVE"
char fmtFlag[4]; //格式块标识,小写"fmt "
unsigned int fmtSize; //格式块长度,可以是 16、 18 、20、40 等
unsigned short compressionCode; //编码格式代码,1为pcm
unsigned short numChannels; //通道个数
unsigned int sampleRate; //采样频率
//该数值为:声道数×采样频率×每样本的数据位数/8。
//播放软件利用此值可以估计缓冲区的大小。
unsigned int bytesPerSecond; //码率(数据传输速率)
//采样帧大小。该数值为:声道数×位数/8。
//播放软件需要一次处理多个该值大小的字节数据,用该数值调整缓冲区。
unsigned short blockAlign; //数据块对其单位
//存储每个采样值所用的二进制数位数。常见的位数有 4、8、12、16、24、32
unsigned short bitsPerSample; //采样位数(采样深度)
char dataFlag[4]; //表示数据开头,小写"data"
unsigned int dataSize; //数据部分的长度
};
/**
* @brief 处理音频解码相关
* @author 龚建波
* @date 2020-11-24
* @details
* 主要功能为将音频数据解码为PCM
* (本来只处理编解码,不带多线程处理部分)
*
* 测试文件地址:
* https://samples.mplayerhq.hu/A-codecs/
* 音乐下载:
* http://www.musictool.top/
* 重采样参考:
* ffmpeg-4.2.4\doc\examples\resampling_audio.c
* https://www.jianshu.com/p/bf5e54f553a4
* https://segmentfault.com/a/1190000025145553
* https://blog.csdn.net/bixinwei22/article/details/86545497
* https://blog.csdn.net/zhuweigangzwg/article/details/53395009
*/
class EasyAudioDecoder
{
public:
//解码单个音频文件,获取pcm数据
//filepath:文件路径
//params:目标格式的参数
//返回数据的size为0则转换无效
static QByteArray toPcmData(const QString &filepath,
const EasyAudioParameter ¶ms);
//转码音频文件为指定参数格式的wav(pcm)
//filepath:原文件路径
//dstpath:目标文件路径
//params:目标格式的参数
//返回false则转换失败
static bool transcodeToWav(const QString &srcpath,
const QString &dstpath,
const EasyAudioParameter ¶ms);
//多个文件转码并拼接为指定参数格式的wav(pcm)
//bool stitchingToWav(const QList<QString> &srcpaths,
// const QString &dstpath,
// const EasyAudioParameter ¶ms);
//获取pcm数据(使用重采样libswresample)
//contextPtr:上下文指针
//params:目标格式的参数,如果参数无效会使用原数据参数
//callBack:转换时的同步回调函数
// 每次packet处理都会调用,若返回false则整个toPcm无效返回false
// 回调函数参描1为输出缓冲区地址,参数2为输出数据有效字节长度
//return false:表示转换无效失败
static bool toPcm(const QSharedPointer<EasyAudioContext> &contextPtr,
const EasyAudioParameter ¶ms,
std::function<bool(const char* outData,int outSize)> callBack);
//对智能指针版本的封装
static bool toPcm(const QString &filepath,
const EasyAudioParameter ¶ms,
std::function<bool(const char* outData,int outSize)> callBack);
//生成wav(pcm)文件头信息
//
//sampleRate: 采样频率
//channels: 通道数,一般为1
//sampleFormat: AVSampleFormat枚举值
//dataSize: pcm数据字节长度
//return EasyWavHead: wav头
static EasyWavHead createWavHead(int sampleRate,int channels,AVSampleFormat sampleFormat,unsigned int dataSize);
//判断导出参数是否有效,在无效的情况下将使用输出参数对应字段值
static EasyAudioParameter getOutParameter( const EasyAudioParameter &inParams,const EasyAudioParameter &outParams);
};
#endif // EASYAUDIODECODER_H
#include "EasyAudioDecoder.h"
#include <QFileInfo>
#include <QFile>
#include <QDir>
#include <QScopeGuard>
#include <QDebug>
QByteArray EasyAudioDecoder::toPcmData(const QString &filepath, const EasyAudioParameter ¶ms)
{
//保存解码后的pcm数据
QByteArray pcm_data;
//toPcm的回调参数
std::function<bool(const char*,int)> call_back=[&](const char* pcmData,int pcmSize)
{
pcm_data.append(pcmData, pcmSize);
return true;
};
if(!toPcm(filepath,params,call_back))
pcm_data.clear();
return pcm_data;
}
bool EasyAudioDecoder::transcodeToWav(const QString &srcpath, const QString &dstpath, const EasyAudioParameter ¶ms)
{
qDebug()<<"transcodeToWav begin. src"<<srcpath<<"dst"<<dstpath;
bool trans_result=false;
QFileInfo src_info(srcpath);
QFileInfo dst_info(dstpath);
if(!src_info.exists())
return trans_result;
//判断目录存在(qfile不能生成目录)
if(dst_info.dir().exists()||dst_info.dir().mkpath(dst_info.absolutePath())){
EasyWavHead head;
QFile dst_file(dstpath);
if(dst_file.open(QIODevice::WriteOnly)){
//先写头,再写数据,再seek0覆盖头
dst_file.write((const char *)&head,sizeof(head));
//缓存pcm数据,达到一定size再写入文件
QByteArray pcm_temp;
//数据总大小
unsigned int size_count=0;
//toPcm的回调参数
std::function<bool(const char*,int)> call_back=[&](const char* pcmData,int pcmSize)
{
//每次只写一点速度比较慢
//dst_file.write(pcmData,pcmSize);
pcm_temp.append(pcmData,pcmSize);
size_count+=pcmSize;
//每次写10M
if(pcm_temp.count()>1024*1024*10){
dst_file.write(pcm_temp);
pcm_temp.clear();
}
return true;
};
trans_result=toPcm(srcpath,params,call_back);
if(trans_result){
//尾巴上那点写文件
dst_file.write(pcm_temp);
EasyAudioContext context{srcpath};
if(context.isValid()){
EasyAudioParameter in_param=context.getAudioParameter();
EasyAudioParameter out_param=getOutParameter(in_param,params);
head=createWavHead(out_param.sampleRate,out_param.channels,out_param.sampleFormat,size_count);
//覆盖头
dst_file.seek(0);
dst_file.write((const char *)&head,sizeof(head));
}else{
trans_result=false;
}
}
dst_file.close();
}
//无效的转换就把那个文件删除
if(!trans_result){
dst_file.remove();
}
}
qDebug()<<"transcodeToWav end."<<trans_result;
return trans_result;
}
bool EasyAudioDecoder::toPcm(const QSharedPointer<EasyAudioContext> &contextPtr,
const EasyAudioParameter ¶ms,
std::function<bool (const char *, int)> callBack)
{
//无效的上下文
if(contextPtr.isNull()||!contextPtr->isValid())
return false;
//描述存储压缩数据
//视频通常包含一个压缩帧,音频可能包含多个压缩帧
AVPacket *packet = NULL;
//描述原始数据
AVFrame *frame = NULL;
//重采样上下文
SwrContext *swr_ctx = NULL;
//解析时out缓冲
int out_bufsize=1024*1024*2; //默认单个通道1M大小
uint8_t *out_buffer=new uint8_t[out_bufsize];
uint8_t *out_buffer_arr[2] = {out_buffer,out_buffer+out_bufsize/2};
//没有变量来接收的话会立即执行
auto clean=qScopeGuard([&]{
if(frame){
av_frame_unref(frame);
av_frame_free(&frame);
}
if(packet){
//av_free_packet改用av_packet_unref
av_packet_unref(packet);
av_packet_free(&packet);
}
if(swr_ctx){
swr_close(swr_ctx);
swr_free(&swr_ctx);
}
if (out_buffer)
delete [] out_buffer;
qDebug()<<"toPcm clean";
});
Q_UNUSED(clean);
packet=av_packet_alloc();
av_init_packet(packet);
frame=av_frame_alloc();
//原数据格式参数
EasyAudioParameter in_param=contextPtr->getAudioParameter();
const int in_channels = in_param.channels;
const AVSampleFormat in_sample_fmt=in_param.sampleFormat;
const int in_sample_rate=in_param.sampleRate;
//目标格式的参数(如果传入的参数无效,就保持输入格式)
EasyAudioParameter out_param=getOutParameter(in_param,params);
const int out_channels = out_param.channels;
const AVSampleFormat out_sample_fmt=out_param.sampleFormat;
const int out_sample_rate=out_param.sampleRate;
//区分planar和packed
const bool out_is_planar=(out_sample_fmt>AV_SAMPLE_FMT_DBL&&out_sample_fmt!=AV_SAMPLE_FMT_S64);
//返回每个sample的字节数,S16=2 bytes
const int sample_bytes=av_get_bytes_per_sample(out_sample_fmt);
//分配SwrContext并设置/重置公共参数
//返回NULL为失败,否则分配上下文
//(目前的需求默认是转为单声道 16bit 的,只有采样率会设置)
swr_ctx=swr_alloc_set_opts(NULL, //现有的swr上下文,不可用则为NULL
av_get_default_channel_layout(out_channels), //输出通道布局 (AV_CH_LAYOUT_*)
out_sample_fmt, //输出采样格式 (AV_SAMPLE_FMT_*).
out_sample_rate, //输出采样频率 (frequency in Hz)
av_get_default_channel_layout(in_channels), //输入通道布局 (AV_CH_LAYOUT_*)
in_sample_fmt, //输入采样格式 (AV_SAMPLE_FMT_*).
in_sample_rate, //输入采样频率 (frequency in Hz)
0, NULL); //日志相关略
if(swr_ctx==NULL)
return false;
//初始化
//如果要修改转换的参数,调用参数设置后再次init
if(swr_init(swr_ctx)<0)
return false;
int ret=0;
//因为av_read_frame后就到了下一帧,为了重入先seek到起始处
//参数一: 上下文;
//参数二: 流索引, 如果stream_index是-1,会选择一个默认流,时间戳会从以AV_TIME_BASE为单位向具体流的时间基自动转换。
//参数三: 将要定位处的时间戳,time_base单位或者如果没有流是指定的就用av_time_base单位。
//参数四: seek功能flag;
//AVSEEK_FLAG_BACKWARD 是seek到请求的timestamp之前最近的关键帧
//AVSEEK_FLAG_BYTE 是基于字节位置的查找
//AVSEEK_FLAG_ANY 是可以seek到任意帧,注意不一定是关键帧,因此使用时可能会导致花屏
//AVSEEK_FLAG_FRAME 是基于帧数量快进
//返回值:成功返回>=0
if(av_seek_frame(contextPtr->formatCtx,-1,0,AVSEEK_FLAG_ANY)<0)
return false;
//av_read_frame取流的下一帧,这里循环读取
//返回0表示成功,小于0表示错误或者文件尾
while (av_read_frame(contextPtr->formatCtx, packet)>=0)
{
//取音频
if (packet->stream_index == contextPtr->audioStreamIndex)
{
//提供原始数据作为解码器的输入(将packet写入到解码队列当中去)
//返回0表示成功
ret=avcodec_send_packet(contextPtr->codecCtx, packet);
if(ret!=0)
continue;
//从解码器循环取数据帧
//返回0表示成功
while (avcodec_receive_frame(contextPtr->codecCtx, frame)==0)
{
//下一个采样数的上限
//swr_get_out_samples貌似算出来的比av_rescale_rnd多一丢丢
//但是比最终导出的采样数多一点
const int out_samples=swr_get_out_samples(swr_ctx,frame->nb_samples);
//const int out_nb_samples=av_rescale_rnd(swr_get_delay(swr_ctx, in_sample_rate)+
// frame->nb_samples,
// out_sample_rate,
// contextPtr->codecCtx->sample_rate,
// AV_ROUND_ZERO);
//qDebug()<<out_samples<<out_nb_samples<<out_bufsize;
//缓冲区大小是否足够,不够就根据计算值扩充
if(out_bufsize<sample_bytes*out_samples*out_channels){
delete[] out_buffer;
out_bufsize=sample_bytes*out_samples*1.5;
out_buffer=new uint8_t[out_bufsize];
out_buffer_arr[0]=out_buffer;
out_buffer_arr[1]=out_buffer+out_bufsize/2;
}
//重采样转换
//如果传入的nb_samles大于了传出的nb_samplse则SwrContext中会有缓存
//如果有缓存,swr_get_out_samples第二个参数填零取大小,swr_convert最后两个也填0来获取数据
//通过使用swr_get_out_samples来获取下一次调用swr_convert在给定输入样本数量下输出样本数量的上限,来提供足够的空间。
//如果是planar类型比如AV_SAMPLE_FMT_S16P,每个data[0]就是左声道,data[1]就是右声道。
//如果是packed类型,这种类型左右声道的数据都是一个一维数据连续存放的。LRLRLRLR...,就只有data[0]有数据。
//return每个通道输出的样本数,出错时为负值
ret = swr_convert(swr_ctx, out_buffer_arr, out_samples,
(const uint8_t **)frame->data,
frame->nb_samples);
if (ret <= 0) {
av_frame_unref(frame);
continue;
}
//获取给定音频参数所需的缓冲区大小=通道数 * 采样点数* 采样位数/8
const int out_bufuse = av_samples_get_buffer_size(NULL, out_channels, ret, out_sample_fmt, 1);
//qDebug()<<"out"<<out_bufuse<<"sample"<<ret<<"channel"<<out_channels<<sample_bytes*out_samples;
if(out_channels==2)
{
//双声道时提取左声道数据
//双声道区分planaer和packed
if(out_is_planar){
//planaer左右声道单独放的
if(!callBack((const char*)out_buffer_arr[0], out_bufuse/2)){
return false;
}
}else{
//packed都在[0],一左一右存放
for(int i = 0; i < out_bufuse; i += sample_bytes*2)
{
//回调false则整体失败返回false
if(!callBack((const char*)out_buffer_arr[0] + i, sample_bytes)){
return false;
}
}
}
}else if(out_channels==1){
//单声道数据
//回调false则整体失败返回false
if(!callBack((const char*)out_buffer_arr[0], out_bufuse)){
return false;
}
}
av_frame_unref(frame);
}
}
av_packet_unref(packet);
}
qDebug()<<"toPcm end";
return true;
}
bool EasyAudioDecoder::toPcm(const QString &filepath, const EasyAudioParameter ¶ms, std::function<bool (const char *, int)> callBack)
{
return toPcm(QSharedPointer<EasyAudioContext>(new EasyAudioContext(filepath)),params,callBack);
}
EasyWavHead EasyAudioDecoder::createWavHead(int sampleRate, int channels, AVSampleFormat sampleFormat, unsigned int dataSize)
{
const int bits=av_get_bytes_per_sample(sampleFormat)*8;
const int head_size = sizeof(EasyWavHead);
EasyWavHead wav_head;
memset(&wav_head, 0, head_size);
memcpy(wav_head.riffFlag, "RIFF", 4);
memcpy(wav_head.waveFlag, "WAVE", 4);
memcpy(wav_head.fmtFlag, "fmt ", 4);
memcpy(wav_head.dataFlag, "data", 4);
//出去头部前8个字节的长度,用的44字节的格式头,所以+44-8=36
wav_head.riffSize = dataSize + 36;
//不知道干嘛的
wav_head.fmtSize = 16;
//1为pcm
wav_head.compressionCode = 0x01;
wav_head.numChannels = channels;
wav_head.sampleRate = sampleRate;
wav_head.bytesPerSecond = (bits / 8) * channels * sampleRate;
wav_head.blockAlign = (bits / 8) * channels;
wav_head.bitsPerSample = bits;
//除去头的数据长度
wav_head.dataSize = dataSize;
return wav_head;
}
EasyAudioParameter EasyAudioDecoder::getOutParameter(const EasyAudioParameter &inParams, const EasyAudioParameter &outParams)
{
//如果导出的参数无效,就用输入数据的参数
EasyAudioParameter param;
param.channels=(outParams.channels<1)
?inParams.channels
:outParams.channels;
param.sampleFormat=(outParams.sampleFormat<=AV_SAMPLE_FMT_NONE||outParams.sampleFormat>=AV_SAMPLE_FMT_NB)
?inParams.sampleFormat
:outParams.sampleFormat;
param.sampleRate=(outParams.sampleRate<1)
?inParams.sampleRate
:outParams.sampleRate;
return param;
}
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include <QPainter>
#include <QFileInfo>
#include <QDebug>
#include "EasyAudioContext.h"
#include "EasyAudioDecoder.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->boxFormat->setCurrentIndex(1);
ui->lineEdit->setText(qApp->applicationDirPath()+"/wav-pcm16.wav");
//选择音频文件
connect(ui->btnFile,&QPushButton::clicked,this,[this]{
const QString filepath=QFileDialog::getOpenFileName(this);
if(!filepath.isEmpty())
ui->lineEdit->setText(filepath);
});
//获取音频信息
connect(ui->btnInfo,&QPushButton::clicked,this,[this]{
const QString filepath=ui->lineEdit->text();
QFileInfo info(filepath);
if(!info.exists())
return;
EasyAudioContext context{filepath};
if(context.isValid()){
EasyAudioInfo info=context.getAudioInfo();
QString info_str=
"\nfilepath: "+info.filepath+
"\nfilename: "+info.filename+
"\nencode: "+info.encode+
"\nsampleRate: "+QString::number(info.sampleRate)+" Hz"+
"\nchannels: "+QString::number(info.channels)+
"\nsampleBit: "+QString::number(info.sampleBit)+" bit"+
"\nduration: "+QString::number(info.duration)+" ms"+
"\nbitRate: "+QString::number(info.bitRate)+" bps"+
"\nsize: "+QString::number(info.size)+" byte"+
"\ntype: "+info.type;
ui->textEdit->append(info_str);
}
});
//转为pcm数据
connect(ui->btnPcm,&QPushButton::clicked,this,[this]{
const QString filepath=ui->lineEdit->text();
QFileInfo info(filepath);
if(!info.exists())
return;
//EasyAudioContext *context=new EasyAudioContext(filepath);
EasyAudioDecoder decoder;
EasyAudioParameter param;
param.channels=ui->spinChannel->value();
param.sampleFormat=AVSampleFormat(ui->boxFormat->currentIndex());
param.sampleRate=ui->spinRate->value();
isS16=(param.sampleFormat==AV_SAMPLE_FMT_S16||param.sampleFormat==AV_SAMPLE_FMT_S16P);
//测试重入
pcmData=decoder.toPcmData(filepath,param);
qDebug()<<"pcm data size"<<pcmData.count();
//pcmData=decoder.toPcmData(filepath,param);
//qDebug()<<"redecode pcm data size"<<pcmData.count();
update();
});
//转为wav(pcm)文件
connect(ui->btnWav,&QPushButton::clicked,this,[this]{
const QString filepath=ui->lineEdit->text();
QFileInfo info(filepath);
if(!info.exists())
return;
//EasyAudioContext *context=new EasyAudioContext(filepath);
EasyAudioDecoder decoder;
EasyAudioParameter param;
param.channels=ui->spinChannel->value();
param.sampleFormat=AVSampleFormat(ui->boxFormat->currentIndex());
param.sampleRate=ui->spinRate->value();
//qDebug()<<info.filePath()<<info.fileName()
// <<info.absoluteDir()<<info.absoluteFilePath()<<info.absolutePath();
decoder.transcodeToWav(filepath,filepath+".wav",param);
});
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.fillRect(rect(),QColor(50,50,50));
//只绘制short
if(pcmData.count()<4||!isS16)
return;
painter.setPen(QColor(100,100,100));
painter.translate(0,height()/2);
const int length=pcmData.count()/2;
//只绘制16位的数据
const short *datas=(const short *)pcmData.constData();
//点的x间隔
double xspace=width()/(double)length;
//绘制采样点步进,测试用的固定值,文件比较大懒得算,测试时不要用大文件就行了
const int step=1;
//qDebug()<<"step"<<step;
for(int i=0;i<length-step;i+=step)
{
painter.drawLine(xspace*i,-datas[i]/150,xspace*(i+step),-datas[i+step]/150);
}
}