透過 Qt6 讀取麥克風、搭配 Whisper.cpp 進行語音辨識

| | 0 Comments| 08:29
Categories:

這篇算是之前《使用 C++ 進行深度學習的語音辨識:Whisper.cpp》的延伸,來記錄一下要怎麼透過 Qt 6 來讀取麥克風的資料、並送給 Whisper.cpp 來進行語音辨識吧~

首先,這邊使用的 Qt 版本是 Qt 6.6.x(連結),「Qt Multimedia」這個模組(官網),在專案的設定必須要加入對應的模組設定。而由於這個模組在 Qt 6 重構過,所以在介面上會和之前 5.x 可能會有所不同,要使用的話最好先確認自己用的 Qt 是哪個版本。

音訊裝置

在 Qt 6 裡面對應到系統音訊裝置的類別是是 QAudioDevice官網),包含了輸入(麥克風等)和輸出(喇叭、耳機等)的裝置。而要取得這些裝置物件的話,主要是透過 QMediaDevices官網)來取得;比如說想要取得系統所有的音訊輸入來源的話,就是:

const QList<QAudioDevice> audioDevices = QMediaDevices::audioInputs();
for (const QAudioDevice& device : audioDevices)
{
  qDebug() << "ID: " << device.id() << "\n"
           << "Description: " << device.description() << "\n"
           << "Is default: " << (device.isDefault() ? "Yes" : "No") << "\n";
}

在 Heresy 的電腦上執行後,會出現下面的結果:

Accessing QMediaDevices without a QCoreApplication
ID:  "{0.0.1.00000000}.{1c6bf656-89ce-4444-a2a5-693a827e167f}"
 Description:  "麥克風 (Logitech BRIO)"
 Is default:  Yes

ID:  "{0.0.1.00000000}.{033d72cd-b632-4476-a3b3-c227b0a5d4f0}"
 Description:  "Microphone (VIVE Virtual Audio Device)"
 Is default:  No

ID:  "{0.0.1.00000000}.{becb16e9-8e08-4cdf-8242-bf7c60248614}"
 Description:  "麥克風 (Realtek USB Audio)"
 Is default:  No

而如果很單純,只是要去抓系統預設的音訊輸入裝置的話,那只要寫成:

QAudioDevice device = QMediaDevices::defaultAudioInput();

這樣就可以了。

讀取資料

在確定要使用的音訊來源裝置後,要讀取聲音資料則是要透過 QAudioSource 這個類別(官網);而在音訊格式設定的部分,則是要透過 QAudioFormat官網)來定義。

這邊要建立 QAudioSource 可以寫成:

// audio format
QAudioFormat format;
format.setSampleRate(16000);
format.setChannelCount(1);
format.setSampleFormat(QAudioFormat::UInt8);

if (device.isFormatSupported(format))
{
  QAudioSource* audio = new QAudioSource(device, format);
  //TODO...
  delete audio;
}
else
{
  qDebug() << " format not support";
}

這邊 QAudioFormat 是設定了採樣頻率、聲道數、還有採樣格式。

然後,要先確認指定的裝置是否有支援這樣的格式,如果有支援就可以建立出 QAudioSource 的物件了。

之後,則就可以透過 start()stop() 這類的函示來錄音和停止了。而要把聲音資料寫到哪裡,則是在呼叫 start() 時設定,允許的型別是繼承自 QIODevice 的型別。

比如說如果要把資料直接寫到檔案的話,可以寫成:

QFile file("./test.raw");
file.open(QIODevice::WriteOnly | QIODevice::Truncate);
audio->start(&file);

要結束的時候,則在呼叫 audio->stop()、並把檔案關閉就可以了。

而如果是要儲存到記憶體裡面、等下要使用的話,則可以用 QBuffer官網)來接。

超簡易範例

下面就是一個可以運作的完整範例:

#include <QApplication>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioSource>
 
#include <QLabel>
#include <QFile>
 
class MyWidget : public QLabel
{
public:
  MyWidget(QWidget* parent) : QLabel(parent)
  {
    devAudio = QMediaDevices::defaultAudioInput();
    setText(devAudio.description());
 
    QAudioFormat format;
    format.setSampleRate(16000);
    format.setChannelCount(1);
    format.setSampleFormat(QAudioFormat::UInt8);
 
    if (devAudio.isFormatSupported(format))
    {
      audioSrc = new QAudioSource(devAudio, format);
 
      file.setFileName("./test.raw");
      file.open(QIODevice::WriteOnly | QIODevice::Truncate);
      audioSrc->start(&file);
    }
  }
 
  ~MyWidget()
  {
    audioSrc->stop();
    file.close();
    delete audioSrc;
  }
 
protected:
  QAudioDevice devAudio;
  QAudioSource* audioSrc = nullptr;
  QFile file;
};
 
int main(int argc, char* argv[])
{
  QApplication app(argc,argv);
  MyWidget w(nullptr);
  w.show();
  return app.exec();
}

這個範例在執行後會開啟一個 QLabel 構成的視窗、顯示目前使用的音訊輸入裝置的名稱,同時也會開始將聲音錄製到執行目錄下的 test.raw 這個檔案;當關閉視窗的時候,會結束錄製。

不過由於這個檔案沒有特別的編碼、就是聲音的原始資料而已,所以一般軟體應該打不開就是了。


搭配 Whisper.cpp

如果要搭配 Whisper.cpp 寫一個最簡單的語音辨識程式,大概可以寫成下面的樣子:

#include <QApplication>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioSource>
 
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QBuffer>
 
#include <whisper.h>
 
class MyWidget : public QWidget
{
public:
  MyWidget(QWidget* parent) : QWidget(parent)
  {
    // setup UI
    QVBoxLayout* layout = new QVBoxLayout(this);
    QPushButton* button = new QPushButton("REC");
    QLabel* label = new QLabel();
    layout->addWidget(button);
    layout->addWidget(label);
    
    // get default audio input
    devAudio = QMediaDevices::defaultAudioInput();
    label->setText(devAudio.description());
 
    // setup audio format
    QAudioFormat format;
    format.setSampleRate(WHISPER_SAMPLE_RATE);
    format.setChannelCount(1);
    format.setSampleFormat(QAudioFormat::Float);
 
    // create audio source
    if (devAudio.isFormatSupported(format))
    {
      audioSrc = new QAudioSource(devAudio, format);
      button->setCheckable(true);
      connect(button, &QPushButton::clicked,
        [this, label](bool checked) {
          if (checked)
          {
            buffer.reset();
            buffer.open(QBuffer::WriteOnly);
            audioSrc->start(&buffer);
          }
          else
          {
            audioSrc->stop();
            buffer.close();
 
            whisper_full_params wparams = whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
            wparams.translate = false;
            if (whisper_full(ctx, wparams, (float*)buffer.buffer().constData(), buffer.size() / 4) == 0)
            {
              std::string text;
              const int n_segments = whisper_full_n_segments(ctx);
              for (int i = 0; i < n_segments; ++i)
                text += whisper_full_get_segment_text(ctx, i);
 
              label->setText(QString::fromStdString(text));
            }
          }
        });
    }
 
    // initial whisper.cpp
    whisper_context_params cparams = whisper_context_default_params();
    cparams.use_gpu = true;
    ctx = whisper_init_from_file_with_params(
      R"(E:\whisper-qt\model\ggml-medium.bin)", cparams);
  }
 
  ~MyWidget()
  {
    if (audioSrc != nullptr)
      delete audioSrc;
 
    whisper_free(ctx);
  }
 
protected:
  QAudioDevice  devAudio;
  QAudioSource* audioSrc = nullptr;
  QBuffer       buffer;
 
  whisper_context* ctx = nullptr;
};
 
int main(int argc, char* argv[])
{
  QApplication app(argc,argv);
  MyWidget w(nullptr);
  w.show();
  return app.exec();
}

這個程式會是一個簡單的小視窗,上面有一個「REC」按鈕、下面則是顯示文字的區域。
在按下按鈕後,會開始錄音到 buffer 裡面,再按一次則會停止錄音、並將資料丟給 whisper.cpp 進行推論;推論完後會把結果顯示在文字顯示區域。

而這邊個人覺得比較需要注意的,一個是錄音的格式:

QAudioFormat format;
format.setSampleRate(WHISPER_SAMPLE_RATE);
format.setChannelCount(1);
format.setSampleFormat(QAudioFormat::Float);

這邊採樣頻率是 whisper.cpp 定義的 WHISPER_SAMPLE_RATE、實際上是 1,600;而採樣格式要記得改成 float。

再來,就是在要把資料傳給 whisper_full() 的時候,資料的指標是 buffer.buffer().constData(),這邊回傳的會是 const char*,需要直接強制轉型成 float*;而也由於大小不一樣,所以這邊需要把 buffer 的大小除以 4。


這篇大概就是這樣了。

理論上,透過這個方法確實可以用 Qt 去讀取系統的麥克風資訊、然後透過 Whisper.cpp 來做語音辨識、輸出成文字;不過這邊算是盡量簡單化,很多東西應該還有更好的寫法就是了。

而且…真正的問題,應該還是要怎麼處理這些文字了。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *