Skip to content

Commit f145fad

Browse files
committed
feat: enhance voice call handling and improve RNNoise integration
- Implement deterministic caller election to prevent double-dial glare. - Introduce a pendingCallsRef to manage outbound connections and avoid duplicates. - Add ICE connection state handling to restart failed connections. - Refactor RNNoise processor to use synchronous WASM loading for improved performance. - Optimize audio processing by managing input and output buffers more efficiently. - Ensure proper handling of audio frames and maintain audio quality during processing.
1 parent 0b49188 commit f145fad

5 files changed

Lines changed: 731 additions & 111 deletions

File tree

.firebase/hosting.ZGlzdA.cache

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
index.html,1774280652987,8bf40e82c66c3dbe4ab83e2124fdd143380352e1332b4bd32b36a1ecf4b4c6fb
2-
icons.svg,1774280652923,cc5ba1c58a9496115441b46650182deb0169982e8a98d605d965ce966ff37184
3-
assets/index-fNcAj_4k.css,1774280652987,6e390470412f0266335552c453978fad3072729804604a7fe73635c9e040ebbd
4-
favicon.svg,1774280652922,201714848d2eaecc14a9e2c9db2694646f7a53af7e7cce941c8bf103b38b4895
5-
logo.svg,1774280652923,2790afba95ebe7a2f9cd57ee6ec7cd7e76763b200544f2dc5bdbbe55b6a2434e
6-
assets/index-si1sbIgS.js,1774280652987,c02b499744a56fdeda9377a571827dc24ac3dbb9d825467b5a61dd73cef8a76c
1+
rnnoise-processor.js,1774286653481,c211d82a0892275db26800afcaab794a451303eab185cee632fac141bd0b81d3
2+
index.html,1774286653551,5b762a8b00f6f27d0991b38330b6cf2af48d0d9d80e15be7cb5da81430693fe2
3+
icons.svg,1774286653480,cc5ba1c58a9496115441b46650182deb0169982e8a98d605d965ce966ff37184
4+
assets/index-fNcAj_4k.css,1774286653551,6e390470412f0266335552c453978fad3072729804604a7fe73635c9e040ebbd
5+
favicon.svg,1774286653480,201714848d2eaecc14a9e2c9db2694646f7a53af7e7cce941c8bf103b38b4895
6+
logo.svg,1774286653481,2790afba95ebe7a2f9cd57ee6ec7cd7e76763b200544f2dc5bdbbe55b6a2434e
7+
assets/index-Dlofa59Z.js,1774286653550,04b37edd3b31b7e2f2d54eef4cb595c71a80486e1735585ce05badb6d9ca0f54
8+
rnnoise-sync.js,1774286653482,8cfb899a29432fd32db3a0d343adc756e59614c46d00494d8579215dea778085

public/rnnoise-processor.js

Lines changed: 70 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,123 @@
11
/**
22
* RNNoise AudioWorklet Processor
3-
* Processes audio in real-time using RNNoise WASM for noise suppression
3+
* Uses the sync wasm loader so WorkletGlobalScope can initialize synchronously.
44
*/
55

6-
import { RNNoise } from '@jitsi/rnnoise-wasm';
6+
import createRNNWasmModuleSync from '/rnnoise-sync.js';
77

88
class RNNoiseProcessor extends AudioWorkletProcessor {
99
constructor() {
1010
super();
11-
12-
this.rnnoise = null;
11+
12+
this.module = null;
13+
this.statePtr = 0;
14+
this.inPtr = 0;
15+
this.outPtr = 0;
1316
this.isInitialized = false;
14-
this.frameSize = 480; // RNNoise frame size for 48kHz (10ms)
17+
this.enabled = true;
18+
19+
this.frameSize = 480; // RNNoise frame size for 48kHz / 10ms
1520
this.inputBuffer = new Float32Array(this.frameSize);
1621
this.outputBuffer = new Float32Array(this.frameSize);
1722
this.bufferIndex = 0;
23+
this.pendingOutput = [];
24+
this.pendingOutputReadIndex = 0;
1825

19-
// Initialize RNNoise
20-
this.initRNNoise();
21-
22-
// Listen for messages from main thread
2326
this.port.onmessage = (event) => {
24-
if (event.data.type === 'enable') {
27+
if (event.data?.type === 'enable') {
2528
this.enabled = event.data.enabled;
2629
}
2730
};
2831

29-
this.enabled = true;
32+
this.initRNNoise();
3033
}
3134

3235
async initRNNoise() {
3336
try {
34-
// Initialize the RNNoise WASM module
35-
await RNNoise.initWasm();
36-
this.rnnoise = new RNNoise();
37+
this.module = createRNNWasmModuleSync();
38+
await this.module.ready;
39+
40+
this.module._rnnoise_init();
41+
this.statePtr = this.module._rnnoise_create();
42+
this.inPtr = this.module._malloc(this.frameSize * 4);
43+
this.outPtr = this.module._malloc(this.frameSize * 4);
44+
3745
this.isInitialized = true;
38-
console.log('RNNoise initialized successfully');
3946
this.port.postMessage({ type: 'initialized' });
4047
} catch (error) {
4148
console.error('Failed to initialize RNNoise:', error);
42-
this.port.postMessage({ type: 'error', message: error.message });
49+
this.port.postMessage({ type: 'error', message: error?.message || 'RNNoise init failed' });
4350
}
4451
}
4552

46-
process(inputs, outputs, parameters) {
47-
if (!this.isInitialized || !this.enabled) {
48-
// Pass through if not initialized or disabled
49-
if (inputs[0] && inputs[0][0]) {
50-
outputs[0][0].set(inputs[0][0]);
53+
denoiseFrame(inputFrame) {
54+
const heapOffsetIn = this.inPtr >> 2;
55+
const heapOffsetOut = this.outPtr >> 2;
56+
57+
this.module.HEAPF32.set(inputFrame, heapOffsetIn);
58+
this.module._rnnoise_process_frame(this.statePtr, this.outPtr, this.inPtr);
59+
60+
const denoised = this.module.HEAPF32.subarray(heapOffsetOut, heapOffsetOut + this.frameSize);
61+
this.outputBuffer.set(denoised);
62+
}
63+
64+
enqueueFrame(frame) {
65+
for (let i = 0; i < frame.length; i++) {
66+
this.pendingOutput.push(frame[i]);
67+
}
68+
}
69+
70+
dequeueSample(fallback) {
71+
if (this.pendingOutputReadIndex < this.pendingOutput.length) {
72+
const sample = this.pendingOutput[this.pendingOutputReadIndex];
73+
this.pendingOutputReadIndex += 1;
74+
75+
if (this.pendingOutputReadIndex > 4096 && this.pendingOutputReadIndex * 2 > this.pendingOutput.length) {
76+
this.pendingOutput = this.pendingOutput.slice(this.pendingOutputReadIndex);
77+
this.pendingOutputReadIndex = 0;
5178
}
52-
return true;
79+
80+
return sample;
5381
}
5482

55-
const input = inputs[0][0];
56-
const output = outputs[0][0];
83+
return fallback;
84+
}
85+
86+
process(inputs, outputs) {
87+
const input = inputs[0]?.[0];
88+
const output = outputs[0]?.[0];
5789

58-
if (!input) {
59-
output.fill(0);
90+
if (!output) return true;
91+
92+
if (!input || !this.isInitialized || !this.enabled) {
93+
if (input) output.set(input);
94+
else output.fill(0);
6095
return true;
6196
}
6297

63-
// Process audio frame by frame (RNNoise requires fixed 480-sample frames)
98+
// First, ingest input and process complete RNNoise frames.
6499
let inputIndex = 0;
65-
66100
while (inputIndex < input.length) {
67-
// Fill the input buffer
68-
const samplesToRead = Math.min(
69-
this.frameSize - this.bufferIndex,
70-
input.length - inputIndex
71-
);
72-
73-
this.inputBuffer.set(
74-
input.subarray(inputIndex, inputIndex + samplesToRead),
75-
this.bufferIndex
76-
);
77-
101+
const samplesToRead = Math.min(this.frameSize - this.bufferIndex, input.length - inputIndex);
102+
this.inputBuffer.set(input.subarray(inputIndex, inputIndex + samplesToRead), this.bufferIndex);
78103
this.bufferIndex += samplesToRead;
79104
inputIndex += samplesToRead;
80105

81-
// Process when we have a complete frame
82106
if (this.bufferIndex === this.frameSize) {
83107
try {
84-
// Apply RNNoise denoising
85-
this.rnnoise.denoise(this.inputBuffer, this.outputBuffer);
86-
87-
// Write output
88-
output.set(this.outputBuffer, inputIndex - this.frameSize);
108+
this.denoiseFrame(this.inputBuffer);
109+
this.enqueueFrame(this.outputBuffer);
89110
} catch (error) {
90111
console.error('RNNoise processing error:', error);
91-
output.set(this.inputBuffer, inputIndex - this.frameSize);
112+
this.enqueueFrame(this.inputBuffer);
92113
}
93-
94-
// Reset buffer index for next frame
95114
this.bufferIndex = 0;
96115
}
97116
}
98117

99-
// If there's remaining audio less than a full frame, pass it through
100-
if (this.bufferIndex > 0) {
101-
output.set(this.inputBuffer.subarray(0, this.bufferIndex), input.length - this.bufferIndex);
118+
// Then, render exactly one output quantum from the queued denoised samples.
119+
for (let i = 0; i < output.length; i++) {
120+
output[i] = this.dequeueSample(input[i] ?? 0);
102121
}
103122

104123
return true;

0 commit comments

Comments
 (0)