Files
gurix a979279735 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
2026-03-26 12:04:33 +01:00

3.8 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
02-audio-synthesis-engine 02 synth
audio-synthesis
stereo-mixing
oscillator-bank
pcm
requires provides affects
02-01
02-03
synth/bank.go
synth/mixer.go
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
created modified
synth/bank.go
synth/bank_test.go
synth/mixer.go
synth/mixer_test.go
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
duration completed tasks files
10min 2026-03-26 2 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