mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 04:10:47 +02:00
263 lines
12 KiB
Markdown
263 lines
12 KiB
Markdown
# 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)
|