這篇算是之前《使用 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 來做語音辨識、輸出成文字;不過這邊算是盡量簡單化,很多東西應該還有更好的寫法就是了。
而且…真正的問題,應該還是要怎麼處理這些文字了。