chromaflock/AGENTS.md
2026-08-13 01:39:02 +02:00

15 KiB
Raw Blame History

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

# 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 (32 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
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 (13, 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)