在使用 FFmpeg 之前,先认识下装载的容器 SDL2(Simple DirectMedia Layer),下载 Development Libraries 后就可以在本地进行配置,配置到项目的方法同上节 FFmpeg 的配置,可以不需要配置系统环境,同时还需要注意的是,将对应文件下的 SDL2.dll 文件复制到项目下来,在 SDL2lib 中分有 x64x86 文件夹,根据自己计算机进行选择,否则会出错哦~

使用 SDL2 显示 bmp 图片

使用 SDL2 的大概流程主要为以下步骤

  1. 初始化 SDL
  2. 创建 SDL_Window 用来承载内容
  3. 创建 SDL_Renderer 用来做些渲染
  4. 创建 SDL_Texture 用来显示一些纹理
  5. 清屏,显示内容

接下来就来看些代码

int main(int argc, char* argv[])
{
// SDL 初始化直接通过 SDL_Init 来处理
// 该方法可传入的参数包括 SDL_INIT_TIMER,SDL_INIT_AUDIO,SDL_INIT_VIDEO 等 9 中,可通过源码查看
// SDL_INIT_EVERYTHING 则是包含了上述的所有
if (SDL_Init(SDL_INIT_EVERYTHING) == -1)
{
std::cout << "Init SDL fail: " << SDL_GetError() << std::endl;
return -1;
}

SDL_Window* window = nullptr;
// SDL 通过 SDL_CreateWindow 方法来创建 Window
// 参数分别为:窗体标题,x 位置,y 位置,宽度,高度,标志位
// x, y 位置可以是某些固定的值,例如 0,也可以是 SDL 中定义的
// SDL_WINDOWPOS_CENTERED(中间位置),SDL_WINDOWPOS_UNDEFINED(任意位置)等等
// 标志位有许多可以选择,包括 SDL_WINDOW_FULLSCREEN,SDL_WINDOW_OPENGL
// SDL_WINDOW_SHOWN,SDL_WINDOW_HIDDEN 等等,具体的可通过源码查看,对应的属性注释也比较清楚
window = SDL_CreateWindow("Hello World", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
640, 480, SDL_WINDOW_SHOWN);
if (window == nullptr)
{
std::cout << "Create window fail: " << SDL_GetError() << std::endl;
return -1;
}

SDL_Renderer* renderer = nullptr;
// SDL 通过 SDL_CreateRenderer 方法创建 renderer 渲染器
// 参数分别为:显示的窗体,用于初始化渲染驱动的索引(一般为 -1),渲染标志位
// 渲染标志位包括 SDL_RENDERER_SOFTWARE,SDL_RENDERER_ACCELERATED 等等,可以根据需要进行选择
// 这里选择了硬件加速,以及使渲染器能够按照显示器刷新率来刷新画面
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (renderer == nullptr)
{
std::cout << "Create renderer fail: " << SDL_GetError() << std::endl;
return -1;
}

SDL_Surface* image = nullptr;
// 通过 SDL_LoadBMP 将图片加载到 SDL_Surface 来
image = SDL_LoadBMP("xxx.bmp");

if (image == nullptr)
{
std::cout << "Open image fail: " << SDL_GetError() << std::endl;
return -1;
}

SDL_Texture* texture = nullptr;
// 通过加载图片的 SDL_Surface 来创建 SDL_Texture
// 同时,因为 SDL_Surface 不需要再次使用了,这边就需要通过 SDL_FreeSurface 方法将其释放,避免内存泄漏
texture = SDL_CreateTextureFromSurface(renderer, image);
SDL_FreeSurface(image);

if (texture == nullptr)
{
std::cout << "Create texture fail: " << SDL_GetError() << std::endl;
return -1;
}

SDL_RenderClear(renderer); // 将渲染器清空
// 将 texture 的内容加载到渲染器来,后面传空的参数分别为
// 指向源矩形的指针(即从图像上裁剪下一块矩形),指向目标矩形的指针
// 两个参数为空即告诉 SDL 渲染整个源图像,并绘制在屏幕 (0, 0) 位置
SDL_RenderCopy(renderer, texture, nullptr, nullptr);
SDL_DestroyTexture(texture); // 加载到渲染器后,如果 Texture 不再使用则将其释放
SDL_RenderPresent(renderer); // 将渲染的内容展示出来

SDL_Delay(5000); // 用于延时 5s,否则屏幕一闪而过

// 显示完毕后,释放 renderer 和 window 对象,
// 同时通过 SDL_Quit 方法告诉 SDL 可以退出了
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}

这样就将图片显示到屏幕上了。

SDL2 显示其他类型图片和文字

SDL2 目前存在些限制,比如加载图片只能加载 bmp 格式图片,如果想加载 png 或者 jpeg 等格式,不好意思,不行。同样,如果想在 SDL2 上加载文字,不好意思,也不行。这就需要用到 SDL 扩展库了,加载图片可以使用 SDL_image,而加载文字通过 SDL_ttf 扩展库来操作,添加到项目同 SDL2 一致,通过扩展库,可以更方便来处理

例如加载图片可以直接通过 IMG_LoadTexture 方法生成一个 SDL_Texture 实例,省去了上述过程中需要 SDL_Surface 进行过渡的过程

SDL_Texture* LoadImage(std::string imagePath, SDL_Renderer render)
{
SDL_Texture* texture = nullptr;
// 目前使用 SDL_image 遇到无法正常打开 png 图片<Failed loading libpng16-16.dll>
// 未找到解决方案,希望了解这块的大佬能指导下
texture = IMG_LoadTexture(renderer, imagePath.c_str());

if (texture == nullptr)
throw std::runtime_error("Fail load image: " + IMG_GetError());

return texture;
}

而加载文字则可以通过如下方式进行加载

SDL_Texture* RenderText(std::string text, std::string fontPath, SDL_Color color, 
int fontSize, SDL_Renderer render)
{
TTF_Font* font = nullptr;
font = TTF_OpenFont(fontPath.c_str(), fontSize); // 打开一个字体文件
if (font == nullptr)
throw std::runtime_error("Fail load font: " + TTF_GetError());

// 将文字渲染到 SDL_Surface
SDL_Surface* surfcae = TTF_RenderText_Blended(font, text.c_str(), color);
SDL_Texture* texture = SDL_CreateTextureFromSurface(render, surface);
SDL_FreeSurface(surface); // 释放 surface 和 font
TTF_CloseFont(font);

return texture;
}

SDL2 事件处理机制

前面显示 Window 通过 SDL_Delay 方法来指定相应时长,然后再消失,这样体验就不太好了,理想的状态是监测到用户点击关闭按钮的时候,关闭窗体,这就涉及到 SDL 的事件处理机制了,SDL 事件包括键盘事件,鼠标事件,窗口事件等等,SDL 会将所有事件都存放在一个队列中,通过获取队列中的事件进行判断并操作。

SDL 处理事件的 API
  • SDL_WindowEvent: Window 窗口相关的事件。
  • SDL_KeyboardEvent: 键盘相关的事件。
  • SDL_MouseMotionEvent: 鼠标移动相关的事件。
  • SDL_QuitEvent: 退出事件。
  • SDL_UserEvent: 用户自定义事件。
SDL 操作事件队列的 API
  • SDL_PollEvent: 将队列头中的事件抛出来。
  • SDL_WaitEvent: 当队列中有事件时,抛出事件。否则处于阻塞状态,并释放 CPU
  • SDL_WaitEventTimeout: 与 SDL_WaitEvent 的区别是,当到达超时时间后,退出阻塞状态。
  • SDL_PeekEvent: 从队列中取出事件,但该事件不从队列中删除。
  • SDL_PushEvent: 向队列中插入事件。
增加窗体退出事件监听

那么我们就可以对上面 SDL_Delay 通过事件机制来替换下

int main(int argc, char* argv[])
{
// ...省略 `SDL_Delay(5000)` 之前的代码
bool quit = false;
while(!quit)
{
SDL_Event event;
SDL_WaitEvent(&event);
if (event.type == SDL_QUIT)
{
quit = true;
}
}
// ...省略 `SDL_Delay(5000)` 之后的代码
}
对键盘事件的监听

SDL 对其他事件支持也通过相应的 type 来进行判断,例如监听键盘方向键

int main(int argc, char* agrv[])
{
bool quit = false;
while (!quit)
{
SDL_Event event;
SDL_WaitEvent(&event);

switch (event.type)
{
case SDL_QUIT:
quit = true;
cout << "quit" << endl;
break;

case SDL_KEYDOWN:
switch (event.key.keysym.sym)
{
case SDLK_LEFT:
cout << "key ← down" << endl;
break;
case SDLK_RIGHT:
cout << "key → down" << endl;
break;
case SDLK_UP:
cout << "key ↑ down" << endl;
break;
case SDLK_DOWN:
cout << "key ↓ down" << endl;
break;
default:
break;
}
break;

case SDL_KEYUP:
switch (event.key.keysym.sym)
{
case SDLK_LEFT:
cout << "key ← up" << endl;
break;
case SDLK_RIGHT:
cout << "key → up" << endl;
break;
case SDLK_UP:
cout << "key ↑ up" << endl;
break;
case SDLK_DOWN:
cout << "key ↓ up" << endl;
break;
default:
break;
}
break;

default:
break;
}
}
// ...
}

当按下相应按键的时候,就会打印出相应的内容出来了

需要注意的是,SDL_PollEventSDL_WaitEvent 都可以达到获取队列事件,但是有不同的使用场景:

  • 对于游戏来说,它要求事件的实时处理,我们最好使用 SDL_PollEvent
  • 对于一些其它实时性不高的场景来说,则可以使用 SDL_WaitEvent

SDL 方法封装

在第一部分,介绍了 SDL 显示内容的基本步骤,为了提高复用,我们可以进行一些必要的封装

// Window.h

#include <functional>
#include <string>
#include <iostream>
#include <SDL.h>
#include <SDL_image.h>
#include <SDL_ttf.h>
#include <initializer_list>

class Window
{
public:
// position 用来确定 window 的位置和宽高信息
static void Init(SDL_Rect& wh, std::string title = "Window Title");

// 主要做些显示完毕的操作
static void Quit(std::initializer_list<SDL_Texture*> ts);

// 将 texture copy 到 render 上
static void Draw(SDL_Texture* texture, SDL_Rect* sorce = nullptr, SDL_Rect* clip = nullptr,
float angle = 0.0, int xPivot = 0, int yPivot = 0, SDL_RendererFlip flip = SDL_FLIP_NONE);

// 加载图片
static SDL_Texture* LoadImage(std::string image);

// 加载文字
static SDL_Texture* RenderText(std::string text, std::string fontPath, int fontSize, SDL_Color color);

// 清屏
static void Clear();

// 显示
static void Present();

private:
// 使用智能指针,方便回收等处理
// 通过如下方式进行智能指针的定义,因为 SDL_Window 和 SDL_Renderer
// 都是 struct,不能使用 new 关键词进行创建实例,用于指定创建方法
static std::unique_ptr<SDL_Window, std::function<void(SDL_Window*)>> mWindow;
static std::unique_ptr<SDL_Renderer, std::function<void(SDL_Renderer*)>> mRenderer;
};

具体的实现,和上面的介绍没太大区别,除了使用智能指针更方便回收内存

#include "Window.h"

std::unique_ptr<SDL_Window, std::function<void(SDL_Window*)>> Window::mWindow
= std::unique_ptr<SDL_Window, void(*)(SDL_Window*)>(nullptr, SDL_DestroyWindow);

std::unique_ptr<SDL_Renderer, std::function<void(SDL_Renderer*)>> Window::mRenderer
= std::unique_ptr<SDL_Renderer, void(*)(SDL_Renderer*)>(nullptr, SDL_DestroyRenderer);

// 初始化实现
void Window::Init(SDL_Rect& wh, std::string title)
{
if (&wh == nullptr)
{
throw std::runtime_error("Window Position can't be null");
}

if (SDL_Init(SDL_INIT_EVERYTHING) == -1)
{
throw std::runtime_error("Init SDL fail");
}

if (TTF_Init() == -1)
{
throw std::runtime_error("Init font fail");
}

// 初始化 mWindow 实例
mWindow.reset(SDL_CreateWindow(title.c_str(), wh.x, wh.y, wh.w, wh.h, SDL_WINDOW_SHOWN));

if (mWindow == nullptr)
{
throw std::runtime_error("Create window fail: ");
}

// 初始化 mRenderer 实例
mRenderer.reset(SDL_CreateRenderer(mWindow.get(), -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC));

if (mRenderer == nullptr)
{
throw std::runtime_error("Create renderer fail");
}
}

void Window::Quit(std::initializer_list<SDL_Texture*> ts)
{
for (auto ptr = ts.begin(); ptr != ts.end(); ptr++)
{
SDL_DestroyTexture(*ptr);
}

// 由于使用了智能指针,mWindow, mRenderer 能够自动释放,所以只需通知 SDL 退出
SDL_Quit();
}

void Window::Draw(SDL_Texture* texture, SDL_Rect* source, SDL_Rect* clip, float angle,
int xPivot, int yPivot, SDL_RendererFlip flip)
{
xPivot += source == nullptr ? 0 : source->w / 2;
yPivot += source == nullptr ? 0 : source->y / 2;
SDL_Point pivot = { xPivot, yPivot };

// 使用 SDL_RenderCopyEx 可以进行角度旋转等操作
SDL_RenderCopyEx(mRenderer.get(), texture, source, clip, angle, &pivot, flip);
}

SDL_Texture* Window::LoadImage(std::string file)
{
SDL_Texture* texture = nullptr;

texture = IMG_LoadTexture(mRenderer.get(), file.c_str());

if (texture == nullptr)
{
throw std::runtime_error(IMG_GetError());
}

return texture;
}

SDL_Texture* Window::RenderText(std::string text, std::string fontPath, int fontSize, SDL_Color color)
{
TTF_Font* font = nullptr;
SDL_Surface* surface = nullptr;
SDL_Texture* texture = nullptr;

font = TTF_OpenFont(fontPath.c_str(), fontSize);

if (font == nullptr)
{
throw std::runtime_error(TTF_GetError());
}

surface = TTF_RenderText_Blended(font, text.c_str(), color);
texture = SDL_CreateTextureFromSurface(mRenderer.get(), surface);
SDL_FreeSurface(surface);
return texture;
}

void Window::Clear()
{
SDL_RenderClear(mRenderer.get());
}

void Window::Present()
{
bool quit = false;
SDL_RenderPresent(mRenderer.get());

while (!quit)
{
SDL_Event event;
SDL_WaitEvent(&event);

if (event.type == SDL_QUIT)
{
quit = true;
}
}
}

那么实际调用的时候就可以更加方便

int main(int argc, char* agrv[])
{
SDL_Texture* background = nullptr, *text = nullptr;
SDL_Rect wh, textZone;

wh.w = 640;
wh.h = 480;
wh.x = SDL_WINDOWPOS_CENTERED;
wh.y = SDL_WINDOWPOS_CENTERED;

try
{
Window::Init(wh, "Hello SDL");
}
catch (const std::exception & e)
{
Window::Quit({});
std::cout << e.what() << std::endl;
return -1;
}

try
{
background = Window::LoadImage("xxx.bmp");
text = Window::RenderText("Hello SDL", "xxx.ttf", 25, {255, 255, 255, 200});
}
catch (const std::exception & e)
{
std::cout << e.what() << std::endl;
return -1;
}

// SDL_QueryTexture 可以测量对应 Texture 的宽高
SDL_QueryTexture(text, nullptr, nullptr, &textZone.w, &textZone.h);
textZone.x = (640 - textZone.w) / 2;
textZone.y = (480 - textZone.h) / 2;

Window::Clear();
Window::Draw(background, nullptr, nullptr, 45, 320, 240, SDL_FLIP_VERTICAL);
Window::Draw(text, nullptr, &textZone);

Window::Present();
Window::Quit({background, text});

return 0;
}

最后还是以图结束 SDL 加载视图

Kt5Z1e.png

SDL 播放音乐(补充)

SDL 除了加载视图外,还内置播放 Api 用来播放音乐,不过自带的 Api 只加载 wav 格式

#define LOGE(...) av_log(nullptr, AV_LOG_ERROR, __VA_ARGS__)

int main(int argc, char* agrv[]) {
Uint32 wav_length;
Uint8* wav_buffer;
SDL_AudioSpec wanted_spec;

// 初始化 SDL
if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
LOGE("init SDL fail: %s", SDL_GetError());
return -1;
}

// 返回一个 SDL_AudioSpec,不为空则加载成功
if (SDL_LoadWAV(music_path, &wanted_spec, &wav_buffer, &wav_length) == nullptr) {
LOGE("load music fail: %s", SDL_GetError());
return -1;
}

// 根据获取到的 SDL_AudioSpec 获取 device
SDL_AudioDeviceID deviceId = SDL_OpenAudioDevice(nullptr, 0, &wanted_spec, nullptr, 0);
LOGE("device: %d\n", deviceId);
if (deviceId <= 0) {
LOGE("didn't find device: %s", SDL_GetError());
return -1;
}

if (SDL_QueueAudio(deviceId, wav_buffer, wav_length) != 0) {
LOGE("queue audio fail: %s", SDL_GetError());
return -1;
}

// 根据 deviceId 播放音乐文件,0 表示打开对应 id 的 device
SDL_PauseAudioDevice(deviceId, 0);

SDL_Delay(20000);

// 释放内存
SDL_CloseAudioDevice(deviceId);
SDL_FreeWAV(wav_buffer);
SDL_Quit();
}

使用 SDL_mixer 加载音乐文件

因为 SDL 加载音乐文件有限制,那么可以选择通过 SDL_mixer 库来加载音乐文件,而且使用也更加方便

#define PLAYING_WAV  0;

int main(int argc, char* agrv[]) {
Mix_Music* music = nullptr;
Mix_Chunk* chunk = nullptr;

if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
LOGE("init SDL fail: %s", SDL_GetError());
return -1;
}

// 打开调音台,参数分别为 频率,格式,声道,chunk 大小
if (Mix_OpenAudio(22050, MIX_DEFAULT_FORMAT, 2, 4096) == -1) {
LOGE("open audio fail: %s", Mix_GetError());
return -1;
}

// 加载的文件类型包括 .wav .mod .s3m .it .xm 等多种类型
// 加载方式有两种,一种通过指定 channel 加载,一种直接加载文件
#if PLAYING_WAV
// 通过 chunk 方式进行加载,方法名中虽然是 loadWav,但是也可加载其他文件
if ((chunk = Mix_LoadWAV(music_path)) == nullptr) {
LOGE("load music file fail: %s", Mix_GetError());
return -1;
}

// 在指定的 channel 播放,如果声道指定 -1,则会在第一个可播放声道播放
// loop 表示循环次数,如果指定 -1 则无限循环
if (Mix_PlayChannel(-1, chunk, 1) == -1) {
LOGE("play music fail: %s\n", Mix_GetError());
return -1;
}

// 检测指定声道的状态,如果为 -1 则检测全部声道
while (Mix_Playing(-1)) {
LOGE("playing wav file...\n");
}

// 播放完毕后释放文件
Mix_FreeChunk(chunk);
#else
if ((music = Mix_LoadMUS(music_path)) == nullptr) {
LOGE("load music file fail: %s", Mix_GetError());
return -1;
}

// 播放文件,loop = -1 表示无限循环
if (Mix_PlayMusic(music, -1) == -1) {
LOGE("play music fail: %s\n", Mix_GetError());
return -1;
}

// 检测声道状态
while (Mix_PlayingMusic()) {
LOGE("music is playing.\n");
}

// 播放完毕释放文件
Mix_FreeMusic(music);
#endif // PLAYING_WAV

// 关闭 audio
Mix_CloseAudio();
SDL_Quit();
}

以上就是 SDL 加载音频文件的几种方式。