【音视频开发】第四章 SDL音视频渲染
文章目录
一、简介
1.什么是 SDL
SDL(Simple DirectMedia Layer)是一个使用 C 语言开发的开源跨平台开发库,它主要用来直接访问操作系统底层的硬件资源,比如:
- 图形(Graphics)
- 音频(Audio)
- 输入设备(键盘、鼠标、手柄)
- 窗口管理
- 多线程(Threading)
- 文件 IO(File I/O)
典型用途
- 2D 游戏(平台跳跃、射击类)
- 模拟器(NES、SNES、GameBoy 等复古游戏模拟器)
- 图形工具或小型 UI 框架
- 作为大型游戏引擎(如 Unity)或自研引擎的底层组件之一
二、Windows 环境搭建
下载地址:https://github.com/libsdl-org/SDL/releases
创建一个 C 项目
将刚刚下载完成的 SDL 压缩包解压,复制到项目根目录中,并将路径配置到.pro文件
将 SDL2-2.32.4\lib\x64 目录下的 SDL2.dll 文件复制到项目根目录中,编写简单的窗口创建程序
#include <stdio.h>
#include <SDL.h>
#undef main
int main()
{
printf("Hello SDL World!\n");
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
printf("SDL_Init Error: %s\n", SDL_GetError());
return 1;
}
SDL_Window *window = SDL_CreateWindow("Basic SDL Window",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
640,
480,
SDL_WINDOW_SHOWN);
if (!window) {
printf("Can't create window, err: %s\n", SDL_GetError());
SDL_Quit();
return 1;
}
// 添加事件循环,保持窗口直到关闭
SDL_Event event;
int running = 1;
while (running) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = 0;
}
}
SDL_Delay(16); // 简单的帧延迟
}
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
三、SDL 子系统
在 SDL(Simple DirectMedia Layer)中,子系统(Subsystem)就是 SDL 提供的某一类功能模块的集合,你可以把它理解成“功能插件”或“功能模块”。这些子系统是独立的,你可以按需开启,避免加载不必要的内容,提升效率。
- SDL_INIT_TIMER:定时器功能(SDL_GetTicks)
- SDL_INIT_AUDIO:音频子系统
- SDL_INIT_VIDEO:视频子系统(窗口、OpenGL、Vulkan)
- SDL_INIT_JOYSTICK:游戏摇杆(Joystick)
- SQL_INIT_HAPTIC:触觉反馈(震动设备)
- SDL_INIT_GAMECONTROLLER:手柄控制器(比 joystick 更高级)
- SDL_INIT_EVENTS:SDL 事件系统(键盘/鼠标等)
- SDL_INIT_SENSOR:传感器(如陀螺仪)
- SDL_INIT_EVERYTHING:初始化所有子系统
四、Window 显示
1.SDL 视频显示函数简介
- SDL_Init():初始化 SDL 系统
- SDL_CreateWindow():创建窗口 SDL_Window
- SDL_CreateRender():创建渲染器 SDL_Renderer
- SDL_CreateTexture():创建纹理 SDL_Texture
- SDL_UpdateTexture():设置纹理的数据
- SDL_RenderCopy():将纹理的数据拷贝给渲染器
- SDL_RenderPresent():显示
- SDL_Delay():工具函数,用于延时
- SDL_Quit():退出 SDL 系统
2.窗口渲染结构体
- SDL_Window:表示一个窗口(由 SDL_CreateWindow() 创建)
- SDL_Render:渲染器,用于在窗口上绘制图形
- SDL_Texture:纹理,加载图像后用于渲染
- SDL_Surface:表示一块像素数据,可用于直接操作图像
- SDL_Rect:表示一个矩形区域(常用于位置和尺寸)
五、SDL 事件
SDL 的事件系统是通过轮询或等待来处理用户输入、窗口变化、系统通知等各种操作的核心机制。
SDL 的所有事件都通过一个统一的 SDL_Event 结构体表示,然后通过 event.type 判断它属于哪种事件类型。
1.通用事件结构体 SDL_Event
SDL_Event event;
while(SDL_PollEvent(&event)){
switch(event.type){
case SDL_QUIT:
//处理退出
break;
//其他事件...
}
}
2.事件类型
- 退出事件:SDL_QUIT
- 窗口事件:SDL_WINDOWEVENT + event.window.event
- 键盘事件:SDL_KEYDOWN, SDL_KEYUP
- 鼠标事件:SDL_MOUSEMOTION, SDL_MOUSEBUTTONDOWN, SDL_MOUSEBUTTONUP, SQL_MOUSEWHEEL
- 控制器:SQL_CONTROLLER… 系列
- 自定义事件:SQL_USEREVENT
六、SDL 线程
SDL 提供了跨平台的线程创建与管理接口,让开发者在不同系统下都能一致地创建和管理线程。
1.常用线程相关 API
创建线程
SQL_Thread* SDL_CreateThread(SDL_ThreadFunction fn, const char *name, void *data);
- fn:线程函数,函数签名为 int thread_func(void *data)
- name:线程名称
- data:传递给线程函数的参数
- 返回值:成功返回 SDL_Thread*,失败返回 NULL
等待线程结束
void SDL_WaitThread(SDL_Thread* thread, int *status);
等待指定线程结束,并获取其返回值
获取当前线程 ID
SDL_threadID SDL_ThreadID(void);
获取线程名
const char* SDL_GetThreadName(SDL_Thread* thread);
互斥锁(Mutex)
SDL_mutex* SDL_CreateMutex();
void SDL_DestroyMutex(SDL_mutex* mutex);
int SDL_LockMutex(SDL_mutex* mutex);
int SDL_UnlockMutex(SDL_mutex* mutex);
七、YUV 显示
使用 SDL 显示 YUV 图像是视频播放中非常常见的一种技术路径。
1.代码示例
- 读取并显示 .yuv 原始视频文件(YUV420P格式)
- SDL 窗口播放视频帧
- 每秒大约播放 25 帧
#include <SDL2/SDL.h>
#include <stdio.h>
// 视频宽度和高度
#define WIDTH 640
#define HEIGHT 480
// YUV 文件路径
#define FILE_PATH "test.yuv"
// 每帧字节数(YUV420P:Y 占 w*h,U/V 各占 w*h/4)
#define FRAME_SIZE(WIDTH * HEIGHT * 3 / 2)
int main(int argc, char* argv[]){
// 初始化 SDL 视频系统
if(SDL_Init(SDL_INIT_VIDEO) < 0){
print("SDL 初始化失败:%s\n", SDL_GetError());
return -1;
}
// 创建 SDL 窗口
SDL_Window* window = SDL_CreateWindow("YUV 显示示例", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WIDTH, HEIGHT, SDL_WINDOW_SHOWN);
if(!window){
printf("窗口创建失败:%s\n", SDL_GetError());
SDL_Quit();
return -1;
}
// 创建渲染器
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, 0);
// 创建 YUV4200P 纹理
SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
// 打开 .yuv 文件
FILE* file = fopen(FILE_PATH, "rb");
if(!file){
printf("无法打开文件:%s\n", FILE_PATH);
return -1;
}
// 分配一帧缓冲区
Uint8* buffer = (Uint8*)malloc(FRAME_SIZE);
if(!buffer){
printf("内存分配失败\n");
fclose(file);
return -1;
}
SDL_Event event;
int quit = 0;
// 简单播放循环:直到用户关闭窗口或播放结束
while(!quit){
// 读取一帧 YUV 数据
size_t readBytes = fread(buffer, 1, FRAME_SIZE, file);
if(readBytes < FRAME_SIZE){
// 播放结束,自动回到文件开头循环播放
fseek(file, 0, SEEK_SET);
continue;
}
// 获取 Y/U/V 分量指针
Uint8* yPlane = buffer;
Uint8* uPlane = buffer + WIDTH * HEIGHT;
Uint8* vPlane = buffer + WIDTH * HEIGHT + (WIDTH * HEIGHT) / 4;
// 更新纹理数据
SDL_UpdateYUVTexture, NULL, yPlane, WIDTH, uPlane, WIDTH / 2, vPlane, WIDTH / 2);
// 清空渲染器并复制纹理
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
// 处理退出事件
while(SDL_PollEvent(&event)){
if(event.type == SDL_QUIT){
quit = 1;
}
}
// 控制帧率(约 25fps)
SDL_Delay(40);
}
// 释放资源
free(buffer);
fclose(file);
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
八、PCM 音频播放
使用 SDL 播放 PCM(Pulse Code Modulation,脉冲编码调制) 音频是实现音频播放功能的一种常见方式,尤其适用于原始音频数据(未压缩的 .pcm 文件,例如 16-bit、44100Hz、立体声)
1.SDL 播放 PCM 音频基本流程
- 初始化 SDL 音频系统
- 设置音频参数(采样率、格式、声道数、回调函数等)
- 打开音频设备
- 播放时通过回调函数往缓冲区填充 PCM 数据
- 启动播放并保持主循环
2.代码示例
#include <SDL2/SDL.h>
#include <stdio.h>
// 定义音频缓冲区结构
typedef struct {
Uint8* pos; //当前播放位置
Uint32* len; //剩余字节数
}AudioData;
// 音频回调函数:SDL 会周期性调用它来获取音频数据
void audio_callback(void* userdata, Uint8* stream, int len) {
AudioData* audio = (AudioData*)userdata;
if(audio->len == 0){
return; //播放完了
}
// 播放剩余字节数不足一帧时,只复制剩余部分
len = (len > audio->len ? audio->len : len);
SDL_memcpy(stream, audio->pos, len);
audio->pos += len;
audio->len -= len;
}
int main(int argc, char* argv[]){
// 假设 PCM 文件是 16-bit signed, stereo, 44100Hz
const char* filePath = "test.pcm";
// 打开 PCM 文件
FILE* file = fopen(filePath, "rb");
if(!file){
printf("无法打开 PCM 文件:%s\n", filePath);
return -1;
}
// 获取文件大小
fseek(file, 0, SEEK_END);
Uint32 fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
// 读取全部 PCM 数据到内存中
Uint8* buffer = (Uint8*)malloc(fileSize);
fread(buffer), 1, fileSize, file);
fclose(file);
// 初始化 SDL
if(SDL_Init(SDL_INIT_AUDIO) < 0){
printf("SDL_Init 错误:%s\n", SDL_GetError());
return -1;
}
// 设置音频格式(必须与 PCM 文件一致)
SDL_AudioSpec spec;
spec.freq = 44100; //采样率
spec.format = AUDIO_S16LSB; //16-bit signed little endian
spec.channels = 2; //立体声
spec.samples = 4096; //音频缓冲区大小(影响延迟)
spec.callback = audio_callback; //设置音频回调
AudioData audio = { buffer, fileSize };
spec.userdata = &audio;
// 打开音频设备
if(SDL_OpenAudio(&spec, NULL) < 0){
printf("SDL_OpenAudio 错误:%s\n", SDL_GetError());
free(buffer);
SDL_Quit();
return -1;
}
// 启动播放
SDL_PauseAudio(0);
// 播放期间阻塞主线程
while(audio.len > 0){
SDL_Delay(100); //每 100ms 轮询一次
}
// 播放完成后清理资源
SDL_CloseAudio();
free(buffer);
SDL_Quit();
return 0;
}