ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity接入百度智能云ASR语音识别完整指南(含C#代码)

Unity接入百度智能云ASR语音识别完整指南(含C#代码) 最近好几个朋友问我Unity项目里要做语音输入到底怎么搞尤其是想在游戏或交互应用里实现语音指令、语音搜索、语音转字幕这类功能。市面上的方案看着不少但真上手就会发现坑很多要么SDK对Unity不友好要么需要自己部署模型要么文档老得跟化石一样。我自己前前后后折腾过好几个方案最后稳定用下来的还是百度智能云ASR接入简单、识别率在线而且Unity这边不需要额外SDK一个REST接口就能搞定语音转文字。这篇干货文我就把完整的落地过程扒开揉碎讲清楚从账号申请到音频处理再到鉴权、请求识别、结果解析全程带完整C#代码。你只要跟着抄配置好Key之后从0到跑通5分钟真不是夸张说法。1. 方案选型百度智能云ASR为什么值得试1.1 云端ASR与本地ASR到底怎么选先说个大方向。语音转文字ASR的实现路径基本分两条一条靠云端API一条靠本地推理。云端方案以百度智能云ASR、各家大厂的语音服务为代表本地方案常见的有FunASR、Whisper.cpp这类框架甚至有人用llama.cpp的思路跑ASR量化模型。这俩怎么选取决于你的项目形态。维度云端ASR百度智能云本地ASRWhisper/FunASR等接入成本极低HTTP请求即可较高需集成推理框架识别准确率依赖厂商模型持续迭代取决于模型版本与量化等级离线可用不行必须联网可以完全离线延迟有网络RTT但整体可接受本地计算无网络延迟隐私性音频会传到云端数据不出设备模型体积无需关心几十MB到几百MB甚至更大并发扩展厂商负责弹性扩容自己扛受设备性能限制我做选择的时候逻辑很简单如果项目是强离线场景比如车载、安防、纯本地工具那就老老实实部署本地模型但如果应用本身需要联网或者只是想快速把语音交互能力跑起来云端ASR是性价比最高的路径。百度智能云ASR起步有免费配额对个人开发和原型验证来说基本够用真正上线了再按量付费成本也可控。1.2 百度智能云ASR的技术原理给非算法同学补个底虽然我们不用自己写声学模型但理解一下ASR的链路有助于排查问题。一次完整的语音识别大致走这几个步骤音频采样 → 端点检测VAD→ 特征提取 → 声学模型 → 语言模型 → 输出文本。音频采样这部分跟Unity的关系最大。麦克风采集到的是模拟信号需要转成数字信号采样率决定了一秒采集多少个采样点。百度智能云ASR短语音识别推荐16000Hz采样率、单声道、PCM格式这个参数组合能覆盖普通语音输入的频率范围同时控制数据量不至于太大。相比电话音质的8000Hz16000Hz对人声的还原度明显更好识别率也更高。至于REST API方式本质就三步客户端把音频数据转成PCM裸数据Base64编码后塞进JSON请求体POST到百度语音识别接口服务端解码音频跑一遍识别链路返回JSON文本结果。Unity这边不需要安装任何厂商SDK一个UnityWebRequest就够这也是它能快速落地的关键原因。1.3 这个方案的适用范围与边界提一嘴边界避免大家踩到预期差。百度智能云ASR的REST接口属于短语音识别一般建议音频控制在60秒以内适合语音指令、搜索词、短句输入这类场景。如果你要做长时间会议转写、实时对话流式识别那需要换用流式接口或者长语音接口架构上会复杂一些但基础的鉴权和音频处理逻辑是一致的本文代码能复用一大半。另外既然是云端识别离线场景就不要考虑这套方案了。如果项目有离线需求那就得调研本地ASR引擎这是另一条技术路线后面有机会单独写一篇。2. 准备阶段账号、音频规范与Unity工程配置2.1 百度智能云控制台配置拿到关键的API Key和Secret Key动手写代码之前先把百度智能云的账号和应用准备好。打开百度智能云控制台完成注册和实名认证实名认证是硬性要求没认证有些服务无法开通然后在搜索框或产品列表里找到语音技术进入短语音识别服务页面点击开通服务。开通之后在控制台的应用列表里创建一个新应用。应用类型选语音技术相关的选项即可创建完成后你会得到两个关键字符串API Key和Secret Key。这两个Key就是后续获取access_token的凭证相当于你在百度智能云这边的账号密码一定不要泄露到公开代码仓库里。如果项目后续要上线建议在百度智能云后台配置IP白名单或调用量限制降低Key泄露风险。调试阶段倒是无所谓但代码里千万别把Key写死在公开的项目里最好走配置表或者云端下发。2.2 Unity工程配置麦克风权限与音频参数Unity方面先确认你的Unity版本2019 LTS以上都没问题新版Unity比如Unity 6000系列也完全兼容本文代码。然后创建一个空工程接下来的操作分平台看。如果是Android平台打开Player Settings → Other Settings → Configuration在最下方Android Permissions里勾选Microphone。iOS平台需要在Info.plist里添加NSMicrophoneUsageDescription字段否则首次调用麦克风会直接闪退或拿不到权限。这里有个我踩过的坑Unity在某些Android版本上并不会自动弹出麦克风权限请求尤其部分国产ROM会默认静默拒绝。所以写代码时最好做一层麦克风权限检测至少在被拒绝时给出明确提示而不是让玩家面对按了录音没反应的迷惑局面。音频参数方面我们的目标是产出符合百度要求的音频数据16000Hz采样率、单声道、16bit量化、PCM格式。为什么不用WAV因为WAV只是多了一个44字节的文件头对识别服务来说没区别但对客户端来说是额外的组装成本。直接用PCM裸数据代码简洁Body还更小。2.3 Unity录音背后的AudioClip机制Unity的麦克风录音走的是Microphone类调用Microphone.Start会返回一个AudioClip音频数据异步写入这个Clip。但这里有个特别容易忽略的点Microphone.Start之后数据并不会瞬间全部就绪它是边录边往AudioClip里写的。所以你不能Start完立刻GetData否则拿到的是空数据或者不完整的数据。正确做法是开始录音时记录状态停止录音时先通过Microphone.GetPosition拿到当前写入的采样帧数再根据这个帧数去AudioClip里取数据。GetPosition返回的是从录音开始到现在的采样帧总数这才是真实有效的音频长度。还有一个细节Microphone.Start有一个maxLength参数如果录制时间超过了这个值录音会自动停止AudioClip不会再变长。所以如果设置了15秒上限那么GetPosition最多也就是15 * sampleRate个采样帧。3. 核心代码实现录音、鉴权、识别一条龙3.1 音频数据转换从float数组到PCM 16bitUnity的AudioClip.GetData拿到的是一堆float取值范围是[-1, 1]这是音频的归一化采样值。而PCM 16bit编码需要的是short类型取值范围是[-32768, 32767]。转换关系很简单shortValue floatValue * 32767。这里有几个细节要处理。第一float值可能在极端情况下略微越界所以建议先Mathf.Clamp到[-1, 1]再做乘法避免生成刺耳的爆音。第二如果录音设备是多声道比如双声道AudioClip.GetData返回的数据是交错排列的也就是说数组里的顺序是左声道采样、右声道采样、左声道采样、右声道采样……百度ASR要求单声道数据所以我们要么在录音端就设定单声道要么在转换时只取其中一个声道。我试过几种麦克风设备发现部分设备即使Unity请求了单声道返回的AudioClip.channels还是2。稳妥的做法是在转换时判断channels按声道数跳着取数只保留第一通道的数据。这样无论设备是单声道还是双声道最终产出的PCM都符合要求。3.2 鉴权流程access_token的获取与缓存策略百度智能云ASR的接口调用需要一个access_token这个token通过API Key和Secret Key换取。请求方式很简单一个GET请求就能搞定https://aip.baidubce.com/oauth/2.0/token?grant_typeclient_credentialsclient_id{API Key}client_secret{Secret Key}返回的JSON长这样{ refresh_token: ..., expires_in: 2592000, access_token: ..., session_secret: ... }expires_in是token的有效期秒数一般是30天。这个token没必要每次识别都重新获取应该缓存在内存里等快过期了再刷新。我在代码里用Time.realtimeSinceStartup记录token的获取时间在过期前60秒就主动刷新避免出现识别请求中间token失效的尴尬。3.3 关键代码一个可以直接跑的BaiduASR管理器下面是完整的管理器代码。它封装了录音、停止、鉴权、识别、结果回调整个链路你只需要挂到场景里填入API Key和Secret Key然后调用StartRecord和StopRecordAndRecognize即可。using System; using System.Collections; using System.Text; using UnityEngine; using UnityEngine.Networking; [Serializable] public class TokenResponse { public string access_token; public int expires_in; } [Serializable] public class AsrResponse { public int err_no; public string err_msg; public string[] result; public string sn; } public class BaiduASR : MonoBehaviour { [Header(百度智能云配置)] [SerializeField] private string apiKey 你的API Key; [SerializeField] private string secretKey 你的Secret Key; [Header(录音参数)] [SerializeField] private int sampleRate 16000; [SerializeField] private int maxRecordSeconds 15; private AudioClip recordingClip; private string accessToken; private float tokenExpireTime; private bool isRecording; // 识别成功事件 public event Actionstring OnRecognized; // 识别失败事件参数为错误码和错误信息 public event Actionint, string OnError; /// summary /// 开始录音 /// /summary public void StartRecord() { if (Microphone.devices.Length 0) { Debug.LogWarning(未检测到麦克风设备); OnError?.Invoke(-1, No microphone device found); return; } recordingClip Microphone.Start(null, false, maxRecordSeconds, sampleRate); isRecording true; Debug.Log(录音开始); } /// summary /// 停止录音并开始识别 /// /summary public IEnumerator StopRecordAndRecognize() { if (!isRecording || recordingClip null) { Debug.LogWarning(当前没有正在进行的录音); yield break; } isRecording false; // 获取实际录音的采样帧数 int position Microphone.GetPosition(null); Microphone.End(null); if (position 0) { Debug.LogWarning(录音数据为空请检测麦克风权限); OnError?.Invoke(-1, Empty recording); yield break; } // 从AudioClip中提取float采样数据 float[] samples new float[position * recordingClip.channels]; recordingClip.GetData(samples, 0); // 转换成PCM 16bit字节数组 byte[] pcmData ConvertFloatToPcm16(samples, recordingClip.channels, position); // 演示日志输出音频字节数 Debug.Log($录音完成采样帧数: {position}, PCM字节数: {pcmData.Length}); // 确保token有效 yield return EnsureToken(); if (string.IsNullOrEmpty(accessToken)) { OnError?.Invoke(-1, Access token is empty); yield break; } // 发起语音识别 yield return RequestAsr(pcmData); } /// summary /// float采样数据转PCM 16bit保留单声道 /// /summary private byte[] ConvertFloatToPcm16(float[] samples, int channels, int frameCount) { short[] pcm new short[frameCount]; for (int i 0; i frameCount; i) { // 取第一个声道的数据多声道时跳过其他声道 float sample Mathf.Clamp(samples[i * channels], -1f, 1f); pcm[i] (short)(sample * 32767); } byte[] bytes new byte[pcm.Length * 2]; Buffer.BlockCopy(pcm, 0, bytes, 0, bytes.Length); return bytes; } /// summary /// 获取并缓存access_token /// /summary private IEnumerator EnsureToken() { // 如果token还有效直接复用 if (!string.IsNullOrEmpty(accessToken) Time.realtimeSinceStartup tokenExpireTime) { yield break; } string url $https://aip.baidubce.com/oauth/2.0/token?grant_typeclient_credentialsclient_id{apiKey}client_secret{secretKey}; using (UnityWebRequest req UnityWebRequest.Get(url)) { yield return req.SendWebRequest(); if (req.result ! UnityWebRequest.Result.Success) { Debug.LogError($获取token失败: {req.error}); accessToken null; yield break; } var tokenResp JsonUtility.FromJsonTokenResponse(req.downloadHandler.text); accessToken tokenResp.access_token; // 提前60秒过期避免边界问题 tokenExpireTime Time.realtimeSinceStartup tokenResp.expires_in - 60f; Debug.Log(获取token成功); } } /// summary /// 发送PCM音频到百度ASR并解析结果 /// /summary private IEnumerator RequestAsr(byte[] pcmData) { string base64Audio Convert.ToBase64String(pcmData); string cuid SystemInfo.deviceUniqueIdentifier; // 构造JSON请求体 StringBuilder sb new StringBuilder(); sb.Append({); sb.Append(\format\:\pcm\,); sb.Append(\rate\:).Append(sampleRate).Append(,); sb.Append(\channel\:1,); sb.Append(\cuid\:\).Append(cuid).Append(\,); sb.Append(\token\:\).Append(accessToken).Append(\,); sb.Append(\dev_pid\:1537,); sb.Append(\speech\:\).Append(base64Audio).Append(\,); sb.Append(\len\:).Append(pcmData.Length); sb.Append(}); string url https://vop.baidu.com/server/api; byte[] bodyRaw Encoding.UTF8.GetBytes(sb.ToString()); using (UnityWebRequest req new UnityWebRequest(url, POST)) { req.uploadHandler new UploadHandlerRaw(bodyRaw); req.downloadHandler new DownloadHandlerBuffer(); req.SetRequestHeader(Content-Type, application/json); req.timeout 30; yield return req.SendWebRequest(); if (req.result ! UnityWebRequest.Result.Success) { Debug.LogError($识别请求失败: {req.error}); OnError?.Invoke(-1, req.error); yield break; } string jsonText req.downloadHandler.text; Debug.Log($ASR原始返回: {jsonText}); var asrResp JsonUtility.FromJsonAsrResponse(jsonText); if (asrResp.err_no 0 asrResp.result ! null asrResp.result.Length 0) { string recognizedText asrResp.result[0]; Debug.Log($识别结果: {recognizedText}); OnRecognized?.Invoke(recognizedText); } else { Debug.LogError($ASR错误: {asrResp.err_no} - {asrResp.err_msg}); OnError?.Invoke(asrResp.err_no, asrResp.err_msg); } } } }这段代码有几个值得留意的设计。第一JsonUtility是Unity内置的JSON解析库不需要引入第三方依赖。我定义TokenResponse和AsrResponse两个类来对接百度接口的返回结构JsonUtility会根据字段名自动匹配非常简单。这里有个小坑JsonUtility对字段名的匹配区分大小写百度返回的是小写加下划线格式所以类字段也得用小写字母命名。第二构造请求体直接用了StringBuilder拼JSON字符串。为什么不直接用JsonUtility把对象序列化成JSON因为百度要求的speech字段值是一个很长很长的Base64字符串直接拼字符串最直观也不会遇到JsonUtility对深层对象序列化支持不好的问题。实测下来这种方式在数据量几千个PCM字节时毫无压力。第三UnityWebRequest发送JSON时不能简单使用UnityWebRequest.Post的默认方式否则Content-Type会被设置成application/x-www-form-urlencoded百度接口会直接报参数错误。正确做法是new UnityWebRequest(url, POST)手动挂UploadHandlerRaw和DownloadHandlerBuffer再显式设置Content-Type为application/json。3.4 场景组装从脚本到可交互的UI脚本准备好了接下来就是把它跑起来。在Unity场景里新建一个空物体挂上BaiduASR脚本把API Key和Secret Key填到Inspector面板。然后做一个简单的演示界面放两个Button开始录音、停止识别和一个Text显示结果。我这里写一个最小化的DemoUI脚本方便你直接测试using System.Collections; using UnityEngine; using UnityEngine.UI; public class DemoUI : MonoBehaviour { public BaiduASR asr; public Text resultText; private void OnEnable() { asr.OnRecognized OnRecognizedHandler; asr.OnError OnErrorHandler; } private void OnDisable() { asr.OnRecognized - OnRecognizedHandler; asr.OnError - OnErrorHandler; } public void OnStartButton() { resultText.text 录音中请说话...; asr.StartRecord(); } public void OnStopButton() { resultText.text 识别中...; StartCoroutine(asr.StopRecordAndRecognize()); } private void OnRecognizedHandler(string text) { resultText.text 识别结果: text; } private void OnErrorHandler(int code, string message) { resultText.text 识别失败: message; } }按钮的OnClick事件在Inspector面板里绑定对应方法即可。运行后点击开始录音对着麦克风说一句话点停止识别正常情况下几秒内Text里就会显示识别出来的文字。我实测的流程大概是点击停止 → 音频转PCM耗时不到50毫秒 → token有效则跳过鉴权 → HTTP请求发出到返回大约1秒到2秒。识别一句十来个字的中文短句整体体验还是挺跟手的。3.5 参数说明请求体里每个字段都代表什么百度备案接口的请求体参数值得单独拎出来解释一下因为排查问题的时候90%的坑都出在参数上。参数含义我用的值说明format音频格式pcm也可以用wav或amr但pcm最直接rate采样率16000与录音采样率保持一致channel声道数1必须是单声道cuid客户端唯一标识设备唯一ID用于调用量统计建议固定token鉴权token动态获取注意有效期30天dev_pid语言模型1537普通话输入法模型日常对话够用speech音频数据Base64字符串关键字段编码后是纯文本len音频字节数PCM字节长度与Base64解码后长度一致dev_pid这里多解释一句。1537是普通话输入法模型适合短语音输入、语音搜索1737是英语模型1936是普通话远场模型适合对话交互场景。如果你的应用有中英文混合需求可以换用对应的中英文模型具体参数以百度官方文档为准。4. 报错排查与实战优化指南4.1 百度ASR常见错误码速查表接口调用多了肯定遇到过各种错误码。我把高频出错的情况整理成一张表方便你查。错误码含义排查方向3300输入参数不正确检查JSON格式、必填字段是否遗漏3301音频格式不识别确认format字段和实际音频编码一致3302音频参数问题采样率、声道数是否与rate/channel一致3303音频数据问题检查Base64解码、len字段是否等于解码后长度3308音频过长超过接口时长限制裁剪音频或改用长语音接口3310无效token重新获取access_token3312未授权或token错误确认API Key/Secret Key配置、token是否过期3315并发超限调用量超过配额控制请求频率或升级服务3307语音服务器繁忙服务端临时问题稍后重试出现3301和3302这类错误优先检查音频数据本身出现3310和3312优先检查鉴权流程。我之前就栽过一次录音设备是双声道我忘了做单声道转换直接把双声道交错数据发过去百度一直报3302排查了半天才发现是声道数的问题。4.2 高频问题录音为空、权限失败、WebGL兼容性录音数据为空。这个问题最常见的原因是Microphone.Start之后立刻GetData。我在前面提过AudioClip的数据是异步写入的Start之后立刻停止可能连第一个采样帧都没写进去。我的建议是加入一个最低录音时长限制比如至少录0.5秒再允许提交识别。另外录音时可以用Microphone.GetPosition轮询当前帧数如果一直没有增长说明麦克风设备没在工作尽早给出提示。Android/iOS权限问题。Unity有时候会自动处理麦克风权限请求但真机环境复杂部分厂商ROM会默认拒绝。如果App第一次调用Microphone.Start时没有弹出权限框后续录音就全是静音数据。排查方法很简单先手动去手机设置里把麦克风权限打开再回到App测试如果打开权限后能正常识别说明是运行时权限申请的逻辑没走通需要引入Android权限管理插件或者在原生层处理。WebGL与微信小游戏兼容性。Unity发布WebGL平台之后Microphone类的支持情况取决于浏览器实现很多PC浏览器可以正常录音但移动端浏览器和微信小游戏环境里Microphone可能拿不到数据或者录音格式不是Unity期望的PCM。我自己在微信小游戏里就踩过这个坑最终方案是用微信小游戏原生的录音接口拿到mp3或pcm数据再通过Unity的native插件层传给C#侧处理。如果目标平台是WebGL建议先做个录音自测确认可行再往下做。4.3 提升识别准确率的几个小技巧识别准确率主要靠模型但输入音频质量同样关键。我这里说几个实际项目里有效的优化手段。第一是静音裁剪。识别接口只发送有效语音段之前可以在客户端先做个简单的能量检测把开头和结尾的静音段去掉。这能减少音频长度降低网络传输时长也能避免模型把环境噪声误识别成语气词。第二是音量归一化。如果用户说话声音过于低沉或离麦克风太远波形振幅普遍偏低转换PCM时数值会很小。可以在转PCM之前先统计一下当前音频的最大振幅做一个整体增益补偿把波形拉高到接近满幅再提交识别实测对识别率提升有帮助。第三是热词表。如果业务里频繁出现生僻词或专业术语人名、地名、品牌名、代码符号可以在百度智能云控制台配置自定义热词表。热词可以在解码时提升优先级效果明显不用改代码。第四是多语言切换。对于中英混合对话场景把dev_pid换成支持中英文的模型比用纯中文模型硬识别英文的效果好很多。切换方法就是改请求体里的一个参数非常方便。4.4 从短语音到流式识别性能与体验的下一步短语音识别适合按住说话→松开识别的交互但如果要类对话式体验用户说完话停顿一两秒就自动出结果那就需要上流式识别。百度的实时语音识别支持WebSocket长连接可以边录音边发送音频帧同时接收阶段性识别结果延迟更低交互更顺滑。流式方案的Unity实现会比REST复杂不少因为要维护WebSocket连接、需要切片发送音频数据、还要处理多帧拼接的识别结果。但好消息是本文的录音、PCM转换、鉴权逻辑完全可以复用。你把ConvertFloatToPcm16的数组切分成固定大小比如每160ms一帧通过WebSocket通道推送再接住服务器返回的结果事件就完成了从短语音到流式的升级。5. 个人实操心得与后续扩展这套方案我在好几个项目里跑过最满意的地方就是不需要引入任何Unity专用SDK核心代码就一个脚本粘贴进去就能用。调试时用Unity的编辑器模式配合电脑麦克风跑通链路后再上真机问题定位容易很多。最后分享一个我自己的血泪教训上线前一定要做一次长时间的连续压测。短语音接口虽然每次调用时间短但如果用户高频使用一天几万次调用很常见。我一开始没关注配额结果某个项目在测试期就触发了并发限制所有识别请求都开始报3315错误。后来在控制台调整了配额代码里也加了失败重试和排队机制才把问题解决。建议你在代码里对OnError回调做一层日志记录上线后持续观察错误码分布比出了问题再排查要高效得多。如果你接下来想在Unity里做语音助手、语音搜索或者会议转写工具这套百度智能云ASR的接法就是最省力的起点。跑通短语音识别之后按需切换到流式接口、接入更多语音能力整个技术栈的扩展路径就已经很清晰了。
返回列表