Coze-JS WsChatClient 实时语音对话源码解析

WsChatClient 源码解析与实战指南

WsChatClient 是一个基于 WebSocket 的实时语音交互客户端 ,核心能力是整合音频录制(PcmRecorder)、音频播放(WavStreamPlayer)、WebSocket 通信三大模块,实现"麦克风输入→降噪处理→实时推流→服务端响应→音频播放"的端到端语音交互流程,适用于智能客服、语音助手、实时语音对话等场景。

一、核心架构与依赖

1.1 技术栈与核心依赖

依赖模块/工具 用途描述
BaseWsChatClient 父类,封装 WebSocket 基础通信能力(连接建立、消息发送/接收、事件监听)
PcmRecorder 音频录制核心,支持 PCM 格式采集、AI 降噪、多设备切换
WavStreamPlayer 音频播放工具,支持 PCM/Opus 等格式的实时流播放、本地回环(监听自己的声音)
uuid 生成唯一 ID,用于标识 WebSocket 消息、事件等
工具函数(getAudioDevices 等) 音频设备管理、对象路径赋值、系统判断(如鸿蒙系统检测)

1.2 核心数据类型

类型名 用途描述
WsChatClientOptions 客户端初始化配置,包含音频采集参数(audioCaptureConfig)、AI 降噪配置(aiDenoisingConfig)、调试开关(debug)等
AudioRecordEvent 音频录制事件,用于传递录制完成的 WAV 文件(如调试时的音频 dump)
ChatUpdateEvent WebSocket 初始化消息,包含音频编解码格式、判停模式(turn_detection)、采样率等核心配置
AudioCodec 音频编解码器枚举(如 pcmopus),决定音频数据的压缩/传输格式
TurnDetectionType 语音判停模式(server_vad 服务端检测、client_interrupt 客户端手动打断)

二、核心功能模块解析

2.1 初始化流程(constructor)

ts 复制代码
constructor(config: WsChatClientOptions) {
  super(config); // 调用父类 BaseWsChatClient 初始化 WebSocket 基础配置

  // 1. 初始化音频播放器(WavStreamPlayer)
  const isMobilePlayer = config.enableLocalLoopback ?? isHarmonOS(); // 适配鸿蒙系统
  this.wavStreamPlayer = new WavStreamPlayer({
    sampleRate: 24000, // 默认播放采样率(与多数语音服务兼容)
    enableLocalLoopback: isMobilePlayer, // 是否开启本地回环(移动端默认开启)
    volume: config.playbackVolumeDefault ?? 1, // 默认播放音量(0-1)
  });

  // 2. 初始化音频录制器(PcmRecorder)
  this.recorder = new PcmRecorder({
    audioCaptureConfig: config.audioCaptureConfig, // 音频采集参数(如采样率、声道数)
    aiDenoisingConfig: config.aiDenoisingConfig,   // AI 降噪配置(如模式、等级)
    mediaStreamTrack: config.mediaStreamTrack,     // 外部传入的媒体流(可选)
    wavRecordConfig: config.wavRecordConfig,       // WAV 录制参数(如是否保存本地文件)
    debug: config.debug,                           // 调试开关
    deviceId: config.deviceId,                     // 默认麦克风设备 ID
  });

  // 3. 初始化静音状态(默认不静音)
  this.isMuted = config.audioMutedDefault ?? false;
}
  • 核心初始化逻辑:先通过父类建立 WebSocket 基础能力,再初始化"录制-播放"双核心组件,同时适配移动端(如鸿蒙系统)的本地回环需求。

2.2 实时语音录制与推流(startRecord)

startRecord 是客户端的核心方法,负责启动音频录制、处理 PCM 数据,并通过 WebSocket 实时推送到服务端:

ts 复制代码
async startRecord() {
  // 1. 状态检查:避免重复录制
  if (this.recorder.getStatus() === 'recording') {
    console.warn('Recorder is already recording');
    return;
  }

  // 2. 客户端判停模式下,先打断当前播放(避免混音)
  if (this.turnDetection === 'client_interrupt') {
    this.interrupt();
  }

  // 3. 启动录制器(指定输入音频编解码器,默认 PCM)
  await this.recorder.start(this.inputAudioCodec);

  // 4. 本地回环配置:将麦克风原始输入传递给播放器,实现"听自己的声音"
  const rawMediaStream = this.recorder.getRawMediaStream();
  if (!rawMediaStream) {
    throw new Error('无法获取原始麦克风输入');
  }
  this.wavStreamPlayer?.setMediaStream(rawMediaStream);

  // 5. 开始录制并处理音频数据
  await this.recorder.record({
    // 5.1 PCM 数据回调:实时推流到服务端
    pcmAudioCallback: data => {
      const { raw } = data; // raw 为 PCM 原始 ArrayBuffer 数据

      // 转换 ArrayBuffer 为 Base64(便于 WebSocket 传输文本格式)
      const base64String = btoa(
        Array.from(new Uint8Array(raw))
          .map(byte => String.fromCharCode(byte))
          .join(''),
      );

      // 通过 WebSocket 发送音频数据
      this.ws?.send({
        id: uuid(), // 唯一消息 ID(用于追踪或重发)
        event_type: WebsocketsEventType.INPUT_AUDIO_BUFFER_APPEND, // 推流事件类型
        data: { delta: base64String }, // 音频数据(Base64 编码)
      });
    },

    // 5.2 WAV 数据回调:录制完成后生成 WAV 文件(调试/存档用)
    wavAudioCallback: (blob, name) => {
      const event: AudioRecordEvent = {
        event_type: ClientEventType.AUDIO_INPUT_DUMP,
        data: { name, wav: blob }, // blob 为 WAV 文件二进制流
      };
      this.emit(WsChatEventNames.AUDIO_INPUT_DUMP, event); // 触发事件,供外部处理(如下载)
    },

    // 5.3 调试用音频 dump 回调(与 wavAudioCallback 功能类似,视需求扩展)
    dumpAudioCallback: (blob, name) => {
      // 逻辑同 wavAudioCallback
    },
  });
}
  • 关键逻辑

    • 数据转换:PCM 原始二进制(ArrayBuffer)→ Base64 字符串,解决 WebSocket 传输二进制的兼容性问题;
    • 本地回环:通过 setMediaStream 将麦克风输入实时播放,提升用户交互体验(如确认自己的声音已被采集);
    • 事件驱动:通过 emit 触发音频 dump 事件,便于外部扩展(如下载录制文件、上传存档)。

2.3 WebSocket 连接与初始化(connect)

connect 方法负责建立 WebSocket 连接、同步服务端配置(如编解码格式、判停模式),并启动录制:

ts 复制代码
async connect({ chatUpdate }: { chatUpdate?: ChatUpdateEvent } = {}) {
  // 1. 初始化 WebSocket 连接(父类 BaseWsChatClient 方法)
  const ws = await this.init();
  this.ws = ws;

  // 2. 构建初始化消息(ChatUpdateEvent):同步客户端与服务端配置
  const event: ChatUpdateEvent = {
    id: chatUpdate?.id || uuid(), // 消息 ID(外部传入或自动生成)
    event_type: WebsocketsEventType.CHAT_UPDATE, // 配置同步事件类型
    data: {
      // 输入音频配置(默认 PCM 格式)
      input_audio: { format: 'pcm', codec: 'pcm' },
      // 输出音频配置(默认 PCM 格式,采样率 24000Hz)
      output_audio: { codec: 'pcm', pcm_config: { sample_rate: 24000 } },
      // 语音判停模式(默认服务端 VAD 检测)
      turn_detection: { type: 'server_vad' },
      need_play_prologue: true, // 是否播放启动提示音(如"您好,有什么可以帮您?")
      ...chatUpdate?.data, // 合并外部传入的配置(覆盖默认值)
    },
  };

  // 3. 补充配置:语音 ID(如指定服务端返回的语音音色)
  if (this.config.voiceId) {
    setValueByPath(event, 'data.output_audio.voice_id', this.config.voiceId);
  }

  // 4. 输入音频采样率配置(默认 48000Hz,覆盖外部传入值)
  setValueByPath(
    event,
    'data.input_audio.sample_rate',
    event.data?.input_audio?.sample_rate || 48000,
  );

  // 5. 解析配置:更新客户端编解码、采样率等核心参数
  this.inputAudioCodec = event.data?.input_audio?.codec || 'pcm'; // 输入编解码器
  this.outputAudioCodec = event.data?.output_audio?.codec || 'pcm'; // 输出编解码器
  if (this.outputAudioCodec === 'opus') {
    // 若输出为 Opus 格式,初始化 Opus 解码器
    this.outputAudioSampleRate = event.data?.output_audio?.opus_config?.sample_rate || 24000;
    await this.initOpusDecoder();
  } else {
    // 若为 PCM 格式,直接设置采样率
    this.outputAudioSampleRate = event.data?.output_audio?.pcm_config?.sample_rate || 24000;
  }

  // 6. 同步播放器配置(格式、采样率)
  this.wavStreamPlayer?.setDefaultFormat(
    (event.data?.output_audio?.codec as AudioFormat) || 'pcm',
  );
  this.wavStreamPlayer?.setSampleRate(this.outputAudioSampleRate);

  // 7. 同步句子同步器配置(确保音频播放与文本同步,如字幕)
  this.sentenceSynchronizer.setOutputAudioConfig(
    this.outputAudioSampleRate,
    this.outputAudioCodec,
  );

  // 8. 语音判停模式配置:客户端手动打断模式下,关闭本地回环(避免混音)
  this.turnDetection = event.data?.turn_detection?.type || 'server_vad';
  if (this.turnDetection === 'client_interrupt') {
    this.wavStreamPlayer?.setLocalLoopbackEnable(false);
  }

  // 9. 发送初始化消息到服务端
  this.ws.send(event);

  // 10. 启动录制:非静音且非客户端打断模式下,自动开始录制
  if (!this.isMuted && this.turnDetection !== 'client_interrupt') {
    await this.startRecord();
  }

  // 11. 触发连接成功事件,供外部处理(如更新 UI 状态)
  this.emit(WsChatEventNames.CONNECTED, event);
  this.isConnected = true;
}
  • 核心作用 :作为客户端与服务端的"配置桥梁",通过 ChatUpdateEvent 同步编解码、采样率、判停模式等关键参数,确保双方数据格式兼容;同时根据配置自动初始化解码器、播放器,为后续交互做好准备。

2.4 语音交互控制(关键辅助方法)

2.4.1 停止录制(stopRecord)
ts 复制代码
stopRecord() {
  // 状态检查:非录制状态下不执行
  if (this.recorder.getStatus() !== 'recording') {
    console.warn('Recorder is not recording');
    return;
  }
  // 销毁录制器(释放麦克风资源)
  this.recorder.destroy();
  // 通知服务端:输入音频已完成(停止服务端监听)
  this.ws?.send({
    id: Date.now().toString(), // 用时间戳作为消息 ID(简单唯一)
    event_type: WebsocketsEventType.INPUT_AUDIO_BUFFER_COMPLETE,
  });
}
2.4.2 打断对话(interrupt)

适用于"客户端手动打断服务端响应"场景(如用户中途改变需求):

ts 复制代码
interrupt() {
  // 通知服务端:取消当前对话
  this.ws?.send({
    id: uuid(),
    event_type: WebsocketsEventType.CONVERSATION_CHAT_CANCEL,
  });
  // 触发打断事件,供外部处理(如更新 UI 为"等待用户输入")
  this.emit(WsChatEventNames.INTERRUPTED, undefined);
}
2.4.3 音频设备切换(setAudioInputDevice)

支持动态切换麦克风设备(如从内置麦克风切换到耳机麦克风):

ts 复制代码
async setAudioInputDevice(deviceId: string) {
  // 1. 停止当前录制(切换设备前需释放资源)
  if (this.recorder.getStatus() !== 'ended') {
    await this.recorder.destroy();
  }
  // 2. 获取所有音频设备
  const devices = await getAudioDevices();
  // 3. 处理默认设备(deviceId 为 'default' 时,使用系统默认麦克风)
  if (deviceId === 'default') {
    this.recorder.config.deviceId = undefined;
  } else {
    // 4. 校验设备 ID 有效性
    const device = devices.audioInputs.find(d => d.deviceId === deviceId);
    if (!device) {
      throw new Error(`Device with id ${deviceId} not found`);
    }
    this.recorder.config.deviceId = device.deviceId;
  }
  // 5. 非静音状态下,重新启动录制
  if (!this.isMuted) {
    await this.startRecord();
  }
  // 6. 触发设备切换事件,供外部更新 UI(如刷新设备列表选中状态)
  this.emit(WsChatEventNames.AUDIO_INPUT_DEVICE_CHANGED, undefined);
}
2.4.4 AI 降噪控制(setDenoiserEnabled/Level/Mode)

支持动态调整 AI 降噪功能(适应不同噪声场景,如办公室、街道):

ts 复制代码
// 启用/禁用降噪
setDenoiserEnabled(enabled: boolean) {
  this.recorder.setDenoiserEnabled(enabled);
  // 触发降噪状态事件
  this.emit(enabled ? WsChatEventNames.DENOISER_ENABLED : WsChatEventNames.DENOISER_DISABLED, undefined);
}

// 设置降噪等级(如 low/medium/high)
setDenoiserLevel(level: AIDenoiserProcessorLevel) {
  this.log('setDenoiserLevel', level);
  this.recorder.setDenoiserLevel(level);
}

// 设置降噪模式(如 speech_only:只保留人声;all:去除所有噪声)
setDenoiserMode(mode: AIDenoiserProcessorMode) {
  this.log('setDenoiserMode', mode);
  this.recorder.setDenoiserMode(mode);
}

2.5 断开连接与资源释放(disconnect)

ts 复制代码
async disconnect() {
  // 1. 通知服务端:取消当前对话
  this.ws?.send({
    id: uuid(),
    event_type: WebsocketsEventType.CONVERSATION_CHAT_CANCEL,
  });
  // 2. 释放核心资源:录制器、播放器、Opus 解码器
  await this.recorder?.destroy();
  await this.wavStreamPlayer?.destroy();
  await this.opusDecoder?.destroy();
  // 3. 触发断开连接事件
  this.emit(WsChatEventNames.DISCONNECTED, undefined);
  // 4. 延迟释放(确保服务端收到消息)
  await new Promise(resolve => setTimeout(resolve, 500));
  // 5. 清理事件监听、关闭 WebSocket
  this.listeners.clear();
  this.closeWs();
  this.isConnected = false;
}
  • 关键注意点:资源释放需按顺序执行(先通知服务端,再释放本地资源),避免服务端因未收到"取消"消息而持续处理;延迟 500ms 是为了确保 WebSocket 消息已发送完成。

三、实战场景与集成示例

3.1 基础集成:初始化客户端并启动交互

ts 复制代码
// 1. 初始化 WsChatClient 配置
const clientConfig: WsChatClientOptions = {
  wsUrl: 'wss://your-websocket-server.com/chat', // WebSocket 服务端地址
  debug: true, // 开启调试日志
  audioMutedDefault: false, // 默认不静音
  aiDenoisingConfig: {
    enabled: true, // 启用 AI 降噪
    level: 'medium', // 降噪等级:中等
    mode: 'speech_only', // 只保留人声
  },
  playbackVolumeDefault: 0.8, // 默认播放音量(0.8倍)
};

// 2. 创建 WsChatClient 实例
const chatClient = new WsChatClient(clientConfig);

// 3. 监听核心事件(更新 UI 或业务逻辑)
chatClient.on(WsChatEventNames.CONNECTED, (event) => {
  console.log('WebSocket 连接成功', event);
  document.getElementById('status').textContent = '已连接,可开始对话';
});

chatClient.on(WsChatEventNames.AUDIO_INPUT_DUMP, (event) => {
  // 下载录制的 WAV 文件(调试用)
  const a = document.createElement('a');
  a.href = URL.createObjectURL(event.data.wav);
  a.download = event.data.name;
  a.click();
  URL.revokeObjectURL(a.href);
});

chatClient.on(WsChatEventNames.INTERRUPTED, () => {
  console.log('对话已打断');
  document.getElementById('status').textContent = '等待用户输入';
});

chatClient.on(WsChatEventNames.DISCONNECTED, () => {
  console.log('WebSocket 已断开');
  document.getElementById('status').textContent = '已断开连接';
});

// 4. 连接服务端并启动交互
document.getElementById('connect-btn').addEventListener('click', async () => {
  try {
    await chatClient.connect({
      chatUpdate: {
        data: {
          turn_detection: { type: 'server_vad' }, // 使用服务端 VAD 判停
          output_audio: { codec: 'opus' }, // 服务端输出 Opus 格式(节省带宽)
        },
      },
    });
  } catch (err) {
    console.error('连接失败', err);
    document.getElementById('status').textContent = '连接失败,请重试';
  }
});

// 5. 断开连接
document.getElementById('disconnect-btn').addEventListener('click', async () => {
  await chatClient.disconnect();
});

3.2 场景二:客户端手动打断模式(按键说话)

适用于需要用户主动控制说话时机的场景(如对讲机式交互),需将 turn_detection 设为 client_interrupt,并通过按钮控制录制启停:

ts 复制代码
// 1. 连接时配置客户端打断模式
document.getElementById('connect-btn').addEventListener('click', async () => {
  await chatClient.connect({
    chatUpdate: {
      data: {
        turn_detection: { type: 'client_interrupt' }, // 客户端手动打断
        input_audio: { codec: 'pcm', sample_rate: 48000 },
      },
    },
  });
});

// 2. 按住按钮说话(启动录制)
document.getElementById('hold-talk-btn').addEventListener('mousedown', async () => {
  try {
    await chatClient.startRecord();
    document.getElementById('talk-status').textContent = '正在录音...';
  } catch (err) {
    console.error('启动录制失败', err);
  }
});

// 3. 松开按钮停止说话(停止录制)
document.getElementById('hold-talk-btn').addEventListener('mouseup', async () => {
  chatClient.stopRecord();
  document.getElementById('talk-status').textContent = '录音已停止';
});

// 4. 点击按钮打断服务端响应
document.getElementById('interrupt-btn').addEventListener('click', () => {
  chatClient.interrupt();
});

3.3 场景三:动态切换音频设备与降噪配置

实现麦克风设备下拉选择、降噪开关与等级调节的 UI 交互逻辑:

ts 复制代码
// 1. 加载音频设备列表到下拉框
async function loadAudioDevices() {
  const devices = await getAudioDevices();
  const deviceSelect = document.getElementById('mic-select');
  if (!deviceSelect) return;
  
  // 清空现有选项
  deviceSelect.innerHTML = '';
  
  // 添加默认设备选项
  const defaultOption = document.createElement('option');
  defaultOption.value = 'default';
  defaultOption.textContent = '默认麦克风';
  deviceSelect.appendChild(defaultOption);
  
  // 添加所有可用麦克风设备
  devices.audioInputs.forEach(dev => {
    const option = document.createElement('option');
    option.value = dev.deviceId;
    option.textContent = dev.label || `麦克风 ${dev.deviceId.slice(0, 8)}`; // 处理无标签设备
    deviceSelect.appendChild(option);
  });
}

// 页面加载时初始化设备列表
window.addEventListener('load', async () => {
  await loadAudioDevices();
  // 初始化降噪控件状态
  const denoiserSwitch = document.getElementById('denoiser-switch') as HTMLInputElement;
  const denoiserLevel = document.getElementById('denoiser-level') as HTMLSelectElement;
  
  // 若初始启用降噪,设置开关为开启状态
  denoiserSwitch.checked = chatClient.recorder.config.aiDenoisingConfig?.enabled ?? false;
  // 同步降噪等级下拉框
  denoiserLevel.value = chatClient.recorder.config.aiDenoisingConfig?.level ?? 'medium';
});

// 2. 切换麦克风设备
document.getElementById('mic-select').addEventListener('change', async (e) => {
  const target = e.target as HTMLSelectElement;
  const deviceId = target.value;
  
  try {
    // 显示加载状态
    target.disabled = true;
    document.getElementById('device-status').textContent = '切换中...';
    
    await chatClient.setAudioInputDevice(deviceId);
    
    // 切换成功更新状态
    document.getElementById('device-status').textContent = `当前设备:${target.options[target.selectedIndex].text}`;
  } catch (err) {
    console.error('设备切换失败', err);
    document.getElementById('device-status').textContent = `切换失败:${(err as Error).message}`;
    // 恢复原选择
    target.value = chatClient.recorder.config.deviceId || 'default';
  } finally {
    target.disabled = false;
  }
});

// 3. 切换降噪功能开关
document.getElementById('denoiser-switch').addEventListener('change', (e) => {
  const enabled = (e.target as HTMLInputElement).checked;
  chatClient.setDenoiserEnabled(enabled);
  
  // 联动控制降噪等级下拉框的禁用状态
  const denoiserLevel = document.getElementById('denoiser-level') as HTMLSelectElement;
  denoiserLevel.disabled = !enabled;
  
  document.getElementById('denoiser-status').textContent = enabled ? '已启用' : '已禁用';
});

// 4. 调节降噪等级
document.getElementById('denoiser-level').addEventListener('change', (e) => {
  const level = (e.target as HTMLSelectElement).value as AIDenoiserProcessorLevel;
  chatClient.setDenoiserLevel(level);
  
  document.getElementById('denoiser-level-status').textContent = level;
});

对应的 HTML 结构示例(供参考):

ts 复制代码
<div class="audio-control">
  <div class="device-selector">
    <label>麦克风设备:</label>
    <select id="mic-select"></select>
    <span id="device-status">未选择</span>
  </div>
  
  <div class="denoiser-control">
    <label>AI 降噪:</label>
    <input type="checkbox" id="denoiser-switch">
    <span id="denoiser-status">已禁用</span>
    
    <label>降噪等级:</label>
    <select id="denoiser-level" disabled>
      <option value="low">低</option>
      <option value="medium" selected>中</option>
      <option value="high">高</option>
    </select>
    <span id="denoiser-level-status">medium</span>
  </div>
</div>

关键说明

  • 设备加载逻辑:通过 getAudioDevices 获取所有音频输入设备,同时处理无标签设备的显示问题;
  • 状态联动:降噪开关启用/禁用时,同步控制降噪等级下拉框的可用状态,提升用户体验;
  • 错误处理:设备切换失败时恢复原选择并显示错误信息,增强鲁棒性。
相关推荐
郝学胜-神的一滴2 小时前
享元模式(Flyweight Pattern)
开发语言·前端·c++·设计模式·软件工程·享元模式
zheshiyangyang2 小时前
Sass开发【四】
前端·css·sass
讨厌吃蛋黄酥2 小时前
🔥 面试必考题:手写数组扁平化,5种方法全解析(附代码+图解)
前端·javascript·面试
GISer_Jing2 小时前
作业帮前端面试(准备)
前端·面试·职场和发展
大数据002 小时前
Flink消费Datahub到ClickhouseSink
java·前端·flink
知识分享小能手3 小时前
React学习教程,从入门到精通,React 前后端交互技术详解(29)
前端·javascript·vue.js·学习·react.js·前端框架·react
天天进步20153 小时前
React Server Components详解:服务端渲染的新纪元
开发语言·前端·javascript
lvchaoq3 小时前
react的依赖项数组
前端·javascript·react.js
qq_10055170753 小时前
WordPress给指定分类文章添加一个自动化高亮(一键复制)功能
运维·前端·自动化·php