首页 > 其他分享 >09_使用SDL播放PCM

09_使用SDL播放PCM

时间:2022-10-05 13:44:23浏览次数:106  
标签:buffer 音频 09 len INIT PCM SDL define

通过命令ffpay播放PCM

可以使用ffplay播放《08_音频录制02_编程》中录制好的PCM文件,测试一下是否录制成功。

播放PCM需要指定相关参数:

  • ar:采样率
  • ac:声道数
  • f:采样格式,sample_fmts + le(小端)或者 be(大端)
    sample_fmts可以通过ffplay -sample_fmts来查询
// 查看pcm文件
ffprobe -ar 44100 -ac 2 -f f32le out.pcm
// 播放pcm文件
ffplay -ar 44100 -ac 2 -f f32le out.pcm

-ar:采样率
-ac:声道数
-f:表示pcm格式,sample_fmts + le(小端)或者 be(大端)
	sample_fmts可以通过ffplay -sample_fmts来查询

其中电脑里支持-f的值可以通过下面命令查询格式

// win
ffmpeg -formats | findstr PCM

//mac
ffmpeg -formats | grep PCM

虽然知道了电脑里支持的pcm格式,但是支持的那么多,我们用那个呢?其实还有一种方法可以确定:就是使用ffmpeg命令录制一个wav文件

// win
ffmpeg -f dshow -i audio="麦克风 (Realtek(R) Audio)" out.wav

// mac
ffmpeg -f avfoundation -i :0 out.wav

我们只需要看Input这里,因为Input是录音设备的一些信息,而Outputwav文件输出的信息,所以可以从Input这里看到pcm格式是f32le

注意:如果pcm格式设置的不对,播放pcm文件就会出现嗤嗤的声音

使用SDL播放PCM

ffplay是基于FFmpeg、SDL两个库实现的。通过编程的方式播放音视频,也是需要用到这2个库。FFmpeg大家都已经清楚了,比较陌生的是SDL。

SDL Logo

简介

SDL(Simple DirectMedia Layer),是一个跨平台的C语言多媒体开发库。

  • 支持Windows、Mac OS X、Linux、iOS、Android
  • 提供对音频、键盘、鼠标、游戏操纵杆、图形硬件的底层访问
  • 很多的视频播放软件、模拟器、受欢迎的游戏都在使用它
  • 目前最新的稳定版是:2.0.14
  • API文档:wiki

下载

SDL官网下载地址:download-sdl2

SDL下载

Windows

由于我们使用的是MinGW编译器,所以选择下载SDL2-devel-2.0.14-mingw.tar.gz

解压后的目录结构如下图所示,跟FFmpeg的目录结构类似,因此就不再赘述每个文件夹的作用。

Windows目录结构

Mac

brew官网可以看得出来:之前执行brew install ffmpeg时,已经顺带安装了SDL,安装目录是:/usr/local/Cellar/sdl2

Mac目录结构

如果没有这个目录,就执行brew install sdl2进行安装即可。

HelloWorld

来个简单的SDL HelloWorld吧,打印一下SDL的版本号。

.pro文件

win32 {
    FFMPEG_HOME = F:/Dev/ffmpeg-4.3.2
    SDL_HOME = D:/SoftwareInstall/SDL2-devel-2.0.14-mingw/x86_64-w64-mingw32
}

macx {
    FFMPEG_HOME = /usr/local/Cellar/ffmpeg/4.3.2
    SDL_HOME = /usr/local/Cellar/sdl2/2.0.14_1
}

INCLUDEPATH += $${FFMPEG_HOME}/include
LIBS += -L$${FFMPEG_HOME}/lib \
        -lavdevice \
        -lavcodec \
        -lavformat \
        -lavutil

INCLUDEPATH += $${SDL_HOME}/include
LIBS += -L$${SDL_HOME}/lib \
        -lSDL2

在Windows环境中,还需要处理一下dll文件,需要讲SDL的bin目录配置成系统环境变量,或者将SDL的bin目录下的SDL2.dll文件考入到项目生成的可执行文件目录下:

cpp代码

#include <SDL2/SDL.h>

SDL_version v;
SDL_VERSION(&v);
// 2 0 14
qDebug() << v.major << v.minor << v.patch;

播放PCM

多线程

playthread.h

#include <QThread>
 
class PlayThread : public QThread {
    Q_OBJECT
private:
    void run();
 
public:
    explicit PlayThread(QObject *parent = nullptr);
    ~PlayThread();
};

playthread.cpp

PlayThread::PlayThread(QObject *parent) : QThread(parent){
    // 在线程结束时自动回收线程的内存
    connect(this, &PlayThread::finished,
            this, &PlayThread::deleteLater);
}

PlayThread::~PlayThread() {
    disconnect();
    // 线程对象的内存回收时,正常结束线程
    requestInterruption();
    quit();
    wait();
}

void PlayThread::run() {
    // 播放音频操作
    // ...
}

初始化子系统

SDL分成好多个子系统(subsystem):

  • Video:显示和窗口管理
  • Audio:音频设备管理
  • Joystick:游戏摇杆控制
  • Timers:定时器
  • ...

目前只用到了音频功能,所以只需要通过SDL_init函数初始化Audio子系统即可。

// 初始化Audio子系统
if (SDL_Init(SDL_INIT_AUDIO)) {
    // 返回值不是0,就代表失败
    qDebug() << "SDL_Init Error" << SDL_GetError();
    return;
}

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

打开音频设备

/* 一些宏定义 */
// 采样率
#define SAMPLE_RATE 44100
// 采样格式
#define SAMPLE_FORMAT AUDIO_S16LSB
// 采样大小
#define SAMPLE_SIZE SDL_AUDIO_BITSIZE(SAMPLE_FORMAT)
// 声道数
#define CHANNELS 2
// 音频缓冲区的样本数量
#define SAMPLES 1024

// 用于存储读取的音频数据和长度
typedef struct {
    int len = 0;
    int pullLen = 0;
    Uint8 *data = nullptr;
} AudioBuffer;

// 音频参数
SDL_AudioSpec spec;
// 采样率
spec.freq = SAMPLE_RATE;
// 采样格式(s16le)
spec.format = SAMPLE_FORMAT;
// 声道数
spec.channels = CHANNELS;
// 音频缓冲区的样本数量(这个值必须是2的幂)
spec.samples = SAMPLES;
// 回调
spec.callback = pullAudioData;
// 传递给回调的参数
AudioBuffer buffer;
spec.userdata = &buffer;

// 打开音频设备
if (SDL_OpenAudio(&spec, nullptr)) {
    qDebug() << "SDL_OpenAudio Error" << SDL_GetError();
    // 清除所有初始化的子系统
    SDL_Quit();
    return;
}

SDL_OpenAudio 有两个参数:

  • 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);

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

打开文件

#define FILENAME "F:/in.pcm"

// 打开文件
QFile file(FILENAME);
if (!file.open(QFile::ReadOnly)) {
    qDebug() << "文件打开失败" << FILENAME;
    // 关闭音频设备
    SDL_CloseAudio();
    // 清除所有初始化的子系统
    SDL_Quit();
    return;
}

开始播放

// 每个样本占用多少个字节
#define BYTES_PER_SAMPLE ((SAMPLE_SIZE * CHANNELS) / 8)
// 文件缓冲区的大小
#define BUFFER_SIZE (SAMPLES * BYTES_PER_SAMPLE)

// 开始播放
SDL_PauseAudio(0);

// 存放文件数据
Uint8 data[BUFFER_LEN];

while (!isInterruptionRequested()) {
    // 只要从文件中读取的音频数据,还没有填充完毕,就跳过
    if (buffer.len > 0) continue;

    buffer.len = file.read((char *) data, BUFFER_SIZE);

    /*
     * SDL_Delay(剩余时间);
     *
     * 采样率(每秒采样的样本次数)用SAMPLE_RATE表示,
     * 每个样本的大小,用BYTES_PER_SAMPLE表示,
     * 
     * 剩余的样本数量 = buffer.pullLen / BYTES_PER_SAMPLE,
     * 剩余时间 = 剩余的样本数量 / 采样率
    */
    // 文件数据已经读取完毕
    if (buffer.len <= 0) {
        // 下面三句代码作用是:推迟线程结束的时间,否则在音频快播放结束时会出现突然停止
        // 剩余的样本数量
        int samples = buffer.pullLen / BYTES_PER_SAMPLE;
        int ms = samples * 1000 / SAMPLE_RATE;
        SDL_Delay(ms);
        break;
    }

    // 读取到了文件数据
    buffer.data = data;
}

上面代码中的if (buffer.len <= 0)里面的三句话主要作用是推迟线程结束的时间,因为这里代码的线程和回调函数pullAudioData中的线程不是同一个,如果在音频快播放结束时,回调函数中的音频缓冲区数据处理没有这里的线程处理快,就会导致这里的线程走释放资源的方法,有时就会导致音频还未完全播放完就结束了。

回调函数

// userdata:SDL_AudioSpec.userdata
// stream:音频缓冲区(需要将音频数据填充到这个缓冲区)
// len:音频缓冲区的大小(SDL_AudioSpec.samples * 每个样本的大小)
void pullAudioData(void *userdata, Uint8 *stream, int len) {
    // 清空stream
    SDL_memset(stream, 0, len);

    // 取出缓冲信息
    AudioBuffer *buffer = (AudioBuffer *) userdata;
    if (buffer->len == 0) return;

    // 取len、bufferLen的最小值(为了保证数据安全,防止指针越界)
    buffer->pullLen = (len > buffer->len) ? buffer->len : len;
    
    // 填充数据
    SDL_MixAudio(stream,
                 buffer->data,
                 buffer->pullLen,
                 SDL_MIX_MAXVOLUME);
    buffer->data += buffer->pullLen;
    buffer->len -= buffer->pullLen;
}

回调函数:

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,注意此参数并不会修改硬件音量;

释放资源

// 关闭文件
file.close();
// 关闭音频设备
SDL_CloseAudio();
// 清理所有初始化的子系统
SDL_Quit();

代码链接

标签:buffer,音频,09,len,INIT,PCM,SDL,define
From: https://www.cnblogs.com/zuojie/p/16755457.html

相关文章

  • 09-RabbitMQ核心API-Fanout Exchange
    FanoutExchange简介不处理路由键,只需要简单的将队列绑定到交换机上发送到交换机的消息都会被转发到与该交换机绑定的所有队列上Fanout交换机转发消息是最快的......
  • 【Ynoi2009】 rla1rmdq 题解 (离线分块 + 线性空复处理)
    洛谷传送门分块。Solution看到是区修区查,还有时限,不难想到是分块,根号复杂度。然后看到空间复杂度,需要离线下来转为线性复杂度。考虑如何分块。观察操作性质,发现节点......
  • CSP202209_3
    CSP202209_3目录CSP202209_3题目思路Code题目防疫大数据思路大模拟。大致题意就是针对当前天,给出以当天开始持续七天的风险地区。同时给出一定数量的用户信息,包括其......
  • 202209_2
    CSP202209_2目录CSP202209_2题目思路DFSDPCode题目何以包邮思路DFS直接DFS,对每件物品根据选与不选进行搜索。当前总价值已经大于答案或者已经满足条件了就显然没有搜......
  • 正经人谁记日记 2022-09-25 周日 21:51:39
    做时间的主人书是人类进步的阶梯作者:DATA_MONK​,转载请注明原文链接​......
  • 2022-09-26 周一
    作者:DATA_MONK​,转载请注明原文链接​......
  • Jenkins 20220927笔记本4
                          ......
  • Jenkins 20220929笔记本5
                                  ......
  • spring-retry 20220929
     1、pom.xml<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-st......
  • SpringCloud重试retry 20220927
    SpringCloud重试retry是一个很赞的功能,能够有效的处理单点故障的问题。主要功能是当请求一个服务的某个实例时,譬如你的User服务启动了2个,它们都在eureka里注册了,那么正常情......