【音视频开发】4. 使用 SDL3 播放 PCM 数据

378 阅读3分钟

使用 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; // volume ranges from 0.0 - 1.0
static constexpr int kPcmBufferSize = 1024 * 4; // must > 1920B = 48000*2*2/100, bytes per 10ms
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) {
    // notify main thread that pcm buffer is empty
    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);

    // change audio volume
    auto mixed_buffer = std::make_unique<uint8_t[]>(copy_size);
    SDL_MixAudio(mixed_buffer.get(), pcm_audio_pos, kAudioFormatS16Le, copy_size, kAudioVolume);

    // put mixed audio data to audio stream
    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
    }; // std=c++20

    // open device in a paused state
    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);

    // begin to playback audio
    SDL_ResumeAudioStreamDevice(stream);

    // read pcm data from file
    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);

        // wait until pcm buffer is empty
        auto lock = std::unique_lock(cv_mutex); // std=c++17
        cv.wait(lock, [&]() -> bool { return buffer_empty.load(); });
    }

    SDL_DestroyAudioStream(stream);
    file.close();
}

// this function can only be called once
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();
}