王兰花敲代码 发表于 5 天前

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

想问下有没有大佬知道COZE的这个WebSocket语音的双向流式对话事件的增量语音 返回的语音二进制 base64 后的字符串怎么处理才能在app端播放

BrendanEich 发表于 5 天前

关于处理COZE WebSocket语音双向流式对话的增量语音数据(base64编码字符串)在App端播放,以下是简洁明了的解决方案:

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

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

// WebSocket接收回调
void onWebSocketMessage(String base64Chunk) {
    // Base64解码
    byte[] audioData = Base64.decode(base64Chunk, Base64.DEFAULT);
   
    // 写入缓冲区
    synchronized(audioBuffer) {
      audioBuffer.write(audioData, 0, audioData.length);
    }
   
    // 通知播放器更新数据
    updateAudioPlayer();
}

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

void initAudioPlayer() {
    int sampleRate = 16000; // 根据实际音频参数修改
    int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
    int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
   
    int bufferSize = AudioTrack.getMinBufferSize(
      sampleRate,
      channelConfig,
      audioFormat
    );
   
    audioTrack = new AudioTrack(
      AudioManager.STREAM_MUSIC,
      sampleRate,
      channelConfig,
      audioFormat,
      bufferSize,
      AudioTrack.MODE_STREAM
    );
   
    audioTrack.play();
}

void updateAudioPlayer() {
    byte[] playbackData;
    synchronized(audioBuffer) {
      playbackData = audioBuffer.toByteArray();
      audioBuffer.reset(); // 清空已处理数据
    }
   
    if (playbackData.length > 0) {
      audioTrack.write(playbackData, 0, playbackData.length);
    }
}

#### 3. 结束时释放资源
void onStreamEnd() {
    audioTrack.stop();
    audioTrack.release();
    audioBuffer.close();
}

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

// 2. 处理base64片段
func handleAudioChunk(base64String: String) {
    guard let data = Data(base64Encoded: base64String) else { return }
   
    let audioFormat = AVAudioFormat(
      commonFormat: .pcmFormatInt16,
      sampleRate: 16000,
      channels: 1,
      interleaved: false
    )!
   
    let audioBuffer = AVAudioPCMBuffer(
      pcmFormat: audioFormat,
      frameCapacity: UInt32(data.count)/2
    )!
   
    audioBuffer.audioBufferList.pointee.mBuffers.mData?.copyMemory(
      from: (data as NSData).bytes,
      byteCount: data.count
    )
   
    playerNode.scheduleBuffer(audioBuffer)
}

// 3. 启动播放
try? audioEngine.start()
playerNode.play()

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

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

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

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

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

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。

王兰花敲代码 发表于 5 天前

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

我用的是uniapp开发的app,目前没使用到安卓原生,能不能直接在uniapp处理呢,里面还有一个问题是智能体返回的增量数据,单个base64Str播放不出来,拼接在一起也播放不出来就很奇怪

BrendanEich 发表于 5 天前

王兰花敲代码 发表于 2025-06-30 12:07
BrendanEich 发表于 2025-6-30 11:27
关于处理COZE W ...

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

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

// 处理增量base64数据
function processAudioChunk(base64Str) {
// 1. Base64转ArrayBuffer
const rawData = base64ToArrayBuffer(base64Str);

// 2. PCM转WAV(添加文件头)
const wavBuffer = encodeWAV(rawData, 16000, 16, 1); // 参数需匹配COZE配置

// 3. 加入播放队列
audioQueue.push(wavBuffer);

// 4. 触发播放
if (!audioContext) initAudioPlayer();
playNextChunk();
}

// Base64转ArrayBuffer
function base64ToArrayBuffer(base64) {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
    bytes = binaryString.charCodeAt(i);
}
return bytes.buffer;
}

// PCM转WAV(关键步骤)
function encodeWAV(samples, sampleRate, bitDepth, channelCount) {
const bytesPerSample = bitDepth / 8;
const blockAlign = channelCount * bytesPerSample;

const buffer = new ArrayBuffer(44 + samples.byteLength);
const view = new DataView(buffer);

// WAV文件头写入
const writeString = (offset, string) => {
    for (let i = 0; i < string.length; i++) {
      view.setUint8(offset + i, string.charCodeAt(i));
    }
};

writeString(0, 'RIFF'); // RIFF头
view.setUint32(4, 36 + samples.byteLength, true); // 文件长度
writeString(8, 'WAVE'); // WAVE标识
writeString(12, 'fmt '); // fmt块
view.setUint32(16, 16, true); // fmt块长度
view.setUint16(20, 1, true); // PCM格式
view.setUint16(22, channelCount, true); // 声道数
view.setUint32(24, sampleRate, true); // 采样率
view.setUint32(28, sampleRate * blockAlign, true); // 字节率
view.setUint16(32, blockAlign, true); // 块对齐
view.setUint16(34, bitDepth, true); // 位深
writeString(36, 'data'); // data块
view.setUint32(40, samples.byteLength, true); // 数据长度

// 写入PCM数据
const uint8View = new Uint8Array(buffer);
uint8View.set(new Uint8Array(samples), 44);

return buffer;
}

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

function playNextChunk() {
if (audioQueue.length === 0 || audioContext.state !== 'running') return;

const wavBuffer = audioQueue.shift();

audioContext.decodeAudioData(wavBuffer, buffer => {
    const source = audioContext.createBufferSource();
    source.buffer = buffer;
    source.connect(audioContext.destination);
    source.start();
    source.onended = () => playNextChunk(); // 播放下一个片段
});
}

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

socket.onmessage = (event) => {
// COZE数据格式通常为: {audio: "base64str", is_end: false}
const data = JSON.parse(event.data);
if (data.audio) {
    processAudioChunk(data.audio);
}
};

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

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

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

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

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

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。

王兰花敲代码 发表于 5 天前

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

我已经合并完成,拿到了combinedBuffer:类型为,最后一步我需要使用uniapp在app的安卓端进行播放,使用plus本地写入文件时卡住了

BrendanEich 发表于 5 天前

王兰花敲代码 发表于 2025-06-30 14:54
BrendanEich 发表于 2025-6-30 12:08
在Uniapp中处理 ...

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

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

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

// 转换函数
function pcmToWav(pcmBuffer) {
const header = new ArrayBuffer(44);
const headerView = new DataView(header);

// RIFF 头
writeString(headerView, 0, 'RIFF');
headerView.setUint32(4, 36 + pcmBuffer.byteLength, true);
writeString(headerView, 8, 'WAVE');

// fmt 块
writeString(headerView, 12, 'fmt ');
headerView.setUint32(16, 16, true);
headerView.setUint16(20, 1, true); // PCM格式
headerView.setUint16(22, CHANNELS, true);
headerView.setUint32(24, SAMPLE_RATE, true);
headerView.setUint32(28, SAMPLE_RATE * CHANNELS * (BIT_DEPTH / 8), true);
headerView.setUint16(32, CHANNELS * (BIT_DEPTH / 8), true);
headerView.setUint16(34, BIT_DEPTH, true);

// data 块
writeString(headerView, 36, 'data');
headerView.setUint32(40, pcmBuffer.byteLength, true);

// 合并 WAV 头 + PCM 数据
const wavBuffer = new Uint8Array(44 + pcmBuffer.byteLength);
wavBuffer.set(new Uint8Array(header), 0);
wavBuffer.set(new Uint8Array(pcmBuffer), 44);

return wavBuffer;
}

function writeString(view, offset, str) {
for (let i = 0; i < str.length; i++) {
    view.setUint8(offset + i, str.charCodeAt(i));
}
}

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

// 写入临时文件
const filePath = `${plus.io.PUBLIC_DOCUMENTS}/audio_${Date.now()}.wav`;
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
fs.root.getFile(filePath, { create: true }, (fileEntry) => {
    fileEntry.createWriter((writer) => {
      writer.onwrite = (e) => {
      // 播放音频
      const audio = plus.audio.createPlayer(filePath);
      audio.play();
      
      // 播放结束自动删除(可选)
      audio.onEnded(() => {
          plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
            entry.remove();
          });
      });
      };
      writer.write(new Blob(, { type: 'audio/wav' }));
    });
});
});

#### 3. 流式播放方案(避免文件写入)
如果不想写入文件,可使用 Web Audio API 直接播放:
function playAudioBuffer(buffer) {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();

// 添加 WAV 头
const wavBuffer = pcmToWav(buffer);

audioCtx.decodeAudioData(wavBuffer.buffer, (audioBuffer) => {
    const source = audioCtx.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(audioCtx.destination);
    source.start();
});
}

// 使用示例
playAudioBuffer(combinedBuffer);

### 常见问题解决

#### 1. 写入文件卡住问题排查
- **权限问题**:确保 manifest.json 添加以下权限:
"permission": {
    "Android": {
      "request": [
      "android.permission.WRITE_EXTERNAL_STORAGE"
      ]
    }
}
- **路径问题**:使用正确的前缀:
// 推荐使用私有目录
const filePath = `${plus.io.PRIVATE_WWW}/audio.wav`;

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

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

// 处理每个片段
function processChunk(chunk) {
const wavChunk = pcmToWav(chunk);
audioQueue.push(wavChunk);
playNextChunk();
}

function playNextChunk() {
if (audioQueue.length === 0) return;

const chunk = audioQueue.shift();
audioContext.decodeAudioData(chunk.buffer, (buffer) => {
    const source = audioContext.createBufferSource();
    source.buffer = buffer;
    source.connect(audioContext.destination);
    source.start();
    source.onended = playNextChunk;
});
}

### 终极调试技巧
1. **导出音频文件检查**:
   // 将WAV文件保存到相册(用于电脑端分析)
   plus.gallery.save(filePath, () => {
   uni.showToast({ title: '音频已保存到相册' });
   });
2. **在线验证工具**:
   - 使用 https://audio-analyzer.com/ 上传导出的WAV文件
   - 或使用Audacity导入原始PCM数据(需设置参数)

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

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
页: [1]
查看完整版本: coze双向流式对话事件的增量语音处理