Skip to content

Commit af87ba5

Browse files
committed
feat(processor): adopt source-rate anlmdn defaults from matrix spike
Drop the 32 kHz pre-anlmdn sample-rate cap and exit-restore aformat clauses; run anlmdn at the source sample rate with r=0.0020 (r_min) and m=3 (m_strict). Downstream filters (gate, LA-2A, de-esser, analysis) now operate at the source rate throughout. - Remove NoiseRemovePreSampleRate field and all cap-related plumbing - Update production constants: ResearchSec 0.0045 -> 0.0020, Smooth 11 -> 3 - Replace anlmdn_sr_32000 / anlmdn_sr_32000_best_r benchmark variants with anlmdn_production_current - Drop TestAnlmdnBenchmarkPreSampleRateSpecs; assert no aformat sample-rate clauses appear in the noise-removal sub-block - Add scripts/anlmdn-matrix-spike.sh to map the speed/quality frontier across two voice fixtures with different noise profiles - Update AGENTS.md, README.md, Levelator gap analysis, and report formatter to reflect the new defaults The matrix spike validated the new path against the previous 32 kHz cap default at ~35% faster Pass 2 with metric-equivalent quality. Signed-off-by: Martin Wimpress <code@wimpress.io>
1 parent 2c0c8c8 commit af87ba5

8 files changed

Lines changed: 824 additions & 166 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ internal/
6565

6666
**Filter chain order (Pass 2):**
6767
```
68-
downmix → ds201_highpass → ds201_lowpass → noiseremove (32 kHz pre-anlmdn, r=0.0045 + compand) → ds201_gate → la2a_compressor → deesser → analysis → resample
68+
downmix → ds201_highpass → ds201_lowpass → noiseremove (anlmdn at source rate, r=0.0020, m=3 + compand) → ds201_gate → la2a_compressor → deesser → analysis → resample
6969
```
7070

7171
Order rationale: downmix to mono first; HP/LP removes frequency extremes before gate (DS201 frequency-conscious side-chain pattern); denoising before gating (lowers noise floor for gate); compression before de-essing (compression emphasises sibilance); analysis measures processed signal; final resample standardises output format last.
7272

73-
**Noise removal default:** Production uses `anlmdn_sr_32000_best_r`: resample to 32 kHz before `anlmdn`, use `r=0.0045`, then continue through compand. In benchmark context, refer to the old production path as `anlmdn_legacy_default`.
73+
**Noise removal default:** Production uses `anlmdn_production_current`: `anlmdn` runs at the source sample rate with `r=0.0020` (`r_min`) and `m=3` (`m_strict`), followed by `compand` for residual suppression when a noise profile is available. No sample-rate cap or exit restore - downstream filters (gate, LA-2A, de-esser, analysis) operate at the source rate throughout. The matrix spike at `.bench/anlmdn-matrix-spike` validated this path against the previous 32 kHz cap default (`r=0.0045`, `m=11`) at ~35 % faster Pass 2 with metric-equivalent quality. In benchmark context, refer to the 0.3.1 historical path as `anlmdn_legacy_default`.
7474

7575
**Adeclick default:** Production uses `adeclick=t=2.0:w=55:o=50:m=s` (spline interpolation, halved overlap vs prior default) for ~75% Pass 4 runtime reduction at metric-parity quality; the gentle limiter attack keeps source clicks below the relaxed threshold. In benchmark context, refer to the production path as `adeclick_current_t_2_0_w_55_o_50_m_s`. No legacy variant is retained in the matrix.
7676

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Filter chain inspired by studio legends, tuned to your specific audio:
3030
| Filter | Hardware Inspiration | What It Does |
3131
|--------|---------------------|--------------|
3232
| **Highpass** | Drawmer DS201 | Removes subsonic rumble (60-120 Hz, adaptive to spectral content) |
33-
| **Noise reduction** | Non-Local Means | Adaptive `anlmdn` denoiser using the production `anlmdn_sr_32000_best_r` path: 32 kHz before `anlmdn`, `r=0.0045`, then compand residual suppression when a noise profile is available |
33+
| **Noise reduction** | Non-Local Means | Adaptive `anlmdn` denoiser at the source sample rate (`r=0.0020`, `m=3`) followed by compand residual suppression when a noise profile is available |
3434
| **Gate** | DS201 expander | Soft expansion for natural inter-phrase cleanup; breath reduction option positions threshold between noise floor and quiet speech level |
3535
| **Compressor** | Teletronix LA-2A | Programme-dependent optical compression; ratio and release adapt to kurtosis and flux. High-crest override pushes ratio, threshold, release, and knee when predicted limiter ceiling deficit is positive |
3636
| **De-esser** || Adaptive intensity (0.0-0.6) based on spectral centroid and rolloff |
@@ -210,7 +210,7 @@ Record → Process → Edit → Finalise
210210
└─ Each presenter records separately, exports FLAC
211211
```
212212

213-
**Include 10-15 seconds of silence somewhere in your recording.** Just sit quietly and let the room breathe - at the start, between sections, or at the end. Jivetalking scans the entire file to find the cleanest quiet section for building a noise profile, which drives the adaptive noise reduction in Pass 2. Without a clean quiet section, the NR compander is disabled entirely and the 32 kHz `anlmdn` path still runs.
213+
**Include 10-15 seconds of silence somewhere in your recording.** Just sit quietly and let the room breathe - at the start, between sections, or at the end. Jivetalking scans the entire file to find the cleanest quiet section for building a noise profile, which drives the adaptive noise reduction in Pass 2. Without a clean quiet section, the NR compander is disabled entirely and the source-rate `anlmdn` path still runs.
214214

215215
---
216216

docs/Levelator-Comparison-And-Gap-Analysis.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ downmix → ds201_highpass → ds201_lowpass → noiseremove → ds201_gate →
102102
|--------|---------------------|-------|
103103
| **DS201 Highpass** | Frequency (60-120Hz), poles, mix | Spectral centroid, spectral decrease, noise floor |
104104
| **DS201 Lowpass** | Cutoff frequency, enable/disable | Content type detection (speech/music/mixed), rolloff, ZCR |
105-
| **NoiseRemove** | Compand threshold, expansion depth (disabled when no noise profile) | Measured noise floor + 5 dB, noise severity; compand requires an elected silence region; production `anlmdn` uses the `anlmdn_sr_32000_best_r` path |
105+
| **NoiseRemove** | Compand threshold, expansion depth (disabled when no noise profile) | Measured noise floor + 5 dB, noise severity; compand requires an elected silence region; production `anlmdn` runs at the source sample rate with `r=0.0020` and `m=3` |
106106
| **DS201 Gate** | Threshold, ratio, attack, release, range, knee | LRA, noise floor, quiet speech estimate, spectral flux, entropy |
107107
| **LA-2A Compressor** | Threshold, ratio, attack, release, knee, mix | Kurtosis, flux, dynamic range, spectral centroid |
108108
| **De-esser** | Intensity (0.0-0.6) | Spectral centroid + rolloff |
@@ -143,7 +143,7 @@ Jivetalking employs speech profile extraction for adaptive tuning:
143143
| **Look-ahead Capability** | Yes—infinite look-ahead via multiple passes | Yes—infinite look-ahead via Pass 1 analysis |
144144
| **Target Loudness Standard** | -18 dB RMS (custom RMS calculation) | -16 LUFS |
145145
| **Silence Detection** | Fixed: 50ms subsegments > -44 dB | Adaptive: spectral analysis + room tone scoring |
146-
| **Noise Reduction** | None | `anlmdn` (Non-Local Means, 32 kHz pre-denoise path, `r=0.0045`) + compand |
146+
| **Noise Reduction** | None | `anlmdn` (Non-Local Means, source-rate denoising at `r=0.0020`, `m=3`) + compand |
147147
| **Dynamics Processing** | Implicit in leveling algorithm | Explicit LA-2A-style compressor + CBS Volumax limiter |
148148
| **Gating/Expansion** | None | DS201-inspired soft expander (2:1-4:1 ratio) |
149149
| **Highpass Filtering** | None | Adaptive 60-120Hz with warm voice protection |
@@ -220,7 +220,7 @@ Jivetalking employs speech profile extraction for adaptive tuning:
220220

221221
### Capabilities Jivetalking Has That Levelator Lacked
222222

223-
1. **Noise Reduction:** Non-Local Means denoising (`anlmdn`) always active with the production `anlmdn_sr_32000_best_r` path: 32 kHz before `anlmdn`, `r=0.0045`, then restored to the standard processing/output rate. Adaptive compand applies when a silence region is elected as the noise profile
223+
1. **Noise Reduction:** Non-Local Means denoising (`anlmdn`) always active, running at the source sample rate with a small research radius (`r=0.0020`) and a tight LUT decay (`m=3`) for sharp rejection of distant patches. Adaptive compand applies when a silence region is elected as the noise profile
224224
2. **Gating:** Soft expander for inter-speech cleanup
225225
3. **True Peak Limiting:** Prevents inter-sample peaks
226226
4. **De-essing:** Automatic sibilance control
@@ -419,5 +419,5 @@ Levelator created its own RMS standard because no spoken-word standard existed.
419419

420420
---
421421

422-
*Report compiled: February 2026*
422+
*Report compiled: February 2026 (noise-removal section updated May 2026)*
423423
*Jivetalking version analyzed: Development branch (AGENTS.md dated prior to February 2026)*

internal/logging/report.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -688,10 +688,7 @@ func formatNoiseRemoveFilter(f *os.File, cfg *processor.FilterChainConfig, m *pr
688688
// Header: filter name and algorithm
689689
fmt.Fprintf(f, "%snoiseremove: anlmdn + compand (Non-Local Means denoiser)\n", prefix)
690690

691-
// anlmdn parameters (fixed from spike validation)
692-
if cfg.NoiseRemovePreSampleRate > 0 {
693-
fmt.Fprintf(f, " pre-anlmdn sample rate: %d Hz\n", cfg.NoiseRemovePreSampleRate)
694-
}
691+
// anlmdn parameters (matrix spike defaults: r_min + m_strict at source rate)
695692
fmt.Fprintf(f, " anlmdn: s=%.5f, p=%.4fs, r=%.4fs, m=%.0f\n",
696693
cfg.NoiseRemoveStrength,
697694
cfg.NoiseRemovePatchSec,

internal/processor/anlmdn_benchmark_test.go

Lines changed: 30 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,10 @@ type anlmdnBenchmarkParams struct {
3636
}
3737

3838
type anlmdnBenchmarkVariant struct {
39-
Name string
40-
ParameterIntent string
41-
ValidationPriority string
42-
PreAnlmdnSampleRate int
43-
Params anlmdnBenchmarkParams
39+
Name string
40+
ParameterIntent string
41+
ValidationPriority string
42+
Params anlmdnBenchmarkParams
4443
}
4544

4645
func anlmdnBenchmarkVariants() []anlmdnBenchmarkVariant {
@@ -60,7 +59,7 @@ func anlmdnBenchmarkVariants() []anlmdnBenchmarkVariant {
6059
return []anlmdnBenchmarkVariant{
6160
{
6261
Name: "anlmdn_legacy_default",
63-
ParameterIntent: "Legacy production baseline before the 32 kHz lower-r default.",
62+
ParameterIntent: "0.3.1 historical reference: source-rate anlmdn with r=0.0058, m=11.",
6463
ValidationPriority: "baseline",
6564
Params: legacyDefault,
6665
},
@@ -109,18 +108,10 @@ func anlmdnBenchmarkVariants() []anlmdnBenchmarkVariant {
109108
},
110109
},
111110
{
112-
Name: "anlmdn_sr_32000",
113-
ParameterIntent: "32 kHz pre-anlmdn sample-rate cap with legacy anlmdn parameters.",
114-
ValidationPriority: "tier2-sample-rate-baseline-r",
115-
PreAnlmdnSampleRate: noiseRemoveProductionPreSampleRate,
116-
Params: legacyDefault,
117-
},
118-
{
119-
Name: "anlmdn_sr_32000_best_r",
120-
ParameterIntent: "Production default: 32 kHz pre-anlmdn sample-rate cap with the selected lower-r candidate.",
121-
ValidationPriority: "production-default",
122-
PreAnlmdnSampleRate: noiseRemoveProductionPreSampleRate,
123-
Params: productionDefault,
111+
Name: "anlmdn_production_current",
112+
ParameterIntent: "Production default: r_min (r=0.0020) with m_strict (m=3) at source rate.",
113+
ValidationPriority: "production-default",
114+
Params: productionDefault,
124115
},
125116
}
126117
}
@@ -134,7 +125,6 @@ func buildAnlmdnBenchmarkVariantConfig(base *FilterChainConfig, variant anlmdnBe
134125
config.Pass = PassProcessing
135126
config.FilterOrder = Pass2FilterOrder
136127
config.NoiseRemoveEnabled = true
137-
config.NoiseRemovePreSampleRate = variant.PreAnlmdnSampleRate
138128
config.NoiseRemoveStrength = variant.Params.Strength
139129
config.NoiseRemovePatchSec = variant.Params.PatchSec
140130
config.NoiseRemoveResearchSec = variant.Params.ResearchSec
@@ -201,7 +191,6 @@ type anlmdnBenchmarkMetricsSnapshot struct {
201191
VariantName string `json:"variant_name"`
202192
ParameterIntent string `json:"parameter_intent"`
203193
ValidationPriority string `json:"validation_priority"`
204-
PreAnlmdnSampleRate int `json:"pre_anlmdn_sample_rate,omitempty"`
205194
FilterSpec string `json:"filter_spec"`
206195
Pass2RuntimeMS float64 `json:"pass2_runtime_ms"`
207196
FinalLUFS float64 `json:"final_lufs"`
@@ -442,7 +431,6 @@ func buildAnlmdnBenchmarkMetricsSnapshot(
442431
VariantName: variantSpec.Variant.Name,
443432
ParameterIntent: variantSpec.Variant.ParameterIntent,
444433
ValidationPriority: variantSpec.Variant.ValidationPriority,
445-
PreAnlmdnSampleRate: variantSpec.Variant.PreAnlmdnSampleRate,
446434
FilterSpec: variantSpec.Spec,
447435
Pass2RuntimeMS: float64(pass2Runtime.Microseconds()) / 1000.0,
448436
FinalLUFS: normResult.OutputLUFS,
@@ -822,8 +810,7 @@ func TestAnlmdnBenchmarkVariantManifest(t *testing.T) {
822810
"anlmdn_r_4_5",
823811
"anlmdn_r_4_0",
824812
"anlmdn_r_patch_adjusted",
825-
"anlmdn_sr_32000",
826-
"anlmdn_sr_32000_best_r",
813+
"anlmdn_production_current",
827814
}
828815

829816
if len(variants) != len(expectedNames) {
@@ -847,22 +834,15 @@ func TestAnlmdnBenchmarkVariantManifest(t *testing.T) {
847834
}
848835
}
849836

850-
for _, name := range []string{"anlmdn_sr_32000", "anlmdn_sr_32000_best_r"} {
851-
if variantByName[name].PreAnlmdnSampleRate != noiseRemoveProductionPreSampleRate {
852-
t.Fatalf("%s PreAnlmdnSampleRate = %d, want %d",
853-
name, variantByName[name].PreAnlmdnSampleRate, noiseRemoveProductionPreSampleRate)
854-
}
855-
}
856-
for _, name := range []string{"anlmdn_legacy_default", "anlmdn_r_5_0", "anlmdn_r_4_5", "anlmdn_r_4_0", "anlmdn_r_patch_adjusted"} {
857-
if variantByName[name].PreAnlmdnSampleRate != 0 {
858-
t.Fatalf("%s should not use a pre-anlmdn sample-rate cap", name)
859-
}
860-
}
861-
862-
if got := variantByName["anlmdn_sr_32000_best_r"].Params.ResearchSec; got != noiseRemoveProductionResearchSec {
863-
t.Fatalf("anlmdn_sr_32000_best_r research radius = %.4f, want production default %.4f",
837+
productionVariant := variantByName["anlmdn_production_current"]
838+
if got := productionVariant.Params.ResearchSec; got != noiseRemoveProductionResearchSec {
839+
t.Fatalf("anlmdn_production_current research radius = %.4f, want production default %.4f",
864840
got, noiseRemoveProductionResearchSec)
865841
}
842+
if got := productionVariant.Params.Smooth; got != noiseRemoveProductionSmooth {
843+
t.Fatalf("anlmdn_production_current smooth = %.0f, want production default %.0f",
844+
got, noiseRemoveProductionSmooth)
845+
}
866846
}
867847

868848
func TestAnlmdnBenchmarkParameterOnlySpecs(t *testing.T) {
@@ -879,6 +859,7 @@ func TestAnlmdnBenchmarkParameterOnlySpecs(t *testing.T) {
879859
{name: "anlmdn_r_4_5", wantSpec: "anlmdn=s=0.00001:p=0.0060:r=0.0045:m=11"},
880860
{name: "anlmdn_r_4_0", wantSpec: "anlmdn=s=0.00001:p=0.0060:r=0.0040:m=11"},
881861
{name: "anlmdn_r_patch_adjusted", wantSpec: "anlmdn=s=0.00001:p=0.0050:r=0.0045:m=11"},
862+
{name: "anlmdn_production_current", wantSpec: "anlmdn=s=0.00001:p=0.0060:r=0.0020:m=3"},
882863
}
883864

884865
for _, tt := range tests {
@@ -910,50 +891,6 @@ func TestAnlmdnBenchmarkParameterOnlySpecs(t *testing.T) {
910891
assertBaseLegacyAnlmdnConfigUnchanged(t, base)
911892
}
912893

913-
func TestAnlmdnBenchmarkPreSampleRateSpecs(t *testing.T) {
914-
base := newAnlmdnBenchmarkTestConfig()
915-
916-
tests := []struct {
917-
name string
918-
wantSpec string
919-
}{
920-
{name: "anlmdn_sr_32000", wantSpec: "anlmdn=s=0.00001:p=0.0060:r=0.0058:m=11"},
921-
{name: "anlmdn_sr_32000_best_r", wantSpec: "anlmdn=s=0.00001:p=0.0060:r=0.0045:m=11"},
922-
}
923-
924-
for _, tt := range tests {
925-
t.Run(tt.name, func(t *testing.T) {
926-
variant := findAnlmdnBenchmarkVariant(t, tt.name)
927-
spec := buildAnlmdnBenchmarkVariantSpec(base, variant)
928-
929-
assertFullbenchSpecContains(t, spec, []string{
930-
"aformat=sample_rates=32000:channel_layouts=mono:sample_fmts=fltp",
931-
tt.wantSpec,
932-
"anlmdn=",
933-
"compand=",
934-
"aformat=sample_rates=44100:channel_layouts=mono:sample_fmts=s16",
935-
"asetnsamples=n=4096",
936-
})
937-
assertFullbenchSpecOrder(t, spec, []string{
938-
"aformat=channel_layouts=mono",
939-
"highpass=",
940-
"lowpass=",
941-
"aformat=sample_rates=32000:channel_layouts=mono:sample_fmts=fltp",
942-
"anlmdn=",
943-
"compand=",
944-
"agate=",
945-
"acompressor=",
946-
"deesser=",
947-
"astats=",
948-
"aspectralstats=",
949-
"ebur128=",
950-
"aformat=sample_rates=44100",
951-
"asetnsamples=",
952-
})
953-
})
954-
}
955-
}
956-
957894
func TestAnlmdnBenchmarkVariantSpecsPreservePass2Ordering(t *testing.T) {
958895
base := newAnlmdnBenchmarkTestConfig()
959896

@@ -967,9 +904,15 @@ func TestAnlmdnBenchmarkVariantSpecsPreservePass2Ordering(t *testing.T) {
967904
assertFullbenchSpecContains(t, spec, []string{
968905
"anlmdn=",
969906
"compand=",
970-
"aformat=sample_rates=44100",
907+
"aformat=sample_rates=44100:channel_layouts=mono:sample_fmts=s16",
971908
"asetnsamples=",
972909
})
910+
assertFullbenchSpecExcludes(t, spec, []string{
911+
// The cap+exit-restore architecture has been removed; no
912+
// noise-removal sub-block aformat clauses should appear.
913+
"aformat=sample_rates=32000",
914+
"aformat=sample_rates=44100:channel_layouts=mono:sample_fmts=fltp",
915+
})
973916
assertFullbenchSpecOrder(t, spec, []string{
974917
"aformat=channel_layouts=mono",
975918
"highpass=",
@@ -982,23 +925,19 @@ func TestAnlmdnBenchmarkVariantSpecsPreservePass2Ordering(t *testing.T) {
982925
"astats=",
983926
"aspectralstats=",
984927
"ebur128=",
985-
"aformat=sample_rates=44100",
928+
"aformat=sample_rates=44100:channel_layouts=mono:sample_fmts=s16",
986929
"asetnsamples=",
987930
})
988931
})
989932
}
990933
}
991934

992-
func TestAnlmdnBenchmarkProductionDefaultsMatchFastVariant(t *testing.T) {
935+
func TestAnlmdnBenchmarkProductionDefaultsMatchProductionVariant(t *testing.T) {
993936
config := DefaultFilterConfig()
994937

995938
if !config.NoiseRemoveEnabled {
996939
t.Fatal("production NoiseRemoveEnabled changed")
997940
}
998-
if config.NoiseRemovePreSampleRate != noiseRemoveProductionPreSampleRate {
999-
t.Fatalf("production NoiseRemovePreSampleRate = %d, want %d",
1000-
config.NoiseRemovePreSampleRate, noiseRemoveProductionPreSampleRate)
1001-
}
1002941
if config.NoiseRemoveStrength != noiseRemoveProductionStrength {
1003942
t.Fatalf("production NoiseRemoveStrength = %.5f, want %.5f",
1004943
config.NoiseRemoveStrength, noiseRemoveProductionStrength)
@@ -1016,9 +955,9 @@ func TestAnlmdnBenchmarkProductionDefaultsMatchFastVariant(t *testing.T) {
1016955
config.NoiseRemoveSmooth, noiseRemoveProductionSmooth)
1017956
}
1018957

1019-
fastVariant := findAnlmdnBenchmarkVariant(t, "anlmdn_sr_32000_best_r")
1020-
if got, want := buildAnlmdnBenchmarkVariantSpec(config, fastVariant), config.BuildFilterSpec(); got != want {
1021-
t.Fatalf("production filter spec drifted from anlmdn_sr_32000_best_r\nvariant: %s\nproduction: %s", got, want)
958+
productionVariant := findAnlmdnBenchmarkVariant(t, "anlmdn_production_current")
959+
if got, want := buildAnlmdnBenchmarkVariantSpec(config, productionVariant), config.BuildFilterSpec(); got != want {
960+
t.Fatalf("production filter spec drifted from anlmdn_production_current\nvariant: %s\nproduction: %s", got, want)
1022961
}
1023962

1024963
expectedOrder := []FilterID{
@@ -1200,9 +1139,6 @@ func assertBaseLegacyAnlmdnConfigUnchanged(tb testing.TB, config *FilterChainCon
12001139
if !config.NoiseRemoveEnabled {
12011140
tb.Fatal("base config NoiseRemoveEnabled changed")
12021141
}
1203-
if config.NoiseRemovePreSampleRate != 0 {
1204-
tb.Fatalf("base NoiseRemovePreSampleRate changed: got %d", config.NoiseRemovePreSampleRate)
1205-
}
12061142
if config.NoiseRemoveStrength != noiseRemoveLegacyStrength {
12071143
tb.Fatalf("base NoiseRemoveStrength changed: got %.5f", config.NoiseRemoveStrength)
12081144
}

0 commit comments

Comments
 (0)