6 changed files with 639 additions and 154 deletions
@ -0,0 +1,209 @@ |
|||
// 心跳间隔
|
|||
const HEART_BEAT_TIME = 15000; |
|||
// 心跳最大失败次数(超过此次数重连)
|
|||
const HEART_BEAT_FAIL_NUM = 1; |
|||
// 重连间隔
|
|||
const RECONNECT_TIME = 3000; |
|||
|
|||
import { generateRequestId } from "./util"; |
|||
|
|||
export default class Audio { |
|||
constructor(option) { |
|||
this._options = option; |
|||
this.socket = null; |
|||
this.session_id = null; |
|||
this.selfCloseStatus = false |
|||
this.connectSocketTimeOut = null |
|||
} |
|||
getToken() { |
|||
return new Promise((resolve, reject) => { |
|||
console.log("-----开始请求token-----"); |
|||
uni.request({ |
|||
method: "GET", |
|||
dataType: "json", |
|||
// url: `http://192.168.124.118:8083/xcx/framework/agent/${this._options.agentId}`,
|
|||
url: `https://des.js-dyyj.com/getDemoToken?id=${this._options.agentId}`, |
|||
success: (res) => { |
|||
console.log("请求token成功", res); |
|||
resolve(res); |
|||
}, |
|||
fail: (err) => { |
|||
wx.showToast({ |
|||
title: "创建失败", |
|||
icon: "none", |
|||
}); |
|||
console.log("请求token失败", err); |
|||
reject(); |
|||
}, |
|||
}); |
|||
}); |
|||
} |
|||
async init() { |
|||
return this.createSocket(); |
|||
} |
|||
async createSocket(options) { |
|||
console.log("开始创建socket", options); |
|||
return new Promise(async (resolve, reject) => { |
|||
const origin = "wss://des.js-dyyj.com/xcx/tts-websocket"; |
|||
// 建立连接
|
|||
const socket = wx.connectSocket({ |
|||
url: `${origin}`, |
|||
success: (e) => { |
|||
console.log("创建语音长链接成功", e); |
|||
}, |
|||
complete: (e) => { |
|||
console.log("socket - complete", e); |
|||
}, |
|||
}); |
|||
this.socket = socket; |
|||
|
|||
socket.onOpen((e) => { |
|||
console.log("socket.onOpen", e); |
|||
// 监听发送
|
|||
options && options.complete && options.complete(); |
|||
}); |
|||
|
|||
socket.onMessage((e) => { |
|||
const { data } = e; |
|||
|
|||
if (data instanceof ArrayBuffer) { |
|||
this._options.onMessage && this._options.onMessage(data); |
|||
} else { |
|||
if ( Object.prototype.toString.call(data) === "[object String]" ) { |
|||
const tmp = JSON.parse(data); |
|||
if (Object.prototype.toString.call(tmp.type) === "[object Number]") { |
|||
this._options.onMessage && this._options.onMessage(tmp); |
|||
} else { |
|||
if (!this.session_id) { |
|||
this.session_id = tmp.data |
|||
} |
|||
} |
|||
} |
|||
} |
|||
// this.on(type, e);
|
|||
resolve(); |
|||
// this.createInter();
|
|||
}); |
|||
// 失败
|
|||
socket.onError((e) => { |
|||
console.log("websocket error 创建语音长链接报错", e); |
|||
//
|
|||
}); |
|||
// 关闭
|
|||
socket.onClose((e) => { |
|||
console.log("websocket close 语音长链接关闭", e); |
|||
this.connectSocketTimeOut && clearTimeout(this.connectSocketTimeOut); |
|||
//非自动关闭重连
|
|||
if (e.code != 1000) { |
|||
this.doConnectTimeout(); |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
doConnectTimeout() { |
|||
if (this.selfCloseStatus){ |
|||
// 主动退出 不在重连
|
|||
return |
|||
} |
|||
// 重连一次
|
|||
console.log("websocket 异常关闭 开始重连"); |
|||
this.connectSocketTimeOut = setTimeout(() => { |
|||
this.createSocket({ |
|||
complete: function (res) { |
|||
// this.reconnectLock = false;
|
|||
}, |
|||
}); |
|||
}, RECONNECT_TIME); |
|||
} |
|||
|
|||
send(e, t) { |
|||
console.log("开始请求 websocket send", e); |
|||
this.socket && this.socket.send(e); |
|||
} |
|||
|
|||
getMsgData(e) { |
|||
if (e && typeof e == "string") { |
|||
const status = e.indexOf("42"); |
|||
const index = e.indexOf("["); |
|||
if (status > -1 && index > -1) { |
|||
const txt = e.substring(index); |
|||
const item = JSON.parse(txt); |
|||
const [type, obj] = item; |
|||
const payload = obj.payload ? obj.payload : {}; |
|||
const params = { |
|||
chatId: this._options.agentId, |
|||
type, |
|||
contentType: "text", //默认文字
|
|||
timestamp: new Date().getTime(), |
|||
...payload, |
|||
}; |
|||
// 缓存聊天记录
|
|||
return params; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
emit(type, text, contentType) { |
|||
console.log("emit", type, text); |
|||
switch (type) { |
|||
case "send": |
|||
// 发送消息
|
|||
// 发送消息
|
|||
// const socketParams = {
|
|||
// request_id: generateRequestId()
|
|||
// };
|
|||
const socketParams = { |
|||
session_id: this.session_id, |
|||
message_id: generateRequestId(), |
|||
action: "ACTION_SYNTHESIS", |
|||
data: text, |
|||
}; |
|||
this.send({ data: JSON.stringify(socketParams) }, contentType); |
|||
break; |
|||
} |
|||
} |
|||
// 监听
|
|||
on(type, params) { |
|||
switch (type) { |
|||
case "reply": |
|||
// 回复结束
|
|||
const tmpParams = { |
|||
...params, |
|||
name: this.robotObj.name, |
|||
headImage: this.robotObj.headImage, |
|||
}; |
|||
if ( |
|||
params && |
|||
params.type === "reply" && |
|||
!params.is_from_self && |
|||
params.can_rating |
|||
) { |
|||
// 回复消息
|
|||
console.log("reply回复内容", tmpParams.content, tmpParams); |
|||
this._options.onMessage && this._options.onMessage(tmpParams); |
|||
} |
|||
|
|||
if (!params.is_from_self && params.is_final) { |
|||
if (this.failMsg[tmpParams.request_id]) { |
|||
this.failMsg[tmpParams.request_id].status = true; |
|||
// 已回复 状态为true
|
|||
} |
|||
|
|||
// 回复结束 写入缓存
|
|||
// setMsgData(tmpParams);
|
|||
} |
|||
// 回复
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
// 关闭socket
|
|||
destroy() { |
|||
if (this.socket && this.socket.readyState == 1) { |
|||
this.socket && this.socket.close(); |
|||
this.socket = null; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,127 @@ |
|||
// utils/pcm-player.js
|
|||
export default class PCMPlayer { |
|||
constructor(options) { |
|||
this.queue = []; |
|||
this._options = options |
|||
this.AllTexts = []; |
|||
this.audioCtx = null; |
|||
this.startTime = 0; |
|||
this._t = null |
|||
} |
|||
feed(data) { |
|||
if (!this.audioCtx) { |
|||
this.audioCtx = wx.createWebAudioContext(); |
|||
} |
|||
this.playPCM(data); |
|||
} |
|||
|
|||
playPCM(pcmBuffer, sampleRate = 16000) { |
|||
const length = pcmBuffer.byteLength / 2; // 16-bit = 2 bytes per sample
|
|||
const audioBuffer = this.audioCtx.createBuffer(1, length, sampleRate); |
|||
const channelData = audioBuffer.getChannelData(0); // Float32Array
|
|||
const view = new DataView(pcmBuffer); |
|||
|
|||
// 将 16-bit PCM 转换为 Float32 [-1, 1]
|
|||
for (let i = 0; i < length; i++) { |
|||
const sample = view.getInt16(i * 2, true); // little-endian
|
|||
channelData[i] = sample / 0x8000; // convert to [-1, 1]
|
|||
} |
|||
|
|||
this.queue.push(audioBuffer); |
|||
if (!this.isPlaying) { |
|||
// 开始播放
|
|||
this.startTime = this.audioCtx.currentTime ?? 0; |
|||
this.playNext(); |
|||
this.isPlaying = true; |
|||
setTimeout(() => { |
|||
this.syncLoop(); |
|||
}); |
|||
|
|||
} |
|||
} |
|||
// 监听音频播放进度
|
|||
syncLoop() { |
|||
const loop = () => { |
|||
const now = this.audioCtx.currentTime - this.startTime; |
|||
// 找当前字幕
|
|||
const index = this.AllTexts.findIndex( |
|||
(s) => now >= s.beginTime && now < s.endTime |
|||
); |
|||
if (index > -1) { |
|||
const obj = this.AllTexts[index]; |
|||
if (this.AllTexts.length == 1) { |
|||
// 最后一条数据
|
|||
obj.is_final = true |
|||
} |
|||
const tmp = this.AllTexts.shift(); |
|||
this._options.onPlay && this._options.onPlay(tmp); |
|||
if (!this.AllTexts.length) { |
|||
clearTimeout(this._t); |
|||
return |
|||
} |
|||
} |
|||
if (!this.AllTexts.length) { |
|||
this._t = setTimeout(loop, 16); |
|||
} else { |
|||
if (now < this.AllTexts[this.AllTexts.length - 1].endTime) { |
|||
this._t = setTimeout(loop, 16); |
|||
} else { |
|||
// 结束
|
|||
clearTimeout(this._t); |
|||
console.log('语音播放结束') |
|||
this.destroy() |
|||
} |
|||
} |
|||
}; |
|||
loop(); |
|||
} |
|||
|
|||
playNext() { |
|||
if (this.queue.length === 0) { |
|||
// 结束
|
|||
this.isPlaying = true; |
|||
return; |
|||
} |
|||
this.isPlaying = true; |
|||
const source = this.audioCtx.createBufferSource(); |
|||
source.buffer = this.queue.shift(); |
|||
source.connect(this.audioCtx.destination); |
|||
source.start(); |
|||
source.onended = () => { |
|||
setTimeout(() => { |
|||
this.playNext(); |
|||
}); |
|||
}; |
|||
} |
|||
|
|||
// 增加文字
|
|||
addText(res) { |
|||
if (res.result && res.result.subtitles && res.result.subtitles.length) { |
|||
const tmpText = res.result.subtitles.map((it) => { |
|||
return { |
|||
...it, |
|||
content: it.text, |
|||
beginTime: it.beginTime / 1000, |
|||
endTime: it.endTime / 1000, |
|||
request_id: res.requestId, |
|||
sessionId: res.sessionId |
|||
}; |
|||
}); |
|||
this.AllTexts = [...this.AllTexts, ...tmpText]; |
|||
} |
|||
} |
|||
|
|||
close() { |
|||
this.isPlaying = false; |
|||
this.audioCtx.close(); |
|||
} |
|||
destroy() { |
|||
this.queue = []; |
|||
this.isPlaying = false; |
|||
this.audioCtx && this.audioCtx.close(); |
|||
this.audioCtx = null; |
|||
this.AllTexts = []; |
|||
this.startTime = 0; |
|||
this._options.onAudioEnd && this._options.onAudioEnd() |
|||
} |
|||
} |
Loading…
Reference in new issue