音视频开发_SDL跨平台多媒体开发库实战

SDL(Simple DirectMedia Layer)是一个非常流行和强大的跨平台开发库,它主要被用来开发视频游戏和实时多媒体应用程序。它提供了一系列的功能来处理视频、音频、键盘、鼠标、操纵杆、图形硬件加速以及聚焦3D硬件的各种功能。SDL的API通过C编程语言被设计和实现,但存在多种语言的绑定,方便不同的开发者使用不同的编程语言。
在这里插入图片描述

在音视频开发方面,SDL提供了基础的API来进行音频播放和视频的渲染。例如,SDL_Audio接口允许你设定音频设备的参数,如采样率、音频格式和声道数,然后可以将音频数据送入这些设备进行播放。对于视频,SDL提供了一个简单的2D渲染API,可以创建窗口,加载图像,并且将它们渲染到屏幕上。

在跨平台方面,SDL支持多个操作系统,包括Windows、Mac OS X、Linux、iOS和Android。SDL的跨平台能力非常强,因为其接口在不同平台上尽可能地保持一致,差异性由其内部实现来抹平。

SDL官方网站地址是 https://www.libsdl.org/。
GitHub地址:https://github.com/libsdl-org/SDL/releases

SDL 环境搭建

一般步骤:

  1. 安装SDL

    • 对于Linux系统

      sudo apt-get install libsdl2-2.0
      sudo apt-get install libsdl2-dev
      

      这将安装SDL库及其开发文件。

    • 对于Windows系统

      • 从SDL官方网站下载SDL开发库。
      • 解压并将其包含在你的项目目录中或者设置系统环境变量以便编译器能找到这些文件。
    • 对于macOS系统

      • 可以使用Homebrew来安装SDL:
      brew install sdl2
      
  2. 安装FFmpeg

    • 对于Linux系统
      sudo apt-get update
      sudo apt-get install ffmpeg
      
    • 对于Windows系统
      • 从FFmpeg官方网站下载构建好的二进制文件。
      • 解压缩到你的项目目录或者添加到你的系统路径。
    • 对于macOS系统
      • 可以使用Homebrew来安装FFmpeg:
      brew install ffmpeg
      
  3. 集成FFmpeg和SDL库到你的项目中

    • 确保在你的项目设置中包含了正确的头文件路径和库文件路径。如果你使用IDE(如Visual Studio或者Xcode),则需要在项目设置中指定这些文件。
    • 包含必要的库文件链接。例如,在Linux上使用g++编译时,你可能需要链接SDL和FFmpeg的库,如下:
      g++ main.cpp -o app.out `sdl2-config --cflags --libs` -lavformat -lavcodec -lavutil -lswscale
      
    • 在代码中包含SDL和FFmpeg的头文件,例如:
      #include <SDL.h>
      #include <libavformat/avformat.h>
      #include <libavcodec/avcodec.h>
      

以下是SDL库中一些常用模块的主要结构体和函数的概览,这将帮助你了解SDL中的一些关键组件:

常用结构体

  • SDL_Window - 表示一个窗口。
  • SDL_Renderer - 用于2D渲染的渲染器。
  • SDL_Surface - 表示一个二维图像或图像集合。
  • SDL_Texture - 通过渲染器使用的一块图像数据。
  • SDL_Rect - 定义矩形区域的位置和大小。
  • SDL_Color - 表示一个RGBA颜色。
  • SDL_Event - 表示一个事件,例如键盘、鼠标事件等。
  • SDL_AudioSpec - 定义音频输出格式和回调。
  • SDL_PixelFormat - 描述像素格式。

常用函数

  • SDL_Init - 初始化SDL库。
  • SDL_CreateWindow - 创建一个窗口。
  • SDL_DestroyWindow - 销毁一个窗口。
  • SDL_CreateRenderer - 创建一个2D渲染器。
  • SDL_DestroyRenderer - 销毁渲染器。
  • SDL_CreateTextureFromSurface - 从表面创建纹理。
  • SDL_PollEvent - 检索当前的事件。
  • SDL_RenderClear - 清除当前渲染目标。
  • SDL_RenderCopy - 将纹理复制到当前渲染目标。
  • SDL_RenderPresent - 更新屏幕上的渲染内容。
  • SDL_LoadBMP - 加载BMP图像文件到SDL_Surface。
  • SDL_FreeSurface - 释放Surface。
  • SDL_OpenAudio - 打开音频设备。
  • SDL_PauseAudio - 暂停或开始播放音频。
  • SDL_CloseAudio - 关闭音频设备。

高级模块下的结构体和函数

除了上述基本功能模块外,SDL还包括其他高级别的模块如:

  • SDL_ttf - 用于字体渲染。
  • SDL_image - 用于加载多种图像格式。
  • SDL_mixer - 用于音频混音。
  • SDL_net - 提供网络功能。

每个高级模块都有它的特定结构体和函数,是独立于基础SDL功能的。

SDL 事件处理

SDL 的事件处理机制是基于事件队列的。SDL对于不同的输入和系统事件都定义了一个事件类型。当这些事件发生时,它们被加入到一个中央事件队列中。你的应用程序则负责周期性地从这个队列中读取和处理这些事件。

以下是SDL事件处理的基本原理:

  1. 事件类型

    • SDL定义了多种类型的事件,如键盘按键事件(SDL_KEYDOWN、SDL_KEYUP)、鼠标事件(SDL_MOUSEMOTION、SDL_MOUSEBUTTONDOWN、SDL_MOUSEBUTTONUP)和窗口事件(SDL_WINDOWEVENT)等。
  2. 事件队列

    • 当事件发生时,例如用户按下键盘、移动鼠标或者窗口状态变更,这些事件被创建并加入到SDL的中央事件队列中。
  3. 事件轮询

    • 你的应用程序通过调用SDL_PollEvent()SDL_WaitEvent()不断查询事件队列。SDL_PollEvent()非阻塞地检查队列,并立即返回;如果没有事件,它返回0。SDL_WaitEvent()则是阻塞操作,直到队列中有事件时才返回。
  4. 事件处理

    • SDL_PollEvent()从队列中抓取一个事件并将其放入一个SDL_Event结构体实例中。你的代码需要根据事件的类型进行检查,并执行相应的操作。

一个简单的事件轮询循环可能看起来像这样:

SDL_Event event;

while (running) {
    // 轮询获取事件
    while (SDL_PollEvent(&event)) {
        // 处理得到的事件
        switch (event.type) {
            case SDL_QUIT:
                running = false;
                break;
            case SDL_KEYDOWN:
                if (event.key.keysym.sym == SDLK_ESCAPE) {
                    running = false;
                }
                break;
            // 更多事件处理...
        }
    }

    // 执行其它游戏循环逻辑...
}

在这个例子中,running是一个控制应用程序主循环的变量;我们继续轮询直到收到一个SDL_QUIT事件,通常由用户关闭窗口时产生,或用户按下了Escape键。

通过这种方式,SDL提供了一种相对简单的机制来处理用户输入和其他类型的事件,使得开发者可以轻松进行事件驱动的程序开发。

SDL 线程处理

SDL提供了一套自己的线程管理API,允许你在应用内创建和管理线程。SDL的线程处理API相对平台无关,意味着你可以在不同操作系统上使用大致相同的代码来处理线程。

SDL线程的处理主要遵循以下几个原则:

  1. 线程创建

    • 使用SDL_CreateThread()函数来创建一个新线程。你需要提供一个函数指针,SDL将会在新线程内调用这个函数。你也可以传递一个指向任何数据的指针,该数据将作为参数传递给线程函数。
  2. 线程函数

    • 线程函数是你希望在新线程中运行的代码。这个函数需要遵循特定的原型(符合SDL_ThreadFunction类型定义),并且返回一个int作为线程退出代码。
  3. 线程同步

    • SDL提供了几种同步机制,包括互斥锁(SDL_mutex)、条件变量(SDL_cond)和信号量(SDL_sem),以帮助管理线程间的协作和避免竞争条件。
  4. 线程管理

    • 创建线程后,你可以使用SDL_WaitThread()等待一个线程结束。使用SDL_GetThreadID()来获取线程标识符,或者用SDL_ThreadID()获取当前线程的ID。
  5. 线程安全

    • 虽然SDL封装了线程创建和同步机制,但不保证所有的SDL API都是线程安全的。你需要确保当多个线程想要访问共享资源时,使用适当的同步机制来避免并发问题。

下面是一个使用SDL创建和管理线程的简单示例:

#include <SDL.h>

int threadFunction(void* data) {
    // 线程的执行代码
    // ...
    return 0;
}

int main(int argc, char* argv[]) {
    if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
        // 初始化失败处理
        return -1;
    }

    // 创建新线程
    SDL_Thread* thread = SDL_CreateThread(threadFunction, "MyThread", (void*)NULL);

    // 等待线程结束
    SDL_WaitThread(thread, NULL);
    
    SDL_Quit();
    return 0;
}

在这个例子中,我们创建了一个名为"MyThread"的新线程,并在新线程中执行threadFunction函数。SDL_WaitThread用于等待这个线程结束才退出程序。记住在多线程编程时,要特别注意资源共享和同步问题。

SDL提供了哪些线程同步机制?

SDL提供了几种线程同步机制来控制对共享资源的访问,确保线程安全。这些同步机制包括:

  1. 互斥锁

    • 创建: 通过SDL_CreateMutex()创建一个互斥锁。
    • 锁定: 使用SDL_LockMutex()来锁定互斥锁,如果互斥锁已经被另一个线程锁定,则当前线程将会阻塞直到互斥锁被解锁。
    • 尝试锁定: SDL_TryLockMutex()尝试锁定互斥锁,但如果锁定不成功,不会阻塞线程。
    • 解锁: SDL_UnlockMutex()解锁互斥锁,允许其他线程能够锁定它。
    • 销毁: 使用SDL_DestroyMutex()来销毁互斥锁。
  2. 信号量

    • 创建: 通过SDL_CreateSemaphore()创建一个带有初始计数值的信号量。
    • 等待信号量: SDL_SemWait()会递减信号量的计数值,如果信号量的值为0,则线程将阻塞。
    • 尝试等待: SDL_SemTryWait()尝试递减信号量的计数值,但在信号量的值为0时不会阻塞线程。
    • 递增信号量: SDL_SemPost()递增信号量的计数值,可能会释放正在等待的线程。
    • 销毁: SDL_DestroySemaphore()销毁信号量对象。
  3. 条件变量

    • 创建: 用SDL_CreateCond()创建一个条件变量。
    • 等待条件变量: SDL_CondWait()使线程阻塞,直到条件变量被信号化。通常这与互斥锁一起使用,以便线程在阻塞前释放互斥锁并再次获取互斥锁以继续执行。
    • 带超时的等待: SDL_CondWaitTimeout()等待条件变量,但如果在给定的超时时间内条件没有被信号化,则返回。
    • 信号化一个线程: SDL_CondSignal()信号化一个等待中的线程来继续执行。
    • 广播信号: SDL_CondBroadcast()将信号发送给所有等待的线程。
    • 销毁: SDL_DestroyCond()销毁条件变量。

视频YUV画面渲染

在SDL中渲染YUV视频数据,通常涉及到使用SDL_TextureSDL_Renderer来绘制YUV图像。

在这里插入图片描述

以下是步骤:

  1. 初始化SDL

    • 在开始之前,确保调用SDL_Init(SDL_INIT_VIDEO)来初始化SDL视频子系统。
  2. 创建窗口和渲染器

    • 使用SDL_CreateWindow创建一个窗口。
    • 使用SDL_CreateRenderer创建一个与窗口关联的渲染器。
  3. 创建YUV纹理

    • 使用SDL_CreateTexture创建一个YUV格式的纹理(SDL_PIXELFORMAT_IYUVSDL_PIXELFORMAT_YV12)。
  4. 更新纹理与YUV数据

    • 使用SDL_UpdateYUVTexture来更新纹理的Y、U、V平面。传入Y、U、V平面的数据和它们各自的行跨距(pitch)。
  5. 渲染纹理

    • 使用SDL_RenderClear来清除当前渲染目标。
    • 使用SDL_RenderCopy将YUV纹理复制到渲染器上。
    • 使用SDL_RenderPresent来更新窗口显示内容。
  6. 循环处理视频帧

    • 对于视频播放,你需要在播放循环中重复执行更新纹理和渲染纹理的步骤,以呈现每个视频帧。
  7. 资源清理

    • 使用SDL_DestroyTexture来销毁纹理。
    • 使用SDL_DestroyRenderer来销毁渲染器。
    • 使用SDL_DestroyWindow来销毁窗口。
    • 最终调用SDL_Quit来退出SDL并清理资源。

以下是基于SDL 2.0的伪代码示例,展示了如何渲染YUV画面:

SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window = SDL_CreateWindow("YUV Player",
                                      SDL_WINDOWPOS_UNDEFINED,
                                      SDL_WINDOWPOS_UNDEFINED,
                                      width, height,
                                      SDL_WINDOW_SHOWN);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
SDL_Texture* texture = SDL_CreateTexture(renderer,
                                         SDL_PIXELFORMAT_IYUV,
                                         SDL_TEXTUREACCESS_STREAMING,
                                         width, height);

while(playing) {
    // 其中yPlane, uPlane, vPlane是外部提供的YUV视频帧数据
    SDL_UpdateYUVTexture(texture, NULL,
                         yPlane, yPitch, 
                         uPlane, uPitch,
                         vPlane, vPitch);

    SDL_RenderClear(renderer);
    SDL_RenderCopy(renderer, texture, NULL, NULL);
    SDL_RenderPresent(renderer);

    // 处理SDL事件,检查是否退出等...
}

SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();

如何使用SDL渲染RGB图像?

在SDL中渲染RGB图像相比于YUV图像稍显简单,因为不需要处理不同的色彩平面。

使用SDL渲染RGB图像:

  1. 初始化SDL:

    • 使用SDL_Init(SDL_INIT_VIDEO)初始化SDL视频子系统。
  2. 创建窗口:

    • 使用SDL_CreateWindow创建一个新的窗口。
  3. 创建渲染器:

    • 使用SDL_CreateRenderer创建一个渲染器,链接到你的窗口。
  4. 创建纹理:

    • 使用SDL_CreateTexture创建一个纹理,其像素格式设置为RGB格式(如SDL_PIXELFORMAT_RGB24SDL_PIXELFORMAT_ARGB8888,取决于你的RGB数据是24位还是32位)。
  5. 更新纹理:

    • 使用SDL_UpdateTexture来将你的RGB数据上传到纹理。你需要提供原始的RGB数据和该数据的行跨距或者 pitch(即每行像素的字节数)。
  6. 清空渲染器:

    • 使用SDL_RenderClear来清除当前渲染目标。
  7. 渲染纹理:

    • 使用SDL_RenderCopy将纹理复制到渲染器上。
  8. 显示图像:

    • 使用SDL_RenderPresent将上述所有的渲染操作呈现到屏幕上。
  9. 循环处理:

    • 如果你正在渲染视频或动画,你需要在一个循环中重复上述更新纹理和渲染纹理的步骤。
  10. 释放资源:

    • 使用SDL_DestroyTexture来销毁纹理,SDL_DestroyRenderer来销毁渲染器,以及SDL_DestroyWindow来销毁窗口。
    • 最后,调用SDL_Quit清理和退出SDL。

以下是一个基本的示例代码,演示了如何在SDL中渲染一个RGB图像:

// 初始化SDL
SDL_Init(SDL_INIT_VIDEO);

// 创建窗口
SDL_Window* window = SDL_CreateWindow("RGB Image Renderer",
                                      SDL_WINDOWPOS_UNDEFINED,
                                      SDL_WINDOWPOS_UNDEFINED,
                                      width, height,
                                      SDL_WINDOW_SHOWN);

// 创建渲染器
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);

// 创建纹理
SDL_Texture* texture = SDL_CreateTexture(renderer,
                                         SDL_PIXELFORMAT_ARGB8888, // 对于32位RGB
                                         SDL_TEXTUREACCESS_STREAMING,
                                         width, height);

// RGB数据更新到纹理
SDL_UpdateTexture(texture, NULL, rgbPixels, pitch);

// 清空渲染器
SDL_RenderClear(renderer);

// 绘制纹理到渲染器
SDL_RenderCopy(renderer, texture, NULL, NULL);

// 展示渲染出的图像
SDL_RenderPresent(renderer);

// 呈现相关处理的代码,比如等待时间或事件处理

// 清理资源
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();

在此示例中,rgbPixels代表你的RGB图像数据,pitch代表每行像素的字节数。这个简单的代码片段无法单独运行,因为它不包含游戏循环或事件处理。

音频PCM声音输出

在SDL中播放PCM音频数据的过程相对直接。你需要设置音频设备的参数,打开音频设备,写入PCM数据到音频缓冲区,最后关闭设备。
在这里插入图片描述

下面是播放PCM音频数据的基本步骤:

  1. 初始化SDL音频子系统:

    • 使用SDL_Init(SDL_INIT_AUDIO)来初始化SDL音频子系统。
  2. 配置音频规格(SDL_AudioSpec:

    • 创建一个SDL_AudioSpec结构体来指定音频数据的格式、声道数、采样率等信息。
    • callback函数设置一个回调,SDL将在需要音频数据时调用这个回调函数。
  3. 打开音频设备:

    • 使用SDL_OpenAudio()SDL_OpenAudioDevice()打开音频设备。
    • 如果成功,SDL将开始播放音频流,并根据需要调用设置的回调函数来获取音频数据填充到播放缓冲区。
  4. 播放音频:

    • 使用SDL_PauseAudio(0)SDL_PauseAudioDevice来开始播放。
    • 在回调函数中,你应该将PCM数据复制到SDL提供的缓冲区中,这会将数据提供给音频设备进行播放。
  5. 音频播放控制:

    • 你可以使用SDL_PauseAudio(1)暂停音频,并使用SDL_PauseAudio(0)恢复播放。
  6. 关闭音频设备:

    • 使用SDL_CloseAudio()SDL_CloseAudioDevice()来关闭音频设备,释放资源。

下面是一个简单的示例代码,演示了上述步骤:

// 回调函数,当需要更多的音频数据时,SDL将调用它
void audio_callback(void* userdata, Uint8* stream, int len) {
    // 用户提供的数据
    AudioData* audio = (AudioData*)userdata;
    // 从音频数据源复制数据到播放缓冲区
    SDL_memset(stream, 0, len);
    SDL_memcpy(stream, audio->audio_pos, len);
    audio->audio_pos += len;
    audio->audio_len -= len;
}

// 初始化SDL音频子系统
SDL_Init(SDL_INIT_AUDIO);
SDL_AudioSpec wanted_spec, obtained_spec;

// 指定PCM音频数据的参数
wanted_spec.freq = 44100; // 采样率
wanted_spec.format = AUDIO_S16SYS; // 16位有符号数,系统字节序
wanted_spec.channels = 2; // 双声道
wanted_spec.samples = 4096; // 音频缓冲区的样本数量
wanted_spec.callback = audio_callback; // 回调函数
wanted_spec.userdata = &audio_data; // 传递给回调的数据

// 打开音频设备
if (SDL_OpenAudio(&wanted_spec, &obtained_spec) < 0) {
    fprintf(stderr, "Couldn't open audio: %s\n", SDL_GetError());
    return -1;
}

// 开始播放
SDL_PauseAudio(0);

// 处理你的应用程序的逻辑...

// 最后,关闭音频设备并清理
SDL_CloseAudio();
SDL_Quit();

在上述代码中,audio_callback函数是当播放队列需要更多数据时由SDL调用的。

如何在SDL中配置回调函数来获取音频数据?

在SDL中,配置回调函数来获取音频数据的目的是,在播放音频数据时可以不断地将音频样本提供给音频设备,以便连续播放。

下面是如何配置SDL音频回调函数的步骤:

  1. 定义音频回调函数:

    • 回调函数必须符合void (*)(void* userdata, Uint8* stream, int len)的签名。
    • userdata参数用于传递给回调函数任何必要的外部数据。
    • stream参数是指向音频设备需要播放的音频缓冲区的指针。
    • len参数表示stream缓冲区的大小,以字节为单位。
  2. 设置音频规格(SDL_AudioSpec)结构体:

    • 将上面定义的回调函数指定给SDL_AudioSpec结构体的callback字段。
    • 配置其它字段,比如音频格式(format)、采样率(freq)、声道数(channels)等。
    • 设置userdata字段以传递你需要在回调中访问的数据。
  3. 打开音频设备并启用回调:

    • 使用SDL_OpenAudio()SDL_OpenAudioDevice()函数打开音频设备,传入SDL_AudioSpec结构体。
    • 如果设备打开成功,当音频设备准备好接收数据时,SDL将定期调用你的回调函数。

搭建这些步骤的代码:

// 自定义的音频数据结构
struct MyAudioData {
    Uint8* audio_buf; // 音频缓冲区
    Uint32 audio_len; // 音频缓冲区的长度
    Uint32 audio_pos; // 当前读取的位置
};

// 音频回调函数
void MyAudioCallback(void* userdata, Uint8* stream, int len) {
    MyAudioData* myAudioData = static_cast<MyAudioData*>(userdata);

    // 填充音频数据
    if (myAudioData->audio_len == 0) {
        // 音频数据已经播放完毕
        memset(stream, 0, len); // 静音处理
        return;
    }
    
    len = (len > myAudioData->audio_len ? myAudioData->audio_len : len); // 不要超出缓冲区
    SDL_memcpy(stream, myAudioData->audio_buf + myAudioData->audio_pos, len); // 复制数据
    myAudioData->audio_pos += len;
    myAudioData->audio_len -= len;
}

// 主函数中的代码:
SDL_Init(SDL_INIT_AUDIO);
SDL_AudioSpec spec;
MyAudioData audioData;

// 填写结构体的字段
spec.freq = 44100; // 这里设置你的音频采样率
spec.format = AUDIO_S16LSB; // 这里设置你的音频数据格式,比如16位符号整形数据
spec.channels = 2; // 这里设置你的音频声道数
spec.samples = 2048; // 这里设置你的音频缓冲区的大小
spec.callback = MyAudioCallback; // 刚定义的回调函数
spec.userdata = &audioData; // 指向音频数据的指针

// 打开音频设备
if (SDL_OpenAudio(&spec, NULL) != 0) {
    SDL_Log("Failed to open audio: %s", SDL_GetError());
    // 这里处理打开失败的情况
}

// 加载音频数据到audioData结构体中
// audioData.audio_buf = ...
// audioData.audio_len = ...
// audioData.audio_pos = 0;

// 开始播放音频
SDL_PauseAudio(0);

// 在程序的其它部分管理音频数据和SDL事件

// 清理工作
SDL_CloseAudio();
SDL_Quit();

在这个代码示例中,需要将音频数据加载到audioData.audio_buf中,并设置audioData.audio_len为音频缓冲区的字节长度。回调函数MyAudioCallback会被定期调用,把音频数据从audioData中复制到stream缓冲区中,这个缓冲区随后会被音频设备播放出来。SDL的音频回调函数是在一个单独的线程中调用的,所以回调中的操作要尽可能的快,并且要保证线程安全。

如何在SDL中设置音频规格?

在SDL中设置音频规格是通过填充SDL_AudioSpec结构体完成的,并使用它打开音频设备。下面是设置音频规格的详细步骤:

  1. 创建SDL_AudioSpec结构体实例:
    你需要先创建一个SDL_AudioSpec结构体的实例。

  2. 指定音频参数:
    SDL_AudioSpec结构体中,你需要设置几个关键的参数,包括频率(freq)、音频格式(format)、声道数(channels)以及缓冲区采样数(samples)。

  3. 设置音频回调:
    SDL_AudioSpec结构体中设置callback函数,SDL会在需要新的音频数据来填充音频设备的播放缓冲时调用这个函数。同时,设置userdata字段,该字段可以在回调中使用,通常用来传递应用程序的音频数据或其它状态信息。

  4. 打开音频设备:
    使用SDL_OpenAudio()SDL_OpenAudioDevice()函数,并提供填充好的SDL_AudioSpec结构体来打开音频设备。你可以选择接收一个SDL_AudioSpec的实例,用来接收设备实际使用的音频规格参数,有时候这可能由于硬件限制与请求的参数不完全相符。

下面是代码说明如何填充SDL_AudioSpec结构体并使用它来打开音频设备:

// 定义回调函数
void MyAudioCallback(void* userdata, Uint8* stream, int len) {
    // 填充音频缓冲区的代码
}

// 主函数或初始化音频的部分
SDL_Init(SDL_INIT_AUDIO);

SDL_AudioSpec wanted_spec; // 请求的音频规格
SDL_AudioSpec obtained_spec; // 实际得到的音频规格

// 设置请求的音频规格参数
wanted_spec.freq = 44100; // 设置采样率为44100Hz
wanted_spec.format = AUDIO_S16SYS; // 设置音频数据格式为16-bit signed samples
wanted_spec.channels = 2; // 设置为双声道
wanted_spec.samples = 2048; // 设置缓冲区采样数
wanted_spec.callback = MyAudioCallback; // 设置回调函数
wanted_spec.userdata = NULL; // userdata可用于传递给回调函数的自定义数据

// 打开音频设备并获取设备的实际音频规格
if (SDL_OpenAudio(&wanted_spec, &obtained_spec) != 0) {
    fprintf(stderr, "Couldn't open audio: %s\n", SDL_GetError());
    // 处理打开音频设备失败的情况
}

// 开始播放
SDL_PauseAudio(0);

// 进入程序的主循环,等待回调函数被调用

// 清理并关闭音频设备
SDL_CloseAudio();
SDL_Quit();

参考资料:

音视频流媒体开发课程(从基础到高级,从理论到实践)学习计划、一对一答疑
音视频开发(FFmpeg/WebRTC/RTMP)

整理了一些音视频开发学习资料、面试题 如有需要自行添加群:739729163 领取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/472485.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

串行通信协议 SPI

SPI&#xff08;Serial Peripheral Interface&#xff09;是一种串行通信协议&#xff0c;常用于连接微控制器、存储器、传感器和其他外围设备。SPI通常由一个主设备&#xff08;通常是微控制器&#xff09;和一个或多个从设备组成。 1、SPI通信一般由四根线组成: SCLK&#x…

Java学习笔记20——枚举类型的创建与使用

在实际编程中&#xff0c;存在着这样的“数据集”&#xff0c;它们的数值在程序中是稳定的并且个数是有限的。例如春、夏、秋、冬四个数据元素组成了四季的“数据集”&#xff0c;一月到十二月组成了十二个月份的“数据集”&#xff0c;周一到周五及周六周日组成了每周七天的“…

无论是自学还是培训,找工作都要有舍我其谁的信心

不要说自己不行&#xff0c;要说自己可以&#xff0c;做的很不错。 我在投简历的时候&#xff0c;没有包装&#xff0c;然后在与hr沟通的时候&#xff0c;就会遇到 hr问技术能力如何这样的相关问题&#xff0c;其实我觉得我自己不够自信&#xff0c;想要获得一个offer。必须先…

25 OpenCV模板匹配算法

文章目录 介绍匹配算法介绍matchTemplate 模板匹配算子minMaxLoc 找出图像中的最大值最小值代码示例 介绍 模板匹配就是在整个图像区域发现与给定子图像匹配的小块区域。 所以模板匹配首先需要一个模板图像T&#xff08;给定的子图像&#xff09; 另外需要一个待检测的图像-源图…

必看!想入行嵌入式该准备些什么?

近日&#xff0c;很多人问华妹&#xff1a;想入行嵌入式该怎么准备。很能理解大家对于嵌入式的关注&#xff0c;嵌入式系统是当今科技领域中的重要组成部分&#xff0c;它存在于我们生活的方方面面&#xff0c;各行各业。智能化时代的到来也让嵌入式技术炙手可热&#xff01; …

高通平台初步

网上找的图&#xff0c;大体差不多&#xff0c;但是有些细节有点奇怪。 上面的图有点小怪&#xff0c;主要是Libraies&#xff0c;Framework&#xff0c;App&#xff0c;这部分感觉应该是Google的。其他的没啥问题。 大概整理一下编译流程吧。 首先是安装环境&#xff0c;一些…

如何对静态IP进行测试?静态IP有什么优点?

随着互联网的普及&#xff0c;越来越多的人开始使用动态IP进行上网。但是在某些情况下&#xff0c;我们可能需要使用静态IP进行测试或特定的网络设置。本文将介绍如何获取静态IP进行测试以及静态IP的优点。 一、如何获取静态IP进行测试&#xff1f; 1.联系ISP&#xff08;Int…

Controller中接收数组参数

1、场景 需要根据用户id集合批量删除用户数据&#xff0c;前端使用post请求&#xff0c;controller中参数接收数组参数并根据用户id删除用户基本信息 2、分析处理&#xff1a; 2.1、前端请求类型contentType:application/json 请求体中为json字符串&#xff0c;后端新建一个U…

力扣23. 合并 K 个升序链表(最小堆)

Problem: 23. 合并 K 个升序链表 文章目录 题目描述思路及解法复杂度Code 题目描述 思路及解法 1.创建虚拟头节点dummy并创建辅助指针p指向dummy&#xff1b; 2.创建最小堆minHeap将每个链表的头节点存入; 3.当minHeap不为空时每次让p指向从最小堆堆顶取出的节点node&#xff0…

【小沐学AI】Google AI大模型的一点点学习(Python)

文章目录 1、Google AI简介1.1 Google AI Studio1.2 Bard1.3 PaLM1.4 Gemini1.5 Gemini API1.6 Vertex AI1.7 Gemma 2、Google AI开发2.1 快速入门2.1.1 配置开发环境2.1.2 列出所有模型2.1.3 从文本输入生成文本2.1.4 从图像和文本输入生成文本2.1.5 聊天对话 结语 1、Google …

Covalent Network(CQT)与 Celo 集成,推动 Web3 下一代现实资产解决方案的发展

Covalent Network&#xff08;CQT&#xff09;是一个统一的区块链 API 提供商&#xff0c;其已正式与 Celo 集成&#xff0c;Celo 是一个以移动优先的 EVM 兼容链。这一重要的里程碑旨在提升 Celo 生态系统中开发者的能力&#xff0c;通过授予他们访问关键链上数据的权限&#…

踏“时间”与“空间”前来探寻复杂度的奥妙(Java篇)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

基于WTR096-28SS芯片方案的宠物喂食器实现智能化喂食功能

一、简介 本方案宠物喂食器采用了WTR096-28SS芯片方案来实现智能化的喂食功能。该方案结合了先进的技术和设计理念&#xff0c;提供了便捷、智能和个性化的宠物喂食解决方案。 该宠物喂食器具备定时、定量喂食功能&#xff0c;可以根据主人设定的时间和食物量&#xff0c;自动…

xercesc库保存XML功能实现

目录 一 参考链接 二 运行结果 三 代码 一 参考链接 DOM Programming Guide (apache.org) Xerces-c DOM XML文件的构造_xerces-c domimplementation-CSDN博客 Xerces-c库的使用-CSDN博客 二 运行结果 三 代码 #if 1//参考链接&#xff1a; https://blog.csdn.net/RGBMa…

HarmonyOS NEXT应用开发之SideBarContainer侧边栏淡入淡出动效实现案例

介绍 在2in1或平板上&#xff0c;群聊侧边栏是一种较为常用的功能&#xff0c;虽然HarmonyOS已经具备了基本的动效&#xff0c;但是部分情况下开发者可能有定制侧边栏动效的需求&#xff0c;本例主要介绍了如何基于显式动画实现侧边栏的淡入淡出动效。 效果图预览 使用说明&a…

【区间、栈】算法例题

目录 六、区间 48. 汇总区间 ① 49. 合并区间 ② 50. 插入区间 ② 51. 用最少数量的箭引爆气球 ② 七、栈 52. 有效的括号 ① 53. 简化路径 ② 54. 最小栈 ② 55. 逆波兰表达式求值 ② √- 56. 基本计算器 ③ 六、区间 48. 汇总区间 ① 给定一个 无重复元素 的 …

静态代理IP如何测试?

随着互联网的普及&#xff0c;越来越多的人开始使用动态IP进行上网。但是在某些情况下&#xff0c;我们可能需要使用静态IP进行测试或特定的网络设置。本文将介绍如何获取静态IP进行测试以及静态IP的优点。 一、如何获取静态IP进行测试&#xff1f; 1.联系ISP&#xff08;Int…

DM-达梦数据库实时主备搭建

dm实时主备说明 将主库产生的 Redo日志传输到备库&#xff0c;备库接收并重演Redo日志&#xff0c;从而实现备库与主库的数据同步。 一、环境准备 1.1、配置环境准备 首先搭建实时主备&#xff0c;要规划好机器的&#xff0c;我准备两台机器服务器 主服务器 mast…

7-5 表格输出

题目链接&#xff1a;7-5 表格输出 一. 题目 1. 题目 2. 输入输出格式 3. 限制 二、代码 实现一 1. 代码实现 #include <stdio.h>int main(void){printf("------------------------------------\n\ Province Area(km2) Pop.(10K)\n\ ------------------…

14|CAMEL:通过角色扮演脑暴一个鲜花营销方案

能否让 ChatGPT 自己生成这些引导文本呢&#xff1f; CAMEL 交流式代理框架 CAMEL 框架旨在通过角色扮演来促进交流代理之间的自主合作&#xff0c;并为其“认知”过程提供洞察。这种方法涉及使用启示式提示来指导聊天代理完成任务&#xff0c;同时保持与人类意图的一致性。…