diff --git a/desktop/app.js b/desktop/app.js index 490f680..25bc725 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -23,6 +23,10 @@ const state = { muted: false, sharingVideo: false, sharingScreen: false, + noiseFilterMode: 'deepfilter', + activeNoiseFilter: 'off', + deepFilterProcessor: null, + deepFilterModule: null, viewMode: 'chat', // 'chat' or 'video' iceServers: [{ urls: "stun:stun.l.google.com:19302" }], }, @@ -114,6 +118,7 @@ const el = { vcChannelName: document.getElementById("vc-channel-name"), voiceVideoBtn: document.getElementById("voice-video-btn"), voiceScreenBtn: document.getElementById("voice-screen-btn"), + voiceFilterBtn: document.getElementById("voice-filter-btn"), voiceMuteBtn: document.getElementById("voice-mute-btn"), voiceLeaveBtn: document.getElementById("voice-leave-btn"), videoGrid: document.getElementById("video-grid"), @@ -703,6 +708,10 @@ function shouldInitiateOffer(peerId) { } function stopAndClearAudioPipeline() { + if (state.voice.deepFilterProcessor) { + state.voice.deepFilterProcessor.destroy(); + state.voice.deepFilterProcessor = null; + } if (state.voice.localStream) { for (const track of state.voice.localStream.getTracks()) track.stop(); } @@ -715,6 +724,8 @@ function stopAndClearAudioPipeline() { state.voice.localStream = null; state.voice.rawStream = null; state.voice.audioContext = null; + state.voice.activeNoiseFilter = 'off'; + updateVoiceFilterButton(); } function stopAndClearVideoPipeline() { @@ -786,11 +797,34 @@ async function buildAudioPipeline(rawStream) { const ctx = new AudioContext(); state.voice.audioContext = ctx; const source = ctx.createMediaStreamSource(rawStream); + let processedSource = source; + state.voice.activeNoiseFilter = 'off'; + + if (state.voice.noiseFilterMode === 'deepfilter') { + try { + const deepFilter = await getDeepFilterModule(); + const processor = new deepFilter.DeepFilterNet3Core({ + sampleRate: 48000, + noiseReductionLevel: 70, + }); + await processor.initialize(); + const workletNode = await processor.createAudioWorkletNode(ctx); + processor.setNoiseSuppressionEnabled(true); + state.voice.deepFilterProcessor = processor; + source.connect(workletNode); + processedSource = workletNode; + state.voice.activeNoiseFilter = 'deepfilter'; + } catch (err) { + console.warn("DeepFilterNet3 unavailable, falling back to raw mic audio", err); + state.voice.activeNoiseFilter = 'off'; + } + } + updateVoiceFilterButton(); // Metering/Speaking detection const analyser = ctx.createAnalyser(); analyser.fftSize = 512; - source.connect(analyser); + processedSource.connect(analyser); const dataArray = new Uint8Array(analyser.frequencyBinCount); let localIsSpeaking = false; @@ -833,17 +867,60 @@ async function buildAudioPipeline(rawStream) { checkVolume(); const destination = ctx.createMediaStreamDestination(); - source.connect(destination); + processedSource.connect(destination); return destination.stream; } +function resolveDeepFilterModuleUrl() { + if (location.protocol === 'file:') { + return new URL('vendor/deepfilternet3-noise-filter.esm.js', location.href).toString(); + } + return '/static/vendor/deepfilternet3-noise-filter.esm.js'; +} + +async function getDeepFilterModule() { + if (state.voice.deepFilterModule) return state.voice.deepFilterModule; + const moduleUrl = resolveDeepFilterModuleUrl(); + state.voice.deepFilterModule = await import(moduleUrl); + return state.voice.deepFilterModule; +} + +function updateVoiceFilterButton() { + if (!el.voiceFilterBtn) return; + const requestedMode = state.voice.noiseFilterMode === 'deepfilter' ? 'DeepFilterNet3' : 'Off'; + const activeMode = state.voice.activeNoiseFilter === 'deepfilter' ? 'DeepFilterNet3' : 'Off'; + const activeSuffix = requestedMode === activeMode ? '' : ` (active: ${activeMode})`; + el.voiceFilterBtn.title = `Noise Filter: ${requestedMode}${activeSuffix}`; + el.voiceFilterBtn.innerHTML = state.voice.noiseFilterMode === 'deepfilter' + ? '' + : ''; + el.voiceFilterBtn.style.color = state.voice.noiseFilterMode === 'deepfilter' + ? 'var(--green)' + : 'var(--text-muted)'; + lucide.createIcons(); +} + +async function toggleNoiseFilter() { + state.voice.noiseFilterMode = state.voice.noiseFilterMode === 'deepfilter' ? 'off' : 'deepfilter'; + updateVoiceFilterButton(); + if (!state.voice.joinedChannelId) return; + + try { + await leaveVoice(); + await joinVoice(); + } catch (err) { + console.error("failed to rejoin voice after filter change", err); + } +} + async function createLocalVoiceStream() { + const useDeepFilter = state.voice.noiseFilterMode === 'deepfilter'; const constraints = { audio: { channelCount: 1, sampleRate: 48000, echoCancellation: true, - noiseSuppression: true, + noiseSuppression: !useDeepFilter, autoGainControl: false, }, video: false, @@ -1455,8 +1532,10 @@ async function init() { el.voiceVideoBtn.onclick = toggleVideo; el.voiceScreenBtn.onclick = toggleScreenShare; + if (el.voiceFilterBtn) el.voiceFilterBtn.onclick = toggleNoiseFilter; el.voiceMuteBtn.onclick = toggleMute; el.voiceLeaveBtn.onclick = leaveVoice; + updateVoiceFilterButton(); el.addSoundBtn.onclick = () => { el.soundModal.classList.remove("hidden"); diff --git a/desktop/index.html b/desktop/index.html index c58c23d..c92f049 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -91,6 +91,7 @@