Qt实现麦克风数据采集并播放

自定义 IODevice 实现麦克风数据采集和播放

这篇注记给出一个自定义的 MircrophoneIODevicebilateralAudioPlay 类,用于实现从麦克风采集音频数据,并通过默认的音频输出设备播放。


一、功能概述

  1. 数据采集和播放:

    • 使用自定义的 MircrophoneIODevice,将麦克风采集到的数据写入缓冲。
    • 通过 Qt 提供的 QAudioSourceQAudioSink,实现音频的输入和输出。
  2. 音频分析:

    • 采集到的数据通过 computeAudioData 方法计算左右声道的 RMS 和 dBFS,用于添加音量计算功能。
  3. 自定义 IO设备:

    • 通过编写 QIODevicereadDatawriteData 方法,实现数据的转换和处理。
  4. 添加异步计算:

    • 使用 QThreadPoolstd::bind 将音量计算过程移至异步线程。

二、代码详解

1. MircrophoneIODevice 类详解

  • 这是一个自定义的 QIODevice
(一) 方法功能
  1. start()stop()

    • start() 打开设备,允许输入和输出。
    • stop() 关闭设备,清除缓冲。
  2. readData()writeData()

    • readData() 从缓冲中读取数据,并调用异步线程计算音量。
    • writeData() 将数据写入缓冲,并触发等待的线程继续进行。
  3. computeAudioData()

    • 通过采样值计算左右声道的 RMS 和 dBFS,渲染音量值。
    • 将计算结果通过 SignalCenter 发送信号。
  4. bytesAvailable()

  • 必须重写此函数才能让audioSink对象当请求数据时自动触发readData()函数从缓冲区中读取到数据。
  • 返回可读的数据大小。
(二) 代码依赖和线程
  • 使用 QMutexLocker 确保缓冲操作的线程安全。
  • 通过 QWaitCondition 触发需要等待的过程继续执行。

2. bilateralAudioPlay 类详解

(一) 主要资源和设备
  • 音频输入和输出:

    • 通过 QMediaDevices 进行输入和输出设备的列表和选择。
    • 默认格式由 preferredFormat() 确定。
  • 音频渲染:

    • 使用 QAudioSource 从选定的输入设备采集数据。
    • 使用 QAudioSink 将数据输出到默认设备。
(二) 采集和播放流程
  1. 选择输入设备

    • 通过传入的 index 选择设备,确保设备支持默认格式。
  2. 开始播放

    • 调用 mircrPhoneIODevice->start() 打开实体设备。
    • 通过 audioSource->start(mircrPhoneIODevice) 将采集数据写入 IO。
    • 通过 mircrPhoneIODevice->write(dummyAudioData.data(), dummyAudioData.size()) 写一段空白的数据到mircrPhoneIODevice的缓冲区,触发audioSinkOutput读。
    • 通过 audioSinkOutput->start(mircrPhoneIODevice) 将 IO数据输出到播放设备。
  3. 结束播放

    • 通过调用 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);
    }
}
posted @   吴海琼  阅读(150)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示