Skip to content

Commit 8a718eb

Browse files
authored
Web demo improvements (#74)
1 parent 57b61eb commit 8a718eb

File tree

8 files changed

+874
-281
lines changed

8 files changed

+874
-281
lines changed

.github/workflows/web-demos.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
strategy:
2727
matrix:
28-
node-version: [16.x, 18.x, 20.x]
28+
node-version: [18.x, 20.x, 22.x]
2929

3030
steps:
3131
- uses: actions/checkout@v3
@@ -43,4 +43,4 @@ jobs:
4343
run: npm install yarn
4444

4545
- name: Install dependencies
46-
run: yarn install
46+
run: yarn install

demo/web/.prettierignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore artifacts:
2+
build
3+
coverage

demo/web/.prettierrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

demo/web/eslint.config.mjs

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import globals from "globals";
2+
import pluginJs from "@eslint/js";
3+
4+
/** @type {import('eslint').Linter.Config[]} */
5+
export default [
6+
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
7+
{
8+
languageOptions: {
9+
globals: {
10+
...globals.browser,
11+
...globals.node,
12+
KoalaWeb: "readonly",
13+
modelParams: "readonly",
14+
},
15+
},
16+
},
17+
pluginJs.configs.recommended,
18+
{
19+
rules: {
20+
"no-unused-vars": ["off"],
21+
},
22+
},
23+
];

demo/web/index.html

+30-276
Original file line numberDiff line numberDiff line change
@@ -1,281 +1,26 @@
1-
<!DOCTYPE html>
1+
<!doctype html>
22
<html lang="en">
33
<head>
44
<script src="node_modules/@picovoice/web-voice-processor/dist/iife/index.js"></script>
55
<script src="node_modules/@picovoice/koala-web/dist/iife/index.js"></script>
66
<script src="koala_params.js"></script>
7-
<script type="application/javascript">
8-
let koala = null;
9-
10-
let originalBuffer;
11-
let enhancedBuffer;
12-
let outputFrames;
13-
14-
window.onload = function () {
15-
const audioContext = new (window.AudioContext || window.webKitAudioContext)(
16-
{sampleRate: 16000}
17-
);
18-
19-
const originalAudioGain = audioContext.createGain();
20-
originalAudioGain.gain.value = 0;
21-
22-
const enhancedAudioGain = audioContext.createGain();
23-
enhancedAudioGain.gain.value = 1;
24-
25-
originalAudioGain.connect(audioContext.destination);
26-
enhancedAudioGain.connect(audioContext.destination);
27-
28-
let originalAudioSource;
29-
let enhancedAudioSource;
30-
let isPlaying = false;
31-
32-
function readAudioFile(selectedFile, callback) {
33-
let reader = new FileReader();
34-
reader.onload = function (ev) {
35-
let wavBytes = reader.result;
36-
audioContext.decodeAudioData(wavBytes, callback);
37-
};
38-
reader.readAsArrayBuffer(selectedFile);
39-
}
40-
41-
const fileSelector = document.getElementById("audioFile");
42-
fileSelector.addEventListener("change", async (event) => {
43-
outputFrames = [];
44-
resultBox.style.display = "none";
45-
46-
originalAudioSource?.stop();
47-
enhancedAudioSource?.stop();
48-
49-
writeMessage("Loading audio file...");
50-
const fileList = event.target.files;
51-
readAudioFile(fileList[0], async (audioBuffer) => {
52-
const f32PCM = audioBuffer.getChannelData(0);
53-
const i16PCM = new Int16Array(f32PCM.length);
54-
55-
const INT16_MAX = 32767;
56-
const INT16_MIN = -32768;
57-
i16PCM.set(
58-
f32PCM.map((f) => {
59-
let i = Math.trunc(f * INT16_MAX);
60-
if (f > INT16_MAX) i = INT16_MAX;
61-
if (f < INT16_MIN) i = INT16_MIN;
62-
return i;
63-
})
64-
);
65-
66-
writeMessage("Processing audio file...");
67-
const splitPcm = [];
68-
await koala.reset();
69-
for (let i = 0; i < (i16PCM.length - koala.frameLength + 1); i += koala.frameLength) {
70-
const split = i16PCM.slice(i, i + koala.frameLength);
71-
splitPcm.push(split);
72-
await koala.process(split);
73-
}
74-
75-
writeMessage("Waiting for Koala engine to finish processing audio file...");
76-
await waitForProcess(splitPcm, outputFrames);
77-
78-
originalBuffer = createBuffer(i16PCM);
79-
enhancedBuffer = createBuffer(mergeFrames(outputFrames, koala.delaySample));
80-
81-
writeMessage("Press 'Play' to listen to recording. Move the slider to play around with noise.");
82-
resultBox.style.display = "block";
83-
});
84-
});
85-
86-
const displayTimer = document.getElementById("displayTimer");
87-
const recordButton = document.getElementById("recordAudio");
88-
const stopRecord = document.getElementById("stopRecord");
89-
const resultBox = document.getElementById("result");
90-
const volumeControl = document.getElementById("volumeControl");
91-
const playAudio = document.getElementById("playAudio");
92-
93-
let timer = null;
94-
let currentTimer = 0.0;
95-
let audioData = [];
96-
const recorderEngine = {
97-
onmessage: (event) => {
98-
switch (event.data.command) {
99-
case "process":
100-
audioData.push(event.data.inputFrame);
101-
break;
102-
}
103-
}
104-
}
105-
106-
recordButton.addEventListener("click", async () => {
107-
displayTimer.style.display = "inline";
108-
stopRecord.style.display = "inline";
109-
recordButton.style.display = "none";
110-
resultBox.style.display = "none";
111-
112-
originalAudioSource?.stop();
113-
enhancedAudioSource?.stop();
114-
115-
currentTimer = 0.0;
116-
audioData = [];
117-
outputFrames = [];
118-
119-
try {
120-
writeMessage("Recording audio...");
121-
window.WebVoiceProcessor.WebVoiceProcessor.setOptions({
122-
frameLength: koala.frameLength
123-
});
124-
await window.WebVoiceProcessor.WebVoiceProcessor.subscribe([recorderEngine, koala]);
125-
timer = setInterval(() => {
126-
currentTimer += 0.1;
127-
displayTimer.innerText = `${currentTimer.toFixed(1)} / 120`;
128-
if (currentTimer === 120) {
129-
stopRecord.click();
130-
}
131-
}, 100);
132-
} catch (e) {
133-
writeMessage(e);
134-
}
135-
});
136-
137-
stopRecord.addEventListener("click", async () => {
138-
displayTimer.style.display = "none";
139-
stopRecord.style.display = "none";
140-
recordButton.style.display = "inline";
141-
142-
await window.WebVoiceProcessor.WebVoiceProcessor.unsubscribe([recorderEngine, koala]);
143-
clearInterval(timer);
144-
145-
writeMessage("Waiting for Koala engine to finish processing...")
146-
await waitForProcess(audioData, outputFrames);
147-
148-
originalBuffer = createBuffer(mergeFrames(audioData));
149-
enhancedBuffer = createBuffer(mergeFrames(outputFrames, koala.delaySample));
150-
151-
writeMessage("Press 'Play' to listen to recording. Move the slider to play around with noise.");
152-
resultBox.style.display = "block";
153-
});
154-
155-
volumeControl.addEventListener("input", (e) => {
156-
originalAudioGain.gain.value = 1 - e.target.value;
157-
enhancedAudioGain.gain.value = e.target.value;
158-
});
159-
160-
playAudio.addEventListener("click", () => {
161-
if (!isPlaying) {
162-
isPlaying = true;
163-
const current_time = audioContext.currentTime;
164-
165-
originalAudioSource = audioContext.createBufferSource();
166-
enhancedAudioSource = audioContext.createBufferSource();
167-
168-
originalAudioSource.buffer = originalBuffer;
169-
originalAudioSource.loop = true;
170-
originalAudioSource.connect(originalAudioGain);
171-
originalAudioSource.start(current_time + 0.2);
172-
173-
enhancedAudioSource.buffer = enhancedBuffer;
174-
enhancedAudioSource.loop = true;
175-
enhancedAudioSource.connect(enhancedAudioGain);
176-
enhancedAudioSource.start(current_time + 0.2);
177-
178-
playAudio.innerHTML = "Stop"
179-
} else {
180-
isPlaying = false;
181-
182-
originalAudioSource.stop();
183-
enhancedAudioSource.stop();
184-
185-
playAudio.innerHTML = "Play"
186-
}
187-
});
188-
189-
function mergeFrames(data, delaySample = 0) {
190-
let delay = 0;
191-
192-
const pcm = new Int16Array(data.length * koala.frameLength);
193-
for (let i = 0; i < data.length; i++) {
194-
if (i * koala.frameLength < delaySample) {
195-
delay += 1;
196-
} else {
197-
pcm.set(data[i], (i - delay) * koala.frameLength);
198-
}
199-
}
200-
return pcm;
201-
}
202-
203-
function createBuffer(data) {
204-
const buffer = audioContext.createBuffer(1, data.length, koala.sampleRate);
205-
const source = new Float32Array(data.length);
206-
for (let i = 0; i < data.length; i++){
207-
source[i] = data[i] < 0 ? data[i] / 32768 : data[i] / 32767;
208-
}
209-
buffer.copyToChannel(source, 0);
210-
return buffer;
211-
}
212-
213-
async function waitForProcess(input, output) {
214-
return new Promise(resolve => {
215-
setInterval(() => {
216-
if (input.length === output.length) {
217-
resolve();
218-
}
219-
}, 100)
220-
});
221-
}
222-
}
223-
224-
function writeMessage(message) {
225-
console.log(message);
226-
document.getElementById("status").innerHTML = message;
227-
}
228-
229-
function processErrorCallback(error) {
230-
writeMessage(error);
231-
}
232-
233-
function processCallback(enhancedPcm) {
234-
outputFrames.push(enhancedPcm);
235-
}
236-
237-
async function startKoala(accessKey) {
238-
writeMessage("Koala is loading. Please wait...");
239-
try {
240-
koala = await KoalaWeb.KoalaWorker.create(
241-
accessKey,
242-
processCallback,
243-
{ base64: modelParams },
244-
{ processErrorCallback: processErrorCallback }
245-
);
246-
247-
writeMessage("Koala worker ready!");
248-
249-
writeMessage(
250-
"WebVoiceProcessor initializing. Microphone permissions requested ..."
251-
);
252-
window.WebVoiceProcessor.WebVoiceProcessor.setOptions({
253-
frameLength: koala.frameLength
254-
});
255-
document.getElementById("control").style.display = "block";
256-
writeMessage("Koala worker is ready!");
257-
} catch (err) {
258-
processErrorCallback(err);
259-
}
260-
}
261-
</script>
7+
<script type="application/javascript" src="scripts/koala.js"></script>
2628
</head>
2639
<body>
26410
<h1>Koala Web Demo</h1>
26511
<p>This demo uses Koala for Web and the WebVoiceProcessor to:</p>
26612
<ol>
13+
<li>Create an instance of Koala with the model file provided.</li>
26714
<li>
268-
Create an instance of Koala with the model file provided.
15+
Select an audio file or acquire microphone (& ask permission) data
16+
stream and convert to voice processing format (16kHz 16-bit linear PCM).
17+
The downsampled audio is forwarded to the Koala engine. The audio
18+
<i>does not</i> leave the browser: all processing is occurring via the
19+
Koala WebAssembly code.
26920
</li>
27021
<li>
271-
Select an audio file or acquire microphone (& ask permission) data stream and convert to voice
272-
processing format (16kHz 16-bit linear PCM). The downsampled audio is
273-
forwarded to the Koala engine. The audio <i>does not</i> leave the
274-
browser: all processing is occurring via the Koala WebAssembly code.
275-
</li>
276-
<li>
277-
Enhance audio real time using Koala engine. Output both original and enhanced
278-
audio.
22+
Enhance audio real time using Koala engine. Output both original and
23+
enhanced audio.
27924
</li>
28025
</ol>
28126
After entering the AccessKey, click the "Start Koala" button.
@@ -291,29 +36,38 @@ <h1>Koala Web Demo</h1>
29136
value="Start Koala"
29237
onclick="startKoala(document.getElementById('accessKey').value)"
29338
/>
294-
<hr/>
39+
<hr />
29540
<div id="control" style="display: none">
29641
<label for="audioFile">Choose audio file to enhance:</label>
297-
<input type="file" id="audioFile" name="audioFile"/>
42+
<input type="file" id="audioFile" name="audioFile" />
29843
<p><b>OR</b></p>
299-
<label for="recordAudio">Record audio to enhance (up to 2 minutes):</label>
44+
<label for="recordAudio"
45+
>Record audio to enhance (up to 2 minutes):</label
46+
>
30047
<button id="recordAudio">Record Audio</button>
301-
<span id="displayTimer" style="display: none;"></span>
302-
<button id="stopRecord" style="display: none;">Stop Recording</button>
303-
<hr/>
48+
<span id="displayTimer" style="display: none"></span>
49+
<button id="stopRecord" style="display: none">Stop Recording</button>
50+
<hr />
30451
</div>
30552
<div id="status"></div>
306-
<br>
53+
<br />
30754
<div id="result" style="display: none">
30855
<label>
30956
Original
310-
<input type="range" id="volumeControl" min="0" max="1" value="1" step="0.01" />
57+
<input
58+
type="range"
59+
id="volumeControl"
60+
min="0"
61+
max="1"
62+
value="1"
63+
step="0.01"
64+
/>
31165
Koalafied
312-
<br>
313-
<br>
66+
<br />
67+
<br />
31468
<button id="playAudio">Play</button>
31569
</label>
31670
</div>
317-
<br>
71+
<br />
31872
</body>
31973
</html>

demo/web/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
"@picovoice/web-voice-processor": "~4.0.8"
2323
},
2424
"devDependencies": {
25-
"http-server": "^14.0.0"
25+
"@eslint/js": "^9.22.0",
26+
"eslint": "^9.22.0",
27+
"globals": "^16.0.0",
28+
"http-server": "^14.0.0",
29+
"prettier": "3.5.1"
2630
}
2731
}

0 commit comments

Comments
 (0)