From f2bb9e7d73f0e13145fa47d00a5152613f5f9bd9 Mon Sep 17 00:00:00 2001 From: fkwp Date: Tue, 7 Apr 2026 12:10:02 +0200 Subject: [PATCH] tests for DeepFilterNet Noise reduction --- .../NoiseSuppressionTransformer.test.ts | 103 +++++++++++++++ .../audioTrackNoiseSuppressionSync.test.ts | 120 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 src/livekit/NoiseSuppressionTransformer.test.ts create mode 100644 src/livekit/audioTrackNoiseSuppressionSync.test.ts diff --git a/src/livekit/NoiseSuppressionTransformer.test.ts b/src/livekit/NoiseSuppressionTransformer.test.ts new file mode 100644 index 00000000..0a42b845 --- /dev/null +++ b/src/livekit/NoiseSuppressionTransformer.test.ts @@ -0,0 +1,103 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("deepfilternet3-noise-filter", () => { + const setEnabled = vi.fn(); + const setSuppressionLevel = vi.fn(); + const destroy = vi.fn(); + + function DeepFilterNoiseFilterProcessor(this: any, options: any) { + Object.assign(this, options); + this.setEnabled = setEnabled; + this.setSuppressionLevel = setSuppressionLevel; + this.destroy = destroy; + } + + return { + __esModule: true, + DeepFilterNoiseFilterProcessor: vi.fn().mockImplementation(DeepFilterNoiseFilterProcessor), + __setEnabledSpy: setEnabled, + __setSuppressionLevelSpy: setSuppressionLevel, + __destroySpy: destroy, + }; +}); + +import { NoiseSuppressionTransformer } from "./NoiseSuppressionTransformer"; +import { + DeepFilterNoiseFilterProcessor, + __setEnabledSpy as mockSetEnabled, + __setSuppressionLevelSpy as mockSetSuppressionLevel, + __destroySpy as mockDestroy, +} from "deepfilternet3-noise-filter"; + +const mockDeepFilterNoiseFilterProcessor = vi.mocked(DeepFilterNoiseFilterProcessor); + +describe("NoiseSuppressionTransformer", () => { + beforeEach(() => { + mockSetEnabled.mockClear(); + mockSetSuppressionLevel.mockClear(); + mockDestroy.mockClear(); + mockDeepFilterNoiseFilterProcessor.mockClear(); + }); + + it("initializes the underlying processor with the expected configuration", () => { + const transformer = new NoiseSuppressionTransformer(); + + transformer.initialize(0.5, false); + + expect(mockDeepFilterNoiseFilterProcessor).toHaveBeenCalledTimes(1); + expect(mockDeepFilterNoiseFilterProcessor).toHaveBeenCalledWith( + expect.objectContaining({ + sampleRate: 48000, + noiseReductionLevel: 50, + enabled: false, + assetConfig: expect.objectContaining({ + cdnUrl: expect.any(String), + }), + }), + ); + + expect(transformer.getProcessor()).not.toBeNull(); + }); + + it("does not initialize twice", () => { + const transformer = new NoiseSuppressionTransformer(); + + transformer.initialize(0.3, true); + transformer.initialize(0.7, false); + + expect(mockDeepFilterNoiseFilterProcessor).toHaveBeenCalledTimes(1); + expect(transformer.getProcessor()).not.toBeNull(); + }); + + it("forwards suppression level changes and clamps out-of-range values", () => { + const transformer = new NoiseSuppressionTransformer(); + transformer.initialize(0.2, true); + + transformer.setSuppressionLevel(1.5); + transformer.setSuppressionLevel(-0.2); + + expect(mockSetSuppressionLevel).toHaveBeenNthCalledWith(1, 100); + expect(mockSetSuppressionLevel).toHaveBeenNthCalledWith(2, 0); + }); + + it("forwards enabled state changes to the underlying processor", () => { + const transformer = new NoiseSuppressionTransformer(); + transformer.initialize(0.4, true); + + transformer.setEnabled(false); + transformer.setEnabled(true); + + expect(mockSetEnabled).toHaveBeenNthCalledWith(1, false); + expect(mockSetEnabled).toHaveBeenNthCalledWith(2, true); + }); + + it("destroys the processor and resets internal state", () => { + const transformer = new NoiseSuppressionTransformer(); + transformer.initialize(0.6, true); + + transformer.destroy(); + + expect(mockDestroy).toHaveBeenCalledTimes(1); + expect(transformer.getProcessor()).toBeNull(); + }); +}); diff --git a/src/livekit/audioTrackNoiseSuppressionSync.test.ts b/src/livekit/audioTrackNoiseSuppressionSync.test.ts new file mode 100644 index 00000000..f6b862d7 --- /dev/null +++ b/src/livekit/audioTrackNoiseSuppressionSync.test.ts @@ -0,0 +1,120 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { BehaviorSubject } from "rxjs"; + +const localStorageMock = { + getItem: vi.fn(() => null), + setItem: vi.fn(() => {}), + removeItem: vi.fn(() => {}), + clear: vi.fn(() => {}), +}; + +Object.defineProperty(globalThis, "localStorage", { + value: localStorageMock, + configurable: true, + writable: true, +}); + +vi.mock("deepfilternet3-noise-filter", () => { + const setEnabled = vi.fn(); + const setSuppressionLevel = vi.fn(); + const destroy = vi.fn(); + + function DeepFilterNoiseFilterProcessor(this: any, options: any) { + Object.assign(this, options); + this.setEnabled = setEnabled; + this.setSuppressionLevel = setSuppressionLevel; + this.destroy = destroy; + } + + return { + __esModule: true, + DeepFilterNoiseFilterProcessor: vi.fn().mockImplementation(DeepFilterNoiseFilterProcessor), + __setEnabledSpy: setEnabled, + __setSuppressionLevelSpy: setSuppressionLevel, + __destroySpy: destroy, + }; +}); + +import { ObservableScope } from "../state/ObservableScope"; +import type { LocalAudioTrack } from "livekit-client"; +import type { Behavior } from "../state/Behavior"; +import { + __setEnabledSpy as mockSetEnabled, + __setSuppressionLevelSpy as mockSetSuppressionLevel, + __destroySpy as mockDestroy, +} from "deepfilternet3-noise-filter"; + +let audioTrackNoiseSuppressionSync: typeof import("./audioTrackNoiseSuppressionSync").audioTrackNoiseSuppressionSync; +let noiseSuppressionEnabled: typeof import("../settings/settings").noiseSuppressionEnabled; +let noiseSuppressionLevel: typeof import("../settings/settings").noiseSuppressionLevel; + +class MockLocalAudioTrack { + private processor: unknown = undefined; + public readonly setProcessor = vi.fn(async (processor: unknown) => { + this.processor = processor; + }); + public readonly getProcessor = vi.fn(() => this.processor); + public readonly stopProcessor = vi.fn(async () => { + this.processor = undefined; + }); +} + +describe("audioTrackNoiseSuppressionSync", () => { + let scope: ObservableScope; + let audioTrack$: Behavior; + let track: MockLocalAudioTrack; + + beforeEach(async () => { + mockSetEnabled.mockClear(); + mockSetSuppressionLevel.mockClear(); + mockDestroy.mockClear(); + track = new MockLocalAudioTrack(); + audioTrack$ = new BehaviorSubject(track as unknown as LocalAudioTrack); + const settingsModule = await import("../settings/settings"); + noiseSuppressionEnabled = settingsModule.noiseSuppressionEnabled; + noiseSuppressionLevel = settingsModule.noiseSuppressionLevel; + const syncModule = await import("./audioTrackNoiseSuppressionSync"); + audioTrackNoiseSuppressionSync = syncModule.audioTrackNoiseSuppressionSync; + noiseSuppressionEnabled.setValue(true); + noiseSuppressionLevel.setValue(0.75); + scope = new ObservableScope(); + }); + + afterEach(async () => { + scope.end(); + await Promise.resolve(); + }); + + it("sets the processor on the audio track and updates the processor settings", async () => { + audioTrackNoiseSuppressionSync(scope, audioTrack$); + await Promise.resolve(); + + expect(track.setProcessor).toHaveBeenCalledTimes(1); + expect(track.getProcessor()).not.toBeUndefined(); + expect(mockSetEnabled).toHaveBeenCalledWith(false); + expect(mockSetSuppressionLevel).toHaveBeenCalledWith(75); + }); + + it("reapplies processor when audio track becomes available", async () => { + audioTrack$ = new BehaviorSubject(null); + audioTrackNoiseSuppressionSync(scope, audioTrack$); + await Promise.resolve(); + + expect(track.setProcessor).toHaveBeenCalledTimes(0); + + audioTrack$.next(track as unknown as LocalAudioTrack); + await Promise.resolve(); + + expect(track.setProcessor).toHaveBeenCalledTimes(1); + }); + + it("destroys the transformer when the scope ends", async () => { + audioTrackNoiseSuppressionSync(scope, audioTrack$); + await Promise.resolve(); + + scope.end(); + await Promise.resolve(); + + expect(mockDestroy).toHaveBeenCalledTimes(1); + }); +});