373 lines
14 KiB
Markdown
373 lines
14 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.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
|
|
|
|
<interfaces>
|
|
<!-- From classify/types.go — the input contract for synthesis -->
|
|
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
|
|
}
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Environment setup and dependency installation</name>
|
|
<files>go.mod, go.sum</files>
|
|
<read_first>
|
|
- go.mod (current dependencies)
|
|
- .planning/phases/02-audio-synthesis-engine/02-RESEARCH.md (environment notes)
|
|
</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>go version && gcc --version && ffprobe -version && grep "go-lame" go.mod && CGO_ENABLED=1 go build ./...</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `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
|
|
</acceptance_criteria>
|
|
<done>Go, gcc, and ffprobe are available. go-lame v0.0.9 is in go.mod. CGo build succeeds.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Synth config, oscillator, and layer with tests</name>
|
|
<files>synth/config.go, synth/oscillator.go, synth/layer.go, synth/config_test.go, synth/layer_test.go</files>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<behavior>
|
|
- 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
|
|
</behavior>
|
|
<action>
|
|
**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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/dev/workspace/yoloyolo && go test ./synth/... -v -count=1</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-audio-synthesis-engine/02-01-SUMMARY.md`
|
|
</output>
|