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>
104 lines
2.8 KiB
Go
104 lines
2.8 KiB
Go
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())
|
|
}
|
|
}
|