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:
@@ -65,6 +65,10 @@ export function AppLayout() {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
const outputDeviceId = useVoiceStore((s) => s.outputDeviceId);
|
||||
useEffect(() => {
|
||||
AudioManager.getInstance().setOutputDevice(outputDeviceId);
|
||||
}, [outputDeviceId]);
|
||||
const { user, isLoading } = useAuth();
|
||||
const setCurrentServer = useServerStore((s) => s.setCurrentServer);
|
||||
const loadServerDetail = useServerStore((s) => s.loadServerDetail);
|
||||
|
||||
@@ -69,6 +69,14 @@ export function AppLayout() {
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Sync persisted output device preference to AudioManager.
|
||||
// AudioManager defers setSinkId until the AudioContext is actually created,
|
||||
// so this is safe to call before any user interaction.
|
||||
const outputDeviceId = useVoiceStore((s) => s.outputDeviceId);
|
||||
useEffect(() => {
|
||||
AudioManager.getInstance().setOutputDevice(outputDeviceId);
|
||||
}, [outputDeviceId]);
|
||||
|
||||
const { user, isLoading } = useAuth();
|
||||
const setCurrentServer = useServerStore((s) => s.setCurrentServer);
|
||||
const loadServerDetail = useServerStore((s) => s.loadServerDetail);
|
||||
|
||||
@@ -144,8 +144,16 @@ function UserAreaPanel({ user, isMuted, isDeafened, onMicToggle, onDeafenToggle,
|
||||
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');
|
||||
const dedup = (list) => {
|
||||
const seen = new Set();
|
||||
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);
|
||||
const currentInput = inputs.find(d => d.deviceId === inputDeviceId);
|
||||
@@ -220,6 +228,7 @@ function UserAreaPanel({ user, isMuted, isDeafened, onMicToggle, onDeafenToggle,
|
||||
};
|
||||
const selectInput = (device) => {
|
||||
setInputDevice(device.deviceId);
|
||||
AudioManager.getInstance().setInputDevice(device.deviceId);
|
||||
setSelectedInputLabel(device.label || 'Default');
|
||||
setShowInputDeviceList(false);
|
||||
};
|
||||
@@ -227,9 +236,7 @@ function UserAreaPanel({ user, isMuted, isDeafened, onMicToggle, onDeafenToggle,
|
||||
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)
|
||||
const micBars = 20;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user