- 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)
42 lines
1.2 KiB
Go
42 lines
1.2 KiB
Go
package synth
|
|
|
|
import "math"
|
|
|
|
// Oscillator is a phase-accumulator oscillator that generates additive sine waveforms.
|
|
type Oscillator struct {
|
|
phase float64
|
|
freq float64
|
|
sr float64
|
|
}
|
|
|
|
// NewOscillator creates an oscillator at the given frequency and sample rate.
|
|
func NewOscillator(freq float64, sampleRate int) *Oscillator {
|
|
return &Oscillator{freq: freq, sr: float64(sampleRate)}
|
|
}
|
|
|
|
// 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 += math.Abs(h.Amplitude)
|
|
}
|
|
o.phase += o.freq / o.sr
|
|
if o.phase >= 1.0 {
|
|
o.phase -= 1.0
|
|
}
|
|
if totalWeight > 0 {
|
|
return sum / totalWeight
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Phase returns the current phase (for testing).
|
|
func (o *Oscillator) Phase() float64 {
|
|
return o.phase
|
|
}
|