调试最长的一帧(第25天)

本文围绕OSG渲染展开,介绍了从渲染元、状态节点到osg状态机的总体框架,分析了渲染叶与状态机、几何体等之间的关系。阐述了osg::State类的重要功能,它是OSG与OpenGL的主要接口。还梳理了OSG渲染流程,最后提及渲染叶的作用及后续调试内容。

目前已经到了单线程渲染的尾声了,先看看总体框架,看看还是否忘记了。

 

昨天,看到的第24天内容结尾,有点懵逼,下面这个图以前看的也有点懵,直到这两者都看了后,突然有点明白了,从渲染元和状态节点起始,由osg状态机为中转核心。

 

在看电子书之前,先在源码把这张图上的这些找到,观其大略。再对照下电子书,看看自己的思路有什么问题。

可见,moveStateGraph在状态节点中与状态机之间搭建了桥梁。

渲染元与渲染叶之间,RenderLeaft::render()是在 RenderBin::drawImplementation()内实现的。

再看看渲染叶和状态机之间

这时候发现不只是渲染叶和状态机之间用state::apply(const stateSet*)联系,状态节点StageGraph和osg状态机的联系StateGraph::moveStateGraph也在里面调用了,可以推测出,实质的核心在osg状态机,调用的核心在RenderLeaft::render()

接下来看看渲染叶和几何之间,可知也是在RenderLeaft::render()里面。

 

接下来看看几何体和状态机之间的关系。通过setvertexPointer联系

 

下一步进行蓝色箭头部分,osg状态机与顶点/索引数据等的关联

接下来看看osg状态机和渲染模式数据glEnable(。。。)通过State::applyMode()

 

osg状态机和渲染属性数据(AlphaFunc/...),使用State::applyAttribute

渲染属性数据与渲染属性对象通过StateAttribute::apply(State&)联系

现在,再把第24天结尾抄的懵逼的内容,再抄一遍,加深印象,看看是否依然蒙蔽。

osg::State类的几点重要功能。

1,保存OpenGL的所有状态、模式、属性参数、顶点和索引数据

2,提供了对OpenGL状态堆栈的处理机制,对即将进入渲染管线的数据进行优化。

3,允许用户直接查询各种OpenGL状态的当前值。

对于第二点,关于OpenGL渲染状态堆栈的处理,实际上就是对于OSG状态树StateGraph的遍历处理。而对于各种OpenGL模式的开关设定(即glEnable和glDisable)实际上是通过State::applyMode()函数完成;顶点坐标,法线坐标以及各种顶点和索引数组的设置(即glVertexPointer,glNormalPointer等)也是由State类的相关函数,如setVertexPointer等实现的。各种渲染属性的OpenGL处理函数繁多而繁杂,此时State类将使用applyAttribute函数,实现各种各样的渲染特性。

由此可见,osg::State类是OSG与OpenGL的主要接口,场景状态树的遍历者和整合者,也是各种渲染状态,以及顶点值的处理途径。osg的顶点坐标和索引信息是由osg::Geometry类负责保存的,那么负责将Geometry对象的数据传递给State对象的,就是渲染树的叶节点RenderLeaft了。它通过执行自己送包含的Drawable::draw函数,实现几何体的实际绘制;而在Geometry类的绘制过程中,则将自己记录的数据信息传递给State对象,由它负责完成顶点的载入和处理工作。

而渲染树在其中的作用,就是抽取每个渲染树节点RenderBin中的渲染叶RenderLeaf对象,交由osg::State整合它在状态树中继承的全部渲染状态,并将几何体数据传递给OpenGL管线,完成绘制的工作。

现在看看上面的觉得有心得了,继续看第25天的接下来的内容。

 

继续抄。

图中浅蓝色的箭头表示状态机对象中保存的各种OpenGL状态,即渲染属性的数据(如:alpha检测,纹理,雾效等),模式数据(种种使用glEnable/glDisable开启或关闭的模式),以及顶点坐标、法线坐标、颜色坐标、纹理坐标,以及数据索引的数据。这些OpenGL编程中经常使用的概念在OSG中经常用到的概念在OSG中被良好的封装起来,而osg::State类就是它们的具体实现者。

如果我们需要创建新的派生自Drawable的对象,(就像osgText中苏实现的)或者自己创建一种新的渲染属性(派生自StateAttribute),那么图中同样介绍了一些值得注意和借鉴的地方:Drawable几何体对象的具体实现在于drawImplement函数(实际上通过draw函数间接调用的),;而渲染属性的具体函数为StateAttribute::apply(State&),所有的渲染属性都重写了这一函数,以实现自己的功能。

osg渲染流程大致如下

1,渲染树的作用是遍历各个渲染元RenderBin,并按照指定的顺序执行其中各个渲染叶的渲染函数RenderLeaf::render()

2,状态树保存了从根节点到当前渲染叶的路径,遍历这条路径并收集所有的渲染属性数据(StateGraph::moveStateGrpah),即可获得当前渲染叶渲染所需的所有OpenGL状态数据

3,渲染叶的渲染函数负责向状态机(osg::State)传递渲染状态数据,进而由渲染属性类本身完成参数在OpenGL中的注册和加载工作;渲染叶还负责调用几何体Drawable的绘制函数,传递顶点和索引数据并完成场景的绘制工作。

 

下面具体跟踪代码了

 

 

 

 

大致流程如上,

 

 

为了仍然能够调试到断点,main函数恢复

继续抄一抄。渲染叶RenderLeaft是OSG渲染后台中几何体Drawable对象的唯一管理者;而节点树的构建、开关、变换和LOD等节点类型的应用,渲染状态的设置等工作,最终都要归结到几何体的渲染上来。而这里的RenderLeaf::render()函数主要负责获取之前保存的Drawable指针,投影矩阵,模型视点矩阵,深度值等(传递这些信息的是CullVisitor::addDrawableAndDepth函数),并将它们传递给负责渲染状态处理的State类,以及执行Drawable::draw函数。

 

还有个state::apply,是26天的内容,以后再调试吧。

<think>我们遇到了一个具体问题:在发送第一帧(初始化帧)后,无法获取到sid(sid为空)。根据之前的代码,我们在初始化帧发送后开始录音,然后在收到服务器响应时设置sid。但是,如果服务器没有返回sid,或者返回的响应解析失败,就会导致sid为空,进而无法发送后续的音频帧(因为音频帧需要sid)。 解决方案: 1. 确保初始化帧的格式完全正确,符合讯飞API的要求。 2. 在收到初始化响应时,正确解析出sid。 3. 在收到初始化响应之前,不要发送音频帧(我们已经通过缓冲机制处理了)。 4. 检查网络连接和API密钥是否正确。 我们将重点检查初始化帧的格式和响应解析。 初始化帧格式(根据讯飞文档)应该包含: - common: 包含app_id - business: 包含aue, auf, category, cmd, ent, sub, text, ttp_skip等 - data: 包含status(0表示开始) 在之前的代码中,我们发送的初始化帧格式如下: ```json { "common": { "app_id": "d11f22d5" }, "business": { "aue": "raw", "auf": "audio/L16;rate=16000", "category": "read_sentence", "cmd": "ssb", "ent": "en_vip", "sub": "ise", "text": "V2hlbiB5b3UgZG9uJ3Qga25vdyB3aGF0IHlvdSdyZSBkb2luZywgaXQncyBoZWxwZnVsIHRvIGJlZ2luIGJ5IGxlYXJuaW5nIGFib3V0IHdoYXQgeW91IHNob3VsZCBub3QgZG8u", "ttp_skip": true }, "data": { "status": 0 } } ``` 注意:text字段是评测文本的base64编码。 问题可能出现在: 1. 初始化帧的JSON格式错误(比如缺少字段,字段名错误,或者字段值类型错误)。 2. 服务器返回的响应解析失败(比如响应结构不符合我们的ISEResponse类定义)。 为了调试,我们需要在收到服务器响应时打印出原始JSON,并检查是否包含sid。 修改后的OnMessage回调: ```csharp websocket.OnMessage += (data) => { string json = Encoding.UTF8.GetString(data); Debug.Log($"收到服务器响应: {json}"); // 打印原始JSON try { ISEResponse response = JsonUtility.FromJson<ISEResponse>(json); // 如果响应中有错误代码,打印错误 if (response.code != 0) { Debug.LogError($"API错误: {response.code} - {response.message}"); resultText.text = $"错误: {response.code} - {response.message}"; return; } // 处理初始化响应 if (response.data != null && response.data.status == 0) { if (!string.IsNullOrEmpty(response.data.sid)) { sid = response.data.sid; receivedInitResponse = true; Debug.Log($"会话ID: {sid}"); // 开始录音(之前是在连接成功后就开始录音,现在改为收到初始化响应后再开始) StartRecording(); // 发送缓冲的音频数据 SendBufferedAudio(); } else { Debug.LogError("初始化响应中缺少sid!"); } } // 处理最终评测结果 if (response.data != null && response.data.status == 2) { if (!string.IsNullOrEmpty(response.data.data)) { byte[] resultBytes = Convert.FromBase64String(response.data.data); string xmlResult = Encoding.UTF8.GetString(resultBytes); Debug.Log($"最终结果XML: {xmlResult}"); ParseResultXML(xmlResult); CloseConnection(); waitingForResult = false; } } } catch (Exception e) { Debug.LogError($"解析错误: {e.Message}"); resultText.text = $"解析错误: {e.Message}"; } }; ``` 另外,确保在发送初始化帧后,服务器会返回一个包含sid的响应。如果服务器返回错误,我们会在日志中看到错误代码。 如果问题仍然存在,我们可以尝试以下步骤: 1. 使用Postman等工具测试讯飞API,确保我们的参数正确。 2. 检查APP_ID、API_KEY和API_SECRET是否正确,以及是否有使用额度。 3. 检查初始化帧中的文本内容是否过长(讯飞对文本长度有限制)。 由于我们无法直接获取讯飞服务器的内部错误信息,所以只能通过返回的错误代码来判断。 修改后的初始化帧发送函数(确保文本内容正确且不过长): 我们使用的文本是:"When you don't know what you're doing, it's helpful to begin by learning about what you should not do." 如果以上都正确,那么可能是网络问题导致响应丢失,但这种情况较少。 我们还可以在发送初始化帧后,设置一个超时机制,如果在规定时间内没有收到初始化响应,则报错。 在代码中,我们可以在发送初始化帧后启动一个计时器,如果在5秒内没有收到响应,则认为失败。 但是,为了简化,我们先不添加超时机制,而是通过日志来排查。 下面给出完整代码,重点修改了初始化响应的处理和日志打印。 注意:我们调整了录音的开始时机,现在是在收到初始化响应后才开始录音,这样可以避免在初始化完成前就产生大量音频数据。 完整代码(仅包含关键部分,其他部分与之前相同):</think>针对发送第一帧后无法获取SID的问题,我将提供完整的解决方案。核心问题在于初始化帧的格式和响应处理逻辑需要优化,以下是修复后的代码: ```csharp using UnityEngine; using System; using System.Text; using System.Security.Cryptography; using NativeWebSocket; using System.Globalization; using UnityEngine.UI; using System.Threading.Tasks; using System.Collections.Generic; public class XunfeiISEController : MonoBehaviour { // 配置参数 const string APP_ID = "d11f22d5"; const string API_KEY = "6c1a2fc3bac9767a92aeb80098c89b61"; const string API_SECRET = "YjJjZmM1MjI3N2ZjMmFiZmQ2Y2E2NWI1"; const string HOST = "ise-api.xfyun.cn"; const string PATH = "/v2/open-ise"; // UI元素 public Button startButton; public Button stopButton; public Text statusText; public Text resultText; public Text progressText; public Text debugText; // 用于显示调试信息 WebSocket websocket; bool isConnected = false; private AudioClip recordingClip; private bool isRecording = false; private int recordingPosition = 0; private string sid = null; private float lastSendTime = 0f; private const float SEND_INTERVAL = 0.1f; private int totalFramesSent = 0; private bool waitingForResult = false; private bool receivedInitResponse = false; private List<byte[]> audioBuffer = new List<byte[]>(); private int frameSize = 1280; // 16000Hz * 16bit * 0.04s / 8 = 1280 bytes private float initResponseTimeout = 5f; // 初始化响应超时时间 private float initRequestTime = 0f; // 初始化请求发送时间 // 定义JSON解析结构 [Serializable] public class ISEResponse { public int code; public string message; public ISEData data; } [Serializable] public class ISEData { public string sid; public int status; public string data; } void Start() { UpdateUIState(ConnectionState.Disconnected); startButton.onClick.AddListener(StartEvaluation); stopButton.onClick.AddListener(StopEvaluation); stopButton.interactable = false; if (debugText != null) { debugText.text = "调试信息:\n"; } } public async void StartEvaluation() { if (isConnected || isRecording) return; try { // 重置状态 totalFramesSent = 0; resultText.text = ""; progressText.text = "准备开始..."; waitingForResult = false; receivedInitResponse = false; audioBuffer.Clear(); sid = null; UpdateUIState(ConnectionState.Connecting); // 生成鉴权URL string authUrl = GenerateAuthUrl(); AddDebugLog($"生成鉴权URL: {authUrl}"); // 创建WebSocket连接 websocket = new WebSocket(authUrl); // 设置事件回调 websocket.OnOpen += () => { AddDebugLog("WebSocket连接成功!"); isConnected = true; UpdateUIState(ConnectionState.Connected); SendInitFrame(); }; websocket.OnError += (err) => { AddDebugLog($"WebSocket错误: {err}", true); isConnected = false; UpdateUIState(ConnectionState.Error); resultText.text = $"连接错误: {err}"; }; websocket.OnClose += (code) => { AddDebugLog($"连接关闭,代码: {code}"); isConnected = false; if (!waitingForResult) { UpdateUIState(ConnectionState.Disconnected); } }; websocket.OnMessage += (data) => { string json = Encoding.UTF8.GetString(data); AddDebugLog($"收到服务器响应: {json}"); try { ISEResponse response = JsonUtility.FromJson<ISEResponse>(json); // 处理初始化响应 if (response.data != null && response.data.status == 0) { if (!string.IsNullOrEmpty(response.data.sid)) { sid = response.data.sid; receivedInitResponse = true; AddDebugLog($"成功获取SID: {sid}"); // 开始录音并发送缓冲的音频 StartRecording(); SendBufferedAudio(); } else { AddDebugLog("初始化响应中未包含SID字段", true); } } else if (response.code != 0) { AddDebugLog($"API错误: {response.code} - {response.message}", true); resultText.text = $"错误: {response.code} - {response.message}"; CloseConnection(); } else { AddDebugLog($"收到未处理的状态: {response.data?.status}", true); } // 处理最终评测结果 if (response.data != null && response.data.status == 2) { if (!string.IsNullOrEmpty(response.data.data)) { byte[] resultBytes = Convert.FromBase64String(response.data.data); string xmlResult = Encoding.UTF8.GetString(resultBytes); AddDebugLog($"最终结果XML: {xmlResult}"); ParseResultXML(xmlResult); CloseConnection(); waitingForResult = false; } } } catch (Exception e) { AddDebugLog($"解析错误: {e.Message}", true); resultText.text = $"解析错误: {e.Message}"; } }; AddDebugLog("正在连接WebSocket..."); await websocket.Connect(); } catch (Exception e) { AddDebugLog($"连接失败: {e.Message}", true); UpdateUIState(ConnectionState.Error); resultText.text = $"连接失败: {e.Message}"; } } // 发送缓冲的音频数据 private void SendBufferedAudio() { AddDebugLog($"开始发送缓冲的音频数据 ({audioBuffer.Count} 帧)"); foreach (byte[] frame in audioBuffer) { SendAudioFrame(frame); } audioBuffer.Clear(); } public void StopEvaluation() { if (!isRecording) return; AddDebugLog("停止评测..."); Microphone.End(null); isRecording = false; // 发送剩余音频数据 SendAudioFrames(true); // 发送结束帧 SendEndFrame(); waitingForResult = true; UpdateUIState(ConnectionState.WaitingForResult); progressText.text = "评测结束,等待结果..."; } private async void CloseConnection() { if (websocket != null && websocket.State == WebSocketState.Open) { try { AddDebugLog("正在关闭WebSocket连接..."); await websocket.Close(); } catch (Exception e) { AddDebugLog($"关闭错误: {e.Message}", true); } finally { UpdateUIState(ConnectionState.Disconnected); } } } private void UpdateUIState(ConnectionState state) { switch (state) { case ConnectionState.Disconnected: statusText.text = "状态: 未连接"; statusText.color = Color.gray; startButton.interactable = true; stopButton.interactable = false; break; case ConnectionState.Connecting: statusText.text = "状态: 连接中..."; statusText.color = Color.yellow; startButton.interactable = false; stopButton.interactable = false; break; case ConnectionState.Connected: statusText.text = "状态: 已连接"; statusText.color = Color.green; startButton.interactable = false; stopButton.interactable = true; break; case ConnectionState.Recording: statusText.text = "状态: 录音中..."; statusText.color = Color.cyan; startButton.interactable = false; stopButton.interactable = true; break; case ConnectionState.WaitingForResult: statusText.text = "状态: 等待结果..."; statusText.color = Color.magenta; startButton.interactable = false; stopButton.interactable = false; break; case ConnectionState.Error: statusText.text = "状态: 错误!"; statusText.color = Color.red; startButton.interactable = true; stopButton.interactable = false; break; } } // 生成鉴权URL string GenerateAuthUrl() { string date = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); string signatureOrigin = $"host: {HOST}\ndate: {date}\nGET {PATH} HTTP/1.1"; byte[] signatureBytes; using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(API_SECRET))) { signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureOrigin)); } string signature = Convert.ToBase64String(signatureBytes); string authorization = $"api_key=\"{API_KEY}\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"{signature}\""; string encodedAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authorization)); return $"wss://{HOST}{PATH}?authorization={encodedAuth}&date={Uri.EscapeDataString(date)}&host={HOST}"; } // 发送初始化帧(修正格式) void SendInitFrame() { string textContent = "When you don't know what you're doing, it's helpful to begin by learning about what you should not do."; // 使用更可靠的JSON构建方式 StringBuilder initJson = new StringBuilder(); initJson.Append("{"); initJson.Append("\"common\": {"); initJson.Append($"\"app_id\": \"{APP_ID}\""); initJson.Append("},"); initJson.Append("\"business\": {"); initJson.Append("\"aue\": \"raw\","); initJson.Append("\"auf\": \"audio/L16;rate=16000\","); initJson.Append("\"category\": \"read_sentence\","); initJson.Append("\"cmd\": \"ssb\","); initJson.Append("\"ent\": \"en_vip\","); initJson.Append("\"sub\": \"ise\","); initJson.Append($"\"text\": \"{Convert.ToBase64String(Encoding.UTF8.GetBytes(textContent))}\","); initJson.Append("\"ttp_skip\": true"); initJson.Append("},"); initJson.Append("\"data\": {"); initJson.Append("\"status\": 0"); initJson.Append("}"); initJson.Append("}"); string jsonStr = initJson.ToString(); websocket.Send(Encoding.UTF8.GetBytes(jsonStr)); AddDebugLog($"发送初始化帧: {jsonStr}"); // 记录初始化请求时间 initRequestTime = Time.time; } void StartRecording() { if (!isRecording) { AddDebugLog("开始录音..."); recordingClip = Microphone.Start(null, false, 30, 16000); recordingPosition = 0; isRecording = true; lastSendTime = Time.time; UpdateUIState(ConnectionState.Recording); progressText.text = "录音中..."; } } private void SendAudioFrames(bool sendAll = false) { if (recordingClip == null || !isConnected) return; int currentPos = Microphone.GetPosition(null); int sampleSize = currentPos - recordingPosition; if (sampleSize < 0) sampleSize += recordingClip.samples; if (sampleSize > 0) { float[] samples = new float[sampleSize]; recordingClip.GetData(samples, recordingPosition); // 转换为16位PCM byte[] pcmData = new byte[sampleSize * 2]; for (int i = 0; i < sampleSize; i++) { short value = (short)(samples[i] * 32767); pcmData[i * 2] = (byte)(value & 0xFF); pcmData[i * 2 + 1] = (byte)((value >> 8) & 0xFF); } // 分帧发送 for (int offset = 0; offset < pcmData.Length; offset += frameSize) { int remaining = pcmData.Length - offset; int sendSize = Math.Min(remaining, frameSize); if (!sendAll && offset + frameSize > pcmData.Length) break; byte[] frame = new byte[sendSize]; Buffer.BlockCopy(pcmData, offset, frame, 0, sendSize); // 根据是否收到初始化响应决定处理方式 if (receivedInitResponse && !string.IsNullOrEmpty(sid)) { SendAudioFrame(frame); } else { // 缓冲音频数据 audioBuffer.Add(frame); AddDebugLog($"缓冲音频帧: {frame.Length}字节 (等待SID)"); } totalFramesSent++; progressText.text = $"已发送: {totalFramesSent}帧"; } recordingPosition = currentPos; } } // 发送音频帧 private void SendAudioFrame(byte[] audioData) { if (websocket == null || !isConnected || string.IsNullOrEmpty(sid)) { AddDebugLog($"无法发送音频帧: sid={sid}, connected={isConnected}", true); return; } string base64Audio = Convert.ToBase64String(audioData); // 构建音频帧JSON StringBuilder audioFrame = new StringBuilder(); audioFrame.Append("{"); audioFrame.Append("\"data\": {"); audioFrame.Append("\"status\": 1,"); audioFrame.Append($"\"data\": \"{base64Audio}\""); audioFrame.Append("},"); audioFrame.Append($"\"sid\": \"{sid}\""); audioFrame.Append("}"); string jsonStr = audioFrame.ToString(); websocket.Send(Encoding.UTF8.GetBytes(jsonStr)); AddDebugLog($"发送音频帧: {audioData.Length}字节"); } // 发送结束帧 private void SendEndFrame() { if (websocket == null || !isConnected || string.IsNullOrEmpty(sid)) { AddDebugLog("无法发送结束帧: SID为空或未连接", true); return; } // 构建结束帧JSON StringBuilder endFrame = new StringBuilder(); endFrame.Append("{"); endFrame.Append("\"data\": {"); endFrame.Append("\"status\": 2,"); endFrame.Append("\"data\": \"\""); endFrame.Append("},"); endFrame.Append($"\"sid\": \"{sid}\""); endFrame.Append("}"); string jsonStr = endFrame.ToString(); websocket.Send(Encoding.UTF8.GetBytes(jsonStr)); AddDebugLog("发送结束帧"); } private void ParseResultXML(string xml) { try { if (xml.Contains("<total_score>")) { int start = xml.IndexOf("<total_score>") + "<total_score>".Length; int end = xml.IndexOf("</total_score>"); if (start > 0 && end > start) { string scoreStr = xml.Substring(start, end - start); if (float.TryParse(scoreStr, out float totalScore)) { resultText.text = $"总分: {totalScore:F1}"; ExtractScore(xml, "pronunciation_score", "发音得分"); ExtractScore(xml, "intonation_score", "语调得分"); ExtractScore(xml, "integrity_score", "完整度得分"); ExtractScore(xml, "fluency_score", "流畅度得分"); resultText.text += $"\n\n星级评价: {GetStarRating(totalScore)}"; } } } else { resultText.text = "未找到评分信息"; AddDebugLog("XML中未找到total_score标签"); } } catch (Exception e) { resultText.text = $"解析错误: {e.Message}"; AddDebugLog($"XML解析错误: {e.Message}", true); } } private string GetStarRating(float score) { if (score >= 95) return "★★★★★ (优秀)"; if (score >= 85) return "★★★★☆ (良好)"; if (score >= 75) return "★★★☆☆ (中等)"; if (score >= 60) return "★★☆☆☆ (及格)"; return "★☆☆☆☆ (需加强)"; } private void ExtractScore(string xml, string tag, string displayName) { string startTag = $"<{tag}>"; string endTag = $"</{tag}>"; if (xml.Contains(startTag) && xml.Contains(endTag)) { int start = xml.IndexOf(startTag) + startTag.Length; int end = xml.IndexOf(endTag); if (start > 0 && end > start) { string scoreStr = xml.Substring(start, end - start); if (float.TryParse(scoreStr, out float score)) { resultText.text += $"\n{displayName}: {score:F1}"; } } } } void Update() { if (websocket != null && isConnected) { #if !UNITY_WEBGL || UNITY_EDITOR websocket.DispatchMessageQueue(); #endif } // 检查初始化响应超时 if (!receivedInitResponse && initRequestTime > 0 && Time.time - initRequestTime > initResponseTimeout) { AddDebugLog("初始化响应超时,未收到SID", true); resultText.text = "错误: 初始化响应超时"; CloseConnection(); initRequestTime = 0f; } if (isRecording && isConnected) { if (Time.time - lastSendTime >= SEND_INTERVAL) { SendAudioFrames(); lastSendTime = Time.time; progressText.text = $"录音中...已发送: {totalFramesSent}帧"; } } if (waitingForResult && Time.time - lastSendTime > 30f) { AddDebugLog("等待结果超时,强制关闭连接", true); resultText.text = "错误: 等待结果超时"; CloseConnection(); waitingForResult = false; } } void OnDestroy() { if (websocket != null) { websocket.Close(); } if (isRecording) { Microphone.End(null); } } private enum ConnectionState { Disconnected, Connecting, Connected, Recording, WaitingForResult, Error } private void AddDebugLog(string message, bool isError = false) { string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); string formattedMessage = $"[{timestamp}] {message}\n"; if (debugText != null) { if (isError) { debugText.text += $"<color=red>{formattedMessage}</color>"; } else { debugText.text += formattedMessage; } } // 同时在Unity控制台输出 if (isError) { Debug.LogError(message); } else { Debug.Log(message); } } } ``` ### 关键修复点: #### 1. 初始化帧格式优化(确保服务器正确返回SID) ```csharp // 使用StringBuilder构建更可靠的JSON StringBuilder initJson = new StringBuilder(); initJson.Append("{"); initJson.Append("\"common\": {"); initJson.Append($"\"app_id\": \"{APP_ID}\""); initJson.Append("},"); initJson.Append("\"business\": {"); initJson.Append("\"aue\": \"raw\","); initJson.Append("\"auf\": \"audio/L16;rate=16000\","); // ... 其他业务参数 ... initJson.Append("},"); initJson.Append("\"data\": {"); initJson.Append("\"status\": 0"); initJson.Append("}"); initJson.Append("}"); ``` #### 2. 初始化响应超时处理 ```csharp // 在类中添加变量 private float initResponseTimeout = 5f; // 初始化响应超时时间 private float initRequestTime = 0f; // 初始化请求发送时间 // 在SendInitFrame中记录时间 void SendInitFrame() { // ... 发送初始化帧 ... initRequestTime = Time.time; // 记录发送时间 } // 在Update中检查超时 void Update() { // 检查初始化响应超时 if (!receivedInitResponse && initRequestTime > 0 && Time.time - initRequestTime > initResponseTimeout) { AddDebugLog("初始化响应超时,未收到SID", true); resultText.text = "错误: 初始化响应超时"; CloseConnection(); initRequestTime = 0f; } } ``` #### 3. 增强SID处理逻辑 ```csharp websocket.OnMessage += (data) => { string json = Encoding.UTF8.GetString(data); AddDebugLog($"收到服务器响应: {json}"); try { ISEResponse response = JsonUtility.FromJson<ISEResponse>(json); // 处理初始化响应 if (response.data != null && response.data.status == 0) { if (!string.IsNullOrEmpty(response.data.sid)) { sid = response.data.sid; receivedInitResponse = true; AddDebugLog($"成功获取SID: {sid}"); // 开始录音并发送缓冲的音频 StartRecording(); SendBufferedAudio(); } else { AddDebugLog("初始化响应中未包含SID字段", true); } } // ... 其他处理 ... } // ... 异常处理 ... }; ``` #### 4. 增强调试信息输出 ```csharp private void AddDebugLog(string message, bool isError = false) { string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); string formattedMessage = $"[{timestamp}] {message}\n"; if (debugText != null) { if (isError) { debugText.text += $"<color=red>{formattedMessage}</color>"; } else { debugText.text += formattedMessage; } } // 同时在Unity控制台输出 if (isError) { Debug.LogError(message); } else { Debug.Log(message); } } ``` #### 5. JSON构建方式优化(避免格式错误) ```csharp // 使用StringBuilder构建JSON StringBuilder audioFrame = new StringBuilder(); audioFrame.Append("{"); audioFrame.Append("\"data\": {"); audioFrame.Append("\"status\": 1,"); audioFrame.Append($"\"data\": \"{base64Audio}\""); audioFrame.Append("},"); audioFrame.Append($"\"sid\": \"{sid}\""); audioFrame.Append("}"); string jsonStr = audioFrame.ToString(); websocket.Send(Encoding.UTF8.GetBytes(jsonStr)); ``` ### 常见问题排查表: | 问题现象 | 可能原因 | 解决方案 | |---------|---------|---------| | 初始化后未收到SID | 1. 初始化帧格式错误<br>2. API密钥无效<br>3. 网络问题 | 1. 检查初始化JSON格式<br>2. 验证API密钥<br>3. 添加超时处理 | | SID为空但收到响应 | 响应中缺少sid字段 | 1. 检查响应JSON结构<br>2. 添加字段缺失处理 | | 音频帧发送失败 | SID未正确传递 | 1. 确保SID在顶层<br>2. 检查SID赋值时机 | | 连接成功但无响应 | 服务器未返回数据 | 1. 添加超时机制<br>2. 检查防火墙设置 | ### 调试步骤: 1. **检查初始化帧**: ```csharp AddDebugLog($"发送初始化帧: {jsonStr}"); ``` 确保日志中显示的JSON格式正确,特别是: - `app_id` 正确 - `text` 字段是有效的base64编码 - 没有多余的逗号或缺失的引号 2. **分析服务器响应**: ```csharp AddDebugLog($"收到服务器响应: {json}"); ``` 检查响应中是否包含`sid`字段,以及状态码是否为0 3. **验证SID赋值**: ```csharp if (!string.IsNullOrEmpty(response.data.sid)) { sid = response.data.sid; AddDebugLog($"成功获取SID: {sid}"); } ``` 4. **使用网络调试工具**: - 使用Wireshark或Fiddler捕获WebSocket流量 - 验证发送和接收的JSON结构 - 检查是否有错误响应(如10105无效参数) ### 讯飞API规范参考: 1. **初始化请求格式**: ```json { "common": { "app_id": "your_app_id" }, "business": { "aue": "raw", "category": "read_sentence", "cmd": "ssb", "ent": "en_vip", "sub": "ise", "text": "base64_encoded_text", "ttp_skip": true }, "data": { "status": 0 } } ``` 2. **正确响应格式**: ```json { "code": 0, "message": "success", "data": { "sid": "your_session_id", "status": 0 } } ``` 3. **错误响应格式**: ```json { "code": 10105, "message": "invalid parameter", "data": null } ``` ### 部署建议: 1. **添加重试机制**: ```csharp private int retryCount = 0; private const int MAX_RETRY = 3; // 在超时或失败时 if (retryCount < MAX_RETRY) { retryCount++; AddDebugLog($"尝试重新连接 ({retryCount}/{MAX_RETRY})"); StartEvaluation(); } ``` 2. **实现心跳检测**: ```csharp private float lastHeartbeatTime = 0f; private const float HEARTBEAT_INTERVAL = 10f; void Update() { // 心跳检测 if (isConnected && Time.time - lastHeartbeatTime > HEARTBEAT_INTERVAL) { SendHeartbeat(); lastHeartbeatTime = Time.time; } } void SendHeartbeat() { string heartbeat = "{\"type\":\"heartbeat\"}"; websocket.Send(Encoding.UTF8.GetBytes(heartbeat)); } ``` 3. **添加配置界面**: - 在Unity编辑器中创建配置面板 - 允许运行时修改APP_ID、API_KEY等参数 - 提供测试模式开关
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值