Replace static EMA-smoothed drones with an evolving ambient soundscape: - ADSR envelope system with sustained (2s attack, 4s release) and bursty (30ms attack, no sustain) modes per protocol group - LFO pitch wobble and amplitude tremolo with incommensurable rates per group (Eno technique) so modulation patterns never repeat - C major pentatonic frequency tuning (just intonation) — any combination of active protocols sounds consonant - tanh soft limiter on master output prevents clipping - Sync all documentation: README, PROJECT.md, ARCHITECTURE.md, v1.2 requirements traceability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
137 lines
3.9 KiB
Go
137 lines
3.9 KiB
Go
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
|
|
}
|