- **KNOWN BUG**: Single `filter` instance processes both L and R sequentially — state gets contaminated between channels, causing crashes when filter env amount > 0. FIX: add `Filter filterR` member, process left through `filter`, right through `filterR`.
- Text boxes: `0xffcccccc` text on `0xff1a1a1a` background
### ComboBox LAF (not yet implemented — currently uses default V4 look)
- Needs custom 3D LAF similar to knobs (gradient, shadow, highlight)
### Header
- Title: "CHROMAFLOCK" at (20, 8), 22pt bold gold
- Subtitle: "SUBTRACTIVE SYNTHESIZER" at (20, 30), 10pt gray
- Divider line at y=48, full width, gold
### UI Scale Controls
- Scale label at (1520, 10), 42×20
- Scale combo at (1566, 8), 80×24
## Planned / In-Progress Work
### Filter Crash Fix (HIGH PRIORITY)
**Problem**: Single `Filter filter` in `SubtractiveVoice` is called for both L and R in sequence (lines 87-88 of Voice.h). The filter's internal state (`stage[4]`) is shared/corrupted between channels, causing instability when filter env amount > 0.
**Fix**: Add `Filter filterR` member. In `renderNextBlock()`:
- Process left channel through `filter`
- Process right channel through `filterR`
- In `startNote()`, call `filter.prepare(sr)` AND `filterR.prepare(sr)`
### Scope Buffer for Visualizers (HIGH PRIORITY)
**Need to add to `ChromaFlockProcessor`**:
-`std::array<float, 1024> scopeBuffer` — ring buffer for waveform data
-`std::atomic<int> scopeWritePos` — write position index
- Write to buffer in `processBlock()` after `synth.renderNextBlock()`, interleaving L+R samples
- Positioned at bottom-left of UI (~190px tall, ~830px wide)
**SpectrumDisplay** (juce::Component)
- Uses `juce::dsp::FFT` to compute spectrum from scope buffer
- Draws vertical bars with HSV color gradient (cool→warm based on frequency)
- Timer callback at 30fps
- Positioned at bottom-right of UI (~190px tall, ~830px wide)
### Height Expansion
-`baseHeight` must increase from 480 to ~620
- Bottom sections (AMP ENV, FILTER ENV, MASTER) at y=262 currently end at y=412
- Visualizers fill the remaining space (y=412 to y=612)
- Editor window resized accordingly
### ComboBox 3D LAF (MEDIUM PRIORITY)
Custom look-and-feel for combo boxes matching the 3D knob aesthetic.
## Common Pitfalls & Gotchas
1.**ComboBox items**: Must manually call `addItemList()` BEFORE creating `ComboBoxAttachment` — the attachment alone does NOT populate items.
2.**AffineTransform::scale**: Use `AffineTransform::scale(float, float)`, NOT `scaled()` — the method name in JUCE is `AffineTransform::scale`.
3.**Filter state sharing**: Never share a single `Filter` instance between L/R or between voices — each needs its own filter state.
4.**JUCE modules**: If you use `juce::dsp::FFT` or any `juce_dsp` functionality, you must link `juce::juce_dsp` in CMakeLists.txt AND add `#include <juce_dsp/juce_dsp.h>`.
5.**Denormals**: `processBlock()` wraps with `juce::ScopedNoDenormals` — always keep this.
6.**Voice parameter update**: `updateVoiceParameters()` is called every `processBlock()` — this reads all 27 params and pushes to all 16 voices. This is intentional for simplicity but is CPU-expensive.
7.**Oscillator pan**: The oscillator's `process()` adds directly to output (`*leftOut += ...`), so it's additive between the two oscs in each voice.
8.**Drive math**: `std::tanh(left * drive) / std::tanh(drive)` — when `drive = 1.0`, division by `tanh(1.0) ≈ 0.762` normalizes. `drive` is clamped to min 1.001 in `setParameters()`.