Qt实现麦克风数据采集并播放
自定义 IODevice 实现麦克风数据采集和播放
这篇注记给出一个自定义的 MircrophoneIODevice
和 bilateralAudioPlay
类,用于实现从麦克风采集音频数据,并通过默认的音频输出设备播放。
一、功能概述
-
数据采集和播放:
- 使用自定义的
MircrophoneIODevice
,将麦克风采集到的数据写入缓冲。 - 通过 Qt 提供的
QAudioSource
和QAudioSink
,实现音频的输入和输出。
- 使用自定义的
-
音频分析:
- 采集到的数据通过
computeAudioData
方法计算左右声道的 RMS 和 dBFS,用于添加音量计算功能。
- 采集到的数据通过
-
自定义 IO设备:
- 通过编写
QIODevice
的readData
和writeData
方法,实现数据的转换和处理。
- 通过编写
-
添加异步计算:
- 使用
QThreadPool
和std::bind
将音量计算过程移至异步线程。
- 使用
二、代码详解
1. MircrophoneIODevice
类详解
- 这是一个自定义的
QIODevice
。
(一) 方法功能
-
start()
和stop()
start()
打开设备,允许输入和输出。stop()
关闭设备,清除缓冲。
-
readData()
和writeData()
readData()
从缓冲中读取数据,并调用异步线程计算音量。writeData()
将数据写入缓冲,并触发等待的线程继续进行。
-
computeAudioData()
- 通过采样值计算左右声道的 RMS 和 dBFS,渲染音量值。
- 将计算结果通过
SignalCenter
发送信号。
-
bytesAvailable()
- 必须重写此函数才能让audioSink对象当请求数据时自动触发readData()函数从缓冲区中读取到数据。
- 返回可读的数据大小。
(二) 代码依赖和线程
- 使用
QMutexLocker
确保缓冲操作的线程安全。 - 通过
QWaitCondition
触发需要等待的过程继续执行。
2. bilateralAudioPlay
类详解
(一) 主要资源和设备
-
音频输入和输出:
- 通过
QMediaDevices
进行输入和输出设备的列表和选择。 - 默认格式由
preferredFormat()
确定。
- 通过
-
音频渲染:
- 使用
QAudioSource
从选定的输入设备采集数据。 - 使用
QAudioSink
将数据输出到默认设备。
- 使用
(二) 采集和播放流程
-
选择输入设备
- 通过传入的 index 选择设备,确保设备支持默认格式。
-
开始播放
- 调用
mircrPhoneIODevice->start()
打开实体设备。 - 通过
audioSource->start(mircrPhoneIODevice)
将采集数据写入 IO。 - 通过
mircrPhoneIODevice->write(dummyAudioData.data(), dummyAudioData.size())
写一段空白的数据到mircrPhoneIODevice
的缓冲区,触发audioSinkOutput
读。 - 通过
audioSinkOutput->start(mircrPhoneIODevice)
将 IO数据输出到播放设备。
- 调用
-
结束播放
- 通过调用
stop()
停止输入和输出。
- 通过调用
(三) 渲染音量
- 通过信号中尽可能地选择不同实体选项,用于控制音量和输入设备。
三、总结
- 这个项目通过自定义的 IO设备,实现了麦克风数据采集和播放。
- 代码计算声道的音量和展示有了较好的结构,并且提供了构建异步功能的样例。
四、详细代码
MircrophoneIODevice.h
#ifndef MIRCROPHONEIODEVICE_H
#define MIRCROPHONEIODEVICE_H
#include <QIODevice>
#include <QMutex>
#include <QWaitCondition>
/**
* @brief The MircrophoneIODevice class 我这个类实现的是双向数据流的接口,因此必须重写readData 和 writeData
* 本类实现的工作是:获取麦克风的数据并写入到本类中作为麦克风的输出源,同时本类作为音频播放器的输入源
*/
using SampleType = float;
class MircrophoneIODevice : public QIODevice
{
public:
explicit MircrophoneIODevice(QObject *parent = nullptr);
void start();
void stop();
bool bufferIsEmpty();
void computeAudioData(char *data, qint64 bytesRead);
protected:
qint64 readData(char *data, qint64 maxSize) override;
qint64 writeData(const char *data, qint64 maxSize) override;
qint64 bytesAvailable() const override;
private:
QByteArray buffer;
mutable QMutex mutex; //mutable表示该变量可以在const成员函数或const对象中被修改
QWaitCondition condition;
bool emitSign;
};
#endif // MIRCROPHONEIODEVICE_H
mircrophoneIODevice.cpp
#include "mircrophoneiodevice.h"
#include <QDateTime>
#include <QDebug>
#include "signalcenter.h"
#include "util.h"
MircrophoneIODevice::MircrophoneIODevice(QObject *parent)
: QIODevice{parent}
{}
void MircrophoneIODevice::start()
{
QIODevice::open(QIODevice::ReadWrite);
}
void MircrophoneIODevice::stop()
{
QMutexLocker locker(&mutex);
QIODevice::close();
buffer.clear();
}
bool MircrophoneIODevice::bufferIsEmpty()
{
return buffer.isEmpty();
}
void MircrophoneIODevice::computeAudioData(char *data, qint64 bytesRead)
{
const SampleType *samples = reinterpret_cast<const SampleType *>(data);
int totalSamples = bytesRead / sizeof(SampleType); // 样本总数
int frameCount = totalSamples / 2; // 每帧包含 2 个声道的样本
// qDebug() << "totalSamples count: " << totalSamples;
// 3. 统计左右声道的音量数据
double sumSquareLeft = 0.0;
double sumSquareRight = 0.0;
SampleType peakLeft = 0.0f;
SampleType peakRight = 0.0f;
// 4. 遍历所有的帧,计算左声道和右声道的 RMS 和峰值
for (int i = 0; i < frameCount; ++i) {
SampleType leftSample = std::clamp(samples[2 * i], -1.0f, 1.0f); // 左声道
SampleType rightSample = std::clamp(samples[2 * i + 1], -1.0f, 1.0f); // 右声道
// 计算左声道的RMS和峰值
sumSquareLeft += leftSample * leftSample; // 平方和
peakLeft = std::max(peakLeft, std::abs(leftSample)); // 取样本的绝对最大值
// 计算右声道的RMS和峰值
sumSquareRight += rightSample * rightSample;
peakRight = std::max(peakRight, std::abs(rightSample));
}
// 5. 计算 RMS (均方根),如果数据出错的话,RMS 可能为 0,如果样本中全部是 0,log10(0) 会产生 -inf (负无穷大)。
//因此需要设定一个最小值是0.00001
double rmsLeft = std::max(0.00001, std::sqrt(sumSquareLeft / frameCount));
double rmsRight = std::max(0.00001, std::sqrt(sumSquareRight / frameCount));
// 6. 计算 dBFS(参考值为 1.0)
double dBFSLeft = 20.0 * std::log10(rmsLeft);
double dBFSRight = 20.0 * std::log10(rmsRight);
//合并dBFS和RMS
double avgRMS = (rmsLeft + rmsRight) / 2.0;
double avgdBFS = (dBFSLeft + dBFSRight) / 2.0;
// 7. 打印音量数据
// qDebug() << "Left Peak:" << peakLeft << "Right Peak:" << peakRight << "RMS Left:" << rmsLeft
// << "RMS Right:" << rmsRight << "dBFS Left:" << dBFSLeft << "dBFS Right:" << dBFSRight;
float Value = std::clamp(static_cast<int>((1 + avgdBFS / 60.0) * 100), 0, 100);
if (Value > 0 && emitSign) {
emit SignalCenter::instance().audioDataComputeCompleted(Value, INPUTLEVEL);
emitSign = false;
} else {
emit SignalCenter::instance().clearProBarVolume(INPUTLEVEL);
emitSign = true;
}
Q_UNUSED(avgRMS);
}
qint64 MircrophoneIODevice::readData(char *data, qint64 maxSize)
{
QMutexLocker locker(&mutex);
if (buffer.isEmpty()) {
//判断设备打没打开
if (!QIODevice::isOpen()) {
qWarning() << "MircrophoneIODevice is not open";
}
condition.wait(&mutex); //释放说并阻塞
}
//只会复制能复制的数据,当数据比maxSize大时,复制maxsize的数据,否则复制全部的
qint64 len = qMin(qint64(buffer.size()), maxSize);
memcpy(data, buffer.constData(), len);
buffer.remove(0, len);
qDebug() << "Read" << len << "bytes from MircrophoneIODevice's buffer at"
<< QDateTime::currentDateTime().toString("hh:mm:ss.zzz");
globalThreadPool->start(std::bind(&MircrophoneIODevice::computeAudioData, this, data, len));
return len;
}
qint64 MircrophoneIODevice::writeData(const char *data, qint64 maxSize)
{
QMutexLocker locker(&mutex);
buffer.append(data, maxSize);
condition.wakeAll();
qDebug() << "Write" << maxSize << "bytes to MircrophoneIODevice's buffer at"
<< QDateTime::currentDateTime().toString("hh:mm:ss.zzz");
// qDebug() << buffer.size();
return maxSize;
}
qint64 MircrophoneIODevice::bytesAvailable() const
{
QMutexLocker locker(&mutex);
// qDebug() << "buffer.size():" << buffer.size();
return buffer.size() + QIODevice::bytesAvailable();
}
bilateralAudioPlay.h
#ifndef BILATERALAUDIOPLAY_H
#define BILATERALAUDIOPLAY_H
#include <QAudioInput>
#include <QAudioSink>
#include <QAudioSource>
#include <QMediaDevices>
#include <QObject>
#include "mircrophoneiodevice.h"
class bilateralAudioPlay : public QObject
{
Q_OBJECT
public:
explicit bilateralAudioPlay(QObject *parent = nullptr);
QVector<QString> getIntputDevicesDescription();
void selectInputAudioDevice(int index);
void start();
void stop();
signals:
protected:
void onIntputAudioVolumeChange(float volume);
private:
QMediaDevices *MediaDevices;
QAudioSink *audioSinkOutput;
MircrophoneIODevice *mircrPhoneIODevice;
QList<QAudioDevice> intputAudioDevices; //这是包含的输入设备的列表
QAudioDevice selectIntputAudioDevice; //这是选择的输入设备
QAudioDevice defalutOutputAudioDevice; //这是默认的输出设备
QVector<QString> intputDevicesDescription;
QAudioSource *audioSource; //这是输入源
QAudioFormat format; //这是输入和输出使用的统一的格式
float outPutVolume; //这是当还没有开启测试时,移动了滑块,需要先记录滑块的音量,因为还没有开始初始化audioSinkOutput
};
#endif // BILATERALAUDIOPLAY_H
bilateralAudioPlay.cpp
#include "bilateralaudioplay.h"
#include <QDebug>
#include <QThread>
#include "signalcenter.h"
bilateralAudioPlay::bilateralAudioPlay(QObject *parent)
: QObject{parent}
, MediaDevices(new QMediaDevices(this))
, mircrPhoneIODevice(new MircrophoneIODevice(this))
, outPutVolume(0.5)
{
defalutOutputAudioDevice = MediaDevices->defaultAudioOutput();
intputAudioDevices = MediaDevices->audioInputs();
connect(&SignalCenter::instance(),
&SignalCenter::intputAudioVolumeChange,
this,
&bilateralAudioPlay::onIntputAudioVolumeChange);
}
QVector<QString> bilateralAudioPlay::getIntputDevicesDescription()
{
for (auto &intputDevice : intputAudioDevices) {
intputDevicesDescription.emplace_back(intputDevice.description());
}
return intputDevicesDescription;
}
void bilateralAudioPlay::selectInputAudioDevice(int index)
{
if (index > intputAudioDevices.size()) {
qWarning() << "index 超出输出设备的个数";
return;
}
selectIntputAudioDevice = intputAudioDevices[index];
format = defalutOutputAudioDevice.preferredFormat();
if (!selectIntputAudioDevice.isFormatSupported(format)) {
qWarning() << "output format not supported";
return;
}
qDebug() << "format.sampleRate: " << format.sampleRate()
<< "format.channelCount: " << format.channelCount()
<< "format.sampleFormat: " << format.sampleFormat();
}
void bilateralAudioPlay::start()
{
mircrPhoneIODevice->start();
//初始化输入源
audioSource = new QAudioSource(selectIntputAudioDevice, format, this);
// 将麦克风捕获的数据写入到 mircrPhoneIODevice 中
audioSource->start(mircrPhoneIODevice);
QByteArray dummyAudioData(1024, '\0');
mircrPhoneIODevice->write(dummyAudioData.data(), dummyAudioData.size());
// //初始化输出源
audioSinkOutput = new QAudioSink(defalutOutputAudioDevice, format, this);
audioSinkOutput->setVolume(outPutVolume);
//将自定义的IO设备绑定到 audioSinkOutput设备的输出源上,
audioSinkOutput->start(mircrPhoneIODevice);
qDebug() << "QAudioSink buffer size:" << audioSinkOutput->bufferSize();
qDebug() << "QAudioSink state:" << audioSinkOutput->state();
}
void bilateralAudioPlay::stop()
{
mircrPhoneIODevice->stop();
audioSource->stop();
audioSinkOutput->stop();
}
void bilateralAudioPlay::onIntputAudioVolumeChange(float volume)
{
if (!audioSinkOutput) {
outPutVolume = volume;
} else {
qDebug() << volume;
audioSinkOutput->setVolume(volume);
}
}
分类:
c/c++
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)