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 ADSR envelope, LFO modulation, // and EMA amplitude smoothing for one traffic class. type Layer struct { Config FreqConfig Osc *Oscillator env *Envelope pitchLFO *LFO tremoloLFO *LFO 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. // LFO and ADSR parameters are derived from the protocol group and bursty flag. func NewLayer(cfg FreqConfig, sampleRate int, tau float64) *Layer { if cfg.WaveformType != WaveformCustom { cfg.Harmonics = WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, sampleRate) } // Select envelope params based on bursty flag envParams := SustainedEnvParams if cfg.Bursty { envParams = BurstyEnvParams } // Get LFO config for this protocol group lfoCfg := LFOConfigForGroup(cfg.Group) return &Layer{ Config: cfg, Osc: NewOscillator(cfg.BaseHz, sampleRate), env: NewEnvelope(envParams, sampleRate, tau), pitchLFO: NewLFO(lfoCfg.PitchRate, lfoCfg.PitchDepth, sampleRate), tremoloLFO: NewLFO(lfoCfg.TremoloRate, lfoCfg.TremoloDepth, sampleRate), whisper: WhisperFloor, } } // UpdateTarget sets the envelope state and traffic rate from packet counts. // Triggers the envelope on first appearance, releases when count drops to 0. func (l *Layer) UpdateTarget(count int64, maxCount int64) { if count > 0 { if !l.seen { l.seen = true l.env.Trigger() } // Re-trigger if we were in release/idle if l.env.State() == EnvRelease || l.env.State() == EnvIdle { l.env.Trigger() } } else if l.seen && count == 0 { l.env.Release() } // Update traffic rate for sustain modulation normalizedRate := 0.0 if maxCount > 0 { normalizedRate = float64(count) / float64(maxCount) } l.env.SetTrafficRate(normalizedRate) } // AdvanceSample renders one sample with LFO modulation and ADSR envelope. func (l *Layer) AdvanceSample() float64 { // Pitch modulation: LFO shifts frequency by a few cents pitchMod := l.pitchLFO.Advance() l.Osc.freq = PitchLFOFreq(l.Config.BaseHz, pitchMod) // Generate waveform sample sample := l.Osc.Advance(l.Config.Harmonics) // Apply ADSR envelope envAmp := l.env.Advance() // Tremolo modulation: LFO modulates amplitude tremoloMod := 1.0 + l.tremoloLFO.Advance() // [1-depth, 1+depth] return sample * envAmp * tremoloMod } // CurrentAmp returns the current envelope level (for testing). func (l *Layer) CurrentAmp() float64 { return l.env.Level() } // TargetAmp returns the current traffic amplitude in the envelope (for testing). func (l *Layer) TargetAmp() float64 { return l.env.trafficAmp } // Seen returns whether this layer has ever received traffic (for testing). func (l *Layer) Seen() bool { return l.seen }