使用QT+FFmpeg制作录音软件

一、前言:

基于windows平台完成一个简单的录音设备,重点在展示使用方法,删减掉健壮性代码(减少非主干业务对您的干扰);另外,我的代码绝对都是可以运行的;

二、环境:

  • FFmpeg:
    • 版本:ffmpeg-5.1.4
  • Log库:
    • 开源的easylogging++库
  • UI:
    • QT5.15.2
  • 操作系统:
    • ​​​​​​​windows10(其实qt之所以流传广泛,就是因为跨平台啦,在下其实平时主要混迹在linux和android系统)
  • 不会编译库的,看我的另外一篇帖子:

三、架构:

  •         流程图:
  • 流程:
    • UI线程启动之后,点击开始录像的Button,会生成Capture线程去录影;
    • UI线程负责收集Button的事件,根据事件类型负责给Capture采集线程安排活;
  • 补充:
    • 主要有两个线程UI线程和Capture(采集声音)线程;
    • 一般UI线程不做耗时工作,否则,用户感觉软件老卡住会骂街;
    • 耗时操作一般开子线程去处理

四、代码:

1、目录结构:

根目录下:

│  CMakeLists.txt
│  CMakeLists.txt.user
│  main.cpp
│  MainWindow.cpp
│  MainWindow.h
│  MainWindow.ui
│  PCaptureThread.cpp
│  PCaptureThread.h
├─build
└─FFmpegController
    │  CMakeLists.txt
    │  PAudioCapture.cpp
    │  PAudioCapture.h
    │  PPureCommon.h
    │
    └─third-party
        ├─easyloggingpp
        │      CMakeLists.txt
        │      easylogging++.cc
        │      easylogging++.h
        │
        └─ffmpeg
            ├─bin
            ├─include
            ├─lib
            └─share

  • main.cpp是程序入口
  • MainWindow是窗口类
  • PCaptureThread是音频采集线程类
  • PAudioCapture真正还行采集(干活的)类
  • third-party存一些常见库(ffmpeg、SDL、aac、x264),这个例子用ffmpeg库就行了

2、具体代码:

上干货。。。。

MainWindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "PCaptureThread.h"

QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

public slots:
    void SlotStartCapture();
    void SlotStopCapture();

    void SlotRecordButtonClicked();

signals:
    void SignalCaptureStarted();
    void SignalCaptureStopped();

private:
    Ui::MainWindow *ui;
    PCaptureThread *m_captureThread{nullptr};
    bool m_isRecording{false};
};
#endif // MAINWINDOW_H

  MainWindow.cpp

#include "MainWindow.h"
#include "./ui_MainWindow.h"
#include "easylogging++.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    connect(ui->pushButton_StartRecord, &QPushButton::clicked, this, &MainWindow::SlotRecordButtonClicked);
    connect(this, &MainWindow::SignalCaptureStarted, this, &MainWindow::SlotStartCapture);
    connect(this, &MainWindow::SignalCaptureStopped, this, &MainWindow::SlotStopCapture);

    m_captureThread = new PCaptureThread(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::SlotStartCapture()
{
    m_captureThread->start();
    m_isRecording = true;
}

void MainWindow::SlotStopCapture()
{
    if (m_captureThread == nullptr) {
        LOG(ERROR) << "[MainWindow]slot stop capture failed, cause m_captureThread is null!";
        return;
    }
    m_captureThread->StopRecord();
    m_isRecording = false;
}

void MainWindow::SlotRecordButtonClicked()
{
    if (m_isRecording) {
        ui->pushButton_StartRecord->setText("录影");
        emit SignalCaptureStopped();
        LOG(INFO) << "[MainWindow]stop recording button clicked!";
    } else {
        ui->pushButton_StartRecord->setText("停止");
        emit SignalCaptureStarted();
        LOG(INFO) << "[MainWindow]start recording button clicked!";
    }
}

   PCaptureThread.h

#ifndef PCAPTURETHREAD_H
#define PCAPTURETHREAD_H

#include <QThread>
#include <QWaitCondition>
#include <QMutex>
#include "PAudioCapture.h"
#include "easylogging++.h"

class PCaptureThread : public QThread
{
    Q_OBJECT
public:
    explicit PCaptureThread(QObject *parent = nullptr);
    ~PCaptureThread();

    void run() override;

    void StopRecord();

private:
    PAudioCapture *m_audioCapture;
};

#endif // PCAPTURETHREAD_H

PCaptureThread.cpp

#include "PCaptureThread.h"
#include <QThread>

PCaptureThread::PCaptureThread(QObject *parent) : m_audioCapture(new PAudioCapture())
{
}

PCaptureThread::~PCaptureThread()
{
}

void PCaptureThread::run()
{
    m_audioCapture->StartRecord();
    LOG(INFO) << "[PStartThread] thread run!";
}

void PCaptureThread::StopRecord()
{
    m_audioCapture->StopRecord();
    LOG(INFO) << "[PCaptureThread] stop waked!";
}

PPureCommon.h

//
// Created by Ricky on 2024/5/5.
//

#ifndef P_PURE_COMMON_H
#define P_PURE_COMMON_H

#ifndef PURE_OK
#define PURE_OK 0
#endif

#ifndef PURE_ERROR
#define PURE_ERROR 1
#endif

#endif //P_PURECOMMON_H

PAudioCapture.h

//
// Created by Ricky on 2024/5/2.
//

#ifndef AUDIO_CAPTURE_H
#define AUDIO_CAPTURE_H

extern "C" {
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavdevice/avdevice.h"
#include "SDL.h"
}
#include "PPureCommon.h"

#include <mutex>
/**
 * 音频数据采集
 * */
class PAudioCapture {
public:
    PAudioCapture();
    ~PAudioCapture();

    void StartRecord();
    void StopRecord();

    bool GetRecordState();

private:
    bool m_isRecording{false};
    std::mutex m_mutex; // 互斥锁
};


#endif // AUDIO_CAPTURE_H

PAudioCapture.cpp

//
// Created by Ricky on 2024/5/2.
//

#include "PAudioCapture.h"
#include "PPureCommon.h"
#include "easylogging++.h"

#include <string>

namespace {
const std::string DEVICE_NAME = "audio=麦克风 (C670i FHD Webcam)"; // 商用产品中应该使用dshow的api获取
// const std::string DEVICE_NAME = "audio=麦克风 (Jabra EVOLVE 30 II)"; // 商用产品中应该使用dshow的api获取
const std::string INPUT_FORMAT_NAME = "dshow";
const std::string OUTPUT_FILE_NAME = "E:\\res\\audio_capture.pcm";
}

PAudioCapture::PAudioCapture()
{
    LOG(INFO) << "[PAudioCapture]avcodec version is:" << avcodec_version() << "!";
}

PAudioCapture::~PAudioCapture()
{
}

void PAudioCapture::StartRecord()
{
    if (m_isRecording) {
        return;
    }
    avdevice_register_all();
    char errors[1024] = {};
    AVFormatContext *fmtCtx = nullptr;
    AVDictionary *options = nullptr;
    const AVInputFormat *fmt = av_find_input_format(INPUT_FORMAT_NAME.c_str());
    if (fmt == nullptr) {
        LOG(ERROR) << "[PAudioCapture]av_find_input_format failed!";
        return;
    }

    int ret = avformat_open_input(&fmtCtx, DEVICE_NAME.c_str(), fmt, &options);
    if (ret != 0) {
        av_strerror(ret, errors, 1024);
        LOG(ERROR) << "[PAudioCapture]avformat_open_input failed, ret=" << ret << ", reason:" << errors;
        return;
    }

    FILE *outFile = fopen(OUTPUT_FILE_NAME.c_str(), "wb+");
    if (outFile == nullptr) {
        avformat_close_input(&fmtCtx);
        LOG(ERROR) << "[PAudioCapture]fopen outFile failed!";
        return;
    }

    // 置为录制状态
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_isRecording = true;
    }

    AVPacket pkt;
    do {
        ret = av_read_frame(fmtCtx, &pkt);
        if (ret < 0) {
            av_strerror(ret, errors, 1024);
            LOG(ERROR) << "[PAudioCapture]av_read_frame failed, ret=" << ret << ", reason:" << errors;
            break;
        }

        // 将PCM数据写入文件中
        fwrite(pkt.data, pkt.size, 1, outFile);
        fflush(outFile);
        av_packet_unref(&pkt);
    } while (m_isRecording);

    fclose(outFile);
    avformat_close_input(&fmtCtx);
    LOG(INFO) << "[PAudioCapture]start record end!";
}

void PAudioCapture::StopRecord()
{
    std::lock_guard<std::mutex> lock(m_mutex);
    m_isRecording = false;
    LOG(INFO) << "[PAudioCapture] stop recording!";
}

bool PAudioCapture::GetRecordState()
{
    std::lock_guard<std::mutex> lock(m_mutex);
    return m_isRecording;
}

五、运行效果:

就开始录音了,再录取一段时间,暂停:

发现生成了一个文件:

生成的是PCM数据,也就是没有进行任何编解码的原始数据,因此,我们可以使用ffplay来播放,当然原始数据可没有一些meta信息来说明音频的通道数、采样率、采样大小,需要我们指定;

我再贴心的为您复制下命令行,免得您截图不好复制;

ffplay -ar 44100 -ac 2 -f s16le audio_capture.pcm

这样就可以听到声音了!!!

多嘴:

有个小朋友又问了,这些参数都是哪儿来的,你为啥填44100,不填48000?

答:这其实是我电脑mic的采集规格,所以,要学会查看windows的mic规格,命令如下:

看看看我们电脑又那些mic设备:

	// 查看麦克风名称:
	ffmpeg -list_devices true -f dshow -i dummy

运行结果如下:

其实也就是我代码中的:

"audio=麦克风 (C670i FHD Webcam)"

六、总结:

  1. 根据"依赖倒置原则",高层模块不应该依赖于低层模块,二者都应该依赖于抽象,所以采集最好抽象出一个AudioCaptureInterface来(我懒,但是我替读者操心,说一嘴)
  2. 这种读取I/O相关的操作,比如写文件,操作系统一般是攒够了一段再往文件写,频繁读写磁盘是很低效的(当然,我这儿还是懒,直接调用fflush给冲进文件了)
  3. 咱这是一个demo,里面各种野路子硬编码(比如麦克风名称,文件名啥的),诸位小朋友写商业产品记得改成变量;
  4. 暂时没想出来,就这样吧!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值