package synth import "math" // EMAAlpha computes the per-sample smoothing coefficient for a given time constant. // tau=1.0 at SR=44100 gives alpha ~0.0000227 (63% of target reached in 1 second). Per D-07. func EMAAlpha(tau float64, sampleRate int) float64 { return 1.0 - math.Exp(-1.0/(tau*float64(sampleRate))) } // Layer combines an oscillator with EMA amplitude smoothing for one traffic class. type Layer struct { Config FreqConfig Osc *Oscillator currentAmp float64 targetAmp float64 alpha float64 // EMA coefficient seen bool // whether this class has ever had count > 0 whisper float64 // whisper floor amplitude (D-08) } // NewLayer creates a Layer for the given config using the specified sample rate and EMA time constant (tau in seconds). // If cfg.WaveformType is not WaveformCustom, harmonics are resolved from the preset at construction time. func NewLayer(cfg FreqConfig, sampleRate int, tau float64) *Layer { if cfg.WaveformType != WaveformCustom { cfg.Harmonics = WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, sampleRate) } return &Layer{ Config: cfg, Osc: NewOscillator(cfg.BaseHz, sampleRate), alpha: EMAAlpha(tau, sampleRate), whisper: WhisperFloor, } } // UpdateTarget sets the target amplitude from a packet count and max count across all classes. // Per D-07/D-08/D-09: once seen, floor is whisper; amplitude scales linearly with normalized rate. func (l *Layer) UpdateTarget(count int64, maxCount int64) { if count > 0 { l.seen = true } if !l.seen { l.targetAmp = 0.0 return } normalizedRate := 0.0 if maxCount > 0 { normalizedRate = float64(count) / float64(maxCount) } l.targetAmp = l.whisper + (1.0-l.whisper)*normalizedRate } // AdvanceSample renders one sample and advances the EMA amplitude toward target. // Returns the raw mono sample (before pan/gain). func (l *Layer) AdvanceSample() float64 { sample := l.Osc.Advance(l.Config.Harmonics) l.currentAmp += l.alpha * (l.targetAmp - l.currentAmp) return sample * l.currentAmp } // CurrentAmp returns the current amplitude (for testing). func (l *Layer) CurrentAmp() float64 { return l.currentAmp } // TargetAmp returns the target amplitude (for testing). func (l *Layer) TargetAmp() float64 { return l.targetAmp } // Seen returns whether this layer has ever received traffic (for testing). func (l *Layer) Seen() bool { return l.seen }