chromaflock/AGENTS.md
2026-07-13 20:57:38 +02:00

12 KiB
Raw Blame History

AGENTS.md — ChromaFlock Development Guide

Project Overview

ChromaFlock is a JUCE-based subtractive synthesizer plugin (VST3/AU/Standalone). Built with CMake, targets macOS. 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

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