Files
yoloyolo/synth/lfo.go
T
gurixandClaude Opus 4.6 6b2db48339 feat(synth): add LFO modulation, ADSR envelopes, pentatonic tuning, and soft limiter
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>
2026-03-27 19:38:58 +01:00

73 lines
2.8 KiB
Go

package synth
import "math"
// LFO is a low-frequency oscillator for modulating synthesis parameters.
// Uses sine waveform. Rate is in Hz (typically 0.01-0.5 Hz for ambient feel).
type LFO struct {
phase float64
rate float64 // Hz
depth float64 // modulation depth (interpretation depends on usage)
sr float64
}
// NewLFO creates an LFO at the given rate (Hz) and depth.
func NewLFO(rate, depth float64, sampleRate int) *LFO {
return &LFO{
rate: rate,
depth: depth,
sr: float64(sampleRate),
}
}
// Advance returns the current LFO value in [-depth, +depth] and advances the phase.
func (l *LFO) Advance() float64 {
val := math.Sin(2 * math.Pi * l.phase)
l.phase += l.rate / l.sr
if l.phase >= 1.0 {
l.phase -= math.Floor(l.phase)
}
return val * l.depth
}
// PitchLFOFreq returns a pitch-modulated frequency given a base frequency
// and an LFO value in semitones. For example, lfoVal=0.1 shifts pitch up by 0.1 semitones.
func PitchLFOFreq(baseHz, lfoSemitones float64) float64 {
return baseHz * math.Pow(2.0, lfoSemitones/12.0)
}
// LFOConfig holds LFO parameters for a synthesis layer.
// Each protocol gets unique, incommensurable rates so modulation patterns never repeat.
type LFOConfig struct {
PitchRate float64 // Hz, typically 0.02-0.08
PitchDepth float64 // semitones, typically 0.05-0.15
TremoloRate float64 // Hz, typically 0.05-0.3
TremoloDepth float64 // amplitude fraction, typically 0.05-0.2
}
// layerLFOConfigs provides unique incommensurable LFO rates per protocol group.
// Rates chosen as non-integer ratios to avoid periodic sync (Eno technique).
var groupLFOConfigs = map[string]LFOConfig{
"Infrastructure": {PitchRate: 0.031, PitchDepth: 0.08, TremoloRate: 0.053, TremoloDepth: 0.12},
"Web": {PitchRate: 0.043, PitchDepth: 0.10, TremoloRate: 0.071, TremoloDepth: 0.15},
"Mail": {PitchRate: 0.037, PitchDepth: 0.07, TremoloRate: 0.059, TremoloDepth: 0.10},
"Remote Access": {PitchRate: 0.029, PitchDepth: 0.12, TremoloRate: 0.047, TremoloDepth: 0.18},
"File Transfer": {PitchRate: 0.041, PitchDepth: 0.09, TremoloRate: 0.067, TremoloDepth: 0.13},
"Database": {PitchRate: 0.023, PitchDepth: 0.06, TremoloRate: 0.083, TremoloDepth: 0.10},
"VoIP": {PitchRate: 0.019, PitchDepth: 0.05, TremoloRate: 0.091, TremoloDepth: 0.08},
"Unknown": {PitchRate: 0.053, PitchDepth: 0.15, TremoloRate: 0.037, TremoloDepth: 0.20},
}
// DefaultLFOConfig is the fallback for groups not in the map.
var DefaultLFOConfig = LFOConfig{
PitchRate: 0.033, PitchDepth: 0.10, TremoloRate: 0.057, TremoloDepth: 0.15,
}
// LFOConfigForGroup returns the LFO config for a protocol group.
func LFOConfigForGroup(group string) LFOConfig {
if cfg, ok := groupLFOConfigs[group]; ok {
return cfg
}
return DefaultLFOConfig
}