二、qt中通过sdl播放pcm视频

前言

在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;

注意:本文为个人记录,新手照搬可能会出现各种问题,请谨慎使用


码字不易,如果这篇博客对你有帮助,麻烦点赞收藏,非常感谢!有不对的地方

猜你喜欢

转载自blog.csdn.net/weixin_44092851/article/details/125372052