# AGENTS.md — ChromaFlock Development Guide

## Project Overview

ChromaFlock is a JUCE-based subtractive synthesizer plugin (VST3/AU/Standalone on macOS, VST3/Standalone on Linux).
Built with CMake, targets macOS and Linux. All DSP is custom (no heavy third-party libs).

## Directory Structure

```
chromaflock/
├── CMakeLists.txt              # Build config, link libraries
├── JUCE/                       # JUCE framework (gitignored, not in repo)
├── Source/
│   ├── PluginProcessor.h/.cpp  # Audio processor, APVTS, synth management
│   ├── PluginEditor.h/.cpp     # UI: LAF, MainContentComponent, ChromaFlockEditor
│   └── DSP/
│       ├── Oscillator.h        # 5 waveforms (Sine, Saw, Square, Triangle, Noise)
│       ├── Filter.h            # TPT SVF (LP12, LP24, BP, HP, Notch)
│       ├── Envelope.h          # ADSR envelope (linear ramps)
│       └── Voice.h             # SubtractiveVoice, SubtractiveSound
└── AGENTS.md                   # This file
```

## Build & Run

```bash
# Configure (first time or after CMakeLists change)
cmake -B build -DCMAKE_BUILD_TYPE=Release

# Build
cmake --build build --config Release -j8

# Output locations
#   build/ChromaFlock_artefacts/Release/VST3/ChromaFlock.vst3
#   build/ChromaFlock_artefacts/Release/au/ChromaFlock.component
#   build/ChromaFlock_artefacts/Release/Standalone/ChromaFlock.app
```

Plugins auto-copy to system folders via `COPY_PLUGIN_AFTER_BUILD TRUE`.

## CMake Configuration

- **C++ standard**: C++17
- **juce_dsp**: NOT currently linked (needed for visualizer FFT — add `juce::juce_dsp` to `target_link_libraries`)
- **Linked libraries**: juce_audio_basics, juce_audio_devices, juce_audio_formats, juce_audio_plugin_client, juce_audio_processors, juce_audio_utils, juce_core, juce_data_structures, juce_events, juce_graphics, juce_gui_basics, juce_gui_extra

## DSP Architecture

### Signal Flow (per voice)
```
Osc1 + Osc2 → Amplitude Envelope × Velocity → Filter (cutoff modulated by Filter Env + Key Tracking) → Saturation (tanh) → Output
```

### Key DSP Classes

**Oscillator** (`DSP/Oscillator.h`)
- 5 waveforms via `Waveform` enum
- Phase accumulator, setFrequency/setWaveform/setLevel/setPan/setPhase
- Output is stereo (pan-law: `sqrt((1±pan)/2)`)
- NOTE: `juce::Random random` member — needs `#include <juce_core/...>` but currently compiles without explicit include

**Filter** (`DSP/Filter.h`)
- TPT (Topology-Preserving Transform) SVF, 2 cascaded biquad stages
- `FilterType` enum: LowPass12, LowPass24, BandPass, HighPass, Notch
- Coefficients computed from tan-based transform
- `prepare(sampleRate)` resets internal state

**ADSREnvelope** (`DSP/Envelope.h`)
- Linear ramp attack/decay/release
- `ADSRStage` enum: Idle, Attack, Decay, Sustain, Release
- `prepare(sampleRate)` resets state to Idle

**SubtractiveVoice** (`DSP/Voice.h`)
- Inherits `juce::SynthesiserVoice`
- Contains: osc1, osc2, filter, env (amp), filterEnv
- `renderNextBlock()` processes sample-by-sample: osc → envelope → filter → drive → output
- **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`.

## Plugin Processor

**ChromaFlockProcessor** (`PluginProcessor.h/.cpp`)
- `juce::AudioProcessorValueTreeState apvts` — all parameters
- `juce::Synthesiser synth` — 16 voices (`maxVoices = 16`)
- `prepareToPlay()`: sets sample rate, calls `updateVoiceParameters()`
- `processBlock()`: clears buffer, calls `updateVoiceParameters()`, then `synth.renderNextBlock()`
- State save/restore via XML binary copy

### Parameters (27 total)

| ID                  | Name             | Range              | Default |
|---------------------|------------------|--------------------|---------|
| osc1Wave            | OSC1 Wave        | Choice(5)          | 1 (Saw) |
| osc1Oct             | OSC1 Octave      | -3..3 (int)        | 0       |
| osc1Semi            | OSC1 Semi        | -12..12 (int)      | 0       |
| osc1Fine            | OSC1 Fine        | -100..100          | 0       |
| osc1Level           | OSC1 Level       | 0..1               | 0.7     |
| osc2Wave            | OSC2 Wave        | Choice(5)          | 2 (Square) |
| osc2Oct             | OSC2 Octave      | -3..3 (int)        | -1      |
| osc2Semi            | OSC2 Semi        | -12..12 (int)      | 0       |
| osc2Fine            | OSC2 Fine        | -100..100          | 7       |
| osc2Level           | OSC2 Level       | 0..1               | 0.7     |
| phaseOffset         | Phase Offset     | 0..1               | 0.5     |
| filterType          | Filter Type      | Choice(5)          | 0 (LP12)|
| filterCutoff        | Filter Cutoff    | 20..20000 (log)    | 8000    |
| filterRes           | Filter Res       | 0..1               | 0.3     |
| filterEnvAmt        | Filter Env Amt   | 0..1               | 0       |
| keyTrack            | Key Tracking     | 0..1               | 0.5     |
| fEnvAttack          | Filter Attack    | 0.001..5 (log)     | 0.01    |
| fEnvDecay           | Filter Decay     | 0.001..5 (log)     | 0.3     |
| fEnvSustain         | Filter Sustain   | 0..1               | 0.5     |
| fEnvRelease         | Filter Release   | 0.001..10 (log)    | 0.5     |
| envAttack           | Attack           | 0.001..5 (log)     | 0.01    |
| envDecay            | Decay            | 0.001..5 (log)     | 0.3     |
| envSustain          | Sustain          | 0..1               | 0.7     |
| envRelease          | Release          | 0.001..10 (log)    | 0.5     |
| pan                 | Pan              | -1..1              | 0       |
| drive               | Drive            | 1..5 (log)         | 1.5     |
| masterLevel         | Master Level     | 0..1               | 0.8     |

## UI Architecture

### Class Hierarchy
```
ChromaFlockEditor (juce::AudioProcessorEditor)
  └── MainContentComponent (juce::Component) — scaled via AffineTransform::scale()
        ├── Section labels (gold, 13pt bold)
        ├── Combo boxes (osc1/osc2 wave, filter type, UI scale)
        └── Knobs (112×112, RotaryVerticalDrag, custom LAF)
```

### Scaling System
- `MainContentComponent` is painted/laid out at `baseWidth × baseHeight`
- `ChromaFlockEditor::resized()` applies `AffineTransform::scale(currentScale)` to content
- `setUIScale()` resizes the editor window to `baseWidth*scale × baseHeight*scale`
- Scale options: 100%, 125%, 150%, 200%

### Layout Dimensions (base coordinates, 1× scale)
- **baseWidth**: 1660
- **baseHeight**: 480 (needs to increase to ~620 for visualizers)
- **Section Y origin**: 56px (below header line at y=48)
- **Knob size**: 112×112
- **Knob spacing**: 10px
- **Label height**: 18px
- **Combo box height**: 48px

### Section Layout (from paint/drawSection calls)
| Section    | X    | Y   | W    | H    |
|------------|------|-----|------|------|
| OSC 1      | 10   | 52  | 498  | 200  |
| OSC 2      | 520  | 52  | 620  | 200  |
| FILTER     | 1152 | 52  | 498  | 200  |
| AMP ENV    | 10   | 262 | 498  | 150  |
| FILTER ENV | 520  | 262 | 498  | 150  |
| MASTER     | 1030 | 262 | 376  | 150  |

### Look-and-Feel (KnobLookAndFeel)
- Inherits `juce::LookAndFeel_V4`
- `drawRotarySlider()` renders:
  - 3D-ish appearance: dark circle fill, colored outline, gold thumb dot, pointer line
  - Label drawn at top of bounds (`labelH = 12px` currently)
- Colors:
  - Knob body: `0xff2a2a2a` fill, `0xff555555` outline
  - Thumb/pointer: `0xffccaa44` / `0xffeebb55`
  - Section labels: `0xffccaa44` (gold)
  - Knob labels: `0xff888888`, 10pt font
  - 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
- Add getter method: `const float* getScopeData() const`
- Add `juce::juce_dsp` to CMakeLists.txt for FFT

### Visualizer Components (HIGH PRIORITY)
Two new classes to add to `PluginEditor.h`:

**WaveformDisplay** (juce::Component)
- Reads from processor scope buffer
- Draws waveform as line graph on dark background
- Timer callback at 30fps
- 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()`.

## Theme Colors Reference

| Element          | Color       | Usage                        |
|------------------|-------------|------------------------------|
| Background       | `0xff1a1a1a`| Main background              |
| Knob body        | `0xff2a2a2a`| Circle fill                  |
| Knob outline     | `0xff555555`| Circle stroke                |
| Gold accent      | `0xffccaa44`| Title, section labels, thumb |
| Pointer gold     | `0xffeebb55`| Knob pointer line            |
| Text light       | `0xffcccccc`| Parameter values, knob labels|
| Text dim         | `0xff888888`| Subtitle, knob label text    |
| Section border   | `0xff333333`| Section box outlines         |
| Text box bg      | `0xff1a1a1a`| Slider text boxes            |
| Popup highlight  | `0xffccaa44`| ComboBox dropdown highlight  |
| Popup text       | `0xff1a1a1a`| ComboBox dropdown text on highlight |

## Testing / Verification

- Build with `cmake --build build --config Release -j8`
- Load VST3 in any DAW (Ableton, Logic, Reaper, etc.)
- Test: play notes, sweep filter cutoff, turn up ENV AMT knob (crash test for filter fix)
- Test: UI scale switching (100% → 200% → back)
- Test: state save/load (preset recall)
