使用 SDL3 播放 PCM 数据
1、Windows 安装 SDL3
cmake_minimum_required(VERSION 3.20)
project(sdl3_test)
set(CMAKE_CXX_STANDARD 20)
set(SDL3_DIR "path/to/sdl3")
set(TARGET_NAME ${PROJECT_NAME})
add_executable(${TARGET_NAME} main.cpp)
target_include_directories(${TARGET_NAME} PRIVATE
${SDL3_DIR}/include
)
target_link_directories(${TARGET_NAME} PRIVATE
${SDL3_DIR}/lib/x64
)
target_link_libraries(${TARGET_NAME} PRIVATE
SDL3.lib
)
if (WIN32)
file(GLOB SDL3_DLLS "${SDL3_DIR}/lib/x64/*.dll")
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SDL3_DLLS} $<TARGET_FILE_DIR:${TARGET_NAME}>
COMMENT "Copying all required DLLs to output directory"
)
endif ()
2、用到的 API
结构体
SDL_AudioStream
:表示音频流,生产者消费者模型
SDL_AudioSpec
:表示音频格式,三个成员:采样率、量化格式、声道数
SDL_AudioStreamCallback
:与 SDL2 不同,流在推进状态时,每秒被固定调用约 100 次
音频 API
SDL_OpenAudioDeviceStream
:打开设备,流是暂停状态,位于起点
SDL_ResumeAudioStreamDevice
:使流转为推进状态,子线程频繁调用流回调/持续消费流中的音频数据
SDL_PauseAudioStreamDevice
:使流转为暂停状态,子线程停止调用流回调/停止消费流中的音频数据
SDL_MixAudio
:对音频数据进行混合处理,同时支持修改音量
SDL_PutAudioStreamData
:向流中添加音频数据,音频流的生产者
3、实战——播放 PCM 文件
需求
- 能够播放
.pcm
文件,格式为 48000 Hz、s16le、2 channels
- 能够设置音量百分比
实现思路
- 主线程是缓冲区数据的生产者
- 回调函数是缓冲区数据的消费者 + 音频流数据的生产者
- SDL3 是音频流数据的消费者
- 每 10 ms 的数据量:
48000 * 2 * 2 / 100 = 1920B
,缓冲区至少要设为其 2 倍来避免卡顿
- 通过 mp4 文件生成 pcm 文件作为程序的输入:
- 转码:
ffmpeg -i a.mp4 -ar 48000 -ac 2 -f s16le 48000_16bit_2ch.pcm
- 测试播放:
ffplay -ar 48000 -ac 2 -f s16le 48000_16bit_2ch.pcm
C++ 代码示例(单缓冲区)
#include <atomic>
#include <string>
#include <fstream>
#include <condition_variable>
extern "C" {
#include <SDL3/SDL.h>
}
static constexpr int kAudioChannels = 2;
static constexpr int kAudioFreq = 48000;
static constexpr float kAudioVolume = 1.0;
static constexpr int kPcmBufferSize = 1024 * 4;
static constexpr SDL_AudioFormat kAudioFormatS16Le = SDL_AUDIO_S16;
static std::atomic_bool buffer_empty{true};
static std::unique_ptr<uint8_t[]> pcm_buffer;
static uint8_t *pcm_buffer_end = nullptr;
static uint8_t *pcm_audio_pos = nullptr;
static std::condition_variable cv;
static std::mutex cv_mutex;
static void SDLCALL AudioStreamCB(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) {
if (buffer_empty.load()) {
cv.notify_one();
return;
}
const auto remain_buffer_size = static_cast<int>(pcm_buffer_end - pcm_audio_pos);
const int copy_size = std::min(additional_amount, remain_buffer_size);
auto mixed_buffer = std::make_unique<uint8_t[]>(copy_size);
SDL_MixAudio(mixed_buffer.get(), pcm_audio_pos, kAudioFormatS16Le, copy_size, kAudioVolume);
SDL_PutAudioStreamData(stream, mixed_buffer.get(), copy_size);
pcm_audio_pos += copy_size;
if (pcm_audio_pos >= pcm_buffer_end) {
buffer_empty.store(true);
cv.notify_one();
}
}
static void PlayPcmAudioInner(const std::string &pcm_file) {
SDL_AudioStream *stream = nullptr;
constexpr SDL_AudioSpec spec = {
.format = kAudioFormatS16Le,
.channels = kAudioChannels,
.freq = kAudioFreq
};
stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, AudioStreamCB, nullptr);
if (!stream) {
SDL_Log("Couldn't open audio device: %s", SDL_GetError());
return;
}
std::ifstream file(pcm_file, std::ios::binary);
if (!file.is_open()) {
SDL_Log("Couldn't open pcm file: %s", pcm_file.c_str());
SDL_DestroyAudioStream(stream);
return;
}
pcm_buffer = std::make_unique<uint8_t[]>(kPcmBufferSize);
SDL_ResumeAudioStreamDevice(stream);
uint64_t total_bytes_read = 0;
while (true) {
file.read(reinterpret_cast<char *>(pcm_buffer.get()), kPcmBufferSize);
const uint64_t bytes_read = file.gcount();
if (bytes_read == 0) {
SDL_Log("End of pcm file");
break;
}
total_bytes_read += bytes_read;
SDL_Log("Read %lld bytes from pcm file, total = %lld", bytes_read, total_bytes_read);
pcm_audio_pos = pcm_buffer.get();
pcm_buffer_end = pcm_audio_pos + bytes_read;
buffer_empty.store(false);
auto lock = std::unique_lock(cv_mutex);
cv.wait(lock, [&]() -> bool { return buffer_empty.load(); });
}
SDL_DestroyAudioStream(stream);
file.close();
}
void PlayPcmAudio(const std::string &pcm_file) {
if (!SDL_Init(SDL_INIT_AUDIO)) {
SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
return;
}
PlayPcmAudioInner(pcm_file);
SDL_Quit();
}