0%

语音识别_SAPI实现长时间语音识别(五)

一、思路

​ 通过事件驱动和定时器机制实现了长时间语音识别和静音检测。用户点击开始按钮后,程序一直监听用户的语音输入,当用户讲话时实时更新识别结果,并将其与数据库中的数据进行匹配。同时,通过定时器检测长时间静音,并在静音时进行识别结果处理。用户点击停止按钮后,停止所有识别操作。

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()
{
// 初始化 COM 库
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); // 启动定时器,间隔 1 s
}

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: // 定时器ID
{
// 获取当前时间(以毫秒为单位)
DWORD currentTime = GetTickCount();
// 检查当前时间与最后一次讲话时间的差值是否大于等于5000毫秒(5秒)
if (currentTime - m_lastSpeechTime >= 5000)
{
// 如果距离上次讲话已经超过5秒,则进行文本处理
PerformRecognition();
}
}
break;
default:
break;
}

// 调用基类的OnTimer函数,以确保基类处理其他定时器事件
CDialogEx::OnTimer(nIDEvent);
}

void CSapiASRDlg::PerformRecognition()
{
// 等待识别上下文中的通知事件,超时时间为1秒
HRESULT hr = m_pRecoContext->WaitForNotifyEvent(1000); // 1秒超时
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对象
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)
{
// 将WPARAM参数转换为CString指针
CString* pNewText = reinterpret_cast<CString*>(wParam);
if (pNewText)
{
// 设置编辑框的文本为新的识别结果
m_edtText.SetWindowText(*pNewText);
}

// 将识别结果转换为std::wstring以便于后续处理
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)
{
// 计算识别结果与数据库中每行数据的Levenshtein距离
double distance = levenshteinDistance(newText, std::wstring(row_data.GetString()));
// 如果找到更小的距离,则更新最匹配的结果
if (distance < minDistance)
{
minDistance = distance;
bestMatch = row_data.GetString();
bestMatchId = i;
}
i++;
}

// 将最相似的结果的ID输出到ID编辑框
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
// Levenshtein 距离算法
int CSapiASRDlg::levenshteinDistance(const std::wstring& s1, const std::wstring& s2)
{
const size_t m(s1.size()); // 字符串s1的长度
const size_t n(s2.size()); // 字符串s2的长度

// 如果s1为空,返回s2的长度
if (m == 0) return n;
// 如果s2为空,返回s1的长度
if (n == 0) return m;

// 创建一个(m+1) x (n+1)的矩阵,用于存储距离
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)
{
// 如果字符相同,代价为0,否则为1
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;
}
}

// 矩阵的右下角值即为Levenshtein距离
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)

-------------本文结束感谢您的阅读-------------