Unity接入Whisper做语音指令

Unity接入Whisper做语音指令

本文解决什么问题:给独立游戏加一套本地中文语音指令

做独立游戏时,我常想给玩家一个「解放双手」的操作方式:喊一声「攻击」,角色就出手;喊「暂停」,游戏就停。这篇教程记录我在本机把 Unity 接入 Whisper 做语音指令 的完整过程——不走云端 API,全程本地跑 whisper.cpp,做出一个能识别「上/下/攻击/暂停」的可运行 Demo,并把我踩过的麦克风权限、WAV 采样率、中文路径、识别延迟四个坑一次讲清。

适合谁

  • 有 Unity C# 基础,想给游戏加语音控制、但不想接付费云识别的独立开发者。
  • 对本地大模型/语音识别落地感兴趣,希望离线、可控、无网络延迟的同学。

实验环境(作者本机实测)

下文所有代码与结论均在以下环境跑通,日期 2026-06:

  • 操作系统:Windows 11 23H2
  • 引擎:Unity 2022.3.21f1 LTS(2D 模板)
  • 语音识别:whisper.cpp(2024 年后可执行文件名为 whisper-cli.exe,旧版叫 main.exe
  • 模型:ggml-small.bin(约 466MB,中文识别精度/速度折中)
  • 脚本运行时:Unity 内置 Mono,.NET Standard 2.1

整体流程

麦克风输入 AudioClip WAV(16kHz) whisper.cpp 中文文本 指令映射 角色控制
图 1:Unity 本地语音指令数据流——录音后落地成 16kHz WAV,交给 whisper.cpp 转写,再把文本映射为游戏指令。

Unity 项目准备:场景、麦克风与 whisper.cpp

建测试场景

新建一个 2D 项目,在场景里放一个 Sprite 当作「玩家」(比如一个方块),挂上后面的控制脚本。UI 上加一行提示文字「按住 V 说话」即可,Demo 不需要复杂美术。

准备 whisper.cpp 与模型

从 whisper.cpp 官方仓库(github.com/ggerganov/whisper.cpp,访问日期 2026-06)获取 Windows 预编译包,或用 CMake 自行编译,得到 whisper-cli.exe。模型从 Hugging Face 仓库 ggerganov/whisper.cpp 下载 ggml-small.bin

关键:把 exe 和模型放在纯英文路径下(我放的是 D:\whisper\),原因见后文踩坑小节。目录结构:

D:\whisper\
├─ whisper-cli.exe
└─ models\
   └─ ggml-small.bin

先在命令行验证 whisper.cpp 本身能跑(用任意一段 16kHz 的中文 wav):

whisper-cli.exe -m models\ggml-small.bin -f test.wav -l zh -nt

其中 -l zh 指定中文,-nt 表示不输出时间戳、只打印纯文本,方便后面直接读它的标准输出。

实操一:C# 录制麦克风并保存为 Whisper 可识别的 WAV

whisper.cpp 内置的 WAV 读取器只认 16kHz、16-bit、单声道 PCM。Unity 的 Microphone.Start 允许直接指定采样率,所以我在录音源头就锁死 16000Hz,省掉后期重采样。

VoiceRecorder.cs——负责录音并落地 WAV:

using System.IO;
using UnityEngine;

public class VoiceRecorder : MonoBehaviour
{
    const int SampleRate = 16000;   // Whisper 只吃 16kHz
    const int MaxSeconds = 5;       // 单条指令最长录音时长

    AudioClip _clip;
    string _device;

    void Start()
    {
        if (Microphone.devices.Length == 0)
        {
            Debug.LogError("未检测到麦克风设备,请检查系统权限");
            return;
        }
        _device = Microphone.devices[0];
        Debug.Log($"使用麦克风:{_device}");
    }

    public void StartRecord()
    {
        // 第 4 个参数强制 16kHz,从源头避免采样率不匹配
        _clip = Microphone.Start(_device, false, MaxSeconds, SampleRate);
    }

    // 结束录音,写成 wav 并返回文件路径
    public string StopAndSave()
    {
        int pos = Microphone.GetPosition(_device);
        Microphone.End(_device);
        if (_clip == null || pos <= 0) return null;

        var samples = new float[pos * _clip.channels];
        _clip.GetData(samples, 0);   // 只取实际录到的采样,避免尾部空白

        string path = Path.Combine(Application.persistentDataPath, "cmd.wav");
        WavUtility.Save(path, samples, SampleRate, _clip.channels);
        return path;
    }
}

WavUtility.cs——把 Unity 的 float 采样写成标准 16-bit PCM WAV。手写 44 字节头,别偷懒用第三方库,方便你自己核对每个字段:

using System.IO;
using System.Text;
using UnityEngine;

public static class WavUtility
{
    static void Tag(BinaryWriter bw, string t) => bw.Write(Encoding.ASCII.GetBytes(t));

    public static void Save(string path, float[] samples, int sampleRate, int channels)
    {
        int byteRate = sampleRate * channels * 2;   // 16-bit = 2 字节
        int dataSize = samples.Length * 2;

        using var fs = new FileStream(path, FileMode.Create);
        using var bw = new BinaryWriter(fs);

        Tag(bw, "RIFF"); bw.Write(36 + dataSize); Tag(bw, "WAVE");
        Tag(bw, "fmt "); bw.Write(16);            // fmt 块大小
        bw.Write((short)1);                       // PCM
        bw.Write((short)channels);
        bw.Write(sampleRate);
        bw.Write(byteRate);
        bw.Write((short)(channels * 2));          // block align
        bw.Write((short)16);                      // 位深
        Tag(bw, "data"); bw.Write(dataSize);

        foreach (var s in samples)
        {
            short v = (short)(Mathf.Clamp(s, -1f, 1f) * short.MaxValue);
            bw.Write(v);
        }
    }
}

实操二:调用本地 Whisper 转写并映射成游戏指令

转写就是用 System.Diagnostics.Process 起一个 whisper.cpp 进程,读它的标准输出。注意 StandardOutputEncoding 一定要设成 UTF-8,否则中文会变乱码。

WhisperRunner.cs

using System.Diagnostics;
using System.Text;

public static class WhisperRunner
{
    // exe 与模型放英文路径,避免中文路径踩坑
    const string ExePath   = @"D:\whisper\whisper-cli.exe";
    const string ModelPath = @"D:\whisper\models\ggml-small.bin";

    public static string Transcribe(string wavPath)
    {
        var psi = new ProcessStartInfo
        {
            FileName  = ExePath,
            Arguments = $"-m \"{ModelPath}\" -f \"{wavPath}\" -l zh -nt",
            RedirectStandardOutput = true,
            RedirectStandardError  = true,
            UseShellExecute = false,
            CreateNoWindow  = true,
            StandardOutputEncoding = Encoding.UTF8   // 关键:中文不乱码
        };

        using var p = Process.Start(psi);
        string output = p.StandardOutput.ReadToEnd();
        p.WaitForExit();
        return output.Trim();
    }
}

Whisper 的中文输出可能带标点(比如「攻击。」),所以映射时用 Contains 做包含匹配,比精确相等鲁棒得多。

CommandMapper.cs

public enum VoiceCommand { None, Up, Down, Attack, Pause }

public static class CommandMapper
{
    public static VoiceCommand Map(string text)
    {
        if (string.IsNullOrEmpty(text)) return VoiceCommand.None;
        if (text.Contains("上"))   return VoiceCommand.Up;
        if (text.Contains("下"))   return VoiceCommand.Down;
        if (text.Contains("攻击")) return VoiceCommand.Attack;
        if (text.Contains("暂停")) return VoiceCommand.Pause;
        return VoiceCommand.None;
    }
}

实操三:接入角色控制,跑通语音 Demo

把三块拼起来:按住 V 录音,松开就转写并执行。转写必须放到后台线程——whisper.cpp 是同步阻塞的,直接在主线程调用会让整个 Unity 画面卡住半秒到几秒,这是我第一版最直观的翻车点。用 Task.Run 丢到线程池,await 回来后再改 Transform(Unity API 只能在主线程调,而 async void 续接后仍在主线程,安全)。

VoiceController.cs

using System.Threading.Tasks;
using UnityEngine;

public class VoiceController : MonoBehaviour
{
    public VoiceRecorder recorder;
    public Transform player;
    public float step = 1f;

    bool _busy;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.V)) recorder.StartRecord();
        if (Input.GetKeyUp(KeyCode.V) && !_busy) HandleVoice();
    }

    async void HandleVoice()
    {
        _busy = true;
        string wav = recorder.StopAndSave();
        if (wav == null) { _busy = false; return; }

        var sw = System.Diagnostics.Stopwatch.StartNew();
        // 转写放后台线程,避免卡住主线程
        string text = await Task.Run(() => WhisperRunner.Transcribe(wav));
        sw.Stop();

        Debug.Log($"识别结果:{text}(耗时 {sw.ElapsedMilliseconds} ms)");
        Apply(CommandMapper.Map(text));   // 回到主线程后再动 Transform
        _busy = false;
    }

    void Apply(VoiceCommand cmd)
    {
        switch (cmd)
        {
            case VoiceCommand.Up:     player.position += Vector3.up   * step; break;
            case VoiceCommand.Down:   player.position += Vector3.down * step; break;
            case VoiceCommand.Attack: Debug.Log("触发攻击动作"); break;
            case VoiceCommand.Pause:
                Time.timeScale = Time.timeScale > 0 ? 0 : 1; break;
        }
    }
}

在场景里把 VoiceRecorderVoiceController 挂到同一个空物体上,把玩家 Sprite 拖到 player 字段、把 recorder 引用连好。运行后按住 V 说「上」,方块上移;说「暂停」,游戏冻结。我本机识别结果长这样:

识别结果:攻击(耗时 1180 ms)
识别结果:暂停(耗时 1063 ms)

真实踩坑与报错处理

坑 1:麦克风没权限,Microphone.devices 为空

现象:Microphone.devices.Length == 0,或录出来全是静音。Windows 11 下要去「设置 → 隐私和安全性 → 麦克风」,同时打开「麦克风访问」和「让桌面应用访问你的麦克风」——注意 Unity 编辑器属于桌面应用,很多人只开了前者。改完重启 Unity 编辑器再测。

坑 2:WAV 采样率不对,whisper.cpp 直接报错

如果偷懒用了设备默认的 44100Hz 录音再喂给 whisper.cpp,会看到类似:

error: read_wav: WAV file 'cmd.wav' must be 16 kHz

解决办法就是本文的做法——在 Microphone.Start 里直接指定 16000,源头对齐,别指望后期转。写 WAV 头时 sampleRate 字段也要跟着写 16000,否则头里标错一样报错。

坑 3:模型/音频路径含中文,进程静默失败

whisper.cpp 在 Windows 上对 UTF-8 路径处理不稳,路径里带中文(比如放在「D:\语音模型\」)时经常读不到文件、进程返回空字符串却不报明显错误。我最初就是卡在这——把 exe、模型统一挪到 D:\whisper\ 纯英文目录后立刻正常。Unity 的 Application.persistentDataPath 一般也是英文,问题主要出在模型侧。

坑 4:识别延迟过高,操作跟不上

ggml-small 在纯 CPU 上跑一条 2–3 秒的短指令,我这台 Ryzen 5 大约 1–1.5 秒返回,做回合制或菜单操作够用;但要做实时动作就偏慢。我实测的几个提速方向:

  • 换更小的模型ggml-baseggml-tiny,短指令场景精度损失有限,延迟能压到几百毫秒。
  • 用量化模型:如 ggml-small-q5_0.bin,体积和耗时都更低。
  • 缩短录音窗口:指令通常 1–2 秒就够,别录满 5 秒,音频越短转写越快。
  • 务必走后台线程(见实操三),否则再快也会有肉眼可见的卡顿。

小结与完整代码

整套方案的核心就三步:Unity 端用 Microphone 以 16kHz 录音并手写成 16-bit PCM WAV → Process 调本地 whisper.cpp 转写中文 → 用 Contains 把文本映射成指令驱动角色。相比接云端 API,本地方案离线、无调用费、数据不出机,代价是要自己扛模型体积和 CPU 延迟。

项目结构一目了然:一个 2D 场景 + 一个玩家 Sprite + 四个脚本(VoiceRecorderWavUtilityWhisperRunnerCommandMapperVoiceController),whisper.cpp 与 ggml-small.bin 放在 D:\whisper\。上文已给出全部脚本,可按步骤复制到本地项目中运行;把 WhisperRunner 里的 ExePathModelPath 改成你自己的实际路径即可。想扩展就往 CommandMapper 里加词、往 VoiceController.Apply 里加动作,指令集可以自由生长。

🤖 本文部分内容由 AI 辅助生成,已经作者人工审核、实测与校订。本文使用 AI 辅助整理代码与排查报错,上述识别结果、耗时与踩坑均为作者本机(Unity 2022.3.21f1 / Windows 11 / whisper.cpp + ggml-small / 2026-06)实测复现。如发现事实或代码错误,欢迎在评论区指正。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注