美文网首页
使用SDL播放PCM音频数据

使用SDL播放PCM音频数据

作者: 村口大白杨 | 来源:发表于2021-03-25 11:56 被阅读0次
    一、使用 ffplay 命令行程序播放

    首先使用 ffmpeg 命令行程序抽出 pcm 数据:

    $ ffmpeg -i 那又如何.mp3 -ar 44100 -ac 2 -f s16le -acodec pcm_s16le out.pcm
    

    使用 ffplay 命令行程序播放 pcm 数据:

    $ ffplay -ar 44100 -ac 2 -f s16le out.pcm
    

    -ar 采样率
    -ac 声道数
    -f 采样格式

    在 Mac 平台使用 ffmpeg -formats | grep PCM 查看更多采样格式:

    DE alaw            PCM A-law
    DE f32be           PCM 32-bit floating-point big-endian
    DE f32le           PCM 32-bit floating-point little-endian
    DE f64be           PCM 64-bit floating-point big-endian
    DE f64le           PCM 64-bit floating-point little-endian
    DE mulaw           PCM mu-law
    DE s16be           PCM signed 16-bit big-endian
    DE s16le           PCM signed 16-bit little-endian
    DE s24be           PCM signed 24-bit big-endian
    DE s24le           PCM signed 24-bit little-endian
    DE s32be           PCM signed 32-bit big-endian
    DE s32le           PCM signed 32-bit little-endian
    DE s8              PCM signed 8-bit
    DE u16be           PCM unsigned 16-bit big-endian
    DE u16le           PCM unsigned 16-bit little-endian
    DE u24be           PCM unsigned 24-bit big-endian
    DE u24le           PCM unsigned 24-bit little-endian
    DE u32be           PCM unsigned 32-bit big-endian
    DE u32le           PCM unsigned 32-bit little-endian
    DE u8              PCM unsigned 8-bit
    DE vidc            PCM Archimedes VIDC
    
    二、使用 SDL 编程实现 PCM 播放

    1、安装 sdl2(如果已安装忽略这一步,如果是使用Homebrew安装的 FFmpeg 也可以省略这一步,因为通过 brew 安装的 FFmpeg 依赖了 sdl2):

    $ brew install sdl2
    

    2、然后使用 Qt 新建一个名为 02_play_pcm_example 的工程

    $ tree
    .
    |____playthread.cpp
    |____mainwindow.h
    |____mainwindow.ui
    |____mainwindow.cpp
    |____02_play_pcm_example.pro
    |____main.cpp
    |____playthread.h
    

    3、在 04_sdl_play_pcm.pro 文件中配置 SDL 头文件和静态库的位置(如果没有安装 SDL 需要先安装,):

    INCLUDEPATH += -I "/usr/local/Cellar/sdl2/2.0.14_1/include"
    LIBS += -L /usr/local/Cellar/sdl2/2.0.14_1/lib -lSDL2
    

    接着在 mainwindow.cpp 中引入 sdl2 头文件:

    #include <SDL2/SDL.h>
    

    打开 mainwindow.ui 文件添加一个 Push Button ,并且更名为 playButton,然后右键选择转到槽:

    在槽函数中添加打印 SDL版本号的代码:

    void MainWindow::on_playButton_clicked()
    {
        SDL_version version;
        SDL_VERSION(&version);
        qDebug() << version.major << version.minor << version.patch;
    }
    

    运行程序点击播放PCM按钮,会打印:

    13:52:48: Starting /Users/mac/Desktop/QtWorkSpace/build-04_sdl_play_pcm-Desktop_Qt_6_0_2_clang_64bit-Debug/04_sdl_play_pcm.app/Contents/MacOS/04_sdl_play_pcm ...
    2 0 14
    

    说明我们引入 sdl2 成功了!

    4、接下来初始化 SDL 子系统:

    if (SDL_Init(SDL_INIT_AUDIO)) {
        qDebug() << "初始化 SDL 失败:" << SDL_GetError();
        return;
    }
    atexit(SDL_Quit);
    

    初始化成功后,就可以使用 SDL 子系统完成相应的任务了,当完成所有工作需要退出程序时,必须使用 SDL_Quit 清除所有子系统。如果初始化失败,使用 SDL_GetError 函数获取错误原因。atexit 是 C 语言标准库函数,作用是向系统注册传进来的函数,以便程序结束时调用该函数,此处希望程序结束时清空 SDL 所有子系统。

    flags 参数取值:

    // 定时器
    #define SDL_INIT_TIMER          0x00000001u  
    // 音频
    #define SDL_INIT_AUDIO          0x00000010u
    // 视频
    #define SDL_INIT_VIDEO          0x00000020u  /**< SDL_INIT_VIDEO implies SDL_INIT_EVENTS */
    // 游戏控制杆
    #define SDL_INIT_JOYSTICK       0x00000200u  /**< SDL_INIT_JOYSTICK implies SDL_INIT_EVENTS */
    // 触摸屏
    #define SDL_INIT_HAPTIC         0x00001000u
    // 游戏控制器
    #define SDL_INIT_GAMECONTROLLER 0x00002000u  /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */
    // 事件
    #define SDL_INIT_EVENTS         0x00004000u
    // 传感器
    #define SDL_INIT_SENSOR         0x00008000u
    // 错误捕获
    #define SDL_INIT_NOPARACHUTE    0x00100000u  /**< compatibility; this flag is ignored. */
    // 全部子系统
    #define SDL_INIT_EVERYTHING ( \
                    SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | \
                    SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR \
                )
    

    初始化成功返回 0,初始化失败函数返回值为 -1,函数只接受各个子系统的常量作为参数。初始化音频子系统,传入参数 SDL_INIT_AUDIO;初始化视频子系统传入 SDL_INIT_VIDEO;并且可初始化一个或者多个子系统,例如同时初始化音频和视频子系统,传入 SDL_INIT_AUDIO | SDL_INIT_VIDEO

    5、打开音频设备:

    SDL_AudioSpec spec;
    spec.freq = sampleRate;
    spec.format = format; 
    spec.channels = channels;
    spec.samples = 1024;
    spec.callback = call_back;
    
    // 打开音频设备
    if (SDL_OpenAudio(&spec, nullptr)) {
        qDebug() << "打开音频设备失败:" << SDL_GetError();
        SDL_Quit();
        return;
    }
    

    SDL_OpenAudio 有两个参数:

    extern DECLSPEC int SDLCALL SDL_OpenAudio(SDL_AudioSpec * desired, SDL_AudioSpec * obtained);
    

    desired:期望参数,播放的音频对应的参数;
    obtained:实际硬件设备参数,可传 nullptr;

    SDL_AudioSpec 结构体:

    typedef struct SDL_AudioSpec
    {
        // 采样率
        int freq;                   /**< DSP frequency -- samples per second */
        // 音频数据格式 
        SDL_AudioFormat format;     /**< Audio data format */
        // 声道数
        Uint8 channels;             /**< Number of channels: 1 mono, 2 stereo */
        // 音频缓冲区静音值
        Uint8 silence;              /**< Audio buffer silence value (calculated) */
        // 采样帧大小
        Uint16 samples;             /**< Audio buffer size in sample FRAMES (total samples divided by channel count) */
        // 兼容性参数
        Uint16 padding;             /**< Necessary for some compile environments */
        // 音频缓冲区大小
        Uint32 size;                /**< Audio buffer size in bytes (calculated) */
        // 填充音频缓冲区回调函数
        SDL_AudioCallback callback; /**< Callback that feeds the audio device (NULL to use SDL_QueueAudio()). */
        // 用户自定义数据,
        void *userdata;             /**< Userdata passed to callback (ignored for NULL callbacks). */
    } SDL_AudioSpec;
    

    回调函数:

    typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream, int len);
    

    当音频设备需要更多数据时会调用该函数;

    6、打开 PCM 文件:

    QFile file(filePath);
    if (!file.open(QFile::ReadOnly)) {
        qDebug() << "打开 PCM 文件失败:" << filePath;
        SDL_CloseAudio();
        SDL_Quit();
        return;
    }
    

    7、开始播放:

    SDL_PauseAudio(0);
    

    参数 pause_on 设置为 0 开始播放音频数据;设置为 1 播放静音值;设置为 0 时 SDL 会调用我们提供的回调函数:

    void call_back(void *userdata, Uint8 * stream, int len)
    {
        // SDL2之后需要先清空需要填充的音频缓冲区
        SDL_memset(stream, 0, len);
        if (bufferLen <= 0) return;
        int readLen = len < bufferLen ? len : bufferLen;
        // 填充音频缓冲区
        SDL_MixAudio(stream, (Uint8 *)bufferData, readLen, SDL_MIX_MAXVOLUME);
        bufferData += readLen;
        bufferLen -= readLen;
    }
    

    回调函数:

    typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream, int len);
    

    userdata:SDL_AudioSpec 结构体中用户自定义的数据,可不用;
    stream:指向音频缓冲区的指针;
    len:音频缓冲区大小;

    混音函数:

    extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src, Uint32 len, int volume);
    

    dst:目标数据,这里传入音频缓冲区指针 stream;
    src:音频数据,这里传入我们读出的 PCM 数据;
    len:音频数据长度,这里传入音频缓冲区大小 len;
    volume:音量,范围 0~128,这里我们传入 SDL_MIX_MAXVOLUME,注意此参数并不会修改硬件音量;

    8、读取 PCM 文件:

    char data[BUFFER_SIZE]; // 4096
    while (!isInterruptionRequested()) {
        bufferLen = file.read(data, BUFFER_SIZE);
        if (bufferLen < 0) break;
        bufferData = data;
        // 延时等待音频播放完毕 
        while (bufferLen > 0) {
            SDL_Delay(1);
        }
    }
    

    9、播放结束,最后关闭音频设备,清除 SDL 子系统:

    // 停止音频处理,关闭音频设备
    SDL_CloseAudio();
    // 清除所有初始化的 SDL 子系统
    SDL_Quit();
    
    三、代码
    #include <QDebug>
    #include <QFile>
    #include <SDL2/SDL.h>
    
    #define BUFFER_SIZE 4096
    
    QString filePath;
    int sampleRate; // 44100
    int format; // AUDIO_S16LSB
    int channels; // 2
    
    int bufferLen;
    char *bufferData;
    
    // SDL 回调取每一帧播放数据
    
    void call_back(void *userdata, Uint8 * stream, int len)
    {
        // SDL2之后需要先清空需要填充的音频缓冲区
        SDL_memset(stream, 0, len);
        if (bufferLen <= 0) return;
        int readLen = len < bufferLen ? len : bufferLen;
        // 填充音频缓冲区
        SDL_MixAudio(stream, (Uint8 *)bufferData, readLen, SDL_MIX_MAXVOLUME);
        bufferData += readLen;
        bufferLen -= readLen;
    }
    
    PlayThread::PlayThread(QObject *parent) : QThread(parent)
    {
        connect(this, &PlayThread::finished, this, &PlayThread::deleteLater);
    }
    
    PlayThread::~PlayThread()
    {
        disconnect();
        requestInterruption();
        quit();
        wait();
    
        qDebug() << "PlayThread 析构函数";
    }
    
    void PlayThread::run()
    {
        qDebug() << "开始播放";
    
        // 初始化 SDL 音频子系统
        if (SDL_Init(SDL_INIT_AUDIO)) {
            qDebug() << "初始化 SDL 失败:" << SDL_GetError();
            return;
        }
        // atexit 是 C 语言标准库函数,作用是向系统注册传进来的函数,以便程序结束时调用该函数
        // 程序结束时清空 SDL 所有子系统
        atexit(SDL_Quit);
    
        SDL_AudioSpec spec;
        spec.freq = sampleRate;
        spec.format = format; //AUDIO_S16LSB
        spec.channels = channels;
        spec.samples = 1024;
        spec.callback = fill_audio;
    
        // 打开音频设备
        if (SDL_OpenAudio(&spec, nullptr)) {
            qDebug() << "打开音频设备失败:" << SDL_GetError();
            SDL_Quit();
            return;
        }
    
        // 打开 PCM 文件
        QFile file(filePath);
        if (!file.open(QFile::ReadOnly)) {
            qDebug() << "打开 PCM 文件失败:" << filePath;
            SDL_CloseAudio();
            SDL_Quit();
            return;
        }
    
        // 开始播放
        SDL_PauseAudio(0);
    
        // 读取 PCM 文件
        char data[BUFFER_SIZE];
        while (!isInterruptionRequested()) {
            bufferLen = file.read(data, BUFFER_SIZE);
            if (bufferLen < 0) break;
            bufferData = data;
            // 延时等待音频播放完毕
            while (bufferLen > 0) {
                SDL_Delay(1);
            }
        }
    
        // 停止音频处理,关闭音频设备
        SDL_CloseAudio();
        // 关闭所有 SDL 子系统,清理 SDL 所占资源
        SDL_Quit();
    
        qDebug() << "播放结束";
    }
    

    相关文章

      网友评论

          本文标题:使用SDL播放PCM音频数据

          本文链接:https://www.haomeiwen.com/subject/kwmlhltx.html