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:
@@ -27,7 +27,7 @@ func writeTOML(t *testing.T, content string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestLoadPartialOverrideFrequency: setting only frequency for ICMP overrides BaseHz,
|
// 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) {
|
func TestLoadPartialOverrideFrequency(t *testing.T) {
|
||||||
path := writeTOML(t, "[sounds.ICMP]\nfrequency = 100.0\n")
|
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 {
|
if cfgs[classify.ClassICMP].BaseHz != 100.0 {
|
||||||
t.Errorf("ICMP BaseHz: got %v, want 100.0", cfgs[classify.ClassICMP].BaseHz)
|
t.Errorf("ICMP BaseHz: got %v, want 100.0", cfgs[classify.ClassICMP].BaseHz)
|
||||||
}
|
}
|
||||||
if cfgs[classify.ClassICMP].WaveformType != synth.WaveformCustom {
|
// WaveformType should remain unchanged from the default (Phase 9: WaveformTriangle for ICMP)
|
||||||
t.Errorf("ICMP WaveformType: got %v, want WaveformCustom (0)", cfgs[classify.ClassICMP].WaveformType)
|
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
|
// DNS should be unchanged
|
||||||
want := synth.ClassFreqConfigs[classify.ClassDNS].BaseHz
|
want := synth.ClassFreqConfigs[classify.ClassDNS].BaseHz
|
||||||
@@ -559,7 +561,7 @@ class = "GameServer"
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestAutoFreqSkipsBuiltins: TOML with [[rules]] (class="HTTPS", protocol="tcp", port=443)
|
// 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) {
|
func TestAutoFreqSkipsBuiltins(t *testing.T) {
|
||||||
toml := `
|
toml := `
|
||||||
[[rules]]
|
[[rules]]
|
||||||
@@ -573,8 +575,9 @@ class = "HTTPS"
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load: %v", err)
|
t.Fatalf("Load: %v", err)
|
||||||
}
|
}
|
||||||
if result.FreqCfgs[classify.ClassHTTPS].BaseHz != 150.0 {
|
wantHz := synth.ClassFreqConfigs[classify.ClassHTTPS].BaseHz
|
||||||
t.Errorf("HTTPS BaseHz: got %v, want 150.0 (default, not auto-assigned)", result.FreqCfgs[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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -104,8 +104,8 @@ func TestMixerNoClip(t *testing.T) {
|
|||||||
func TestStereoPan(t *testing.T) {
|
func TestStereoPan(t *testing.T) {
|
||||||
b := NewBank(0.01, ClassFreqConfigs) // fast EMA
|
b := NewBank(0.01, ClassFreqConfigs) // fast EMA
|
||||||
counts := make(map[classify.TrafficClass]int64)
|
counts := make(map[classify.TrafficClass]int64)
|
||||||
// ClassDHCP has pan=-0.75 (wide-left in config.go)
|
// ClassSSH has pan=-0.7 (wide-left in Phase 9 config.go)
|
||||||
counts[classify.ClassDHCP] = 1000
|
counts[classify.ClassSSH] = 1000
|
||||||
snap := classify.WindowSnapshot{
|
snap := classify.WindowSnapshot{
|
||||||
Counts: counts,
|
Counts: counts,
|
||||||
TotalPackets: 1000,
|
TotalPackets: 1000,
|
||||||
@@ -125,7 +125,7 @@ func TestStereoPan(t *testing.T) {
|
|||||||
rmsL := math.Sqrt(sumL2 / float64(len(frames)))
|
rmsL := math.Sqrt(sumL2 / float64(len(frames)))
|
||||||
rmsR := math.Sqrt(sumR2 / float64(len(frames)))
|
rmsR := math.Sqrt(sumR2 / float64(len(frames)))
|
||||||
if rmsL <= rmsR {
|
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
@@ -39,8 +39,8 @@ func TestFrequenciesUnique(t *testing.T) {
|
|||||||
|
|
||||||
func TestHarmonicsNonEmpty(t *testing.T) {
|
func TestHarmonicsNonEmpty(t *testing.T) {
|
||||||
for class, cfg := range synth.ClassFreqConfigs {
|
for class, cfg := range synth.ClassFreqConfigs {
|
||||||
if len(cfg.Harmonics) < 2 {
|
if len(cfg.Harmonics) < 1 {
|
||||||
t.Errorf("class %q has fewer than 2 harmonics (got %d)", class, len(cfg.Harmonics))
|
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()))
|
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
@@ -15,12 +15,15 @@ func NewOscillator(freq float64, sampleRate int) *Oscillator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Advance returns one sample: fundamental + harmonics summed and normalized to [-1, 1].
|
// 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 {
|
func (o *Oscillator) Advance(harmonics []HarmonicDef) float64 {
|
||||||
sum := 0.0
|
sum := 0.0
|
||||||
totalWeight := 0.0
|
totalWeight := 0.0
|
||||||
for _, h := range harmonics {
|
for _, h := range harmonics {
|
||||||
sum += h.Amplitude * math.Sin(2*math.Pi*o.phase*float64(h.Ratio))
|
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
|
o.phase += o.freq / o.sr
|
||||||
if o.phase >= 1.0 {
|
if o.phase >= 1.0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user