2026-03-26 17:33:03 +01:00
|
|
|
package synth_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/netsynth/netsynth/synth"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestWaveformPresetHarmonics_Sine(t *testing.T) {
|
|
|
|
|
harmonics := synth.WaveformPresetHarmonics(synth.WaveformSine, 440.0, 44100)
|
|
|
|
|
if len(harmonics) != 1 {
|
|
|
|
|
t.Fatalf("WaveformSine: expected 1 harmonic, got %d", len(harmonics))
|
|
|
|
|
}
|
|
|
|
|
if harmonics[0].Ratio != 1 {
|
|
|
|
|
t.Errorf("WaveformSine: expected Ratio=1, got %d", harmonics[0].Ratio)
|
|
|
|
|
}
|
|
|
|
|
if harmonics[0].Amplitude != 1.0 {
|
|
|
|
|
t.Errorf("WaveformSine: expected Amplitude=1.0, got %.4f", harmonics[0].Amplitude)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWaveformPresetHarmonics_Square(t *testing.T) {
|
|
|
|
|
harmonics := synth.WaveformPresetHarmonics(synth.WaveformSquare, 440.0, 44100)
|
|
|
|
|
if len(harmonics) == 0 {
|
|
|
|
|
t.Fatal("WaveformSquare: expected at least one harmonic, got 0")
|
|
|
|
|
}
|
|
|
|
|
nyquist := 22050.0
|
|
|
|
|
for _, h := range harmonics {
|
|
|
|
|
if float64(h.Ratio)*440.0 >= nyquist {
|
|
|
|
|
t.Errorf("WaveformSquare: harmonic ratio %d exceeds Nyquist (freq=%.1f)", h.Ratio, float64(h.Ratio)*440.0)
|
|
|
|
|
}
|
|
|
|
|
expectedAmp := 1.0 / float64(h.Ratio)
|
|
|
|
|
if abs(h.Amplitude-expectedAmp) > 1e-9 {
|
|
|
|
|
t.Errorf("WaveformSquare: harmonic %d: expected amplitude %.6f, got %.6f", h.Ratio, expectedAmp, h.Amplitude)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWaveformPresetHarmonics_Sawtooth(t *testing.T) {
|
|
|
|
|
harmonics := synth.WaveformPresetHarmonics(synth.WaveformSawtooth, 440.0, 44100)
|
|
|
|
|
if len(harmonics) == 0 {
|
|
|
|
|
t.Fatal("WaveformSawtooth: expected at least one harmonic, got 0")
|
|
|
|
|
}
|
|
|
|
|
nyquist := 22050.0
|
|
|
|
|
for _, h := range harmonics {
|
|
|
|
|
if float64(h.Ratio)*440.0 >= nyquist {
|
|
|
|
|
t.Errorf("WaveformSawtooth: harmonic ratio %d exceeds Nyquist (freq=%.1f)", h.Ratio, float64(h.Ratio)*440.0)
|
|
|
|
|
}
|
|
|
|
|
expectedAmp := 1.0 / float64(h.Ratio)
|
|
|
|
|
if abs(h.Amplitude-expectedAmp) > 1e-9 {
|
|
|
|
|
t.Errorf("WaveformSawtooth: harmonic %d: expected amplitude %.6f, got %.6f", h.Ratio, expectedAmp, h.Amplitude)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWaveformPresetHarmonics_Triangle(t *testing.T) {
|
|
|
|
|
harmonics := synth.WaveformPresetHarmonics(synth.WaveformTriangle, 440.0, 44100)
|
|
|
|
|
if len(harmonics) == 0 {
|
|
|
|
|
t.Fatal("WaveformTriangle: expected at least one harmonic, got 0")
|
|
|
|
|
}
|
|
|
|
|
nyquist := 22050.0
|
|
|
|
|
sign := 1.0
|
|
|
|
|
for i, h := range harmonics {
|
|
|
|
|
if float64(h.Ratio)*440.0 >= nyquist {
|
|
|
|
|
t.Errorf("WaveformTriangle: harmonic ratio %d exceeds Nyquist (freq=%.1f)", h.Ratio, float64(h.Ratio)*440.0)
|
|
|
|
|
}
|
|
|
|
|
expectedAmp := sign / float64(h.Ratio*h.Ratio)
|
|
|
|
|
if abs(h.Amplitude-expectedAmp) > 1e-9 {
|
|
|
|
|
t.Errorf("WaveformTriangle: harmonic %d (index %d): expected amplitude %.6f, got %.6f", h.Ratio, i, expectedAmp, h.Amplitude)
|
|
|
|
|
}
|
|
|
|
|
sign = -sign
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWaveformPresetHarmonics_Custom(t *testing.T) {
|
|
|
|
|
harmonics := synth.WaveformPresetHarmonics(synth.WaveformCustom, 440.0, 44100)
|
|
|
|
|
if harmonics != nil {
|
|
|
|
|
t.Errorf("WaveformCustom: expected nil, got %v", harmonics)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBandlimitedHarmonicsNoAliasing(t *testing.T) {
|
|
|
|
|
waveforms := []synth.WaveformType{
|
|
|
|
|
synth.WaveformSine,
|
|
|
|
|
synth.WaveformSquare,
|
|
|
|
|
synth.WaveformSawtooth,
|
|
|
|
|
synth.WaveformTriangle,
|
|
|
|
|
}
|
|
|
|
|
for _, cfg := range synth.ClassFreqConfigs {
|
|
|
|
|
for _, wt := range waveforms {
|
|
|
|
|
harmonics := synth.WaveformPresetHarmonics(wt, cfg.BaseHz, 44100)
|
|
|
|
|
for _, h := range harmonics {
|
|
|
|
|
freq := float64(h.Ratio) * cfg.BaseHz
|
|
|
|
|
if freq >= 22050.0 {
|
|
|
|
|
t.Errorf("waveform %d, baseHz=%.1f: harmonic ratio %d produces freq=%.1f >= Nyquist 22050", wt, cfg.BaseHz, h.Ratio, freq)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWaveformPresetHarmonics_SquareOddOnly(t *testing.T) {
|
|
|
|
|
harmonics := synth.WaveformPresetHarmonics(synth.WaveformSquare, 440.0, 44100)
|
|
|
|
|
for _, h := range harmonics {
|
|
|
|
|
if h.Ratio%2 == 0 {
|
|
|
|
|
t.Errorf("WaveformSquare: found even ratio %d (should be odd-only)", h.Ratio)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWaveformPresetHarmonics_TriangleOddOnly(t *testing.T) {
|
|
|
|
|
harmonics := synth.WaveformPresetHarmonics(synth.WaveformTriangle, 440.0, 44100)
|
|
|
|
|
for _, h := range harmonics {
|
|
|
|
|
if h.Ratio%2 == 0 {
|
|
|
|
|
t.Errorf("WaveformTriangle: found even ratio %d (should be odd-only)", h.Ratio)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWaveformPresetHarmonics_SawtoothConsecutive(t *testing.T) {
|
|
|
|
|
harmonics := synth.WaveformPresetHarmonics(synth.WaveformSawtooth, 440.0, 44100)
|
|
|
|
|
for i, h := range harmonics {
|
|
|
|
|
expected := i + 1
|
|
|
|
|
if h.Ratio != expected {
|
|
|
|
|
t.Errorf("WaveformSawtooth: index %d: expected ratio %d, got %d", i, expected, h.Ratio)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 17:34:45 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 17:33:03 +01:00
|
|
|
// abs returns the absolute value of x.
|
|
|
|
|
func abs(x float64) float64 {
|
|
|
|
|
if x < 0 {
|
|
|
|
|
return -x
|
|
|
|
|
}
|
|
|
|
|
return x
|
|
|
|
|
}
|