parent
babbb33f69
commit
e954433289
6 changed files with 665 additions and 7 deletions
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
249
desktop/vendor/deepfilternet3-noise-filter.esm.js
vendored
Normal file
249
desktop/vendor/deepfilternet3-noise-filter.esm.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
249
static/vendor/deepfilternet3-noise-filter.esm.js
vendored
Normal file
249
static/vendor/deepfilternet3-noise-filter.esm.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue