【音视频开发】第四章 SDL音视频渲染

发布于:2025-04-17 ⋅ 阅读:(27) ⋅ 点赞:(0)

【音视频开发】第四章 SDL音视频渲染

一、简介

1.什么是 SDL

官网:https://www.libsdl.org

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 音频基本流程

  1. 初始化 SDL 音频系统
  2. 设置音频参数(采样率、格式、声道数、回调函数等)
  3. 打开音频设备
  4. 播放时通过回调函数往缓冲区填充 PCM 数据
  5. 启动播放并保持主循环

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;
}

网站公告

今日签到

点亮在社区的每一天
去签到