--- phase: 09-frequency-design-and-group-architecture plan: "01" subsystem: synth tags: [frequency-design, group-architecture, config, oscillator-fix] dependency_graph: requires: [08-01] provides: [FreqConfig.Group, rebalanced-ClassFreqConfigs, oscillator-normalization-fix] affects: [synth, config, bank_test, config_test] tech_stack: added: [] patterns: - "Major-second ladder Hz allocation for family-band frequency design" - "math.Abs normalization for alternating-sign harmonic series in additive oscillator" key_files: created: [] modified: - synth/config.go - synth/config_test.go - synth/oscillator.go - synth/bank_test.go - config/config_test.go decisions: - "Group field added to FreqConfig as a string (not enum) — extensible for Phase 10 new family names without code changes" - "Within-family spacing satisfies major second (1.122 ratio): 65->73 = 1.123, 73->82 = 1.123, 82->133 = 1.622 (Infrastructure skips slots reserved for Phase 10), 150->169 = 1.127 (Web), 771->866 = 1.123 (Unknown)" - "Oscillator normalization fixed to use math.Abs — alternating-sign harmonics (triangle wave) were causing output to exceed [-1,1] bounds when using WaveformPresetHarmonics" metrics: duration: "6m 30s" completed: "2026-03-27" tasks_completed: 2 files_modified: 5 --- # Phase 9 Plan 1: Frequency Design and Group Architecture Summary FreqConfig gains a Group string field; all 14 ClassFreqConfigs entries rebalanced to major-second ladder in family frequency bands (65-1375 Hz), with family-specific waveforms. Oscillator normalization bug fixed for alternating-sign harmonic series. ## What Was Built ### Task 1: Add Group field to FreqConfig and rebalance ClassFreqConfigs `synth/config.go` was updated with: 1. `Group string` field added to `FreqConfig` struct after `WaveformType` 2. Frequency allocation table comment block documenting all 32 designed slots (Phase 9 + Phase 10 placeholders) 3. Stale `NumLayers` and `GainPerLayer` constants removed (Phase 8 cleanup carried forward) 4. All 14 `ClassFreqConfigs` entries rebalanced to the major-second ladder: - Infrastructure (Triangle, 65-133 Hz): ICMP=65, NTP=73, DHCP=82, DNS=133 - Web (Sawtooth, 150-169 Hz): HTTPS=150, HTTP=169 - Mail (Triangle, 214 Hz): SMTP=214 - Remote Access (Square, 343 Hz): SSH=343 - Unknown (Custom harmonics, 771-1375 Hz): Unknown1=771, Unknown2=866, Unknown3=972, Unknown4=1091, OtherTCP=1225, OtherUDP=1375 Non-Unknown entries use `WaveformPresetHarmonics`; Unknown entries retain hand-tuned `{1,1.0},{2,0.8},{3,0.4}` with `WaveformType` left as zero value (`WaveformCustom`) so `bank.go` uses stored harmonics directly. ### Task 2: Update synth tests and fix oscillator normalization bug `synth/config_test.go`: - `TestGroupFieldPopulated` added: iterates all ClassFreqConfigs entries, fails if any have empty Group - `TestHarmonicsNonEmpty` threshold changed from `< 2` to `< 1` (accepts single-harmonic sine entries for Phase 10 SIP) **Auto-fix (Rule 1 - Bug): Oscillator normalization for alternating-sign harmonics** Found during Task 2 verification: `TestNewBankCustomConfigNoClip` was failing with `L=-1.023`. Root cause: `oscillator.go` `Advance()` accumulated `totalWeight += h.Amplitude` (signed sum), but `WaveformPresetHarmonics(WaveformTriangle, ...)` produces alternating-sign amplitudes. The signed sum (~0.916 for 65 Hz triangle with 170 harmonics) is much smaller than the absolute sum (~1.232), causing the normalization denominator to be deflated, which inflated output amplitude beyond [-1, 1]. Fix: changed to `totalWeight += math.Abs(h.Amplitude)` in `oscillator.go`. The old config used positive-only amplitudes `{k, 1/k}` so the bug was latent. The new `WaveformPresetHarmonics(WaveformTriangle, ...)` exposed it. **Test updates (Rule 1 - Bug):** - `synth/bank_test.go`: `TestStereoPan` updated from ClassDHCP (new pan=0.1, right-biased) to ClassSSH (pan=-0.7, wide-left); `TestNewBankCustomConfigNoClip` passes after oscillator fix - `config/config_test.go`: `TestLoadPartialOverrideFrequency` changed from hardcoded `WaveformCustom` to `synth.ClassFreqConfigs[classify.ClassICMP].WaveformType` (ICMP is now WaveformTriangle); `TestAutoFreqSkipsBuiltins` changed from hardcoded 175.0 to `synth.ClassFreqConfigs[classify.ClassHTTPS].BaseHz` (now 150.0) ## Verification Results ``` go build ./synth/... → exit 0 go test ./synth/... -v -count=1 → 42 tests PASS go test ./... → all 7 packages PASS grep -c 'Group:' synth/config.go → 14 grep 'Group string' synth/config.go → FOUND grep -c 'BaseHz: 65.0' synth/config.go → ICMP confirmed grep '// Frequency Allocation Table' synth/config.go → FOUND ``` ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Fixed oscillator normalization for alternating-sign harmonic amplitudes** - **Found during:** Task 2 verification (`go test ./synth/...`) - **Issue:** `oscillator.Advance()` used `totalWeight += h.Amplitude` (signed sum). Triangle wave harmonics from `WaveformPresetHarmonics` alternate signs (1, -1/9, 1/25, -1/49...), making signed total ~0.916 vs absolute total ~1.232. This caused 34% undercount, so output exceeded [-1, 1] bounds. - **Fix:** Changed to `totalWeight += math.Abs(h.Amplitude)` in `synth/oscillator.go` - **Files modified:** `synth/oscillator.go`, `synth/bank_test.go`, `config/config_test.go` - **Commit:** c97682d **2. [Rule 1 - Bug] Updated TestStereoPan for new ClassDHCP pan value** - **Found during:** Task 2 verification - **Issue:** TestStereoPan expected ClassDHCP to be left-panned (old pan=-0.75) but new config has pan=0.1 (right-biased) - **Fix:** Changed test to use ClassSSH (pan=-0.7, clearly left-biased in new config) - **Files modified:** `synth/bank_test.go` - **Commit:** c97682d **3. [Rule 1 - Bug] Updated config_test.go for new default values** - **Found during:** Task 2 running `go test ./...` - **Issue:** Two config tests had hardcoded values from old defaults (ICMP WaveformCustom, HTTPS BaseHz=175.0) - **Fix:** Changed assertions to derive expected values from `synth.ClassFreqConfigs` defaults - **Files modified:** `config/config_test.go` - **Commit:** c97682d ## Known Stubs None. All 14 ClassFreqConfigs entries have populated Group, Hz, waveform, and harmonics. Phase 10 slot placeholders are documented in comments only. ## Self-Check: PASSED - synth/config.go: FOUND - synth/config_test.go: FOUND - synth/oscillator.go: FOUND - 09-01-SUMMARY.md: FOUND - Commit 812f0de: FOUND (feat: Group field + rebalanced ClassFreqConfigs) - Commit c97682d: FOUND (fix: TestGroupFieldPopulated + oscillator normalization + test updates)