92 lines
3.8 KiB
Markdown
92 lines
3.8 KiB
Markdown
---
|
|||
|
|
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
|