From a97927973532d12a5fe97ac6016e78ad30fcd9e5 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Thu, 26 Mar 2026 12:04:33 +0100 Subject: [PATCH] docs(02-02): complete OscillatorBank and stereo mixer plan - Created 02-02-SUMMARY.md with plan results and 14 test descriptions - Updated STATE.md: advanced to plan 3 of 3, recorded metrics and decisions - Updated ROADMAP.md: phase 02 showing 2/3 summaries complete - Marked SYNTH-03 complete in REQUIREMENTS.md --- .planning/REQUIREMENTS.md | 4 +- .planning/ROADMAP.md | 8 +- .planning/STATE.md | 15 +-- .../02-02-SUMMARY.md | 91 +++++++++++++++++++ 4 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 .planning/phases/02-audio-synthesis-engine/02-02-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 68c940c..99cd370 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -27,7 +27,7 @@ Requirements for initial release. Each maps to roadmap phases. - [x] **SYNTH-01**: Each traffic class generates a distinct ambient/drone layer (layered sine/harmonic waves) - [x] **SYNTH-02**: Drone layer amplitudes evolve over time windows based on traffic volume per class -- [ ] **SYNTH-03**: Multiple drone layers are mixed into a single coherent audio stream without distortion +- [x] **SYNTH-03**: Multiple drone layers are mixed into a single coherent audio stream without distortion ### Output @@ -78,7 +78,7 @@ Which phases cover which requirements. Updated during roadmap creation. | CLAS-04 | Phase 1 | Complete | | SYNTH-01 | Phase 2 | Complete | | SYNTH-02 | Phase 2 | Complete | -| SYNTH-03 | Phase 2 | Pending | +| SYNTH-03 | Phase 2 | Complete | | OUT-01 | Phase 2 | Pending | | OUT-02 | Phase 2 | Pending | | OUT-03 | Phase 2 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 1a93f09..16f0c97 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -47,11 +47,11 @@ Plans: 3. Drone layer amplitude rises and falls with traffic volume over time — sustained traffic sounds louder, quiet periods fade 4. User can specify output path via `-o` flag; it defaults to `netsynth-.mp3` when omitted 5. An empty (zero-packet) input produces a clear error message instead of a corrupt or zero-byte MP3 -**Plans:** 0/3 plans executed +**Plans:** 2/3 plans executed Plans: -- [ ] 02-01-PLAN.md — Environment setup (gcc, ffprobe, go-lame), synth config table, oscillator, EMA layer with tests -- [ ] 02-02-PLAN.md — Stereo mixer (constant-power panning), OscillatorBank multi-layer rendering with tests +- [x] 02-01-PLAN.md — Environment setup (gcc, ffprobe, go-lame), synth config table, oscillator, EMA layer with tests +- [x] 02-02-PLAN.md — Stereo mixer (constant-power panning), OscillatorBank multi-layer rendering with tests - [ ] 02-03-PLAN.md — MP3 encoder package, zero-packet guard, -o CLI flag, ffprobe integration test ### Phase 3: Pipeline Integration and MVP @@ -82,6 +82,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 1. Capture and Classification | 4/4 | Complete | 2026-03-25 | -| 2. Audio Synthesis Engine | 0/3 | Planned | | +| 2. Audio Synthesis Engine | 2/3 | In Progress| | | 3. Pipeline Integration and MVP | 0/? | Not started | - | | 4. Power User Features | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index efba90c..8f2908d 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,13 +3,13 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: Ready to execute -stopped_at: "Completed 02-01: synth config, oscillator, and layer" -last_updated: "2026-03-26T10:59:51.259Z" +stopped_at: "Completed 02-02: OscillatorBank and stereo mixer" +last_updated: "2026-03-26T11:04:17.995Z" progress: total_phases: 4 completed_phases: 1 total_plans: 7 - completed_plans: 4 + completed_plans: 6 --- # Project State @@ -24,7 +24,7 @@ See: .planning/PROJECT.md (updated 2026-03-24) ## Current Position Phase: 02 (audio-synthesis-engine) — EXECUTING -Plan: 2 of 3 +Plan: 3 of 3 ## Performance Metrics @@ -51,6 +51,7 @@ Plan: 2 of 3 | Phase 01-capture-and-classification P02 | 3min | 1 tasks | 4 files | | Phase 01-capture-and-classification P04 | 15min | 2 tasks | 2 files | | Phase 02 P01 | 15min | 2 tasks | 8 files | +| Phase 02 P02 | 10min | 2 tasks | 4 files | ## Accumulated Context @@ -76,6 +77,8 @@ Recent decisions affecting current work: - [Phase 01-04]: Three-stage pipeline: capture -> classify goroutine -> aggregate via buffered channels; 1024-buffered classified channel absorbs burst processing - [Phase 02]: go-lame v0.0.9 used for MP3 encoding (embedded LAME C source, no system library needed) - [Phase 02]: go-audio/wav excluded — writing PCM bytes directly to LameWriter is simpler (per Claude's Discretion grant in CONTEXT.md) +- [Phase 02]: GainPerLayer applied in bank.go during mixing to ensure 11 max layers sum to 1.0 (D-10) +- [Phase 02]: int16 conversion uses float * 32767 to avoid positive overflow at exactly +1.0 ### Pending Todos @@ -89,6 +92,6 @@ None yet. ## Session Continuity -Last session: 2026-03-26T10:59:51.252Z -Stopped at: Completed 02-01: synth config, oscillator, and layer +Last session: 2026-03-26T11:04:17.988Z +Stopped at: Completed 02-02: OscillatorBank and stereo mixer Resume file: None diff --git a/.planning/phases/02-audio-synthesis-engine/02-02-SUMMARY.md b/.planning/phases/02-audio-synthesis-engine/02-02-SUMMARY.md new file mode 100644 index 0000000..51b3e6e --- /dev/null +++ b/.planning/phases/02-audio-synthesis-engine/02-02-SUMMARY.md @@ -0,0 +1,91 @@ +--- +phase: 02-audio-synthesis-engine +plan: "02" +subsystem: synth +tags: [audio-synthesis, stereo-mixing, oscillator-bank, pcm] +dependency_graph: + requires: ["02-01"] + provides: ["02-03"] + affects: ["synth/bank.go", "synth/mixer.go"] +tech_stack: + added: [] + patterns: + - constant-power (equal-power) pan law via cos/sin mapping + - GainPerLayer (1/11) fixed-gain mixing to prevent clipping by construction + - WindowSnapshot -> OscillatorBank.RenderWindow -> [][2]float64 pipeline + - interleaved little-endian int16 PCM bytes for go-lame encoder +key_files: + created: + - synth/bank.go + - synth/bank_test.go + - synth/mixer.go + - synth/mixer_test.go + modified: [] +decisions: + - "GainPerLayer applied in bank.go during mixing (not in layer.go) per D-10 — ensures 11 max layers sum to 1.0" + - "PanGains uses angle = (p+1)/2 * pi/2 mapping so p=-1 -> cos(0)=1.0/sin(0)=0.0 and p=1 -> cos(pi/2)=0.0/sin(pi/2)=1.0" + - "int16 conversion: float * 32767 (not 32768) avoids positive overflow at exactly +1.0" +metrics: + duration: "10min" + completed: "2026-03-26" + tasks: 2 + files: 4 +--- + +# Phase 02 Plan 02: OscillatorBank and Stereo Mixer Summary + +**One-liner:** OscillatorBank with 11 EMA-smoothed layers driven by WindowSnapshot, mixed with 1/11 fixed gain and constant-power stereo panning into interleaved int16 PCM bytes. + +## What Was Built + +### synth/mixer.go + +Provides two core utilities: + +- `PanGains(p float64) (gainL, gainR float64)` — constant-power pan law using cos/sin. Maps [-1, 1] pan position to L/R gain pair where `gainL^2 + gainR^2 = 1.0` at all positions. +- `StereoFramesToInt16Bytes(frames [][2]float64) []byte` — converts stereo float64 frames to interleaved little-endian int16 bytes for go-lame's `Write()` method. Clamps values to [-1.0, 1.0] before conversion to prevent int16 overflow. + +### synth/bank.go + +Provides the core synthesis engine: + +- `NewBank(tau float64) *OscillatorBank` — creates 11 layers, one per TrafficClass, each initialized from `ClassFreqConfigs`. +- `RenderWindow(snap classify.WindowSnapshot) [][2]float64` — updates all layer amplitude targets from the snapshot's packet counts, then renders exactly `SamplesPerWindow` (22050) stereo frames. Each layer contributes `GainPerLayer` (1/11) of headroom, so even 11 simultaneously maxed layers cannot exceed [-1.0, 1.0]. + +## Tests Added (14 new tests) + +**mixer_test.go (7 tests):** +- `TestPanGainsCenter` — verifies center position yields sqrt(2)/2 for both channels +- `TestPanGainsFullLeft` — gainL=1.0, gainR=0.0 at p=-1.0 +- `TestPanGainsFullRight` — gainL=0.0, gainR=1.0 at p=1.0 +- `TestPanGainsPowerPreserved` — gainL^2 + gainR^2 = 1.0 for 9 pan values +- `TestStereoFramesToInt16Bytes` — 1.0/-1.0 maps to 32767/-32767 LE int16 +- `TestStereoFramesToInt16BytesZero` — 0.0/0.0 maps to 4 zero bytes +- `TestClampPreventsOverflow` — 2.0/-2.0 clamps to 32767/-32767 (no wrap-around) + +**bank_test.go (7 tests):** +- `TestNewBankHas11Layers` — bank has exactly 11 layers, one per AllClasses() +- `TestRenderWindowOutputLength` — returns exactly 22050 frames +- `TestRenderWindowSilentWhenNoTraffic` — all-zero frames when no class ever seen +- `TestRenderWindowNonZeroWithTraffic` — ICMP count=100 produces non-zero output +- `TestMixerNoClip` — 11 classes at max count, 10 windows rendered, no |L| or |R| > 1.0 +- `TestStereoPan` — ClassDHCP (pan=-0.75) yields rmsL > rmsR (wide-left verified) +- `TestMultipleWindowsEMAConvergence` — RMS grows over 5 windows with same snapshot + +## Deviations from Plan + +None — plan executed exactly as written. + +## Self-Check + +Files created: +- synth/mixer.go: EXISTS +- synth/mixer_test.go: EXISTS +- synth/bank.go: EXISTS +- synth/bank_test.go: EXISTS + +Commits: +- c9794cd: feat(02-02): stereo mixer utilities with constant-power pan law +- 23dcfdb: feat(02-02): OscillatorBank multi-layer rendering from WindowSnapshot + +## Self-Check: PASSED