播放器开发(六):音频帧处理并用SDL播放
目录
学习课题:逐步构建开发播放器【QT5 + FFmpeg6 + SDL2】
步骤
AudioOutPut模块
1、初始化【分配缓存、读取信息】
2、开始线程工作【从队列读帧->重采样->SDL回调->写入音频播放数据->SDL进行播放】
主要代码
分配缓存
// 对于样本队列
av_audio_fifo_alloc(playSampleFmt, playChannels, spec.samples * 5);// 对于帧的音频字节数据
// 首次计算帧大小,并且开辟缓冲区
maxOutSamples = (int) av_rescale_rnd(decCtxSamples, playSampleRate, srcSampleRate, AV_ROUND_UP);
audioBufferSize = av_samples_get_buffer_size(nullptr, srcChannels, maxOutSamples, playSampleFmt, 0);
audioBuffer = (uint8_t *) av_malloc(audioBufferSize);
重采样相关
//配置重采样器参数
swr_alloc_set_opts2(&swrContext,&srcChannelLayout, playSampleFmt, playSampleRate,&srcChannelLayout, AVSampleFormat(srcSampleFmt), srcSampleRate,0, nullptr);
//初始化重采样器
swr_init(swrContext);//重采样流程
// 计算重采样后要输出多少样本数delay = swr_get_delay(swrContext, sample_rate);out_samples = (int) av_rescale_rnd(nb_samples + delay,playSampleRate,sample_rate,AV_ROUND_DOWN);// 判断预测的输出样本数是否>本次任务的最大样本数if (out_samples > maxOutSamples) {// 释放缓冲区,重新初始化缓冲区大小av_freep(&audioBuffer);audioBufferSize = av_samples_get_buffer_size(nullptr, srcChannels, out_samples, playSampleFmt, 0);audioBuffer = (uint8_t *) av_malloc(audioBufferSize);maxOutSamples = out_samples;}playSamples = swr_convert(swrContext, &audioBuffer, out_samples, (const uint8_t **) frame->data, nb_samples);
SDL的音频回调
// SDL音频回调函数提供了一个回调接口,可以让我们在音频设备需要数据的时候向里面写入数据
// 从而进行声音播放
// 回调函数示例 函数名自定义,放在类中需要加静态(static)
void AudioOutPut::AudioCallBackFunc(void *userdata, Uint8 *stream, int len) {//userdata 是在初始化时赋值的,有时候会把类中"this"传进去//stream 是音频流,在回调函数中需要把音频数据写入到stream就可以实现声音播放//len是由SDL传入的SDL缓冲区的大小,如果这个缓冲未满,我们就一直往里填充数据...
}
完整模块
AudioOutPut
//AudioOutPut.h
#include "FFmpegHeader.h"
#include "SDL.h"
#include "queue/AVFrameQueue.h"
#include <QDebug>
#include <QObject>
#include <QtGui>
#include <QtWidgets>
#include <thread>class AudioOutPut {
private:std::thread *m_thread;bool isStopped = true; // 是否已经停止 停止时退出线程bool isPlaying = false;// 是否正在播放bool isPause = false; // 是否暂停void run();int resampleFrame(AVFrame *frame);int sdlCallBackMode = 1;QString url; //视频地址uint8_t *audioBuffer; //存储解码后音频bufferint audioBufferSize = 0;//buffer大小int audioBufferIndex = 0;SDL_mutex *mtx = nullptr;// 队列锁SDL_AudioDeviceID audioDevice;AVAudioFifo *fifo = nullptr;//Audio BufferAVFrameQueue *frameQueue; //解码后的帧队列SwrContext *swrContext; //重采样上下文// 解码器上下文AVCodecContext *decCtx; // 音频解码器上下文int srcChannels; // 源通道数AVChannelLayout srcChannelLayout;// 源通道布局enum AVSampleFormat srcSampleFmt;// 源采样格式int srcSampleRate; // 源音频采样率// playerint maxOutSamples; // 最大样本数,用于计算缓存区大小int playSamples; // 最终播放的样本数int playSampleRate;// 最终播放的音频采样率enum AVSampleFormat playSampleFmt;int playChannels;// 源通道数
public:AudioOutPut(AVCodecContext *dec_ctx, AVFrameQueue *frame_queue);int init(int mode = 1);static void AudioCallBackFunc(void *userdata, Uint8 *stream, int len);//SDL音频回调函数实体普通版void AudioCallBack(Uint8 *stream, int len);//SDL音频回调函数实体队列版void AudioCallBackFromQueue(Uint8 *stream, int len);int start();
};//AudioOutPut.cpp
#include "AudioOutPut.h"
AudioOutPut::AudioOutPut(AVCodecContext *dec_ctx, AVFrameQueue *frame_queue): decCtx(dec_ctx), frameQueue(frame_queue) {srcSampleFmt = decCtx->sample_fmt;srcSampleRate = decCtx->sample_rate;srcChannelLayout = decCtx->ch_layout;srcChannels = srcChannelLayout.nb_channels;
}
int AudioOutPut::init(int mode) {sdlCallBackMode = mode;// SDL initif (SDL_Init(SDL_INIT_AUDIO) != 0) {qDebug() << "SDL_INIT_AUDIO error";return -1;}SDL_AudioSpec wanted_spec, spec;wanted_spec.channels = decCtx->ch_layout.nb_channels;wanted_spec.freq = decCtx->sample_rate;SDL_AudioFormat sample_type;switch (srcSampleFmt) {case AV_SAMPLE_FMT_FLTP:case AV_SAMPLE_FMT_FLT:sample_type = AUDIO_F32SYS;break;case AV_SAMPLE_FMT_U8P:case AV_SAMPLE_FMT_U8:sample_type = AUDIO_U8;break;case AV_SAMPLE_FMT_S64P:case AV_SAMPLE_FMT_S64:case AV_SAMPLE_FMT_S32P:case AV_SAMPLE_FMT_S32:sample_type = AUDIO_S32SYS;break;case AV_SAMPLE_FMT_S16P:case AV_SAMPLE_FMT_S16:sample_type = AUDIO_S16SYS;break;default:sample_type = AUDIO_S16SYS;qDebug() << "不支持的采样格式:AVSampleFormat(" << srcSampleFmt << ")";}wanted_spec.format = sample_type;wanted_spec.silence = 0;wanted_spec.callback = AudioCallBackFunc;wanted_spec.userdata = this;wanted_spec.samples = decCtx->frame_size;int ret;// ret = SDL_OpenAudio(&wanted_spec, &spec);audioDevice = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, SDL_AUDIO_ALLOW_ANY_CHANGE);if (audioDevice == 0) {qDebug() << "SDL_OpenAudio error";return -1;}playChannels = spec.channels;playSampleRate = spec.freq;playSampleFmt = av_get_packed_sample_fmt(srcSampleFmt);if (mode == 1) {fifo = av_audio_fifo_alloc(playSampleFmt, playChannels, spec.samples * 5);}ret = swr_alloc_set_opts2(&swrContext,&srcChannelLayout, playSampleFmt, playSampleRate,&srcChannelLayout, AVSampleFormat(srcSampleFmt), srcSampleRate,0, nullptr);if (ret != 0) {qDebug() << "swr_alloc_set_opts2错误";return -1;}if (!swrContext) {qDebug() << "创建音频重采样上下文错误 swr_alloc";return -1;}ret = swr_init(swrContext);if (ret < 0) {qDebug() << "初始化音频重采样上下文错误 swr_init";return -1;}// 解码器上下文保存的帧样本数int decCtxSamples = 1024;if (decCtx->frame_size > 1024) {decCtxSamples = decCtx->frame_size;}// 首次计算帧大小,并且开辟缓冲区maxOutSamples = (int) av_rescale_rnd(decCtxSamples, playSampleRate, srcSampleRate, AV_ROUND_UP);audioBufferSize = av_samples_get_buffer_size(nullptr, srcChannels, maxOutSamples, playSampleFmt, 0);audioBuffer = (uint8_t *) av_malloc(audioBufferSize);return 1;
}void AudioOutPut::AudioCallBackFunc(void *userdata, Uint8 *stream, int len) {AudioOutPut *player = (AudioOutPut *) userdata;if (player->sdlCallBackMode == 1) {player->AudioCallBackFromQueue(stream, len);} else {player->AudioCallBack(stream, len);}
}void AudioOutPut::AudioCallBack(Uint8 *stream, int len) {int len1;// sdl的内部stream可用空间/* len是由SDL传入的SDL缓冲区的大小,如果这个缓冲未满,我们就一直往里填充数据 */while (len > 0) {/* audioBufferIndex 和 audioBufferSize 标示我们自己用来放置解码出来的数据的缓冲区,*//* 这些数据待copy到SDL缓冲区, 当audioBufferIndex >= audioBufferSize的时候意味着我*//* 们的缓冲为空,没有数据可供copy,这时候需要调用audio_decode_frame来解码出更多的桢数据 */if (audioBufferIndex >= audioBufferSize) {AVFrame *frame = frameQueue->pop(10);if (frame) {audioBufferSize = resampleFrame(frame);/* audioBufferSize < 0 标示没能解码出数据,我们默认播放静音 */if (audioBufferSize <= 0) {/* silence */audioBufferSize = 1024;/* 清零,静音 */memset(audioBuffer, 0, audioBufferSize);}}audioBufferIndex = 0;}/* 当audioBufferIndex < audioBufferSize 查看stream可用空间,决定一次copy多少数据,剩下的下次继续copy */len1 = audioBufferSize - audioBufferIndex;// 可用空间>if (len1 > len) {len1 = len;}if (audioBuffer == nullptr) return;memcpy(stream, (uint8_t *) audioBuffer + audioBufferIndex, len1);len -= len1;stream += len1;audioBufferIndex += len1;}
}void AudioOutPut::AudioCallBackFromQueue(Uint8 *stream, int len) {//由于AVAudioFifo非线程安全,且是子线程触发此回调,所以需要加锁SDL_LockMutex(mtx);//读取队列中的音频数据av_audio_fifo_read(fifo, (void **) &stream, playSamples);SDL_UnlockMutex(mtx);
}
int AudioOutPut::start() {SDL_PauseAudioDevice(audioDevice, 0);// SDL_PauseAudio(0);if (sdlCallBackMode == 1) {m_thread = new std::thread(&AudioOutPut::run, this);if (!m_thread->joinable()) {qDebug() << "AudioOutPut音频帧处理线程创建失败";return -1;}}isStopped = false;isPlaying = true;return 0;
}
void AudioOutPut::run() {AVFrame *frame;while (!isStopped) {frame = frameQueue->pop(10);if (frame) {audioBufferSize = resampleFrame(frame);while (true) {SDL_LockMutex(mtx);if (av_audio_fifo_space(fifo) >= playSamples) {av_audio_fifo_write(fifo, (void **) &audioBuffer, playSamples);SDL_UnlockMutex(mtx);av_frame_unref(frame);break;}SDL_UnlockMutex(mtx);//队列可用空间不足则延时等待SDL_Delay((double) playSamples / playSampleRate);}}}
}
int AudioOutPut::resampleFrame(AVFrame *frame) {int64_t delay; // 重采样后延迟int out_samples;// 预测的重采样后的输出样本数int sample_rate;// 帧原采样率int nb_samples; // 帧原样本数sample_rate = frame->sample_rate;nb_samples = frame->nb_samples;// 计算重采样后要输出多少样本数delay = swr_get_delay(swrContext, sample_rate);out_samples = (int) av_rescale_rnd(nb_samples + delay,playSampleRate,sample_rate,AV_ROUND_DOWN);// 判断预测的输出样本数是否>本次任务的最大样本数if (out_samples > maxOutSamples) {// 释放缓冲区,重新初始化缓冲区大小av_freep(&audioBuffer);audioBufferSize = av_samples_get_buffer_size(nullptr, srcChannels, out_samples, playSampleFmt, 0);audioBuffer = (uint8_t *) av_malloc(audioBufferSize);maxOutSamples = out_samples;}playSamples = swr_convert(swrContext, &audioBuffer, out_samples, (const uint8_t **) frame->data, nb_samples);if (playSamples <= 0) {return -1;}return av_samples_get_buffer_size(nullptr, srcChannels, playSamples, playSampleFmt, 1);
}
PlayerMain
添加音频输出代码
AudioOutPut *audioOutPut;
audioOutPut = new AudioOutPut(audioDecodeThread->dec_ctx, &audioFrameQueue);
audioOutPut->init(1);
audioOutPut->start();
测试运行结果
如果需要同时执行视频和音频的输出,记得要在解复用模块那把限制队列大小的位置把视频队列的大小限制给去掉。
目前只是实现了音频播放和视频渲染显示画面,但是可以看到音频和视频是不同步的,下一章我们就要让音频和视频同步起来。
播放器开发(六):音频帧处理并用SDL播放结果
相关文章:

播放器开发(六):音频帧处理并用SDL播放
目录 学习课题:逐步构建开发播放器【QT5 FFmpeg6 SDL2】 步骤 AudioOutPut模块 1、初始化【分配缓存、读取信息】 2、开始线程工作【从队列读帧->重采样->SDL回调->写入音频播放数据->SDL进行播放】 主要代码 分配缓存 // 对于样本队列 av_audio_…...

Qt 问题记录
问题记录 运行时出现的问题 运行出现的warning QWidget::repaint: Recursive repaint detected在paintEvent中使用painter绘制了线段、图片,移动了QWidget,加入了下面代码导致的 QApplication::processEvents();屏蔽后没有出现该warning QApplicati…...

Go 语言真正有什么用处?
在其十几年的发展过程中,Google 的Go 编程语言已经从 alpha 极客的好奇心发展成为世界上一些最重要的云原生软件项目背后经过考验的编程语言。 为什么Docker、Kubernetes等项目的开发者会选择 Go ?Go 的定义特征是什么?它与其他编程语言有何…...

贪心 55. 跳跃游戏 45.跳跃游戏 II
55. 跳跃游戏 题目: 给定非负数组,初始位置在数组第一格,数组值是可以选择的最大跳跃步数,判断能不能达到数组末尾。 示例 1: * 输入: [2,3,1,1,4] * 输出: true * 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1,…...

为XiunoBBS4.0开启redis缓存且支持密码验证
修改模块文件1 xiunoPHP/cache_redis.class.php: <?phpclass cache_redis {public $conf array();public $link NULL;public $cachepre ;public $errno 0;public $errstr ;public function __construct($conf array()) {if(!extension_loaded(Redis)) {return $thi…...

手把手教你写一个Shell脚本部署你的服务
我们都知道,在开发的过程中,有很多部署自己微服务的方式,其中有各种各样的不同操作,比如使用 docker 打包为镜像的方式,还有基础使用 jar 包的方式进行部署,但是呢?使用 jar 包部署,…...

银行数字化产品方案
在互联网及金融科技公司快速发展的时代背景下,银行客户普遍都意识到了自己在客户体验、客户洞察、产品服务方面受到的来自互联网的挑战 。为了更好地面对各方面的挑战,传统的业务模式必须革新。传统银行都在积极进行数字化转型。同时,也要面对…...

C# datagridview控件 绑定数据库中表中数据的方式-3
1.如下图所示,为数据库中的一张表结构,注意该表中共有11个字段 2.首先在窗体后台代码中拖入一个datagridview控件,并在窗体加载时,给datagridview控件添加列,添加的方式如下所示:请注意,每个列…...

Amazon CodeWhisperer 正式发布可免费供个人使用
文章作者:sunny 亚马逊云科技日前推出了实时 AI 编程助手 Amazon CodeWhisperer,包括个人套餐和专业套餐,所有开发人员均可免费使用个人套餐。Amazon CodeWhisperer 让开发人员能够保持专注、高效,帮助他们快速、安全地编写代码&a…...

el-table根据返回数据回显选择复选框
接口给你返回一个集合,然后如果这个集合里面的status2,就把这一行的复选框给选中 注意: 绑定的ref :row-key"getRowKeys" this.$refs.multiTableInst.toggleRowSelection(this.list[i], true); <el-table :data"list"…...

代码随想录算法训练营第四十二天 _ 动态规划_01背包问题。
学习目标: 动态规划五部曲: ① 确定dp[i]的含义 ② 求递推公式 ③ dp数组如何初始化 ④ 确定遍历顺序 ⑤ 打印递归数组 ---- 调试 引用自代码随想录! 60天训练营打卡计划! 学习内容: 二维数组处理01背包问题 听起来…...

会话 cookie 及隐私的那些事
什么是会话 Cookie? 会话 Cookie 的概念非常简单。 会话 Cookie,也称为临时 Cookie 或内存 Cookie,是网站在浏览会话期间存储在用户计算机或设备上的小数据片段。 它是由网站生成并由您的浏览器存储和使用的多种 Cookie 之一。 常规 Cookie 或“持久”Cookie 是通常在您的…...

前端知识笔记(二十九)———MySQL通配符和正则表达式
一、通配符 1.% 匹配0,1,多个字符,但不匹配NULL 2._ 匹配单个字符 3.[charlist] 匹配字符列中的任何单一字符 4.[^charlist] 或 [!charlist] 匹配不在字符列中的任何单一字符 二、正则表达式 通配符的LIKE替换为REGEXP LIKE 匹配整个列&…...

C#网络编程(System.Net.Sockets命名空间)
目录 一、Socket类 1.示例源码 2.生成效果 二、TcpClient类和TcpListener类 1.示例源码 2.生成效果 三、UdpClient类 1.示例源码 2.生成效果 System.Net.Sockets命名空间主要提供制作Sockets网络应用程序的相关类,其中Socket类、TcpClient类、TcpListener类…...

linux 系统重装 ssh 连接失败
一.错误描述 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED 二.解决方案 输入以下指令: ssh-keygen -R XXX(ip地址) 按照我的例子(ip:10.165.7.136),会返回以下信息: 重新尝试连…...

stream流操作List对象,指定属性,取差集、交集
差集 // 差集 (list1 - list2 list1 中不同数据)List<Person> reduce1 list1.stream().filter(a -> !list2.stream().map(b -> b.getAge() "&" b.getName()).collect(Collectors.toList()).contains(a.getAge() "&" a.getName()…...

计算机相关行业在大数据库时代下的潮流和趁势
还记得当初自己为什么选择计算机? 随着数据的爆炸性增长,数据科学和数据分析成为了热门的领域。这些专业涉及处理和分析大规模数据集的技术和方法,以从中提取有价值的信息和洞察。数据科学家和数据分析师在各个行业中的需求不断增加…...

Mac苹果视频剪辑:Final Cut Pro Mac
Final Cut Pro是一款由Apple公司开发的专业视频非线性编辑软件,是业界著名的视频剪辑软件之一。它最初发布于1999年,是Mac电脑上的一款独占软件。Final Cut Pro具有先进的剪辑工具、丰富的特效和颜色分级、音频处理等功能,使得用户可以轻松地…...

高德Map
使用 官网:JS API 结合Vue使用 npm i amap/amap-jsapi-loader --saveimport AMapLoader from amap/amap-jsapi-loader;marker的属性、事件、方法 https://lbs.amap.com/api/javascript-api-v2/documentation#marker 自定义marker 为创建的 Marker 指定自定义图…...

SSM新闻发布管理系统
SSM毕设分享 序号1:SSM新闻发布管理系统 1 项目简介 Hi,各位同学好,这里是郑师兄! 今天向大家分享一个毕业设计项目作品【SSM新闻发布管理系统】 师兄根据实现的难度和等级对项目进行评分(最低0分,满分5分) 难度系数…...

客户销售目标拆解:数据驱动的方法和策略
写在开头 在当今竞争激烈的商业环境中,企业需要更加精准地制定销售目标以实现业务增长。数据驱动的方法在这一过程中扮演着关键的角色,帮助企业深入了解客户特征、行为和需求。本篇博客将深入探讨销售目标拆解在企业管理中的重要性,并介绍如何利用数据驱动的方法和策略来制…...

“丝路电商”与泛欧在线公共采购平台Peppol
近期上海商务委员会公布《关于在上海市创建“丝路电商”合作先行区的方案》(以下简称方案),方案中提出:“全面贯彻落实党的二十大精神,立足新发展阶段,完整、准确、全面贯彻新发展理念,加快构建…...

今日思考 -- 创新领导力(CIO)读后感
收获3个观点: 1 ,IT DT 商业,才是未来IT人的出路之一 ! 2 ,在CXO中,CIO像CEO一样,具备了整个企业的业务全视角 ,同时也更具解决 ‘’系统性‘’问题的能力 ! 3 &…...

Python实现Excel自动化
个人网站 文章首发于公众号:小肖学数据分析 Excel是办公自动化的关键工具之一,用于数据存储、处理和分析。 Python通过 openpyxl 库,提供了强大的Excel操作能力,让我们可以读取、写入、修改和创建复杂的Excel文件。 安装 open…...

WT2605-24SS高品质录音语音芯片:实现五种变音效果,为音频应用增添无限创意
在音频技术的世界里,录音芯片作为声音处理和传输的核心部件,一直以来都承载着人们对高品质音频的追求。而唯创知音推出的WT2605-24SS高品质录音语音芯片则在此基础上更进一步,带来了五种独特的变音效果,为音频应用注入了无限的创意…...

最美早安心语问候朋友们,祝你心情美好,万事如意
1、真诚的友谊,不会忘记,永远的朋友,每天想起。生活就是大海,朋友就是浪花,大海因有了浪花而美丽,生活有了朋友而甜蜜;祝福依旧,问候依然,祝朋友们开心快乐每一天……大家…...

2312skia,16画布
创建SkCanvas 首先,阅读SkCanvasAPI概述. Skia有多个接收SkCanvas绘图命令的后端.每个后端都有创建SkCanvas的独特方式.本页给出了每个示例: 光栅化 光栅化后端将绘画到可由Skia或客户管理的内存块. 推荐用管理画布命令要绘画内存对象的SkSurface为Raster和Ganesh后端创建画…...

mysql文本类型的最大长度限制
mysql支持很多类型,不同的文本有不同的长度限制。可以根据实际需要进行选择。 TINYBLOB, TINYTEXT L 1 bytes, where L < 2^8 (255 Bytes) BLOB, TEXT L 2 bytes, where L < 2^16 (64 Kilobytes) MEDIUMBLOB, MEDIUMTEXT L 3 b…...

ASP.NET《数据库原理及应用技术》课程指导平台的开发
1.1 系统设计目标 研制《数据库原理及应用技术》课程指导平台在功能上可以满足网络课堂教学活动的需要,在Internet上实现教学活动的各个环节。系统的基本设计原则有:先进性与方便性原则、功能实用性原则、开放性与可扩展性原则等。系统设计时采用较好的…...

OSHI-操作系统和硬件信息库
文章目录 引言一、快速入门1.1 OSHI的简介1.2 引入依赖1.3 涉及的包(package)1.4 涉及的核心类 二、操作系统信息:OperatingSystem2.1 总揽2.2 文件系统信息:FileSystem2.3 网络参数信息:NetworkParams2.4 进程信息&am…...