前言
在qt中实现ffmpeg通过外接设备录制音频,因c语言相关代码执行步骤较为复杂,于是做此记录。
ffmpeg系列博客会陆续记录下来。
测试环境:
- ffmpeg的shared版本
- windows环境
- qt5.12
- sdl2.0.22(mingw编译器)
注意:播放音频在命令行使用的是ffplay,其底层是通过ffmpeg和sdl实现音频的播放,故代码中需要使用sdl库
链接:https://pan.baidu.com/s/1dx-YgjycVh_py8FwUqVtNQ?pwd=n4dd
提取码:n4dd
–来自百度网盘超级会员V3的分享
qt中实现使用sdl播放视频
在使用sdl前,需要配置导入库(不需要配置sdl的环境变量),这里不多介绍
sdl播放pcm视频思路:
1、导入库(.pro文件中导入库的时候需要多加一个DEFINES)
DEFINES += QT_DEPRECATED_WARNINGS \
SDL_MAIN_HANDLED
SDL_HOME = D:/1c++/SDL2-devel-2.0.22-mingw/SDL2-2.0.22/x86_64-w64-mingw32
INCLUDEPATH += $${
SDL_HOME}/include
LIBS += -L$${
SDL_HOME}/lib \
-lSDL2
2、导入头文件
extern "C"{
#include <SDL2/SDL.h>
}
3、初始化子系统
sdl通过不同的子系统实现对应的功能
//相关子系统的名称需要在sdl源码或官方文档中查
extern DECLSPEC int SDLCALL SDL_Init(Uint32 flags);
链接:sdl官方文档
4、打开音频设备
//desired为必填项,obtained可为null,需要用SDL_CloseAudio()关闭
extern DECLSPEC int SDLCALL SDL_OpenAudio(SDL_AudioSpec * desired,
SDL_AudioSpec * obtained);
特别介绍一下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) */ //样本数,必须是2的幂次方
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;
SDL_AudioFormat结构体的值:
#define AUDIO_U8 0x0008 /**< Unsigned 8-bit samples */
#define AUDIO_S8 0x8008 /**< Signed 8-bit samples */
#define AUDIO_U16LSB 0x0010 /**< Unsigned 16-bit samples */
#define AUDIO_S16LSB 0x8010 /**< Signed 16-bit samples */
#define AUDIO_U16MSB 0x1010 /**< As above, but big-endian byte order */
#define AUDIO_S16MSB 0x9010 /**< As above, but big-endian byte order */
#define AUDIO_U16 AUDIO_U16LSB
#define AUDIO_S16 AUDIO_S16LSB
SDL_AudioCallback结构体的值(是一个回调函数,该回调函数需要字节在别处自定义):
// userdata:SDL_AudioSpec.userdata
// stream:音频缓冲区(需要将音频数据填充到这个缓冲区)
// len:音频缓冲区的大小(SDL_AudioSpec.samples * 每个样本的大小
typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream,
int len);
该回调函数要如何自定义,请继续往下看
5、播放音频
//0表示播放,其他数字表示暂停,只要开启就会自动调用回调函数
SDL_PauseAudio(0);
当播放音频时,自动循环执行回调函数,并不停地将userdata传入回调函数
所以回调函数需要实现的是能循环将userdata中的数据写入缓冲区,这里需要用到SDL_MixAudio()函数实现这个功能
extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src,
Uint32 len, int volume);
5、关闭文件、音频设备、子系统
//清除所有子系统
SDL_Quit();
完整代码:(我这里是将功能封装在线程里,其中还考虑了线程的关闭问题)
AudioStartThread.h
#ifndef AUDIOSTARTTHREAD_H
#define AUDIOSTARTTHREAD_H
#include <QObject>
#include <QThread>
#include <SDL2/SDL.h>
class AudioStartThread : public QThread
{
Q_OBJECT
public:
explicit AudioStartThread(QObject *parent = nullptr);
~AudioStartThread();
private:
void run() override;
signals:
};
#endif // AUDIOSTARTTHREAD_H
AudioStartThread.cpp
#include "audiostartthread.h"
#include <QDebug>
#include <QFile>
extern "C"{
#include <SDL2/SDL.h>
}
//采样率
#define SAMPLE_RATE 44100
// 采样格式
#define SAMPLE_FORMAT AUDIO_S16LSB
// 采样大小,等同于SAMPLE_FORMAT & 0xFF
#define SAMPLE_SIZE SDL_AUDIO_BITSIZE(SAMPLE_FORMAT)
// 声道数
#define CHANNELS 2
// 音频缓冲区的样本数量,必须是2的幂次方
#define SAMPLES 1024
// 每个样本占用多少个字节 ,向右移三位表示除以8,效率更高
#define BYTES_PER_SAMPLE ((SAMPLE_SIZE * CHANNELS) >> 3)
// 定义缓冲区大小,SAMPLES*CHANNELS*SAMPLE_FORMAT/8
#define BUFFER_SIZE (SAMPLES * BYTES_PER_SAMPLE)
#ifdef Q_OS_WIN
// PCM文件的文件名
#define FILENAME "E:/media/out.pcm"
#else
#define FILENAME "/Users/mj/Desktop/out.pcm"
#endif
typedef struct {
int len = 0;
int pullLen = 0;
Uint8 *data = nullptr;
} AudioBuffer;
// userdata:SDL_AudioSpec.userdata
// stream:音频缓冲区(需要将音频数据填充到这个缓冲区)
// len:音频缓冲区的大小(SDL_AudioSpec.samples * 每个样本的大小
void pull_audio_data(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;
}
AudioStartThread::AudioStartThread(QObject *parent) : QThread(parent)
{
// 当监听到线程结束时(finished),就调用deleteLater回收内存
connect(this,&AudioStartThread::finished,this,[=](){
this->deleteLater();
qDebug()<<"线程结束";
});
}
AudioStartThread::~AudioStartThread()
{
//强制关闭窗口时,线程也能安全关闭
requestInterruption();
wait();
qDebug()<<"析构函数";
}
void AudioStartThread::run()
{
if(SDL_Init(SDL_INIT_AUDIO)){
qDebug()<<"初始化子系统失败";
SDL_Quit();
return;
}
//配置音频设备的参数
SDL_AudioSpec audio_spec;
audio_spec.freq=SAMPLE_RATE;
audio_spec.format=AUDIO_S16LSB;
audio_spec.channels=CHANNELS;
audio_spec.samples=SAMPLES;
audio_spec.callback= pull_audio_data;
// 传递给回调的参数
AudioBuffer buffer;
audio_spec.userdata = &buffer;
if(SDL_OpenAudio(&audio_spec,nullptr)){
qDebug()<<"打开设备失败";
SDL_Quit();
return;
}
QFile file(FILENAME);
if(!file.open(QIODevice::ReadOnly)){
qDebug()<<"文件打开失败";
SDL_CloseAudio();
SDL_Quit();
return;
}
// 存放文件数据
Uint8 data[BUFFER_SIZE];
//0表示播放,其他数字表示暂停,只要开启就会自动调用回调函数
SDL_PauseAudio(0);
while(!isInterruptionRequested()){
//当没发出中断请求时,执行循环体
// 只要从文件中读取的音频数据,还没有填充完毕,就跳过
if(buffer.len>0){
continue;
}
buffer.len = file.read((char *) data, BUFFER_SIZE);
// 文件数据已经读取完毕,防止数据还没读完,线程就结束了
if (buffer.len <= 0) {
// 剩余的样本数量
int samples = buffer.pullLen / BYTES_PER_SAMPLE;
int ms = samples * 1000 / SAMPLE_RATE;
SDL_Delay(ms);
break;
}
// 读取到了文件数据
buffer.data = data;
}
// 关闭文件
file.close();
// 关闭音频设备
SDL_CloseAudio();
//清除所有子系统
SDL_Quit();
}
注意:该代码对可能出现的问题都进行了一定的优化,读者使用时只需关注关键代码
线程调用
void MainWindow::on_pushButton_play_clicked()
{
if(!is_record_start){
audio_start_thread=new AudioStartThread(this);
audio_start_thread->start();
connect(audio_start_thread,&AudioStartThread::finished,audio_start_thread,[=](){
ui->pushButton_play->setText("开始播放");
is_record_start=false;
});
qDebug()<<"开始播放";
ui->pushButton_play->setText("结束播放");
is_record_start=true;
}else{
audio_start_thread->requestInterruption();
audio_start_thread->wait();
audio_start_thread=nullptr;
qDebug()<<"结束播放";
ui->pushButton_play->setText("开始播放");
is_record_start=false;
}
}
注意:.h文件中提前声明了以下全局变量
AudioStartThread *audio_start_thread=nullptr;
bool is_record_start=false;
注意:本文为个人记录,新手照搬可能会出现各种问题,请谨慎使用
码字不易,如果这篇博客对你有帮助,麻烦点赞收藏,非常感谢!有不对的地方