91 lines
2.7 KiB
Go
91 lines
2.7 KiB
Go
package synth_test
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/netsynth/netsynth/synth"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestLFOBounds(t *testing.T) {
|
||
|
|
lfo := synth.NewLFO(1.0, 0.5, 44100) // 1 Hz, depth 0.5
|
||
|
|
for i := 0; i < 44100; i++ {
|
||
|
|
val := lfo.Advance()
|
||
|
|
if val < -0.5 || val > 0.5 {
|
||
|
|
t.Errorf("LFO value %v out of bounds [-0.5, 0.5] at sample %d", val, i)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLFOPeriod(t *testing.T) {
|
||
|
|
// A 1 Hz LFO should complete one cycle in exactly SampleRate samples
|
||
|
|
lfo := synth.NewLFO(1.0, 1.0, 44100)
|
||
|
|
// Advance to first zero-crossing (quarter period)
|
||
|
|
var firstPositive float64
|
||
|
|
for i := 0; i < 44100; i++ {
|
||
|
|
val := lfo.Advance()
|
||
|
|
if i == 0 {
|
||
|
|
firstPositive = val
|
||
|
|
}
|
||
|
|
// After one full cycle, value should be close to the first value
|
||
|
|
if i == 44099 {
|
||
|
|
lastVal := val
|
||
|
|
// They won't be exactly equal due to phase advancement, but should be close
|
||
|
|
if math.Abs(lastVal-firstPositive) > 0.01 {
|
||
|
|
t.Errorf("LFO not periodic: first=%v, after 1 cycle=%v", firstPositive, lastVal)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPitchLFOFreq(t *testing.T) {
|
||
|
|
// 0 semitones should return baseHz unchanged
|
||
|
|
if synth.PitchLFOFreq(440.0, 0.0) != 440.0 {
|
||
|
|
t.Errorf("PitchLFOFreq(440, 0) should be 440, got %v", synth.PitchLFOFreq(440.0, 0.0))
|
||
|
|
}
|
||
|
|
// 12 semitones = one octave up
|
||
|
|
result := synth.PitchLFOFreq(440.0, 12.0)
|
||
|
|
if math.Abs(result-880.0) > 0.01 {
|
||
|
|
t.Errorf("PitchLFOFreq(440, 12) should be 880, got %v", result)
|
||
|
|
}
|
||
|
|
// Small detuning: 0.1 semitones
|
||
|
|
result = synth.PitchLFOFreq(440.0, 0.1)
|
||
|
|
if result <= 440.0 || result >= 445.0 {
|
||
|
|
t.Errorf("PitchLFOFreq(440, 0.1) should be slightly above 440, got %v", result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLFOConfigForGroup(t *testing.T) {
|
||
|
|
cfg := synth.LFOConfigForGroup("Web")
|
||
|
|
if cfg.PitchRate == 0 {
|
||
|
|
t.Error("Web group should have non-zero PitchRate")
|
||
|
|
}
|
||
|
|
if cfg.TremoloRate == 0 {
|
||
|
|
t.Error("Web group should have non-zero TremoloRate")
|
||
|
|
}
|
||
|
|
// Unknown group should return default
|
||
|
|
cfg2 := synth.LFOConfigForGroup("NonexistentGroup")
|
||
|
|
if cfg2.PitchRate == 0 {
|
||
|
|
t.Error("fallback config should have non-zero PitchRate")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGroupLFORatesIncommensurable(t *testing.T) {
|
||
|
|
// Verify that no two groups share the exact same pitch or tremolo rate
|
||
|
|
groups := []string{"Infrastructure", "Web", "Mail", "Remote Access", "File Transfer", "Database", "VoIP", "Unknown"}
|
||
|
|
pitchRates := make(map[float64]string)
|
||
|
|
tremoloRates := make(map[float64]string)
|
||
|
|
for _, g := range groups {
|
||
|
|
cfg := synth.LFOConfigForGroup(g)
|
||
|
|
if prev, exists := pitchRates[cfg.PitchRate]; exists {
|
||
|
|
t.Errorf("groups %q and %q share PitchRate=%v", prev, g, cfg.PitchRate)
|
||
|
|
}
|
||
|
|
pitchRates[cfg.PitchRate] = g
|
||
|
|
if prev, exists := tremoloRates[cfg.TremoloRate]; exists {
|
||
|
|
t.Errorf("groups %q and %q share TremoloRate=%v", prev, g, cfg.TremoloRate)
|
||
|
|
}
|
||
|
|
tremoloRates[cfg.TremoloRate] = g
|
||
|
|
}
|
||
|
|
}
|