车机AI语音识别实现
文章标题
基于 Linux 的语音识别模块开发实践:从车机 AI 助手到通用语音交互
1. 背景
在车机中控项目里,语音模块往往不是一个独立的“功能页”,而是 AI 助手中的核心能力。用户说一句话,系统需要先把语音转成文本,再将文本发送给后端或大模型,最终通过语音合成播放回答。这个链路看起来并不复杂,但真正落到工程代码中时,涉及到音频采集、VAD(语音活动检测)、识别回调、结果拼接、网络通信、TTS(文本转语音)和状态切换等多个关键环节。
不过,这个语音模块的价值并不局限于车机领域。从工程和复用角度看,它完全可以脱离车机 UI,作为一个通用语音交互模块,应用到 PC、嵌入式终端或服务器端语音助手中。也就是说,车机只是这套能力的一个落地场景,而不是它的全部价值。
2. 语音模块的职责
语音模块的核心职责可以概括为四步:
- 采集音频
- 识别音频中的文字
- 将识别结果交给后续业务逻辑处理
- 触发 AI 回复或语音输出
这套流程最关键的关注点有两个:一是“识别结果如何稳定产出”,二是“何时判断一段语音结束”。在实际工程中,很多问题都集中在这两点上。
3. 关键设计:VAD 轮询与识别回调
这个模块的核心基础,是科大讯飞的语音识别 SDK。它通过回调函数返回识别文本,并通过 VAD 结束信号判断一句话是否结束。
核心逻辑大致如下:
char *g_result = NULL;
static unsigned int g_buffersize = BUFFER_SIZE;
static volatile int g_speech_done = 0;
void on_result(const char *result, char is_last)
{
if (result) {
size_t left = g_buffersize - 1 - strlen(g_result);
size_t size = strlen(result);
if (left < size) {
g_result = (char*)realloc(g_result, g_buffersize + BUFFER_SIZE);
if (g_result)
g_buffersize += BUFFER_SIZE;
else {
printf("mem alloc failed\n");
return;
}
}
strncat(g_result, result, size);
show_result(g_result, is_last);
if (is_last) {
printf("最终识别结果: %s\n", g_result);
}
}
}
void on_speech_begin()
{
if (g_result)
{
free(g_result);
}
g_result = (char*)malloc(BUFFER_SIZE);
g_buffersize = BUFFER_SIZE;
memset(g_result, 0, g_buffersize);
printf("Start Listening...\n");
}
void on_speech_end(int reason)
{
if (reason == END_REASON_VAD_DETECT) {
printf("\nSpeaking done \n");
g_speech_done = 1;
} else {
printf("\nRecognizer error %d\n", reason);
}
}
这里有两个关键点:
on_speech_begin():每次开始识别前,先清空上一次结果,避免上一轮的文本残留。on_result():识别结果是分段返回的,因此需要把回调中的片段拼接在一起,最终得到完整语句。
这对于语音输入场景特别重要,因为用户所说的内容,通常不是一次性返回,而是被 SDK 按片段不断回调。我们必须把这些片段串起来,才能得到最终文本。
4. VAD 轮询:等待“说完话”
很多语音模块的难点,不在于识别是否能工作,而在于“何时判断这段语音已经结束”。在实际应用中,如果结束判定不准确,就容易出现识别中断、结果不完整或多次重复处理的问题。
这里采用了 VAD 轮询方式,等待识别器检测到语音结束信号:
static void demo_mic(const char* session_begin_params)
{
int errcode;
int timeout = 0;
struct speech_rec iat;
struct speech_rec_notifier recnotifier = {
on_result,
on_speech_begin,
on_speech_end
};
errcode = sr_init(&iat, session_begin_params, SR_MIC, &recnotifier);
if (errcode) {
printf("speech recognizer init failed\n");
return;
}
errcode = sr_start_listening(&iat);
if (errcode) {
printf("start listen failed %d\n", errcode);
}
g_speech_done = 0;
while (!g_speech_done && timeout++ < 600)
usleep(100000);
errcode = sr_stop_listening(&iat);
if (errcode) {
printf("stop listening failed %d\n", errcode);
}
sr_uninit(&iat);
}
这种写法很适合嵌入式环境。它不需要复杂的回调框架,只要周期性检查 g_speech_done,就能知道“一段话已经结束”,之后再做后续的 AI 请求、命令处理或 TTS 回复。
5. 识别结果如何继续交互
识别完成后,通常会将文本交给 AI 服务或后端业务进行处理。这个过程在车机中往往和“AI 助手”直接绑定,但它本身也可以脱离车机单独应用。
if (g_result && strlen(g_result) > 0)
{
printf("识别结果: %s\n", g_result);
if (tcp_send_text(g_result)) {
char *llm_reply = tcp_recv_text();
if (llm_reply) {
const char* tts_params =
"voice_name = xiaoyan, text_encoding = utf8, "
"sample_rate = 16000, speed = 50, volume = 50, pitch = 50, rdn = 2";
if (text_to_speech(llm_reply, "reply.wav", tts_params) == MSP_SUCCESS) {
system("aplay reply.wav &");
}
free(llm_reply);
}
}
}
这里的思路非常清晰:
- 识别出的文本作为输入
- 发送到 AI 或后端
- 拿回回复
- 再通过语音合成播放
这是语音交互链路的核心,也能很好地扩展成通用语音助手。
6. 可复用的 AI 请求模块(不依赖车机)
这个模块同样可以独立拿出来,做一个普通的语音命令工具或者 AI 问答助手。下面这段代码就是在不依赖车机界面的情况下,也可以直接启用的版本。它会连接后端或大模型服务,发起文本请求并接收回复:
/* ================================================================ */
/* 原有: deepseek (保留不动) */
/* ================================================================ */
#if 0
// 发送文本,返回大模型回复的文本(需要调用者 free)
char* deepseek(const char* text_to_send)
{
if (text_to_send == NULL || strlen(text_to_send) == 0) {
printf("没有要发送的文本\n");
return NULL;
}
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sock_fd) {
printf("create socket error: %s\n", strerror(errno));
return NULL;
}
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(AGENT_PORT);
dest_addr.sin_addr.s_addr = inet_addr(AGENT_IP_ADDR);
if (connect(sock_fd, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
printf("connect agent error: %s\n", strerror(errno));
close(sock_fd);
return NULL;
}
printf("=======连接AGENT成功=======\n");
char sendbuf[4096] = {0};
snprintf(sendbuf, sizeof(sendbuf),
"POST /chat HTTP/1.1\r\n"
"Content-Length: %ld\r\n"
"\r\n"
"%s",
strlen(text_to_send), text_to_send);
send(sock_fd, sendbuf, strlen(sendbuf), 0);
char recvbuf[4096] = {0};
int len = recv(sock_fd, recvbuf, sizeof(recvbuf) - 1, 0);
close(sock_fd);
if (len <= 0) {
printf("未收到回复\n");
return NULL;
}
recvbuf[len] = '\0';
printf("大模型原始回复:\n%s\n", recvbuf);
char *body = strstr(recvbuf, "\r\n\r\n");
if (body) {
body += 4;
return strdup(body);
} else {
return strdup(recvbuf);
}
}
#endif
这段代码的思路非常适合扩展为:
- 桌面端语音助手
- Linux 命令交互器
- 智能问答工具
- 语音驱动的后台任务执行器
只要保留识别模块,配上一个文本交互后端,就可以完成通用语音交互。
7. 这套方案的价值
这类语音模块真正的价值,不在于“能不能识别一个字”,而在于是否能够稳定完成以下链路:
语音输入 → VAD 判断结束 → 识别结果拼接 → 文本交付 AI/后端 → 收到回复 → 语音播放
这条链路一旦稳定落地,它就能用于:
- 智能车机语音助手
- 智能家居语音控制
- 工业终端命令控制
- Linux 桌面语音助手
- 语音问答机器人
8. 总结
如果把这个语音模块抽象成工程化能力,它更像是一个“通用语音交互引擎”,而不是一段只服务于某个车机界面的临时代码。要做好这个模块,关键不只是 SDK 的调用,而是:
- 稳定的 VAD 判定
- 可靠的识别结果拼接
- 清晰的状态管理
- 后续 AI 或 TTS 的无缝接入
浙公网安备 33010602011771号