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, muted: false,
sharingVideo: false, sharingVideo: false,
sharingScreen: false, sharingScreen: false,
noiseFilterMode: 'deepfilter',
activeNoiseFilter: 'off',
deepFilterProcessor: null,
deepFilterModule: null,
viewMode: 'chat', // 'chat' or 'video' viewMode: 'chat', // 'chat' or 'video'
iceServers: [{ urls: "stun:stun.l.google.com:19302" }], iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
}, },
@ -114,6 +118,7 @@ const el = {
vcChannelName: document.getElementById("vc-channel-name"), vcChannelName: document.getElementById("vc-channel-name"),
voiceVideoBtn: document.getElementById("voice-video-btn"), voiceVideoBtn: document.getElementById("voice-video-btn"),
voiceScreenBtn: document.getElementById("voice-screen-btn"), voiceScreenBtn: document.getElementById("voice-screen-btn"),
voiceFilterBtn: document.getElementById("voice-filter-btn"),
voiceMuteBtn: document.getElementById("voice-mute-btn"), voiceMuteBtn: document.getElementById("voice-mute-btn"),
voiceLeaveBtn: document.getElementById("voice-leave-btn"), voiceLeaveBtn: document.getElementById("voice-leave-btn"),
videoGrid: document.getElementById("video-grid"), videoGrid: document.getElementById("video-grid"),
@ -703,6 +708,10 @@ function shouldInitiateOffer(peerId) {
} }
function stopAndClearAudioPipeline() { function stopAndClearAudioPipeline() {
if (state.voice.deepFilterProcessor) {
state.voice.deepFilterProcessor.destroy();
state.voice.deepFilterProcessor = null;
}
if (state.voice.localStream) { if (state.voice.localStream) {
for (const track of state.voice.localStream.getTracks()) track.stop(); for (const track of state.voice.localStream.getTracks()) track.stop();
} }
@ -715,6 +724,8 @@ function stopAndClearAudioPipeline() {
state.voice.localStream = null; state.voice.localStream = null;
state.voice.rawStream = null; state.voice.rawStream = null;
state.voice.audioContext = null; state.voice.audioContext = null;
state.voice.activeNoiseFilter = 'off';
updateVoiceFilterButton();
} }
function stopAndClearVideoPipeline() { function stopAndClearVideoPipeline() {
@ -786,11 +797,34 @@ async function buildAudioPipeline(rawStream) {
const ctx = new AudioContext(); const ctx = new AudioContext();
state.voice.audioContext = ctx; state.voice.audioContext = ctx;
const source = ctx.createMediaStreamSource(rawStream); 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 // Metering/Speaking detection
const analyser = ctx.createAnalyser(); const analyser = ctx.createAnalyser();
analyser.fftSize = 512; analyser.fftSize = 512;
source.connect(analyser); processedSource.connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount); const dataArray = new Uint8Array(analyser.frequencyBinCount);
let localIsSpeaking = false; let localIsSpeaking = false;
@ -833,17 +867,60 @@ async function buildAudioPipeline(rawStream) {
checkVolume(); checkVolume();
const destination = ctx.createMediaStreamDestination(); const destination = ctx.createMediaStreamDestination();
source.connect(destination); processedSource.connect(destination);
return destination.stream; 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'
? '<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 createLocalVoiceStream() { async function createLocalVoiceStream() {
const useDeepFilter = state.voice.noiseFilterMode === 'deepfilter';
const constraints = { const constraints = {
audio: { audio: {
channelCount: 1, channelCount: 1,
sampleRate: 48000, sampleRate: 48000,
echoCancellation: true, echoCancellation: true,
noiseSuppression: true, noiseSuppression: !useDeepFilter,
autoGainControl: false, autoGainControl: false,
}, },
video: false, video: false,
@ -1455,8 +1532,10 @@ async function init() {
el.voiceVideoBtn.onclick = toggleVideo; el.voiceVideoBtn.onclick = toggleVideo;
el.voiceScreenBtn.onclick = toggleScreenShare; el.voiceScreenBtn.onclick = toggleScreenShare;
if (el.voiceFilterBtn) el.voiceFilterBtn.onclick = toggleNoiseFilter;
el.voiceMuteBtn.onclick = toggleMute; el.voiceMuteBtn.onclick = toggleMute;
el.voiceLeaveBtn.onclick = leaveVoice; el.voiceLeaveBtn.onclick = leaveVoice;
updateVoiceFilterButton();
el.addSoundBtn.onclick = () => { el.addSoundBtn.onclick = () => {
el.soundModal.classList.remove("hidden"); el.soundModal.classList.remove("hidden");

View file

@ -91,6 +91,7 @@
<div class="vc-actions"> <div class="vc-actions">
<button id="voice-video-btn" title="Turn on Camera"><i data-lucide="video-off"></i></button> <button id="voice-video-btn" title="Turn on Camera"><i data-lucide="video-off"></i></button>
<button id="voice-screen-btn" title="Share Screen"><i data-lucide="monitor-off"></i></button> <button id="voice-screen-btn" title="Share Screen"><i data-lucide="monitor-off"></i></button>
<button id="voice-filter-btn" title="Noise Filter"><i data-lucide="filter"></i></button>
<button id="voice-mute-btn" title="Mute"><i data-lucide="mic"></i></button> <button id="voice-mute-btn" title="Mute"><i data-lucide="mic"></i></button>
<button id="voice-leave-btn" title="Disconnect"><i data-lucide="phone-off"></i></button> <button id="voice-leave-btn" title="Disconnect"><i data-lucide="phone-off"></i></button>
</div> </div>
@ -263,4 +264,4 @@
<script src="app.js" defer></script> <script src="app.js" defer></script>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

View file

@ -23,6 +23,10 @@ const state = {
muted: false, muted: false,
sharingVideo: false, sharingVideo: false,
sharingScreen: false, sharingScreen: false,
noiseFilterMode: 'deepfilter',
activeNoiseFilter: 'off',
deepFilterProcessor: null,
deepFilterModule: null,
viewMode: 'chat', // 'chat' or 'video' viewMode: 'chat', // 'chat' or 'video'
iceServers: [{ urls: "stun:stun.l.google.com:19302" }], iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
}, },
@ -70,6 +74,7 @@ const el = {
vcChannelName: document.getElementById("vc-channel-name"), vcChannelName: document.getElementById("vc-channel-name"),
voiceVideoBtn: document.getElementById("voice-video-btn"), voiceVideoBtn: document.getElementById("voice-video-btn"),
voiceScreenBtn: document.getElementById("voice-screen-btn"), voiceScreenBtn: document.getElementById("voice-screen-btn"),
voiceFilterBtn: document.getElementById("voice-filter-btn"),
voiceMuteBtn: document.getElementById("voice-mute-btn"), voiceMuteBtn: document.getElementById("voice-mute-btn"),
voiceLeaveBtn: document.getElementById("voice-leave-btn"), voiceLeaveBtn: document.getElementById("voice-leave-btn"),
videoGrid: document.getElementById("video-grid"), videoGrid: document.getElementById("video-grid"),
@ -651,6 +656,10 @@ function shouldInitiateOffer(peerId) {
} }
function stopAndClearAudioPipeline() { function stopAndClearAudioPipeline() {
if (state.voice.deepFilterProcessor) {
state.voice.deepFilterProcessor.destroy();
state.voice.deepFilterProcessor = null;
}
if (state.voice.localStream) { if (state.voice.localStream) {
for (const track of state.voice.localStream.getTracks()) track.stop(); for (const track of state.voice.localStream.getTracks()) track.stop();
} }
@ -663,6 +672,8 @@ function stopAndClearAudioPipeline() {
state.voice.localStream = null; state.voice.localStream = null;
state.voice.rawStream = null; state.voice.rawStream = null;
state.voice.audioContext = null; state.voice.audioContext = null;
state.voice.activeNoiseFilter = 'off';
updateVoiceFilterButton();
} }
function stopAndClearVideoPipeline() { function stopAndClearVideoPipeline() {
@ -730,15 +741,80 @@ function renderVideo(peerId, displayName, stream, source) {
videoEl.srcObject = stream; 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) { async function buildAudioPipeline(rawStream) {
const ctx = new AudioContext(); const ctx = new AudioContext();
state.voice.audioContext = ctx; state.voice.audioContext = ctx;
const source = ctx.createMediaStreamSource(rawStream); 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 // Metering/Speaking detection
const analyser = ctx.createAnalyser(); const analyser = ctx.createAnalyser();
analyser.fftSize = 512; analyser.fftSize = 512;
source.connect(analyser); processedSource.connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount); const dataArray = new Uint8Array(analyser.frequencyBinCount);
let localIsSpeaking = false; let localIsSpeaking = false;
@ -781,17 +857,18 @@ async function buildAudioPipeline(rawStream) {
checkVolume(); checkVolume();
const destination = ctx.createMediaStreamDestination(); const destination = ctx.createMediaStreamDestination();
source.connect(destination); processedSource.connect(destination);
return destination.stream; return destination.stream;
} }
async function createLocalVoiceStream() { async function createLocalVoiceStream() {
const useDeepFilter = state.voice.noiseFilterMode === 'deepfilter';
const constraints = { const constraints = {
audio: { audio: {
channelCount: 1, channelCount: 1,
sampleRate: 48000, sampleRate: 48000,
echoCancellation: true, echoCancellation: true,
noiseSuppression: true, noiseSuppression: !useDeepFilter,
autoGainControl: false, autoGainControl: false,
}, },
video: false, video: false,
@ -1398,8 +1475,10 @@ async function init() {
el.voiceVideoBtn.onclick = toggleVideo; el.voiceVideoBtn.onclick = toggleVideo;
el.voiceScreenBtn.onclick = toggleScreenShare; el.voiceScreenBtn.onclick = toggleScreenShare;
if (el.voiceFilterBtn) el.voiceFilterBtn.onclick = toggleNoiseFilter;
el.voiceMuteBtn.onclick = toggleMute; el.voiceMuteBtn.onclick = toggleMute;
el.voiceLeaveBtn.onclick = leaveVoice; el.voiceLeaveBtn.onclick = leaveVoice;
updateVoiceFilterButton();
el.addSoundBtn.onclick = () => { el.addSoundBtn.onclick = () => {
el.soundModal.classList.remove("hidden"); el.soundModal.classList.remove("hidden");

View file

@ -96,6 +96,7 @@
<div class="vc-actions"> <div class="vc-actions">
<button id="voice-video-btn" title="Turn on Camera"><i data-lucide="video-off"></i></button> <button id="voice-video-btn" title="Turn on Camera"><i data-lucide="video-off"></i></button>
<button id="voice-screen-btn" title="Share Screen"><i data-lucide="monitor-off"></i></button> <button id="voice-screen-btn" title="Share Screen"><i data-lucide="monitor-off"></i></button>
<button id="voice-filter-btn" title="Noise Filter"><i data-lucide="filter"></i></button>
<button id="voice-mute-btn" title="Mute"><i data-lucide="mic"></i></button> <button id="voice-mute-btn" title="Mute"><i data-lucide="mic"></i></button>
<button id="voice-leave-btn" title="Disconnect"><i data-lucide="phone-off"></i></button> <button id="voice-leave-btn" title="Disconnect"><i data-lucide="phone-off"></i></button>
</div> </div>

File diff suppressed because one or more lines are too long