feat(02-01): implement synth config, oscillator, and layer
- synth/config.go: FreqConfig, HarmonicDef types; ClassFreqConfigs for all 11 traffic classes with frequencies in 60-800 Hz using musical intervals; pan positions per D-12 - synth/oscillator.go: phase-accumulator oscillator with additive harmonic synthesis and phase subtraction wrap (not math.Mod) per D-05 - synth/layer.go: EMA amplitude smoothing with tau-based alpha; whisper floor (0.03) prevents silence once class has been seen; UpdateTarget/AdvanceSample interface - All 12 tests pass (TestAllClassesHaveConfig, TestFrequenciesUnique, TestEMAAmplitudeRise, etc.)
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package synth
|
||||
|
||||
import "github.com/netsynth/netsynth/classify"
|
||||
|
||||
const (
|
||||
SampleRate = 44100 // D-13: CD quality
|
||||
WindowMs = 500 // matches aggregate.DefaultWindowMs
|
||||
SamplesPerWindow = SampleRate * WindowMs / 1000 // 22050
|
||||
NumLayers = 11
|
||||
GainPerLayer = 1.0 / float64(NumLayers) // D-10: ~0.0909
|
||||
WhisperFloor = 0.03 // D-08/D-09: 3% of max amplitude
|
||||
)
|
||||
|
||||
// HarmonicDef defines one partial in an additive synthesizer.
|
||||
type HarmonicDef struct {
|
||||
Ratio int // harmonic number: 1=fundamental, 2=octave, 3=fifth+octave, etc.
|
||||
Amplitude float64 // relative weight
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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},
|
||||
// D-04: 437 Hz is ~12 cents flat from A4 (440 Hz/SMTP).
|
||||
// Creates 3 Hz beating when SMTP is present = dissonant "doesn't belong" signal.
|
||||
classify.ClassUnknown: {437.0, []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, 0.0},
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package synth
|
||||
|
||||
import "math"
|
||||
|
||||
// EMAAlpha computes the per-sample smoothing coefficient for a given time constant.
|
||||
// tau=1.0 at SR=44100 gives alpha ~0.0000227 (63% of target reached in 1 second). Per D-07.
|
||||
func EMAAlpha(tau float64, sampleRate int) float64 {
|
||||
return 1.0 - math.Exp(-1.0/(tau*float64(sampleRate)))
|
||||
}
|
||||
|
||||
// Layer combines an oscillator with EMA amplitude smoothing for one traffic class.
|
||||
type Layer struct {
|
||||
Config FreqConfig
|
||||
Osc *Oscillator
|
||||
currentAmp float64
|
||||
targetAmp float64
|
||||
alpha float64 // EMA coefficient
|
||||
seen bool // whether this class has ever had count > 0
|
||||
whisper float64 // whisper floor amplitude (D-08)
|
||||
}
|
||||
|
||||
// NewLayer creates a Layer for the given config using the specified sample rate and EMA time constant (tau in seconds).
|
||||
func NewLayer(cfg FreqConfig, sampleRate int, tau float64) *Layer {
|
||||
return &Layer{
|
||||
Config: cfg,
|
||||
Osc: NewOscillator(cfg.BaseHz, sampleRate),
|
||||
alpha: EMAAlpha(tau, sampleRate),
|
||||
whisper: WhisperFloor,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTarget sets the target amplitude from a packet count and max count across all classes.
|
||||
// Per D-07/D-08/D-09: once seen, floor is whisper; amplitude scales linearly with normalized rate.
|
||||
func (l *Layer) UpdateTarget(count int64, maxCount int64) {
|
||||
if count > 0 {
|
||||
l.seen = true
|
||||
}
|
||||
if !l.seen {
|
||||
l.targetAmp = 0.0
|
||||
return
|
||||
}
|
||||
normalizedRate := 0.0
|
||||
if maxCount > 0 {
|
||||
normalizedRate = float64(count) / float64(maxCount)
|
||||
}
|
||||
l.targetAmp = l.whisper + (1.0-l.whisper)*normalizedRate
|
||||
}
|
||||
|
||||
// AdvanceSample renders one sample and advances the EMA amplitude toward target.
|
||||
// Returns the raw mono sample (before pan/gain).
|
||||
func (l *Layer) AdvanceSample() float64 {
|
||||
sample := l.Osc.Advance(l.Config.Harmonics)
|
||||
l.currentAmp += l.alpha * (l.targetAmp - l.currentAmp)
|
||||
return sample * l.currentAmp
|
||||
}
|
||||
|
||||
// CurrentAmp returns the current amplitude (for testing).
|
||||
func (l *Layer) CurrentAmp() float64 {
|
||||
return l.currentAmp
|
||||
}
|
||||
|
||||
// TargetAmp returns the target amplitude (for testing).
|
||||
func (l *Layer) TargetAmp() float64 {
|
||||
return l.targetAmp
|
||||
}
|
||||
|
||||
// Seen returns whether this layer has ever received traffic (for testing).
|
||||
func (l *Layer) Seen() bool {
|
||||
return l.seen
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package synth
|
||||
|
||||
import "math"
|
||||
|
||||
// Oscillator is a phase-accumulator oscillator that generates additive sine waveforms.
|
||||
type Oscillator struct {
|
||||
phase float64
|
||||
freq float64
|
||||
sr float64
|
||||
}
|
||||
|
||||
// NewOscillator creates an oscillator at the given frequency and sample rate.
|
||||
func NewOscillator(freq float64, sampleRate int) *Oscillator {
|
||||
return &Oscillator{freq: freq, sr: float64(sampleRate)}
|
||||
}
|
||||
|
||||
// Advance returns one sample: fundamental + harmonics summed and normalized to [-1, 1].
|
||||
func (o *Oscillator) Advance(harmonics []HarmonicDef) float64 {
|
||||
sum := 0.0
|
||||
totalWeight := 0.0
|
||||
for _, h := range harmonics {
|
||||
sum += h.Amplitude * math.Sin(2*math.Pi*o.phase*float64(h.Ratio))
|
||||
totalWeight += h.Amplitude
|
||||
}
|
||||
o.phase += o.freq / o.sr
|
||||
if o.phase >= 1.0 {
|
||||
o.phase -= 1.0
|
||||
}
|
||||
if totalWeight > 0 {
|
||||
return sum / totalWeight
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Phase returns the current phase (for testing).
|
||||
func (o *Oscillator) Phase() float64 {
|
||||
return o.phase
|
||||
}
|
||||
Reference in New Issue
Block a user