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