diff --git a/synth/layer.go b/synth/layer.go index a07d018..8ec0b63 100644 --- a/synth/layer.go +++ b/synth/layer.go @@ -20,7 +20,11 @@ type Layer struct { } // NewLayer creates a Layer for the given config using the specified sample rate and EMA time constant (tau in seconds). +// If cfg.WaveformType is not WaveformCustom, harmonics are resolved from the preset at construction time. func NewLayer(cfg FreqConfig, sampleRate int, tau float64) *Layer { + if cfg.WaveformType != WaveformCustom { + cfg.Harmonics = WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, sampleRate) + } return &Layer{ Config: cfg, Osc: NewOscillator(cfg.BaseHz, sampleRate), diff --git a/synth/waveform_test.go b/synth/waveform_test.go index fc1582b..cbfba02 100644 --- a/synth/waveform_test.go +++ b/synth/waveform_test.go @@ -127,6 +127,71 @@ func TestWaveformPresetHarmonics_SawtoothConsecutive(t *testing.T) { } } +func TestNewLayerResolvesWaveformPreset(t *testing.T) { + cfg := synth.FreqConfig{ + BaseHz: 440.0, + WaveformType: synth.WaveformSquare, + // Harmonics intentionally empty — preset should be resolved + } + layer := synth.NewLayer(cfg, synth.SampleRate, 1.0) + if len(layer.Config.Harmonics) <= 1 { + t.Errorf("expected layer.Config.Harmonics to have length > 1 after preset resolution, got %d", len(layer.Config.Harmonics)) + } + if layer.Config.Harmonics[0].Ratio != 1 { + t.Errorf("expected first harmonic Ratio=1, got %d", layer.Config.Harmonics[0].Ratio) + } +} + +func TestNewLayerPreservesCustomHarmonics(t *testing.T) { + cfg := synth.FreqConfig{ + BaseHz: 440.0, + Harmonics: []synth.HarmonicDef{{Ratio: 1, Amplitude: 1.0}, {Ratio: 2, Amplitude: 0.4}}, + // WaveformType zero value = WaveformCustom + } + layer := synth.NewLayer(cfg, synth.SampleRate, 1.0) + if len(layer.Config.Harmonics) != 2 { + t.Errorf("expected exactly 2 harmonics preserved, got %d", len(layer.Config.Harmonics)) + } + if layer.Config.Harmonics[1].Amplitude != 0.4 { + t.Errorf("expected second harmonic Amplitude=0.4, got %.4f", layer.Config.Harmonics[1].Amplitude) + } +} + +func TestSineRegressionVsCustomHarmonics(t *testing.T) { + // Sine preset should produce identical output to a single-harmonic custom config + cfgSine := synth.FreqConfig{ + BaseHz: 440.0, + WaveformType: synth.WaveformSine, + } + cfgCustom := synth.FreqConfig{ + BaseHz: 440.0, + Harmonics: []synth.HarmonicDef{{Ratio: 1, Amplitude: 1.0}}, + } + // Use fast-converging tau for test + layerSine := synth.NewLayer(cfgSine, synth.SampleRate, 0.001) + layerCustom := synth.NewLayer(cfgCustom, synth.SampleRate, 0.001) + + // Set both to same target amplitude + layerSine.UpdateTarget(1, 1) + layerCustom.UpdateTarget(1, 1) + + // Advance enough samples for EMA to converge (tau=0.001 at 44100 SR: ~44 samples to 63%) + for i := 0; i < 200; i++ { + layerSine.AdvanceSample() + layerCustom.AdvanceSample() + } + + // Next 100 samples should match exactly + for i := 0; i < 100; i++ { + s1 := layerSine.AdvanceSample() + s2 := layerCustom.AdvanceSample() + if abs(s1-s2) > 1e-12 { + t.Errorf("sample %d: sine preset (%.10f) != custom harmonic (%.10f), diff=%.2e", i, s1, s2, abs(s1-s2)) + break + } + } +} + // abs returns the absolute value of x. func abs(x float64) float64 { if x < 0 {