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