--- phase: 02-audio-synthesis-engine plan: 01 type: execute wave: 1 depends_on: [] files_modified: - synth/config.go - synth/oscillator.go - synth/layer.go - synth/config_test.go - synth/layer_test.go - go.mod - go.sum autonomous: true requirements: [SYNTH-01, SYNTH-02] must_haves: truths: - "Each of 11 traffic classes has a unique frequency, harmonic profile, and pan position defined" - "Phase-accumulator oscillator produces non-zero PCM samples with harmonics" - "EMA amplitude smoothing moves current amplitude toward target over time" - "Whisper floor prevents amplitude from reaching zero once a class has been seen" - "go-lame v0.0.9 is in go.mod and CGo build succeeds" artifacts: - path: "synth/config.go" provides: "FreqConfig, HarmonicDef types and ClassFreqConfigs table for all 11 classes" contains: "ClassFreqConfigs" - path: "synth/oscillator.go" provides: "Phase-accumulator oscillator with Advance method" contains: "func (o *Oscillator) Advance" - path: "synth/layer.go" provides: "Layer struct with EMA amplitude, whisper floor, target updates" contains: "func (l *Layer) UpdateTarget" key_links: - from: "synth/config.go" to: "classify/types.go" via: "import classify.TrafficClass as map key" pattern: "map\\[classify\\.TrafficClass\\]FreqConfig" - from: "synth/layer.go" to: "synth/oscillator.go" via: "Layer embeds/uses Oscillator" pattern: "Oscillator" --- Build the synthesis foundation: frequency/harmonic config table, phase-accumulator oscillator, and EMA amplitude layer — all with tests. Also install gcc, ffprobe, and go-lame dependency. Purpose: SYNTH-01 (distinct drone layers) and SYNTH-02 (amplitude dynamics) depend on these building blocks. Everything in this plan is tested in isolation before the mixer and encoder are built in Plan 02. Output: `synth/config.go`, `synth/oscillator.go`, `synth/layer.go` with corresponding test files. Environment ready for CGo builds. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-audio-synthesis-engine/02-CONTEXT.md @.planning/phases/02-audio-synthesis-engine/02-RESEARCH.md @classify/types.go From classify/types.go: ```go type TrafficClass string const ( ClassICMP TrafficClass = "ICMP" ClassDNS TrafficClass = "DNS" ClassHTTPS TrafficClass = "HTTPS" ClassHTTP TrafficClass = "HTTP" ClassSSH TrafficClass = "SSH" ClassSMTP TrafficClass = "SMTP" ClassNTP TrafficClass = "NTP" ClassDHCP TrafficClass = "DHCP" ClassOtherTCP TrafficClass = "other-TCP" ClassOtherUDP TrafficClass = "other-UDP" ClassUnknown TrafficClass = "unknown" ) func AllClasses() []TrafficClass { ... } type WindowSnapshot struct { Counts map[TrafficClass]int64 TotalPackets int64 WindowIndex int } ``` Task 1: Environment setup and dependency installation go.mod, go.sum - go.mod (current dependencies) - .planning/phases/02-audio-synthesis-engine/02-RESEARCH.md (environment notes) 1. Confirm Go is available. Check `which go` — if not found, check common paths: `/usr/local/go/bin/go`, `/home/dev/tools/go-install/go/bin/go`, or install via `sudo apt-get install -y golang-go`. Export PATH so `go` is accessible for subsequent commands. 2. Install gcc (required for sjzar/go-lame CGo build): ``` sudo apt-get update && sudo apt-get install -y gcc ``` 3. Install ffprobe (required for MP3 validation in tests): ``` sudo apt-get install -y ffmpeg ``` 4. Add go-lame to go.mod: ``` go get github.com/sjzar/go-lame@v0.0.9 ``` 5. Run `go mod tidy` to clean up. 6. Verify CGo build works by running `CGO_ENABLED=1 go build ./...` — should succeed with no errors. NOTE: Skipping go-audio/wav — per CONTEXT.md Claude's Discretion grant for "WAV intermediate format usage," the WAV intermediate is unnecessary. Phase 2 research (Pitfall 4) confirmed go-audio/wav requires io.WriteSeeker which bytes.Buffer does not satisfy. Writing interleaved int16 PCM bytes directly to go-lame's LameWriter.Write() is simpler and eliminates one dependency. CLAUDE.md updated to reflect this decision. go version && gcc --version && ffprobe -version && grep "go-lame" go.mod && CGO_ENABLED=1 go build ./... - `go version` outputs a version string containing "go1.2" - `gcc --version` outputs a version string - `ffprobe -version` outputs a version string - go.mod contains the line `github.com/sjzar/go-lame v0.0.9` - `CGO_ENABLED=1 go build ./...` exits 0 Go, gcc, and ffprobe are available. go-lame v0.0.9 is in go.mod. CGo build succeeds. Task 2: Synth config, oscillator, and layer with tests synth/config.go, synth/oscillator.go, synth/layer.go, synth/config_test.go, synth/layer_test.go - classify/types.go (TrafficClass constants, AllClasses, WindowSnapshot) - .planning/phases/02-audio-synthesis-engine/02-RESEARCH.md (patterns 1-2, frequency table, EMA formula) - .planning/phases/02-audio-synthesis-engine/02-CONTEXT.md (decisions D-01 through D-09) - TestAllClassesHaveConfig: Every class from classify.AllClasses() has an entry in ClassFreqConfigs - TestFrequenciesInRange: All BaseHz values are within 60-800 Hz (per D-01) - TestFrequenciesUnique: No two classes share the same BaseHz - TestHarmonicsNonEmpty: Every class has at least 2 HarmonicDef entries (fundamental + at least 1 harmonic, per D-05) - TestPanPositionsInRange: All Pan values in [-1.0, 1.0] - TestOscillatorAdvance: Oscillator at 440 Hz / 44100 SR produces non-zero samples; 100 samples have both positive and negative values (sine wave) - TestOscillatorPhaseWrap: After 44100 advances, phase stays in [0, 1) - TestOscillatorDistinctFreqs: Oscillators at 65 Hz and 440 Hz produce different sample sequences - TestEMAAmplitudeRise: Layer with target=1.0 and currentAmp=0.0 has currentAmp > 0.5 after 44100 samples (1 second at tau=1.0) - TestEMAAmplitudeDecay: Layer with target=0.03 (whisper floor) and currentAmp=1.0 has currentAmp < 0.5 after 44100 samples - TestWhisperFloor: Layer marked as seen=true with target set from zero-count snapshot has targetAmp >= whisperFloor (0.03) - TestWhisperFloorNotSeenIsZero: Layer with seen=false has targetAmp == 0.0 **synth/config.go** — Create the frequency/harmonic/pan configuration table. Per D-01 through D-06 and D-11/D-12: ```go 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 ) type HarmonicDef struct { Ratio int // harmonic number: 1=fundamental, 2=octave, 3=fifth+octave, etc. Amplitude float64 // relative weight } 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}, classify.ClassUnknown: {437.0, []HarmonicDef{{1, 1.0}, {2, 0.8}, {3, 0.4}}, 0.0}, // 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. } ``` **synth/oscillator.go** — Phase-accumulator oscillator (Pattern 1 from research): ```go package synth import "math" type Oscillator struct { phase float64 freq float64 sr float64 } 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 } ``` **synth/layer.go** — Layer with EMA amplitude smoothing (Pattern 2): ```go 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))) } 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) } 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 } ``` Write tests FIRST (RED), then create the implementation files (GREEN). Run `go test ./synth/...` after each. cd /home/dev/workspace/yoloyolo && go test ./synth/... -v -count=1 - synth/config.go contains `var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{` - synth/config.go contains `WhisperFloor = 0.03` - synth/config.go contains `SampleRate = 44100` - synth/config.go contains `GainPerLayer` - synth/oscillator.go contains `func (o *Oscillator) Advance(harmonics []HarmonicDef) float64` - synth/oscillator.go contains `o.phase -= 1.0` (NOT math.Mod) - synth/layer.go contains `func (l *Layer) UpdateTarget(count int64, maxCount int64)` - synth/layer.go contains `func (l *Layer) AdvanceSample() float64` - synth/layer.go contains `l.whisper + (1.0-l.whisper)*normalizedRate` - synth/config_test.go contains `TestAllClassesHaveConfig` - synth/config_test.go contains `TestFrequenciesInRange` - synth/layer_test.go contains `TestEMAAmplitudeRise` - synth/layer_test.go contains `TestWhisperFloor` - `go test ./synth/...` exits 0 All 11 traffic classes have config entries with unique frequencies in 60-800 Hz. Oscillator produces correct waveform samples. EMA amplitude smoothing converges toward target with whisper floor enforcement. All tests pass. - `go test ./synth/... -v` passes all tests - `CGO_ENABLED=1 go build ./...` succeeds (gcc + go-lame working) - `grep -c "ClassFreqConfigs" synth/config.go` returns at least 1 - Every class from `classify.AllClasses()` has config entry (verified by TestAllClassesHaveConfig) - synth/config.go defines FreqConfig for all 11 TrafficClass values with frequencies in 60-800 Hz range - synth/oscillator.go implements phase-accumulator with harmonic rendering - synth/layer.go implements EMA amplitude smoothing with whisper floor - All tests in synth/ pass - gcc, ffprobe, and go-lame are installed and working After completion, create `.planning/phases/02-audio-synthesis-engine/02-01-SUMMARY.md`