1. SDL-Windows 平台开发简介
1.1 SDL 简介
SDL,全称 Simple DirectMedia Layer,是一套跨平台的开发库,专门用于开发具有图形渲染的应用,如游戏、模拟器、媒体播放器等。由于其简单、直接的接口和高效的性能,SDL 在游戏开发和媒体处理中得到了广泛的应用。
SDL 提供了对图形硬件、音频、网络、事件、定时器等的底层访问。此外,SDL 还提供了对 2D 图形渲染、音频混合、文件系统访问、线程管理和其他相关功能的支持。SDL 对 OpenGL 和 Direct3D 这两种主流的图形接口都提供了支持,使得开发者可以方便地开发 2D 和 3D 应用。
SDL 的设计目标是提供一套简单、高效、稳定且可移植的开发接口。因此,SDL 用 C 语言编写,可以在几乎所有的平台上运行,包括 Windows、Mac OS X、Linux、iOS 和 Android 等。SDL 的开源性质也让开发者可以自由地使用、修改和分发 SDL,使得 SDL 成为了游戏和媒体开发者的重要工具。
1.2 SDL 主要功能模块介绍
- 视频: SDL 提供了对各种图形硬件的抽象访问,使得开发者可以更方便地创建窗口、处理窗口事件、以及进行图形渲染。此外,SDL 还支持 OpenGL 和 Direct3D,方便开发者创建2D或3D的图形程序。
- 音频: SDL 为音频处理提供了强大的支持,包括音频播放、音频录制、音频混合以及对多种音频格式的支持。开发者可以用 SDL 来播放 PCM 数据,也可以使用 SDL_mixer 扩展库来播放 MP3、WAV 等格式的音频。
- 输入: SDL 提供了一套完善的事件处理机制,支持键盘、鼠标、触摸屏以及游戏手柄等多种输入设备。此外,SDL 还支持剪贴板操作,以及对 Unicode 文本的输入。
- 线程: SDL 包含了一套线程操作的 API,包括创建线程、线程同步以及线程间的通信。这些功能使得 SDL 可以支持多线程编程。
- 定时器: SDL 提供了获取和操作时间的功能,包括获取当前时间、延时等待、设置定时器等。这些功能对于游戏开发和动画效果的实现非常有用。
- 文件系统: SDL 提供了一套简单的文件访问接口,支持读写本地文件,也支持通过 HTTP 或者 FTP 访问网络文件。此外,SDL 还支持对 ZIP 和 LZMA 压缩文件的访问。
2. Window 显示
2.1 创建窗口
SDL (Simple DirectMedia Layer) 提供了用于处理窗口的 API,使得开发者可以方便地创建窗口、调整窗口大小、处理窗口事件等。下面是一些关于 SDL 中窗口显示的详细介绍:
-
创建窗口: 使用
SDL_CreateWindow函数可以创建一个窗口。这个函数需要指定窗口的标题、初始位置、初始大小以及一些窗口标志。例如:SDL_Window *window = SDL_CreateWindow("Hello, SDL", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 800, 600, SDL_WINDOW_SHOWN);这个函数将创建一个初始大小为 800x600、标题为 "Hello, SDL" 的窗口。
-
窗口大小和位置: SDL 提供了
SDL_SetWindowSize、SDL_GetWindowSize、SDL_SetWindowPosition、SDL_GetWindowPosition等函数来设置和获取窗口的大小和位置。 -
窗口状态: 使用
SDL_SetWindowFullscreen、SDL_ShowWindow、SDL_HideWindow、SDL_MaximizeWindow、SDL_MinimizeWindow等函数可以改变窗口的状态。例如,可以将窗口设置为全屏模式,或者隐藏窗口。 -
窗口事件: SDL 提供了一套事件处理机制,可以用来处理窗口事件。例如,可以通过
SDL_Event结构体和SDL_PollEvent函数来处理窗口关闭事件:SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { // handle window close event } } -
销毁窗口: 当不再需要一个窗口时,可以使用
SDL_DestroyWindow函数来销毁它。
以上就是 SDL 中处理窗口的一些基本操作,通过这些操作,开发者可以方便地在应用程序中显示和控制窗口。
2.2 渲染纹理
在 SDL 中,渲染纹理是一个很常见的操作。纹理实际上就是显存中的一块区域,可以用来保存图像数据。在我们需要将图像数据展示到窗口上时,就可以将这个纹理渲染到窗口上。
首先,我们需要一个 SDL_Renderer 对象来进行渲染操作。当我们创建窗口时,可以使用 SDL_CreateRenderer 函数来创建一个 SDL_Renderer 对象:
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
然后,我们可以使用 SDL_CreateTexture 函数来创建一个纹理:
SDL_Texture *texture = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
width,
height);
这个函数需要指定纹理的像素格式、访问方式以及纹理的宽度和高度。
当我们需要将图像数据展示到窗口上时,就可以使用 SDL_RenderCopy 函数将纹理渲染到窗口上:
SDL_RenderCopy(renderer, texture, NULL, NULL);
最后,我们需要使用 SDL_RenderPresent 函数来更新窗口:
SDL_RenderPresent(renderer);
以上就是在 SDL 中创建和渲染纹理的基本步骤。需要注意的是,当我们不再需要一个纹理时,应该使用 SDL_DestroyTexture 函数来销毁它。同样,当我们不再需要一个 SDL_Renderer 对象时,应该使用 SDL_DestroyRenderer 函数来销毁它。下面给出一个完整示例:
#include <stdio.h>
#include <SDL.h>
#undef main
int main()
{
int run = 1;
SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
SDL_Texture *texture = NULL;
SDL_Rect rect; // 长方形,原点在左上角
rect.w = 50; //方块大小
rect.h = 50;
SDL_Init(SDL_INIT_VIDEO);//初始化函数,可以确定希望激活的子系统
window = SDL_CreateWindow("2 Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
640,
480,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);// 创建窗口
if (!window)
{
return -1;
}
renderer = SDL_CreateRenderer(window, -1, 0);//基于窗口创建渲染器
if (!renderer)
{
return -1;
}
texture = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
640,
480); //创建纹理
if (!texture)
{
return -1;
}
int show_count = 0;
while (run)
{
rect.x = rand() % 600;
rect.y = rand() % 400;
SDL_SetRenderTarget(renderer, texture); // 设置渲染目标为纹理
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); // 纹理背景为黑色
SDL_RenderClear(renderer); //清屏
SDL_RenderDrawRect(renderer, &rect); //绘制一个长方形
SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255); //长方形为白色
SDL_RenderFillRect(renderer, &rect);
SDL_SetRenderTarget(renderer, NULL); //恢复默认,渲染目标为窗口
SDL_RenderCopy(renderer, texture, NULL, NULL); //拷贝纹理到CPU
SDL_RenderPresent(renderer); //输出到目标窗口上
SDL_Delay(300);
if(show_count++ > 30)
{
run = 0; // 不跑了
}
}
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window); //销毁窗口
SDL_Quit();
return 0;
}
3. Event 事件
在 SDL 中,事件(Event)是一种重要的输入处理机制。通过事件,程序可以接收到来自用户的各种输入,如键盘按键、鼠标移动、窗口关闭等。以下是 SDL 事件处理的基本介绍:
首先需要创建一个 SDL_Event 类型的变量来接收事件数据。这个变量通常在主循环中创建,如:
SDL_Event event;
然后可以使用 SDL_PollEvent 函数来获取一个事件。这个函数会检查事件队列中是否有事件,如果有,就将事件数据填充到提供的 SDL_Event 结构中,并从事件队列中删除这个事件。如果没有事件,这个函数就立即返回。因此,你通常需要在一个循环中调用这个函数,如:
while (SDL_PollEvent(&event)) {
// handle event
}
接下来可以根据 SDL_Event 结构的 type 成员来判断事件的类型,然后进行相应的处理。例如,你可以这样处理键盘按键事件:
if (event.type == SDL_KEYDOWN) {
switch (event.key.keysym.sym) {
case SDLK_LEFT:
// handle left arrow key
break;
case SDLK_RIGHT:
// handle right arrow key
break;
// ...
}
}
或者这样处理窗口关闭事件:
if (event.type == SDL_QUIT) {
// handle window close event
}
以上就是 SDL 中处理事件的基本方法。SDL 支持多种类型的事件,如键盘事件、鼠标事件、窗口事件、游戏手柄事件等。通过这些事件可以接收到用户的各种输入,并在程序中进行相应的处理。下面给出一个完整实例。
#include <SDL.h>
#include <stdio.h>
#define FF_QUIT_EVENT (SDL_USEREVENT + 2) // 用户自定义事件
#undef main
int main(int argc, char* argv[])
{
SDL_Window *window = NULL; // Declare a pointer
SDL_Renderer *renderer = NULL;
SDL_Init(SDL_INIT_VIDEO); // Initialize SDL2
// Create an application window with the following settings:
window = SDL_CreateWindow(
"An SDL2 window", // window title
SDL_WINDOWPOS_UNDEFINED, // initial x position
SDL_WINDOWPOS_UNDEFINED, // initial y position
640, // width, in pixels
480, // height, in pixels
SDL_WINDOW_SHOWN | SDL_WINDOW_BORDERLESS// flags - see below
);
// Check that the window was successfully created
if (window == NULL)
{
// In the case that the window could not be made...
printf("Could not create window: %s\n", SDL_GetError());
return 1;
}
/* We must call SDL_CreateRenderer in order for draw calls to affect this window. */
renderer = SDL_CreateRenderer(window, -1, 0);
/* Select the color for drawing. It is set to red here. */
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
/* Clear the entire screen to our selected color. */
SDL_RenderClear(renderer);
/* Up until now everything was drawn behind the scenes.
This will show the new, red contents of the window. */
SDL_RenderPresent(renderer);
SDL_Event event;
int b_exit = 0;
for (;;)
{
SDL_WaitEvent(&event);
switch (event.type)
{
case SDL_KEYDOWN: /* 键盘事件 */
switch (event.key.keysym.sym)
{
case SDLK_a:
printf("key down a\n");
break;
case SDLK_s:
printf("key down s\n");
break;
case SDLK_d:
printf("key down d\n");
break;
case SDLK_q:
printf("key down q and push quit event\n");
SDL_Event event_q;
event_q.type = FF_QUIT_EVENT;
SDL_PushEvent(&event_q);
break;
default:
printf("key down 0x%x\n", event.key.keysym.sym);
break;
}
break;
case SDL_MOUSEBUTTONDOWN: /* 鼠标按下事件 */
if (event.button.button == SDL_BUTTON_LEFT)
{
printf("mouse down left\n");
}
else if(event.button.button == SDL_BUTTON_RIGHT)
{
printf("mouse down right\n");
}
else
{
printf("mouse down %d\n", event.button.button);
}
break;
case SDL_MOUSEMOTION: /* 鼠标移动事件 */
printf("mouse movie (%d,%d)\n", event.button.x, event.button.y);
break;
case FF_QUIT_EVENT:
printf("receive quit event\n");
b_exit = 1;
break;
}
if(b_exit)
break;
}
//destory renderer
if (renderer)
SDL_DestroyRenderer(renderer);
// Close and destroy the window
if (window)
SDL_DestroyWindow(window);
// Clean up
SDL_Quit();
return 0;
}
4. Thread
SDL 中的多线程处理是通过 SDL_Thread 来完成的。多线程能让程序同时处理多个任务,这对于需要实时响应用户输入或处理大量数据的程序非常重要。以下是 SDL 中使用线程的基本步骤:
首先需要定义一个函数来作为线程的入口点。这个函数必须接受一个 void * 类型的参数,并返回一个 int:
int MyThread(void *data) {
// do something
return 0;
}
然后可以使用 SDL_CreateThread 函数来创建一个线程:
SDL_Thread *thread = SDL_CreateThread(MyThread, "MyThread", (void *)NULL);
这个函数需要指定线程入口点函数、线程的名称以及传递给线程的参数。线程创建后将开始运行。
如果你需要等待一个线程结束,可以使用 SDL_WaitThread 函数:
SDL_WaitThread(thread, NULL);
这个函数将阻塞当前线程,直到指定的线程结束。如果你需要获取线程的返回值,可以提供一个 int * 类型的参数。
在多线程环境中,数据访问往往需要同步。SDL 提供了几种同步机制,如互斥量(SDL_mutex)、条件变量(SDL_cond)等。例如,你可以这样创建和使用互斥量:
SDL_mutex *mutex = SDL_CreateMutex();
SDL_LockMutex(mutex);
// critical section
SDL_UnlockMutex(mutex);
SDL_DestroyMutex(mutex);
SDL 还提供了 SDL_Semaphore 类型和一系列函数来支持信号量的使用。以下是在 SDL 中使用同步信号量的基本步骤:
首先使用 SDL_CreateSemaphore 函数来创建一个信号量:
SDL_sem *sem = SDL_CreateSemaphore(initial_value);
这个函数接受一个参数,表示信号量的初始值,即可用的资源数量。
当一个线程需要使用资源时,它应该使用 SDL_SemWait 或 SDL_SemTryWait 函数来等待信号量:
SDL_SemWait(sem); // wait until the semaphore is positive
SDL_SemTryWait(sem); // try to wait and return immediately
SDL_SemWait 会阻塞当前线程,直到信号量变为正。SDL_SemTryWait 会立即返回,如果信号量是正的,它就将信号量减一并返回0,否则就返回非零值。
当一个线程使用完资源后,它应该使用 SDL_SemPost 函数来增加信号量:
SDL_SemPost(sem); // increase the semaphore
这个函数将信号量加一,并唤醒等待队列中的一个线程。
当你不再需要一个信号量时,应该使用 SDL_DestroySemaphore 函数来销毁它:
SDL_DestroySemaphore(sem);
以上就是在 SDL 中使用信号量的基本方法。使用信号量时需要注意,每次 SDL_SemWait 或 SDL_SemTryWait 成功后,都应该有对应的 SDL_SemPost。否则,信号量可能会永远变为非正,导致其它线程无法继续执行。多线程编程需要注意数据同步和线程安全问题,否则可能会出现数据错误或程序崩溃等问题。下面给出一个完整实例。
#include <SDL.h>
#include <stdio.h>
SDL_mutex *s_lock = NULL;
SDL_cond *s_cond = NULL;
int thread_work(void *arg)
{
SDL_LockMutex(s_lock);
printf(" <============thread_work sleep\n");
sleep(10); // 用来测试获取锁
printf(" <============thread_work wait\n");
// 释放s_lock资源,并等待signal。之所以释放s_lock是让别的线程能够获取到s_lock
SDL_CondWait(s_cond, s_lock); //另一个线程(1)发送signal和(2)释放lock后,这个函数退出
printf(" <===========thread_work receive signal, continue to do ~_~!!!\n");
printf(" <===========thread_work end\n");
SDL_UnlockMutex(s_lock);
return 0;
}
#undef main
int main()
{
s_lock = SDL_CreateMutex();
s_cond = SDL_CreateCond();
SDL_Thread * t = SDL_CreateThread(thread_work,"thread_work",NULL);
if(!t)
{
printf(" %s",SDL_GetError);
return -1;
}
for(int i = 0;i< 2;i++)
{
sleep(2);
printf("main execute =====>\n");
}
printf("main SDL_LockMutex(s_lock) before ====================>\n");
SDL_LockMutex(s_lock); // 获取锁,但是子线程还拿着锁
printf("main ready send signal====================>\n");
printf("main SDL_CondSignal(s_cond) before ====================>\n");
SDL_CondSignal(s_cond); // 发送信号,唤醒等待的线程
printf("main SDL_CondSignal(s_cond) after ====================>\n");
sleep(10);
SDL_UnlockMutex(s_lock);// 释放锁,让其他线程可以拿到锁
printf("main SDL_UnlockMutex(s_lock) after ====================>\n");
SDL_WaitThread(t, NULL);
SDL_DestroyMutex(s_lock);
SDL_DestroyCond(s_cond);
return 0;
}
5. YUV 播放
在 SDL 中,播放 YUV 视频需要经过以下几个步骤:
-
初始化 SDL: 首先,我们需要初始化 SDL 的视频子系统。这可以通过调用
SDL_Init()函数并传入SDL_INIT_VIDEO常量来完成:if (SDL_Init(SDL_INIT_VIDEO) < 0) { printf("Could not initialize SDL - %s\n", SDL_GetError()); return -1; }
SDL_Init() 函数的作用是初始化 SDL 库的各种子系统。它的参数可以是一个或多个子系统的常量,通过位或运算(|)组合在一起。SDL_INIT_AUDIO 是其中之一,表示初始化音频子系统。其他常量还包括 SDL_INIT_VIDEO(初始化视频子系统)、SDL_INIT_TIMER(初始化定时器子系统)等。
如果 SDL_Init(SDL_INIT_AUDIO) 执行成功,会返回 0;如果失败,会返回 -1,并通过 SDL_GetError() 函数返回错误信息。
-
创建窗口: 接着,我们需要创建一个窗口来显示视频。这可以通过调用
SDL_CreateWindow()函数来完成:SDL_Window *window = SDL_CreateWindow("SDL Video Player", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, SDL_WINDOW_OPENGL); if (!window) { printf("Could not create window - %s\n", SDL_GetError()); return -1; }这个函数需要指定窗口的标题、初始位置、初始大小以及一些窗口选项。
-
创建渲染器: 有了窗口之后,我们还需要创建一个渲染器来进行绘图。这可以通过调用
SDL_CreateRenderer()函数来完成:SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); if (!renderer) { printf("Could not create renderer - %s\n", SDL_GetError()); return -1; }这个函数需要指定关联的窗口、渲染器的索引(-1表示选择第一个支持指定选项的渲染器)以及渲染器选项。
-
创建纹理: 接着,我们需要创建一个纹理来存储视频帧。这可以通过调用
SDL_CreateTexture()函数来完成:SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, width, height); if (!texture) { printf("Could not create texture - %s\n", SDL_GetError()); return -1; }这个函数需要指定关联的渲染器、像素格式(YUV视频通常使用
SDL_PIXELFORMAT_IYUV或SDL_PIXELFORMAT_YV12)、纹理访问方式以及纹理的大小。 -
显示视频帧: 最后,我们可以通过
SDL_UpdateTexture()和SDL_RenderCopy()函数来更新纹理并将其复制到渲染器,然后使用SDL_RenderPresent()函数来显示视频帧:SDL_UpdateTexture(texture, NULL, pixels, pitch); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, NULL, NULL); SDL_RenderPresent(renderer);这里的
pixels和pitch应该从你的视频解码器获取。SDL_UpdateTexture()函数用于将视频帧数据更新到纹理,SDL_RenderCopy()函数用于将纹理复制到渲染器,SDL_RenderPresent()函数用于更新窗口的显示内容。
以上就是在 SDL 中播放 YUV 视频的基本流程。请注意,这个流程只适用于本地视频播放。如果你需要进行流媒体播放或者使用其他视频格式,可能需要进行相应的修改。
#include <stdio.h>
#include <string.h>
#include <SDL.h>
//自定义消息类型
#define REFRESH_EVENT (SDL_USEREVENT + 1) // 请求画面刷新事件
#define QUIT_EVENT (SDL_USEREVENT + 2) // 退出事件
//定义分辨率
// YUV像素分辨率
#define YUV_WIDTH 320
#define YUV_HEIGHT 240
//定义YUV格式
#define YUV_FORMAT SDL_PIXELFORMAT_IYUV
int s_thread_exit = 0; // 退出标志 = 1则退出
int refresh_video_timer(void *data)
{
while (!s_thread_exit)
{
SDL_Event event;
event.type = REFRESH_EVENT;
SDL_PushEvent(&event);
SDL_Delay(40);
}
s_thread_exit = 0;
//push quit event
SDL_Event event;
event.type = QUIT_EVENT;
SDL_PushEvent(&event);
return 0;
}
#undef main
int main(int argc, char* argv[])
{
//初始化 SDL
if(SDL_Init(SDL_INIT_VIDEO))
{
fprintf( stderr, "Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
// SDL
SDL_Event event; // 事件
SDL_Rect rect; // 矩形
SDL_Window *window = NULL; // 窗口
SDL_Renderer *renderer = NULL; // 渲染
SDL_Texture *texture = NULL; // 纹理
SDL_Thread *timer_thread = NULL; // 请求刷新线程
uint32_t pixformat = YUV_FORMAT; // YUV420P,即是SDL_PIXELFORMAT_IYUV
// 分辨率
// 1. YUV的分辨率
int video_width = YUV_WIDTH;
int video_height = YUV_HEIGHT;
// 2.显示窗口的分辨率
int win_width = YUV_WIDTH;
int win_height = YUV_WIDTH;
// YUV文件句柄
FILE *video_fd = NULL;
const char *yuv_path = "yuv420p_320x240.yuv";
size_t video_buff_len = 0;
uint8_t *video_buf = NULL; //读取数据后先把放到buffer里面
// 我们测试的文件是YUV420P格式
uint32_t y_frame_len = video_width * video_height;
uint32_t u_frame_len = video_width * video_height / 4;
uint32_t v_frame_len = video_width * video_height / 4;
uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len;
//创建窗口
window = SDL_CreateWindow("Simplest YUV Player",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
video_width, video_height,
SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);
if(!window)
{
fprintf(stderr, "SDL: could not create window, err:%s\n",SDL_GetError());
goto _FAIL;
}
// 基于窗口创建渲染器
renderer = SDL_CreateRenderer(window, -1, 0);
// 基于渲染器创建纹理
texture = SDL_CreateTexture(renderer,
pixformat,
SDL_TEXTUREACCESS_STREAMING,
video_width,
video_height);
// 分配空间
video_buf = (uint8_t*)malloc(yuv_frame_len);
if(!video_buf)
{
fprintf(stderr, "Failed to alloce yuv frame space!\n");
goto _FAIL;
}
// 打开YUV文件
video_fd = fopen(yuv_path, "rb");
if( !video_fd )
{
fprintf(stderr, "Failed to open yuv file\n");
goto _FAIL;
}
// 创建请求刷新线程
timer_thread = SDL_CreateThread(refresh_video_timer,
NULL,
NULL);
while (1)
{
// 收取SDL系统里面的事件
SDL_WaitEvent(&event);
if(event.type == REFRESH_EVENT) // 画面刷新事件
{
video_buff_len = fread(video_buf, 1, yuv_frame_len, video_fd);
if(video_buff_len <= 0)
{
fprintf(stderr, "Failed to read data from yuv file!\n");
goto _FAIL;
}
// 设置纹理的数据 video_width = 320, plane
SDL_UpdateTexture(texture, NULL, video_buf, video_width);
// 显示区域,可以通过修改w和h进行缩放
rect.x = 0;
rect.y = 0;
float w_ratio = win_width * 1.0 /video_width;
float h_ratio = win_height * 1.0 /video_height;
// 320x240 怎么保持原视频的宽高比例
rect.w = video_width * w_ratio;
rect.h = video_height * h_ratio;
// rect.w = video_width * 0.5;
// rect.h = video_height * 0.5;
// 清除当前显示
SDL_RenderClear(renderer);
// 将纹理的数据拷贝给渲染器
SDL_RenderCopy(renderer, texture, NULL, &rect);
// 显示
SDL_RenderPresent(renderer);
}
else if(event.type == SDL_WINDOWEVENT)
{
//If Resize
SDL_GetWindowSize(window, &win_width, &win_height);
printf("SDL_WINDOWEVENT win_width:%d, win_height:%d\n",win_width,
win_height );
}
else if(event.type == SDL_QUIT) //退出事件
{
s_thread_exit = 1;
}
else if(event.type == QUIT_EVENT)
{
break;
}
}
_FAIL:
s_thread_exit = 1; // 保证线程能够退出
// 释放资源
if(timer_thread)
SDL_WaitThread(timer_thread, NULL); // 等待线程退出
if(video_buf)
free(video_buf);
if(video_fd)
fclose(video_fd);
if(texture)
SDL_DestroyTexture(texture);
if(renderer)
SDL_DestroyRenderer(renderer);
if(window)
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
6. PCM 声音播放
-
初始化 SDL: 和播放视频一样,我们首先需要初始化 SDL 的音频子系统。这可以通过调用
SDL_Init()函数并传入SDL_INIT_AUDIO常量来完成。if (SDL_Init(SDL_INIT_AUDIO) < 0) { printf("Could not initialize SDL - %s\n", SDL_GetError()); return -1; } -
设定音频参数: 播放 PCM 数据需要知道一些关键的音频参数,例如采样率(Sample Rate)、采样格式(Sample Format)、声道数(Channels)、采样大小(Sample Size)。这些参数将在
SDL_AudioSpec结构体中设定。SDL_AudioSpec wantSpec; wantSpec.freq = SAMPLE_RATE; // e.g., 44100 wantSpec.format = AUDIO_S16SYS; // e.g., AUDIO_S16SYS wantSpec.channels = CHANNELS; // e.g., 2 wantSpec.silence = 0; wantSpec.samples = 1024; // Good low-latency value for callback wantSpec.callback = fill_audio; // your audio callback function wantSpec.userdata = pcmData; // pointer to your PCM data -
打开音频设备: 在设定好音频参数之后,我们可以使用
SDL_OpenAudio()或SDL_OpenAudioDevice()函数打开音频设备。if (SDL_OpenAudio(&wantSpec, &haveSpec) < 0) { printf("Failed to open audio: %s\n", SDL_GetError()); return -1; }这里的
haveSpec结构体将存储系统实际使用的音频参数,可能与wantSpec中设定的不同。因此,在播放音频时,应该按照haveSpec中的参数进行。 -
开始播放音频: 一旦音频设备打开,我们可以调用
SDL_PauseAudio()函数开始播放音频。SDL_PauseAudio(0); // start playing audio在这个过程中,SDL 会在需要更多音频数据时调用我们在
SDL_AudioSpec结构体中设定的回调函数。 -
在回调函数中填充音频数据: 回调函数应该是这样的:
void fill_audio(void *udata, Uint8 *stream, int len) { // 'stream' is the audio output buffer, 'len' is the length // 'udata' is a pointer to the audio data source ... }在这个函数中,我们需要将音频数据从
udata复制到stream,长度为len。请注意,这个函数会在音频线程中被调用,因此在此函数中应该避免执行耗时的操作。
以上就是在 SDL 中播放 PCM 数据的基本步骤。使用这个流程,你应该能够播放大多数常见的 PCM 格式的音频。但是,如果你需要播放其他格式的音频(例如 MP3 或 AAC),你可能需要使用额外的库进行解码,然后将解码后的 PCM 数据传递给 SDL 进行播放。
#include <stdio.h>
#include <SDL.h>
// 每次读取2帧数据, 以1024个采样点一帧 2通道 16bit采样点为例
#define PCM_BUFFER_SIZE (1024*2*2*2)
// 音频PCM数据缓存
static Uint8 *s_audio_buf = NULL;
// 目前读取的位置
static Uint8 *s_audio_pos = NULL;
// 缓存结束位置
static Uint8 *s_audio_end = NULL;
//音频设备回调函数
void fill_audio_pcm(void *udata, Uint8 *stream, int len)
{
SDL_memset(stream, 0, len);
if(s_audio_pos >= s_audio_end) // 数据读取完毕
{
return;
}
// 数据够了就读预设长度,数据不够就只读部分(不够的时候剩多少就读取多少)
int remain_buffer_len = s_audio_end - s_audio_pos;
len = (len < remain_buffer_len) ? len : remain_buffer_len;
// 拷贝数据到stream并调整音量
SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME/8);
printf("len = %d\n", len);
s_audio_pos += len; // 移动缓存指针
}
// 提取PCM文件
// ffmpeg -i input.mp4 -t 20 -codec:a pcm_s16le -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
// 测试PCM文件
// ffplay -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
#undef main
int main(int argc, char *argv[])
{
int ret = -1;
FILE *audio_fd = NULL;
SDL_AudioSpec spec;
const char *path = "44100_16bit_2ch.pcm";
// 每次缓存的长度
size_t read_buffer_len = 0;
//SDL initialize
if(SDL_Init(SDL_INIT_AUDIO)) // 支持AUDIO
{
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
return ret;
}
//打开PCM文件
audio_fd = fopen(path, "rb");
if(!audio_fd)
{
fprintf(stderr, "Failed to open pcm file!\n");
goto _FAIL;
}
s_audio_buf = (uint8_t *)malloc(PCM_BUFFER_SIZE);
// 音频参数设置SDL_AudioSpec
spec.freq = 44100; // 采样频率
spec.format = AUDIO_S16SYS; // 采样点格式
spec.channels = 2; // 2通道
spec.silence = 0;
spec.samples = 1024; // 23.2ms -> 46.4ms 每次读取的采样数量,多久产生一次回调和 samples
spec.callback = fill_audio_pcm; // 回调函数
spec.userdata = NULL;
//打开音频设备
if(SDL_OpenAudio(&spec, NULL))
{
fprintf(stderr, "Failed to open audio device, %s\n", SDL_GetError());
goto _FAIL;
}
//play audio
SDL_PauseAudio(0);
int data_count = 0;
while(1)
{
// 从文件读取PCM数据
read_buffer_len = fread(s_audio_buf, 1, PCM_BUFFER_SIZE, audio_fd);
if(read_buffer_len == 0)
{
break;
}
data_count += read_buffer_len; // 统计读取的数据总字节数
printf("now playing %10d bytes data.\n",data_count);
s_audio_end = s_audio_buf + read_buffer_len; // 更新buffer的结束位置
s_audio_pos = s_audio_buf; // 更新buffer的起始位置
//the main thread wait for a moment
while(s_audio_pos < s_audio_end)
{
SDL_Delay(10); // 等待PCM数据消耗
}
}
printf("play PCM finish\n");
// 关闭音频设备
SDL_CloseAudio();
_FAIL:
//release some resources
if(s_audio_buf)
free(s_audio_buf);
if(audio_fd)
fclose(audio_fd);
//quit SDL
SDL_Quit();
return 0;
}