73 lines
2.8 KiB
Go
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
|
||
|
|
}
|