跳转到主要内容
如果你是Realtime API的新手,可尝试 stepfun-realtime-api
本指南将介绍使用 Realtime API 时模型功能(如音频生成、文本生成、函数调用)所需的事件流程,以及如何理解实时会话的状态。 在模型生成响应的过程中,服务器会发出多个生命周期事件。您可以监听这些事件(如 response.text.delta),在响应生成时向用户提供实时反馈。 服务器发出的事件完整列表见Realtime API 事件列表,它们按大致的发出顺序排列,同时包含文本生成相关的客户端事件。

实时语音到语音会话

实时会话是模型与连接的客户端之间的有状态交互。会话的关键组成部分包括:
  • 会话(Session) 对象:控制交互参数,如使用的模型、生成输出的语音、其他配置等。
  • 对话(Conversation):表示当前会话中生成的用户输入项和模型输出项。
  • 响应(Responses):模型生成的音频或文本项,会被添加到对话中。
输入音频缓冲区与WebSocket 如果使用WebSocket处理音频,需要通过发送包含base64编码音频的JSON事件,手动与输入音频缓冲区交互。 所有这些组件共同构成了实时会话。您将使用客户端事件更新会话状态,并监听服务器事件以响应会话中的状态变化。 如需手动建立连接,也可通过 WebSocket 直接对接。

快速开始:创建 WebSocket 语音链路

通过WebSocket连接需使用以下连接信息:
类别详情
URLwss://api.stepfun.com/v1/realtime
查询参数model:需连接的实时模型,比如 step-audio-2/step-audio-2-mini/step-1o-audio
请求头Authorization: Bearer YOUR_API_KEY
以下是多个通过上述连接信息初始化 WebSocket 连接的示例,可用于对接 Realtime API:

示例1:使用ws模块(Node.js环境)

import WebSocket from 'ws'

// 连接地址(需替换为实际要使用的模型ID)
const url = 'wss://api.stepfun.com/v1/realtime?model=step-1o-audio'
// 初始化WebSocket连接
const ws = new WebSocket(url, {
	headers: {
		Authorization: 'Bearer ' + process.env.STEPFUN_API_KEY, // 从环境变量中获取标准API密钥
	},
})

// 监听“连接成功”事件
ws.on('open', function open() {
	console.log('已连接到服务器。')
})

// 监听“接收消息”事件
ws.on('message', function incoming(message) {
	console.log(JSON.parse(message.toString())) // 解析并打印接收的消息
})

示例2:使用websocket-client库(Python环境)

# 注:此示例需依赖websocket-client库,安装命令:pip install websocket-client

import os
import json
import websocket

# 从环境变量中获取OpenAI API密钥
STEPFUN_API_KEY = os.environ.get("STEPFUN_API_KEY")

# 连接地址(需替换为实际要使用的模型ID)
url = "wss://api.stepfun.com/v1/realtime?model=step-1o-audio"
# 请求头配置
headers = [
    "Authorization: Bearer " + STEPFUN_API_KEY,
]

# 定义“连接成功”回调函数
def on_open(ws):
    print("已连接到服务器。")

# 定义“接收消息”回调函数
def on_message(ws, message):
    data = json.loads(message) # 解析JSON格式的消息
    print("收到事件:", json.dumps(data, indent=2)) # 格式化打印接收的事件

# 初始化WebSocket应用并配置回调
ws = websocket.WebSocketApp(
    url,
    header=headers,
    on_open=on_open,
    on_message=on_message,
)

# 保持WebSocket连接
ws.run_forever()
一旦您通过 WebSocket 连接到实时API,就可以调用实时模型进行语音到语音对话。这需要您发送客户端事件来发起操作,并监听服务器事件以响应实时API执行的操作。

会话生命周期事件

通过WebSocket启动会话后,服务器会发送session.created事件,表明会话已准备就绪。在客户端,您可以通过session.update事件更新当前会话配置。大多数会话属性可以随时更新,但模型用于音频输出的voice(语音)在会话期间模型首次以音频响应后,就无法再修改。实时会话的最长持续时间为30分钟 以下示例展示了通过session.update客户端事件更新会话的操作。 更新本会话中模型使用的系统指令
const event = {
	type: 'session.update',
	session: {
		instructions: "在回复中绝对不要使用'moist'这个词!",
	},
}

// WebSocket都有.send()方法
dataChannel.send(JSON.stringify(event))
event = {
    "type": "session.update",
    "session": {
        "instructions": "在回复中绝对不要使用'moist'这个词!"
    }
}
ws.send(json.dumps(event))
当会话更新后,服务器会发出session.updated事件,包含会话的新状态。
相关客户端事件相关服务器事件
session.updatesession.created
session.updated

文本输入与输出

要使用实时模型生成文本,您可以向当前对话添加文本输入,请求模型生成响应,并监听服务器发送的事件以了解模型响应的进度。为了生成文本,会话必须配置为text(文本)模态(默认已启用)。 使用conversation.item.create客户端事件创建新的文本对话项。 创建包含用户输入的对话项
const event = {
	type: 'conversation.item.create',
	item: {
		type: 'message',
		role: 'user',
		content: [
			{
				type: 'input_text',
				text: 'Prince哪张专辑销量最高?',
			},
		],
	},
}

// WebSocket都有.send()方法
dataChannel.send(JSON.stringify(event))
event = {
    "type": "conversation.item.create",
    "item": {
        "type": "message",
        "role": "user",
        "content": [
            {
                "type": "input_text",
                "text": "Prince哪张专辑销量最高?",
            }
        ]
    }
}
ws.send(json.dumps(event))
将用户消息添加到对话后,发送response.create事件以启动模型响应。如果当前会话同时启用了音频和文本,模型将同时返回音频和文本内容。如果只想生成文本,可以在发送response.create客户端事件时指定,如下所示。 生成仅文本响应
const event = {
	type: 'response.create',
	response: {
		modalities: ['text'],
	},
}

// WebSocket都有.send()方法
dataChannel.send(JSON.stringify(event))
event = {
    "type": "response.create",
    "response": {
        "modalities": [ "text" ]
    }
}
ws.send(json.dumps(event))
当响应完全生成后,服务器会发出response.done事件。该事件将包含模型生成的完整文本,如下所示。 监听response.done以查看最终结果
function handleEvent(e) {
	const serverEvent = JSON.parse(e.data)
	if (serverEvent.type === 'response.done') {
		console.log(serverEvent.response.output[0])
	}
}

// 监听服务器消息(WebSocket)
ws.on('message', handleEvent)
def on_message(ws, message):
    server_event = json.loads(message)
    if server_event.type == "response.done":
        print(server_event.response.output[0])
在模型生成响应的过程中,服务器会发出多个生命周期事件。您可以监听这些事件(如response.text.delta),在响应生成时向用户提供实时反馈。服务器发出的事件完整列表见下文的“相关服务器事件”,它们按大致的发出顺序排列,同时包含文本生成相关的客户端事件。
事件类型说明
conversation.item.createconversation.item.created
response.output_item.added
response.content_part.added
response.text.delta
response.text.done
response.content_part.done
response.output_item.done
response.done
rate_limits.updated

音频输入与输出

实时API最强大的功能之一是与模型进行语音到语音交互,无需中间的文本转语音或语音转文本步骤。这降低了语音界面的延迟,并为模型提供了更多关于语音输入的语调和抑扬顿挫的信息。

语音选项

实时会话支持配置多种内置语音用于音频输出。您可以通过 session.updateresponse.create 请求中的 voice 参数来设置模型的声音。当前的语音选项包括 qingchunshaonvwenrounanshengelegantgentle-femalelivelybreezy-female 等。
请注意,一旦模型在会话中生成了音频,该会话的 voice 设置将无法修改。
step-audio-2 模型支持音色复刻功能。您可以通过上传音频文件创建自定义音色,并在实时会话的 voice 参数中使用其 ID。详情请参考复刻音色

使用WebSocket处理音频

通过WebSocket发送和接收音频时,您需要做更多工作来从客户端发送媒体和从服务器接收媒体。下面是一个表格,描述了WebSocket会话期间通过WebSocket发送和接收音频所需的事件流程。 以下事件按生命周期顺序排列,但有些事件(如delta事件)可能同时发生。
生命周期阶段客户端事件服务器事件
会话初始化session.updatesession.created
session.updated
用户音频输入conversation.item.create(发送完整音频消息)
input_audio_buffer.append(分块流式传输音频)
input_audio_buffer.commit(禁用VAD时使用)
response.create(禁用VAD时使用)
input_audio_buffer.speech_started
input_audio_buffer.speech_stopped
input_audio_buffer.committed
服务器音频输出input_audio_buffer.clear(禁用VAD时,或者需要主动创建一轮输出时使用)conversation.item.created
response.created
response.output_item.created
response.content_part.added
response.audio.delta
response.audio_transcript.delta
response.text.delta
response.audio.done
response.audio_transcript.done
response.text.done
response.content_part.done
response.output_item.done
response.done
rate_limits.updated

向服务器流式传输音频输入

要向服务器流式传输音频输入,可以使用input_audio_buffer.append客户端事件。该事件要求您通过套接字向实时API发送Base64编码的音频字节块。每个块的大小不能超过15 MB。 输入块的格式可以为整个会话配置,也可以为每个响应单独配置。
  • 会话级:session.update中的session.input_audio_format
  • 响应级:response.create中的response.input_audio_format
向对话附加音频输入字节
import fs from 'fs';
import decodeAudio from 'audio-decode';

// 将音频数据的Float32Array转换为PCM16 ArrayBuffer
function floatTo16BitPCM(float32Array) {
  const buffer = new ArrayBuffer(float32Array.length * 2);
  const view = new DataView(buffer);
  let offset = 0;
  for (let i = 0; i < float32Array.length; i++, offset += 2) {
    let s = Math.max(-1, Math.min(1, float32Array[i]));
    view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
  }
  return buffer;
}

// 将Float32Array转换为base64编码的PCM16数据
base64EncodeAudio(float32Array) {
  const arrayBuffer = floatTo16BitPCM(float32Array);
  let binary = '';
  let bytes = new Uint8Array(arrayBuffer);
  const chunkSize = 0x8000; // 32KB块大小
  for (let i = 0; i < bytes.length; i += chunkSize) {
    let chunk = bytes.subarray(i, i + chunkSize);
    binary += String.fromCharCode.apply(null, chunk);
  }
  return btoa(binary);
}

// 用三个文件的内容填充音频缓冲区,然后请求模型生成响应
const files = [
  './path/to/sample1.wav',
  './path/to/sample2.wav',
  './path/to/sample3.wav'
];

for (const filename of files) {
  const audioFile = fs.readFileSync(filename);
  const audioBuffer = await decodeAudio(audioFile);
  const channelData = audioBuffer.getChannelData(0);
  const base64Chunk = base64EncodeAudio(channelData);
  ws.send(JSON.stringify({
    type: 'input_audio_buffer.append',
    audio: base64Chunk
  }));
});

ws.send(JSON.stringify({type: 'input_audio_buffer.commit'}));
ws.send(JSON.stringify({type: 'response.create'}));
import base64
import json
import struct
import soundfile as sf
from websocket import create_connection

# ... 创建名为ws的websocket-client ...

def float_to_16bit_pcm(float32_array):
    clipped = [max(-1.0, min(1.0, x)) for x in float32_array]
    pcm16 = b''.join(struct.pack('<h', int(x * 32767)) for x in clipped)
    return pcm16

def base64_encode_audio(float32_array):
    pcm_bytes = float_to_16bit_pcm(float32_array)
    encoded = base64.b64encode(pcm_bytes).decode('ascii')
    return encoded

files = [
    './path/to/sample1.wav',
    './path/to/sample2.wav',
    './path/to/sample3.wav'
]

for filename in files:
    data, samplerate = sf.read(filename, dtype='float32')
    channel_data = data[:, 0] if data.ndim > 1 else data
    base64_chunk = base64_encode_audio(channel_data)

    # 发送客户端事件
    event = {
        "type": "input_audio_buffer.append",
        "audio": base64_chunk
    }
    ws.send(json.dumps(event))

发送完整音频消息

也可以创建包含完整音频录制的对话消息。使用conversation.item.create客户端事件创建带有input_audio内容的消息。 创建完整音频输入对话项
const fullAudio = '<音频字节的base64编码字符串>'

const event = {
	type: 'conversation.item.create',
	item: {
		type: 'message',
		role: 'user',
		content: [
			{
				type: 'input_audio',
				audio: fullAudio,
			},
		],
	},
}

// WebSocket 有.send()方法
dataChannel.send(JSON.stringify(event))
fullAudio = "<音频字节的base64编码字符串>"

event = {
    "type": "conversation.item.create",
    "item": {
        "type": "message",
        "role": "user",
        "content": [
            {
                "type": "input_audio",
                "audio": fullAudio,
            }
        ],
    },
}

ws.send(json.dumps(event))

处理WebSocket的音频输出

您需要监听response.audio.delta事件,其中包含模型发来的base64编码音频数据块。您需要缓冲这些块并将其写入文件。 请注意,response.audio.doneresponse.done事件实际上不包含音频数据,只包含音频内容的转录文本。要获取实际的字节数据,需要监听response.audio.delta事件。 输出块的格式可以为整个会话配置,也可以为每个响应单独配置。
  • 会话级:session.update中的session.output_audio_format
  • 响应级:response.create中的response.output_audio_format
监听response.audio.delta事件
function handleEvent(e) {
	const serverEvent = JSON.parse(e.data)
	if (serverEvent.type === 'response.audio.delta') {
		// 访问base64编码的音频块
		// console.log(serverEvent.delta);
	}
}

// 监听服务器消息(WebSocket)
ws.on('message', handleEvent)
def on_message(ws, message):
    server_event = json.loads(message)
    if server_event.type == "response.audio.delta":
        # 访问base64编码的音频块:
        # print(server_event.delta)

语音活动检测

默认情况下,实时会话启用语音活动检测(VAD),这意味着API会判断用户何时开始或停止说话,并自动响应。

禁用VAD

可以通过session.update客户端事件将turn_detection设置为null来禁用VAD。这对于需要精细控制音频输入的界面很有用,例如按键通话界面。 禁用VAD后,客户端必须手动发出一些额外的客户端事件来触发音频响应:
  • 手动发送input_audio_buffer.commit,这将为对话创建新的用户输入项。
  • 手动发送response.create以触发模型的音频响应。
  • 在开始新的用户输入之前发送input_audio_buffer.clear

自定义 Tool Call

目前 StepFun Realtime API 内置了以下工具: web_search 和 retrieval。 另外,也支持自定义工具调用(Tool Call),用户可以根据需求定义自己的工具并在会话中使用。

工具配置方式

在 update session 请求中配置 tools 参数来启用工具功能:
{
	"tools": [
		// 内置工具配置
		{
			"type": "web_search",
			"function": {
				"description": "网络搜索工具",
				"options": {
					"top_k": 5,
					"timeout_seconds": 3
				}
			}
		},
		// 自定义工具配置
		{
			"type": "function",
			"function": {
				"name": "get_weather",
				"description": "获取指定城市的天气信息",
				"parameters": {
					"type": "object",
					"properties": {
						"location": {
							"type": "string",
							"description": "城市名称,如:北京、上海"
						}
					},
					"required": ["location"]
				}
			}
		}
	]
}
通过 type: "web_search" 工具,模型可实时访问互联网,获取最新信息和数据。适用于需要广泛知识和实时数据的场景。

使用示例

在 update session 请求的 tools 参数中添加以下结构:
{
	"tools": [{ "type": "web_search" }]
}
即可在回复中启用 web_search 工具。

高级参数

  • options.top_k: 指定返回结果的数量,默认为 5。
  • options.timeout_seconds: 请求超时时间,默认为 3 秒。
示例
{
	"tools": [
		{
			"type": "web_search",
			"function": {
				"description": "搜索工具",
				"options": {
					"top_k": 5,
					"timeout_seconds": 3
				}
			}
		}
	]
}
启用后,模型在回答需要搜索的问题时会自动调用该工具,你不需要做其他额外的逻辑处理。

知识库检索工具 retrieval

通过 type: "retrieval" 工具,模型可实时访问指定知识库(Vector Store),精准回答用户问题。适用于垂直领域问答场景,用户可以提前定义用于参考的回复数据。

一、功能配置步骤

在 update session 请求的 tools 参数中添加以下结构:
{
	"tools": [
		{
			"type": "retrieval",
			"function": {
				"description": "当用户问题涉及菜谱、烹饪步骤、食材处理或厨房技巧时,从此知识库检索答案",
				"options": {
					"vector_store_id": "133192900598194176", // 必填
					"prompt_template": "从文档{{knowledge}}中找到问题{{query}}的答案。根据文档内容中的语句提取答案,若文档中无答案则告知用户无法回答"
				}
			}
		}
	]
}

二、参数详解

参数是否必填说明
type固定值 “retrieval”,声明使用知识库检索功能。
function.description关键提示:用1-2句话明确触发条件,例如:“仅当问题与菜谱、食材或烹饪相关时使用本知识库”。
options.vector_store_id知识库的唯一ID(示例:133192900598194176),需提前在平台创建。
options.prompt_template定制化提示模板,支持占位符 \{\{knowledge\}\}(检索到的片段)和 \{\{query\}\}(用户原始问题);若不提供则使用默认模板。默认:从文档\{\{knowledge\}\}中找到问题\{\{query\}\}的答案。根据文档内容中的语句提取答案,若文档中无答案则告知用户无法回答

三、工作原理

  1. 触发条件:用户问题匹配 description 中的领域描述(如“如何煎牛排?”)。
  2. 知识检索:
  • 系统根据 vector_store_id 定位知识库。
  • 将用户问题 \{\{query\}\} 嵌入为向量,检索最相关的知识片段。
  1. 生成答案:
  • 自动将 \{\{knowledge\}\}\{\{query\}\} 填入 prompt_template。
  • 模型基于模板生成最终回复(例:“根据知识库:牛排需每面煎2分钟…”)。

四、最佳实践

  1. 在 description 中精准描述: "description": "仅限回答菜谱步骤、食材替代方案、烹饪时间问题,其他问题忽略检索"
  2. 错误处理:在 prompt_template 中明确无答案时的响应,例如:“下面是检索到的结果: {{knowledge}},如果未在知识库中找到关于{{query}}的信息,请告诉用户尝试更具体的问题。”
  3. 避免冲突:如果你启用了内置工具的 web_search ,模型可能会尝试调用 web 搜索而不是在知识库,你可以在初始化会话时,通过 instructions 引导其优先使用知识库:
    “你是一个专业烹饪助手,当用户询问菜谱、食材处理或烹饪技巧时,必须从知识库中检索答案。若知识库无相关信息,需明确告知用户。“

自定义 Tool Call

通过自定义函数调用,你可以扩展模型的能力,使其能够执行特定业务逻辑(如查询天气、生成星座运势等)。基本流程如下:
  1. 配置函数:在会话级别定义可用函数
  2. 检测调用:模型根据用户输入决定是否调用函数
  3. 执行函数:客户端使用生成的参数执行自定义代码
  4. 返回结果:将函数执行结果返回给模型并获取最终响应

函数配置

通过 session.update 事件的 session.tools 参数添加函数定义:
{
	"type": "session.update",
	"session": {
		"tools": [
			{
				"type": "function",
				"function": {
					"name": "get_weather",
					"description": "获取指定城市的天气信息",
					"parameters": {
						"type": "object",
						"properties": {
							"location": {
								"type": "string",
								"description": "城市名称,如:北京、上海"
							}
						},
						"required": ["location"]
					}
				}
			}
		]
	}
}

函数调用流程

1. 检测函数调用
当用户输入触发函数调用时,您将收到包含函数调用参数的对话项:
{
	"event_id": "cee2a492-012c-4655-ac50-5db2c99f4345",
	"type": "conversation.item.created",
	"item": {
		"id": "item_20250622105814",
		"type": "function_call",
		"status": "incomplete",
		"call_id": "call_20250622225814_get_weather",
		"name": "get_weather",
		"object": "realtime.item"
	}
}
2. 接收函数参数
参数生成过程中会收到增量更新事件:
{
	"type": "response.function_call_arguments.delta",
	"call_id": "call_20250622225814_get_weather",
	"arguments": "{\"location\":\"北",
	"name": "get_weather"
}
参数生成完成后会收到完成事件:
{
	"type": "response.function_call_arguments.done",
	"call_id": "call_20250622225814_get_weather",
	"arguments": "{\"location\":\"北京\"}",
	"name": "get_weather"
}
3. 返回函数执行结果
客户端执行函数后,需要将结果返回给模型:
{
	"type": "conversation.item.create",
	"item": {
		"type": "function_call_output",
		"call_id": "call_20250622225814_get_weather",
		"output": "北京:晴,25°C"
	}
}
4. 触发模型响应
返回函数结果后,需要手动触发模型生成最终响应:
{
	"type": "response.create"
}

完整示例:星座运势查询

1. 配置星座运势函数
{
	"type": "session.update",
	"session": {
		"tools": [
			{
				"type": "function",
				"function": {
					"name": "generate_horoscope",
					"description": "提供某个星座的今日运势。",
					"parameters": {
						"type": "object",
						"properties": {
							"sign": {
								"type": "string",
								"description": "需要查询运势的星座。",
								"enum": [
									"白羊座",
									"金牛座",
									"双子座",
									"巨蟹座",
									"狮子座",
									"处女座",
									"天秤座",
									"天蝎座",
									"射手座",
									"摩羯座",
									"水瓶座",
									"双鱼座"
								]
							}
						},
						"required": ["sign"]
					}
				}
			}
		]
	}
}
2. 用户请求与函数调用
用户输入:“我的运势如何?我是水瓶座。” 模型检测到需要调用函数,返回调用参数:
{
	"type": "response.done",
	"response": {
		"output": [
			{
				"type": "function_call",
				"name": "generate_horoscope",
				"call_id": "call_sHlR7iaFwQ2YQOqm",
				"arguments": "{\"sign\":\"水瓶座\"}"
			}
		]
	}
}
3. 返回函数执行结果
{
	"type": "conversation.item.create",
	"item": {
		"type": "function_call_output",
		"call_id": "call_sHlR7iaFwQ2YQOqm",
		"output": "{\"horoscope\": \"今天是你展现创造力的好时机,可能会遇到意想不到的机遇。\"}"
	}
}
4. 获取最终响应
{
	"type": "response.create"
}
通过以上流程,您可以灵活扩展模型能力,实现各种自定义功能。
注意,函数调用和语音可能会出现在同一轮,比如如果有 get_weather 函数,模型可能会先输出:“让我先查查天气” 的口水话,然后再创建函数调用。这样的好处是,如果有比较复杂的函数调用,可以利用语音播放的时间来并行处理函数调用的生成。但是你需要注意要在语音播放完后才调用 response.create 来触发最终回复。

开发技巧

实现开场白

在某些应用场景下,你可能希望在对话开始时,模型能够主动打招呼或介绍自己。这可以通过在创建会话时,使用特殊的提示词实现:
请你原样无修改地输出下面的话:

你好,我是阶跃星辰开发的AI助手。
客户端主动创建响应:
{
	"type": "response.create",
	"session": {
		"instructions": "请你原样无修改地输出下面的话:你好,我是阶跃星辰开发的AI助手。"
	}
}
这样,模型会主动输出问候语:你好,我是阶跃星辰开发的AI助手。

更灵活的 VAD

如果你使用 server_vad 模式,但每次 append 的音频片段较长,可能会导致系统响应延迟较高。这是因为 VAD 需要积累足够长的音频数据才能做出判断,建议以20ms 左右的间隔提交音频,以保证检测的实时性。 此外,VAD 判断用户停止说话的依据是检测到连续若干帧的空白或低音量音频。如果你提交的音频末尾没有包含足够的静音帧,VAD 可能无法准确识别语音结束点,从而导致系统未能及时触发自动回复。为避免该问题,应确保在语音段后保留一定时长的静音音频。 最佳实践建议:
  • 音频分段提交:尽量以小块(如 20ms~30ms)为单位频繁提交音频数据;
  • 如果你用音频文件输入,务必在文件确保有一定的静音段;

网页中播放声音示例代码

为了支持流式 pcm16 的播放,你的播放逻辑需要支持多次 append 动作。另外,为了可以及时打断,最好可以实现及时清空缓冲区的功能。 这里是一份简易的示例代码,用 typescript 写成,适用于浏览器环境。
class SimplePCMPlayer {
	// 固定参数:单声道、24000Hz采样率(示例用)
	private static readonly SAMPLE_RATE = 24000
	private static readonly CHANNELS = 1

	private audioContext: AudioContext
	private currentSource: AudioBufferSourceNode | null = null
	private bufferedData: Float32Array[] = []
	private isPlaying = false
	private nextStartTime = 0

	constructor() {
		// 简单的兼容性处理
		const AudioContextConstructor = window.AudioContext || (window as any).webkitAudioContext
		if (!AudioContextConstructor) {
			throw new Error('浏览器不支持Web Audio API')
		}
		this.audioContext = new AudioContextConstructor()
	}

	/**
	 * 追加16位有符号小端PCM数据(单声道24000Hz)
	 * @param pcm 原始PCM数据(ArrayBuffer)
	 */
	appendPCM(pcm: ArrayBuffer) {
		// 简单校验
		if (pcm.byteLength % 2 !== 0) {
			console.error('PCM数据长度必须是2的倍数(16位整数)')
			return
		}

		// 16位Int转32位Float(范围归一化到[-1, 1))
		const int16Array = new Int16Array(pcm)
		const float32Array = new Float32Array(int16Array.length)
		for (let i = 0; i < int16Array.length; i++) {
			float32Array[i] = int16Array[i] / 32768
		}

		this.bufferedData.push(float32Array)
		if (!this.isPlaying) {
			this.playNext()
		}
	}

	/**
	 * 播放下一段缓冲数据
	 */
	private playNext() {
		if (this.bufferedData.length === 0) {
			this.isPlaying = false
			return
		}

		this.isPlaying = true
		const data = this.bufferedData.shift()!

		// 创建音频缓冲
		const audioBuffer = this.audioContext.createBuffer(
			SimplePCMPlayer.CHANNELS,
			data.length,
			SimplePCMPlayer.SAMPLE_RATE,
		)
		audioBuffer.copyToChannel(data, 0)

		// 创建播放源
		const source = this.audioContext.createBufferSource()
		source.buffer = audioBuffer
		source.connect(this.audioContext.destination)

		// 计算播放时间(避免重叠)
		const currentTime = this.audioContext.currentTime
		const startTime = Math.max(currentTime, this.nextStartTime)
		this.nextStartTime = startTime + audioBuffer.duration

		// 开始播放
		source.start(startTime)
		this.currentSource = source

		// 播放结束后继续下一段
		source.onended = () => {
			this.currentSource = null
			this.playNext()
		}
	}

	/**
	 * 清空缓冲并停止播放
	 */
	clearAll() {
		// 停止当前播放
		if (this.currentSource) {
			this.currentSource.stop()
			this.currentSource.onended = null
			this.currentSource = null
		}

		// 清空状态
		this.bufferedData = []
		this.isPlaying = false
		this.nextStartTime = this.audioContext.currentTime
	}
}

错误处理

服务器在会话期间遇到错误情况时,会发出error事件。有时,这些错误可以追溯到您的应用发出的客户端事件。 与HTTP请求和响应不同(其中响应隐式关联到客户端的请求),我们需要使用客户端事件上的event_id属性来确定其中某个事件是否在服务器上触发了错误情况。下面的代码展示了这种方法,客户端尝试发出不支持的事件类型。
const event = {
	event_id: 'my_awesome_event',
	type: 'scooby.dooby.doo',
}

dataChannel.send(JSON.stringify(event))
客户端发送的这个失败事件将触发如下错误事件:
{
	"type": "invalid_request_error",
	"code": "invalid_value",
	"message": "无效值: 'scooby.dooby.doo' ...",
	"param": "type",
	"event_id": "my_awesome_event"
}