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,103 @@
|
||||
package synth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/netsynth/netsynth/synth"
|
||||
)
|
||||
|
||||
func TestEnvelopeStartsIdle(t *testing.T) {
|
||||
env := synth.NewEnvelope(synth.SustainedEnvParams, synth.SampleRate, 1.0)
|
||||
if env.State() != synth.EnvIdle {
|
||||
t.Errorf("expected EnvIdle, got %v", env.State())
|
||||
}
|
||||
if env.Level() != 0 {
|
||||
t.Errorf("expected level=0 in idle, got %v", env.Level())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvelopeTriggerStartsAttack(t *testing.T) {
|
||||
env := synth.NewEnvelope(synth.SustainedEnvParams, synth.SampleRate, 1.0)
|
||||
env.Trigger()
|
||||
if env.State() != synth.EnvAttack {
|
||||
t.Errorf("expected EnvAttack after Trigger, got %v", env.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvelopeAttackReachesPeak(t *testing.T) {
|
||||
env := synth.NewEnvelope(synth.SustainedEnvParams, synth.SampleRate, 1.0)
|
||||
env.Trigger()
|
||||
// Advance through the full attack phase (2 seconds)
|
||||
for i := 0; i < synth.SampleRate*3; i++ {
|
||||
env.Advance()
|
||||
}
|
||||
if env.Level() < 0.8 {
|
||||
t.Errorf("expected level >= 0.8 after attack, got %v", env.Level())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvelopeReleaseDecays(t *testing.T) {
|
||||
env := synth.NewEnvelope(synth.SustainedEnvParams, synth.SampleRate, 1.0)
|
||||
env.Trigger()
|
||||
// Build up
|
||||
for i := 0; i < synth.SampleRate*3; i++ {
|
||||
env.Advance()
|
||||
}
|
||||
peakLevel := env.Level()
|
||||
|
||||
env.Release()
|
||||
// Advance through release (4 seconds)
|
||||
for i := 0; i < synth.SampleRate*6; i++ {
|
||||
env.Advance()
|
||||
}
|
||||
if env.Level() >= peakLevel*0.3 {
|
||||
t.Errorf("expected level to decay well below peak after release, peak=%v, current=%v", peakLevel, env.Level())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBurstyEnvelopeNoSustain(t *testing.T) {
|
||||
env := synth.NewEnvelope(synth.BurstyEnvParams, synth.SampleRate, 1.0)
|
||||
env.Trigger()
|
||||
// Advance 2 seconds — bursty should have attacked, decayed (sustain=0), and be releasing
|
||||
for i := 0; i < synth.SampleRate*2; i++ {
|
||||
env.Advance()
|
||||
}
|
||||
// Should be very quiet (in release or idle)
|
||||
if env.Level() > 0.1 {
|
||||
t.Errorf("bursty envelope should be near zero after 2s, got %v (state=%v)", env.Level(), env.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvelopeIdleOutputsZero(t *testing.T) {
|
||||
env := synth.NewEnvelope(synth.SustainedEnvParams, synth.SampleRate, 1.0)
|
||||
for i := 0; i < 100; i++ {
|
||||
val := env.Advance()
|
||||
if val != 0 {
|
||||
t.Errorf("idle envelope should output 0, got %v at sample %d", val, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvelopeRetriggerFromRelease(t *testing.T) {
|
||||
env := synth.NewEnvelope(synth.SustainedEnvParams, synth.SampleRate, 1.0)
|
||||
env.Trigger()
|
||||
for i := 0; i < synth.SampleRate*3; i++ {
|
||||
env.Advance()
|
||||
}
|
||||
env.Release()
|
||||
for i := 0; i < synth.SampleRate; i++ {
|
||||
env.Advance()
|
||||
}
|
||||
levelBeforeRetrigger := env.Level()
|
||||
|
||||
// Re-trigger
|
||||
env.Trigger()
|
||||
for i := 0; i < synth.SampleRate*3; i++ {
|
||||
env.Advance()
|
||||
}
|
||||
if env.Level() <= levelBeforeRetrigger {
|
||||
t.Errorf("re-triggered envelope should rise above release level, before=%v, after=%v",
|
||||
levelBeforeRetrigger, env.Level())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user