String与Byte数组转换----EOFException问题

本文探讨了String与Byte数组之间的转换问题,特别是在不同字符集下转换导致的长度不一致现象。通过指定使用ISO-8859-1字符集,可以确保转换前后长度一致。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

String与Byte数组之间可以相互转换,但是细心人会发现,转换前的String长度与byte数组的Size大小有时候会不一样的,

为什么呢?因为charset!!!

在Eclipse IDE中,默认的charset也不知道是啥,总之是它引起的,要解决两个大小不一,可以在String与Byte数组相互转化期间,指定charset为“ISO-8859-1”,便可以解决大小不一的问题了。

 

 

有机会研究一下charset!

 

在以下代码基础上: using UnityEngine; using System; using System.Text; using System.Collections; using System.Security.Cryptography; using NativeWebSocket; using System.Globalization; 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”; WebSocket websocket; bool isConnected = false; IEnumerator Start() { // 1. 生成鉴权URL string authUrl = GenerateAuthUrl(); Debug.Log(“Connecting to: " + authUrl); // 2. 创建WebSocket连接 websocket = new WebSocket(authUrl); // 设置事件回调 websocket.OnOpen += () => { Debug.Log(“WebSocket connected!”); isConnected = true; }; websocket.OnError += (err) => { Debug.LogError(“Error: " + err); isConnected = false; }; websocket.OnClose += (code) => { Debug.Log(“Connection closed: " + code); isConnected = false; }; websocket.OnMessage += (data) => { string json = Encoding.UTF8.GetString(data); Debug.Log(“Received: " + json); // 结果解析逻辑 }; // 3. 异步连接 websocket.Connect(); // 4. 等待连接建立(最多5秒) float timeout = 5f; float startTime = Time.time; while (!isConnected && Time.time - startTime < timeout) { yield return null; } if (!isConnected) { Debug.LogError(“Connection timeout”); yield break; } // 5. 发送初始化参数帧 SendInitFrame(); } // 生成鉴权URL(严格遵循示例格式) string GenerateAuthUrl() { // 获取RFC1123格式的GMT时间(示例格式一致) string date = DateTime.UtcNow.ToString(“r”, CultureInfo.InvariantCulture); Debug.Log(“Original Date: " + date); // 构建签名字符串(注意格式) string signatureOrigin = ParseError: KaTeX parse error: Undefined control sequence: \ndate at position 14: "host: {HOST}\̲n̲d̲a̲t̲e̲: {date}\nGET {…“api_key="{API_KEY}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature}"”; Debug.Log(“Authorization: " + authorization); // Base64编码参数 string encodedAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authorization)); //string encodedDate = Convert.ToBase64String(Encoding.UTF8.GetBytes(date)); //string encodedHost = Convert.ToBase64String(Encoding.UTF8.GetBytes(HOST)); // 构造完整URL(示例格式完全一致) return "wss://HOSTPATH?"+"wss://HOSTPATH?"+“authorization={encodedAuth}&” + ParseError: KaTeX parse error: Expected 'EOF', got '&' at position 13: "date={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格式) string initJson = @”{ ““common””: { ““app_id””: “”” + APP_ID + @””” }, ““business””: { ““aue””: ““raw””, ““auf””: ““audio/L16;rate=16000"”, ““category””: ““read_sentence””, ““cmd””: ““ssb””, ““ent””: ““en_vip””, ““sub””: ““ise””, ““text””: “”” + Convert.ToBase64String(Encoding.UTF8.GetBytes(textContent)) + @”“”, ““ttp_skip””: true }, ““data””: { ““status””: 0 } }"; // 发送初始化帧 websocket.Send(Encoding.UTF8.GetBytes(initJson)); Debug.Log("Sent init frame: " + initJson); } void Update() { // 处理消息队列 if (websocket != null && isConnected) { #if !UNITY_WEBGL || UNITY_EDITOR websocket.DispatchMessageQueue(); #endif } } void OnDestroy() { if (websocket != null) { _ = websocket.Close(); } } // HMAC-SHA256计算 byte[] HMACSHA256(byte[] key, byte[] data) } 根据https://www.xfyun.cn/doc/Ise/IseAPI.html 增加录音,当结束录音时,发送音频数据,返回评测结果的功能实现
最新发布
08-09
<think>根据讯飞ISE API文档,我们需要实现以下功能: 1. 录音功能:在Unity中,我们可以使用Microphone类进行录音。 2. 在录音结束后,将录音数据发送到WebSocket服务器。 3. 接收并解析服务器返回的评测结果。 步骤: 1. 在Start方法中,我们已经在建立WebSocket连接后发送了初始化帧(InitFrame)。根据文档,初始化帧发送后,服务器会返回一个sid(会话ID),后续的数据帧需要带上这个sid。但注意,我们目前没有处理这个sid,所以需要在OnMessage回调中解析初始化帧的响应,获取sid。 2. 开始录音:我们可以在连接建立并发送初始化帧后开始录音。 3. 结束录音:例如,我们可以设定一个录音时长(比如5秒),者通过一个按钮事件来结束录音。 4. 录音结束后,将音频数据按照要求(16000采样率,16位,单声道)进行处理,并分帧发送(每帧640字节,即320ms的音频数据,因为16000采样率,16位=2字节,所以每帧数据量=16000*2*0.32=10240字节?不对,需要重新计算)。 实际上,文档中要求每次发送音频数据为640字节(即320个采样点,因为每个采样点2字节)。注意:16000采样率下,320个采样点对应20ms的音频(320/16000=0.02秒)。所以每帧音频数据为640字节,每次发送一帧。 5. 发送完所有音频数据后,发送一个结束帧(status=2)。 6. 接收服务器返回的结果,并解析。 修改点: 1. 在Start协程中,发送初始化帧后开始录音。 2. 在Update中,除了处理消息队列,还需要在录音过程中不断读取麦克风数据,并发送音频帧。 3. 添加结束录音的逻辑(比如按某个键结束,者固定时长结束)。 4. 在OnMessage回调中,解析服务器返回的消息,特别是初始化帧的响应(包含sid)和最终结果。 注意:根据文档,音频数据需要是PCM格式,16位,单声道,16000采样率。 具体步骤: 1. 修改Start协程:在发送初始化帧后,开始录音。 2. 定义录音相关变量: - AudioClip对象 - 录音是否开始的标志 - 录音结束标志 - 用于存储录音数据的缓冲区(者直接使用Microphone.GetPosition和AudioClip.GetData来读取) 3. 由于WebSocket是异步的,我们可以在Update中读取麦克风数据并发送。 但是,注意:录音数据是连续的,我们需要在录音过程中不断读取新的数据并发送。我们可以使用一个协程者每帧读取。 考虑到每帧发送的数据量很小(640字节,即20ms),我们可以每帧读取并发送,但要注意避免发送过快,因为实际录音速度是固定的。我们可以使用一个缓冲区,每次读取一定量的数据,然后发送。 然而,Unity的Microphone不会自动提供回调,所以我们需要自己循环读取。 另一种做法:开始录音后,启动一个协程,每隔20ms读取一次数据并发送。但这样可能会因为协程调度不精确导致数据丢失。因此,我们可以在Update中,根据已经录制的总长度,计算是否有新的数据可以读取。 实现步骤: 定义变量: private AudioClip recordingClip; private bool isRecording = false; private int recordingPosition = 0; // 当前录音位置(采样点) private string sid = null; // 用于存储初始化帧返回的sid 修改Start协程: 在发送初始化帧后,开始录音: recordingClip = Microphone.Start(null, false, 10, 16000); // 10秒长度,16000采样率 isRecording = true; 在Update中: 处理消息队列(已有) 如果正在录音,则检查录音位置,读取新的数据并发送。 如何读取数据: 获取当前录音位置:int currentPos = Microphone.GetPosition(null); 计算可读取的采样点数:currentPos - recordingPosition 注意:由于环形缓冲区的存在,如果currentPos小于recordingPosition,表示已经回绕(但我们录音时间短,暂不考虑) 将可读取的采样点数转换字节数(每个采样点2字节)。 每次读取至少640字节(320个采样点)的数据,然后发送一帧。 但是,由于Update每帧执行一次,而音频数据是连续产生的,我们需要确保每次发送的数据量是640字节的整数倍?实际上,我们可以每次读取所有可读数据,然后按照640字节分帧发送。 注意:文档要求每次发送640字节(320个采样点)的数据。 因此,我们可以: int sampleSize = currentPos - recordingPosition; if (sampleSize < 0) sampleSize += recordingClip.samples; // 处理回绕,但一般不会发生,因为我们录音时间短 int byteSize = sampleSize * 2; // 每个采样点2字节 // 计算有多少个完整的640字节块 int numChunks = byteSize / 640; if (numChunks > 0) { // 读取数据 float[] samples = new float[sampleSize]; recordingClip.GetData(samples, recordingPosition); // 将float数组转换byte数组(注意:Microphone返回的是float数组,范围在-1到1,需要转换为16位整数) // 但是我们之前使用Microphone.Start时,返回的AudioClip是16位整数的吗?实际上,Unity的AudioClip.GetData返回的是float数组。 // 所以我们需要将float数组转换为16位整数(short)的字节数组。 // 创建一个足够大的字节数组 byte[] audioData = new byte[sampleSize * 2]; for (int i=0; i<sampleSize; i++) { // 将float转换为short short value = (short)(samples[i] * 32767); byte[] bytes = BitConverter.GetBytes(value); audioData[i*2] = bytes[0]; audioData[i*2+1] = bytes[1]; } // 然后,将audioData按照640字节分块发送 for (int i=0; i<numChunks; i++) { byte[] chunk = new byte[640]; Buffer.BlockCopy(audioData, i*640, chunk, 0, 640); SendAudioFrame(chunk); } // 更新recordingPosition recordingPosition = (recordingPosition + numChunks * 320) % recordingClip.samples; } 但是,这样处理可能效率不高,因为每帧都要读取整个缓冲区并转换,而且如果缓冲区很大,可能会造成卡顿。 优化:我们可以每次只读取320个采样点(640字节)的数据,然后发送。但是,如何确保每次读取的位置正确? 另一种做法:我们使用一个队列,在另一个线程中读取麦克风数据,但Unity中多线程操作Unity对象比较麻烦。 因此,我们简化:每次读取当前可读数据,然后分帧发送。由于每帧发送的数据量很小(20ms),所以每帧最多可能发送几帧数据(取决于帧率,但不会太多)。 但是,如果游戏帧率很低,可能会导致数据堆积。所以,我们最好确保每帧都处理,并且游戏帧率要高于50帧(即每帧时间小于20ms)?这不一定,所以我们需要能够处理堆积的数据。 我们按照上述思路实现,但注意:如果可读数据不足640字节,则等待下一帧。 另外,结束录音:我们可以设定一个录音时长,比如5秒后自动结束。者通过按键结束。 修改: 添加一个结束录音的方法,比如按空格键停止录音。 在Update中: if (Input.GetKeyDown(KeyCode.Space)) { StopRecording(); } 停止录音时,需要将剩余数据发送(如果可读数据不是640的整数倍,则丢弃剩余部分?但文档要求每次640字节,所以只发送完整帧),然后发送结束帧(status=2)。 另外,在初始化帧的响应中,我们需要解析出sid,并在后续的数据帧中带上这个sid。 根据文档,初始化帧的响应格式: { "data": { "sid": "xxx" }, "code": 0, "message": "success" } 所以,在OnMessage回调中,我们需要解析这个sid并存储。 数据帧格式: { "data": { "status": 1, // 1表示中间数据,2表示结束 "data": "base64编码的音频数据" }, "sid": "初始化返回的sid" } 因此,发送音频帧的函数需要sid。 步骤总结: 1. 连接WebSocket,发送初始化帧。 2. 开始录音。 3. 在Update中,读取麦克风数据并分帧发送(每帧640字节)。 4. 当收到初始化响应时,保存sid。 5. 当停止录音时,发送结束帧(status=2)。 6. 解析服务器返回的结果。 代码修改: 首先,添加成员变量: private string sid = null; private AudioClip recordingClip = null; private bool isRecording = false; private int recordingPosition = 0; // 当前录音位置(采样点) 然后,修改OnMessage回调,处理初始化帧的响应: websocket.OnMessage += (data) => { string json = Encoding.UTF8.GetString(data); Debug.Log("Received: " + json); // 解析JSON try { var jsonObj = JsonUtility.FromJson<ISEBaseResponse>(json); if (jsonObj.data != null && jsonObj.data.sid != null) { sid = jsonObj.data.sid; Debug.Log("Got sid: " + sid); } // 如果收到结果帧(status=2),则解析结果 // 注意:结果帧可能有多个(中间结果和最终结果),但最终结果会有一个结束标志 // 根据文档,当status=2时,表示最后一个数据块,服务器会返回最终结果 // 但是,我们发送结束帧后,服务器会返回最终结果,所以在这里可以解析 if (jsonObj.code == 0 && jsonObj.data != null && jsonObj.data.status == 2) { // 解析结果 Debug.Log("Final result: " + json); } } catch (Exception e) { Debug.LogError("Parse message error: " + e.Message); } }; 注意:我们需要定义对应的数据结构来解析JSON。由于讯飞返回的JSON结构比较复杂,我们需要定义多个类。 但是,为了简化,我们可以先定义基础结构: [Serializable] public class ISEBaseResponse { public int code; public string message; public ISEData data; } [Serializable] public class ISEData { public string sid; public int status; // 用于数据帧响应 public string data; // 对于初始化响应,这里没有data字段,但可能有sid;对于结果,这里包含base64编码的XML结果 // 其他字段... } 但是,注意:初始化响应中,data是一个对象,包含sid,而结果帧中data字段是字符串(base64编码的XML)。所以我们需要更灵活的处理。 由于时间关系,我们简化处理:只解析sid,然后对于结果帧,我们直接打印。 接下来,实现发送音频帧的方法: void SendAudioFrame(byte[] audioChunk) { if (sid == null) { Debug.LogWarning("Sid not received, skip audio frame"); return; } // 构造数据帧 string base64Audio = Convert.ToBase64String(audioChunk); string audioFrameJson = @"{ ""data"": { ""status"": 1, ""data"": """ + base64Audio + @""" }, ""sid"": """ + sid + @""" }"; websocket.Send(Encoding.UTF8.GetBytes(audioFrameJson)); } 注意:在发送结束帧时,我们发送一个状态为2的帧,数据部分为空(者最后一帧音频数据?)。但是,我们在停止录音时,可能还有最后一帧不完整的音频数据,我们只发送完整的帧,所以结束帧只发送状态为2,数据部分为空。 void SendEndFrame() { if (sid == null) { Debug.LogWarning("Sid not received, skip end frame"); return; } string endFrameJson = @"{ ""data"": { ""status"": 2, ""data"": """" }, ""sid"": """ + sid + @""" }"; websocket.Send(Encoding.UTF8.GetBytes(endFrameJson)); } 然后,修改Update,处理录音数据: void Update() { // 处理消息队列 if (websocket != null && isConnected) { #if !UNITY_WEBGL || UNITY_EDITOR websocket.DispatchMessageQueue(); #endif } // 录音处理 if (isRecording && recordingClip != null) { // 获取当前录音位置 int currentPos = Microphone.GetPosition(null); int sampleSize = currentPos - recordingPosition; if (sampleSize < 0) // 处理环形缓冲区 { sampleSize += recordingClip.samples; } // 如果可读取的采样点数大于0 if (sampleSize > 0) { // 计算字节数 int byteSize = sampleSize * 2; // 每个采样点2字节 // 计算完整帧数(每帧320个采样点,640字节) int numFrames = sampleSize / 320; if (numFrames > 0) { // 读取数据:从recordingPosition开始,读取numFrames*320个采样点 float[] samples = new float[numFrames * 320]; recordingClip.GetData(samples, recordingPosition); // 转换字节数组 byte[] audioData = new byte[numFrames * 640]; for (int i = 0; i < samples.Length; i++) { // 将float转换为short short value = (short)(samples[i] * 32767); byte[] bytes = BitConverter.GetBytes(value); audioData[i * 2] = bytes[0]; audioData[i * 2 + 1] = bytes[1]; } // 分帧发送 for (int i = 0; i < numFrames; i++) { byte[] frame = new byte[640]; Buffer.BlockCopy(audioData, i * 640, frame, 0, 640); SendAudioFrame(frame); } // 更新recordingPosition recordingPosition = (recordingPosition + numFrames * 320) % recordingClip.samples; } } } // 按空格键停止录音 if (Input.GetKeyDown(KeyCode.Space) && isRecording) { StopRecording(); } } void StopRecording() { if (isRecording) { isRecording = false; Microphone.End(null); // 发送结束帧 SendEndFrame(); Debug.Log("Recording stopped."); } } 注意:由于我们每帧最多发送numFrames帧,所以不会造成堆积(因为录音速度是固定的,每帧产生的数据量是固定的,而我们的帧率远高于50Hz,所以可以及时发送)。 但是,如果游戏帧率很低,可能会导致延迟。我们可以考虑使用协程来定时发送,但这里为了简单,使用Update。 另外,在开始录音时,我们设定了10秒的缓冲区,所以录音最多持续10秒。 最后,在OnDestroy中停止录音和关闭WebSocket。 注意:在发送结束帧后,服务器会返回评测结果,我们在OnMessage回调中解析。 由于结果是一个base64编码的XML字符串,我们可以将其解码,然后解析XML。但根据需求,我们可以直接显示者处理。 这里,我们只打印结果。 修改OnMessage回调,当收到最终结果时(status=2),解析base64编码的XML: if (jsonObj.code == 0 && jsonObj.data != null && jsonObj.data.status == 2) { string base64Result = jsonObj.data.data; if (!string.IsNullOrEmpty(base64Result)) { byte[] bytes = Convert.FromBase64String(base64Result); string xmlResult = Encoding.UTF8.GetString(bytes); Debug.Log("Result XML: " + xmlResult); // 这里可以解析XML,获取评测结果 } } 以上是一个基本的实现。由于代码较长,下面给出完整的修改后的代码。 注意:由于讯飞API的响应结构可能复杂,我们定义的数据结构可能不完整,实际使用中需要根据返回的JSON调整。 完整代码: 注意:我们添加了必要的类和成员变量,并修改了Start和Update方法。 由于代码较长,请仔细阅读注释。</think>根据讯飞ISE API文档和你的需求,我将为你的代码添加录音功能、音频数据发送和评测结果解析功能。以下是完整的实现方案: ```csharp using UnityEngine; using System; using System.Text; using System.Collections; using System.Security.Cryptography; using NativeWebSocket; using System.Globalization; 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"; WebSocket websocket; bool isConnected = false; private AudioClip recordingClip; private bool isRecording = false; private int recordingPosition = 0; private string sid = null; // 定义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; } IEnumerator Start() { // 1. 生成鉴权URL string authUrl = GenerateAuthUrl(); Debug.Log("Connecting to: " + authUrl); // 2. 创建WebSocket连接 websocket = new WebSocket(authUrl); // 设置事件回调 websocket.OnOpen += () => { Debug.Log("WebSocket connected!"); isConnected = true; }; websocket.OnError += (err) => { Debug.LogError("Error: " + err); isConnected = false; }; websocket.OnClose += (code) => { Debug.Log("Connection closed: " + code); isConnected = false; }; websocket.OnMessage += (data) => { string json = Encoding.UTF8.GetString(data); Debug.Log("Received: " + json); // 解析结果 try { ISEResponse response = JsonUtility.FromJson<ISEResponse>(json); // 保存会话ID if (response.data != null && !string.IsNullOrEmpty(response.data.sid)) { sid = response.data.sid; Debug.Log("Session ID: " + sid); } // 处理最终评测结果 if (response.data != null && response.data.status == 2) { if (!string.IsNullOrEmpty(response.data.data)) { // 解码Base64结果 byte[] resultBytes = Convert.FromBase64String(response.data.data); string xmlResult = Encoding.UTF8.GetString(resultBytes); Debug.Log("Final Result XML:\n" + xmlResult); // 这里可以添加XML解析代码提取具体分数 ParseResultXML(xmlResult); } } } catch (Exception e) { Debug.LogError("Parse error: " + e.Message); } }; // 3. 异步连接 websocket.Connect(); // 4. 等待连接建立(最多5秒) float timeout = 5f; float startTime = Time.time; while (!isConnected && Time.time - startTime < timeout) { yield return null; } if (!isConnected) { Debug.LogError("Connection timeout"); yield break; } // 5. 发送初始化参数帧 SendInitFrame(); } // 生成鉴权URL string GenerateAuthUrl() { // 获取RFC1123格式的GMT时间 string date = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); // 构建签名字符串 string signatureOrigin = $"host: {HOST}\ndate: {date}\nGET {PATH} HTTP/1.1"; // HMAC-SHA256签名 byte[] signatureBytes = HMACSHA256( Encoding.UTF8.GetBytes(API_SECRET), 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}\""; // Base64编码授权参数 string encodedAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authorization)); // 构造完整URL 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."; // 构建初始化帧数据结构 string initJson = @"{ ""common"": { ""app_id"": """ + APP_ID + @""" }, ""business"": { ""aue"": ""raw"", ""auf"": ""audio/L16;rate=16000"", ""category"": ""read_sentence"", ""cmd"": ""ssb"", ""ent"": ""en_vip"", ""sub"": ""ise"", ""text"": """ + Convert.ToBase64String(Encoding.UTF8.GetBytes(textContent)) + @""", ""ttp_skip"": true }, ""data"": { ""status"": 0 } }"; // 发送初始化帧 websocket.Send(Encoding.UTF8.GetBytes(initJson)); Debug.Log("Sent init frame: " + initJson); } // 开始录音 public void StartRecording() { if (!isRecording) { Debug.Log("Starting recording..."); recordingClip = Microphone.Start(null, false, 10, 16000); // 10秒缓冲区 recordingPosition = 0; isRecording = true; } } // 停止录音并发送数据 public void StopRecordingAndSend() { if (isRecording) { Debug.Log("Stopping recording..."); Microphone.End(null); isRecording = false; // 发送剩余音频数据 SendAudioFrames(); // 发送结束帧 SendEndFrame(); } } // 发送音频数据帧 private void SendAudioFrames() { if (recordingClip == null || string.IsNullOrEmpty(sid)) 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); byte[] bytes = BitConverter.GetBytes(value); pcmData[i * 2] = bytes[0]; pcmData[i * 2 + 1] = bytes[1]; } // 分帧发送(每帧640字节 = 320ms) int frameSize = 640; for (int offset = 0; offset < pcmData.Length; offset += frameSize) { int remaining = pcmData.Length - offset; int sendSize = Math.Min(remaining, frameSize); byte[] frame = new byte[sendSize]; Buffer.BlockCopy(pcmData, offset, frame, 0, sendSize); SendAudioFrame(frame); } recordingPosition = currentPos; } } // 发送单帧音频数据 private void SendAudioFrame(byte[] audioData) { if (websocket == null || !isConnected || string.IsNullOrEmpty(sid)) return; string base64Audio = Convert.ToBase64String(audioData); string audioFrame = @"{ ""data"": { ""status"": 1, ""data"": """ + base64Audio + @""" }, ""sid"": """ + sid + @""" }"; websocket.Send(Encoding.UTF8.GetBytes(audioFrame)); Debug.Log($"Sent audio frame: {audioData.Length} bytes"); } // 发送结束帧 private void SendEndFrame() { if (websocket == null || !isConnected || string.IsNullOrEmpty(sid)) return; string endFrame = @"{ ""data"": { ""status"": 2, ""data"": """" }, ""sid"": """ + sid + @""" }"; websocket.Send(Encoding.UTF8.GetBytes(endFrame)); Debug.Log("Sent end frame"); } // 解析评测结果XML private void ParseResultXML(string xml) { try { // 这里简化解析,实际应用中应使用XML解析器 if (xml.Contains("<read_chapter>")) { 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 score)) { Debug.Log($"Total Score: {score}"); } } } } catch (Exception e) { Debug.LogError("XML parse error: " + e.Message); } } void Update() { // 处理消息队列 if (websocket != null && isConnected) { #if !UNITY_WEBGL || UNITY_EDITOR websocket.DispatchMessageQueue(); #endif } // 录音时发送音频帧 if (isRecording && isConnected && !string.IsNullOrEmpty(sid)) { SendAudioFrames(); } } void OnDestroy() { if (websocket != null) { websocket.Close(); } if (isRecording) { Microphone.End(null); } } // HMAC-SHA256计算 byte[] HMACSHA256(byte[] key, byte[] data) { using (var hmac = new HMACSHA256(key)) { return hmac.ComputeHash(data); } } } ``` ### 代码解释: 1. **录音功能**: - 添加了`StartRecording()`方法开始录音,使用Unity的`Microphone`类 - 设置16000Hz采样率(符合讯飞要求) - 使用10秒音频缓冲区 2. **音频数据处理**: - `StopRecordingAndSend()`停止录音并发送数据 - `SendAudioFrames()`将音频数据转换为16位PCM格式 - 分帧发送(每帧640字节=320ms音频) - 实时发送:在Update()中持续发送录音数据 3. **数据帧格式**: - 音频帧:status=1 + base64编码的PCM数据 - 结束帧:status=2 + 空数据 - 所有帧都包含会话ID(sid) 4. **结果解析**: - 添加了JSON解析结构`ISEResponse`和`ISEData` - 解析服务器返回的XML格式结果 - 提取总分示例(实际应用需完整解析XML) 5. **音频格式转换**: - 将Unity的float音频数据(-1.0~1.0)转换为16位PCM - 符合讯飞要求的L16格式 ### 使用说明: 1. 将此脚本附加到Unity场景中的GameObject 2. 调用`StartRecording()`开始录音 3. 调用`StopRecordingAndSend()`停止录音并发送数据 4. 评测结果将在控制台输出(XML格式) ### 注意事项: 1. 确保麦克风权限已开启 2. 实际使用时需要处理网络错误和超时 3. 完整的XML解析需要根据讯飞文档实现 4. 在移动设备上测试时注意性能问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值