车机AI语音识别实现

文章标题

基于 Linux 的语音识别模块开发实践:从车机 AI 助手到通用语音交互


1. 背景

在车机中控项目里,语音模块往往不是一个独立的“功能页”,而是 AI 助手中的核心能力。用户说一句话,系统需要先把语音转成文本,再将文本发送给后端或大模型,最终通过语音合成播放回答。这个链路看起来并不复杂,但真正落到工程代码中时,涉及到音频采集、VAD(语音活动检测)、识别回调、结果拼接、网络通信、TTS(文本转语音)和状态切换等多个关键环节。

不过,这个语音模块的价值并不局限于车机领域。从工程和复用角度看,它完全可以脱离车机 UI,作为一个通用语音交互模块,应用到 PC、嵌入式终端或服务器端语音助手中。也就是说,车机只是这套能力的一个落地场景,而不是它的全部价值。


2. 语音模块的职责

语音模块的核心职责可以概括为四步:

  1. 采集音频
  2. 识别音频中的文字
  3. 将识别结果交给后续业务逻辑处理
  4. 触发 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 的无缝接入

语音识别工程链接:https://gitcode.com/gcw_M1iyh4vx/voice_recognition

posted @ 2026-08-29 16:27  2956508864  阅读(3)  评论(0)    收藏  举报