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)
This commit is contained in:
2026-03-27 14:18:37 +01:00
parent eb36587ccf
commit 2ef180b4ba
4 changed files with 26 additions and 12 deletions
+3 -3
View File
@@ -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)
}
}
+10 -2
View File
@@ -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)
}
}
}
+4 -1
View File
@@ -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 {