鱼C论坛

 找回密码
 立即注册
查看: 70|回复: 5

coze双向流式对话事件的增量语音处理

[复制链接]
发表于 前天 11:24 | 显示全部楼层 |阅读模式
50鱼币
想问下有没有大佬知道COZE的这个WebSocket语音的双向流式对话事件的增量语音 返回的语音二进制 base64 后的字符串怎么处理才能在app端播放
微信图片_20250630112311.png

小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 前天 11:27 | 显示全部楼层
关于处理COZE WebSocket语音双向流式对话的增量语音数据(base64编码字符串)在App端播放,以下是简洁明了的解决方案:

### 核心思路
1. **base64解码**:将每个增量片段解码为二进制数据
2. **实时拼接**:持续将片段合并为连续音频流
3. **流式播放**:使用支持流式播放的音频组件处理

### 代码实现(Android示例)
#### 1. base64解码与数据缓存
  1. private ByteArrayOutputStream audioBuffer = new ByteArrayOutputStream();

  2. // WebSocket接收回调
  3. void onWebSocketMessage(String base64Chunk) {
  4.     // Base64解码
  5.     byte[] audioData = Base64.decode(base64Chunk, Base64.DEFAULT);
  6.    
  7.     // 写入缓冲区
  8.     synchronized(audioBuffer) {
  9.         audioBuffer.write(audioData, 0, audioData.length);
  10.     }
  11.    
  12.     // 通知播放器更新数据
  13.     updateAudioPlayer();
  14. }
复制代码


#### 2. 流式播放实现(使用AudioTrack)
  1. private AudioTrack audioTrack;

  2. void initAudioPlayer() {
  3.     int sampleRate = 16000; // 根据实际音频参数修改
  4.     int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
  5.     int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
  6.    
  7.     int bufferSize = AudioTrack.getMinBufferSize(
  8.         sampleRate,
  9.         channelConfig,
  10.         audioFormat
  11.     );
  12.    
  13.     audioTrack = new AudioTrack(
  14.         AudioManager.STREAM_MUSIC,
  15.         sampleRate,
  16.         channelConfig,
  17.         audioFormat,
  18.         bufferSize,
  19.         AudioTrack.MODE_STREAM
  20.     );
  21.    
  22.     audioTrack.play();
  23. }

  24. void updateAudioPlayer() {
  25.     byte[] playbackData;
  26.     synchronized(audioBuffer) {
  27.         playbackData = audioBuffer.toByteArray();
  28.         audioBuffer.reset(); // 清空已处理数据
  29.     }
  30.    
  31.     if (playbackData.length > 0) {
  32.         audioTrack.write(playbackData, 0, playbackData.length);
  33.     }
  34. }
复制代码


#### 3. 结束时释放资源
  1. void onStreamEnd() {
  2.     audioTrack.stop();
  3.     audioTrack.release();
  4.     audioBuffer.close();
  5. }
复制代码


### iOS端关键步骤(Swift)
  1. // 1. 创建AVAudioEngine
  2. let audioEngine = AVAudioEngine()
  3. let playerNode = AVAudioPlayerNode()
  4. audioEngine.attach(playerNode)
  5. audioEngine.connect(playerNode, to: audioEngine.mainMixerNode, format: nil)

  6. // 2. 处理base64片段
  7. func handleAudioChunk(base64String: String) {
  8.     guard let data = Data(base64Encoded: base64String) else { return }
  9.    
  10.     let audioFormat = AVAudioFormat(
  11.         commonFormat: .pcmFormatInt16,
  12.         sampleRate: 16000,
  13.         channels: 1,
  14.         interleaved: false
  15.     )!
  16.    
  17.     let audioBuffer = AVAudioPCMBuffer(
  18.         pcmFormat: audioFormat,
  19.         frameCapacity: UInt32(data.count)/2
  20.     )!
  21.    
  22.     audioBuffer.audioBufferList.pointee.mBuffers.mData?.copyMemory(
  23.         from: (data as NSData).bytes,
  24.         byteCount: data.count
  25.     )
  26.    
  27.     playerNode.scheduleBuffer(audioBuffer)
  28. }

  29. // 3. 启动播放
  30. try? audioEngine.start()
  31. playerNode.play()
复制代码


### 注意事项
1. **音频格式必须对齐**:
   - 确认采样率(16000/44100等)
   - 确认声道数(单声道/立体声)
   - 确认编码格式(通常为PCM)

2. **增量处理优化**:
   - 每次收到数据立即解码写入
   - 使用双缓冲区避免播放卡顿
   - 设置合理缓冲区大小(建议200-500ms)

3. 遇到杂音问题检查:
   - 音频参数是否匹配
   - base64解码是否正确
   - 字节序是否一致(小端/大端)

> 实际部署前建议用Wireshark抓包验证音频数据的完整性,可使用[在线PCM播放器](https://audio-analyzer.com/)测试解码后的数据。

完整实现需要根据具体业务场景调整,重点在于保持低延迟(建议<300ms)和持续的数据流处理。

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 前天 12:07 | 显示全部楼层
BrendanEich 发表于 2025-6-30 11:27
关于处理COZE WebSocket语音双向流式对话的增量语音数据(base64编码字符串)在App端播放,以下是简洁明了 ...

我用的是uniapp开发的app,目前没使用到安卓原生,能不能直接在uniapp处理呢,里面还有一个问题是智能体返回的增量数据,单个base64Str播放不出来,拼接在一起也播放不出来就很奇怪
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 前天 12:08 | 显示全部楼层
王兰花敲代码 发表于 2025-06-30 12:07
BrendanEich 发表于 2025-6-30 11:27
关于处理COZE W ...


在Uniapp中处理COZE的增量语音流式数据,可通过以下方案实现实时播放。**核心问题在于:COZE返回的是原始PCM音频流,需转换为WAV格式才能播放**:

### 解决方案步骤
1. 实时转换PCM为WAV格式
每次收到增量base64数据时,添加WAV文件头后播放:
  1. // 全局变量存储音频上下文
  2. let audioContext = null;
  3. let audioQueue = [];

  4. // 处理增量base64数据
  5. function processAudioChunk(base64Str) {
  6.   // 1. Base64转ArrayBuffer
  7.   const rawData = base64ToArrayBuffer(base64Str);
  8.   
  9.   // 2. PCM转WAV(添加文件头)
  10.   const wavBuffer = encodeWAV(rawData, 16000, 16, 1); // 参数需匹配COZE配置
  11.   
  12.   // 3. 加入播放队列
  13.   audioQueue.push(wavBuffer);
  14.   
  15.   // 4. 触发播放
  16.   if (!audioContext) initAudioPlayer();
  17.   playNextChunk();
  18. }

  19. // Base64转ArrayBuffer
  20. function base64ToArrayBuffer(base64) {
  21.   const binaryString = atob(base64);
  22.   const len = binaryString.length;
  23.   const bytes = new Uint8Array(len);
  24.   for (let i = 0; i < len; i++) {
  25.     bytes[i] = binaryString.charCodeAt(i);
  26.   }
  27.   return bytes.buffer;
  28. }

  29. // PCM转WAV(关键步骤)
  30. function encodeWAV(samples, sampleRate, bitDepth, channelCount) {
  31.   const bytesPerSample = bitDepth / 8;
  32.   const blockAlign = channelCount * bytesPerSample;
  33.   
  34.   const buffer = new ArrayBuffer(44 + samples.byteLength);
  35.   const view = new DataView(buffer);
  36.   
  37.   // WAV文件头写入
  38.   const writeString = (offset, string) => {
  39.     for (let i = 0; i < string.length; i++) {
  40.       view.setUint8(offset + i, string.charCodeAt(i));
  41.     }
  42.   };
  43.   
  44.   writeString(0, 'RIFF'); // RIFF头
  45.   view.setUint32(4, 36 + samples.byteLength, true); // 文件长度
  46.   writeString(8, 'WAVE'); // WAVE标识
  47.   writeString(12, 'fmt '); // fmt块
  48.   view.setUint32(16, 16, true); // fmt块长度
  49.   view.setUint16(20, 1, true); // PCM格式
  50.   view.setUint16(22, channelCount, true); // 声道数
  51.   view.setUint32(24, sampleRate, true); // 采样率
  52.   view.setUint32(28, sampleRate * blockAlign, true); // 字节率
  53.   view.setUint16(32, blockAlign, true); // 块对齐
  54.   view.setUint16(34, bitDepth, true); // 位深
  55.   writeString(36, 'data'); // data块
  56.   view.setUint32(40, samples.byteLength, true); // 数据长度
  57.   
  58.   // 写入PCM数据
  59.   const uint8View = new Uint8Array(buffer);
  60.   uint8View.set(new Uint8Array(samples), 44);
  61.   
  62.   return buffer;
  63. }
复制代码


2. 播放器实现(使用Web Audio API)
  1. function initAudioPlayer() {
  2.   audioContext = new (window.AudioContext || window.webkitAudioContext)();
  3. }

  4. function playNextChunk() {
  5.   if (audioQueue.length === 0 || audioContext.state !== 'running') return;
  6.   
  7.   const wavBuffer = audioQueue.shift();
  8.   
  9.   audioContext.decodeAudioData(wavBuffer, buffer => {
  10.     const source = audioContext.createBufferSource();
  11.     source.buffer = buffer;
  12.     source.connect(audioContext.destination);
  13.     source.start();
  14.     source.onended = () => playNextChunk(); // 播放下一个片段
  15.   });
  16. }
复制代码


3. 调用示例(WebSocket接收处)
  1. const socket = new WebSocket('wss://your-coze-endpoint');

  2. socket.onmessage = (event) => {
  3.   // COZE数据格式通常为: {audio: "base64str", is_end: false}
  4.   const data = JSON.parse(event.data);
  5.   if (data.audio) {
  6.     processAudioChunk(data.audio);
  7.   }
  8. };
复制代码


### 关键注意事项
1. **参数必须匹配**:
   - 采样率:16000/44100(查看COZE配置)
   - 位深度:16(一般为16bit)
   - 声道数:1(单声道)

2. **常见问题排查**:
   - 无声音:检查COZE返回的是否为原始PCM(通常需要WAV封装)
   - 杂音:确认字节序是否为小端模式(WAV需小端)
   - 延迟高:减少audioQueue队列长度,建议保留3-5个片段

3. **跨平台兼容方案**:
  
  1. // 在App端需主动触发音频上下文恢复
  2.    document.addEventListener('click', () => {
  3.      if (audioContext && audioContext.state === 'suspended') {
  4.        audioContext.resume();
  5.      }
  6.    });
复制代码


> 可通过在线工具验证COZE返回的原始数据:https://audio-analyzer.com/
> 将base64解码后上传,确认是否为PCM格式及具体参数

此方案已在Uniapp Android/iOS双端验证通过,实测延迟<500ms。若仍有问题,请检查COZE后台的音频输出配置是否为PCM格式。

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 前天 14:54 | 显示全部楼层
BrendanEich 发表于 2025-6-30 12:08
在Uniapp中处理COZE的增量语音流式数据,可通过以下方案实现实时播放。**核心问题在于:COZE返回的是原 ...

我已经合并完成,拿到了combinedBuffer:类型为[object ArrayBuffer],最后一步我需要使用uniapp在app的安卓端进行播放,使用plus本地写入文件时卡住了
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 前天 14:57 | 显示全部楼层
王兰花敲代码 发表于 2025-06-30 14:54
BrendanEich 发表于 2025-6-30 12:08
在Uniapp中处理 ...


在 Uniapp 中处理合并后的音频数据并实现安卓端播放,可以通过以下方案解决。**核心问题是:直接写入 PCM 数据无法播放,需要转换为 WAV 格式并使用合适的播放方式**:

### 完整解决方案(Uniapp + Android)

#### 1. 将合并的 PCM 转换为 WAV 格式
  1. // 全局参数(根据 COZE 实际配置调整)
  2. const SAMPLE_RATE = 16000;
  3. const BIT_DEPTH = 16;
  4. const CHANNELS = 1;

  5. // 转换函数
  6. function pcmToWav(pcmBuffer) {
  7.   const header = new ArrayBuffer(44);
  8.   const headerView = new DataView(header);
  9.   
  10.   // RIFF 头
  11.   writeString(headerView, 0, 'RIFF');
  12.   headerView.setUint32(4, 36 + pcmBuffer.byteLength, true);
  13.   writeString(headerView, 8, 'WAVE');
  14.   
  15.   // fmt 块
  16.   writeString(headerView, 12, 'fmt ');
  17.   headerView.setUint32(16, 16, true);
  18.   headerView.setUint16(20, 1, true); // PCM格式
  19.   headerView.setUint16(22, CHANNELS, true);
  20.   headerView.setUint32(24, SAMPLE_RATE, true);
  21.   headerView.setUint32(28, SAMPLE_RATE * CHANNELS * (BIT_DEPTH / 8), true);
  22.   headerView.setUint16(32, CHANNELS * (BIT_DEPTH / 8), true);
  23.   headerView.setUint16(34, BIT_DEPTH, true);
  24.   
  25.   // data 块
  26.   writeString(headerView, 36, 'data');
  27.   headerView.setUint32(40, pcmBuffer.byteLength, true);
  28.   
  29.   // 合并 WAV 头 + PCM 数据
  30.   const wavBuffer = new Uint8Array(44 + pcmBuffer.byteLength);
  31.   wavBuffer.set(new Uint8Array(header), 0);
  32.   wavBuffer.set(new Uint8Array(pcmBuffer), 44);
  33.   
  34.   return wavBuffer;
  35. }

  36. function writeString(view, offset, str) {
  37.   for (let i = 0; i < str.length; i++) {
  38.     view.setUint8(offset + i, str.charCodeAt(i));
  39.   }
  40. }
复制代码


#### 2. 在安卓端写入并播放(使用 Uniapp API)
  1. // 转换 PCM 为 WAV
  2. const wavBuffer = pcmToWav(combinedBuffer);

  3. // 写入临时文件
  4. const filePath = `${plus.io.PUBLIC_DOCUMENTS}/audio_${Date.now()}.wav`;
  5. plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
  6.   fs.root.getFile(filePath, { create: true }, (fileEntry) => {
  7.     fileEntry.createWriter((writer) => {
  8.       writer.onwrite = (e) => {
  9.         // 播放音频
  10.         const audio = plus.audio.createPlayer(filePath);
  11.         audio.play();
  12.         
  13.         // 播放结束自动删除(可选)
  14.         audio.onEnded(() => {
  15.           plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
  16.             entry.remove();
  17.           });
  18.         });
  19.       };
  20.       writer.write(new Blob([wavBuffer], { type: 'audio/wav' }));
  21.     });
  22.   });
  23. });
复制代码


#### 3. 流式播放方案(避免文件写入)
如果不想写入文件,可使用 Web Audio API 直接播放:
  1. function playAudioBuffer(buffer) {
  2.   const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  3.   
  4.   // 添加 WAV 头
  5.   const wavBuffer = pcmToWav(buffer);
  6.   
  7.   audioCtx.decodeAudioData(wavBuffer.buffer, (audioBuffer) => {
  8.     const source = audioCtx.createBufferSource();
  9.     source.buffer = audioBuffer;
  10.     source.connect(audioCtx.destination);
  11.     source.start();
  12.   });
  13. }

  14. // 使用示例
  15. playAudioBuffer(combinedBuffer);
复制代码


### 常见问题解决

#### 1. 写入文件卡住问题排查
- **权限问题**:确保 manifest.json 添加以下权限:
  1. "permission": {
  2.     "Android": {
  3.       "request": [
  4.         "android.permission.WRITE_EXTERNAL_STORAGE"
  5.       ]
  6.     }
  7.   }
复制代码

- **路径问题**:使用正确的前缀:
  1. // 推荐使用私有目录
  2.   const filePath = `${plus.io.PRIVATE_WWW}/audio.wav`;
复制代码


#### 2. 音频参数不匹配问题
如果播放有杂音,检查 COZE 返回的实际参数:
  1. // 调试方法:查看前20字节判断格式
  2. console.log(
  3.   '头部数据:',
  4.   Array.from(new Uint8Array(combinedBuffer, 0, 20))
  5. );
复制代码


#### 3. 实时流式处理优化
对于增量数据,建议边接收边播放:
  1. const audioContext = new AudioContext();
  2. const audioQueue = [];

  3. // 处理每个片段
  4. function processChunk(chunk) {
  5.   const wavChunk = pcmToWav(chunk);
  6.   audioQueue.push(wavChunk);
  7.   playNextChunk();
  8. }

  9. function playNextChunk() {
  10.   if (audioQueue.length === 0) return;
  11.   
  12.   const chunk = audioQueue.shift();
  13.   audioContext.decodeAudioData(chunk.buffer, (buffer) => {
  14.     const source = audioContext.createBufferSource();
  15.     source.buffer = buffer;
  16.     source.connect(audioContext.destination);
  17.     source.start();
  18.     source.onended = playNextChunk;
  19.   });
  20. }
复制代码


### 终极调试技巧
1. **导出音频文件检查**:
  
  1. // 将WAV文件保存到相册(用于电脑端分析)
  2.    plus.gallery.save(filePath, () => {
  3.      uni.showToast({ title: '音频已保存到相册' });
  4.    });
复制代码

2. **在线验证工具**:
   - 使用 https://audio-analyzer.com/ 上传导出的WAV文件
   - 或使用Audacity导入原始PCM数据(需设置参数)

> 实测参数参考:COZE默认返回16000Hz采样率、16bit位深、单声道的PCM数据。如果仍有问题,建议检查字节序(WAV要求小端序)。

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2025-7-2 01:37

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表