Skip to content

Commit f0ff28b

Browse files
authored
Vanilla javascript websocket with microphone example (#171)
* Vanilla javascript websocket Vanilla javascript websocket client using AudioWorklet * Update data-conversion-processor.js Adding buffer to the AudioWorkletProcessor * Create voice_client_with_script_processor.js Adding deprecated ScriptProcessorNode as another example. * Added README Adding both solutions in the html and adding a README explaining the examples.
1 parent f340e89 commit f0ff28b

File tree

5 files changed

+294
-0
lines changed

5 files changed

+294
-0
lines changed

client-samples/javascript/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
### Vanilla JS Microphone Client - Audio Worklet vs Script Processor Node
2+
3+
This code shows two examples of communicating to a remote Vosk server using a microphone in javascript.
4+
5+
One example is using the latest Audio Worklet API, the other example is using a deprecated Script Processor Node.
6+
7+
[Audio Worklet](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_AudioWorklet) has replaced a deprecated ScriptProcessorNode. One issue with the new AudioWorkletProcessor, is that it only runs over https (see [this](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletNode) doc).
8+
In order to test it, one must run it on a secure server and streaming over to another (Vosk) secure server. It should work in local environment if a localhost Vosk is running,
9+
but it won't work with a remote server over plain http.
10+
11+
Now deprecated ScriptProcessorNode is added as another example (voice_client_with_script_processor.js), so it is possible to still use ScriptProcessorNode instead,
12+
which will communicate over plain http.
13+
14+
A pair of listen/stop listening buttons is added in the html to compare the two solutions.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
class DataConversionAudioProcessor extends AudioWorkletProcessor {
2+
bufferSize = 4096
3+
_bytesWritten = 0
4+
_buffer = new Int16Array(this.bufferSize)
5+
6+
constructor(options) {
7+
super()
8+
this.initBuffer()
9+
}
10+
11+
initBuffer() {
12+
this._bytesWritten = 0
13+
}
14+
15+
isBufferEmpty() {
16+
return this._bytesWritten === 0
17+
}
18+
19+
isBufferFull() {
20+
return this._bytesWritten === this.bufferSize
21+
}
22+
23+
process(inputs, outputs, parameters) {
24+
const inputData = inputs[0][0];
25+
26+
if (this.isBufferFull()) {
27+
this.flush()
28+
}
29+
30+
if (!inputData) return
31+
32+
for (let index = inputData.length; index > 0; index--) {
33+
this._buffer[this._bytesWritten++] = 32767 * Math.min(1, inputData[index]);
34+
}
35+
36+
return true;
37+
}
38+
39+
flush() {
40+
this.port.postMessage(
41+
this._bytesWritten < this.bufferSize
42+
? this._buffer.slice(0, this._bytesWritten)
43+
: this._buffer
44+
)
45+
this.initBuffer()
46+
}
47+
}
48+
49+
registerProcessor('data-conversion-processor', DataConversionAudioProcessor)

client-samples/javascript/index.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Vosk WS ASR</title>
7+
<script type="text/javascript" src="voice_client_with_audio_worklet.js"></script>
8+
<script type="text/javascript" src="voice_client_with_script_processor.js"></script>
9+
10+
</head>
11+
12+
<body>
13+
<div id="main">
14+
<button id="listen">Listen</button>
15+
<button id="stopListening">Stop Listening</button>
16+
<button id="listenWithScript">Listen With Script Processor</button>
17+
<button id="stopListeningWithScript">Stop Listening With Script Processor</button>
18+
</div>
19+
<div id="messages">
20+
<textarea id="q" rows="5" cols="60"></textarea>
21+
</div>
22+
23+
</body>
24+
25+
</html>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
var context;
2+
var source;
3+
var processor;
4+
var streamLocal;
5+
var webSocket;
6+
var inputArea;
7+
const sampleRate = 8000;
8+
const wsURL = 'ws://localhost:2700';
9+
var initComplete = false;
10+
11+
(function () {
12+
document.addEventListener('DOMContentLoaded', (event) => {
13+
inputArea = document.getElementById('q');
14+
15+
const listenButton = document.getElementById('listen');
16+
const stopListeningButton = document.getElementById('stopListening');
17+
18+
listenButton.addEventListener('mousedown', function () {
19+
listenButton.disabled = true;
20+
21+
initWS();
22+
navigator.mediaDevices.getUserMedia({
23+
audio: {
24+
echoCancellation: true,
25+
noiseSuppression: true,
26+
channelCount: 1,
27+
sampleRate
28+
}, video: false
29+
}).then(handleSuccess);
30+
listenButton.style.color = 'green';
31+
initComplete = true;
32+
});
33+
34+
stopListeningButton.addEventListener('mouseup', function () {
35+
if (initComplete === true) {
36+
37+
webSocket.send('{"eof" : 1}');
38+
webSocket.close();
39+
40+
processor.port.close();
41+
source.disconnect(processor);
42+
context.close();
43+
44+
if (streamLocal.active) {
45+
streamLocal.getTracks()[0].stop();
46+
}
47+
listenButton.style.color = 'black';
48+
listenButton.disabled = false;
49+
initComplete = false;
50+
inputArea.innerText = ""
51+
}
52+
});
53+
54+
});
55+
}())
56+
57+
58+
const handleSuccess = function (stream) {
59+
streamLocal = stream;
60+
61+
context = new AudioContext({sampleRate: sampleRate});
62+
63+
context.audioWorklet.addModule('data-conversion-processor.js').then(
64+
function () {
65+
processor = new AudioWorkletNode(context, 'data-conversion-processor', {
66+
channelCount: 1,
67+
numberOfInputs: 1,
68+
numberOfOutputs: 1
69+
});
70+
let constraints = {audio: true};
71+
navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
72+
source = context.createMediaStreamSource(stream);
73+
74+
source.connect(processor);
75+
processor.connect(context.destination);
76+
77+
processor.port.onmessage = event => webSocket.send(event.data)
78+
processor.port.start()
79+
});
80+
}
81+
);
82+
};
83+
84+
function initWS() {
85+
webSocket = new WebSocket(wsURL);
86+
webSocket.binaryType = "arraybuffer";
87+
88+
webSocket.onopen = function (event) {
89+
console.log('New connection established');
90+
};
91+
92+
webSocket.onerror = function (event) {
93+
console.error(event.data);
94+
};
95+
96+
webSocket.onmessage = function (event) {
97+
if (event.data) {
98+
let parsed = JSON.parse(event.data);
99+
if (parsed.result) console.log(parsed.result);
100+
if (parsed.text) inputArea.innerText = parsed.text;
101+
}
102+
};
103+
}
104+
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
var context;
2+
var source;
3+
var processor;
4+
var streamLocal;
5+
var webSocket;
6+
var inputArea;
7+
const bufferSize = 8192;
8+
const sampleRate = 8000;
9+
const wsURL = 'ws://localhost:2700';
10+
var initComplete = false;
11+
12+
(function () {
13+
document.addEventListener('DOMContentLoaded', (event) => {
14+
inputArea = document.getElementById('q');
15+
16+
const listenButton = document.getElementById('listenWithScript');
17+
const stopListeningButton = document.getElementById('stopListeningWithScript');
18+
19+
listenButton.addEventListener('mousedown', function () {
20+
listenButton.disabled = true;
21+
22+
initWS();
23+
navigator.mediaDevices.getUserMedia({
24+
audio: {
25+
echoCancellation: true,
26+
noiseSuppression: true,
27+
channelCount: 1,
28+
sampleRate
29+
}, video: false
30+
}).then(handleSuccess);
31+
listenButton.style.color = 'green';
32+
initComplete = true;
33+
});
34+
35+
stopListeningButton.addEventListener('mouseup', function () {
36+
if (initComplete === true) {
37+
38+
webSocket.send('{"eof" : 1}');
39+
webSocket.close();
40+
41+
source.disconnect(processor);
42+
processor.disconnect(context.destination);
43+
if (streamLocal.active) {
44+
streamLocal.getTracks()[0].stop();
45+
}
46+
listenButton.style.color = 'black';
47+
listenButton.disabled = false;
48+
initComplete = false;
49+
inputArea.innerText = ""
50+
}
51+
});
52+
});
53+
}())
54+
55+
56+
const handleSuccess = function (stream) {
57+
streamLocal = stream;
58+
context = new AudioContext({sampleRate: sampleRate});
59+
source = context.createMediaStreamSource(stream);
60+
processor = context.createScriptProcessor(bufferSize, 1, 1);
61+
62+
source.connect(processor);
63+
processor.connect(context.destination);
64+
65+
processor.onaudioprocess = function (audioDataChunk) {
66+
console.log(audioDataChunk.inputBuffer);
67+
sendAudio(audioDataChunk);
68+
};
69+
};
70+
71+
function sendAudio(audioDataChunk) {
72+
if (webSocket.readyState === WebSocket.OPEN) {
73+
// convert to 16-bit payload
74+
const inputData = audioDataChunk.inputBuffer.getChannelData(0) || new Float32Array(bufferSize);
75+
const targetBuffer = new Int16Array(inputData.length);
76+
for (let index = inputData.length; index > 0; index--) {
77+
targetBuffer[index] = 32767 * Math.min(1, inputData[index]);
78+
}
79+
webSocket.send(targetBuffer.buffer);
80+
}
81+
}
82+
83+
function initWS() {
84+
webSocket = new WebSocket(wsURL);
85+
webSocket.binaryType = "arraybuffer";
86+
87+
webSocket.onopen = function (event) {
88+
console.log('New connection established');
89+
};
90+
91+
webSocket.onerror = function (event) {
92+
console.error(event.data);
93+
};
94+
95+
webSocket.onmessage = function (event) {
96+
if (event.data) {
97+
let parsed = JSON.parse(event.data);
98+
if (parsed.result) console.log(parsed.result);
99+
if (parsed.text) inputArea.innerText = parsed.text;
100+
}
101+
};
102+
}

0 commit comments

Comments
 (0)