09_使用SDL播放PCM
通过命令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
是录音设备的一些信息,而Output
是wav
文件输出的信息,所以可以从Input
这里看到pcm格式是f32le
注意:如果pcm格式设置的不对,播放pcm文件就会出现嗤嗤的声音
使用SDL播放PCM
ffplay是基于FFmpeg、SDL两个库实现的。通过编程的方式播放音视频,也是需要用到这2个库。FFmpeg大家都已经清楚了,比较陌生的是SDL。
简介
SDL(Simple DirectMedia Layer),是一个跨平台的C语言多媒体开发库。
- 支持Windows、Mac OS X、Linux、iOS、Android
- 提供对音频、键盘、鼠标、游戏操纵杆、图形硬件的底层访问
- 很多的视频播放软件、模拟器、受欢迎的游戏都在使用它
- 目前最新的稳定版是:2.0.14
- API文档:wiki
下载
SDL官网下载地址:download-sdl2。
Windows
由于我们使用的是MinGW编译器,所以选择下载SDL2-devel-2.0.14-mingw.tar.gz。
解压后的目录结构如下图所示,跟FFmpeg的目录结构类似,因此就不再赘述每个文件夹的作用。
Mac
从brew官网可以看得出来:之前执行brew install ffmpeg时,已经顺带安装了SDL,安装目录是:/usr/local/Cellar/sdl2。
如果没有这个目录,就执行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();
09_使用SDL播放PCM的更多相关文章
- FFMPEG学习----使用SDL播放PCM数据
参考雷神的代码: /** * 最简单的SDL2播放音频的例子(SDL2播放PCM) * Simplest Audio Play SDL2 (SDL2 play PCM) * * 本程序使用SDL2播放 ...
- SDL 开发实战(七): 使用 SDL 实现 PCM播放器
在上文,我们做了YUV播放器,这样我们就入门了SDL播放视频.下面我们来做一个PCM播放,即使用SDL播放PCM数据. 下面说明一下使用SDL播放PCM音频的基本流程,主要分为两大部分:初始化SDL. ...
- ffplay代码播放pcm数据
摘抄雷兄 http://blog.csdn.net/leixiaohua1020/article/details/46890259 /** * 最简单的SDL2播放音频的例子(SDL2播放PCM) * ...
- 最简单的视音频播放示例9:SDL2播放PCM
本文记录SDL播放音频的技术.在这里使用的版本是SDL2.实际上SDL本身并不提供视音频播放的功能,它只是封装了视音频播放的底层API.在Windows平台下,SDL封装了Direct3D这类的API ...
- SDL开发笔记(二):音频基础介绍、使用SDL播放音频
若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...
- DirectSound播放PCM(可播放实时采集的音频数据)
前言 该篇整理的原始来源为http://blog.csdn.net/leixiaohua1020/article/details/40540147.非常感谢该博主的无私奉献,写了不少关于不同多媒体库的 ...
- 最简单的视音频播放示例8:DirectSound播放PCM
本文记录DirectSound播放音频的技术.DirectSound是Windows下最常见的音频播放技术.目前大部分的音频播放应用都是通过DirectSound来播放的.本文记录一个使用Direct ...
- 使用AudioTrack播放PCM音频数据(android)
众所周知,Android的MediaPlayer包含了Audio和video的播放功能,在Android的界面上,Music和Video两个应用程序都是调用MediaPlayer实现的.MediaPl ...
- 最简单的视音频播放演示样例8:DirectSound播放PCM
===================================================== 最简单的视音频播放演示样例系列文章列表: 最简单的视音频播放演示样例1:总述 最简单的视音频 ...
- MFC中使用SDL播放音频没有声音的解决方法
本文所说的音频是指的纯音频,不包含视频的那种. 在控制台中使用SDL播放音频,一般情况下不会有问题. 但是在MFC中使用SDL播放音频的时候,会出现没有声音的情况.经过长时间探索,没有找到特别好的解决 ...
随机推荐
- .NET Core开发实战(第18课:日志框架:聊聊记日志的最佳姿势)--学习笔记(下)
18 | 日志框架:聊聊记日志的最佳姿势 除了使用 CreateLogger 指定 logger 的名称,实际上还可以借助容器来构造 logger,通常情况下我们会定义自己的类 namespace L ...
- Kafka-数据出现积压的原因以及如何解决积压问题?
Kafka数据积压的原因有很多,比如消费端处理能力不足.生产端消息发送速度过快等.解决方法也有很多,以下是一些常见的解决方法 : 增加分区数:如果数据量很大,合理的增加Kafka分区数是关键.但是分区 ...
- DP的各种优化小结
动态规划算法(简称动规,DP),是IO中最为常见的,也是最为重要的算法之一.这也就意味着,在各种题目与比赛中它会有很多稀奇古怪的算法和优化,时不时地在你的面前出现一个TLE,MLE和RE来搞你的心态. ...
- 贝壳云P1刷机记录(5.10内核Armbian)
说明 贝壳云基于瑞芯微的RK3328芯片, 芯片介绍, Cortex-A53架构, 4核, 1G内存, 8G eMMC. 板载1个千兆网口, 4个USB3.0. 这个盒子比较赞的地方就是不到百元的价格 ...
- Keil MDK STM32系列(十) Ubuntu下的PlatformIO开发环境
Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...
- ElementUI导入Excel文件
功能介绍 最近用ElementUI做管理系统需要把excel数据导入到系统内,我想这是一个很常见的功能点,把它分享出来,希望对大家有所帮助:) 实现效果 实现步骤 1.定义导入组件 <el-up ...
- 24个javascript最佳实践
1. 使用 === 代替 == JavaScript utilizes two different kinds of equality operators: === | !== and == | != ...
- os.path.relpath和os.path.basename,返回文件路径中的文件名
from os import path print(path.relpath("/home/hpcadmin/lw/demo.py", start="/home/hpca ...
- 图像识别算法--VGG16
前言:人类科技就是不断烧开水(发电).丢石头(航天等).深度学习就是一个不断解方程的过程(参数量格外大的方程) 本文内容: 1.介绍VGG16基本原理 2.VGG16 pytorch复现 图像识别算法 ...
- MongoDB下载和可视化工具NoSQL Manager for MongoDB 软件的下载,连接数据库
在官网下载MongoDB的版本为4.0.28,之前试了好几个高版本和低版本,都不行,最后,4.0.28版本好了.下载网页:https://www.mongodb.com/try/download/co ...