feat(05-01): add WaveformType enum and WaveformPresetHarmonics function

- WaveformType int with five constants: WaveformCustom (0), WaveformSine,
  WaveformSquare, WaveformSawtooth, WaveformTriangle
- WaveformPresetHarmonics generates bandlimited harmonic series for each type
- WaveformCustom returns nil to preserve existing hand-tuned harmonics
- All generated harmonics are below Nyquist (sampleRate/2)
- FreqConfig gains WaveformType field (zero value = WaveformCustom)
- ClassFreqConfigs converted to named fields (required for new struct field)
- All existing synth tests continue to pass (38 tests total)
This commit is contained in:
2026-03-26 17:34:02 +01:00
parent 88dee31264
commit 82d1e37d0f
+65 -17
View File
@@ -11,6 +11,53 @@ const (
WhisperFloor = 0.03 // D-08/D-09: 3% of max amplitude
)
// WaveformType selects the harmonic preset for a synthesis layer.
// The zero value WaveformCustom preserves existing hand-tuned harmonics in FreqConfig.Harmonics.
type WaveformType int
const (
WaveformCustom WaveformType = iota // zero value: use FreqConfig.Harmonics as-is
WaveformSine // pure fundamental, single harmonic
WaveformSquare // odd harmonics with 1/k amplitude (bandlimited)
WaveformSawtooth // all harmonics with 1/k amplitude (bandlimited)
WaveformTriangle // odd harmonics with alternating 1/k^2 amplitude (bandlimited)
)
// WaveformPresetHarmonics returns the bandlimited harmonic series for the given waveform type
// at the given base frequency and sample rate. Returns nil for WaveformCustom.
// All returned harmonics are below Nyquist (sampleRate/2).
func WaveformPresetHarmonics(wt WaveformType, baseHz float64, sampleRate int) []HarmonicDef {
nyquist := float64(sampleRate) / 2.0
switch wt {
case WaveformCustom:
return nil
case WaveformSine:
return []HarmonicDef{{Ratio: 1, Amplitude: 1.0}}
case WaveformSquare:
var harmonics []HarmonicDef
for k := 1; float64(k)*baseHz < nyquist; k += 2 {
harmonics = append(harmonics, HarmonicDef{Ratio: k, Amplitude: 1.0 / float64(k)})
}
return harmonics
case WaveformSawtooth:
var harmonics []HarmonicDef
for k := 1; float64(k)*baseHz < nyquist; k++ {
harmonics = append(harmonics, HarmonicDef{Ratio: k, Amplitude: 1.0 / float64(k)})
}
return harmonics
case WaveformTriangle:
var harmonics []HarmonicDef
sign := 1.0
for k := 1; float64(k)*baseHz < nyquist; k += 2 {
harmonics = append(harmonics, HarmonicDef{Ratio: k, Amplitude: sign / float64(k*k)})
sign = -sign
}
return harmonics
default:
return nil
}
}
// HarmonicDef defines one partial in an additive synthesizer.
type HarmonicDef struct {
Ratio int // harmonic number: 1=fundamental, 2=octave, 3=fifth+octave, etc.
@@ -19,29 +66,30 @@ type HarmonicDef struct {
// FreqConfig holds synthesis parameters for one traffic class.
type FreqConfig struct {
BaseHz float64
Harmonics []HarmonicDef
Pan float64 // [-1, 1]: -1=full left, 0=center, +1=full right
BaseHz float64
Harmonics []HarmonicDef
Pan float64 // [-1, 1]: -1=full left, 0=center, +1=full right
WaveformType WaveformType // zero value WaveformCustom uses Harmonics as-is
}
// ClassFreqConfigs maps each traffic class to its synthesis parameters.
// Frequencies use musical intervals per D-02/D-03. Harmonics per D-05/D-06.
// Pan positions per D-12.
var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{
classify.ClassICMP: {65.0, []HarmonicDef{{1, 1.0}, {2, 0.4}, {3, 0.15}}, 0.0},
classify.ClassDNS: {110.0, []HarmonicDef{{1, 1.0}, {2, 0.5}, {3, 0.25}}, -0.2},
classify.ClassHTTPS: {175.0, []HarmonicDef{{1, 1.0}, {2, 0.6}, {3, 0.3}}, 0.2},
classify.ClassHTTP: {220.0, []HarmonicDef{{1, 1.0}, {2, 0.5}, {4, 0.2}}, -0.35},
classify.ClassSSH: {330.0, []HarmonicDef{{1, 1.0}, {3, 0.6}, {5, 0.3}}, 0.35},
classify.ClassSMTP: {440.0, []HarmonicDef{{1, 1.0}, {2, 0.3}, {3, 0.1}}, -0.55},
classify.ClassNTP: {520.0, []HarmonicDef{{1, 1.0}, {2, 0.25}}, 0.55},
classify.ClassDHCP: {600.0, []HarmonicDef{{1, 1.0}, {2, 0.35}, {3, 0.15}}, -0.75},
classify.ClassOtherTCP: {700.0, []HarmonicDef{{1, 1.0}, {2, 0.2}}, 0.75},
classify.ClassOtherUDP: {780.0, []HarmonicDef{{1, 1.0}, {2, 0.2}}, -0.75},
classify.ClassICMP: {BaseHz: 65.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.4}, {3, 0.15}}, Pan: 0.0},
classify.ClassDNS: {BaseHz: 110.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.5}, {3, 0.25}}, Pan: -0.2},
classify.ClassHTTPS: {BaseHz: 175.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.6}, {3, 0.3}}, Pan: 0.2},
classify.ClassHTTP: {BaseHz: 220.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.5}, {4, 0.2}}, Pan: -0.35},
classify.ClassSSH: {BaseHz: 330.0, Harmonics: []HarmonicDef{{1, 1.0}, {3, 0.6}, {5, 0.3}}, Pan: 0.35},
classify.ClassSMTP: {BaseHz: 440.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.3}, {3, 0.1}}, Pan: -0.55},
classify.ClassNTP: {BaseHz: 520.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.25}}, Pan: 0.55},
classify.ClassDHCP: {BaseHz: 600.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.35}, {3, 0.15}}, Pan: -0.75},
classify.ClassOtherTCP: {BaseHz: 700.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.2}}, Pan: 0.75},
classify.ClassOtherUDP: {BaseHz: 780.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.2}}, Pan: -0.75},
// D-05: Unknown buckets in 850-1100 Hz dissonant range, detuned intervals
// D-06: Same dissonant harmonic character {1,1.0},{2,0.8},{3,0.4} for all 4
classify.ClassUnknown1: {862.0, []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, 0.6},
classify.ClassUnknown2: {920.0, []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, -0.6},
classify.ClassUnknown3: {981.0, []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, 0.9},
classify.ClassUnknown4: {1047.0, []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, -0.9},
classify.ClassUnknown1: {BaseHz: 862.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, Pan: 0.6},
classify.ClassUnknown2: {BaseHz: 920.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, Pan: -0.6},
classify.ClassUnknown3: {BaseHz: 981.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, Pan: 0.9},
classify.ClassUnknown4: {BaseHz: 1047.0, Harmonics: []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, Pan: -0.9},
}