【Windows & Esp32】基于 libjpeg-9e 编解码库的视频播放器
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
目录
🔍 如理解有误望不吝指正。
🌎 仓库https://gitee.com/npc-gitee/libjpeg_avi_player.git🚀
一、音视频基础
编码的主要目的是减小文件大小从而减小存储空间与传输带宽从某种意义上讲就是一种压缩行为像通过Zip软件对文件进行打包压缩行为类似。在压缩过程中分为有损压缩和无损压缩。
- 有损压缩丢弃不敏感信息。
- 无损压缩保留全部信息。
💡 对于音频数据按照扬声器的底层工作逻辑通过改变电压达到改变振动频率从而发出不同声音。PCM
格式作为一种非编码格式可通过将 PCM
的数据经过DAC
即可输出音频信号。对于 MP3
等经过编码的音频文件需要通过解码后将其转换为PCM数据后然后输出音频信号。
💡 对于图像数据按照显示器的底层工作逻辑显示器由多个像素组成一个像素由三个发光源RGB
组合成不同的颜色。RGB
数据格式写入 GRAM
中即可显示图像。RGB
只是图像显示的一种颜色空间还有YUV
、HSV
等颜色空间颜色空间对应的是一个数学概念叫做向量空间一个非线性相关的向量组就可以当成线性空间一个基可通过基来描述空间中任何一个向量颜色空间与向量空间是一个与之类似的场景。 🚀 这些颜色空间之间可以相互转换比如 YUV
转 RGB
RGB
转 YUV
。
对于 jpeg 等经过编码的图像文件需要经过解码后转换为相应的颜色空间的数据然后依据显示器的显示原理将其转换为 RGB 颜色空间完成显示任务。
💡 对于视频数据本质上是多帧图像和音频数据的集合图像的颜色空间的概念与图像中一样。多张图片在短时间内切换给人呈现出一种动画的效果。图像的编码针对于帧内的数据进行而视频的编码除了在帧内完成编码外还会在帧间完成编码工作。图像所做的是 空间冗余视频所做的是 空间冗余 和 时间冗余。
参考
[1]: 音视频基础知识、YUV、H264 🚀
1.1、图像编码
bmp
格式没有压缩像素格式存储在文件中时先有文件头、再图像头、后面就都是像素数据了上下颠倒存储。bmp格式也是可以压缩bmp格式也可以有颜色板。JPEG
是一种静止图像的压缩标准属于有损压缩它是一种标准的帧内压缩编码方式。jpeg 编码是通过将RGB
数据转换为YUV
的色彩空间然后进行 压缩 的。M-JPEG
源于JPEG压缩技术是一种简单的帧内JPEG压缩但是由于这种压缩本身技术限制 无法做到大比例压缩。png
是一种无损压缩格式。gif
可以保存多帧图像。gif中有个参数可以控制图片变化的快慢。
所谓颜色看板就是在文件中创建一个颜色索引图中像素用到的某个颜色则文件中存储的为索引值而不是之前的 RGB 数值。
1.2、视频编码
MPEG
是压缩运动图像及其伴音的视音频编码标准它采用了帧间压缩仅存储连续帧之间有差别的地方 从而达到较大的压缩比。H264
视频帧编码和jpeg的编码逻辑一样空间冗余在其基础上添加了 IPB 帧的逻辑在帧与帧之间做差运算时间冗余。
视频文件格式 windows设置后缀名的目的是让相应的应用程序来打开相应的文件。可以随意更改后缀名不会更改文件的内部数据格式。
视频封装格式 一种存储视频信息的容器。视频封装格式不同也不会影响视频数据主要是一种对视频数据的组合。视频封装格式与视频文件格式一一对应。
视频编码方式 对多帧图像数据进行压缩。
参考
[1]: 图像和视频的主要格式与编码格式 🚀
[2]: H264系列7H.264与MPEG4区别 🚀
[3]: JPEG编码和H264 🚀
[4]: 视频编码与封装方式整合 🚀
1.3、AVI 文件结构
AVI 其音频数据采用 16
位线性 PCM
格式未压缩而视频数据则采用MJPG
编码方式。
MJPG
是 MJPEG
的缩写还可以文件格式扩展名。
MJPEG
不像 MPEG
不使用帧间编码。
MJPEG
的工作是将 RGB 格式的影像转换成 YCrCB 格式目的是为了减少档案大小一般约可减少 1/3 ~ 1/2 左右。
MJPEG
是视频就是由系列 jpg 图片组成的视频。
AVI 视频封装格式采用的是 RIFF
文件结构方式。构造 RIFF 文件的基本单元叫做数据块Chunk每个数据块至少包含 3
个部分
- 4字节的数据块标记又称数据块的IDChunk ID
- 数据块大小
- 4 字节的形式类型或者列表类型 IDRIFF块和LIST块独有
- 数据
整个 RIFF 文件可以看成一个数据块其数据 ID 为 RIFF称为 RIFF 块。一个 RIFF 文件中只允许存在一个 RIFF 块
。RIFF 块中包含一系列的子块其中有一种子块的 ID 为 “LIST”称为 LIST 块
。LIST 块包含一系列的子块但是 LIST 块外的其他所有子块都不能再包含子块。
RIFF 块和 LIST 块比普通的数据块多了一个 形式类型Form Type 和 列表类型List Type 的数据域。 AVI 的 RIFF 块的形式类型是 AVI
注意‘AVI ’是带了一个空格的它一般包含 3 个子块
- 信息块HeaderList一个 ID 为
“hdrl”
的 LIST 块定义 AVI 文件的数据格式。 - 数据块MovieList 一个 ID 为
“movi”
的 LIST 块定义 AVI 文件的音视频序列数据格式。 - 索引块Index Chunk一个 ID 为
“idxl”
的子块定义 “movi” LIST 块的索引数据。
AVI 音视频文件的二进制内容可通过 WinHex
软件查看
将上述数据按 AVI 文件结构划分
AVI 文件结构图
- avih 块用于记录 AVI 的全局信息比如数据流的数量视频图像的宽度和高度等信息
AVIH_HEADER
为数据保存结构。 - strl 子列表文件中有多少种数据流即前面的 Streams就有多少个 strl 子列表。
- 每个 strl 中至少包含一个
strhStream Header
和一个strfStream Formate
可选strnStream Name。 - strf 的数据类型与strh相关如果strh为视频流那么 strf 对应视频流的结构保存信息。
- strl 子列表出现的顺序与后面数据块中的编号对应假设 strl 子列表第一个是视频流那么后面数据块视频帧的编号为
00dc
第二个是音频流那么编号为01wb
。
- 每个 strl 中至少包含一个
- 数据块图像数据和音频数据交错存储在数据块中
##dc/##wb
后面接当前一帧数据的大小值不包含标准类型值和数据大小值所以偏移需要加8如果这个值为奇数则加1在读数据的时候一般一次性读完一帧方便解码。 - 索引块 为 AVI 文件中每一个媒体数据块进行索引并且记录它们在文件中的偏移可能相对于‘movi’列表也可能相对于 AVI 文件开头。
二、TF卡基础
控制器对 SD 卡进行读写通信操作一般有两种通信接口可选一种是 SPI 接口另外一种就是 SDIO 接口。
根据容量SD 卡可划分为SDSC(<2GB)
、SDHC(2~32GB)
、SDXC(32GB~2TB)
。
当前 SD 协议提供的 SD 卡规范版本最新是 8.0 版本2020年但是有些芯片如stm32f4xx系列控制器只支持 SD 卡规范版本 2.0即只支持标准容量 SD 和高容量 SDHC 标准卡不支持超大容量 SDXC 标准卡。
一张 SD 卡包括有存储单元、存储单元接口、电源检测、卡及接口控制器和接口驱动器 5 个部分。
- 存储单元是存储数据部件存储单元通过存储单元接口与卡控制单元进行数据传输
- 电源检测单元保证 SD 卡工作在合适的电压下如出现掉电或上状态时它会使控制单元和存储单元接口复位
- 卡及接口控制单元控制 SD 卡的运行状态它包括有 8 个寄存器
- 接口驱动器控制 SD 卡引脚的输入输出。
转载自[野火]STM32 库开发实战指南
三、Windows上播放音视频
libjpeg
是一个完全用 C 语言编写的库包含功能有 JPEG 解码、JPEG 编码 和其它的JPEG功能的实现这里使用的版本为 jpeg-9e
。
开发平台vs2019
3.1、在 Windows 下使用 vs2019 编译 libjpeg 库
win10
系统下没有这个文件win7
系统可能会有。这里是在网站上下的文件。
win32.mdk
https://gitee.com/guangpengz/ms-sdk?_from=gitee_search 🚀
cmd
切换目录Windows-cmd切换目录
1、下载 libjpeg
这里使用 jpegsr9e.zip
官网http://www.ijg.org/ 🚀
2、解压源码
3、进入解压后的目录找到 makefile.vc
文件修改下面语句
!include <win32.mak>
所在行并将 win32.mak
替换为实际位置, 我这边修改后的值为修改为
!include <C:\Users\user\Desktop\X\ms-sdk-master\win32.mak>
5、生成 sln
工程文件在电脑的 “开始” 菜单中找到 vs2019
的命令行工具并打开这里是
x86 Native Tools Command Prompt for VS 2019
6、切换到 jpegsr9e
目录下然后输入下面命令
NMAKE /f makefile.vc setup-v16
7、打开 jpeg.sln
文件然后在 vs2019
中选择 Win32
之后点击生成解决方案最后就会在 jpegsr9e
文件夹下多一个 Release
文件夹里面有 jpeg.lib
静态库。
参考
[1]: 在Windows下使用vs2019编译libjpeg库静态库与动态库 🚀
[2]: Windows10下利用VS2022编译JpegLib 🚀
3.2、创建 libjpeg 解码项目
创建项目过程同上一篇博客 🚀 的创建方式相似这里创建项目的名称为libjpeg_for_windows
1、头文件路径添加
libjpeg_for_windows 右键 ——> 属性 ——> C/C++ ——> 常规 ——> 附加库目录中输入
.\lib\jpeg-9e
——> 确定
2、库文件路径添加
libjpeg_for_windows 右键 ——> 属性 ——> 链接器 ——> 常规 ——> 附加包含目录中输入
.\lib\jpeg-9e\Release\Win32
——> 确定
3、附加依赖项添加
libjpeg_for_windows 右键 ——> 属性 ——> 链接器 ——> 输入 ——> 附加依赖项点击右边向下箭头’ v ’ ——> 编辑 ——> 输入jpeg.lib 之前编译的静态库 ——> 确定 ——> 应用 ——> 确定。
将 jpegsr9e
文件夹下的 example.c
添加到项目中生成解决方案。这时候就会出现这些报错
LNK2001 无法解析的外部符号 _image_buffer
LNK2001 无法解析的外部符号 _image_height
LNK2001 无法解析的外部符号 _image_width
LNK2001 无法解析的外部符号 _main
LNK2001 无法解析的外部符号 _put_scanline_someplace
这些符号都是没有定义的需要通过外部链接所以这里直接在 example.c
文件中定义就可以了。添加代码如下
JSAMPLE* image_buffer;
int image_height;
int image_width;
int put_scanline_someplace(JSAMPROW buffer, int row_stride)
{
/* 根据需要自己完善 */
}
int main()
{
/* 根据需要自己完善 */
}
jpeglib.h
头文件依赖于stdio.h
和string.h
头文件
错误C4996 fopen‘fscanf’、strcmpThis function or variable may be unsafe
解决在程序最前面加#define _CRT_SECURE_NO_WARNINGS
编译还是报错所以这里通过 右键项目 —> 预处理器 —> 预处理器定义 —> 点击 ‘v’ —> 编辑 —> 添加 _CRT_SECURE_NO_WARNINGS —> 确定应用。
参考
[1]: LIBJPEG 安装编译读取jpeg图像数据 🚀
3.3、libjpeg 中 example.c 功能解析
/* example1.c */
#include <stdio.h>
#include "jpeglib.h"
#include <setjmp.h>
JSAMPLE* image_buffer; /* Points to large array of R,G,B-order data */
int image_height; /* Number of rows in image */
int image_width; /* Number of columns in image */
/* 这里主要测试解码所以这段代码注释了 */
//GLOBAL(void) write_JPEG_file(char *filename, int quality)
//{...}
struct my_error_mgr {
struct jpeg_error_mgr pub; /* "public" fields */
jmp_buf setjmp_buffer; /* for return to caller */
};
typedef struct my_error_mgr* my_error_ptr;
METHODDEF(void)
my_error_exit(j_common_ptr cinfo)
{
/* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
my_error_ptr myerr = (my_error_ptr)cinfo->err;
/* Always display the message. */
/* We could postpone this until after returning, if we chose. */
(*cinfo->err->output_message) (cinfo);
/* Return control to the setjmp point */
longjmp(myerr->setjmp_buffer, 1);
}
GLOBAL(int) read_JPEG_file(char* filename)
{
struct jpeg_decompress_struct cinfo;
struct my_error_mgr jerr;
FILE* infile; /* source file */
JSAMPARRAY buffer; /* Output row buffer */ // unsigned char ** buffer;
int row_stride; /* physical row width in output buffer */
if ((infile = fopen(filename, "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
return 0;
}
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
if (setjmp(jerr.setjmp_buffer)) {
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return 0;
}
/* Now we can initialize the JPEG decompression object. */
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, infile);
(void)jpeg_read_header(&cinfo, TRUE);
printf("image_width = %d\n", cinfo.image_width);
printf("image_height = %d\n", cinfo.image_height);
printf("num_components = %d\n", cinfo.num_components);
printf("output_width = %d\n", cinfo.output_width);
printf("output_components = %d\n", cinfo.output_components);
cinfo.out_color_space = JCS_RGB; // 以 RGB 为结果输出
(void)jpeg_start_decompress(&cinfo);
row_stride = cinfo.output_width * cinfo.output_components;
printf("output_width2 = %d\n", cinfo.output_width);
printf("output_components2 = %d\n", cinfo.output_components);
// 计算buffer大小并申请相应空间
buffer = (*cinfo.mem->alloc_sarray)
((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
while (cinfo.output_scanline < cinfo.output_height) {
(void)jpeg_read_scanlines(&cinfo, buffer, 1);
put_scanline_someplace(buffer[0], row_stride);
}
(void)jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return 1;
}
int put_scanline_someplace(JSAMPROW buffer, int row_stride)
{
int i = 0;
for (; i < row_stride; i++) {
printf("0x%x ", buffer[i]);
}
printf("\nend\n");
}
int main()
{
read_JPEG_file("C:\\Users\\xxx\\Desktop\\pic.jpg");
return 0;
}
这里对 example.c
文件的框架没有改动只是在程序中加入一些打印函数测试输出结果。
这里加入了一条语句
cinfo.out_color_space = JCS_RGB; // 以 RGB 为结果输出
程序的目的
- 1、查看程序是否能正确获得图片信息。
- 2、row_stride 变量 = 图像宽度 x RGB3字节。
- 3、jpeg_read_scanlines 函数是一行一行解码且解码后的 RGB 数据放到 buffer 中第三个参数为1设定一次解码行数。
这里通过 PS
软件构造了一张 6x6
的红色图片。如何构造请看下文 👀
从打印的结果可知正好是 6 行 6 列且每个像素的 RGB 为0xFE, 0x00x0如果将数据发送给屏幕那么就能显示一张完整的图片了。
参考
[1]: libjpeg库的简单使用rgb565与rgb888互转以及色块的寻找 🚀
3.4、SDL2 库配置与使用
对于 RGB
数据在 Windows 系统显示方面这里采用 SDL2
开源库。
SDL2
安装教程参考VS2019配置SDL2库 🚀
开发基本流程
- 【初始化SDL系统】
SDL_int()
- 【创建窗口】
SDL_CreateWindows()
- 【创建渲染器】
SDL_CreateRenderer()
- 【渲染器中创建一个材质】
SDL_CreateTexture()
- 【申请待显示RGB图像空间】
- 【为图像数据赋值】
- 【将内存中的RGB数据写入材质】
SDL_UpdateTexture()
- 【清理渲染区(清理屏幕)】
SDL_RenderClear()
- 【设定渲染的目标区域】
- 【复制材质到渲染器对象】
SDL_RenderCopy()
- 【执行渲染操作】
SDL_RenderPresent()
- 【销毁渲染器】
SDL_DestroyRenderer()
- 【销毁窗口】
SDL_DestroyWindow()
- 【退出SDL系统】
SDL_Quit()
#include <stdio.h>
#include "jpeglib.h"
#include <setjmp.h>
#include <iostream>
#include <SDL.h>
using namespace std;
#pragma comment(lib, "SDL2.lib")
// SDL 库里面定义了一个宏 main, 这里给取消掉否则会与main 函数名冲突导致编译错误
#undef main
#define __DEBUG__ 0
JSAMPLE* image_buffer; /* Points to large array of R,G,B-order data */
int image_height; /* Number of rows in image */
int image_width; /* Number of columns in image */
int Windows_Width = 0;
int Windows_Height = 0;
SDL_Window* screen = NULL;
SDL_Renderer* render = NULL;
SDL_Texture* texture = NULL;
unsigned char* r = NULL;
int put_scanline_someplace(JSAMPROW buffer, int row_stride);
int Image_Init(void);
int Image_Display(void);
//GLOBAL(void) write_JPEG_file(char* filename, int quality)
//{...}
struct my_error_mgr {
struct jpeg_error_mgr pub; /* "public" fields */
jmp_buf setjmp_buffer; /* for return to caller */
};
typedef struct my_error_mgr* my_error_ptr;
METHODDEF(void)
my_error_exit(j_common_ptr cinfo)
{
my_error_ptr myerr = (my_error_ptr)cinfo->err;
(*cinfo->err->output_message) (cinfo);
longjmp(myerr->setjmp_buffer, 1);
}
GLOBAL(int)
read_JPEG_file(char* filename)
{
struct jpeg_decompress_struct cinfo;
struct my_error_mgr jerr;
FILE* infile; /* source file */
JSAMPARRAY buffer; /* Output row buffer */ // unsigned char ** buffer;
int row_stride; /* physical row width in output buffer */
if ((infile = fopen(filename, "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
return 0;
}
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
/* Establish the setjmp return context for my_error_exit to use. */
if (setjmp(jerr.setjmp_buffer)) {
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return 0;
}
/* Now we can initialize the JPEG decompression object. */
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, infile);
(void)jpeg_read_header(&cinfo, TRUE);
#if __DEBUG__
printf("image_width = %d\n", cinfo.image_width);
printf("image_height = %d\n", cinfo.image_height);
printf("num_components = %d\n", cinfo.num_components);
printf("output_width = %d\n", cinfo.output_width);
printf("output_components = %d\n", cinfo.output_components);
#endif
cinfo.out_color_space = JCS_RGB;
// 显示初始化
// 定义图像的宽高
Windows_Width = cinfo.image_width;
Windows_Height = cinfo.image_height;
// 显示初始化
if (Image_Init() != 0) {
printf("Image Init error\n");
return -1;
}
// 准备一幅w*h的红色RGB图像数据
shared_ptr<unsigned char> rgb(new unsigned char[Windows_Width * Windows_Height * 4]); // 乘以4是因为像素格式已指定为ARGB888单个像素点占4字节
r = rgb.get();
(void)jpeg_start_decompress(&cinfo);
row_stride = cinfo.output_width * cinfo.output_components;
#if __DEBUG__
printf("output_width2 = %d\n", cinfo.output_width);
printf("output_components2 = %d\n", cinfo.output_components);
#endif
// 计算buffer大小并申请相应空间
buffer = (*cinfo.mem->alloc_sarray)
((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
int j = 0;
int lineR = 0; // 每一行R分量的起始位置
while (cinfo.output_scanline < cinfo.output_height) {
int i = 0;
(void)jpeg_read_scanlines(&cinfo, buffer, 1);
/* Assume put_scanline_someplace wants a pointer and sample count. */
//put_scanline_someplace(buffer[0], row_stride);
// 为上述图像数据赋值
for (int k = 0; k < Windows_Width * 4; k += 4)
{
r[lineR + k] = buffer[0][i+2]; // B
r[lineR + k + 1] = buffer[0][i+1]; // G
r[lineR + k + 2] = buffer[0][i]; // R
r[lineR + k + 3] = 0; // A
i += 3;
}
j++;
lineR = j * Windows_Width * 4;
}
// 执行显示操作
Image_Display();
// 释放解码资源
(void)jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return 1;
}
int put_scanline_someplace(JSAMPROW buffer, int row_stride)
{
int i = 0;
for (; i < row_stride; i++) {
printf("0x%x ", buffer[i]);
}
printf("\nend\n");
return 0;
}
int Image_Init(void)
{
// 1. 初始化SDL库 成功返回0, 失败返回非0值
if (SDL_Init(SDL_INIT_VIDEO))
{
cout << SDL_GetError() << endl;
return -1;
}
// 2. 创建SDL窗口
screen = SDL_CreateWindow("test_sdl_ffmpeg", // 窗口标题
SDL_WINDOWPOS_CENTERED, // 窗口位置
SDL_WINDOWPOS_CENTERED,
Windows_Width, Windows_Height, // 窗口宽高
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE // 窗口属性指定使用OpenGL, 并且可调整大小
);
if (!screen)
{
cout << SDL_GetError() << endl;
return -2;
}
// 3. 创建渲染器
render = SDL_CreateRenderer(screen, // 指定渲染到哪个窗口
-1, // 指定渲染器驱动默认传-1
SDL_RENDERER_ACCELERATED // 指定渲染模式这里采用硬件加速模式
);
if (!render)
{
cout << SDL_GetError() << endl;
return -3;
}
// 4. 在渲染器当中创建一个材质
texture = SDL_CreateTexture(render, // 指定在哪个渲染器当中创建
SDL_PIXELFORMAT_ARGB8888, // 指定当前材质的像素格式
SDL_TEXTUREACCESS_STREAMING, // 设定当前材质可修改
Windows_Width, Windows_Height // 指定材质宽高
);
if (!texture)
{
cout << SDL_GetError() << endl;
return -4;
}
return 0;
}
int Image_Display(void)
{
// 5. 将内存中的RGB数据写入材质
if (SDL_UpdateTexture(texture, NULL, r, Windows_Width * 4))
{
cout << SDL_GetError() << endl;
return -5;
}
// 6. 清理渲染区(清理屏幕)
if (SDL_RenderClear(render))
{
cout << SDL_GetError() << endl;
return -6;
}
// 设定渲染的目标区域
SDL_Rect destRect;
destRect.x = 0;
destRect.y = 0;
destRect.w = Windows_Width;
destRect.h = Windows_Height;
// 7. 复制材质到渲染器对象
if (SDL_RenderCopy(render, texture, NULL, &destRect))
{
cout << SDL_GetError() << endl;
return -7;
}
// 8. 执行渲染操作
SDL_RenderPresent(render);
return 0;
}
int main(int argc, const char *argv[])
{
SDL_Event ev;
bool quit_flag = 0;
// jpeg 解码与显示
read_JPEG_file((char *)"C:\\Users\\npc\\Desktop\\24110307_20.jpg");
while (!quit_flag) {
while (SDL_PollEvent(&ev) != 0) {
if (ev.type == SDL_QUIT) {
SDL_DestroyWindow(screen);
quit_flag = true;
}
}
}
return 0;
}
参考
[1]: 音视频技术应用(7)-使用SDL渲染一幅指定的图像并且动态修改图像数据 🚀
[2]: SDL渲染应用1 🚀
[3]: SDL 教程 🚀
3.5、音频部分程序移植
这里参考之前 mp3博客 🚀这里需要添加 winmm.lib 库、将wave_out.c 与 wave_out.h 文件添加到项目中。
由于项目中为 C++ 的编译方式这里加入 C 程序需要做一点修改
- 1、在 videoplayer.h 中加入
extern "C" { #include "wave_out.h" }
- 2、在 wav_out.h 中加入一行语句
#pragma comment(lib,"User32.lib")
参考
[1]: C与C++混合编程 🚀
[2]: error LNK2019: 无法解析的外部符号_imp__MessageBoxA@16该符号在函数 _WinMain@16 中被引用 🚀
3.6、音视频文件格式转换
网上下载的视频多为 mp4 格式这里通过 「 格式工厂 」 完成转码工作。
格式工厂下载http://www.pcfreetime.com/formatfactory/CN/download.html 🚀
格式工厂安装后会自启动 brightdata.exe
如何彻底关闭Bright Data 🚀
1、点击 -> AVI FLV MOV Etc...
2、点击添加文件将待转换的文件添加到其中 ——> 点击输出配置设置导出格式
3、设置导出配置屏幕大小、码率可以手动输入想要的值
视频编码 选择 MJPEG解码库只支持 MJPG 视频解码。
视频尺寸 这里需要根据屏幕的分辨率选择。可以手动输入特殊的值
比特率 对于图像设置时这里显示为“码率”但是实际码率计算 = 文件大小(KB)*8/时间(s)
所以格式工厂中的码率图像/比特率音频指的是什么如果有知道的望不吝相告
帧率 每秒中播放多少张图片。
音频编码 本例程只支持 PCM 音频所以选这个可以将数据直接输出到DAC播放而不需要经过解码这里默认为16位深。
采样率 这里设置为 22050即 22.05Khz 的采样率音频文件大小(MB) = 采样率(kHz)*位深(bit)*通道数*时间(s)/8/1024
。
3.7、程序的编写与说明
实现了图像显示和音频播放那么接下来就是根据 1.3 节
中 AVI 文件结构编写程序解封装解析音视频文件获取相关信息初始化图像显示和音频播放硬件资源读取视频帧/音频帧将数据发送到相关模块进行处理处理完成后发送给显示器显示、扬声器/耳机播放。
具体步骤如下
- 打开音视频文件
- 解封装查看当前文件信息用于完成显示和声卡硬件的初始化
- 显示初始化
- 声卡初始化
- 定时器初始化用于实现指定的帧率
- 读取一个完整的数据帧视频帧/音频帧+下一帧的标记和帧大小视频帧读取后开始解码然后显示音频帧读取后直接播放
- 获取窗口操作当点击 “X” 时退出程序
- 将下一帧的标记和帧大小填充到avix全局变量
- 6-8步骤循环直到主动退出或读完文件
- 释放硬件资源。
文件关系
main.cpp
|
videoplayer.cpp
|
---------------------------------------
| | | |
mjpeg.cpp avi.cpp timer.cpp wave_out.c
关于 fopen(“xxx”, “rb”) 导致的问题
Windows 平台上fopen(“xxxx”, “r”) 方式打开文件后fread最多读取的字节数为10130采用 fopen(“xxx”, “rb”) 方式打开文件后fread 读取的字节数能达到所要求的60KB。
关于 avi_get_streaminfo(pbuf + offset + 4) 函数的说明
该函数的作用就是获取文件中的 stream 流信息这里需要注意的点在于存储顺序问题以流类型为例假设地址 0 存储 0x00、地址 1 存储 0x00、地址 2 存储 0x64(‘d’)、地址 3 存储 0x63(‘c’)那么存储到 short 类型的数据中就需要调整一下位置不然存储的就是 0xcd这里就需要MAKEWORD(ptr)
带参数的宏来实现。
#define MAKEWORD(ptr) (u16)(((u16)*((u8*)(ptr))<<8)|(u16)*(u8*)((ptr)+1))
MAKEDWORD(ptr)
宏定义有相同的作用。
关于 Image_Init 函数不放到 mjpegdec_decode 函数中说明
mjpegdec_decode 函数的作用是读取一帧图像/音频数据后开始解码如果将 Image_Init 则会重复创建宽口影响体验效果其次创建完成后如果不将其删除完成资源回收易造成内存溢出的情况所以综合考虑将该 Image_Init 函数内容放到外部。
关于定时器选用说明
- 首先 SDL 提供了定时器功能可以选用 SDL 的方案timer 定时器 🚀
- CreateTimerQueueTimer 作为 Windows 提供的 API虽然定时上不是最精确但是相对更为灵活且能满足视频播放所需要达到的 ms 级别。这个回调函数中执行内容尽可能短在中断服务例程中也一样不然会影响系统响应速度不然如果前一个回调函数还没执行完会启动另一个线程来调用该回调函数。
参考① Windows精确定时(ms) 🚀 ② CreateTimerQueueTimer定时器使用 🚀 ③ Windows 应用开发文档 🚀 ④ CreateTimerQueueTimer学习笔记 🚀
关于 SDL_PollEvent 函数的重要性
该函数的主要作用为检测事件比如点击窗口右上角的“X”如果没有这个函数当 PC 界面上出现弹窗或者移动视频播放窗口会发生无法响应的情况从应用的角度考虑当点击最小化、最大化、关闭窗口都希望有对应的响应就可通过该函数完成事件获取程序中完成事件判断并编写相应的程序完成对应的操作。
参考SDL2教程二evevt driven programming 🚀
四、Esp32 上播放音视频
4.1、硬件选用及芯片引脚资源使用
主要用到的硬件如下
- 开发板NodeMCU32sEsp32
- 显示屏 240*240 中景圆 ST7789
- 功放板PAM8406 数字功放板
- 扬声器3 瓦 4 欧小喇叭
- TF卡闪迪32G这里格式化采用FAT32文件系统
- TF读写模块SPI 接口、SDIO 接口二合一TF卡读写模块
- 读卡器TF 卡读卡器用于将 PC 端转码完成的视频拷贝到TF卡
- 电源模块首先通过开发板提供的电源引脚有限其次开发板供电不稳定。
- 杜邦线若干
由于 LCD
屏幕的通信方式为 SPI
所以这里 SD
卡采用 SDMMC
通信方式。
Esp32
虽然有两组SDMMC接口但Arduino core for the ESP32
中只用到了其中一组。
具体芯片引脚资源使用情况
-----------------------------------------------
| NodeMCU32s | SD Card |
| --------------------------------------------
| GPIO12 | D02(DAT2) |
| GPIO4 | D01(DAT1) |
| GPIO15 | CMD |
| GPIO2 | D00(DAT0) |
| GPIO14 | CLK |
| GPIO13 | D03(DAT3) |
| GND | GND |
| VCC(3.3v) | VCC |
-----------------------------------------------
-----------------------------------------------
| NodeMCU32s | LCD Screen |
-----------------------------------------------
| GND | GND |
| VCC(3.3v) | VCC |
| GPIO18(VSPI SCK) | SCL |
| GPIO23(VSPI MOSI) | SDA |
| GPIO26 | RES |
| GPIO27 | DC |
| GPIO5 | CS |
-----------------------------------------------
-----------------------------------------------
| NodeMCU32s | Speaker |
-----------------------------------------------
| GPIO25(DAC_1) | RIN |
| GND | GND |
| GPIO26(DAC_2) | LIN |
-----------------------------------------------
4.2、libjpeg 编解码库移植
- 下载
libjpeg
这里使用jpegsr9e.zip
官网http://www.ijg.org/ 🚀 - 解压压缩包
- 在
VS Code
项目中的lib
目录下新建一个文件夹这里命名为jpeg-9e
想起啥就起啥最好是英文 - 将压缩包中的文件复制到
VS Code
项目中的lib\jpeg-9e
目录下。
压缩包中待复制的文件
jaricom.c、jcapimin.c、jcapistd.c、jccoefct.c、jccolor.c、jcdctmgr.c、jchuff.c、jcinit.c、jcmainct.c、jcmarker.c、jcmaster.c、jcomapi.c、jcparam.c、jcprepct.c、jcsample.c、jctrans.c、jdapimin.c、jdapistd.c、jdarith.c、jdatadst.c、jdatasrc.c、jdcoefct.c、jdcolor.c、jddctmgr.c、jdhuff.c、jdinput.c、jdmainct.c、jdmarker.c、jdmaster.c、jdmerge.c、jdpostct.c、jdsample.c、jdtrans.c、jerror.c、jfdctflt.c、jfdctfst.c、jfdctint.c、jidctflt.c、jidctfst.c、jidctint.c、jmemmgr.c、jmemnobs.c、jquant1.c、jquant2.c、 jutils.c
测试代码将 example.c 中 read_JPEG_file 函数部分的代码来测试编译链接是否通过
/* example.c */
#include <stdio.h>
#include "jpeglib.h"
#include <setjmp.h>
struct my_error_mgr {
struct jpeg_error_mgr pub; /* "public" fields */
jmp_buf setjmp_buffer; /* for return to caller */
};
typedef struct my_error_mgr * my_error_ptr;
METHODDEF(void)
my_error_exit (j_common_ptr cinfo)
{
/* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
my_error_ptr myerr = (my_error_ptr) cinfo->err;
/* Always display the message. */
/* We could postpone this until after returning, if we chose. */
(*cinfo->err->output_message) (cinfo);
/* Return control to the setjmp point */
longjmp(myerr->setjmp_buffer, 1);
}
int put_scanline_someplace(JSAMPROW buffer, int row_stride)
{
/* 根据需要自己完善 */
return 0;
}
/*
* Sample routine for JPEG decompression. We assume that the source file name
* is passed in. We want to return 1 on success, 0 on error.
*/
GLOBAL(int)
read_JPEG_file (char * filename)
{
/* This struct contains the JPEG decompression parameters and pointers to
* working space (which is allocated as needed by the JPEG library).
*/
struct jpeg_decompress_struct cinfo;
/* We use our private extension JPEG error handler.
* Note that this struct must live as long as the main JPEG parameter
* struct, to avoid dangling-pointer problems.
*/
struct my_error_mgr jerr;
/* More stuff */
FILE * infile; /* source file */
JSAMPARRAY buffer; /* Output row buffer */
int row_stride; /* physical row width in output buffer */
/* In this example we want to open the input file before doing anything else,
* so that the setjmp() error recovery below can assume the file is open.
* VERY IMPORTANT: use "b" option to fopen() if you are on a machine that
* requires it in order to read binary files.
*/
if ((infile = fopen(filename, "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
return 0;
}
/* Step 1: allocate and initialize JPEG decompression object */
/* We set up the normal JPEG error routines, then override error_exit. */
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
/* Establish the setjmp return context for my_error_exit to use. */
if (setjmp(jerr.setjmp_buffer)) {
/* If we get here, the JPEG code has signaled an error.
* We need to clean up the JPEG object, close the input file, and return.
*/
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return 0;
}
/* Now we can initialize the JPEG decompression object. */
jpeg_create_decompress(&cinfo);
/* Step 2: specify data source (eg, a file) */
jpeg_stdio_src(&cinfo, infile);
/* Step 3: read file parameters with jpeg_read_header() */
(void) jpeg_read_header(&cinfo, TRUE);
/* We can ignore the return value from jpeg_read_header since
* (a) suspension is not possible with the stdio data source, and
* (b) we passed TRUE to reject a tables-only JPEG file as an error.
* See libjpeg.txt for more info.
*/
/* Step 4: set parameters for decompression */
/* In this example, we don't need to change any of the defaults set by
* jpeg_read_header(), so we do nothing here.
*/
/* Step 5: Start decompressor */
(void) jpeg_start_decompress(&cinfo);
/* We can ignore the return value since suspension is not possible
* with the stdio data source.
*/
/* We may need to do some setup of our own at this point before reading
* the data. After jpeg_start_decompress() we have the correct scaled
* output image dimensions available, as well as the output colormap
* if we asked for color quantization.
* In this example, we need to make an output work buffer of the right size.
*/
/* JSAMPLEs per row in output buffer */
row_stride = cinfo.output_width * cinfo.output_components;
/* Make a one-row-high sample array that will go away when done with image */
buffer = (*cinfo.mem->alloc_sarray)
((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
/* Step 6: while (scan lines remain to be read) */
/* jpeg_read_scanlines(...); */
/* Here we use the library's state variable cinfo.output_scanline as the
* loop counter, so that we don't have to keep track ourselves.
*/
while (cinfo.output_scanline < cinfo.output_height) {
/* jpeg_read_scanlines expects an array of pointers to scanlines.
* Here the array is only one element long, but you could ask for
* more than one scanline at a time if that's more convenient.
*/
(void) jpeg_read_scanlines(&cinfo, buffer, 1);
/* Assume put_scanline_someplace wants a pointer and sample count. */
put_scanline_someplace(buffer[0], row_stride);
}
/* Step 7: Finish decompression */
(void) jpeg_finish_decompress(&cinfo);
/* We can ignore the return value since suspension is not possible
* with the stdio data source.
*/
/* Step 8: Release JPEG decompression object */
/* This is an important step since it will release a good deal of memory. */
jpeg_destroy_decompress(&cinfo);
/* After finish_decompress, we can close the input file.
* Here we postpone it until after no more JPEG errors are possible,
* so as to simplify the setjmp error logic above. (Actually, I don't
* think that jpeg_destroy can do an error exit, but why assume anything...)
*/
fclose(infile);
/* At this point you may want to check to see whether any corrupt-data
* warnings occurred (test whether jerr.pub.num_warnings is nonzero).
*/
/* And we're done! */
return 1;
}
/* main.cpp */
#include <Arduino.h>
#define boolean libjpeg_boolean
#include "jpeglib.h"
#undef boolean
extern "C"{
GLOBAL(int) read_JPEG_file (char * filename);
}
void setup() {
// put your setup code here, to run once:
read_JPEG_file((char *)"test.avi");
}
void loop() {
// put your main code here, to run repeatedly:
}
说明 Arduino.h 和 jmorecfg.hlibjpeg库重复定义 boolean这两个头文件在 main.cpp 文件中相遇所以出现了编译错误。
头文件的作用范围为包含该头文件的文件范围内jpeglib.h 的头文件包含 jmorecfg.hmain.cpp 文件通过包含 jpeglib.h 从而间接包含 jmorecfg.h因此在 main.cpp 中采用宏定义的方式避免冲突
#define boolean libjpeg_boolean
#include "jpeglib.h"
#undef boolean
头文件在预编译的时候从上到下被顺序处理这样的话当 jpeglib.h 被包含时该头文件中的 boolean 被替换成 libjpeg_boolean当包含完后取消宏定义这样 main.cpp 之后的关于 boolean 都按照 Arduino.h 中的定义来。
说明由于 Arduino 框架采用 C++ 方式编写libjpeg 采用 C 方式编写所以依然存在 C/C++ 混合编程在链接的过程中依然会存在无法链接成功的情况这里参考上面的方式在函数声明/头文件放到
extern "C" {}
中。
如果将example.c
文件重命名成example.cpp
不会存在这个问题。libjpeg 库本身是兼容 C++ 程序调用。
4.3、LCD 库搬移
Arduino IDE
和 VSCode
配合使用通过 IDE 获取各种库文件然后在安装的目录下C:\Users\<用户名>\Documents\Arduino\libraries
找到库文件将TFT_eSPI文件夹整个复制到 VS Code
项目中的 lib
目录下。具体步骤如下
- Arduino IDE 软件中工具——>管理库…——>搜索框中输入
TFT_eSPI
——> 安装
- 安装完成后在
C:\Users\<用户名>\Documents\Arduino\libraries
目录下找到TFT_eSPI
文件夹将其复制到VS Code
项目中的lib
目录下。 - 打开
User_Setup.h
文件根据显示屏型号选择对应的驱动其余注释掉。
- 设置屏幕显示的颜色和屏幕尺寸大小
- 设置 NodeMCU32s 与显示屏通信引脚默认使用的是Esp8266所以这里需要将这部分引脚注释掉将Esp32使用的引脚打开。
- SPI通信速率这里使用ST7735推荐使用的 27MHz。
到这里完成了对显示屏驱动的搬移可通过下面的测试代码测试当前库是否能正常工作。
#include <SPI.h>
#include "TFT_eSPI.h"
TFT_eSPI tft = TFT_eSPI(240.240);
void setup() {
// 初始化LCD
tft.init(); //LCD初始化
tft.fillScreen(TFT_RED); //屏幕颜色
tft.setRotation(0); //不旋转显示角度
}
void loop()
{}
当通过 Arduino 框架实现某一个功能的时候可以先打开Arduino软件在文件 ——> 示例 ——> 寻找需要的范例通过查看范例以及修改范例实现需要的功能。
4.4、LCD 显示说明
LCD 显示部分包含视频、图像、文字、视频时长、日期、跳跃音符以及一些边框其中视频、视频时长、日期、跳跃音符是 动态变化 的在程序设计时视频和视频时长放在一个任务中而日期、跳跃音符为单独的任务这里就涉及到对 LCD 资源竞争 的情况因此加入了 互斥锁。
说明 libjpeg 解码完成后为 RGB888 的数据类型然而 LCD 显示为 RGB565 的数据类型所以在发送给 LCD 之前需要先完成 RGB888 转换为 RGB565。
uint16_t color565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
当然可以直接调用TFT_eSPI
提供的函数color565
。
这里还需要注意一点关于 16 位数据存储顺序的问题是高 8 位存储在高地址还是存储在低地址
当刷新LCD显示时通过调用TFT_eSPI
提供的函数pushImage
这个函数有 5 个重载这里使用该函数
void TFT_eSPI::pushImage(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t *data);
- 这个函数取 16 位数据其认为高 8 位在低地址低 8 位在高地址然而 color565 转换后存储在地址中是正好相反的所以需要做以下数据调整。
- 若实参中传入 data 的数据类型为 uint8_t * 则调用另一个函数所以在调用的时候需要强制类型转换。
4.5、视频时长计算
AVI 文件结构中的 Header List 信息块
中的 AVI Header
中的 Struct
里面有一个信息是 Total Frame
文件总帧数这个指的就是 图像总的帧数。
相同的 Struct
中有一个信息 SecPerFrame
视频帧间隔时间单位us通过这个信息可计算出视频的帧率也就可以计算 视频总时长。之后按照当前的帧数计算剩余时间。
// 视频时长计算公式单位ms
totsec=(aviinfo->SecPerFrame/1000)*aviinfo->TotalFrame;
4.6、Unicode 字符集与 UTF-8 编码
国际标准化组织ISO给全球所有文化使用的字母和符号进行编号对每个字符指定一个唯一的编号号码从 0x000000
到 0x10FFFF
这只是对字符进行编号但是具体怎么对每个字符进行编码没有指定。因此衍生处了 UTF-32
、UTF-16
、UTF-8
等编码方案。
vs code
默认使用 UTF-8
编码格式。
UTF-8 采用变长的编码方式它的编码有 1、2、3、4 字节长度的方式。
- 对于单字节的编码字节的第一位设为 0最高位剩余的位用来写入字符的 Unicode 编号。
- 对于 N 字节的编码第一个字节的前 N 位设为 1第 N+1 位设为 0后面字节的前两位都设为 10这 N 个字节的其余空位填充该字符的 Unicode 编号。
这里采用的字库制作软件
FontMaker-V1.2.0
通过这个软件导出的字模数量为65536
少于Unicode
字符集基本汉字在0x4E00-0x9FA5
基本汉字补充在0x9FA6-0x9FCB
符合基本需求。
/*
作用根据 UTF-8 编码值转换为 Unicode 字符集偏移量
参数编码值首地址
返回值Unicode 字符集偏移量
*/
int utf8_to_unicode(char *parm)
{
int res = 0;
if((parm[0] & 0x80) == 0x00){
res = parm[0] & 0x7F;
}
else if((parm[0] & 0xE0) == 0xC0){
res = parm[1] & 0x3F;
res |= ((parm[0] & 0x1F) << 6);
}
else if((parm[0] & 0xF0) == 0xE0){
res = parm[2] & 0x3F;
res |= ((parm[1] & 0x3F) << 6);
res |= ((parm[0] & 0x0F) << 12);
}
else if((parm[0] & 0xF8) == 0xF0){
res = parm[3] & 0x3F;
res |= ((parm[2] & 0x3F) << 6);
res |= ((parm[1] & 0x0F) << 12);
res |= ((parm[0] & 0x7) << 18);
}
else{
res = -1;
}
return res;
}
4.7、日期获取
这里日期通过访问NTP服务器
来获取因此在使用之前需要完成无线网络连接
。
Arduino
对于 NTP
的使用也有具体的示例相关的编程可以参考示例。
具体使用步骤同 <4.3 LCD 库搬移> 一致
- Arduino IDE 软件中工具——>管理库…——>搜索框中输入
NTPClient
——> 安装 - 安装完成后在
C:\Users\<用户名>\Documents\Arduino\libraries
目录下找到NTPClient
文件夹将其复制到VS Code
项目中的lib
目录下。 - 参考示例程序移植到本项目中。
说明
- NTP 服务器地址pool.ntp.org
- 时间获取前需要先切换到东八区。
- 起始年份从1900年开始所以在获得“年”后需要加上1900。
- 获得月份时需要加1。
五、使用前注意事项
5.1、Windows 上播放音视频
由于 SDL2 库路径指向 x86 的目录下动态链接库基于 x86 平台编辑的所以项目编译的时候需要将平台设置成 x86 进行编译链接。
5.2、Esp32 上播放音视频
1、在程序下载前需要断开 SD 卡供电将读卡器模块的 VCC 引脚杜邦线拔掉否则会出现报错。 【VSPI 除外】
2、打开串口终端 ——> 将读卡器模块的 VCC 引脚杜邦线拔掉 ——> 复位Esp32 ——> 串口终端上显示“请插入内存卡” ——> 插上读卡器模块的 VCC 引脚杜邦线
3、交互通过PC端串口进行
ls
—查看根目录下的文件play <文件名.avi>
— 播放视频文件
Arduino IDE 提供的文件操作接口open/read…暂不支持中文操作所以目录文件查询等操作都不支持中文这里文件名采用中文拼音。所以SD卡中的文件不要用中文命名。
六、说明
6.1、PS 构造纯色图片
文件 ——> 新建 ——> 修改大小25x25 ——> 修改颜色红色25500 ——> 确定
文件 ——> 存储为 ——> 选择JPEG ——> 保存 ——> 这里选择默认选项 ——> 确定
6.2、图片转为 LCD 显示数据类型
工具Image2Lcd
6.3、UTF-8 字库获取
当文件格式为【C文件】时可以查看每个字符所占用的字节数这里字号为 20
点阵宽度为 20x20
一行像素点数量需要为8的倍数不够自动补足所以实际点阵宽度为 24x20
则一个字符占用的存储空间大小为 24x20/8 = 60
字节。
在制作字库文件时这里使用【BIN文件】生成的文件保存到 SD 卡中通过文件读写的方式获取字符的字模。
6.4、数字字模获取
工具PCtoLCD2002
由于音频的播放计时一直在跳转所以这部分数据存储在 FLASH 中方便读取。对于字号大小和字模占用字节数的关系同上面字库制作有同样的问题。
字号20
点阵宽度20x20
由于是数字所以实际点阵大小为10x20
一行像素点数量需要为8的倍数不够自动补足所以点阵宽度为 16x20
则一个字符占用的存储空间大小为 16x20/8 = 40
字节。
/*字模显示函数宽度为 8 的倍数如果不够自动补足所以下面两个函数的效果一致*/
tft.drawBitmap(138, 154, number_one, 16, 20, TFT_WHITE);
tft.drawBitmap(138, 154, number_one, 10, 20, TFT_WHITE);
6.5、关于 dma_buf_len 和 dma_buf_count 的设置
关于 dma_buf_len 和 dma_buf_count 的工作机制
i2S 通过 DMA 发送到 DAC 模块在 i2s_driver_install 函数安装初始化的时候通过这两个参数、通道数以及sample的位数申请 DMA_buffer这个是在片内 SRAM 空间里申请的当通过 i2s_write 函数发送数据的时候可能先看哪个 DMA_buffer 空着没有空就阻塞等待将数据搬移到该 DMA_buffer 中然后 DMA 控制器将数据从 DMA_buffer 搬运到 I2S 的数据寄存器I2S 数据寄存器连接到 DAC利用 I2S 的 CK 时钟线按照采样频率将数据由 I2S 数据寄存器发送到 DAC。
当前素材音频参数
- 采样频率22050Hz
- 位深16bit
- 通道双通道
- 一帧音频数据1816 字节左右那么音频播放的时长约为20 ms音频数据量/通道数/位深/8 = 单通道样本数单通道样本数/采样频率 = 播放时长
当前素材图像参数
- 分辨率240x135
- 一帧图像处理+显示所需时间45 ms
- 从 SD 卡读取到 RAM 中所需时间3~5 ms
本程序设计的时候图像数据帧和音频数据帧之间是交错存储的测试所用的视频文件中图像帧和音频帧之比为1:4
当dma_buf_len=256
、dma_buf_count=4
时出现播放音视频时出现音频播放很~~~~~~慢~~~~~~
当将【图像帧解码+显示】部分代码注释掉只播放音频发现音频能正常播放了。
(下图所示的时长没有将 SD 卡读到 RAM 中的时间以及一些零碎的时间)
这样的话上面的 dma_buf_len
和 dma_buf_count
设置可以存储 2
帧音频帧假设这 4
帧音频数据发音为“等”
。
除了 视频帧率控制 的时长为动态调整其余部分所需的时间为固定所以如果固定的时间超过了 音频播放时长 那么导致 4 帧音频数据 之间间距较大导致出现音频播放很~~~~~~慢~~~~~~
的情况。
当dma_buf_len=256
、dma_buf_count=8
时视频和音频正常播放。
固定的时间小于 音频播放时长 加上动态时间一组时间在83ms内那么 4 帧音频数据 之间间距很小不会出现音频播放很慢的情况。
如果内存比较紧张可以尝试通过 提高主频的方式 🚀减少视频解码所需要的时间这样的话就能减小固定部分的时长。
素材视频文件中根据帧率可知一帧图像数据的时长为 83ms图像帧与音频帧的数量比为14而4帧音频帧的可播放时长约为80ms83ms和80ms很接近想必不是巧合。