2026-03-26 11:58:44 +01:00
|
|
|
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)))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 19:38:58 +01:00
|
|
|
// Layer combines an oscillator with ADSR envelope, LFO modulation,
|
|
|
|
|
// and EMA amplitude smoothing for one traffic class.
|
2026-03-26 11:58:44 +01:00
|
|
|
type Layer struct {
|
|
|
|
|
Config FreqConfig
|
|
|
|
|
Osc *Oscillator
|
2026-03-27 19:38:58 +01:00
|
|
|
env *Envelope
|
|
|
|
|
pitchLFO *LFO
|
|
|
|
|
tremoloLFO *LFO
|
2026-03-26 11:58:44 +01:00
|
|
|
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).
|
2026-03-26 17:34:45 +01:00
|
|
|
// If cfg.WaveformType is not WaveformCustom, harmonics are resolved from the preset at construction time.
|
2026-03-27 19:38:58 +01:00
|
|
|
// LFO and ADSR parameters are derived from the protocol group and bursty flag.
|
2026-03-26 11:58:44 +01:00
|
|
|
func NewLayer(cfg FreqConfig, sampleRate int, tau float64) *Layer {
|
2026-03-26 17:34:45 +01:00
|
|
|
if cfg.WaveformType != WaveformCustom {
|
|
|
|
|
cfg.Harmonics = WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, sampleRate)
|
|
|
|
|
}
|
2026-03-27 19:38:58 +01:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
2026-03-26 11:58:44 +01:00
|
|
|
return &Layer{
|
2026-03-27 19:38:58 +01:00
|
|
|
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,
|
2026-03-26 11:58:44 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 19:38:58 +01:00
|
|
|
// UpdateTarget sets the envelope state and traffic rate from packet counts.
|
|
|
|
|
// Triggers the envelope on first appearance, releases when count drops to 0.
|
2026-03-26 11:58:44 +01:00
|
|
|
func (l *Layer) UpdateTarget(count int64, maxCount int64) {
|
|
|
|
|
if count > 0 {
|
2026-03-27 19:38:58 +01:00
|
|
|
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()
|
2026-03-26 11:58:44 +01:00
|
|
|
}
|
2026-03-27 19:38:58 +01:00
|
|
|
|
|
|
|
|
// Update traffic rate for sustain modulation
|
2026-03-26 11:58:44 +01:00
|
|
|
normalizedRate := 0.0
|
|
|
|
|
if maxCount > 0 {
|
|
|
|
|
normalizedRate = float64(count) / float64(maxCount)
|
|
|
|
|
}
|
2026-03-27 19:38:58 +01:00
|
|
|
l.env.SetTrafficRate(normalizedRate)
|
2026-03-26 11:58:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 19:38:58 +01:00
|
|
|
// AdvanceSample renders one sample with LFO modulation and ADSR envelope.
|
2026-03-26 11:58:44 +01:00
|
|
|
func (l *Layer) AdvanceSample() float64 {
|
2026-03-27 19:38:58 +01:00
|
|
|
// Pitch modulation: LFO shifts frequency by a few cents
|
|
|
|
|
pitchMod := l.pitchLFO.Advance()
|
|
|
|
|
l.Osc.freq = PitchLFOFreq(l.Config.BaseHz, pitchMod)
|
|
|
|
|
|
|
|
|
|
// Generate waveform sample
|
2026-03-26 11:58:44 +01:00
|
|
|
sample := l.Osc.Advance(l.Config.Harmonics)
|
2026-03-27 19:38:58 +01:00
|
|
|
|
|
|
|
|
// 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
|
2026-03-26 11:58:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 19:38:58 +01:00
|
|
|
// CurrentAmp returns the current envelope level (for testing).
|
2026-03-26 11:58:44 +01:00
|
|
|
func (l *Layer) CurrentAmp() float64 {
|
2026-03-27 19:38:58 +01:00
|
|
|
return l.env.Level()
|
2026-03-26 11:58:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 19:38:58 +01:00
|
|
|
// TargetAmp returns the current traffic amplitude in the envelope (for testing).
|
2026-03-26 11:58:44 +01:00
|
|
|
func (l *Layer) TargetAmp() float64 {
|
2026-03-27 19:38:58 +01:00
|
|
|
return l.env.trafficAmp
|
2026-03-26 11:58:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Seen returns whether this layer has ever received traffic (for testing).
|
|
|
|
|
func (l *Layer) Seen() bool {
|
|
|
|
|
return l.seen
|
|
|
|
|
}
|