feat(lotus-denoise): quality — dry/wet attenuation floor, gate-after-ML, softer DFN
CI / Build embedded bundle (push) Successful in 1m38s
CI / Publish to Gitea npm registry (push) Has been skipped

Track-B audio-quality changes to reduce the "robotic/underwater" artifact.

- Dry/wet attenuation floor (default 0.15 ≈ -16 dB) blends a little of the raw
  mic under the denoised signal so suppression can't fully collapse the noise
  floor between words (the main cause of the RNNoise "underwater"/pumping
  sound). Applied ONLY to the low-latency flat models (RNNoise/Speex); DTLN/DFN
  add algorithmic latency that would comb-filter an undelayed dry mix, so they
  rely on their own level instead. Tunable via `lotusDenoiseFloor`.
- Noise gate now runs AFTER the ML model, not before — gating the raw signal
  fed hard-zeroed frames into the model and tuned the threshold on pre-denoise
  levels.
- DeepFilterNet 3 noiseReductionLevel 80 -> 60: full strength was the main
  "over-processed" contributor; 60 keeps voice natural.

Defaults are conservative and tunable; final values are meant to be dialed in
with real-call A/B listening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Lotus CI
2026-07-01 00:16:23 -04:00
co-authored by Claude Opus 4.8
parent 9a4e9bf7da
commit 6ab52d9926
2 changed files with 62 additions and 10 deletions
+8
View File
@@ -73,6 +73,7 @@ export function startLotusDenoise(vm: CallViewModel): () => void {
: "rnnoise";
const rawThreshold = lotusParam("lotusGateThreshold");
const rawFloor = lotusParam("lotusDenoiseFloor");
const config: LotusDenoiseConfig = {
model,
assetBase: safeAssetBase(lotusParam("lotusDenoiseBase")),
@@ -83,6 +84,13 @@ export function startLotusDenoise(vm: CallViewModel): () => void {
rawThreshold !== null && Number.isFinite(Number(rawThreshold))
? Number(rawThreshold)
: -45,
// Dry/wet attenuation floor. Default 0.15 (~-16 dB) tames the
// over-suppression "underwater"/pumping artifact; host can tune via
// `lotusDenoiseFloor` (0 = full suppression, no floor).
floor:
rawFloor !== null && Number.isFinite(Number(rawFloor))
? Math.min(0.5, Math.max(0, Number(rawFloor)))
: 0.15,
};
const micOf = (room: LivekitRoom): LocalAudioTrack | undefined =>