Skip to content

Commit ea0a5a2

Browse files
author
Jicheng Lu
committed
refine realtime
1 parent 8248b05 commit ea0a5a2

2 files changed

Lines changed: 47 additions & 38 deletions

File tree

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ class AudioProcessingWorklet extends AudioWorkletProcessor {
1010
// current write index
1111
bufferWriteIndex = 0;
1212
13+
speaking = false;
14+
threshold = 0.1;
15+
1316
constructor() {
1417
super();
15-
this.hasAudio = false;
1618
}
1719
1820
/**
@@ -22,6 +24,7 @@ class AudioProcessingWorklet extends AudioWorkletProcessor {
2224
process(inputs) {
2325
if (inputs[0].length) {
2426
const channel0 = inputs[0][0];
27+
this.speaking = channel0.some(sample => Math.abs(sample) >= this.threshold);
2528
this.processChunk(channel0);
2629
}
2730
return true;
@@ -31,6 +34,7 @@ class AudioProcessingWorklet extends AudioWorkletProcessor {
3134
this.port.postMessage({
3235
event: "chunk",
3336
data: {
37+
speaking: this.speaking,
3438
int16arrayBuffer: this.buffer.slice(0, this.bufferWriteIndex).buffer,
3539
},
3640
});

src/lib/services/realtime-chat-service.js

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { PUBLIC_SERVICE_URL } from "$env/static/public";
2-
import { AudioRecordingWorklet } from "$lib/helpers/pcmProcessor";
2+
import { AudioRecordingWorklet } from "$lib/helpers/realtime/pcmProcessor";
33

44
// @ts-ignore
55
const AudioContext = window.AudioContext || window.webkitAudioContext;
@@ -15,60 +15,63 @@ let audioQueue = [];
1515
/** @type {boolean} */
1616
let isPlaying = false;
1717

18-
export const realtimeChat = {
19-
20-
/** @type {WebSocket | null} */
21-
socket: null,
18+
/** @type {WebSocket | null} */
19+
let socket = null;
2220

23-
/** @type {MediaStream | null} */
24-
mediaStream: null,
21+
/** @type {MediaStream | null} */
22+
let mediaStream = null;
2523

26-
/** @type {AudioWorkletNode | null} */
27-
workletNode: null,
24+
/** @type {AudioWorkletNode | null} */
25+
let workletNode = null;
2826

29-
/** @type {MediaStreamAudioSourceNode | null} */
30-
micSource: null,
27+
/** @type {MediaStreamAudioSourceNode | null} */
28+
let micSource = null;
3129

30+
export const realtimeChat = {
31+
3232
/**
3333
* @param {string} agentId
3434
* @param {string} conversationId
3535
*/
3636
start(agentId, conversationId) {
3737
reset();
3838
const wsUrl = buildWebsocketUrl();
39-
this.socket = new WebSocket(`${wsUrl}/chat/stream/${agentId}/${conversationId}`);
39+
socket = new WebSocket(`${wsUrl}/chat/stream/${agentId}/${conversationId}`);
4040

41-
this.socket.onopen = async () => {
41+
socket.onopen = async () => {
4242
console.log("WebSocket connected");
4343

44-
this.socket?.send(JSON.stringify({
44+
socket?.send(JSON.stringify({
4545
event: "start"
4646
}));
4747

48-
this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
48+
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
4949
audioCtx = new AudioContext({ sampleRate: sampleRate });
5050

5151
const workletName = "audio-recorder-worklet";
5252
const src = createWorkletFromSrc(workletName, AudioRecordingWorklet);
5353
await audioCtx.audioWorklet.addModule(src);
5454

55-
this.workletNode = new AudioWorkletNode(audioCtx, workletName);
56-
this.micSource = audioCtx.createMediaStreamSource(this.mediaStream);
57-
this.micSource.connect(this.workletNode);
55+
workletNode = new AudioWorkletNode(audioCtx, workletName);
56+
micSource = audioCtx.createMediaStreamSource(mediaStream);
57+
micSource.connect(workletNode);
5858

59-
this.workletNode.port.onmessage = event => {
59+
workletNode.port.onmessage = event => {
6060
const arrayBuffer = event.data.data.int16arrayBuffer;
61-
if (arrayBuffer && this.socket?.readyState === WebSocket.OPEN) {
61+
if (arrayBuffer && socket?.readyState === WebSocket.OPEN) {
62+
if (event.data.data.speaking) {
63+
reset();
64+
}
6265
const arrayBufferString = arrayBufferToBase64(arrayBuffer);
63-
this.socket.send(JSON.stringify({
66+
socket.send(JSON.stringify({
6467
event: 'media',
6568
payload: arrayBufferString
6669
}));
6770
}
6871
};
6972
};
7073

71-
this.socket.onmessage = (/** @type {MessageEvent} */ e) => {
74+
socket.onmessage = (/** @type {MessageEvent} */ e) => {
7275
try {
7376
const json = JSON.parse(e.data);
7477
if (json.event === 'media' && !!json.media.payload) {
@@ -80,34 +83,37 @@ export const realtimeChat = {
8083
}
8184
};
8285

83-
this.socket.onclose = () => {
86+
socket.onclose = () => {
8487
console.log("Websocket closed");
8588
};
8689

87-
this.socket.onerror = (/** @type {Event} */ e) => {
90+
socket.onerror = (/** @type {Event} */ e) => {
8891
console.error('WebSocket error', e);
8992
};
9093
},
9194

9295
stop() {
9396
reset();
9497

95-
if (this.mediaStream) {
96-
this.mediaStream.getTracks().forEach(t => t.stop());
97-
this.mediaStream = null;
98+
if (mediaStream) {
99+
mediaStream.getTracks().forEach(t => t.stop());
100+
mediaStream = null;
98101
}
99102

100-
if (this.workletNode) {
101-
this.micSource?.disconnect(this.workletNode);
102-
this.workletNode.port.close();
103-
this.workletNode.disconnect();
103+
if (workletNode) {
104+
micSource?.disconnect(workletNode);
105+
workletNode.port.close();
106+
workletNode.disconnect();
107+
micSource = null;
108+
workletNode = null;
104109
}
105110

106-
if (this.socket?.readyState === WebSocket.OPEN) {
107-
this.socket.send(JSON.stringify({
111+
if (socket?.readyState === WebSocket.OPEN) {
112+
socket.send(JSON.stringify({
108113
event: 'disconnect'
109114
}));
110-
this.socket.close();
115+
socket.close();
116+
socket = null;
111117
}
112118
}
113119
};
@@ -143,7 +149,7 @@ function enqueueAudioChunk(base64Audio) {
143149
audioQueue.push(audioBuffer);
144150

145151
if (!isPlaying) {
146-
playNext();
152+
playNext();
147153
}
148154
}
149155

@@ -159,7 +165,6 @@ function playNext() {
159165
const source = audioCtx.createBufferSource();
160166
source.buffer = buffer;
161167
source.connect(audioCtx.destination);
162-
163168
source.onended = () => {
164169
playNext();
165170
};
@@ -221,7 +226,7 @@ function convert16BitPCMToFloat32(buffer) {
221226
for (let i = 0; i< chunk.length / 2; i++) {
222227
try {
223228
const int16 = dataView.getInt16(i * 2, true);
224-
output[i] = int16 / 32767;
229+
output[i] = int16 / 32768;
225230
} catch (e) {
226231
console.error(e);
227232
}

0 commit comments

Comments
 (0)