Xamarin Android基于WebSocket协议的微软认知语音连续识别问题
解决Xamarin Android上Azure认知语音服务WebSocket连续识别的消息发送问题
嘿,我之前帮好几个Xamarin开发者搞定过类似的Azure语音WebSocket识别难题,咱们一步步来拆解你遇到的瓶颈——核心问题大概率出在握手请求头的配置和消息发送的顺序/格式上,这也是官方WebSocket文档里最容易被忽略的细节。
首先,确认WebSocket握手的路径与请求头
Azure语音的WebSocket连接不是随便连个地址就行,必须在握手阶段就把关键参数传对:
- URL格式:
wss://<你的订阅区域>.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1?language=<目标语言>
比如中文简体就是language=zh-CN,区域要和你Azure订阅的语音服务区域完全一致(比如eastasia、westus等)。 - 必须携带的请求头:
Authorization:Bearer <你的Bearer Token>(要先调用Azure的token接口获取,有效期10分钟)X-Microsoft-OutputFormat: 指定识别结果的返回格式,比如simple(仅文本)或detailed(带置信度)Content-Type:application/jsonOrigin: 可以填你的App包名或者任意合法域名(比如https://your-app-domain.com)
我见过很多开发者在这里踩坑:要么token过期了,要么区域不匹配,要么漏了某个请求头,导致连接成功后发消息被服务器拒绝。
然后,消息发送的正确顺序
连接成功后,不能直接发音频,必须按以下步骤来:
1. 先发送启动识别的文本消息(JSON格式)
这是告诉服务器“我要开始发音频了,按这个配置来识别”,格式必须严格符合要求:
{ "context": { "system": { "version": "1.0.0", "name": "XamarinSpeechClient", "build": "Android" } }, "config": { "format": "audio/wav; codecs=audio/pcm; samplerate=16000", "language": "zh-CN", "profanity": "masked" } }
format必须和你实际发送的音频参数完全一致:16kHz采样率、单声道、16位PCM,这个是硬要求,不匹配的话服务器会直接断开。profanity可选,用来处理脏话,可选值masked(替换为*)、removed(删除)、raw(保留)。
在Xamarin里用ClientWebSocket发送这个消息的代码示例:
using System.Text.Json; using System.Net.WebSockets; // 构建启动消息 var startPayload = new { context = new { system = new { version = "1.0.0", name = "XamarinSpeechClient", build = "Android" } }, config = new { format = "audio/wav; codecs=audio/pcm; samplerate=16000", language = "zh-CN", profanity = "masked" } }; var startMessage = JsonSerializer.Serialize(startPayload); var messageBytes = Encoding.UTF8.GetBytes(startMessage); // 发送文本消息 await webSocket.SendAsync( new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None);
2. 分帧发送麦克风的PCM音频数据
接下来要捕获麦克风的音频,必须转成16kHz、单声道、16位PCM格式,然后按20ms一帧的大小发送(也就是640字节,计算方式:16000采样率 × 16位/8 × 1声道 × 0.02秒 = 640字节)。
Xamarin Android里用AudioRecord捕获音频并发送的示例:
using Android.Media; // 配置音频捕获参数 var sampleRate = 16000; var channel = ChannelIn.Mono; var encoding = Encoding.Pcm16bit; var bufferSize = AudioRecord.GetMinBufferSize(sampleRate, channel, encoding); using var audioRecord = new AudioRecord( MediaRecorder.AudioSource.Mic, sampleRate, channel, encoding, bufferSize); audioRecord.StartRecording(); var audioBuffer = new byte[640]; // 20ms帧大小 // 持续发送音频直到连接关闭 while (webSocket.State == WebSocketState.Open) { var readBytes = audioRecord.Read(audioBuffer, 0, audioBuffer.Length); if (readBytes > 0) { await webSocket.SendAsync( new ArraySegment<byte>(audioBuffer, 0, readBytes), WebSocketMessageType.Binary, endOfMessage: true, CancellationToken.None); } // 控制帧率,避免发送过快 await Task.Delay(20); } audioRecord.Stop(); audioRecord.Release();
3. 结束识别时发送停止信号
如果要停止识别,发送一个{"eof": true}的文本消息,告诉服务器“我发完了”,然后再关闭WebSocket连接:
var stopMessage = Encoding.UTF8.GetBytes("{\"eof\": true}"); await webSocket.SendAsync( new ArraySegment<byte>(stopMessage), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None); await webSocket.CloseAsync( WebSocketCloseStatus.NormalClosure, "识别结束", CancellationToken.None);
常见问题排查
- Token过期:Bearer Token有效期只有10分钟,过期后要重新获取,别一直用同一个token。
- 音频格式不匹配:一定要确保捕获的音频是16kHz、单声道、16位PCM,很多开发者不小心用了44.1kHz的采样率,直接导致识别失败。
- 帧大小不合适:不要一次性发超大的音频帧,服务器对帧大小有限制,20ms一帧是官方推荐的最优解。
- 连接超时:如果长时间没有音频输入,要定期发送空的二进制帧(比如每10秒发一次),避免服务器因为无数据断开连接。
内容的提问来源于stack exchange,提问作者Vincent Elbert Budiman




