# 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        # 17 waveforms (Sine, Saw, Square, Triangle, Noise, Pulse25, Pulse12, ReverseSaw, FullRectSine, HalfRectSine, Stair4, Stair8, SoftSine, Sinc, ExpPulse, DoubleSine, SuperSaw)
│       ├── 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`)
- 17 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 (32 total)

| ID                  | Name             | Range              | Default |
|---------------------|------------------|--------------------|---------|
| osc1Wave            | OSC1 Wave        | Choice(17)         | 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(17)         | 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     |
| arpEnabled          | Arp On           | Choice(2) Off/On   | 0 (Off) |
| arpPattern          | Arp Pattern      | Choice(20)         | 0 (Up)  |
| arpOctaves          | Arp Octaves      | Choice(3) 1/2/3    | 1 (2)   |
| arpDirection        | Arp Direction    | Choice(2) Up/Down  | 0 (Up)  |
| arpRate             | Arp Rate         | Choice(7) 1/32..2  | 1 (1/16)|

## 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**: 1028
- **Header**: y 0..80 (logo + preset buttons + transpose + scale)
- **Knob size**: 112×112 (main rows), 80×80 (FX/LFO rows)
- **Visualizer row**: y=746, h=128 (VU meter, waveform w/ spectrum overlay, arpeggiator)
- **Piano roll**: y=884, h=136

### Section Layout (from paint/drawSection calls)
| Section      | X    | Y   | W    | H    |
|--------------|------|-----|------|------|
| OSC 1        | 10   | 80  | 498  | 200  |
| OSC 2        | 518  | 80  | 624  | 200  |
| FILTER       | 1152 | 80  | 498  | 200  |
| AMP ENV      | 10   | 290 | 563  | 160  |
| FILTER ENV   | 583  | 290 | 502  | 160  |
| MASTER       | 1095 | 290| 555  | 160  |
| DISTORTION   | 10   | 460 | 510  | 115  |
| COMPRESSION  | 528  | 460 | 468  | 115  |
| PANNING      | 1004 | 460 | 268  | 115  |
| LIMITER      | 1280 | 460 | 370  | 115  |
| LFO 1        | 10   | 586 | 540  | 150  |
| LFO 2        | 560  | 586 | 530  | 150  |
| DELAY        | 1015 | 586 | 375  | 150  |
| REVERB       | 1395 | 586 | 255  | 150  |
| ARPEGGIATOR  | 860  | 746 | 790  | 128  |
| PIANO ROLL   | 10   | 884 | 1640 | 136  |

### 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 (implemented — `ComboBoxLookAndFeel`)
- Custom 3D LAF (gradient spotlight, drop shadow, gold arrow, highlighted popup items)
- `drawPopupMenuItem` highlights the selected entry persistently and inverts text color on hover

### 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

## Implemented Features

### Filter Crash Fix
`SubtractiveVoice` now has separate `filter` (L) and `filterR` (R) instances; `startNote()` prepares both. Resolves the state-sharing crash when filter env amount > 0.

### Arpeggiator
- New class `DSP/Arpeggiator.h` — tempo-synced, pure note-logic (no audio). Tracks held notes (press order for the As Played pattern), builds a pitch pool from held notes × octave range (1–3, clamped to MIDI range), and sequences it per pattern.
- Patterns: Up, Down, UpDown, DownUp, Random, As Played, Chord, Up&Down X, Down&Up X, Random Once, Octave Up, Octave Down, Pinky Up, Pinky Down. X variants are endpoint-exclusive bounces; Random Once shuffles per cycle (no repeat until full pass); Octave Up/Down play each root followed by its ±12 copies spanning the selected OCTAVES count; Pinky Up/Down alternate extremes low/high. Up arpeggiate a minor third + fifth (root, +3, +7) and the Up major variant a major third + fifth (root, +4, +7); UpDown/DownUp arpeggiate major (root, +4, +7) plus an UpDown minor variant (root, +3, +7), each as a separate step. Dropdown labels: "Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Up / Major", "Down / Major", "Up & Down / Minor". "Down" patterns always descend from each held note through the chord tones below it (root, fifth below, third below, next-octave root — e.g. C3, G2, E2, C2 for major), never playing above the held note; any harmony pattern does the same when DIR is set to Down. C3/C4 1/2/3 anchor to the most recently held key and play fixed 8-step loops alternating it and its octave above (3-3-4-3-4-3-3-4, 4-3-3-3-3-3-3-3, 3-3-3-4-3-3-3-3).
- `arpDirection` (Up/Down) controls whether octave copies extend above or below the root AND sorts sequential patterns descending so Direction Down actually steps downward; Random and As Played ignore the inversion. Harmony patterns with Down direction descend through chord tones below each root (see above).
- `arpRate` step divisions (beats): 1/32, 1/16, 1/8, 1/4, 1/2, 1, 2 — tick period = `rateBeats * 60 / bpm`.
- Routing (`PluginProcessor.cpp::processBlock`): when `arpEnabled`, MIDI note events feed the arp instead of the synth; the arp drives `synth.noteOn/noteOff` (with transpose) on each tick via `arpSampleCount` accumulation. Piano-roll `noteOn/noteOff` route through the same logic. When disabled, `arp.reset()` stops any sounding arp note and notes play normally.
- UI: bottom-right section at (860, 746) titled ARPEGGIATOR with 5 combos (ON, PATTERN, OCTAVES, DIR, RATE).

### Visualizers
- `WaveformDisplay` (bottom-left, 60×746, 790×128) draws the scope ring buffer waveform AND a semi-transparent `juce::dsp::FFT` spectrum overlay behind it (30 fps). Requires `juce::juce_dsp` linked and `#include <juce_dsp/juce_dsp.h>`.
- Standalone `SpectrumAnalyzer` component was removed.

### Preset Level Normalization (`Tools/LevelNormalizer.cpp`)
- Standalone JUCE console tool that renders every preset offline (C4, 8s, 44.1 kHz) and derives a new `masterLevel` for consistent loudness: peak normalized to 0.80, RMS-normalized to 0.15, final value = `min(gainPeak, gainRms) * current`, clamped to 0..1.
- Build target: `cmake --build build --target level_normalizer`; run `./build/level_normalizer --apply` to rewrite the `masterLevel` entries in `Source/PresetManager.h` (reports a count mismatch and aborts if preset order/count ever drifts).
- Applied 2026: all 136 presets re-balanced; `masterLevel` now ranges 0.325..1.0. Sustained/washy presets (heavy reverb/delay) get the largest cuts; quiet transient presets clamp at 1.0.
- Caveat: noise-based presets render with slight run-to-run variance (`juce::Random` reseeds per process), so re-running can shift borderline values by a few percent.

## 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)
