feat: RNNoise WASM AudioWorklet for ML-based noise suppression (Phase 3)

Inject @sapphi-red/web-noise-suppressor into AudioManager's input pipeline
as a toggleable AudioWorkletNode. The worklet is loaded lazily on first
enable, then kept alive — toggling bypasses by rewiring the graph without
destroying the WASM instance. Browser NS is forced off when RNNoise is
active to avoid double-processing. InputGain forced to stereo up-mix to
prevent mono-left-only output from the worklet. Also wires Phase 2 output
device routing (setSinkId) into AppLayout/ChannelSidebar.
This commit is contained in:
Jannis Braun
2026-02-23 00:28:29 +01:00
parent dbebd38576
commit ccf2001047
15 changed files with 345 additions and 49 deletions
@@ -408,8 +408,18 @@ function UserAreaPanel({
await navigator.mediaDevices.getUserMedia({ audio: true }).then(s => s.getTracks().forEach(t => t.stop()));
}
const devices = await navigator.mediaDevices.enumerateDevices();
const inputs = devices.filter(d => d.kind === 'audioinput');
const outputs = devices.filter(d => d.kind === 'audiooutput');
// Deduplicate by deviceId — USB devices sharing the same audio chipset
// (e.g. C-Media 0d8c:0134) appear as multiple entries with identical IDs.
const dedup = (list: MediaDeviceInfo[]): MediaDeviceInfo[] => {
const seen = new Set<string>();
return list.filter(d => {
if (seen.has(d.deviceId)) return false;
seen.add(d.deviceId);
return true;
});
};
const inputs = dedup(devices.filter(d => d.kind === 'audioinput'));
const outputs = dedup(devices.filter(d => d.kind === 'audiooutput'));
setInputDevices(inputs);
setOutputDevices(outputs);
@@ -483,7 +493,8 @@ function UserAreaPanel({
};
const selectInput = (device: MediaDeviceInfo) => {
setInputDevice(device.deviceId);
setInputDevice(device.deviceId); // Pure state update → triggers syncMic if in voice call
AudioManager.getInstance().setInputDevice(device.deviceId); // Immediate preview for mic level meter
setSelectedInputLabel(device.label || 'Default');
setShowInputDeviceList(false);
};
@@ -492,8 +503,7 @@ function UserAreaPanel({
setOutputDevice(device.deviceId);
setSelectedOutputLabel(device.label || 'Default');
setShowOutputDeviceList(false);
const room = getActiveRoom();
if (room) room.switchActiveDevice('audiooutput', device.deviceId).catch(() => {});
AudioManager.getInstance().setOutputDevice(device.deviceId);
};
// Generate mic level bars (20 bars like Discord)