Compare commits
2 Commits
call_noise
...
rnnoise
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0788e56c51 | ||
|
|
411e18c48a |
@@ -25,6 +25,9 @@ interface NoiseGateParams {
|
|||||||
attackMs: number;
|
attackMs: number;
|
||||||
holdMs: number;
|
holdMs: number;
|
||||||
releaseMs: number;
|
releaseMs: number;
|
||||||
|
transientEnabled: boolean;
|
||||||
|
transientThresholdDb: number; // dB above background RMS that triggers suppression
|
||||||
|
transientReleaseMs: number; // how quickly suppression fades after transient ends
|
||||||
}
|
}
|
||||||
|
|
||||||
function dbToLinear(db: number): number {
|
function dbToLinear(db: number): number {
|
||||||
@@ -32,21 +35,36 @@ function dbToLinear(db: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AudioWorkletProcessor implementing a noise gate.
|
* AudioWorkletProcessor implementing a noise gate and an optional transient
|
||||||
|
* suppressor, both running per-sample in a single pass.
|
||||||
*
|
*
|
||||||
* Per-sample peak detection across all channels. Gate opens when the
|
* Noise gate: opens when instantaneous peak exceeds threshold, closes below.
|
||||||
* instantaneous peak exceeds the threshold and closes when it drops below.
|
|
||||||
* Attack, hold, and release times smooth the attenuation envelope.
|
* Attack, hold, and release times smooth the attenuation envelope.
|
||||||
|
*
|
||||||
|
* Transient suppressor: tracks a slow-moving RMS background level. When the
|
||||||
|
* instantaneous peak exceeds the background by more than transientThresholdDb,
|
||||||
|
* gain is instantly cut to 0 and releases over transientReleaseMs. This catches
|
||||||
|
* desk hits, mic bumps, and other sudden loud impacts without affecting speech.
|
||||||
*/
|
*/
|
||||||
class NoiseGateProcessor extends AudioWorkletProcessor {
|
class NoiseGateProcessor extends AudioWorkletProcessor {
|
||||||
|
// Noise gate state
|
||||||
private threshold = dbToLinear(-60);
|
private threshold = dbToLinear(-60);
|
||||||
private attackRate = 1.0 / (0.025 * 48000);
|
private attackRate = 1.0 / (0.025 * sampleRate);
|
||||||
private releaseRate = 1.0 / (0.15 * 48000);
|
private releaseRate = 1.0 / (0.15 * sampleRate);
|
||||||
private holdTime = 0.2;
|
private holdTime = 0.2;
|
||||||
|
|
||||||
private isOpen = false;
|
private isOpen = false;
|
||||||
private attenuation = 0;
|
private gateAttenuation = 0;
|
||||||
private heldTime = 0;
|
private heldTime = 0;
|
||||||
|
|
||||||
|
// Transient suppressor state
|
||||||
|
private transientEnabled = false;
|
||||||
|
private transientRatio = dbToLinear(15); // peak must exceed rms by this factor
|
||||||
|
private transientReleaseRate = 1.0 / (0.08 * sampleRate);
|
||||||
|
private transientAttenuation = 1.0; // 1 = fully open, ramps to 0 on transient
|
||||||
|
private slowRms = 0;
|
||||||
|
// Exponential smoothing coefficient for background RMS (~200ms time constant)
|
||||||
|
private rmsCoeff = Math.exp(-1.0 / (0.2 * sampleRate));
|
||||||
|
|
||||||
private logCounter = 0;
|
private logCounter = 0;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
@@ -54,7 +72,10 @@ class NoiseGateProcessor extends AudioWorkletProcessor {
|
|||||||
this.port.onmessage = (e: MessageEvent<NoiseGateParams>): void => {
|
this.port.onmessage = (e: MessageEvent<NoiseGateParams>): void => {
|
||||||
this.updateParams(e.data);
|
this.updateParams(e.data);
|
||||||
};
|
};
|
||||||
this.updateParams({ threshold: -60, attackMs: 25, holdMs: 200, releaseMs: 150 });
|
this.updateParams({
|
||||||
|
threshold: -60, attackMs: 25, holdMs: 200, releaseMs: 150,
|
||||||
|
transientEnabled: false, transientThresholdDb: 15, transientReleaseMs: 80,
|
||||||
|
});
|
||||||
this.port.postMessage({ type: "log", msg: "[NoiseGate worklet] constructor called, sampleRate=" + sampleRate });
|
this.port.postMessage({ type: "log", msg: "[NoiseGate worklet] constructor called, sampleRate=" + sampleRate });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,12 +84,14 @@ class NoiseGateProcessor extends AudioWorkletProcessor {
|
|||||||
this.attackRate = 1.0 / ((p.attackMs / 1000) * sampleRate);
|
this.attackRate = 1.0 / ((p.attackMs / 1000) * sampleRate);
|
||||||
this.releaseRate = 1.0 / ((p.releaseMs / 1000) * sampleRate);
|
this.releaseRate = 1.0 / ((p.releaseMs / 1000) * sampleRate);
|
||||||
this.holdTime = p.holdMs / 1000;
|
this.holdTime = p.holdMs / 1000;
|
||||||
|
this.transientEnabled = p.transientEnabled;
|
||||||
|
this.transientRatio = dbToLinear(p.transientThresholdDb);
|
||||||
|
this.transientReleaseRate = 1.0 / ((p.transientReleaseMs / 1000) * sampleRate);
|
||||||
this.port.postMessage({
|
this.port.postMessage({
|
||||||
type: "log",
|
type: "log",
|
||||||
msg: "[NoiseGate worklet] params: threshold=" + this.threshold.toFixed(5)
|
msg: "[NoiseGate worklet] params updated: threshold=" + p.threshold
|
||||||
+ " attack=" + this.attackRate.toFixed(8)
|
+ " transientEnabled=" + p.transientEnabled
|
||||||
+ " release=" + this.releaseRate.toFixed(8)
|
+ " transientThresholdDb=" + p.transientThresholdDb,
|
||||||
+ " hold=" + this.holdTime,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +102,7 @@ class NoiseGateProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
const channels = input.length;
|
const channels = input.length;
|
||||||
const blockSize = input[0]?.length ?? 128;
|
const blockSize = input[0]?.length ?? 128;
|
||||||
const sampleRateI = 1.0 / sampleRate;
|
const samplePeriod = 1.0 / sampleRate;
|
||||||
|
|
||||||
for (let i = 0; i < blockSize; i++) {
|
for (let i = 0; i < blockSize; i++) {
|
||||||
// Peak detection across all channels
|
// Peak detection across all channels
|
||||||
@@ -88,32 +111,50 @@ class NoiseGateProcessor extends AudioWorkletProcessor {
|
|||||||
curLevel = Math.max(curLevel, Math.abs(input[j]?.[i] ?? 0));
|
curLevel = Math.max(curLevel, Math.abs(input[j]?.[i] ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open gate when signal exceeds threshold
|
// --- Transient suppressor ---
|
||||||
|
let transientGain = 1.0;
|
||||||
|
if (this.transientEnabled) {
|
||||||
|
// Update slow RMS background (exponential moving average of energy)
|
||||||
|
this.slowRms = Math.sqrt(
|
||||||
|
this.rmsCoeff * this.slowRms * this.slowRms +
|
||||||
|
(1.0 - this.rmsCoeff) * curLevel * curLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
const background = Math.max(this.slowRms, 1e-6);
|
||||||
|
if (curLevel > background * this.transientRatio) {
|
||||||
|
// Transient detected — instantly cut gain
|
||||||
|
this.transientAttenuation = 0.0;
|
||||||
|
} else {
|
||||||
|
// Release: ramp back toward 1
|
||||||
|
this.transientAttenuation = Math.min(1.0, this.transientAttenuation + this.transientReleaseRate);
|
||||||
|
}
|
||||||
|
transientGain = this.transientAttenuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Noise gate ---
|
||||||
if (curLevel > this.threshold && !this.isOpen) {
|
if (curLevel > this.threshold && !this.isOpen) {
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close gate when signal drops below threshold; hold timer starts
|
|
||||||
if (curLevel <= this.threshold && this.isOpen) {
|
if (curLevel <= this.threshold && this.isOpen) {
|
||||||
this.heldTime = 0;
|
this.heldTime = 0;
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ramp attenuation toward 1 (open) or wait for hold then ramp toward 0 (closed)
|
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
this.attenuation = Math.min(1.0, this.attenuation + this.attackRate);
|
this.gateAttenuation = Math.min(1.0, this.gateAttenuation + this.attackRate);
|
||||||
} else {
|
} else {
|
||||||
this.heldTime += sampleRateI;
|
this.heldTime += samplePeriod;
|
||||||
if (this.heldTime > this.holdTime) {
|
if (this.heldTime > this.holdTime) {
|
||||||
this.attenuation = Math.max(0.0, this.attenuation - this.releaseRate);
|
this.gateAttenuation = Math.max(0.0, this.gateAttenuation - this.releaseRate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gain = this.gateAttenuation * transientGain;
|
||||||
|
|
||||||
for (let c = 0; c < output.length; c++) {
|
for (let c = 0; c < output.length; c++) {
|
||||||
const inCh = input[c] ?? input[0];
|
const inCh = input[c] ?? input[0];
|
||||||
const outCh = output[c];
|
const outCh = output[c];
|
||||||
if (inCh && outCh) {
|
if (inCh && outCh) {
|
||||||
outCh[i] = (inCh[i] ?? 0) * this.attenuation;
|
outCh[i] = (inCh[i] ?? 0) * gain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,9 +163,10 @@ class NoiseGateProcessor extends AudioWorkletProcessor {
|
|||||||
if (this.logCounter % 375 === 0) {
|
if (this.logCounter % 375 === 0) {
|
||||||
this.port.postMessage({
|
this.port.postMessage({
|
||||||
type: "log",
|
type: "log",
|
||||||
msg: "[NoiseGate worklet] isOpen=" + this.isOpen
|
msg: "[NoiseGate worklet] gateOpen=" + this.isOpen
|
||||||
+ " attenuation=" + this.attenuation.toFixed(3)
|
+ " gateAtten=" + this.gateAttenuation.toFixed(3)
|
||||||
+ " heldTime=" + this.heldTime.toFixed(3),
|
+ " transientAtten=" + this.transientAttenuation.toFixed(3)
|
||||||
|
+ " slowRms=" + this.slowRms.toFixed(5),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export interface NoiseGateParams {
|
|||||||
attackMs: number;
|
attackMs: number;
|
||||||
holdMs: number;
|
holdMs: number;
|
||||||
releaseMs: number;
|
releaseMs: number;
|
||||||
|
transientEnabled: boolean;
|
||||||
|
transientThresholdDb: number; // dB above background RMS that triggers suppression
|
||||||
|
transientReleaseMs: number; // ms for suppression to fade after transient ends
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ import {
|
|||||||
noiseGateAttack as noiseGateAttackSetting,
|
noiseGateAttack as noiseGateAttackSetting,
|
||||||
noiseGateHold as noiseGateHoldSetting,
|
noiseGateHold as noiseGateHoldSetting,
|
||||||
noiseGateRelease as noiseGateReleaseSetting,
|
noiseGateRelease as noiseGateReleaseSetting,
|
||||||
|
transientSuppressorEnabled as transientSuppressorEnabledSetting,
|
||||||
|
transientThreshold as transientThresholdSetting,
|
||||||
|
transientRelease as transientReleaseSetting,
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
||||||
import { Slider } from "../Slider";
|
import { Slider } from "../Slider";
|
||||||
@@ -126,6 +129,20 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
|
|
||||||
const [showAdvancedGate, setShowAdvancedGate] = useState(false);
|
const [showAdvancedGate, setShowAdvancedGate] = useState(false);
|
||||||
|
|
||||||
|
// Transient suppressor settings
|
||||||
|
const [transientEnabled, setTransientEnabled] = useSetting(transientSuppressorEnabledSetting);
|
||||||
|
const [transientThreshold, setTransientThreshold] = useSetting(transientThresholdSetting);
|
||||||
|
const [transientThresholdRaw, setTransientThresholdRaw] = useState(transientThreshold);
|
||||||
|
const [transientRelease, setTransientRelease] = useSetting(transientReleaseSetting);
|
||||||
|
const [transientReleaseRaw, setTransientReleaseRaw] = useState(transientRelease);
|
||||||
|
|
||||||
|
const resetTransientDefaults = useCallback((): void => {
|
||||||
|
const t = transientThresholdSetting.defaultValue;
|
||||||
|
const r = transientReleaseSetting.defaultValue;
|
||||||
|
setTransientThreshold(t); setTransientThresholdRaw(t);
|
||||||
|
setTransientRelease(r); setTransientReleaseRaw(r);
|
||||||
|
}, [setTransientThreshold, setTransientRelease]);
|
||||||
|
|
||||||
const resetGateDefaults = useCallback((): void => {
|
const resetGateDefaults = useCallback((): void => {
|
||||||
const a = noiseGateAttackSetting.defaultValue;
|
const a = noiseGateAttackSetting.defaultValue;
|
||||||
const h = noiseGateHoldSetting.defaultValue;
|
const h = noiseGateHoldSetting.defaultValue;
|
||||||
@@ -293,6 +310,67 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.noiseGateSection}>
|
||||||
|
<Heading
|
||||||
|
type="body"
|
||||||
|
weight="semibold"
|
||||||
|
size="sm"
|
||||||
|
as="h4"
|
||||||
|
className={styles.noiseGateHeading}
|
||||||
|
>
|
||||||
|
Transient Suppressor
|
||||||
|
</Heading>
|
||||||
|
<Separator className={styles.noiseGateSeparator} />
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="transientEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
label="Enable transient suppressor"
|
||||||
|
description="Cut sudden loud impacts like desk hits or mic bumps."
|
||||||
|
checked={transientEnabled}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||||
|
setTransientEnabled(e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
{transientEnabled && (
|
||||||
|
<>
|
||||||
|
<div className={`${styles.volumeSlider} ${styles.thresholdSlider}`}>
|
||||||
|
<span className={styles.sliderLabel}>Sensitivity: {transientThresholdRaw} dB above background</span>
|
||||||
|
<p>Lower values catch more impacts; higher values only catch the loudest ones.</p>
|
||||||
|
<Slider
|
||||||
|
label="Transient threshold"
|
||||||
|
value={transientThresholdRaw}
|
||||||
|
onValueChange={setTransientThresholdRaw}
|
||||||
|
onValueCommit={setTransientThreshold}
|
||||||
|
min={8}
|
||||||
|
max={30}
|
||||||
|
step={1}
|
||||||
|
tooltip={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.volumeSlider}>
|
||||||
|
<span className={styles.sliderLabel}>Release: {transientReleaseRaw} ms</span>
|
||||||
|
<p>How quickly audio returns after suppression.</p>
|
||||||
|
<Slider
|
||||||
|
label="Transient release"
|
||||||
|
value={transientReleaseRaw}
|
||||||
|
onValueChange={setTransientReleaseRaw}
|
||||||
|
onValueCommit={setTransientRelease}
|
||||||
|
min={20}
|
||||||
|
max={200}
|
||||||
|
step={10}
|
||||||
|
tooltip={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.restoreDefaults}>
|
||||||
|
<Button kind="secondary" size="sm" onClick={resetTransientDefaults}>
|
||||||
|
Restore defaults
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -145,6 +145,21 @@ export const noiseGateHold = new Setting<number>("noise-gate-hold", 200);
|
|||||||
// Time in ms for the gate to fully close after hold expires
|
// Time in ms for the gate to fully close after hold expires
|
||||||
export const noiseGateRelease = new Setting<number>("noise-gate-release", 150);
|
export const noiseGateRelease = new Setting<number>("noise-gate-release", 150);
|
||||||
|
|
||||||
|
export const transientSuppressorEnabled = new Setting<boolean>(
|
||||||
|
"transient-suppressor-enabled",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// How many dB above the background RMS a peak must be to trigger suppression
|
||||||
|
export const transientThreshold = new Setting<number>(
|
||||||
|
"transient-suppressor-threshold",
|
||||||
|
15,
|
||||||
|
);
|
||||||
|
// Time in ms for suppression to fade after transient ends
|
||||||
|
export const transientRelease = new Setting<number>(
|
||||||
|
"transient-suppressor-release",
|
||||||
|
80,
|
||||||
|
);
|
||||||
|
|
||||||
export enum MatrixRTCMode {
|
export enum MatrixRTCMode {
|
||||||
Legacy = "legacy",
|
Legacy = "legacy",
|
||||||
Compatibility = "compatibility",
|
Compatibility = "compatibility",
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ import {
|
|||||||
noiseGateAttack,
|
noiseGateAttack,
|
||||||
noiseGateHold,
|
noiseGateHold,
|
||||||
noiseGateRelease,
|
noiseGateRelease,
|
||||||
|
transientSuppressorEnabled,
|
||||||
|
transientThreshold,
|
||||||
|
transientRelease,
|
||||||
} from "../../../settings/settings.ts";
|
} from "../../../settings/settings.ts";
|
||||||
import {
|
import {
|
||||||
type NoiseGateParams,
|
type NoiseGateParams,
|
||||||
@@ -438,6 +441,9 @@ export class Publisher {
|
|||||||
attackMs: noiseGateAttack.getValue(),
|
attackMs: noiseGateAttack.getValue(),
|
||||||
holdMs: noiseGateHold.getValue(),
|
holdMs: noiseGateHold.getValue(),
|
||||||
releaseMs: noiseGateRelease.getValue(),
|
releaseMs: noiseGateRelease.getValue(),
|
||||||
|
transientEnabled: transientSuppressorEnabled.getValue(),
|
||||||
|
transientThresholdDb: transientThreshold.getValue(),
|
||||||
|
transientReleaseMs: transientRelease.getValue(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attach / detach processor when enabled state or the track changes.
|
// Attach / detach processor when enabled state or the track changes.
|
||||||
@@ -482,10 +488,17 @@ export class Publisher {
|
|||||||
noiseGateAttack.value$,
|
noiseGateAttack.value$,
|
||||||
noiseGateHold.value$,
|
noiseGateHold.value$,
|
||||||
noiseGateRelease.value$,
|
noiseGateRelease.value$,
|
||||||
|
transientSuppressorEnabled.value$,
|
||||||
|
transientThreshold.value$,
|
||||||
|
transientRelease.value$,
|
||||||
])
|
])
|
||||||
.pipe(scope.bind())
|
.pipe(scope.bind())
|
||||||
.subscribe(([threshold, attackMs, holdMs, releaseMs]) => {
|
.subscribe(([threshold, attackMs, holdMs, releaseMs,
|
||||||
transformer?.updateParams({ threshold, attackMs, holdMs, releaseMs });
|
transientEnabled, transientThresholdDb, transientReleaseMs]) => {
|
||||||
|
transformer?.updateParams({
|
||||||
|
threshold, attackMs, holdMs, releaseMs,
|
||||||
|
transientEnabled, transientThresholdDb, transientReleaseMs,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user