Files
yoloyolo/synth/layer.go
T

106 lines
3.3 KiB
Go
Raw Normal View History

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
}