Files
yoloyolo/.planning/phases/02-audio-synthesis-engine/02-01-PLAN.md
T

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-audio-synthesis-engine 01 execute 1
synth/config.go
synth/oscillator.go
synth/layer.go
synth/config_test.go
synth/layer_test.go
go.mod
go.sum
true
SYNTH-01
SYNTH-02
truths artifacts key_links
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
path provides contains
synth/config.go FreqConfig, HarmonicDef types and ClassFreqConfigs table for all 11 classes ClassFreqConfigs
path provides contains
synth/oscillator.go Phase-accumulator oscillator with Advance method func (o *Oscillator) Advance
path provides contains
synth/layer.go Layer struct with EMA amplitude, whisper floor, target updates func (l *Layer) UpdateTarget
from to via pattern
synth/config.go classify/types.go import classify.TrafficClass as map key map[classify.TrafficClass]FreqConfig
from to via pattern
synth/layer.go synth/oscillator.go Layer embeds/uses Oscillator 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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

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):

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):

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 <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> 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)

<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>
After completion, create `.planning/phases/02-audio-synthesis-engine/02-01-SUMMARY.md`