From 2ef180b4ba96ee794375538156c38cdbb7ce9e65 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Fri, 27 Mar 2026 14:15:27 +0100 Subject: [PATCH] fix(09-01): add TestGroupFieldPopulated, fix oscillator normalization, update tests - Add TestGroupFieldPopulated test to verify all ClassFreqConfigs have non-empty Group (GRP-01) - Change TestHarmonicsNonEmpty threshold from < 2 to < 1 (accepts single-harmonic sine entries) - Fix oscillator Advance() to use math.Abs(h.Amplitude) for normalization weight accumulation: triangle wave uses alternating-sign amplitudes; signed sum underestimates totalWeight causing output to exceed [-1,1] bounds when using WaveformPresetHarmonics (Rule 1 bug fix) - Update TestStereoPan to use ClassSSH (pan=-0.7) instead of ClassDHCP (pan changed to 0.1) - Update TestNewBankCustomConfigNoClip: passes after oscillator normalization fix - Fix TestLoadPartialOverrideFrequency: derive expected WaveformType from defaults (not hardcoded 0) - Fix TestAutoFreqSkipsBuiltins: derive expected HTTPS BaseHz from defaults (150.0 in Phase 9) --- config/config_test.go | 15 +++++++++------ synth/bank_test.go | 6 +++--- synth/config_test.go | 12 ++++++++++-- synth/oscillator.go | 5 ++++- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index 967dfde..9d8ef7f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -27,7 +27,7 @@ func writeTOML(t *testing.T, content string) string { } // TestLoadPartialOverrideFrequency: setting only frequency for ICMP overrides BaseHz, -// leaves WaveformType unchanged (WaveformCustom), and leaves other classes unchanged. +// leaves WaveformType unchanged (preserves default), and leaves other classes unchanged. func TestLoadPartialOverrideFrequency(t *testing.T) { path := writeTOML(t, "[sounds.ICMP]\nfrequency = 100.0\n") @@ -40,8 +40,10 @@ func TestLoadPartialOverrideFrequency(t *testing.T) { if cfgs[classify.ClassICMP].BaseHz != 100.0 { t.Errorf("ICMP BaseHz: got %v, want 100.0", cfgs[classify.ClassICMP].BaseHz) } - if cfgs[classify.ClassICMP].WaveformType != synth.WaveformCustom { - t.Errorf("ICMP WaveformType: got %v, want WaveformCustom (0)", cfgs[classify.ClassICMP].WaveformType) + // WaveformType should remain unchanged from the default (Phase 9: WaveformTriangle for ICMP) + wantWaveform := synth.ClassFreqConfigs[classify.ClassICMP].WaveformType + if cfgs[classify.ClassICMP].WaveformType != wantWaveform { + t.Errorf("ICMP WaveformType: got %v, want %v (default, unchanged)", cfgs[classify.ClassICMP].WaveformType, wantWaveform) } // DNS should be unchanged want := synth.ClassFreqConfigs[classify.ClassDNS].BaseHz @@ -559,7 +561,7 @@ class = "GameServer" } // TestAutoFreqSkipsBuiltins: TOML with [[rules]] (class="HTTPS", protocol="tcp", port=443) -// -> FreqCfgs["HTTPS"].BaseHz == 150.0 (the default), NOT an auto-assigned value. +// -> FreqCfgs["HTTPS"].BaseHz == 150.0 (Phase 9 default), NOT an auto-assigned value. func TestAutoFreqSkipsBuiltins(t *testing.T) { toml := ` [[rules]] @@ -573,8 +575,9 @@ class = "HTTPS" if err != nil { t.Fatalf("Load: %v", err) } - if result.FreqCfgs[classify.ClassHTTPS].BaseHz != 150.0 { - t.Errorf("HTTPS BaseHz: got %v, want 150.0 (default, not auto-assigned)", result.FreqCfgs[classify.ClassHTTPS].BaseHz) + wantHz := synth.ClassFreqConfigs[classify.ClassHTTPS].BaseHz + if result.FreqCfgs[classify.ClassHTTPS].BaseHz != wantHz { + t.Errorf("HTTPS BaseHz: got %v, want %.1f (default, not auto-assigned)", result.FreqCfgs[classify.ClassHTTPS].BaseHz, wantHz) } } diff --git a/synth/bank_test.go b/synth/bank_test.go index 37dd5e2..6e396a4 100644 --- a/synth/bank_test.go +++ b/synth/bank_test.go @@ -104,8 +104,8 @@ func TestMixerNoClip(t *testing.T) { func TestStereoPan(t *testing.T) { b := NewBank(0.01, ClassFreqConfigs) // fast EMA counts := make(map[classify.TrafficClass]int64) - // ClassDHCP has pan=-0.75 (wide-left in config.go) - counts[classify.ClassDHCP] = 1000 + // ClassSSH has pan=-0.7 (wide-left in Phase 9 config.go) + counts[classify.ClassSSH] = 1000 snap := classify.WindowSnapshot{ Counts: counts, TotalPackets: 1000, @@ -125,7 +125,7 @@ func TestStereoPan(t *testing.T) { rmsL := math.Sqrt(sumL2 / float64(len(frames))) rmsR := math.Sqrt(sumR2 / float64(len(frames))) if rmsL <= rmsR { - t.Errorf("ClassDHCP (pan=-0.75) should have rmsL > rmsR; got rmsL=%v, rmsR=%v", rmsL, rmsR) + t.Errorf("ClassSSH (pan=-0.7) should have rmsL > rmsR; got rmsL=%v, rmsR=%v", rmsL, rmsR) } } diff --git a/synth/config_test.go b/synth/config_test.go index f041e7f..2d0a405 100644 --- a/synth/config_test.go +++ b/synth/config_test.go @@ -39,8 +39,8 @@ func TestFrequenciesUnique(t *testing.T) { func TestHarmonicsNonEmpty(t *testing.T) { for class, cfg := range synth.ClassFreqConfigs { - if len(cfg.Harmonics) < 2 { - t.Errorf("class %q has fewer than 2 harmonics (got %d)", class, len(cfg.Harmonics)) + if len(cfg.Harmonics) < 1 { + t.Errorf("class %q has no harmonics (got %d)", class, len(cfg.Harmonics)) } } } @@ -59,3 +59,11 @@ func TestClassFreqConfigsMatchAllClasses(t *testing.T) { len(synth.ClassFreqConfigs), len(classify.AllClasses())) } } + +func TestGroupFieldPopulated(t *testing.T) { + for class, cfg := range synth.ClassFreqConfigs { + if cfg.Group == "" { + t.Errorf("class %q has empty Group field in ClassFreqConfigs", class) + } + } +} diff --git a/synth/oscillator.go b/synth/oscillator.go index 818497e..7adac91 100644 --- a/synth/oscillator.go +++ b/synth/oscillator.go @@ -15,12 +15,15 @@ func NewOscillator(freq float64, sampleRate int) *Oscillator { } // Advance returns one sample: fundamental + harmonics summed and normalized to [-1, 1]. +// Normalization uses sum of absolute amplitudes so that alternating-sign harmonic series +// (e.g. triangle wave) are correctly bounded. Without math.Abs, signed cancellation +// produces an inflated normalization denominator that causes output to exceed [-1, 1]. func (o *Oscillator) Advance(harmonics []HarmonicDef) float64 { sum := 0.0 totalWeight := 0.0 for _, h := range harmonics { sum += h.Amplitude * math.Sin(2*math.Pi*o.phase*float64(h.Ratio)) - totalWeight += h.Amplitude + totalWeight += math.Abs(h.Amplitude) } o.phase += o.freq / o.sr if o.phase >= 1.0 {