diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 1c9396e..0ea81fb 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -37,7 +37,11 @@ Full details: `.planning/milestones/v1.0-ROADMAP.md`
1. User can set a traffic class to square, sawtooth, or triangle waveform and hear a tonally distinct sound with no audible aliasing or buzzing artifacts
2. Sine waveform continues to produce the same output as v1.0 — no regression
3. The synthesis bank builds layers from a passed-in config map rather than a hardcoded class list
-**Plans**: TBD
+**Plans:** 2 plans
+
+Plans:
+- [ ] 05-01-PLAN.md — Waveform types: WaveformType enum, WaveformPresetHarmonics, NewLayer resolution
+- [ ] 05-02-PLAN.md — Bank decoupling: NewBank injected config map, dynamic GainPerLayer, test updates
### Phase 6: Config Package and Sound Overrides
**Goal**: Users can create a TOML config file to override frequency and waveform per traffic class, with auto-discovery, partial override semantics, and clear validation errors
@@ -70,6 +74,6 @@ Full details: `.planning/milestones/v1.0-ROADMAP.md`
| 2. Audio Synthesis Engine | v1.0 | 3/3 | Complete | 2026-03-26 |
| 3. Pipeline Integration and MVP | v1.0 | 2/2 | Complete | 2026-03-26 |
| 4. Power User Features | v1.0 | 2/2 | Complete | 2026-03-26 |
-| 5. Waveform Types and Bank Decoupling | v1.1 | 0/? | Not started | - |
+| 5. Waveform Types and Bank Decoupling | v1.1 | 0/2 | Not started | - |
| 6. Config Package and Sound Overrides | v1.1 | 0/? | Not started | - |
| 7. Custom Rules and Print-Config | v1.1 | 0/? | Not started | - |
diff --git a/.planning/phases/05-waveform-types-and-bank-decoupling/05-01-PLAN.md b/.planning/phases/05-waveform-types-and-bank-decoupling/05-01-PLAN.md
new file mode 100644
index 0000000..9dea026
--- /dev/null
+++ b/.planning/phases/05-waveform-types-and-bank-decoupling/05-01-PLAN.md
@@ -0,0 +1,216 @@
+---
+phase: 05-waveform-types-and-bank-decoupling
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - synth/config.go
+ - synth/layer.go
+ - synth/waveform_test.go
+autonomous: true
+requirements:
+ - WAVE-01
+ - WAVE-02
+
+must_haves:
+ truths:
+ - "WaveformType enum exists with five values: WaveformCustom (0), WaveformSine, WaveformSquare, WaveformSawtooth, WaveformTriangle"
+ - "WaveformPresetHarmonics returns correct bandlimited harmonic series for each waveform type"
+ - "All generated partials are below Nyquist frequency (22050 Hz)"
+ - "WaveformCustom returns nil, preserving existing hand-tuned harmonics"
+ - "NewLayer resolves waveform presets at construction time, not at render time"
+ - "Existing tests still pass — no regression in v1.0 behavior"
+ artifacts:
+ - path: "synth/config.go"
+ provides: "WaveformType enum and WaveformPresetHarmonics function"
+ contains: "WaveformType"
+ exports: ["WaveformType", "WaveformCustom", "WaveformSine", "WaveformSquare", "WaveformSawtooth", "WaveformTriangle", "WaveformPresetHarmonics"]
+ - path: "synth/layer.go"
+ provides: "Waveform resolution in NewLayer"
+ contains: "WaveformPresetHarmonics"
+ - path: "synth/waveform_test.go"
+ provides: "Tests for waveform preset generation and bandlimiting"
+ key_links:
+ - from: "synth/layer.go"
+ to: "synth/config.go"
+ via: "NewLayer calls WaveformPresetHarmonics when cfg.WaveformType != WaveformCustom"
+ pattern: "WaveformPresetHarmonics\\(cfg\\.WaveformType"
+---
+
+
+Add four waveform types (sine, square, sawtooth, triangle) to the synthesis engine using bandlimited additive synthesis.
+
+Purpose: Enables per-traffic-class waveform selection (WAVE-01) with aliasing-free generation (WAVE-02). This is the foundation that Phase 6 config loading will expose to users.
+Output: WaveformType enum, WaveformPresetHarmonics() function, NewLayer waveform resolution, and comprehensive tests.
+
+
+
+@$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/05-waveform-types-and-bank-decoupling/05-CONTEXT.md
+@.planning/phases/05-waveform-types-and-bank-decoupling/05-RESEARCH.md
+
+@synth/config.go
+@synth/layer.go
+@synth/oscillator.go
+
+
+
+
+From synth/config.go:
+```go
+type HarmonicDef struct {
+ Ratio int
+ Amplitude float64
+}
+
+type FreqConfig struct {
+ BaseHz float64
+ Harmonics []HarmonicDef
+ Pan float64
+}
+
+const SampleRate = 44100
+```
+
+From synth/oscillator.go:
+```go
+func (o *Oscillator) Advance(harmonics []HarmonicDef) float64
+```
+
+From synth/layer.go:
+```go
+func NewLayer(cfg FreqConfig, sampleRate int, tau float64) *Layer
+func (l *Layer) AdvanceSample() float64 // calls l.Osc.Advance(l.Config.Harmonics)
+```
+
+
+
+
+
+
+ Task 1: Add WaveformType enum and WaveformPresetHarmonics function
+ synth/config.go, synth/waveform_test.go
+ synth/config.go, synth/oscillator.go, synth/layer.go
+
+ - TestWaveformPresetHarmonics_Sine: WaveformPresetHarmonics(WaveformSine, 440.0, 44100) returns exactly []HarmonicDef{{Ratio: 1, Amplitude: 1.0}}
+ - TestWaveformPresetHarmonics_Square: WaveformPresetHarmonics(WaveformSquare, 440.0, 44100) returns odd harmonics (1,3,5,...) with amplitude 1/k, all below Nyquist
+ - TestWaveformPresetHarmonics_Sawtooth: WaveformPresetHarmonics(WaveformSawtooth, 440.0, 44100) returns all harmonics (1,2,3,...) with amplitude 1/k, all below Nyquist
+ - TestWaveformPresetHarmonics_Triangle: WaveformPresetHarmonics(WaveformTriangle, 440.0, 44100) returns odd harmonics with alternating sign and 1/k^2 amplitude, all below Nyquist
+ - TestWaveformPresetHarmonics_Custom: WaveformPresetHarmonics(WaveformCustom, 440.0, 44100) returns nil
+ - TestBandlimitedHarmonicsNoAliasing: For each non-custom waveform type, at every ClassFreqConfigs base frequency, no harmonic's Ratio*baseHz exceeds 22050
+ - TestWaveformPresetHarmonics_SquareOddOnly: All returned ratios for square are odd numbers
+ - TestWaveformPresetHarmonics_TriangleOddOnly: All returned ratios for triangle are odd numbers
+ - TestWaveformPresetHarmonics_SawtoothConsecutive: Returned ratios for sawtooth are consecutive integers starting at 1
+
+
+Per D-01 and D-02, add to synth/config.go:
+
+1. Define WaveformType as `type WaveformType int` with five constants:
+ ```go
+ const (
+ WaveformCustom WaveformType = iota // zero value: use FreqConfig.Harmonics as-is
+ WaveformSine
+ WaveformSquare
+ WaveformSawtooth
+ WaveformTriangle
+ )
+ ```
+
+2. Add `WaveformType WaveformType` field to the `FreqConfig` struct (after Pan). Zero value is WaveformCustom, so all existing ClassFreqConfigs entries automatically use their hand-tuned harmonics (per D-03).
+
+3. Add function `WaveformPresetHarmonics(wt WaveformType, baseHz float64, sampleRate int) []HarmonicDef`:
+ - WaveformCustom: return nil
+ - WaveformSine: return `[]HarmonicDef{{Ratio: 1, Amplitude: 1.0}}`
+ - WaveformSquare: loop `k := 1; float64(k)*baseHz < nyquist; k += 2` — append `HarmonicDef{Ratio: k, Amplitude: 1.0 / float64(k)}`
+ - WaveformSawtooth: loop `k := 1; float64(k)*baseHz < nyquist; k++` — append `HarmonicDef{Ratio: k, Amplitude: 1.0 / float64(k)}`
+ - WaveformTriangle: loop `k := 1; float64(k)*baseHz < nyquist; k += 2` with alternating sign — append `HarmonicDef{Ratio: k, Amplitude: sign / float64(k*k)}`, then `sign = -sign` (start `sign := 1.0`)
+ - Nyquist is `float64(sampleRate) / 2.0`
+
+4. Do NOT modify ClassFreqConfigs entries — they retain their hand-tuned harmonics with the default WaveformCustom zero value (per D-03).
+
+5. Create synth/waveform_test.go (package synth_test) with all tests from the behavior block. Use `synth.WaveformPresetHarmonics(...)` calls. The bandlimit test should iterate all ClassFreqConfigs entries, call WaveformPresetHarmonics for each of {WaveformSine, WaveformSquare, WaveformSawtooth, WaveformTriangle} with that entry's BaseHz, and assert `float64(h.Ratio) * baseHz < 22050.0` for every returned HarmonicDef.
+
+
+ cd /home/dev/workspace/yoloyolo && go test ./synth/... -run "TestWaveformPreset|TestBandlimited" -v
+
+
+ - synth/config.go contains `type WaveformType int`
+ - synth/config.go contains `WaveformCustom WaveformType = iota`
+ - synth/config.go contains `WaveformSine`, `WaveformSquare`, `WaveformSawtooth`, `WaveformTriangle`
+ - synth/config.go contains `func WaveformPresetHarmonics(wt WaveformType, baseHz float64, sampleRate int) []HarmonicDef`
+ - FreqConfig struct contains `WaveformType WaveformType`
+ - synth/waveform_test.go exists and contains `TestWaveformPresetHarmonics` and `TestBandlimitedHarmonicsNoAliasing`
+ - `go test ./synth/... -run "TestWaveformPreset|TestBandlimited"` exits 0
+ - `go test ./synth/...` exits 0 (no regression in existing tests)
+
+ WaveformType enum exported with 5 values, WaveformPresetHarmonics generates correct bandlimited series for all 4 waveform types, returns nil for WaveformCustom, all tests pass including existing suite
+
+
+
+ Task 2: Wire waveform resolution into NewLayer
+ synth/layer.go, synth/waveform_test.go
+ synth/layer.go, synth/config.go, synth/waveform_test.go
+
+Per D-02 and research Pattern 2, modify `NewLayer` in synth/layer.go to resolve waveform presets at construction time:
+
+1. In `NewLayer(cfg FreqConfig, sampleRate int, tau float64) *Layer`, add waveform resolution BEFORE creating the Layer. Insert at the top of the function:
+ ```go
+ if cfg.WaveformType != WaveformCustom {
+ cfg.Harmonics = WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, sampleRate)
+ }
+ ```
+ This overwrites cfg.Harmonics (the local copy, not the original) with the bandlimited preset. The rest of NewLayer is unchanged — it stores cfg in `Layer.Config`, so `AdvanceSample` calls `l.Osc.Advance(l.Config.Harmonics)` with the resolved harmonics.
+
+2. Add two tests to synth/waveform_test.go:
+
+ `TestNewLayerResolvesWaveformPreset`: Create a `synth.FreqConfig{BaseHz: 440.0, WaveformType: synth.WaveformSquare}` with empty Harmonics. Call `synth.NewLayer(cfg, synth.SampleRate, 1.0)`. Assert the returned layer's `Config.Harmonics` has length > 1 (preset was resolved). Verify the first harmonic has Ratio=1.
+
+ `TestNewLayerPreservesCustomHarmonics`: Create a `synth.FreqConfig{BaseHz: 440.0, Harmonics: []synth.HarmonicDef{{Ratio: 1, Amplitude: 1.0}, {Ratio: 2, Amplitude: 0.4}}}` with WaveformType left at zero (WaveformCustom). Call `synth.NewLayer(cfg, synth.SampleRate, 1.0)`. Assert harmonics length is exactly 2 and second harmonic Amplitude is 0.4.
+
+ `TestSineRegressionVsCustomHarmonics`: Create two layers — one with `WaveformType: synth.WaveformSine` and empty Harmonics, one with `WaveformType: synth.WaveformCustom` and `Harmonics: []synth.HarmonicDef{{Ratio: 1, Amplitude: 1.0}}`. Advance both 100 samples (calling layer.AdvanceSample on each). Assert samples are identical (both are pure sine at same frequency). Use a target amplitude of 1.0 by calling UpdateTarget(1, 1) first.
+
+Note: The Layer struct fields Config, Osc are exported (capital first letter), so external tests (package synth_test) can access them. However AdvanceSample needs the layer to have a non-zero amplitude — call `layer.UpdateTarget(1, 1)` before advancing to set target to whisper+rate level, then advance enough samples for EMA to converge, OR use a very small tau like 0.001 for fast convergence in tests.
+
+
+ cd /home/dev/workspace/yoloyolo && go test ./synth/... -v -count=1
+
+
+ - synth/layer.go NewLayer function contains `if cfg.WaveformType != WaveformCustom`
+ - synth/layer.go NewLayer function contains `WaveformPresetHarmonics(cfg.WaveformType, cfg.BaseHz, sampleRate)`
+ - synth/waveform_test.go contains `TestNewLayerResolvesWaveformPreset`
+ - synth/waveform_test.go contains `TestNewLayerPreservesCustomHarmonics`
+ - synth/waveform_test.go contains `TestSineRegressionVsCustomHarmonics`
+ - `go test ./synth/...` exits 0 (all existing tests still pass)
+
+ NewLayer resolves waveform presets at construction time. Custom harmonics are preserved when WaveformType is zero. Sine preset produces identical output to single-harmonic custom config. All tests pass.
+
+
+
+
+
+- `go test ./synth/... -v` — all tests pass, including new waveform tests and all existing tests
+- `go test ./encode/...` — encode package still compiles and passes (no changes to it in this plan)
+- `go vet ./synth/...` — no warnings
+
+
+
+- WaveformType enum with 5 values is exported from synth package
+- WaveformPresetHarmonics produces correct harmonic series for all 4 waveform types
+- All generated harmonics are below Nyquist (no aliasing)
+- WaveformCustom preserves existing hand-tuned harmonics
+- NewLayer resolves presets at construction time (not render time)
+- Sine waveform preset produces identical output to v1.0 single-harmonic custom
+- All existing synth and encode tests pass without modification
+
+
+
diff --git a/.planning/phases/05-waveform-types-and-bank-decoupling/05-02-PLAN.md b/.planning/phases/05-waveform-types-and-bank-decoupling/05-02-PLAN.md
new file mode 100644
index 0000000..8b5a49d
--- /dev/null
+++ b/.planning/phases/05-waveform-types-and-bank-decoupling/05-02-PLAN.md
@@ -0,0 +1,350 @@
+---
+phase: 05-waveform-types-and-bank-decoupling
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - "05-01"
+files_modified:
+ - synth/bank.go
+ - synth/bank_test.go
+ - synth/config_test.go
+ - encode/mp3.go
+autonomous: true
+requirements:
+ - WAVE-01
+ - WAVE-02
+
+must_haves:
+ truths:
+ - "NewBank accepts a config map parameter instead of reading the ClassFreqConfigs global"
+ - "GainPerLayer is computed dynamically as 1.0/len(configs) inside NewBank"
+ - "RenderWindow iterates b.layers instead of classify.AllClasses() in both loops"
+ - "encode.RunSynthesis passes synth.ClassFreqConfigs as the default config map"
+ - "All 14 built-in classes still produce the same audio output as v1.0"
+ - "No-clip guarantee holds with dynamic gain scaling"
+ artifacts:
+ - path: "synth/bank.go"
+ provides: "Decoupled OscillatorBank with injected config map"
+ contains: "gainPerLayer"
+ exports: ["NewBank", "OscillatorBank", "RenderWindow"]
+ - path: "encode/mp3.go"
+ provides: "Updated NewBank call site"
+ contains: "synth.ClassFreqConfigs"
+ - path: "synth/bank_test.go"
+ provides: "Updated tests for new NewBank signature"
+ - path: "synth/config_test.go"
+ provides: "Updated TestNumLayersMatchesAllClasses"
+ key_links:
+ - from: "encode/mp3.go"
+ to: "synth/bank.go"
+ via: "synth.NewBank(1.0, synth.ClassFreqConfigs)"
+ pattern: "NewBank\\(1\\.0,\\s*synth\\.ClassFreqConfigs\\)"
+ - from: "synth/bank.go"
+ to: "synth/layer.go"
+ via: "NewLayer(cfg, SampleRate, tau) for each config map entry"
+ pattern: "NewLayer\\(cfg,\\s*SampleRate"
+ - from: "synth/bank.go"
+ to: "synth/config.go"
+ via: "gainPerLayer computed from len(cfgs)"
+ pattern: "1\\.0\\s*/\\s*float64\\(len\\("
+---
+
+
+Decouple OscillatorBank from the global ClassFreqConfigs variable and fix GainPerLayer to be dynamic.
+
+Purpose: Creates the injection seam for Phase 6 config loading (D-05) and fixes gain scaling for variable class counts (D-04). After this plan, NewBank accepts any config map — not just the hardcoded 14 built-in classes.
+Output: Updated bank.go with new NewBank signature, updated encode/mp3.go call site, updated tests.
+
+
+
+@$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/05-waveform-types-and-bank-decoupling/05-CONTEXT.md
+@.planning/phases/05-waveform-types-and-bank-decoupling/05-RESEARCH.md
+@.planning/phases/05-waveform-types-and-bank-decoupling/05-01-SUMMARY.md
+
+@synth/bank.go
+@synth/bank_test.go
+@synth/config_test.go
+@encode/mp3.go
+
+
+
+
+From synth/config.go (after Plan 01):
+```go
+type WaveformType int
+
+const (
+ WaveformCustom WaveformType = iota
+ WaveformSine
+ WaveformSquare
+ WaveformSawtooth
+ WaveformTriangle
+)
+
+type FreqConfig struct {
+ BaseHz float64
+ Harmonics []HarmonicDef
+ Pan float64
+ WaveformType WaveformType
+}
+
+func WaveformPresetHarmonics(wt WaveformType, baseHz float64, sampleRate int) []HarmonicDef
+
+var ClassFreqConfigs = map[classify.TrafficClass]FreqConfig{ ... } // 14 entries, all WaveformCustom
+```
+
+From synth/layer.go (after Plan 01):
+```go
+func NewLayer(cfg FreqConfig, sampleRate int, tau float64) *Layer
+// Now resolves WaveformPresetHarmonics at construction if cfg.WaveformType != WaveformCustom
+```
+
+From classify package:
+```go
+type TrafficClass string
+type WindowSnapshot struct {
+ Counts map[TrafficClass]int64
+ TotalPackets int64
+ WindowIndex int
+}
+func AllClasses() []TrafficClass
+```
+
+
+
+
+
+
+ Task 1: Decouple NewBank and fix GainPerLayer
+ synth/bank.go, encode/mp3.go
+ synth/bank.go, synth/config.go, encode/mp3.go, synth/layer.go
+
+Per D-04 and D-05, refactor bank.go and update the single caller in encode/mp3.go:
+
+1. In synth/bank.go, add `gainPerLayer float64` field to `OscillatorBank` struct:
+ ```go
+ type OscillatorBank struct {
+ layers map[classify.TrafficClass]*Layer
+ tau float64
+ gainPerLayer float64
+ }
+ ```
+
+2. Change `NewBank` signature from `NewBank(tau float64)` to `NewBank(tau float64, cfgs map[classify.TrafficClass]FreqConfig)`:
+ ```go
+ func NewBank(tau float64, cfgs map[classify.TrafficClass]FreqConfig) *OscillatorBank {
+ b := &OscillatorBank{
+ layers: make(map[classify.TrafficClass]*Layer, len(cfgs)),
+ tau: tau,
+ gainPerLayer: 1.0 / float64(len(cfgs)),
+ }
+ for class, cfg := range cfgs {
+ b.layers[class] = NewLayer(cfg, SampleRate, tau)
+ }
+ return b
+ }
+ ```
+ Key changes: iterate `cfgs` (not `classify.AllClasses()`), compute `gainPerLayer` dynamically from `len(cfgs)` (per D-04).
+
+3. Update `RenderWindow` method — change BOTH loops from `classify.AllClasses()` to `b.layers`:
+
+ Loop 1 (UpdateTarget): Change from:
+ ```go
+ for _, class := range classify.AllClasses() {
+ count := snap.Counts[class]
+ b.layers[class].UpdateTarget(count, maxCount)
+ }
+ ```
+ To:
+ ```go
+ for class, layer := range b.layers {
+ count := snap.Counts[class]
+ layer.UpdateTarget(count, maxCount)
+ }
+ ```
+
+ Loop 2 (Render): Change from:
+ ```go
+ for _, class := range classify.AllClasses() {
+ layer := b.layers[class]
+ sample := layer.AdvanceSample()
+ gainL, gainR := PanGains(layer.Config.Pan)
+ sumL += sample * GainPerLayer * gainL
+ sumR += sample * GainPerLayer * gainR
+ }
+ ```
+ To:
+ ```go
+ for _, layer := range b.layers {
+ sample := layer.AdvanceSample()
+ gainL, gainR := PanGains(layer.Config.Pan)
+ sumL += sample * b.gainPerLayer * gainL
+ sumR += sample * b.gainPerLayer * gainR
+ }
+ ```
+ Note: use `b.gainPerLayer` (the instance field) NOT the package constant `GainPerLayer`.
+
+4. Update the `RenderWindow` doc comment to remove "Per D-10: each layer gets GainPerLayer (1/11)" — replace with "Each layer gets 1/N of the total gain where N is the number of layers."
+
+5. Remove the `classify` import from bank.go ONLY IF it is no longer used. After the changes, `classify.TrafficClass` is still used in the `cfgs` parameter type and `b.layers` map type, and `classify.WindowSnapshot` is used in `RenderWindow`. So the import stays. However, `classify.AllClasses()` is no longer called — verify it is not referenced anywhere in bank.go.
+
+6. In encode/mp3.go, change the single `NewBank` call from:
+ ```go
+ bank := synth.NewBank(1.0)
+ ```
+ To:
+ ```go
+ bank := synth.NewBank(1.0, synth.ClassFreqConfigs)
+ ```
+ This preserves v1.0 behavior exactly.
+
+
+ cd /home/dev/workspace/yoloyolo && go build ./... && go vet ./synth/... ./encode/...
+
+
+ - synth/bank.go OscillatorBank struct contains `gainPerLayer float64`
+ - synth/bank.go NewBank signature is `func NewBank(tau float64, cfgs map[classify.TrafficClass]FreqConfig) *OscillatorBank`
+ - synth/bank.go NewBank contains `gainPerLayer: 1.0 / float64(len(cfgs))`
+ - synth/bank.go NewBank iterates `for class, cfg := range cfgs` (NOT classify.AllClasses())
+ - synth/bank.go RenderWindow UpdateTarget loop uses `for class, layer := range b.layers`
+ - synth/bank.go RenderWindow render loop uses `for _, layer := range b.layers`
+ - synth/bank.go RenderWindow render loop uses `b.gainPerLayer` (NOT the GainPerLayer constant)
+ - synth/bank.go does NOT contain `classify.AllClasses()`
+ - encode/mp3.go contains `synth.NewBank(1.0, synth.ClassFreqConfigs)`
+ - `go build ./...` exits 0
+
+ NewBank accepts injected config map. GainPerLayer is dynamic. RenderWindow iterates b.layers in both loops. encode/mp3.go passes ClassFreqConfigs as default. Project compiles.
+
+
+
+ Task 2: Update tests for new NewBank signature and dynamic gain
+ synth/bank_test.go, synth/config_test.go
+ synth/bank_test.go, synth/config_test.go, synth/bank.go, synth/config.go
+
+Per Pitfall 4 from research, update all tests that call NewBank or reference NumLayers:
+
+1. In synth/bank_test.go, update ALL `NewBank(...)` calls to pass `ClassFreqConfigs`:
+
+ - `TestNewBankHas14Layers`: Change `NewBank(1.0)` to `NewBank(1.0, ClassFreqConfigs)`. Keep the assertion `len(b.layers) != 14` and the loop verifying each class has a layer. (This test uses internal package access since it's `package synth`.)
+
+ - `TestRenderWindowOutputLength`: Change `NewBank(1.0)` to `NewBank(1.0, ClassFreqConfigs)`.
+
+ - `TestRenderWindowSilentWhenNoTraffic`: Change `NewBank(1.0)` to `NewBank(1.0, ClassFreqConfigs)`.
+
+ - `TestRenderWindowNonZeroWithTraffic`: Change `NewBank(1.0)` to `NewBank(1.0, ClassFreqConfigs)`.
+
+ - `TestMixerNoClip`: Change `NewBank(0.01)` to `NewBank(0.01, ClassFreqConfigs)`. Also change `classify.AllClasses()` in the count setup loop to iterate `ClassFreqConfigs` keys instead:
+ ```go
+ for class := range ClassFreqConfigs {
+ counts[class] = 1000
+ }
+ ```
+ And update TotalPackets to `int64(len(ClassFreqConfigs)) * 1000`.
+
+ - `TestStereoPan`: Change `NewBank(0.01)` to `NewBank(0.01, ClassFreqConfigs)`.
+
+ - `TestMultipleWindowsEMAConvergence`: Change `NewBank(1.0)` to `NewBank(1.0, ClassFreqConfigs)`.
+
+2. Add a new test `TestNewBankDynamicGain` to synth/bank_test.go:
+ ```go
+ func TestNewBankDynamicGain(t *testing.T) {
+ // Create a config map with only 3 classes
+ cfgs := map[classify.TrafficClass]FreqConfig{
+ classify.ClassICMP: ClassFreqConfigs[classify.ClassICMP],
+ classify.ClassDNS: ClassFreqConfigs[classify.ClassDNS],
+ classify.ClassHTTPS: ClassFreqConfigs[classify.ClassHTTPS],
+ }
+ b := NewBank(0.01, cfgs)
+ if len(b.layers) != 3 {
+ t.Errorf("NewBank with 3 configs has %d layers, want 3", len(b.layers))
+ }
+ // Verify gainPerLayer is 1/3
+ expected := 1.0 / 3.0
+ if b.gainPerLayer != expected {
+ t.Errorf("gainPerLayer = %v, want %v", b.gainPerLayer, expected)
+ }
+ }
+ ```
+
+3. Add a test `TestNewBankCustomConfigNoClip` to synth/bank_test.go to verify no-clip with a non-14 config:
+ ```go
+ func TestNewBankCustomConfigNoClip(t *testing.T) {
+ cfgs := map[classify.TrafficClass]FreqConfig{
+ classify.ClassICMP: ClassFreqConfigs[classify.ClassICMP],
+ classify.ClassDNS: ClassFreqConfigs[classify.ClassDNS],
+ }
+ b := NewBank(0.01, cfgs)
+ counts := map[classify.TrafficClass]int64{
+ classify.ClassICMP: 1000,
+ classify.ClassDNS: 1000,
+ }
+ snap := classify.WindowSnapshot{Counts: counts, TotalPackets: 2000, WindowIndex: 0}
+ for i := 0; i < 10; i++ {
+ for _, frame := range b.RenderWindow(snap) {
+ if frame[0] > 1.0 || frame[0] < -1.0 || frame[1] > 1.0 || frame[1] < -1.0 {
+ t.Fatalf("clipped with 2-class config: L=%v R=%v", frame[0], frame[1])
+ }
+ }
+ }
+ }
+ ```
+
+4. In synth/config_test.go, update `TestNumLayersMatchesAllClasses`:
+ Change from asserting `synth.NumLayers != len(classify.AllClasses())` to asserting `len(synth.ClassFreqConfigs) == len(classify.AllClasses())`:
+ ```go
+ func TestNumLayersMatchesAllClasses(t *testing.T) {
+ if len(synth.ClassFreqConfigs) != len(classify.AllClasses()) {
+ t.Errorf("ClassFreqConfigs has %d entries but AllClasses() has %d entries",
+ len(synth.ClassFreqConfigs), len(classify.AllClasses()))
+ }
+ }
+ ```
+ This preserves the invariant that every built-in class has a config entry, without depending on the NumLayers constant.
+
+
+ cd /home/dev/workspace/yoloyolo && go test ./synth/... ./encode/... -v -count=1
+
+
+ - synth/bank_test.go contains no calls to `NewBank(1.0)` or `NewBank(0.01)` — all calls have two arguments
+ - synth/bank_test.go contains `TestNewBankDynamicGain` with assertion `b.gainPerLayer != expected`
+ - synth/bank_test.go contains `TestNewBankCustomConfigNoClip`
+ - synth/bank_test.go TestMixerNoClip iterates `ClassFreqConfigs` keys (not `classify.AllClasses()`)
+ - synth/config_test.go TestNumLayersMatchesAllClasses asserts `len(synth.ClassFreqConfigs) == len(classify.AllClasses())`
+ - synth/config_test.go TestNumLayersMatchesAllClasses does NOT reference `synth.NumLayers`
+ - `go test ./synth/... ./encode/...` exits 0
+ - `go test ./...` exits 0
+
+ All tests updated to new NewBank two-argument signature. Dynamic gain verified with custom config maps. No-clip test passes with non-14 class counts. TestNumLayersMatchesAllClasses updated. Full test suite green.
+
+
+
+
+
+- `go test ./... -v` — full suite passes with no failures
+- `go vet ./...` — no warnings
+- `go build ./...` — compiles cleanly
+- grep confirms no remaining `classify.AllClasses()` in bank.go
+- grep confirms no remaining single-arg `NewBank(` calls in production or test code
+
+
+
+- NewBank accepts (tau, cfgs) — no global state dependency
+- GainPerLayer computed as 1.0/len(cfgs) — correct for any class count
+- RenderWindow iterates b.layers in both loops — no classify.AllClasses() calls
+- encode.RunSynthesis passes ClassFreqConfigs — v1.0 behavior preserved
+- No-clip guarantee holds for 2-class, 3-class, and 14-class configs
+- Full test suite green (synth + encode + all other packages)
+
+
+