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>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user