一、思路 通过事件驱动和定时器机制实现了长时间语音识别和静音检测。用户点击开始按钮后,程序一直监听用户的语音输入,当用户讲话时实时更新识别结果,并将其与数据库中的数据进行匹配。同时,通过定时器检测长时间静音,并在静音时进行识别结果处理。用户点击停止按钮后,停止所有识别操作。
1.1、初始化 SAPI 在程序启动时,初始化 SAPI 相关组件。主要包括创建语音识别器、识别上下文、语法对象,并加载听写语法。同时设置通知事件和输入音频对象。
1.2、 开始语音识别 用户点击“开始”按钮时,启动语音识别功能,并设置相关标志位和定时器。定时器用于检测是否有长时间的静音,以便进行语音识别的处理。
1.3、 处理语音事件 通过事件驱动机制,当 SAPI 检测到语音输入时,触发 SPEI_RECOGNITION 事件。此时,获取识别结果并更新最后一次讲话的时间。
1.4、 更新文本并进行处理 当收到语音识别结果时,更新对话框中的文本框内容。并使用 Levenshtein 距离算法将识别结果与数据库中的数据进行匹配,找出最相似的文本及其 ID。
1.5、 停止语音识别 用户点击“停止”按钮时,停止语音识别功能,取消定时器,并重置相关标志位。
1.6、 定时器检测静音 定时器定时检查用户是否长时间没有讲话。如果检测到用户静音超过设定的时间(如 5 秒),则调用 PerformRecognition
函数进行语音识别的处理,并更新识别结果。
1.7、 数据库连接和查询 程序初始化时,连接到数据库并查询所有识别内容。将查询结果存储在内存中,以便后续的文本匹配使用。
1.8、 文本相似度匹配 使用 Levenshtein 距离算法计算识别结果与数据库中每条数据之间的相似度,找出最匹配的文本及其对应的 ID,并显示在对话框中。
二、具体实现步骤 2.1、初始化 SAPI 和音频输入 InitializeSAPI
函数将会初始化SAPI,配置识别器和音频输入,并设置事件通知以处理识别结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 void CSapiASRDlg::InitializeSAPI () { HRESULT hr = ::CoInitialize (NULL ); if (FAILED (hr)) { AfxMessageBox (_T("Failed to initialize COM library." )); return ; } hr = m_pRecognizer.CoCreateInstance (CLSID_SpInprocRecognizer); if (FAILED (hr)) { CString message; message.Format (_T("Failed to create recognizer: 0x%08X" ), hr); AfxMessageBox (message); ::CoUninitialize (); return ; } hr = m_pRecognizer->CreateRecoContext (&m_pRecoContext); if (FAILED (hr)) { CString message; message.Format (_T("Failed to create recognition context: 0x%08X" ), hr); AfxMessageBox (message); ::CoUninitialize (); return ; } hr = m_pRecoContext->CreateGrammar (1 , &m_pGrammar); if (FAILED (hr)) { CString message; message.Format (_T("Failed to create grammar: 0x%08X" ), hr); AfxMessageBox (message); ::CoUninitialize (); return ; } hr = m_pGrammar->LoadDictation (NULL , SPLO_STATIC); if (FAILED (hr)) { CString message; message.Format (_T("Failed to load dictation grammar: 0x%08X" ), hr); AfxMessageBox (message); ::CoUninitialize (); return ; } hr = m_pGrammar->SetDictationState (SPRS_INACTIVE); if (FAILED (hr)) { CString message; message.Format (_T("Failed to set dictation state: 0x%08X" ), hr); AfxMessageBox (message); ::CoUninitialize (); return ; } hr = m_pRecoContext->SetNotifyWin32Event (); if (FAILED (hr)) { CString message; message.Format (_T("Failed to set notify event: 0x%08X" ), hr); AfxMessageBox (message); ::CoUninitialize (); return ; } HANDLE hEvent = m_pRecoContext->GetNotifyEventHandle (); if (hEvent == NULL ) { AfxMessageBox (_T("Failed to get event handle." )); ::CoUninitialize (); return ; } hr = m_pRecoContext->SetInterest (SPFEI (SPEI_RECOGNITION), SPFEI (SPEI_RECOGNITION)); if (FAILED (hr)) { CString message; message.Format (_T("Failed to set interest for recognition events: 0x%08X" ), hr); AfxMessageBox (message); ::CoUninitialize (); return ; } hr = CoCreateInstance (CLSID_SpMMAudioIn, NULL , CLSCTX_INPROC_SERVER, IID_ISpAudio, (void **)&m_cpAudio); if (FAILED (hr)) { CString message; message.Format (_T("Failed to create audio input object: 0x%08X" ), hr); AfxMessageBox (message); ::CoUninitialize (); return ; } hr = m_pRecognizer->SetInput (m_cpAudio, TRUE); if (FAILED (hr)) { CString message; message.Format (_T("Failed to set audio input: 0x%08X" ), hr); AfxMessageBox (message); ::CoUninitialize (); return ; } AfxMessageBox (_T("Initialize SAPI is successful" )); }
2.2、开始按钮 点击开始按钮,开始进行录音,因为要支持长期识别,可以添加用户讲话状态和讲话时间,来保证合适进行语音识别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void CSapiASRDlg::OnBnClickedButtonStart () { HRESULT hr = m_pGrammar->SetDictationState (SPRS_ACTIVE); if (FAILED (hr)) { AfxMessageBox (_T("Failed to start recognition." )); return ; } m_bRunning = true ; m_bUserSpeaking = false ; m_lastSpeechTime = GetTickCount (); AfxMessageBox (_T("Recognition started." )); SetTimer (TIMER_ID, 1000 , NULL ); }
2.3、定时器识别录音 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 void CSapiASRDlg::OnTimer (UINT_PTR nIDEvent) { switch (nIDEvent) { case TIMER_ID: { DWORD currentTime = GetTickCount (); if (currentTime - m_lastSpeechTime >= 5000 ) { PerformRecognition (); } } break ; default : break ; } CDialogEx::OnTimer (nIDEvent); } void CSapiASRDlg::PerformRecognition () { HRESULT hr = m_pRecoContext->WaitForNotifyEvent (1000 ); if (FAILED (hr)) { return ; } CSpEvent event; while (event.GetFrom (m_pRecoContext) == S_OK) { if (event.eEventId == SPEI_RECOGNITION) { ISpRecoResult* pResult = event.RecoResult (); if (pResult) { m_lastSpeechTime = GetTickCount (); LPWSTR pwszText = nullptr ; hr = pResult->GetText (SP_GETWHOLEPHRASE, SP_GETWHOLEPHRASE, FALSE, &pwszText, NULL ); if (SUCCEEDED (hr)) { CString* pNewText = new CString (pwszText); PostMessage (WM_USER_UPDATE_TEXT, reinterpret_cast <WPARAM>(pNewText), 0 ); CoTaskMemFree (pwszText); } } } } } BEGIN_MESSAGE_MAP (CSapiASRDlg, CDialogEx) ON_MESSAGE (WM_USER_UPDATE_TEXT, &CSapiASRDlg::OnUpdateText) END_MESSAGE_MAP () LRESULT CSapiASRDlg::OnUpdateText (WPARAM wParam, LPARAM lParam) { CString* pNewText = reinterpret_cast <CString*>(wParam); if (pNewText) { m_edtText.SetWindowText (*pNewText); } std::wstring newText ((*pNewText).GetString()) ; std::wstring bestMatch; int bestMatchId = -1 ; double minDistance = INT_MAX; int i = 1 ; for (const auto & row_data : m_database) { double distance = levenshteinDistance (newText, std::wstring (row_data.GetString ())); if (distance < minDistance) { minDistance = distance; bestMatch = row_data.GetString (); bestMatchId = i; } i++; } CString idResult; idResult.Format (_T("%d" ), bestMatchId); m_edtIdResult.SetWindowText (idResult); m_edtResult.SetWindowText (bestMatch.c_str ()); delete pNewText; return 0 ; }
2.4、结束按钮 如果点击结束按钮,会将定时器关闭,结束语音识别;又需要保证自己最后的录音数据可以被识别,在停止听十七之后再次调用语音识别函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void CSapiASRDlg::OnBnClickedButtonStop () { if (!m_bRunning) { AfxMessageBox (_T("Recognition is not started." )); return ; } HRESULT hr = m_pGrammar->SetDictationState (SPRS_INACTIVE); if (FAILED (hr)) { AfxMessageBox (_T("Failed to stop recognition." )); return ; } m_bRunning = false ; KillTimer (TIMER_ID); PerformRecognition (); AfxMessageBox (_T("Recognition stopped." )); }
2.5、Levenshtein距离算法 以下是带有详细注释的Levenshtein距离算法的实现代码和分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 int CSapiASRDlg::levenshteinDistance (const std::wstring& s1, const std::wstring& s2) { const size_t m (s1. size()) ; const size_t n (s2. size()) ; if (m == 0 ) return n; if (n == 0 ) return m; std::vector<std::vector<size_t >> matrix (m + 1 , std::vector <size_t >(n + 1 )); for (size_t i = 0 ; i <= m; ++i) matrix[i][0 ] = i; for (size_t j = 0 ; j <= n; ++j) matrix[0 ][j] = j; for (size_t i = 1 ; i <= m; ++i) { for (size_t j = 1 ; j <= n; ++j) { size_t cost = (s1[i - 1 ] == s2[j - 1 ]) ? 0 : 1 ; size_t deletion = matrix[i - 1 ][j] + 1 ; size_t insertion = matrix[i][j - 1 ] + 1 ; size_t substitution = matrix[i - 1 ][j - 1 ] + cost; size_t minValue = deletion; if (insertion < minValue) minValue = insertion; if (substitution < minValue) minValue = substitution; matrix[i][j] = minValue; } } size_t distance = matrix[m][n]; size_t lengthDifference = std::abs (static_cast <int >(m) - static_cast <int >(n)); const float lengthPenaltyFactor = 0.5f ; size_t lengthPenalty = static_cast <size_t >(lengthDifference * lengthPenaltyFactor); distance += lengthPenalty; return distance; }
原理
Levenshtein距离算法用于计算两个字符串之间的编辑距离,即将一个字符串转换成另一个字符串所需的最小操作次数。允许的操作包括插入、删除和替换字符。该算法通过动态规划的方式实现,创建一个二维矩阵,其中每个元素表示将字符串的某个前缀转换成另一个字符串的某个前缀的代价。通过依次填充矩阵,最终得到两个字符串的编辑距离。
此外,添加了长度差异的惩罚项,以更加公平地比较长度差异较大的字符串。惩罚项的比例因子可以根据具体需求进行调整。
三、源码 Github仓库:lxq-02/ContinuousSapiASR (github.com)