zl程序教程

您现在的位置是:首页 >  工具

当前栏目

Qt系列文章027-QAudioInput音频输入

Qt输入 系列 文章 音频
2023-09-14 09:01:43 时间

1 前言

  前面讲解了音频的录入和音频文件的播放,本章将会继续讲解音频输入的获取图形显示,下面看基本功能概述,先看效果图

2 效果图

在这里插入图片描述
  这里波形段保持一个序列,是我这边输入的问题,插上的耳机,有点问题,所以没有什么波段显示,

3 QAudioInput获取音频输入功能概述

  QAudioInput 类提供了接收音频设备输入数据的接口,创建QAudioInput 对象实例时,需要用两个参数,一个是 QAudioDeviceInfo 类表示的音频设备,一个是 QAudioFormat 表示的音频输入格式。QAudiolnput::start() 函数开始音频数据输入时,需要指定一个流设备接收输入的音频数据,如可以指定一个QFile表示的文件。

  上章讲解了QAudioRecorder 的实际代码运用,所以下面看看它与QAudioInput 不同之处在于何处:

  • QAudioInput 创建时指定的QaudioFormat将直接作用于音频输入设备,也就是音频输入的
    数据将直接按照设置的参数进行采样,而 QAudioRecorder 不能直接控制采样字长、采样
    点类型等底层参数。
  • QAudioInput:start(QIODevice *device) 指定 一个 QIODevice 设备作为数据输出对象,可以
    是文件,也可以是其他从 QIODevice 继承的类。如从 QIODevice 继承-一个类, 对输入的缓
    冲区数据进行处理,而不是保存到文件。而 QAudioRecorder 只能指定文件作为保存对象。
    所以,QAudioInput 可以实现更加底层的音频输入控制。

  下图是使用 QAudioInput 实现的一个音频数据输入并实时显示原始信号波形的实例程序的运行界面。
在这里插入图片描述
  图左侧显示的是用 QAudioDeviceInfo 类获取的音频设备,以及设备支持的各种参数,单击 测试音频设置 可以判断音频设备是否支持所设置的采集配置。为了更方便读取原始数据,
在开始采集时采用固定的设置,即8000Hz1通道8位无符号整数

  窗口右侧是一个 QChart 组件,采用 QLineSeries 作为显示序列。开始采集后,从缓冲区读取的数据将实时显示在图表上。

4 程序主窗口定义与初始化

  程序主窗口是基于QWidget 的类 AudioInputSamp ,窗口界面设计由UI设计器实现。

主窗口类 AudioInputSamp 的定义如下:

class AudioInputSamp : public QWidget
{
    Q_OBJECT

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

private slots:
    //自定义槽函数
    void onIODeviceUpdateBlockSize(qint64 blockSize) ;

private slots:
    //UI 设计自动关联槽
    void on_comboDevices_currentIndexChanged(int index);
    void on_actDeviceTest_clicked();
    void on_actStart_clicked();

    void on_actStop_clicked();

private:
    Ui::AudioInputSamp *ui;

    const  qint64  m_displayPointsCount = 4000;
    QLineSeries * m_lineSeries = nullptr; //曲线序列

    QList <QAudioDeviceInfo> m_DeviceList; //音频输入设备列表

    QAudioDeviceInfo m_currentDevice; //当前输入设备
    MineDisplayDevice * m_mineDisplayDevice; //用于显示的IODevice

    QAudioInput * m_AudioInput = nullptr; //音频输入设备

private:
    QString SampleTypeString (QAudioFormat::SampleType sampleType);
    QString ByteOrderString (QAudioFormat::Endian endian);
};

  这里定义了较多的私有变量,其中 MineDisplayDevice 是一个自定义的从 QIODevice 继承的类,用于读取音频输入缓冲区的数据,并在图表上显示。其具体实现在后面介绍。

AudioInputSamp 的构造函数代码如下:

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

    //创建显示图表
    QChart *chart = new QChart;
    chart->setTitle ("音频输入原始信号") ;
    ui->widget_chartView->setChart(chart);
    m_lineSeries= new QLineSeries(); //序列
    chart->addSeries (m_lineSeries) ;

    QValueAxis *axisX = new QValueAxis; //坐标轴
    axisX->setRange (0, m_displayPointsCount); //chart 显示4000个采样点数据
    axisX->setLabelFormat ("%g") ;
    axisX->setTitleText ("Samples") ;
    QValueAxis *axisY = new QValueAxis; //坐标轴
    axisY->setRange(0, 256); // UnSingedInt 采样,数据范围0-255
    axisY->setTitleText ("Audio level") ;

    //! 最新方法设置轴
    //设置X在下面,Y在zuo左边
    chart->addAxis(axisX, Qt::AlignBottom) ;
    chart->addAxis(axisY, Qt::AlignLeft) ;
    m_lineSeries->attachAxis(axisX);
    m_lineSeries->attachAxis(axisY);
    //! 下面两个方法是过时方法,同上面四行代码效果,目前一样可用
//    chart->setAxisX(axisX, m_lineSeries);
//    chart->setAxisY(axisY, m_lineSeries);

    chart->legend()->hide() ;
    ui->comboDevices->clear() ;

    m_DeviceList =QAudioDeviceInfo::availableDevices (QAudio::AudioInput);

    for(int i=0; i<m_DeviceList.count() ;i++){
        QAudioDeviceInfo device=m_DeviceList.at (i) ;
        ui ->comboDevices->addItem (device. deviceName () ) ;
    }

    if (m_DeviceList.size()>0){
        ui -> comboDevices->setCurrentIndex (0) ;
        m_currentDevice =m_DeviceList.at(0);
    }
    else{
        ui->actStart->setEnabled(false) ;
        ui ->actDeviceTest->setEnabled(false) ;
        ui->groupBoxDevice->setTitle("支持的音频输入设置(无设备)") ;
    }
}

  构造函数创建了用于图表显示的 QChart 对象chart,创建了 QLineSeries 类型的序列m_lineSeries,创建了X和Y坐标轴;其X轴的范围等于0到显示的数据点的总数4000,Y轴的范围是0至256,采用8位无符号整数,采样数据范围是0至255。

  QAudioDeviceInfo::availableDevices(QAudio::AudioInput) 可以获取音频输入设备列表,设备名称被添加到窗口.上的 comboDevices 下拉列表框里。

5 音频输入设备支持的格式

  在主窗口的构造函数中,向 comboDevices 下拉列表框中添加了系统所有的音频输入设备。在下拉列表框里选择一个设 备时,发射 currentIndexChanged(int index) 信号,在其槽函数里获取设备支持的各种音频输入参数,包括支持的音频编码、采样率、通道数、采样点类型和采样点大小等,以此更新窗口.上的组件显示。代码如下:

void AudioInputSamp::on_comboDevices_currentIndexChanged(int index)
{
    //!通过UI设计器右键跳转 comboDevices 组件 currentIndexChanged( int index ) 信号来关联的信号槽
    //选择音频输入设备
    m_currentDevice =m_DeviceList.at(index);//当前音频设备
    ui ->comboCodec->clear(); //支持的 音频编码

    QStringList codecs = m_currentDevice.supportedCodecs() ;
    for (int i = 0; i < codecs.size() ; ++i){
        ui->comboCodec->addItem(codecs.at(i));
    }

    ui->comboSampleRate->clear(); // 支持的采样率
    QList<int> sampleRate = m_currentDevice.supportedSampleRates ();
    for (int i = 0; i < sampleRate.size() ; ++i){
        ui->comboSampleRate->addItem(QString ("%1") .arg (sampleRate.at(i))) ;
    }

    ui->comboChannels->clear() ;//支持的通道数
    QList<int> Channels = m_currentDevice.supportedChannelCounts();
    for (int i = 0; i < Channels.size() ; ++i){
        ui-> comboChannels->addItem (QString ("%1") .arg (Channels.at(i)));
    }

    ui->comboSampleTypes->clear(); //支持的采样点类型
    QList<QAudioFormat::SampleType> sampleTypes = m_currentDevice.supportedSampleTypes() ;
    for (int i = 0; i < sampleTypes.size() ; ++i){
        ui ->comboSampleTypes->addItem(SampleTypeString (sampleTypes.at(i)),QVariant (sampleTypes.at(i))) ;
    }

    ui->comboSampleSizes->clear() ;//采样点大小.
    QList<int> sampleSizes = m_currentDevice.supportedSampleSizes() ;
    for (int i = 0; i < sampleSizes.size() ; ++i){
        ui->comboSampleSizes->addItem (QString("%1") .arg(sampleSizes.at(i))) ;
    }

    ui->comboByteOrder->clear();//字节序
    QList<QAudioFormat::Endian> endians = m_currentDevice.supportedByteOrders () ;
    for (int i = 0; i < endians.size() ; ++i){
        ui->comboByteOrder->addItem(ByteOrderString(endians.at (i) )) ;
    }
}

QString AudioInputSamp::SampleTypeString(QAudioFormat::SampleType sampleType)
{
    //将QAudioFormat: :SampleType类型转换为字符串
    QString result ("Unknown") ;
    switch (sampleType) {
    case QAudioFormat::SignedInt:
        result = "SignedInt";
        break;
    case QAudioFormat::UnSignedInt:
        result = "UnSignedInt";
        break;
    case QAudioFormat::Float:
        result = "Float";
        break;
    case QAudioFormat::Unknown:
        result = "Unknown";
        break;
    }

    return result;
}

QString AudioInputSamp::ByteOrderString(QAudioFormat::Endian endian)
{
    //将QAudioFormat: :Endian转换为字符串
    if (endian==QAudioFormat::LittleEndian){
        return "LittleEndian";
    }
    else if (endian==QAudioFormat::BigEndian)
    {
        return "BigEndian";
    }
    else{
        return "Unknown";
    }
}

  创建一个 QAudioInput 对象时需要传递一个 QAudioFormat 类型作为参数,用于指定音频输入配置,而音频设备是否支持这些配置需要进行测试。窗口.上的 测试音频设置 按钮可以进行测试,代码如下:

void AudioInputSamp::on_actDeviceTest_clicked()
{
    //测试音频输入设备是否支持选择的设置
    QAudioFormat settings;
    settings.setCodec (ui->comboCodec->currentText()) ;
    settings.setSampleRate (ui->comboSampleRate->currentText().toInt() ) ;
    settings.setChannelCount (ui->comboChannels->currentText().toInt()) ;
    settings.setSampleType(QAudioFormat::SampleType (ui->comboSampleTypes->currentData() . toInt()));
    settings.setSampleSize (ui->comboSampleSizes->currentText().toInt()) ;

    if (ui->comboByteOrder ->currentText ()=="LittleEndian"){
        settings.setByteOrder(QAudioFormat::LittleEndian) ;
    }
    else{
        settings.setByteOrder(QAudioFormat::BigEndian) ;
    }

    if (m_currentDevice.isFormatSupported (settings)){
        QMessageBox::information(this, "音频测试", "测试成功,输入设备支持此设置.");
    }
    else{
        QMessageBox::critical (this, "音频测试", "测试失败,输入设备不支持此设置.");
    }
}

  QAudioFormat 类对象 settings 从界面上各个组件获取设置,包括编码格式、采样率、通道数等,然后用QAudioDevicelnfo::isFormatSupported() 函数测试是否支持此设置;如果不支持,还可以使用 QAudioDeviceInfonearestFormat() 函数获取最接近的配置。

6 开始音频输入

  单击窗口工具栏上 开始 按钮,即可开始音频数据输入,其代码如下:

void AudioInputSamp::on_actStart_clicked()
{
    //开始音频输入
    QAudioFormat defaultAudioFormat;  //缺省格式
    defaultAudioFormat.setSampleRate (8000) ;
    defaultAudioFormat.setChannelCount (1) ;
    defaultAudioFormat.setSampleSize(8) ;
    defaultAudioFormat.setCodec ("audio/pcm") ;
    defaultAudioFormat.setByteOrder(QAudioFormat::LittleEndian) ;
    defaultAudioFormat. setSampleType (QAudioFormat::UnSignedInt) ;
    if (!m_currentDevice.isFormatSupported (defaultAudioFormat)){

        QMessageBox::critical (this, "测试", "测试失败,输入设备不支持此设置.") ;
        return;
    }

    m_AudioInput = new QAudioInput (m_currentDevice, defaultAudioFormat, this) ;
    m_AudioInput->setBufferSize(m_displayPointsCount) ;

    //接收音频输入数据的流设备
    m_mineDisplayDevice = new MineDisplayDevice (m_lineSeries, m_displayPointsCount, this) ;

    connect (m_mineDisplayDevice , SIGNAL (updateBlockSize (qint64)) ,this, SLOT (onIODeviceUpdateBlockSize(qint64)));

    m_mineDisplayDevice->open(QIODevice::WriteOnly) ;
    m_AudioInput->start(m_mineDisplayDevice) ;
    ui->actStart->setEnabled(false) ;
    ui ->actStop->setEnabled(true) ;
}

  为了便于解析音频输入的原始数据,音频输入的配置采用固定的简单方式,而不是根据界面上的设置进行配置。音频输入配置固定为8000 Hz采样率、1个通道、8位无符号整数、audio/pcm编码和小端字节序

  创建 QAudioInput 类对象 m_AudioInput 时,传递 defaultAudioFormatm_currentDevice 作为参数,并设置缓冲区大小为 m_displayPointsCount( 等于4000)。

m_AudioInput= new QAudioInput (m_currentDevice, defaultAudioFormat, this) ;
m_AudioInput->setBufferSize (m_displayPointsCount) ;

  使用 setBufferSize 设置的缓冲区,需要在调用 start() 之前设置才有效。缓冲区的大小大于每次更新输入的原始数据块的大小,在图15-4中可见缓冲区大小为4000,而每次更新的原始数据的数据字节数为800。

  随后程序创建一个 MineDisplayDevice 类型的 IO 设备 m_mineDisplayDevice , 这个类实现了流设备的 writeData() 函数,用于读取音频输入的数据并在曲线上显示。其构造函数接收lineSeries 和m_displayPointsCount作为参数。再将 m_mineDisplayDevice 的信号 updateBlockSize (与一个自定义槽函数关联,然后将 m_mineDisplayDevice 以只写方式打开。

  最后调用 QAudiolnput:start() 函数开始音频输入,以 m_mineDisplayDevice 作为IO流 设备。

 m_AudioInput->start(m_mineDisplayDevice) ;

  自定义槽函数onIODeviceUpdateBlockSize 用于显示缓冲区大小和数据块大小,代码如下:

void AudioInputSamp::onIODeviceUpdateBlockSize(qint64 blockSize)
{
    //显示缓冲区大小和数据块大小
    ui->LabBufferSize->setText(QString::asprintf("QAudioInput::bufferSize () =%d" , m_AudioInput->bufferSize())) ;
    ui->LabBufferSize->setText(ui->LabBufferSize->text() +"\t"+QString("IODevice数据块字节数=%1") .arg (blockSize));
}

7 流设备MineDisplayDevice的功能实现

  使用 QAudioInput 获取音频输入数据时,需要使用一个 QIODevice 类型的设备作为流输出设备,一般采用QFile可以将数据记录到文件。本例中使用一个自定义的类 MineDisplayDevice,用于获取音频输入数据并在曲线上实时显示。.

  MineDisplayDevice 类的定义如下:

#include <QIODevice>
#include <QXYSeries>
#include <QtCharts>

using namespace QtCharts;

class MineDisplayDevice : public QIODevice
{
    Q_OBJECT
public:
    explicit MineDisplayDevice(QXYSeries * series, qint64 pointsCount, QObject *parent= nullptr);
protected:
    qint64 readData (char * data, qint64 maxSize) ;
    qint64 writeData(const char * data, qint64 maxSize) ;
private :
    QXYSeries *m_series = nullptr;
    qint64 range=4000;
signals:
    void updateBlockSize(qint64 blockSize) ;
};

  因为 MineDisplayDevice 类从QIODevice继承,所以具有流数据读写功能。重新实现的readData() 和 writeData() 是实现流数据读写功能的。定义了m_seriesrange 两个私有变量。m_series 是用于图表曲线显示的 QXYSeries 序列,range 缺省值为4000,是序列最多显示的数据点数。构造函数里接收seriespointsCount 对上述两个变量进行初始化。

  MineDisplayDevice 类的实现代码如下:

MineDisplayDevice::MineDisplayDevice(QXYSeries *series, qint64 pointsCount, QObject *parent):QIODevice(parent)
{
    //构造函数
    m_series= series ;
    range=pointsCount;
}

qint64 MineDisplayDevice::readData(char *data, qint64 maxSize)
{
    //流的读操作,不处理
    Q_UNUSED(data)
    Q_UNUSED(maxSize)

    return -1;
}

qint64 MineDisplayDevice::writeData(const char *data, qint64 maxSize)
{
    //读取数据块内的数据,更新到序列
    QVector<QPointF> oldPoints = m_series->pointsVector() ;
    QVector<QPointF> points; //临时

    if (oldPoints.count() < range)
    {   //m_ series序列的数据未满4000点,
        points = m_series->pointsVector () ;
    }
    else
    {       //将原来maxSize至4000的数据点前移
        for (int i = maxSize; i < oldPoints.count () ; i++){
            points. append (QPointF(i - maxSize, oldPoints.at(i) .y()));
        }
    }
    qint64 size = points.count() ;
    for (int k = 0; k < maxSize; k++)   //数据块内的数据填充序列的尾部
    {
            points. append (QPointF(k + size, (quint8)data[k])) ;
    }

    m_series->replace (points);

    emit updateBlockSize (maxSize) ;
    return maxSize ;
}

  MineDisplayDevice 无需实现流的读操作,所以readData() 函数不做什么处理,重点是流的写操作 writeData() 函数的实现。

  writeData() 传递进来的参数 data 是数据块的指针,maxSize是数据块的字节数,这是需要读取出来的音频输入数据。

  由于音频输入配置为1通道8位无符号的整数采样,所以一个数据点就是-一个字节的数据。

  显示序列 m_series 存储的显示数据点个数限定为4000个点,大于maxSize (此例中为800),所以对于序列的数据点的更新采用FIFO (先入先出)的方式。

  更新临时数据点向量points,采用序列的replace()函数替换序列原有的数据点向量,是最快的方式。

  writeData() 函数最后发射信号 updateBlockSize(maxSize),用于主窗口的关联槽函数onIODeviceUpdateBlockSize() 则显示数据块的大小。