fix(denoise-tester): play models in both ears; match the call's DeepFilterNet strength #483
@@ -6,6 +6,7 @@ import {
|
||||
DenoiseNode,
|
||||
buildGateNode,
|
||||
buildModelNode,
|
||||
monoDownmix,
|
||||
readDb,
|
||||
sampleRateFor,
|
||||
} from '../../../utils/denoisePipeline';
|
||||
@@ -139,7 +140,7 @@ export function DenoiseTester({ model, useGate, gateThreshold, nativeNS }: Denoi
|
||||
inAnalyser.fftSize = 1024;
|
||||
source.connect(inAnalyser);
|
||||
|
||||
let head: AudioNode = source;
|
||||
let head: AudioNode = source.connect(monoDownmix(ctx));
|
||||
let gate: AudioWorkletNode | null = null;
|
||||
if (useGate) {
|
||||
gate = await buildGateNode(ctx, gateThreshold);
|
||||
@@ -308,10 +309,11 @@ export function DenoiseTester({ model, useGate, gateThreshold, nativeNS }: Denoi
|
||||
const ctx = new AudioContext({ sampleRate: sampleRateFor(playModel ?? 'rnnoise') });
|
||||
const source = ctx.createBufferSource();
|
||||
source.buffer = clip;
|
||||
const mono = source.connect(monoDownmix(ctx));
|
||||
let playGate: AudioWorkletNode | null = null;
|
||||
let playModelNode: DenoiseNode | null = null;
|
||||
if (playModel) {
|
||||
let head: AudioNode = source;
|
||||
let head: AudioNode = mono;
|
||||
if (useGate) {
|
||||
playGate = await buildGateNode(ctx, gateThreshold);
|
||||
head.connect(playGate);
|
||||
@@ -321,7 +323,7 @@ export function DenoiseTester({ model, useGate, gateThreshold, nativeNS }: Denoi
|
||||
head.connect(playModelNode.node);
|
||||
playModelNode.node.connect(ctx.destination);
|
||||
} else {
|
||||
source.connect(ctx.destination);
|
||||
mono.connect(ctx.destination);
|
||||
}
|
||||
// Superseded while the WASM/worklet loaded (another Play, a Stop, or the
|
||||
// panel unmounted)? Tear down this now-orphaned graph instead of storing
|
||||
|
||||
@@ -21,6 +21,20 @@ const BASE = `${import.meta.env.BASE_URL.replace(/\/+$/, '')}/public/element-cal
|
||||
*/
|
||||
export const sampleRateFor = (model: DenoiseModelId): number => (model === 'dtln' ? 16000 : 48000);
|
||||
|
||||
/**
|
||||
* A pass-through node that downmixes whatever feeds it to ONE channel. Every
|
||||
* model only processes channel 0, and a recorded clip often decodes as stereo
|
||||
* (Chrome's Opus/WebM), so without this RNNoise/Speex played the cleaned audio
|
||||
* in the left ear only. Lotus Call does the same (#24). A mono output is then
|
||||
* upmixed to both ears by the destination.
|
||||
*/
|
||||
export const monoDownmix = (ctx: BaseAudioContext): GainNode =>
|
||||
new GainNode(ctx, {
|
||||
channelCount: 1,
|
||||
channelCountMode: 'explicit',
|
||||
channelInterpretation: 'speakers',
|
||||
});
|
||||
|
||||
export type DenoiseNode = {
|
||||
node: AudioWorkletNode;
|
||||
dispose: () => void;
|
||||
@@ -85,7 +99,10 @@ export async function buildModelNode(
|
||||
const mod: any = await import(/* @vite-ignore */ `${BASE}deepfilternet/index.esm.js`);
|
||||
const core = new mod.DeepFilterNet3Core({
|
||||
sampleRate: sampleRateFor(model),
|
||||
noiseReductionLevel: 80,
|
||||
// Same attenuation limit as Lotus Call (dB). Higher values mute the
|
||||
// background outright, so words drop into dead silence and sound
|
||||
// clipped at the end.
|
||||
noiseReductionLevel: 25,
|
||||
assetConfig: { cdnUrl: dfnBase },
|
||||
});
|
||||
await core.initialize();
|
||||
@@ -105,6 +122,8 @@ export async function buildModelNode(
|
||||
const [, wasmBinary] = await Promise.all([addModuleOnce(ctx, cfg.script), fetchWasm(cfg.wasm)]);
|
||||
const node = new AudioWorkletNode(ctx, cfg.proc, {
|
||||
channelCount: 1,
|
||||
channelCountMode: 'explicit',
|
||||
channelInterpretation: 'speakers',
|
||||
numberOfInputs: 1,
|
||||
numberOfOutputs: 1,
|
||||
processorOptions: { maxChannels: 1, wasmBinary },
|
||||
@@ -127,6 +146,9 @@ export async function buildGateNode(
|
||||
): Promise<AudioWorkletNode> {
|
||||
await addModuleOnce(ctx, 'noiseGateWorklet.js');
|
||||
return new AudioWorkletNode(ctx, '@sapphi-red/web-noise-suppressor/noise-gate', {
|
||||
channelCount: 1,
|
||||
channelCountMode: 'explicit',
|
||||
channelInterpretation: 'speakers',
|
||||
processorOptions: {
|
||||
openThreshold: thresholdDb,
|
||||
closeThreshold: thresholdDb - 5,
|
||||
|
||||
Reference in New Issue
Block a user