feat(05-01): wire waveform resolution into NewLayer at construction time
- NewLayer resolves WaveformPresetHarmonics when cfg.WaveformType != WaveformCustom
- Preset harmonics stored in Layer.Config so AdvanceSample uses them unchanged
- WaveformCustom path preserves existing hand-tuned harmonics (backward compatible)
- TestNewLayerResolvesWaveformPreset: verifies preset fills harmonics on construction
- TestNewLayerPreservesCustomHarmonics: verifies hand-tuned harmonics are untouched
- TestSineRegressionVsCustomHarmonics: verifies WaveformSine == {Ratio:1,Amp:1.0}
- All 41 synth tests pass, encode tests unaffected
This commit is contained in:
@@ -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).
|
// 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 {
|
func NewLayer(cfg FreqConfig, sampleRate int, tau float64) *Layer {
|
||||||
|
if cfg.WaveformType != WaveformCustom {
|
||||||
|
cfg.Harmonics = WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, sampleRate)
|
||||||
|
}
|
||||||
return &Layer{
|
return &Layer{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Osc: NewOscillator(cfg.BaseHz, sampleRate),
|
Osc: NewOscillator(cfg.BaseHz, sampleRate),
|
||||||
|
|||||||
@@ -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.
|
// abs returns the absolute value of x.
|
||||||
func abs(x float64) float64 {
|
func abs(x float64) float64 {
|
||||||
if x < 0 {
|
if x < 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user