dnn3
All checks were successful
/ upload (release) Successful in 3m17s

This commit is contained in:
pavel 2026-02-25 18:29:19 +01:00
commit e954433289
6 changed files with 665 additions and 7 deletions

View file

@ -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" }],
},
@ -70,6 +74,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"),
@ -651,6 +656,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();
}
@ -663,6 +672,8 @@ function stopAndClearAudioPipeline() {
state.voice.localStream = null;
state.voice.rawStream = null;
state.voice.audioContext = null;
state.voice.activeNoiseFilter = 'off';
updateVoiceFilterButton();
}
function stopAndClearVideoPipeline() {
@ -730,15 +741,80 @@ function renderVideo(peerId, displayName, stream, source) {
videoEl.srcObject = 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'
? '<i data-lucide="filter"></i>'
: '<i data-lucide="filter-x"></i>';
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 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;
@ -781,17 +857,18 @@ async function buildAudioPipeline(rawStream) {
checkVolume();
const destination = ctx.createMediaStreamDestination();
source.connect(destination);
processedSource.connect(destination);
return destination.stream;
}
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,
@ -1398,8 +1475,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");