Skip to content

Commit fb0f339

Browse files
bbba -> la suite meet
1 parent 69c6e58 commit fb0f339

8 files changed

Lines changed: 364 additions & 10 deletions

File tree

src/frontend/public/wasm/BBBA-mapi.js

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
3.51 MB
Binary file not shown.

src/frontend/public/wasm/BBBA-nosimd-mapi.js

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
3.48 MB
Binary file not shown.
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright 2025-2026 Filipe Coelho <falktx@falktx.com>
2+
// SPDX-License-Identifier: ISC
3+
4+
// known constants
5+
const maxSymbolLength = 255;
6+
const nominalBufferSize = 128;
7+
8+
// function to setup wasm + emscripten module options for offline fetch
9+
const createWasmOpts = (wasmBlob, postRunCallback, errorCallback) => {
10+
return {
11+
// override to use previously retrieved blob data, as `fetch` is not allowed in worklets
12+
instantiateWasm: (imports, successCallback) => {
13+
WebAssembly.instantiate(wasmBlob, imports).then(output => {
14+
successCallback(output.instance, output.module);
15+
}).catch(error => {
16+
errorCallback(error);
17+
});
18+
19+
return {};
20+
},
21+
postRun: postRunCallback,
22+
};
23+
};
24+
25+
// class that holds a mono audio plugin instance
26+
// see https://github.com/DISTRHO/MAPI for the API used here
27+
class MapiProcessorInstance {
28+
constructor(port, module) {
29+
this.port = port;
30+
this.module = module;
31+
this.handle = module._mapi_create(sampleRate, nominalBufferSize);
32+
this.enabled = true;
33+
this.monitor_param = null;
34+
this.monitor_value = null;
35+
36+
this.audioData = module._malloc(module.HEAPF32.BYTES_PER_ELEMENT * nominalBufferSize);
37+
this.audioPtrs = module._malloc(module.HEAPU32.BYTES_PER_ELEMENT);
38+
module.HEAPU32[this.audioPtrs + (0 << 2) >> 2] = this.audioData;
39+
40+
this.csymbolData = module._malloc(maxSymbolLength);
41+
}
42+
43+
csymbol(symbol) {
44+
const len = Math.min(maxSymbolLength, this.module.lengthBytesUTF8(symbol) + 1);
45+
this.module.stringToUTF8(symbol, this.csymbolData, len);
46+
return this.csymbolData;
47+
}
48+
49+
param(symbol, value) {
50+
this.module._mapi_set_parameter(this.handle, this.csymbol(symbol), value);
51+
}
52+
53+
monitor(symbol) {
54+
this.monitor_param = symbol;
55+
this.monitor_value = this.module._mapi_get_parameter(this.handle, this.csymbol(symbol));
56+
}
57+
58+
process(buffer, bufferSize, bufferOffset) {
59+
if (! this.enabled)
60+
return;
61+
62+
for (let i = 0; i < bufferSize; ++i)
63+
this.module.HEAPF32[this.audioData + (i << 2) >> 2] = buffer[bufferOffset + i];
64+
65+
this.module._mapi_process(this.handle, this.audioPtrs, this.audioPtrs, bufferSize);
66+
67+
for (let i = 0; i < bufferSize; ++i)
68+
buffer[bufferOffset + i] = this.module.HEAPF32[this.audioData + (i << 2) >> 2];
69+
70+
if (this.monitor_param != null) {
71+
const value = this.module._mapi_get_parameter(this.handle, this.csymbol(this.monitor_param));
72+
if (this.monitor_value != value) {
73+
this.monitor_value = value;
74+
this.port.postMessage({ type: 'monitor', value: value });
75+
}
76+
}
77+
}
78+
};
79+
80+
// worklet processor implementation
81+
class MapiWorkletProcessor extends AudioWorkletProcessor {
82+
constructor(options) {
83+
super(options);
84+
85+
// validity checks
86+
if (options.numberOfInputs != options.numberOfOutputs)
87+
throw Error('Mis-matching IO, number of inputs must match outputs');
88+
if (options.numberOfInputs != 1)
89+
throw Error('Invalid IO, must be mono');
90+
91+
// workaround for Chromium-based browsers, return true in `process` until disconnected
92+
this.disconnected = false;
93+
94+
// MAPI processor instance
95+
this.bbba = null;
96+
97+
// bi-directional port communication
98+
this.port.onmessage = event => {
99+
switch (event.data.type)
100+
{
101+
case 'init':
102+
this.init(event.data);
103+
break;
104+
case 'enable':
105+
this.enable(event.data);
106+
break;
107+
case 'monitor':
108+
this.monitor(event.data);
109+
break;
110+
case 'param':
111+
this.param(event.data);
112+
break;
113+
case 'destroy':
114+
this.destroy();
115+
break;
116+
}
117+
};
118+
}
119+
120+
init(data) {
121+
// execute JS to expose the emscripten load module function
122+
const jsfn_bbba = new Function(data.js + 'return mapi_bbba;');
123+
const create_module_bbba = jsfn_bbba.call();
124+
125+
// create wasm opts for offline loading
126+
const opts = createWasmOpts(data.wasm,
127+
(module) => {
128+
this.bbba = new MapiProcessorInstance(this.port, module);
129+
this.port.postMessage({ type: 'loaded' });
130+
},
131+
(error) => {
132+
this.port.postMessage({ type: 'error', error: error });
133+
},
134+
);
135+
136+
// create the wasm module and instance
137+
create_module_bbba(opts);
138+
}
139+
140+
enable(data) {
141+
if (!this.bbba) {
142+
console.error('BBBA wasm is not loaded yet!');
143+
return;
144+
}
145+
146+
this.bbba.enabled = !!data.enable;
147+
}
148+
149+
monitor(data) {
150+
if (!this.bbba) {
151+
console.error('BBBA wasm is not loaded yet!');
152+
return;
153+
}
154+
155+
this.bbba.monitor(data.symbol);
156+
}
157+
158+
param(data) {
159+
if (!this.bbba) {
160+
console.error('BBBA wasm is not loaded yet!');
161+
return;
162+
}
163+
164+
this.bbba.param(data.symbol, data.value);
165+
}
166+
167+
destroy() {
168+
this.disconnected = true;
169+
this.bbba = null;
170+
}
171+
172+
process(inputs, outputs, parameters) {
173+
if (this.disconnected)
174+
return false;
175+
if (!this.bbba)
176+
return true;
177+
178+
const input = inputs[0];
179+
const output = outputs[0];
180+
181+
// IO check, can be zero if stream is not connected yet
182+
if (input.length == 0 || output.length == 0)
183+
return true;
184+
185+
// use in-place processing
186+
const buffer = output[0];
187+
buffer.set(input[0]);
188+
189+
for (let offset = 0; offset < buffer.length; offset += nominalBufferSize)
190+
this.bbba.process(buffer, Math.min(nominalBufferSize, buffer.length - offset), offset);
191+
192+
// reuse mono buffer if stereo
193+
if (output.length == 2)
194+
output[1].set(buffer);
195+
196+
return true;
197+
}
198+
};
199+
200+
registerProcessor("mapi-proc", MapiWorkletProcessor);

src/frontend/src/features/rooms/components/Join.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,16 @@ export const Join = ({
217217
try {
218218
const track = await createLocalAudioTrack({
219219
deviceId: { exact: audioDeviceId },
220+
noiseSuppression: false,
221+
echoCancellation: true,
222+
autoGainControl: true,
223+
voiceIsolation: false,
224+
225+
226+
// Audio quality optimized for voice
227+
sampleRate: 48000, // High quality sample rate
228+
channelCount: 1, // Mono for voice calls (saves bandwidth)
229+
sampleSize: 16 // 16-bit audio
220230
})
221231
setDynamicAudioTrack(track)
222232
} catch (error) {

src/frontend/src/features/rooms/livekit/processors/RnnNoiseProcessor.ts

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,99 @@
11
import { Track, TrackProcessor, ProcessorOptions } from 'livekit-client'
2-
import { NoiseSuppressorWorklet_Name } from '@timephy/rnnoise-wasm'
3-
4-
// This is an example how to get the script path using Vite, may be different when using other build tools
5-
// NOTE: `?worker&url` is important (`worker` to generate a working script, `url` to get its url to load it)
6-
import NoiseSuppressorWorklet from '@timephy/rnnoise-wasm/NoiseSuppressorWorklet?worker&url'
72

83
// Use Jitsi's approach: maintain a global AudioContext variable
94
// and suspend/resume it as needed to manage audio state
105
let audioContext: AudioContext
116

7+
// load wasm files and worklet
8+
const loadedFiles: {
9+
error: string | undefined;
10+
wasmBlob: ArrayBuffer | undefined;
11+
wasmJS: string | undefined;
12+
worklet: BlobPart;
13+
} = {
14+
// store first caught error
15+
error: undefined,
16+
// BBBA-mapi.wasm
17+
wasmBlob: undefined,
18+
// BBBA-mapi.js
19+
wasmJS: undefined,
20+
// mapi-proc.js
21+
worklet: '',
22+
};
23+
24+
const loadFiles = () => {
25+
return new Promise<void>((success, reject) => {
26+
// return early if already loaded before
27+
if (typeof loadedFiles.error !== 'undefined') {
28+
reject(loadedFiles.error);
29+
return;
30+
}
31+
if (loadedFiles.wasmBlob && loadedFiles.wasmJS && loadedFiles.worklet) {
32+
success();
33+
return;
34+
}
35+
36+
if (typeof AudioContext === 'undefined') {
37+
loadedFiles.error = 'AudioContext unsupported';
38+
reject(loadedFiles.error);
39+
return;
40+
}
41+
if (typeof WebAssembly === 'undefined') {
42+
loadedFiles.error = 'WebAssembly unsupported';
43+
reject(loadedFiles.error);
44+
return;
45+
}
46+
// eslint-disable-next-line max-len
47+
if (!WebAssembly.validate(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 2, 8, 1, 1, 97, 1, 98, 3, 127, 1, 6, 6, 1, 127, 1, 65, 0, 11, 7, 5, 1, 1, 97, 3, 1]))) {
48+
loadedFiles.error = 'Importable/Exportable mutable globals unsupported';
49+
reject(loadedFiles.error);
50+
return;
51+
}
52+
53+
// check if SIMD is supported, needed for old Safari versions
54+
const supportsSIMD = WebAssembly.validate(
55+
// eslint-disable-next-line max-len
56+
new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11]),
57+
);
58+
59+
const catchHandler = (error: string) => {
60+
// only reject Promise once
61+
if (!loadedFiles.error) {
62+
loadedFiles.error = error;
63+
reject(loadedFiles.error);
64+
}
65+
};
66+
67+
const checkResolved = () => {
68+
if (loadedFiles.wasmBlob && loadedFiles.wasmJS && loadedFiles.worklet) {
69+
success();
70+
}
71+
};
72+
73+
// load wasm files and worklet
74+
const basepath = '/wasm/';
75+
const suffix = supportsSIMD ? '' : '-nosimd';
76+
fetch(`${basepath}BBBA${suffix}-mapi.wasm`).then((resp) => {
77+
resp.arrayBuffer().then((bytes) => {
78+
loadedFiles.wasmBlob = bytes;
79+
checkResolved();
80+
}).catch(catchHandler);
81+
}).catch(catchHandler);
82+
fetch(`${basepath}BBBA${suffix}-mapi.js`).then((resp) => {
83+
resp.text().then((text) => {
84+
loadedFiles.wasmJS = text;
85+
checkResolved();
86+
}).catch(catchHandler);
87+
}).catch(catchHandler);
88+
fetch(`${basepath}mapi-proc.js`).then((resp) => {
89+
resp.text().then((text) => {
90+
loadedFiles.worklet = text;
91+
checkResolved();
92+
}).catch(catchHandler);
93+
}).catch(catchHandler);
94+
});
95+
};
96+
1297
export interface AudioProcessorInterface
1398
extends TrackProcessor<Track.Kind.Audio> {
1499
name: string
@@ -36,17 +121,34 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
36121
await audioContext.resume()
37122
}
38123

39-
await audioContext.audioWorklet.addModule(NoiseSuppressorWorklet)
124+
await loadFiles();
125+
126+
const processorBlob = new Blob([loadedFiles.worklet], { type: 'text/javascript' });
127+
const processorURL = URL.createObjectURL(processorBlob);
128+
129+
await audioContext.audioWorklet.addModule(processorURL)
40130

41131
this.sourceNode = audioContext.createMediaStreamSource(
42132
new MediaStream([this.source])
43133
)
44134

135+
45136
this.noiseSuppressionNode = new AudioWorkletNode(
46137
audioContext,
47-
NoiseSuppressorWorklet_Name
138+
'mapi-proc'
48139
)
49-
140+
const nn = this.noiseSuppressionNode;
141+
this.noiseSuppressionNode.port.onmessage = (event) => {
142+
if (event.data?.type === 'loaded') {
143+
nn.port.postMessage({ type: 'param', symbol: "intensity", value: 90 });
144+
nn.port.postMessage({ type: 'param', symbol: "leveler_target", value: -18 });
145+
nn.port.postMessage({ type: 'param', symbol: "sb_strength", value: 60 });
146+
nn.port.postMessage({ type: 'param', symbol: "mb_strength", value: 60 });
147+
nn.port.postMessage({ type: 'param', symbol: "pre_gain", value: 2 });
148+
nn.port.postMessage({ type: 'param', symbol: "post_gain", value: 0 });
149+
}
150+
};
151+
this.noiseSuppressionNode.port.postMessage({ type: 'init', wasm: loadedFiles.wasmBlob, js: loadedFiles.wasmJS });
50152
this.destinationNode = audioContext.createMediaStreamDestination()
51153

52154
// Connect the audio processing chain

src/frontend/src/locales/en/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
"disabled": "Microphone disabled"
2222
},
2323
"noiseReduction": {
24-
"label": "Noise reduction",
25-
"heading": "Noise reduction",
24+
"label": "Advanced voice optimization",
25+
"heading": "Audio filters",
2626
"ariaLabel": {
2727
"enable": "Enable noise reduction",
2828
"disable": "Disable noise reduction"

0 commit comments

Comments
 (0)