一、简介

接着上节的音频解码,使用SDL播放音频。

通过上节程序运行打印发现这些音频信息明显不符合SDL的,所以我们需要进行重采样

二、音频重采样

这里我们可以参考之前的《12_采样格式&音频重采样》来实现现在的重采样。

2.1 引入头文件

extern "C" {
#include <libswresample/swresample.h>
}

还需要在pro文件中引入swresample

LIBS += -L $${FFMPEG_HOME}/lib \
-lavcodec \
-lavformat \
-lavutil \
-lswresample

2.2 定义重采样相关属性

/******** 音频相关 ********/
typedef struct {
int sampleRate;
AVSampleFormat sampleFmt;
int chLayout;
int chs;
int bytesPerSampleFrame;
} AudioSwrSpec; /** 音频重采样上下文 */
SwrContext *_aSwrCtx = nullptr;
/** 音频重采样输入\输出参数 */
AudioSwrSpec _aSwrInSpec, _aSwrOutSpec;
/** 音频重采样输入\输出frame */
AVFrame *_aSwrInFrame = nullptr, *_aSwrOutFrame = nullptr;
/** 音频重采样输出PCM的索引(从哪个位置开始取出PCM数据填充到SDL的音频缓冲区) */
int _aSwrOutIdx = 0;
/** 音频重采样输出PCM的大小 */
int _aSwrOutSize = 0; /** 初始化音频重采样 */
int initSwr();

2.3初始化重采样

int VideoPlayer::initSwr() {
// 重采样输入参数
_aSwrInSpec.sampleFmt = _aDecodeCtx->sample_fmt;
_aSwrInSpec.sampleRate = _aDecodeCtx->sample_rate;
_aSwrInSpec.chLayout = _aDecodeCtx->channel_layout;
_aSwrInSpec.chs = _aDecodeCtx->channels; // 重采样输出参数
_aSwrOutSpec.sampleFmt = AV_SAMPLE_FMT_S16;
_aSwrOutSpec.sampleRate = 44100;
_aSwrOutSpec.chLayout = AV_CH_LAYOUT_STEREO;
_aSwrOutSpec.chs = av_get_channel_layout_nb_channels(_aSwrOutSpec.chLayout);
_aSwrOutSpec.bytesPerSampleFrame = _aSwrOutSpec.chs
* av_get_bytes_per_sample(_aSwrOutSpec.sampleFmt); // 创建重采样上下文
_aSwrCtx = swr_alloc_set_opts(nullptr,
// 输出参数
_aSwrOutSpec.chLayout,
_aSwrOutSpec.sampleFmt,
_aSwrOutSpec.sampleRate,
// 输入参数
_aSwrInSpec.chLayout,
_aSwrInSpec.sampleFmt,
_aSwrInSpec.sampleRate,
0, nullptr);
if (!_aSwrCtx) {
qDebug() << "swr_alloc_set_opts error";
return -1;
} // 初始化重采样上下文
int ret = swr_init(_aSwrCtx);
RET(swr_init); // 初始化重采样的输入frame
_aSwrInFrame = av_frame_alloc();
if (!_aSwrInFrame) {
qDebug() << "av_frame_alloc error";
return -1;
} // 初始化重采样的输出frame
_aSwrOutFrame = av_frame_alloc();
if (!_aSwrOutFrame) {
qDebug() << "av_frame_alloc error";
return -1;
} return 0;
}

initAudioInfo方法中调用initSwr方法

int VideoPlayer::initAudioInfo() {
int ret = initDecoder(&_aDecodeCtx,&_aStream,AVMEDIA_TYPE_AUDIO);
RET(initDecoder); // 初始化音频重采样
ret = initSwr();
RET(initSwr); // 初始化SDL
ret = initSDL();
RET(initSDL); return 0;
}

2.4 重采样

上面进行了重采样的初始化后,现在我们可以在解码出来的PCM进行重采样

int VideoPlayer::decodeAudio(){
...... // 重采样输出的样本数
int outSamples = av_rescale_rnd(_aSwrOutSpec.sampleRate,
_aSwrInFrame->nb_samples,
_aSwrInSpec.sampleRate, AV_ROUND_UP); // 由于解码出来的PCM。跟SDL要求的PCM格式可能不一致,需要进行重采样
ret = swr_convert(_aSwrCtx,
_aSwrOutFrame->data,
outSamples,
(const uint8_t **) _aSwrInFrame->data,
_aSwrInFrame->nb_samples);
RET(swr_convert); return ret * _aSwrOutSpec.bytesPerSampleFrame;
}

swr_convert函数的参数解释:

int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
const uint8_t **in , int in_count);
  • 参数1:重采样上下文
  • 参数2:输出到什么地方,这里我们希望输出到_aSwrOutFrame->data,到时候可以直接通过_aSwrOutFrame->data[0]拿到它指向的PCM数据,如果是Planar格式data[0]指向第一个声道, data[1]指向下一个声道,但是这里最终重采样出来的数据是非Planar,是s16的所以这里可以直接通data[0]拿到PCM数据

  • 参数3:希望输出多少个样本,outSamples = outSampleRate * inSamples / inSampleRate,可以直接使用ffmpeg的av_rescale_rnd函数得到。
  • 参数4:输入数据,可以使用 _aSwrInFrame->data
  • 参数5:重采样输入数据里面包含多少个样本,可以使用_aSwrInFrame->nb_samples,这个值不一定是固定的,
  • 返回值:真正转换成功的样本,也就是每一个声道的样本数。

如果此时你运行代码会出现内存错误,这是因为重采样时,_aSwrOutFrame->datadata[0]未分配空间,所以需要在初始化重采样的地方给data[0]分配空间

// 初始化重采样的输出frame的data[0]空间
ret = av_samples_alloc(_aSwrOutFrame->data,
_aSwrOutFrame->linesize,
_aSwrOutSpec.chs,
4096, _aSwrOutSpec.sampleFmt, 1);
RET(av_samples_alloc);

三、SDL播放

上面实现了重采样,那么现在我们需要把重采样的数据填充到回调函数sdlAudioCallbackstream里面。

void VideoPlayer::sdlAudioCallback(Uint8 *stream, int len){
// 清零(静音)
SDL_memset(stream, 0, len); // len:SDL音频缓冲区剩余的大小(还未填充的大小)
while (len > 0) {
if (_state == Stopped) break; // 说明当前PCM的数据已经全部拷贝到SDL的音频缓冲区了
// 需要解码下一个pkt,获取新的PCM数据
if (_aSwrOutIdx >= _aSwrOutSize) {
// 全新PCM的大小
_aSwrOutSize = decodeAudio();
// 索引清0
_aSwrOutIdx = 0;
// 没有解码出PCM数据,那就静音处理
if (_aSwrOutSize <= 0) {
// 假定PCM的大小
_aSwrOutSize = 1024;
// 给PCM填充0(静音)
memset(_aSwrOutFrame->data[0], 0, _aSwrOutSize);
}
} // 本次需要填充到stream中的PCM数据大小
int fillLen = _aSwrOutSize - _aSwrOutIdx;
fillLen = std::min(fillLen, len); // 填充SDL缓冲区
SDL_MixAudio(stream,
_aSwrOutFrame->data[0] + _aSwrOutIdx,
fillLen, SDL_MIX_MAXVOLUME); // 移动偏移量
len -= fillLen;
stream += fillLen;
_aSwrOutIdx += fillLen;
}
}

SDL_MixAudio函数解释:

extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src,
Uint32 len, int volume);
  • 参数1:填充的目的地
  • 参数2:数据的源头,就是PCM从那个地方开始
  • 参数3:填充数据的长度,需要填充多少数据
  • 参数4:音量大小

也就是把src这个位置开始的多少个数据len填入到dst里面去

各个字段解释:

_aSwrOutSize:表面这次重采样PCm的大小

fillLen = _aSwrOutSize - _aSwrOutIdx:需要填充到stream中的PCM数据大小,减去_aSwrOutIdx主要是用于一次采样PCM大小大于了stream的缓冲区。

len -= fillLen:SDL音频缓冲区剩余的大小(还未填充的大小)

stream += fillLen:跳过刚刚已经填充的大小

_aSwrOutIdx += fillLen:跳过刚刚已经填充的大小

如果_aSwrOutIdx >= _aSwrOutSize说明PCM所有内容都已经拷贝到stream里面了,此时的PCM数据已经没有利用价值了,这个时候就得解码下一个pkt,获取新的PCM数据,此时_aSwrOutIdx就需要清零。如果没有解码出PCM数据,那就静音处理(_aSwrOutSize = 1024是经验值)。

四、停止功能

首先需要修改videoplayer.cppplay方法,在读取文件时判断当前状态释放时停止状态。

void VideoPlayer::play() {
if (_state == Playing) return;
// 状态可能是:暂停、停止、正常完毕 if(_state == Stopped){
// 开始线程:读取文件
std::thread([this](){
readFile();
}).detach();// detach 等到readFile方法执行完,这个线程就会销毁 setState(Playing);
}
}

videoplayer.h新增释放资源的方法

/** 释放资源 */
void free();
void freeAudio();
void freeVideo();

videoplayer.cpp中释放公共的一些资源

void VideoPlayer::free(){
avformat_close_input(&_fmtCtx); freeAudio();
freeVideo();
}

现在主要是释放音频相关的资源

void VideoPlayer::freeAudio(){
_aSwrOutIdx = 0;
_aSwrOutSize =0; clearAudioPktList();
avcodec_free_context(&_aDecodeCtx);
swr_free(&_aSwrCtx);
av_frame_free(&_aSwrInFrame);
if(_aSwrOutFrame){
av_freep(&_aSwrOutFrame->data[0]);// 因手动创建了data[0]的空间
av_frame_free(&_aSwrOutFrame);
} // 停止播放
SDL_PauseAudio(1);
SDL_CloseAudio();
}

在解码音频的方法decodeAudio中,还需要判断状态释放为停止状态,因为,一执行此方法就加锁了,就会再次阻塞等待,等到后终于可以拿到锁了,但是在我们等待期间有可能就被我们关掉了,此时就会出现问题,因此这里还需要在判断一下状态_state == Stopped

int VideoPlayer::decodeAudio(){
// 加锁
_aMutex->lock(); if (_aPktList->empty() || _state == Stopped) {
_aMutex->unlock();
return 0;
}
......
}

videoplayer.cpp的读取文件的while循环中也要判断释放为停止状态

while (true) {
if(_state == Stopped) break; AVPacket pkt;
ret = av_read_frame(_fmtCtx,&pkt);
if ( ret == 0) {
if (pkt.stream_index == _aStream->index) { // 读取到的是音频数据
addAudioPkt(pkt);
}else if(pkt.stream_index == _vStream->index){// 读取到的是视频数据
addVideoPkt(pkt);
}
}else{
continue;
}
}

我们之前分装好的END的宏函数最后是goto去是释放资源,现在我们直接调用free方法就可以了

#define END(func) \
if (ret < 0) { \
ERROR_BUF; \
qDebug() << #func << "error" << errbuf; \
setState(Stopped); \
emit playFailed(this); \
free(); \
return; \
}
// 初始化音频信息
bool hasAudio = initAudioInfo() >= 0;
// 初始化视频信息
bool hasVideo = initVideoInfo() >= 0;
if (!hasAudio && !hasVideo) {
emit playFailed(this);
free();
return;
}

五、处理读完音频包的情况

while (_state != Stopped) {
AVPacket pkt;
ret = av_read_frame(_fmtCtx, &pkt);
if (ret == 0) {
if (pkt.stream_index == _aStream->index) { // 读取到的是音频数据
addAudioPkt(pkt);
} else if (pkt.stream_index == _vStream->index) { // 读取到的是视频数据
addVideoPkt(pkt);
}
} else if (ret == AVERROR_EOF) { // 读到了文件的尾部
qDebug() << "已经读取到文件尾部";
break;
} else {
ERROR_BUF;
qDebug() << "av_read_frame error" << errbuf;
continue;
}
}

六、实现调节音量







七、实现静音功能



代码链接

32_音视频播放器_SDL播放的更多相关文章

  1. Pyqt 音视频播放器

    在寻找如何使用Pyqt做一个播放器时首先找到的是openCV2 openCV2 貌似太强大了,各种关于图像处理的事情它都能完成,如 读取摄像头.图像识别.人脸识别.  图像灰度处理 . 播放视频等,强 ...

  2. 分享几个不错的Android开源音视频播放器

    整理了一下Github上几个开源的音视频播放器项目,有兴趣的同学可以clone代码去研究学习.   UniversalMusicPlayer https://github.com/googlesamp ...

  3. 一些不错的Android开源音视频播放器

    摘要:来自Github上的一点点整理,希望对你有用! 整理了一下Github上几个开源的音视频播放器项目,有兴趣的同学可以clone代码去研究学习. 1.UniversalMusicPlayer ht ...

  4. 开源安卓Android流媒体音视频播放器实现声音自动停止、恢复、一键静音功能源码

    本文转自EasyDarwin团队John的博客:http://blog.csdn.net/jyt0551/article/details/60802145 我们在开发安卓Android流媒体音视频播放 ...

  5. 分析如何直接绕过超时代VPY视频播放器的播放密码

    声明:仅技术交流和学习! 前言: 你有没试过在网上下载一套视频,因网盘限速整整开机下载好几晚,下完后打开发现加密了,又找不到卖家注册.心里是几万只草泥马飞奔啊. 于是不甘心和好奇下,偿试自己破解. 目 ...

  6. 自己做的一个android 音视频播放器

    欢迎大家下载: http://download.csdn.net/detail/q610098308/8504335

  7. android形状属性、锁屏密码、动态模糊、kotlin项目、抖音动画、记账app、视频播放器等源码

    Android精选源码 直观了解Android的"形状"属性如何影响Drawable的外观. 一个灵活的视频播放器, 可替换播放器内核. android锁屏输入密码功能源码 背景动 ...

  8. jqm视频播放器,html5视频播放器,html5音乐播放器,html5媒体播放器,video开展demo,html5视频播放演示示例,html5移动视频播放器

    最近看到很多有用的论坛html5视频播放的发展,音乐播放功能,大多数都在寻找答案.所以,我在这里做一个demo.对于大家互相学习.html5开发越来越流行,至于这也是一个不可缺少的一部分的视频. 如何 ...

  9. FFmpeg简易播放器的实现-音视频播放

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10235926.html 基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文 ...

  10. Atitit.web 视频播放器classid clsid 大总结quicktime,vlc 1. Classid的用处。用来指定播放器 1 2. <object> 标签用于包含对象,比如图像、音

    Atitit.web 视频播放器classid clsid 大总结quicktime,vlc 1. Classid的用处.用来指定播放器 1 2. <object> 标签用于包含对象,比如 ...

随机推荐

  1. 配置PHP的运行环境

    一.wamp Wamp是Windows Apache Mysql PHP的缩写,即在windows下将Apache+PHP+Mysql集成的开发环境,操作简单一键安装,摆脱手动修改配置文件的繁琐. 图 ...

  2. 嫌 OSS 查询太慢?看我们如何将速度提升 10 倍!

    背景 HDFS 是 Hadoop 生态的默认存储系统,很多数据分析和管理工具都是基于它的 API 设计和实现的.但 HDFS 是为传统机房设计的,在云上维护 HDFS 一点也不轻松,需要投入不少人力进 ...

  3. SpringMVC的执行流程及初始化流程

    今天大致来看一下Spring MVC的执行流程和初始化流程是什么样的 1,执行流程:也就是一个请求是怎么到我们Controller的 2,初始化流程:也就是那些 HandlerMapping.Hand ...

  4. 除了mysql 和 sql server, 你还有另外一种选择 postgreSQL

    数据库的重要性,不用多说.数据库的名字,大家应该也知道很多.就国内来说,使用者最多的应该是mysql 和sql server,大企业用ORACLE的也不在少数. 就我个人而言,在使用.NET的时候,基 ...

  5. 【题解】U405180 计算平方和

    \(\bold{Part\ 0}\) 目录 \(/\ \bold{Contents}\) \(\bold{Part\ 1}\) 题目大意 \(/\ \bold{Item\ content}\) \(\ ...

  6. .NET Core开发实战(第13课:配置绑定:使用强类型对象承载配置数据)--学习笔记

    13 | 配置绑定:使用强类型对象承载配置数据 要点: 1.支持将配置值绑定到已有对象 2.支持将配置值绑定到私有属性上 继续使用上一节代码 首先定义一个类作为接收配置的实例 class Config ...

  7. MySQL的CTE(公用表表达式)

    (一)概念 MySQL的CTE是在MySQL8.0版本开始支持的,公用表表达式是一个命名的临时结果集,仅在单个SQL语句(例如select.insert.delete和update)的执行范围内存在. ...

  8. NC16498 [NOIP2014]寻找道路

    题目链接 题目 题目描述 在有向图G中,每条边的长度均为1,现给定起点和终点,请你在图中找一条从起点到终点的路径,该路径满足以下条件: 1.路径上的所有点的出边所指向的点都直接或间接与终点连通. 2. ...

  9. 【Android】使用 MediaMetadataRetriever 获取视频信息

    1 环境配置与初始化 ​ (1)申请权限 ​ 在 AndroidManifest.xml 中的 manifest 标签(application 同级标签)下添加外部存储读写权限,如下: <use ...

  10. Spring Boot图书管理系统项目实战-11.检索图书

    导航: pre:10.借还统计 next: 只挑重点的讲,具体的请看项目源码. 1.项目源码 需要源码的朋友,请捐赠任意金额后留下邮箱发送:) 2.页面设计 2.1 index.html <!D ...