Files
yoloyolo/synth/envelope.go
T

137 lines
3.9 KiB
Go
Raw Normal View History

package synth
// EnvelopeState tracks the current phase of an ADSR envelope.
type EnvelopeState int
const (
EnvIdle EnvelopeState = iota // silent, waiting for trigger
EnvAttack // ramping up to peak
EnvDecay // falling from peak to sustain level
EnvSustain // holding at sustain level (modulated by traffic rate)
EnvRelease // fading out after traffic stops
)
// Envelope is an ADSR envelope generator with exponential curves.
// It wraps the traffic-rate amplitude: the envelope shapes onset/offset,
// while the traffic rate modulates within the sustain phase.
type Envelope struct {
state EnvelopeState
level float64 // current envelope level [0, 1]
attackRate float64 // per-sample (exponential approach)
decayRate float64 // per-sample
sustainLevel float64 // target level during sustain [0, 1]
releaseRate float64 // per-sample
trafficAmp float64 // EMA-smoothed traffic amplitude [0, 1]
emaAlpha float64 // EMA coefficient for traffic smoothing
}
// EnvelopeParams configures ADSR timing.
type EnvelopeParams struct {
AttackSec float64 // seconds to reach peak
DecaySec float64 // seconds from peak to sustain level
SustainLevel float64 // sustain amplitude [0, 1]
ReleaseSec float64 // seconds to fade to silence
}
// Sustained flow envelope: slow, ambient feel.
var SustainedEnvParams = EnvelopeParams{
AttackSec: 2.0,
DecaySec: 1.0,
SustainLevel: 0.85,
ReleaseSec: 4.0,
}
// Bursty protocol envelope: percussive, event-like.
var BurstyEnvParams = EnvelopeParams{
AttackSec: 0.03,
DecaySec: 0.3,
SustainLevel: 0.0,
ReleaseSec: 1.5,
}
// NewEnvelope creates an ADSR envelope for the given params and sample rate.
// tau is the EMA time constant for traffic amplitude smoothing (seconds).
func NewEnvelope(params EnvelopeParams, sampleRate int, tau float64) *Envelope {
sr := float64(sampleRate)
return &Envelope{
state: EnvIdle,
attackRate: 1.0 / (params.AttackSec * sr),
decayRate: 1.0 / (params.DecaySec * sr),
sustainLevel: params.SustainLevel,
releaseRate: 1.0 / (params.ReleaseSec * sr),
emaAlpha: EMAAlpha(tau, sampleRate),
}
}
// Trigger starts the envelope (called when traffic first appears for this class).
func (e *Envelope) Trigger() {
if e.state == EnvIdle || e.state == EnvRelease {
e.state = EnvAttack
}
}
// Release begins the release phase (called when traffic stops).
func (e *Envelope) Release() {
if e.state != EnvIdle {
e.state = EnvRelease
}
}
// SetTrafficRate updates the EMA-smoothed traffic amplitude target.
// rate should be normalized [0, 1] (count / maxCount).
func (e *Envelope) SetTrafficRate(rate float64) {
e.trafficAmp += e.emaAlpha * (rate - e.trafficAmp)
}
// Advance processes one sample and returns the envelope amplitude [0, 1].
func (e *Envelope) Advance() float64 {
switch e.state {
case EnvIdle:
return 0
case EnvAttack:
e.level += e.attackRate * (1.05 - e.level) // overshoot target slightly for exponential feel
if e.level >= 1.0 {
e.level = 1.0
e.state = EnvDecay
}
case EnvDecay:
target := e.sustainLevel
e.level += e.decayRate * (target - e.level)
if e.level-target < 0.001 {
e.level = target
if target > 0 {
e.state = EnvSustain
} else {
// Bursty: sustain=0, go to release
e.state = EnvRelease
}
}
case EnvSustain:
// Modulate sustain level by traffic rate
target := e.sustainLevel * (WhisperFloor + (1.0-WhisperFloor)*e.trafficAmp)
e.level += e.emaAlpha * (target - e.level)
case EnvRelease:
e.level -= e.releaseRate * e.level
if e.level < 0.001 {
e.level = 0
e.state = EnvIdle
}
}
return e.level
}
// State returns the current envelope state (for testing).
func (e *Envelope) State() EnvelopeState {
return e.state
}
// Level returns the current envelope level (for testing).
func (e *Envelope) Level() float64 {
return e.level
}