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:
+9
-9
@@ -129,24 +129,24 @@ func TestStereoPan(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleWindowsEMAConvergence(t *testing.T) {
|
||||
func TestMultipleWindowsEnvelopeConvergence(t *testing.T) {
|
||||
b := NewBank(1.0, ClassFreqConfigs)
|
||||
counts := make(map[classify.TrafficClass]int64)
|
||||
counts[classify.ClassICMP] = 100
|
||||
counts[classify.ClassHTTPS] = 100 // sustained protocol — slow attack
|
||||
snap := classify.WindowSnapshot{
|
||||
Counts: counts,
|
||||
TotalPackets: 100,
|
||||
WindowIndex: 0,
|
||||
}
|
||||
// Compute RMS for first and last window render
|
||||
// Compute RMS for first window render
|
||||
rmsFirst := windowRMS(b.RenderWindow(snap))
|
||||
// Render 4 more windows with the same snapshot
|
||||
// Render more windows to let ADSR attack build up (2s attack at 500ms/window ≈ 4 windows)
|
||||
var rmsLast float64
|
||||
for i := 0; i < 4; i++ {
|
||||
for i := 0; i < 8; i++ {
|
||||
rmsLast = windowRMS(b.RenderWindow(snap))
|
||||
}
|
||||
if rmsLast <= rmsFirst {
|
||||
t.Errorf("EMA should converge upward: rmsFirst=%v, rmsLast=%v", rmsFirst, rmsLast)
|
||||
t.Errorf("envelope should converge upward: rmsFirst=%v, rmsLast=%v", rmsFirst, rmsLast)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +161,9 @@ func TestNewBankDynamicGain(t *testing.T) {
|
||||
if len(b.layers) != 3 {
|
||||
t.Errorf("NewBank with 3 configs has %d layers, want 3", len(b.layers))
|
||||
}
|
||||
// Verify gainPerLayer is 1/3
|
||||
expected := 1.0 / 3.0
|
||||
if b.gainPerLayer != expected {
|
||||
// Verify gainPerLayer accounts for tremolo headroom: 1/(3 * 1.2)
|
||||
expected := 1.0 / (3.0 * (1.0 + maxTremoloDepth))
|
||||
if math.Abs(b.gainPerLayer-expected) > 1e-12 {
|
||||
t.Errorf("gainPerLayer = %v, want %v", b.gainPerLayer, expected)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user