播放音频主要是用SDL库,SDL播放音频有2种模式:
1、Push(推):【程序】主动推送数据给【音频设备】
2、Pull(拉):【音频设备】主动向【程序】拉取数据
代码播放音频的逻辑
1、初始化SDL的子系统
2、打开设备
3、打开文件
4、播放音频
5、清除子系统
播放音频也是耗时操作,所以开启一个子线程去读取数据播放。线程内代码如下
#include "playthread.h"
#include <QFile>
#include <QDebug>
#include <SDL.h>
/*宏定义*/
//文件名
#define FILE_NAME "C:/Users/love/Desktop/QTWork/小河淌水.pcm"
//采样率
#define SAMPLE_RATE 44100
//采样大小
#define SAMPLE_SIZE 16
//采样格式
#define SAMPLE_FORMAT AUDIO_S16LSB
//通道数
#define CHANNELS 2
//音频缓冲区的样本数量.必须是2的幂
#define SAMPLES 1024
//每个样品占用多少个字节
#define BYTE_PER_SAMPLE (SAMPLE_SIZE * CHANNELS / 8)
//文件缓冲区的大小
#define BUFFER_SIZE (BYTE_PER_SAMPLE * SAMPLES)
//
typedef struct {
int len = 0;
//为了记录最后一次读取的数据,方便计算最后一次数据的时长,这样避免还在播放音频的时候线程已经销毁了
int pullLen = 0;
Uint8 *data = nullptr;
} AudioBuffer;
//构造函数
PlayThread::PlayThread(QObject *parent) : QThread(parent)
{
//监听线程执行完成后调用函数回收内存
connect(this, &PlayThread::finished, this, &PlayThread::deleteLater);
}
//析构函数
PlayThread::~PlayThread(){
//断开所有监听
disconnect();
//主动请求提前完成
requestInterruption();
quit();
wait();
qDebug() << this << "播放音频线程析构函数------";
}
/*
一个数据样品的大小:(通道*位深)
希望填充缓冲区的大小:音频缓冲区的样品数量*单个样品的大小
*/
//等待音频设备回调(会回调多次)
void pull_AudioData (void *userdata,
//音频缓冲区。需要往stream中填充PCM数据
Uint8 * stream,
//缓冲区的大小。希望填充的大小(samples * format * channels /8)
int len){
//1、清空stream缓冲区
SDL_memset(stream, 0, len);
//取出AudioBuffer
AudioBuffer *buffer = (AudioBuffer *)userdata;
//因为音频设备一直在回调读取,一开始的时候文件数据还没推过来
//如果数据还没准备好
if(buffer->len <= 0) return;
//2、为了保证数据安全,防止指针越界。取最小值
buffer->pullLen = (len > buffer->len) ? buffer->len : len;
//3、填充数据
//SDL_MIX_MAXVOLUME 软件的音量大小
SDL_MixAudio(stream, buffer->data, buffer->pullLen, SDL_MIX_MAXVOLUME);
//移动指针 前面buffer->pullLen个数据已经被填充了
buffer->data += buffer->pullLen;
buffer->len -= buffer->pullLen;
}
/*
* 播放音频主要是用SDL库
*
* SDL播放音频有2种模式:
* Push(推):【程序】主动推送数据给【音频设备】
* Pull(拉):【音频设备】主动向【程序】拉取数据
*
代码播放音频的逻辑
1、初始化SDL的子系统
2、打开设备
3、打开文件
4、播放音频
5、清除子系统
*/
//线程启动会自动调用内部的run函数
void PlayThread::run(){
qDebug() << this << "播放音频线程开始";
//1、初始化SDL的子系统 0表示成功
//目前只是播放音频,所以只需要初始化Audio子系统即可
if(SDL_Init(SDL_INIT_AUDIO)){//非0即真
qDebug() << this << "SDL_Init error" << SDL_GetError();
return;
}
//2、打开设备
//音频参数 要对应播放的音频,以下参数是我文件夹中的pcm音频的参数
SDL_AudioSpec spec;
//采样率
spec.freq = SAMPLE_RATE;
//采样格式
spec.format = SAMPLE_FORMAT;
//通道数
spec.channels = CHANNELS;
//音频缓冲区数量
spec.samples = SAMPLES;
//这个回调就是设备主动去程序中拉取数据 多次拉取
spec.callback = pull_AudioData;
//传递给回调的参数
AudioBuffer buffer;
spec.userdata = &buffer;
if(SDL_OpenAudio(&spec, nullptr)){//非0即真
qDebug() << this << "SDL_OpenAudio error" << SDL_GetError();
//清除所有初始化的子系统
SDL_Quit();
return;
}
//3、打开文件
QFile file(FILE_NAME);
if(!file.open(QIODevice::ReadOnly)){
//关闭设备
SDL_CloseAudio();
//清除子系统
SDL_Quit();
qDebug() << "file.open Error" << FILE_NAME;
return;
}
//4、播放音频
//当调用这个函数的时候,系统会不停地回调去拉取数据
SDL_PauseAudio(0); //是否停止播放音频,0代表不停止
//如何在这个子线程把文件的数据放到另外一个子线程的那个回调函数里面去填充呢??
//且看下面
//存放从文件中读取的数据
Uint8 data[BUFFER_SIZE];
while (!isInterruptionRequested()) {
//只要从文件中读取的音频数据,还没填充完毕,就跳过
if(buffer.len > 0) continue;
//读取文件
//buffer.len 是实际大小,因为最后可能不足BUFFER_SIZE
buffer.len = file.read((char *)data, BUFFER_SIZE);
if(buffer.len <= 0) {
//计算最后一次读取数据的时长 然后延时。
//否则直接break的话,有可能音频还在播放,线程已经销毁了
// 数据的大小/单个样本大小 = 样本数量
// 样本数/采样率 = 时间
int samples = buffer.pullLen / (SAMPLE_SIZE*CHANNELS / 8);
int ms = (samples * 1000) / SAMPLE_RATE;
qDebug() << "最后一次读取的数据大小:" << buffer.pullLen << "播放时长为:" << ms << "毫秒";
SDL_Delay(ms);
break;
}
//全局变量指向了数据和大小,这样在回调函数中就能用来填充音频缓冲区了
buffer.data = data;
//等待音频数据填充完毕再继续下一轮的读取
//只要音频数据还没有填充完毕,就等待
// while (bufferLen > 0) {
// SDL_Delay(1);//睡眠1毫秒
// }
}// end while
//5、清除子系统
//关闭文件
file.close();
//关闭设备
SDL_CloseAudio();
//清除子系统
SDL_Quit();
qDebug() << this << "播放音频线程正常退出------";
}