commit dca008e86034845e5e2329f92f985a53d5908abe Author: Armin Date: Mon Jul 13 20:57:38 2026 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c338c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Build directories +build/ +Builds/ +cmake-build-*/ + +# JUCE +JUCE/ +!JUCE/.gitkeep + +# IDE +.vscode/ +.idea/ +*.xcodeproj/ +*.xcworkspace/ +*.suo +*.user +*.sln.docstates +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 +*.xcuserstate +.DS_Store + +# Compiled +*.o +*.obj +*.dylib +*.so +*.a +*.lib +*.dll +*.exp +*.pdb + +# Plugin binaries +*.vst3 +*.component +*.app + +# OS +.DS_Store +Thumbs.db diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..54c32a3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,263 @@ +# 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 + +```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 ` 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 scopeBuffer` — ring buffer for waveform data +- `std::atomic 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 `. + +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) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ff9d5ad --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,61 @@ +cmake_minimum_required(VERSION 3.22) +project(ChromaFlock VERSION 1.0.0 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_subdirectory(JUCE) + +juce_add_binary_data(ChromaFlockBinaryData + NAMESPACE BinaryData + SOURCES + chromaflockbg5.png + cfnew.png +) + +juce_add_plugin(ChromaFlock + COMPANY_NAME "ChromaFlock" + PLUGIN_MANUFACTURER_CODE CFlk + PLUGIN_CODE CFlk + FORMATS VST3 AU Standalone + PRODUCT_NAME "ChromaFlock" + IS_SYNTH TRUE + NEEDS_MIDI_INPUT TRUE + IS_MIDI_EFFECT FALSE + EDITOR_WANTS_KEYBOARD_FOCUS FALSE + COPY_PLUGIN_AFTER_BUILD TRUE + VST3_CATEGORIES Synth + AU_MAIN_TYPE kAudioUnitType_MusicDevice + DESCRIPTION "Subtractive Synthesizer" +) + +target_sources(ChromaFlock PRIVATE + Source/PluginProcessor.cpp + Source/PluginEditor.cpp +) + +target_compile_definitions(ChromaFlock PUBLIC + JUCE_VST3_CAN_REPLACE_VST2=0 + JUCE_WEB_BROWSER=0 + JUCE_USE_CURL=0 + JUCE_DISPLAY_SPLASH_SCREEN=0 + JUCE_PLUGIN_WANTS_MIDI=1 + JUCE_MODAL_LOOPS_PERMITTED=0 +) + +target_link_libraries(ChromaFlock PRIVATE + ChromaFlockBinaryData + juce::juce_audio_basics + juce::juce_audio_devices + juce::juce_audio_formats + juce::juce_audio_plugin_client + juce::juce_audio_processors + juce::juce_audio_utils + juce::juce_core + juce::juce_data_structures + juce::juce_dsp + juce::juce_events + juce::juce_graphics + juce::juce_gui_basics + juce::juce_gui_extra +) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a3bbb70 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +# ChromaFlock — Makefile wrapping CMake +# Usage: +# make — configure (if needed) + build Release +# make debug — configure + build Debug +# make clean — remove build directory +# make rebuild — clean + build Release +# make install — build + copy plugins to system dirs +# make run — build Standalone and launch + +BUILD_DIR := build +BUILD_TYPE := Release +CMAKE_FLAGS := -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) +JOBS := $(shell sysctl -n hw.ncpu 2>/dev/null || echo 4) + +# Artefact paths +VST3 := $(BUILD_DIR)/ChromaFlock_artefacts/$(BUILD_TYPE)/VST3/ChromaFlock.vst3 +AU := $(BUILD_DIR)/ChromaFlock_artefacts/$(BUILD_TYPE)/au/ChromaFlock.component +APP := $(BUILD_DIR)/ChromaFlock_artefacts/$(BUILD_TYPE)/Standalone/ChromaFlock.app + +.PHONY: all debug release clean rebuild configure install run help + +all: release + +release: $(VST3) + +debug: + @cmake -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_FLAGS) + @cmake --build $(BUILD_DIR) --config Debug -j$(JOBS) + +$(VST3): + @cmake -B $(BUILD_DIR) $(CMAKE_FLAGS) + @cmake --build $(BUILD_DIR) --config $(BUILD_TYPE) -j$(JOBS) + +configure: + @cmake -B $(BUILD_DIR) $(CMAKE_FLAGS) + +clean: + @rm -rf $(BUILD_DIR) + +rebuild: clean all + +install: all + @echo "Plugins were copied to system folders by CMAKE_COPY_PLUGIN_AFTER_BUILD." + @echo "VST3: ~/Library/Audio/Plug-Ins/VST3/" + @echo "AU: ~/Library/Audio/Plug-Ins/Components/" + +run: $(APP) + open $(APP) + +help: + @echo "ChromaFlock Makefile" + @echo "" + @echo " make — Build Release (VST3/AU/Standalone)" + @echo " make debug — Build Debug" + @echo " make clean — Remove build/" + @echo " make rebuild — Clean + Build Release" + @echo " make configure — Run CMake configure only" + @echo " make install — Show plugin install paths" + @echo " make run — Build and launch Standalone app" + @echo " make help — This message" diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b98017 --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# ChromaFlock + +A subtractive synthesizer plugin built with [JUCE](https://juce.com). VST3, AU, and Standalone formats. macOS. + +## Overview + +ChromaFlock is a fully-featured subtractive synthesizer with dual oscillators, multi-mode filter, dual LFOs, two envelopes, and a complete effects chain. All DSP is custom - no external audio libraries. 55 automatable parameters, 16-voice polyphony, 27 built-in presets, and a real-time visual interface with waveform display, spectrum analyzer, VU meter, and interactive piano keyboard. + +## Obligatory AI-slop disclaimer + +Chromaflock is 99.5% vibe-coded (a.k.a. "ai slop"). If that's a problem for you, please +kindly just use a different synthesizer. There is also absolutely *NO* guarantee this +will work reliably, be useful in any way, or even make any sense whatsoever. + +## Signal Flow + +``` +Osc1 + Osc2 → Amp Envelope × Velocity → Filter → Drive → Distortion → Compressor → Auto-Pan → Delay → Reverb → Output +``` + +## Oscillators + +Two independent oscillators per voice, each with: + +- **5 waveforms**: Sine, Saw, Square, Triangle, Noise +- **Octave shift**: -3 to +3 +- **Semitone shift**: -12 to +12 +- **Fine tune**: ±100 cents +- **Level**: 0.0 – 1.0 +- **Phase offset** (OSC 2): 0.0 – 1.0 for detuned/synced phase relationships +- **Stereo panning**: Equal-power pan law per oscillator, summed additively + +## Filter + +TPT (Topology-Preserving Transform) SVF - stable, alias-free, modulation-friendly. Two independent filter instances per voice for proper L/R stereo processing. + +- **5 types**: Low-Pass 12dB, Low-Pass 24dB, Band-Pass, High-Pass, Notch +- **Cutoff**: 25 Hz – 20 kHz (logarithmic) +- **Resonance**: 0.0 – 1.0 +- **Envelope amount**: Modulates cutoff via filter envelope (±4 octave range) +- **Key tracking**: Cutoff follows pitch, 0% – 100% + +## Envelopes + +Two independent ADSR envelopes per voice - one for amplitude, one for filter modulation: + +| Parameter | Range | +|-----------|-------| +| Attack | 0.001s – 5.0s (log) | +| Decay | 0.001s – 5.0s (log) | +| Sustain | 0.0 – 1.0 | +| Release | 0.001s – 10.0s (log) | + +Linear ramp shape. Both envelopes reset per-note. + +## LFOs + +Two independent LFOs per voice, each with: + +- **4 shapes**: Sine, Triangle, Saw, Square +- **Rate**: 0.05 Hz – 20 Hz (log) +- **Depth**: 0.0 – 1.0 +- **4 destinations**: Filter Cutoff, OSC1 Pitch, OSC2 Pitch, Both OSCs +- Phase resets on note-on +- Pitch modulation range: ±12 semitones + +## Effects Chain + +All effects are stereo and processed in series after the synth voice. + +### Distortion + +- **4 types**: Soft Clip (tanh), Hard Clip, Foldback (wave-folding), Overdrive +- **Amount**: 0.0 – 1.0 (drives input gain 1x – 10x) +- **Mix** (dry/wet): 0.0 – 1.0 +- Bypassed when amount < 0.001 + +### Compressor + +- **Threshold**: -60 dB – 0 dB +- **Ratio**: 1:1 – 20:1 +- **Attack**: 0.1 ms – 100 ms +- **Release**: 10 ms – 1000 ms +- **Makeup gain**: 0 – 24 dB +- **Mix** (dry/wet): 0.0 – 1.0 +- Feed-forward envelope detector with separate attack/release time constants + +### Auto-Pan + +- Sinusoidal LFO panning +- **Rate**: 0.05 Hz – 20 Hz +- **Depth**: 0.0 – 1.0 +- Bypassed when depth < 0.001 + +### Delay + +- **Time**: 10 ms – 1000 ms +- **Feedback**: 0.0 – 0.95 +- **Ping-pong mode**: Cross-channel feedback for bouncing stereo echoes +- **Spread**: 0.25 – 0.75 (secondary tap time) +- **Mix** (dry/wet): 0.0 – 1.0 +- Dual-tap mode when ping-pong is off (wider stereo image) + +### Reverb + +- **Room size**: 0.0 – 1.0 +- **Damping**: 0.0 – 1.0 +- **Mix** (wet/dry): 0.0 – 1.0 +- Fixed stereo width at 0.8 + +## Master + +- **Pan**: Full left – Full right +- **Drive**: 1x – 5x (tanh soft clipping per-voice, normalized) +- **Master Level**: 0.0 – 1.0 +- **Pitch Bend Range**: 1 – 12 semitones + +## Voices + +- **16-voice polyphony** +- Velocity-sensitive amplitude +- Per-sample rendering for smooth modulation +- Denormal protection (`ScopedNoDenormals`) +- All 55 parameters pushed to all voices every block + +## Presets + +27 built-in presets across 7 categories: + +| Category | Presets | +|----------|---------| +| Init | Init | +| Bass | Sub Bass, Reese Bass, Acid Bass, Pluck Bass, Moog Bass, Gritty Bass, FM Bass | +| Lead | Sync Lead, Square Lead, Soft Lead, Saw Lead | +| Pad | Warm Pad, Glass Pad, Dark Pad, Shimmer Pad, Evolving Pad, Choir Pad | +| Keys | Electric Piano, Organ, Clav, Rhodes, Bell Keys, Toy Piano | +| FX1 | Wobble, Distorted Lead, Auto-Pan Spread, Filtered Noise | +| FX2 | Dub Delay, Reverse Reverb, Laser Zap, Space Drone, Ring Mod, Glitch Stutter | + +Preset menu accessible from the PRESET button (top-left). Each preset stores all 55 parameters. + +## UI + +Custom dark theme with gold accents. ~50 knobs across 9 sections. + +### Sections + +| Section | Controls | +|---------|----------| +| OSC 1 | Waveform selector, Octave, Semi, Fine, Level knobs | +| OSC 2 | Waveform selector, Octave, Semi, Fine, Level, Phase knobs | +| Filter | Type selector, Cutoff, Resonance, Env Amount, Key Tracking knobs | +| AMP ENV | Attack, Decay, Sustain, Release knobs | +| FILTER ENV | Attack, Decay, Sustain, Release knobs | +| Master | Pan, Drive, Level, PB Range knobs | +| Distortion | Type selector, Amount, Mix knobs | +| Compressor | Threshold, Ratio, Attack, Release, Makeup, Mix knobs | +| Auto-Pan | Rate, Depth knobs | +| LFO 1/2 | Rate, Depth knobs, Shape and Destination selectors | +| Delay | Time, Feedback, Mix knobs, Ping-Pong selector, Spread knob | +| Reverb | Size, Damping, Mix knobs | + +### Visualizers + +- **VU meter**: RMS + peak level, green-to-red gradient, 30 fps +- **Waveform display**: Real-time from 1024-sample ring buffer, green gradient, 30 fps +- **Spectrum analyzer**: 2048-point FFT, 48-bar log frequency display, Hamming window, 30 fps +- **Piano keyboard**: Interactive C3–C7 (49 keys), click/drag to play, gold highlight on press +- **Pitch bend strip**: Vertical drag control on left side of keyboard, resets to center on release + +### Scaling + +UI scaling: 100%, 125%, 150%, 200% via dropdown (top-right). Window resizes proportionally. + +## Parameters + +55 total parameters, all DAW-automatable and saved with session state via APVTS XML serialization. + +## Building + +Requires JUCE (drop the framework into `./JUCE/`). Then: + +```bash +make # Build Release (VST3/AU/Standalone) +make debug # Build Debug +make clean # Remove build directory +make rebuild # Clean + build +make run # Build and launch Standalone app +``` + +Or directly with CMake: + +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release -j8 +``` + +Output: `build/ChromaFlock_artefacts/Release/VST3/ChromaFlock.vst3` + +Plugins auto-copy to system folders (`~/Library/Audio/Plug-Ins/`) after build. + +## Tech + +- **Language**: C++17 +- **Framework**: JUCE 8 +- **DSP**: All custom - no external audio libs +- **Filter**: TPT SVF (Topology-Preserving Transform State Variable Filter) +- **Build**: CMake + Makefile wrapper +- **Platform**: macOS diff --git a/Source/DSP/Compressor.h b/Source/DSP/Compressor.h new file mode 100644 index 0000000..d1b9fa2 --- /dev/null +++ b/Source/DSP/Compressor.h @@ -0,0 +1,47 @@ +#pragma once +#include +#include + +class Compressor { +public: + void prepare(double sr) { + sampleRate = sr; + envelope = 0.0f; + } + + void setParameters(float thresholdDb, float ratio, float attackMs, float releaseMs, + float makeupDb, float mix) { + threshold = std::pow(10.0f, thresholdDb / 20.0f); + compRatio = ratio; + attackCoeff = std::exp(-1.0f / (attackMs * 0.001f * static_cast(sampleRate))); + releaseCoeff = std::exp(-1.0f / (releaseMs * 0.001f * static_cast(sampleRate))); + makeupGain = std::pow(10.0f, makeupDb / 20.0f); + dryWet = mix; + } + + float process(float input) { + float absIn = std::abs(input); + float coeff = (absIn > envelope) ? attackCoeff : releaseCoeff; + envelope = coeff * envelope + (1.0f - coeff) * absIn; + + float gain = 1.0f; + if (envelope > threshold && compRatio > 1.0f) { + float overDb = 20.0f * std::log10(envelope / threshold); + float compressedDb = overDb / compRatio; + gain = std::pow(10.0f, (compressedDb - overDb) / 20.0f); + } + + float wet = input * gain * makeupGain; + return input + (wet - input) * dryWet; + } + +private: + double sampleRate = 44100.0; + float threshold = 0.1f; + float compRatio = 4.0f; + float attackCoeff = 0.0f; + float releaseCoeff = 0.0f; + float makeupGain = 1.0f; + float dryWet = 1.0f; + float envelope = 0.0f; +}; diff --git a/Source/DSP/Delay.h b/Source/DSP/Delay.h new file mode 100644 index 0000000..77cc1ea --- /dev/null +++ b/Source/DSP/Delay.h @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include + +class Delay { +public: + void prepare(double sr) { + sampleRate = sr; + int maxSamples = static_cast(sr * 2.0); + bufferL.resize(maxSamples, 0.0f); + bufferR.resize(maxSamples, 0.0f); + writePos = 0; + } + + void setParameters(float timeMs, float fb, float mx, bool pp, float spr) { + timeSamples = static_cast(timeMs * 0.001f * sampleRate); + int bufMax = static_cast(bufferL.size()) - 1; + timeSamples = std::clamp(timeSamples, 1, bufMax); + feedback = std::clamp(fb, 0.0f, 0.95f); + mix = std::clamp(mx, 0.0f, 1.0f); + pingpong = pp; + spread = std::clamp(spr, 0.25f, 0.75f); + } + + void process(float& left, float& right) { + int bufSize = static_cast(bufferL.size()); + + if (pingpong) { + int timeR = std::max(1, static_cast(timeSamples * spread)); + int readL = (writePos - timeSamples + bufSize) % bufSize; + int readR = (writePos - timeR + bufSize) % bufSize; + + float outL = bufferR[readR]; + float outR = bufferL[readL]; + + bufferL[writePos] = left + outR * feedback; + bufferR[writePos] = right + outL * feedback; + + writePos = (writePos + 1) % bufSize; + + left = left * (1.0f - mix) + outL * mix; + right = right * (1.0f - mix) + outR * mix; + } else { + int readPos1 = (writePos - timeSamples + bufSize) % bufSize; + int time2 = std::max(1, static_cast(timeSamples * spread)); + int readPos2 = (writePos - time2 + bufSize) % bufSize; + + float tap1L = bufferL[readPos1]; + float tap1R = bufferR[readPos1]; + float tap2L = bufferL[readPos2]; + float tap2R = bufferR[readPos2]; + + float outL = tap1L + tap2L * 0.6f; + float outR = tap1R + tap2R * 0.6f; + + bufferL[writePos] = left + outL * feedback; + bufferR[writePos] = right + outR * feedback; + + writePos = (writePos + 1) % bufSize; + + left = left * (1.0f - mix) + outL * mix; + right = right * (1.0f - mix) + outR * mix; + } + } + +private: + std::vector bufferL, bufferR; + int writePos = 0; + double sampleRate = 44100; + int timeSamples = 8820; + float feedback = 0.3f; + float mix = 0.25f; + bool pingpong = false; + float spread = 0.5f; +}; diff --git a/Source/DSP/Distortion.h b/Source/DSP/Distortion.h new file mode 100644 index 0000000..06d7425 --- /dev/null +++ b/Source/DSP/Distortion.h @@ -0,0 +1,71 @@ +#pragma once +#include +#include + +enum class DistortionType { SoftClip = 0, HardClip, Foldback, Overdrive, NumTypes }; + +class Distortion { +public: + void prepare(double /*sr*/) {} + + void setParameters(DistortionType type, float amount, float mix) { + distType = type; + distAmount = amount; + dryWet = mix; + } + + float process(float input) { + float driven = input * (1.0f + distAmount * 9.0f); + float wet = 0.0f; + + switch (distType) { + case DistortionType::SoftClip: + wet = std::tanh(driven); + break; + + case DistortionType::HardClip: { + float threshold = 1.0f / (1.0f + distAmount * 9.0f); + wet = std::clamp(driven, -threshold, threshold); + break; + } + + case DistortionType::Foldback: { + float threshold = 1.0f + distAmount * 3.0f; + wet = driven; + while (wet > threshold || wet < -threshold) { + if (wet > threshold) + wet = 2.0f * threshold - wet; + else if (wet < -threshold) + wet = -2.0f * threshold - wet; + else + break; + } + if (threshold > 0.0f) + wet /= threshold; + break; + } + + case DistortionType::Overdrive: { + float t = driven; + if (t > 1.0f) t = 1.0f; + else if (t < -1.0f) t = -1.0f; + wet = t * (1.5f - 0.5f * t * t); + break; + } + + default: + wet = driven; + break; + } + + if (distAmount < 0.001f) + return input; + + return input + (wet - input) * dryWet; + } + +private: + DistortionType distType = DistortionType::SoftClip; + float distAmount = 0.0f; + float dryWet = 0.0f; +}; diff --git a/Source/DSP/Envelope.h b/Source/DSP/Envelope.h new file mode 100644 index 0000000..28cd71e --- /dev/null +++ b/Source/DSP/Envelope.h @@ -0,0 +1,87 @@ +#pragma once +#include +#include + +enum class ADSRStage { Idle = 0, Attack, Decay, Sustain, Release }; + +class ADSREnvelope { +public: + void prepare(double sr) { + sampleRate = sr; + stage = ADSRStage::Idle; + level = 0.0f; + } + + void setAttack(float a) { attackTime = std::max(a, 0.001f); } + void setDecay(float d) { decayTime = std::max(d, 0.001f); } + void setSustain(float s) { sustainLevel = std::clamp(s, 0.0f, 1.0f); } + void setRelease(float r) { releaseTime = std::max(r, 0.001f); } + + void noteOn() { + stage = ADSRStage::Attack; + level = 0.0f; + } + + void noteOff() { + if (stage != ADSRStage::Idle) + stage = ADSRStage::Release; + } + + bool isActive() const { return stage != ADSRStage::Idle; } + + float getNextSample() { + switch (stage) { + case ADSRStage::Idle: + level = 0.0f; + break; + + case ADSRStage::Attack: { + float attackInc = 1.0f / (attackTime * static_cast(sampleRate)); + level += attackInc; + if (level >= 1.0f) { + level = 1.0f; + stage = ADSRStage::Decay; + } + break; + } + + case ADSRStage::Decay: { + float decayInc = (1.0f - sustainLevel) / (decayTime * static_cast(sampleRate)); + level -= decayInc; + if (level <= sustainLevel) { + level = sustainLevel; + stage = ADSRStage::Sustain; + } + break; + } + + case ADSRStage::Sustain: + level = sustainLevel; + break; + + case ADSRStage::Release: { + float releaseInc = level / (releaseTime * static_cast(sampleRate)); + level -= releaseInc; + if (level <= 0.0f) { + level = 0.0f; + stage = ADSRStage::Idle; + } + break; + } + } + + return level; + } + + float getCurrentLevel() const { return level; } + ADSRStage getCurrentStage() const { return stage; } + +private: + double sampleRate = 44100.0; + float attackTime = 0.01f; + float decayTime = 0.3f; + float sustainLevel = 0.7f; + float releaseTime = 0.5f; + float level = 0.0f; + ADSRStage stage = ADSRStage::Idle; +}; diff --git a/Source/DSP/Filter.h b/Source/DSP/Filter.h new file mode 100644 index 0000000..cc3ce4a --- /dev/null +++ b/Source/DSP/Filter.h @@ -0,0 +1,110 @@ +#pragma once +#include +#include + +enum class FilterType { LowPass12 = 0, LowPass24, BandPass, HighPass, Notch, NumTypes }; + +class Filter { +public: + void prepare(double sr) { + sampleRate = sr; + reset(); + } + + void reset() { + for (int i = 0; i < 4; ++i) { + stage[i] = 0.0f; + } + } + + void setCoefficients(float cutoff, float res, FilterType type) { + cutoff = std::clamp(cutoff, 20.0f, static_cast(sampleRate * 0.49)); + resonance = std::clamp(res, 0.0f, 1.0f); + filterType = type; + + float g = std::tan(static_cast(pi) * cutoff / static_cast(sampleRate)); + float k = 2.0f - 2.0f * resonance; + float a1 = 1.0f / (1.0f + g * (g + k)); + float a2 = g * a1; + + gCoeff = g; + kCoeff = k; + + m0 = 0.0f; + m1 = 0.0f; + m2 = 0.0f; + m3 = 0.0f; + + switch (filterType) { + case FilterType::LowPass12: + m0 = 0.0f; m1 = 0.0f; m2 = a2; m3 = 0.0f; + break; + case FilterType::LowPass24: + m0 = 0.0f; m1 = 0.0f; m2 = 0.0f; m3 = a2 * a2; + break; + case FilterType::BandPass: + m0 = 0.0f; m1 = a2; m2 = 0.0f; m3 = 0.0f; + break; + case FilterType::HighPass: + m0 = 1.0f; m1 = -a1; m2 = -a2; m3 = 0.0f; + break; + case FilterType::Notch: + m0 = 1.0f; m1 = -a1 * k; m2 = -a2; m3 = 0.0f; + break; + case FilterType::NumTypes: + break; + } + } + + float process(float input) { + float denom = 1.0f + kCoeff * gCoeff + gCoeff * gCoeff; + float hp = (input - (kCoeff + gCoeff) * stage[0] - stage[1]) / denom; + float bp = gCoeff * hp + stage[0]; + float lp = gCoeff * bp + stage[1]; + + stage[0] = gCoeff * hp + bp; + stage[1] = gCoeff * bp + lp; + + float hp2 = (lp - (kCoeff + gCoeff) * stage[2] - stage[3]) / denom; + float bp2 = gCoeff * hp2 + stage[2]; + float lp2 = gCoeff * bp2 + stage[3]; + + stage[2] = gCoeff * hp2 + bp2; + stage[3] = gCoeff * bp2 + lp2; + + float output = 0.0f; + + switch (filterType) { + case FilterType::LowPass12: + output = lp; + break; + case FilterType::LowPass24: + output = lp2; + break; + case FilterType::BandPass: + output = bp; + break; + case FilterType::HighPass: + output = hp; + break; + case FilterType::Notch: + output = input - bp; + break; + case FilterType::NumTypes: + output = input; + break; + } + + return output; + } + +private: + static constexpr float pi = 3.14159265358979323846f; + double sampleRate = 44100.0; + float stage[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + + float gCoeff = 0.0f, kCoeff = 0.0f; + float m0 = 0.0f, m1 = 0.0f, m2 = 0.0f, m3 = 0.0f; + float resonance = 0.0f; + FilterType filterType = FilterType::LowPass12; +}; diff --git a/Source/DSP/LFO.h b/Source/DSP/LFO.h new file mode 100644 index 0000000..83185a0 --- /dev/null +++ b/Source/DSP/LFO.h @@ -0,0 +1,54 @@ +#pragma once +#include + +enum class LFODest { FilterCutoff = 0, OSC1Freq, OSC2Freq, BothOsc }; +enum class LFOShape { Sine = 0, Triangle, Saw, Square }; + +class LFO { +public: + void prepare(double sr) { sampleRate = sr; } + + void setParameters(float rate, float depth, LFOShape s, LFODest d) { + lfoRate = rate; + lfoDepth = depth; + shape = s; + dest = d; + } + + float getNextSample() { + float twoPi = 6.2831853f; + phase += lfoRate / static_cast(sampleRate); + if (phase >= 1.0f) phase -= 1.0f; + + float out = 0.0f; + float p = phase; + + switch (shape) { + case LFOShape::Sine: + out = std::sin(p * twoPi); + break; + case LFOShape::Triangle: + out = 2.0f * std::abs(2.0f * (p - 0.5f)) - 1.0f; + break; + case LFOShape::Saw: + out = 2.0f * p - 1.0f; + break; + case LFOShape::Square: + out = p < 0.5f ? 1.0f : -1.0f; + break; + } + + return out * lfoDepth; + } + + LFODest getDest() const { return dest; } + void reset() { phase = 0.0f; } + +private: + double sampleRate = 44100.0; + float phase = 0.0f; + float lfoRate = 2.0f; + float lfoDepth = 0.0f; + LFOShape shape = LFOShape::Sine; + LFODest dest = LFODest::FilterCutoff; +}; diff --git a/Source/DSP/Oscillator.h b/Source/DSP/Oscillator.h new file mode 100644 index 0000000..64dd9af --- /dev/null +++ b/Source/DSP/Oscillator.h @@ -0,0 +1,80 @@ +#pragma once +#include +#include + +enum class Waveform { Sine = 0, Saw, Square, Triangle, Noise, NumWaveforms }; + +class Oscillator { +public: + void prepare(double sampleRate) { + this->sampleRate = sampleRate; + phase = 0.0; + phaseIncrement = 0.0; + } + + void setFrequency(float freq) { + frequency = freq; + if (sampleRate > 0.0) + phaseIncrement = frequency / static_cast(sampleRate); + } + + void setWaveform(Waveform wf) { waveform = wf; } + void setLevel(float l) { level = std::clamp(l, 0.0f, 1.0f); } + void setPan(float p) { pan = std::clamp(p, -1.0f, 1.0f); } + void setSemitone(float s) { semitoneOffset = s; } + void setFineTune(float f) { fineTune = f; } + void setPhase(float p) { phase = p; } + + float getFrequency() const { return frequency; } + + void process(float* leftOut, float* rightOut) { + float output = 0.0f; + + switch (waveform) { + case Waveform::Sine: + output = std::sin(phase * 2.0f * pi); + break; + case Waveform::Saw: + output = 2.0f * (phase - 0.5f); + break; + case Waveform::Square: + output = (phase < 0.5f) ? 1.0f : -1.0f; + break; + case Waveform::Triangle: + output = 4.0f * std::abs(phase - 0.5f) - 1.0f; + break; + case Waveform::Noise: + output = random.nextFloat() * 2.0f - 1.0f; + break; + default: + output = 0.0f; + break; + } + + phase += phaseIncrement; + if (phase >= 1.0f) phase -= 1.0f; + + output *= level; + + float leftGain = std::sqrt((1.0f - pan) * 0.5f); + float rightGain = std::sqrt((1.0f + pan) * 0.5f); + + *leftOut += output * leftGain; + *rightOut += output * rightGain; + } + + float getCurrentPhase() const { return phase; } + +private: + static constexpr float pi = 3.14159265358979323846f; + float frequency = 440.0f; + float phase = 0.0f; + float phaseIncrement = 0.0f; + float level = 0.7f; + float pan = 0.0f; + float semitoneOffset = 0.0f; + float fineTune = 0.0f; + double sampleRate = 44100.0; + Waveform waveform = Waveform::Saw; + juce::Random random; +}; diff --git a/Source/DSP/Reverb.h b/Source/DSP/Reverb.h new file mode 100644 index 0000000..36eca10 --- /dev/null +++ b/Source/DSP/Reverb.h @@ -0,0 +1,25 @@ +#pragma once +#include + +class ReverbFX { +public: + void prepare(double /*sr*/) {} + + void setParameters(float size, float damping, float width, float mix) { + juce::Reverb::Parameters params; + params.roomSize = size; + params.damping = damping; + params.width = width; + params.wetLevel = mix; + params.dryLevel = 1.0f - mix * 0.5f; + params.freezeMode = 0.0f; + reverb.setParameters(params); + } + + void process(float* left, float* right, int numSamples) { + reverb.processStereo(left, right, numSamples); + } + +private: + juce::Reverb reverb; +}; diff --git a/Source/DSP/Voice.h b/Source/DSP/Voice.h new file mode 100644 index 0000000..cf052ff --- /dev/null +++ b/Source/DSP/Voice.h @@ -0,0 +1,231 @@ +#pragma once +#include "Oscillator.h" +#include "Filter.h" +#include "Envelope.h" +#include "LFO.h" +#include +#include + +struct SubtractiveSound : public juce::SynthesiserSound { + bool appliesToNote(int /*midiNoteNumber*/) override { return true; } + bool appliesToChannel(int /*midiChannel*/) override { return true; } +}; + +class SubtractiveVoice : public juce::SynthesiserVoice { +public: + SubtractiveVoice() = default; + + bool canPlaySound(juce::SynthesiserSound* sound) override { + return dynamic_cast(sound) != nullptr; + } + + void startNote(int midiNoteNumber, float velocity, + juce::SynthesiserSound* sound, int currentPitchWheelPosition) override { + currentMidiNote = midiNoteNumber; + noteVelocity = velocity; + + double sr = getSampleRate(); + if (sr <= 0.0) sr = 44100.0; + osc1.prepare(sr); + osc2.prepare(sr); + env.prepare(sr); + filterEnv.prepare(sr); + filter.prepare(sr); + filterR.prepare(sr); + lfo1.prepare(sr); + lfo2.prepare(sr); + lfo1.reset(); + lfo2.reset(); + + baseFreq1 = 440.0f * std::pow(2.0f, (static_cast(midiNoteNumber) - 69.0f) / 12.0f) + * std::pow(2.0f, semitone1 / 12.0f + fineTune1 / 1200.0f) * std::pow(2.0f, octave1); + baseFreq2 = 440.0f * std::pow(2.0f, (static_cast(midiNoteNumber) - 69.0f) / 12.0f) + * std::pow(2.0f, semitone2 / 12.0f + fineTune2 / 1200.0f) * std::pow(2.0f, octave2); + + osc1.setFrequency(baseFreq1); + osc2.setFrequency(baseFreq2); + osc1.setWaveform(waveform1); + osc2.setWaveform(waveform2); + osc1.setPan(pan); + osc2.setPan(pan); + + osc1.setPhase(0.0f); + osc2.setPhase(phaseOffset2); + + filter.setCoefficients(filterCutoff, filterResonance, filterType); + filterR.setCoefficients(filterCutoff, filterResonance, filterType); + + env.noteOn(); + filterEnv.noteOn(); + } + + void stopNote(float velocity, bool allowTailOff) override { + env.noteOff(); + filterEnv.noteOff(); + + if (!allowTailOff || !env.isActive()) + clearCurrentNote(); + } + + void renderNextBlock(juce::AudioBuffer& outputBuffer, + int startSample, int numSamples) override { + if (!isVoiceActive()) return; + + for (int i = startSample; i < startSample + numSamples; ++i) { + float envSample = env.getNextSample(); + float fEnvSample = filterEnv.getNextSample(); + + if (!env.isActive()) { + clearCurrentNote(); + return; + } + + // LFO outputs + float lfo1Out = lfo1.getNextSample(); + float lfo2Out = lfo2.getNextSample(); + + // Apply pitch bend + LFO to oscillator frequencies + float pitchBendMult = std::pow(2.0f, pitchBendSemitones / 12.0f); + float freq1 = baseFreq1 * pitchBendMult; + float freq2 = baseFreq2 * pitchBendMult; + + auto applyPitchLfo = [&](float lfoOut, LFODest dest) { + float semitoneShift = lfoOut * 12.0f; // +/- 12 semitones range + float pitchMult = std::pow(2.0f, semitoneShift / 12.0f); + if (dest == LFODest::OSC1Freq || dest == LFODest::BothOsc) + freq1 *= pitchMult; + if (dest == LFODest::OSC2Freq || dest == LFODest::BothOsc) + freq2 *= pitchMult; + }; + + if (lfo1Depth > 0.001f) + applyPitchLfo(lfo1Out, lfo1Dest); + if (lfo2Depth > 0.001f) + applyPitchLfo(lfo2Out, lfo2Dest); + + osc1.setFrequency(freq1); + osc2.setFrequency(freq2); + + float left = 0.0f, right = 0.0f; + osc1.process(&left, &right); + osc2.process(&left, &right); + + left *= noteVelocity * envSample; + right *= noteVelocity * envSample; + + // Filter cutoff with env + keytrack + float modCutoff = filterCutoff * std::pow(2.0f, fEnvSample * filterEnvAmount * 4.0f); + float keyTrackFactor = std::pow(2.0f, (static_cast(currentMidiNote) - 60.0f) / 12.0f * keyTrack); + modCutoff *= keyTrackFactor; + + // Apply LFO to filter cutoff + if (lfo1Depth > 0.001f && lfo1Dest == LFODest::FilterCutoff) + modCutoff *= std::pow(2.0f, lfo1Out * 4.0f); + if (lfo2Depth > 0.001f && lfo2Dest == LFODest::FilterCutoff) + modCutoff *= std::pow(2.0f, lfo2Out * 4.0f); + + modCutoff = std::clamp(modCutoff, 25.0f, 1200.0f); + + filter.setCoefficients(modCutoff, filterResonance, filterType); + filterR.setCoefficients(modCutoff, filterResonance, filterType); + + left = filter.process(left); + right = filterR.process(right); + + left = std::tanh(left * drive) / std::tanh(drive); + right = std::tanh(right * drive) / std::tanh(drive); + + outputBuffer.addSample(0, i, left * outputLevel); + outputBuffer.addSample(1, i, right * outputLevel); + } + } + + void pitchWheelMoved(int newPitchWheelValue) override {} + void controllerMoved(int controllerNumber, int newControllerValue) override {} + + void setParameters(Waveform wf1, Waveform wf2, + float oct1, float oct2, + float semi1, float semi2, + float fine1, float fine2, + float lvl1, float lvl2, + float phOff2, + float cut, float res, FilterType ft, + float fEnvAmt, float kTrack, + float att, float dec, float sus, float rel, + float fAtt, float fDec, float fSus, float fRel, + float p, float drv, float outLvl, + float l1Rate, float l1Depth, int l1Shape, int l1Dest, + float l2Rate, float l2Depth, int l2Shape, int l2Dest) { + waveform1 = wf1; + waveform2 = wf2; + octave1 = oct1; + octave2 = oct2; + semitone1 = semi1; + semitone2 = semi2; + fineTune1 = fine1; + fineTune2 = fine2; + osc1.setLevel(lvl1); + osc2.setLevel(lvl2); + phaseOffset2 = phOff2; + filterCutoff = cut; + filterResonance = res; + filterType = ft; + filterEnvAmount = fEnvAmt; + keyTrack = kTrack; + pan = p; + drive = std::max(drv, 1.001f); + outputLevel = outLvl; + + lfo1Depth = l1Depth; + lfo1Dest = static_cast(l1Dest); + lfo1.setParameters(l1Rate, l1Depth, static_cast(l1Shape), lfo1Dest); + + lfo2Depth = l2Depth; + lfo2Dest = static_cast(l2Dest); + lfo2.setParameters(l2Rate, l2Depth, static_cast(l2Shape), lfo2Dest); + + env.setAttack(att); + env.setDecay(dec); + env.setSustain(sus); + env.setRelease(rel); + + filterEnv.setAttack(fAtt); + filterEnv.setDecay(fDec); + filterEnv.setSustain(fSus); + filterEnv.setRelease(fRel); + } + + void setPitchBend(float semitones) { pitchBendSemitones = semitones; } + +private: + Oscillator osc1, osc2; + Filter filter; + Filter filterR; + ADSREnvelope env; + ADSREnvelope filterEnv; + LFO lfo1, lfo2; + + Waveform waveform1 = Waveform::Saw; + Waveform waveform2 = Waveform::Square; + float octave1 = 0.0f, octave2 = -1.0f; + float semitone1 = 0.0f, semitone2 = 0.0f; + float fineTune1 = 0.0f, fineTune2 = 7.0f; + float phaseOffset2 = 0.5f; + float filterCutoff = 800.0f; + float filterResonance = 0.7f; + FilterType filterType = FilterType::LowPass12; + float filterEnvAmount = 0.0f; + float keyTrack = 0.5f; + float pan = 0.0f; + float drive = 1.5f; + float outputLevel = 0.8f; + float baseFreq1 = 440.0f; + float baseFreq2 = 440.0f; + float lfo1Depth = 0.0f; + float lfo2Depth = 0.0f; + LFODest lfo1Dest = LFODest::FilterCutoff; + LFODest lfo2Dest = LFODest::FilterCutoff; + float pitchBendSemitones = 0.0f; + int currentMidiNote = 60; + float noteVelocity = 1.0f; +}; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp new file mode 100644 index 0000000..c108cf4 --- /dev/null +++ b/Source/PluginEditor.cpp @@ -0,0 +1,968 @@ +#include "PluginEditor.h" +#include + +void KnobLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, + float sliderPos, float rotaryStartAngle, + float rotaryEndAngle, juce::Slider& slider) { + auto bounds = juce::Rectangle(x, y, width, height).toFloat(); + + float labelH = 20.0f; + auto dialBounds = bounds.withTrimmedTop(labelH); + + auto radius = juce::jmin(dialBounds.getWidth(), dialBounds.getHeight()) / 2.0f - 4.0f; + auto centreX = dialBounds.getCentreX(); + auto centreY = dialBounds.getCentreY(); + auto rx = centreX - radius; + auto ry = centreY - radius; + auto rw = radius * 2.0f; + auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); + + // Drop shadow + g.setColour(juce::Colour(0x40000000)); + g.fillEllipse(rx + 2.0f, ry + 3.0f, rw, rw); + + // Outer bevel (dark bottom-right, light top-left) + auto bevelPath = juce::Path(); + bevelPath.addEllipse(rx, ry, rw, rw); + + // Knob body gradient — light top-left to dark bottom-right for 3D bulge + juce::ColourGradient bodyGrad(juce::Colour(0xff404040), rx, ry, + juce::Colour(0xff1a1a1a), rx + rw, ry + rw, true); + g.setGradientFill(bodyGrad); + g.fillEllipse(rx, ry, rw, rw); + + // Soft inner shadow ring (bottom-right dark edge, subtle) + g.setColour(juce::Colour(0x18000000)); + auto shadowArc = juce::Path(); + float innerR = radius - 2.0f; + shadowArc.addCentredArc(centreX, centreY, innerR, innerR, 0.0f, + 0.5f, 2.7f, true); + g.strokePath(shadowArc, juce::PathStrokeType(1.5f)); + + // Outer rim + g.setColour(juce::Colour(0xff555555)); + g.drawEllipse(rx, ry, rw, rw, 1.5f); + + // Pointer line with glow — green (left) → yellow (mid) → red (right) + auto indicatorColour = juce::Colour::fromHSV(0.33f * (1.0f - sliderPos), 0.85f, 0.9f, 1.0f); + auto glowColour = juce::Colour::fromHSV(0.33f * (1.0f - sliderPos), 0.85f, 0.9f, 0.4f); + + g.setColour(glowColour); + juce::Path pointerGlow; + pointerGlow.addRoundedRectangle(-2.5f, -radius + 9, 5.0f, radius * 0.5f, 2.0f); + g.fillPath(pointerGlow, juce::AffineTransform::rotation(angle).translated(centreX, centreY)); + + g.setColour(indicatorColour); + juce::Path pointer; + pointer.addRoundedRectangle(-1.5f, -radius + 9, 3.0f, radius * 0.5f, 1.5f); + g.fillPath(pointer, juce::AffineTransform::rotation(angle).translated(centreX, centreY)); + + // Label above knob + auto name = slider.getName(); + if (name.isNotEmpty()) { + g.setColour(juce::Colour(0xff999999)); + g.setFont(juce::Font(13.0f).boldened()); + g.drawText(name, bounds.getX(), bounds.getY(), bounds.getWidth(), labelH, + juce::Justification::centredBottom); + } +} + +void ComboBoxLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, + bool isButtonDown, int /*buttonX*/, int /*buttonY*/, + int /*buttonW*/, int /*buttonH*/, juce::ComboBox& /*box*/) { + auto bounds = juce::Rectangle(0, 0, width, height).toFloat(); + + // Drop shadow + g.setColour(juce::Colour(0x40000000)); + g.fillRoundedRectangle(bounds.getX() + 1.5f, bounds.getY() + 2.0f, + bounds.getWidth(), bounds.getHeight(), 4.0f); + + // Body gradient — light top to dark bottom for 3D raised look + juce::ColourGradient bodyGrad(juce::Colour(0xff404040), bounds.getCentreX(), bounds.getY(), + juce::Colour(0xff1a1a1a), bounds.getCentreX(), bounds.getBottom(), true); + g.setGradientFill(bodyGrad); + g.fillRoundedRectangle(bounds, 4.0f); + + // Top highlight edge + g.setColour(juce::Colour(0x30ffffff)); + g.drawRoundedRectangle(bounds.getX() + 0.5f, bounds.getY() + 0.5f, + bounds.getWidth() - 1.0f, bounds.getHeight() - 1.0f, 4.0f, 1.0f); + + // Bottom shadow edge + g.setColour(juce::Colour(0x30000000)); + g.drawLine(bounds.getX() + 4.0f, bounds.getBottom() - 0.5f, + bounds.getRight() - 4.0f, bounds.getBottom() - 0.5f, 1.0f); + + // Outline + g.setColour(juce::Colour(0xff555555)); + g.drawRoundedRectangle(bounds, 4.0f, 1.0f); + + // Arrow (gold triangle on right) + float arrowSize = 6.0f; + float arrowX = bounds.getRight() - 16.0f; + float arrowY = bounds.getCentreY(); + + juce::Path arrow; + arrow.addTriangle(arrowX, arrowY - arrowSize / 2, + arrowX + arrowSize, arrowY, + arrowX, arrowY + arrowSize / 2); + g.setColour(juce::Colour(0xffccaa44)); + g.fillPath(arrow); +} + +void ComboBoxLookAndFeel::drawPopupMenuItem(juce::Graphics& g, const juce::Rectangle& area, + bool isSeparator, bool /*isActive*/, + bool isHighlighted, bool /*isTicked*/, + bool /*hasSubMenu*/, const juce::String& text, + const juce::String& /*shortcutKeyText*/, + const juce::Drawable* /*icon*/, + const juce::Colour* /*textColour*/) { + if (isSeparator) { + g.setColour(juce::Colour(0xff444444)); + g.drawLine(area.getX() + 8.0f, area.getCentreY(), + area.getRight() - 8.0f, area.getCentreY(), 1.0f); + return; + } + + if (isHighlighted) { + g.setColour(juce::Colour(0xffccaa44)); + g.fillRoundedRectangle(area.reduced(2).toFloat(), 3.0f); + } + + g.setColour(isHighlighted ? juce::Colour(0xff1a1a1a) : juce::Colour(0xffcccccc)); + g.setFont(juce::Font(13.0f)); + g.drawText(text, area.reduced(8, 0), juce::Justification::centredLeft, true); +} + +// --- VU Meter --- +void VuMeter::paint(juce::Graphics& g) { + auto bounds = getLocalBounds().toFloat(); + + juce::ColourGradient bgGrad(juce::Colour(0xff222222), bounds.getCentreX(), bounds.getY(), + juce::Colour(0xff111111), bounds.getCentreX(), bounds.getBottom(), false); + g.setGradientFill(bgGrad); + g.fillRoundedRectangle(bounds, 3.0f); + + float level = processor.getRmsLevel(); + float clamped = juce::jlimit(0.0f, 1.0f, level); + float barH = bounds.getHeight() * clamped; + auto barBounds = juce::Rectangle(bounds.getX(), bounds.getBottom() - barH, bounds.getWidth(), barH); + auto innerBar = barBounds.reduced(1.0f); + + { + juce::Graphics::ScopedSaveState saved(g); + g.setOpacity(0.7f); + + juce::ColourGradient grad(juce::Colour(0xff00cc44), 0.0f, bounds.getBottom(), + juce::Colour(0xffcc2200), 0.0f, bounds.getY(), false); + g.setGradientFill(grad); + g.fillRoundedRectangle(innerBar, 2.0f); + } + + g.setColour(juce::Colour(0xff444444)); + g.drawRoundedRectangle(bounds, 3.0f, 1.0f); + + g.setColour(juce::Colour(0xffccaa44)); + g.setFont(juce::Font(13.0f).boldened()); + g.drawText("VU", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred); +} + +// --- Waveform Display --- +void WaveformDisplay::paint(juce::Graphics& g) { + auto bounds = getLocalBounds().toFloat(); + + juce::ColourGradient bgGrad(juce::Colour(0xff222222), bounds.getCentreX(), bounds.getY(), + juce::Colour(0xff111111), bounds.getCentreX(), bounds.getBottom(), false); + g.setGradientFill(bgGrad); + g.fillRoundedRectangle(bounds, 3.0f); + + g.setColour(juce::Colour(0xff555555)); + g.drawRoundedRectangle(bounds, 3.0f, 1.0f); + + g.setColour(juce::Colour(0xff333333)); + g.drawLine(bounds.getX(), bounds.getCentreY(), bounds.getRight(), bounds.getCentreY(), 1.0f); + + int writePos = processor.scopeWritePos.load(std::memory_order_acquire); + int bufSize = ChromaFlockProcessor::scopeBufferSize; + float w = bounds.getWidth(); + float h = bounds.getHeight(); + float midY = bounds.getY() + h * 0.5f; + + juce::Path wavePath; + bool started = false; + for (int i = 0; i < bufSize; ++i) { + int idx = (writePos + i) % bufSize; + float x = bounds.getX() + (static_cast(i) / static_cast(bufSize)) * w; + float sample = processor.scopeBuffer[idx]; + float y = midY - sample * h * 0.45f; + + if (!started) { + wavePath.startNewSubPath(x, y); + started = true; + } else { + wavePath.lineTo(x, y); + } + } + + juce::Path filledPath(wavePath); + filledPath.lineTo(bounds.getRight(), midY); + filledPath.lineTo(bounds.getX(), midY); + filledPath.closeSubPath(); + + { + juce::Graphics::ScopedSaveState saved(g); + g.setOpacity(0.7f); + + juce::ColourGradient waveGrad(juce::Colour(0xff00ff88), 0.0f, bounds.getY(), + juce::Colour(0xff005522), 0.0f, bounds.getBottom(), false); + g.setGradientFill(waveGrad); + g.fillPath(filledPath); + + g.setColour(juce::Colour(0xff00ff88).withAlpha(0.8f)); + g.strokePath(wavePath, juce::PathStrokeType(1.5f)); + } + + g.setColour(juce::Colour(0xffccaa44)); + g.setFont(juce::Font(13.0f).boldened()); + g.drawText("WAVE", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred); +} + +// --- Spectrum Analyzer --- +void SpectrumAnalyzer::paint(juce::Graphics& g) { + auto bounds = getLocalBounds().toFloat(); + + juce::ColourGradient bgGrad(juce::Colour(0xff222222), bounds.getCentreX(), bounds.getY(), + juce::Colour(0xff111111), bounds.getCentreX(), bounds.getBottom(), false); + g.setGradientFill(bgGrad); + g.fillRoundedRectangle(bounds, 3.0f); + + g.setColour(juce::Colour(0xff555555)); + g.drawRoundedRectangle(bounds, 3.0f, 1.0f); + + std::array fftData{}; + int writePos = processor.fftWritePos.load(std::memory_order_acquire); + int fftSize = ChromaFlockProcessor::fftSize; + + for (int i = 0; i < fftSize; ++i) { + int idx = (writePos + i) % fftSize; + fftData[i] = processor.fftInput[idx]; + } + + for (int i = 0; i < fftSize; ++i) { + float window = 0.5f - 0.5f * std::cos(2.0f * 3.14159265f * static_cast(i) / static_cast(fftSize)); + fftData[i] *= window; + } + + fft->performFrequencyOnlyForwardTransform(fftData.data()); + + int numBars = 48; + float w = bounds.getWidth(); + float h = bounds.getHeight() - 22.0f; + float barBottom = bounds.getBottom() - 2.0f; + float barW = w / static_cast(numBars); + int maxBin = fftSize / 4; + + for (int bar = 0; bar < numBars; ++bar) { + float t = static_cast(bar) / static_cast(numBars); + float tNext = static_cast(bar + 1) / static_cast(numBars); + int binStart = static_cast(std::pow(t, 2.0f) * static_cast(maxBin)); + int binEnd = static_cast(std::pow(tNext, 2.0f) * static_cast(maxBin)); + if (binEnd <= binStart) binEnd = binStart + 1; + if (binEnd > maxBin) binEnd = maxBin; + + float mag = 0.0f; + int count = 0; + for (int b = binStart; b < binEnd; ++b) { + mag += fftData[b]; + ++count; + } + mag = count > 0 ? mag / static_cast(count) : 0.0f; + mag = mag / static_cast(fftSize); + mag = std::sqrt(mag) * 6.0f; + mag = juce::jlimit(0.0f, 1.0f, mag); + + float barH = mag * h; + float x = bounds.getX() + static_cast(bar) * barW; + float bw = barW - 2.0f; + float bx = x + 1.0f; + float by = barBottom - barH; + + { + juce::Graphics::ScopedSaveState saved(g); + g.setOpacity(0.7f); + + juce::ColourGradient barGrad(juce::Colour(0xff00ff66), 0.0f, by, + juce::Colour(0xff005522), 0.0f, barBottom, false); + g.setGradientFill(barGrad); + g.fillRect(bx, by, bw, barH); + } + } + + g.setColour(juce::Colour(0xffccaa44)); + g.setFont(juce::Font(13.0f).boldened()); + g.drawText("SPECTRUM", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred); +} + +MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) + : processorRef(p), vuMeter(p), waveformDisplay(p), spectrumAnalyzer(p), pianoRoll(p) { + + auto setupParam = [&](juce::Slider& knob, std::unique_ptr& attach, + const juce::String& paramId, const juce::String& name) { + setupKnob(knob, name); + attach = std::make_unique(processorRef.apvts, paramId, knob); + }; + + auto setupCB = [&](juce::ComboBox& box, std::unique_ptr& attach, + const juce::String& paramId, const juce::StringArray& items) { + setupCombo(box); + box.addItemList(items, 1); + attach = std::make_unique(processorRef.apvts, paramId, box); + }; + + setupLabel(osc1Label, "OSC 1"); + setupCB(osc1WaveBox, osc1WaveAttach, "osc1Wave", {"Sine", "Saw", "Square", "Triangle", "Noise"}); + setupParam(osc1OctKnob, osc1OctAttach, "osc1Oct", "OCT"); + setupParam(osc1SemiKnob, osc1SemiAttach, "osc1Semi", "SEMI"); + setupParam(osc1FineKnob, osc1FineAttach, "osc1Fine", "FINE"); + setupParam(osc1LevelKnob, osc1LevelAttach, "osc1Level", "LEVEL"); + + setupLabel(osc2Label, "OSC 2"); + setupCB(osc2WaveBox, osc2WaveAttach, "osc2Wave", {"Sine", "Saw", "Square", "Triangle", "Noise"}); + setupParam(osc2OctKnob, osc2OctAttach, "osc2Oct", "OCT"); + setupParam(osc2SemiKnob, osc2SemiAttach, "osc2Semi", "SEMI"); + setupParam(osc2FineKnob, osc2FineAttach, "osc2Fine", "FINE"); + setupParam(osc2LevelKnob, osc2LevelAttach, "osc2Level", "LEVEL"); + setupParam(phaseOffsetKnob, phaseOffsetAttach, "phaseOffset", "PHASE"); + + setupLabel(filterLabel, "FILTER"); + setupCB(filterTypeBox, filterTypeAttach, "filterType", {"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch"}); + setupParam(filterCutoffKnob, filterCutoffAttach, "filterCutoff", "CUTOFF"); + setupParam(filterResKnob, filterResAttach, "filterRes", "RES"); + setupParam(filterEnvAmtKnob, filterEnvAmtAttach, "filterEnvAmt", "ENV AMT"); + setupParam(keyTrackKnob, keyTrackAttach, "keyTrack", "KEY TRK"); + + setupLabel(envLabel, "AMP ENV"); + setupParam(envAttackKnob, envAttackAttach, "envAttack", "ATTACK"); + setupParam(envDecayKnob, envDecayAttach, "envDecay", "DECAY"); + setupParam(envSustainKnob, envSustainAttach, "envSustain", "SUSTAIN"); + setupParam(envReleaseKnob, envReleaseAttach, "envRelease", "RELEASE"); + + setupLabel(fEnvLabel, "FILTER ENV"); + setupParam(fEnvAttackKnob, fEnvAttackAttach, "fEnvAttack", "ATTACK"); + setupParam(fEnvDecayKnob, fEnvDecayAttach, "fEnvDecay", "DECAY"); + setupParam(fEnvSustainKnob, fEnvSustainAttach, "fEnvSustain", "SUSTAIN"); + setupParam(fEnvReleaseKnob, fEnvReleaseAttach, "fEnvRelease", "RELEASE"); + + setupLabel(globalLabel, "MASTER"); + setupParam(panKnob, panAttach, "pan", "PAN"); + setupParam(driveKnob, driveAttach, "drive", "DRIVE"); + setupParam(masterKnob, masterAttach, "masterLevel", "LEVEL"); + setupParam(pbRangeKnob, pbRangeAttach, "pitchBendRange", "PB RANGE"); + + // FX + setupCB(distTypeBox, distTypeAttach, "distType", {"Soft Clip", "Hard Clip", "Foldback", "Overdrive"}); + setupParam(distAmountKnob, distAmountAttach, "distAmount", "AMOUNT"); + setupParam(distMixKnob, distMixAttach, "distMix", "MIX"); + setupParam(compThresholdKnob, compThresholdAttach, "compThreshold", "THRESH"); + setupParam(compRatioKnob, compRatioAttach, "compRatio", "RATIO"); + setupParam(compAttackKnob, compAttackAttach, "compAttack", "ATTACK"); + setupParam(compReleaseKnob, compReleaseAttach, "compRelease", "RELEASE"); + setupParam(compMakeupKnob, compMakeupAttach, "compMakeup", "MAKEUP"); + setupParam(compMixKnob, compMixAttach, "compMix", "MIX"); + setupParam(autoPanRateKnob, autoPanRateAttach, "autoPanRate", "RATE"); + setupParam(autoPanDepthKnob, autoPanDepthAttach, "autoPanDepth", "DEPTH"); + + // LFO + setupParam(lfo1RateKnob, lfo1RateAttach, "lfo1Rate", "RATE"); + setupParam(lfo1DepthKnob, lfo1DepthAttach, "lfo1Depth", "DEPTH"); + setupCB(lfo1ShapeBox, lfo1ShapeAttach, "lfo1Shape", {"Sine", "Triangle", "Saw", "Square"}); + setupCB(lfo1DestBox, lfo1DestAttach, "lfo1Dest", {"Filter", "OSC1", "OSC2", "Both OSC"}); + setupParam(lfo2RateKnob, lfo2RateAttach, "lfo2Rate", "RATE"); + setupParam(lfo2DepthKnob, lfo2DepthAttach, "lfo2Depth", "DEPTH"); + setupCB(lfo2ShapeBox, lfo2ShapeAttach, "lfo2Shape", {"Sine", "Triangle", "Saw", "Square"}); + setupCB(lfo2DestBox, lfo2DestAttach, "lfo2Dest", {"Filter", "OSC1", "OSC2", "Both OSC"}); + + // FX2 + setupParam(delayTimeKnob, delayTimeAttach, "delayTime", "TIME"); + setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK"); + setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX"); + setupCB(delayPingPongBox, delayPingPongAttach, "delayPingPong", {"Off", "On"}); + setupParam(delaySpreadKnob, delaySpreadAttach, "delaySpread", "SPREAD"); + setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE"); + setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP"); + setupParam(reverbMixKnob, reverbMixAttach, "reverbMix", "MIX"); + + // Preset button + presetButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a2a)); + presetButton.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffccaa44)); + presetButton.setLookAndFeel(&comboLaf); + presetButton.onClick = [this]() { showPresetMenu(); }; + addAndMakeVisible(presetButton); + + scaleLabel.setText("SCALE", juce::dontSendNotification); + scaleLabel.setFont(juce::Font(9.0f)); + scaleLabel.setColour(juce::Label::textColourId, juce::Colour(0xff888888)); + scaleLabel.setJustificationType(juce::Justification::centredRight); + addAndMakeVisible(scaleLabel); + + addAndMakeVisible(vuMeter); + addAndMakeVisible(waveformDisplay); + addAndMakeVisible(spectrumAnalyzer); + addAndMakeVisible(pianoRoll); + + setupCombo(uiScaleBox); + uiScaleBox.addItem("100%", 1); + uiScaleBox.addItem("125%", 2); + uiScaleBox.addItem("150%", 3); + uiScaleBox.addItem("200%", 4); + uiScaleBox.setSelectedId(1, juce::dontSendNotification); + uiScaleBox.onChange = [this]() { + static const float scales[] = {1.0f, 1.25f, 1.5f, 2.0f}; + int idx = uiScaleBox.getSelectedId() - 1; + if (idx >= 0 && idx < 4 && onScaleChanged) + onScaleChanged(scales[idx]); + }; +} + +void MainContentComponent::setupLabel(juce::Label& label, const juce::String& text) { + label.setText(text, juce::dontSendNotification); + label.setFont(juce::Font(13.0f).boldened()); + label.setColour(juce::Label::textColourId, juce::Colour(0xffccaa44)); + label.setJustificationType(juce::Justification::centred); + addAndMakeVisible(label); +} + +void MainContentComponent::setupKnob(juce::Slider& knob, const juce::String& name) { + knob.setSliderStyle(juce::Slider::RotaryVerticalDrag); + knob.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 55, 16); + knob.setLookAndFeel(&knobLaf); + knob.setName(name); + knob.setColour(juce::Slider::textBoxTextColourId, juce::Colour(0xffcccccc)); + knob.setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xff1a1a1a)); + knob.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack); + addAndMakeVisible(knob); +} + +void MainContentComponent::setupCombo(juce::ComboBox& combo) { + combo.setEditableText(false); + combo.setLookAndFeel(&comboLaf); + combo.setColour(juce::ComboBox::backgroundColourId, juce::Colour(0xff2a2a2a)); + combo.setColour(juce::ComboBox::textColourId, juce::Colour(0xffcccccc)); + combo.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff555555)); + combo.setColour(juce::ComboBox::arrowColourId, juce::Colour(0xffccaa44)); + combo.setColour(juce::PopupMenu::backgroundColourId, juce::Colour(0xff2a2a2a)); + combo.setColour(juce::PopupMenu::textColourId, juce::Colour(0xffcccccc)); + combo.setColour(juce::PopupMenu::highlightedBackgroundColourId, juce::Colour(0xffccaa44)); + combo.setColour(juce::PopupMenu::highlightedTextColourId, juce::Colour(0xff1a1a1a)); + addAndMakeVisible(combo); +} + +void MainContentComponent::showPresetMenu() { + juce::PopupMenu menu; + + auto categories = processorRef.presetManager.getCategories(); + int menuId = 1; + + for (auto& cat : categories) { + juce::PopupMenu subMenu; + auto presets = processorRef.presetManager.getPresetsInCategory(cat); + for (auto& preset : presets) { + subMenu.addItem(menuId++, preset.name); + } + menu.addSubMenu(cat, subMenu); + } + + menu.showMenuAsync(juce::PopupMenu::Options(), [this](int result) { + if (result > 0) { + auto& presets = processorRef.presetManager.getPresets(); + int idx = 0; + for (int i = 0; i < result - 1 && i < static_cast(presets.size()); ++i) + ++idx; + if (idx < static_cast(presets.size())) + processorRef.presetManager.applyPreset(presets[idx].name, processorRef.apvts); + } + }); +} + +void MainContentComponent::paint(juce::Graphics& g) { + g.fillAll(juce::Colour(0xff1a1a1a)); + + // Draw background image tiled + if (!bgImageLoaded) { + bgImage = juce::ImageFileFormat::loadFrom( + BinaryData::chromaflockbg5_png, BinaryData::chromaflockbg5_pngSize); + bgImageLoaded = true; + } + if (bgImage.isValid()) { + int imgW = bgImage.getWidth(); + int imgH = bgImage.getHeight(); + int compW = getWidth(); + int compH = getHeight(); + + g.setOpacity(0.35f); + for (int x = 0; x < compW; x += imgW) + for (int y = 0; y < compH; y += imgH) + g.drawImageAt(bgImage, x, y); + g.setOpacity(1.0f); + } + + // Load logo + if (!logoLoaded) { + logoImage = juce::ImageFileFormat::loadFrom( + BinaryData::cfnew_png, BinaryData::cfnew_pngSize); + logoLoaded = true; + } + if (logoImage.isValid()) { + int logoH = 124; + int logoW = static_cast(logoH * 805.0 / 119.0); + int logoX = (getWidth() - logoW) / 2; + auto logoArea = juce::Rectangle(logoX, 0, logoW, logoH); + g.drawImage(logoImage, logoArea.toFloat(), juce::RectanglePlacement::centred); + } + + g.setColour(juce::Colour(0xffccaa44)); + g.drawHorizontalLine(124, 10.0f, 1650.0f); + + auto drawSection = [&](int x, int y, int w, int h) { + auto rect = juce::Rectangle(static_cast(x), static_cast(y), + static_cast(w), static_cast(h)); + // 3D fill gradient: slight light top, dark bottom + juce::ColourGradient fillGrad(juce::Colour(0xff222222), rect.getCentreX(), rect.getY(), + juce::Colour(0xff111111), rect.getCentreX(), rect.getBottom(), false); + g.setGradientFill(fillGrad); + g.fillRoundedRectangle(rect, 4.0f); + // Border + g.setColour(juce::Colour(0xff333333)); + g.drawRoundedRectangle(rect, 4.0f, 1.0f); + }; + + drawSection(10, 128, 498, 200); + drawSection(520, 128, 620, 200); + drawSection(1152, 128, 498, 200); + drawSection(75, 338, 498, 150); + drawSection(585, 338, 498, 150); + drawSection(1095, 338, 490, 150); + drawSection(10, 900, 1640, 128); + drawSection(10, 1038, 1640, 136); + + // FX sub-sections + auto drawSubSection = [&](int x, int y, int w, int h, const juce::String& title, int titlePadY = 2) { + auto rect = juce::Rectangle(static_cast(x), static_cast(y), + static_cast(w), static_cast(h)); + juce::ColourGradient fillGrad(juce::Colour(0xff222222), rect.getCentreX(), rect.getY(), + juce::Colour(0xff111111), rect.getCentreX(), rect.getBottom(), false); + g.setGradientFill(fillGrad); + g.fillRoundedRectangle(rect, 3.0f); + g.setColour(juce::Colour(0xff333333)); + g.drawRoundedRectangle(rect, 3.0f, 1.0f); + g.setColour(juce::Colour(0xffccaa44)); + g.setFont(juce::Font(11.0f).boldened()); + g.drawText(title, x + 6, y + titlePadY, w - 12, 14, juce::Justification::centred); + }; + + drawSubSection(15, 524, 395, 140, "DISTORTION", 6); + drawSubSection(420, 524, 660, 140, "COMPRESSOR", 6); + drawSubSection(1090, 524, 240, 140, "AUTO-PAN", 6); + + // LFO sub-sections + drawSubSection(15, 690, 530, 150, "LFO 1", 4); + drawSubSection(560, 690, 530, 150, "LFO 2", 4); + + // FX2 sub-sections + drawSubSection(1100, 690, 265, 200, "DELAY", 4); + drawSubSection(1375, 690, 265, 200, "REVERB", 4); +} + +void MainContentComponent::resized() { + auto sectionY = 132; + auto knobSize = 112; + auto knobSpacing = 10; + auto labelH = 18; + auto comboH = 48; + + osc1Label.setBounds(10, sectionY, 498, labelH); + osc1WaveBox.setBounds(20, sectionY + labelH + 4, 200, comboH); + int knobY1 = sectionY + labelH + comboH + 8; + osc1OctKnob.setBounds(20, knobY1, knobSize, knobSize); + osc1SemiKnob.setBounds(20 + knobSize + knobSpacing, knobY1, knobSize, knobSize); + osc1FineKnob.setBounds(20 + (knobSize + knobSpacing) * 2, knobY1, knobSize, knobSize); + osc1LevelKnob.setBounds(20 + (knobSize + knobSpacing) * 3, knobY1, knobSize, knobSize); + + osc2Label.setBounds(520, sectionY, 620, labelH); + osc2WaveBox.setBounds(530, sectionY + labelH + 4, 200, comboH); + int knobY2 = sectionY + labelH + comboH + 8; + osc2OctKnob.setBounds(530, knobY2, knobSize, knobSize); + osc2SemiKnob.setBounds(530 + knobSize + knobSpacing, knobY2, knobSize, knobSize); + osc2FineKnob.setBounds(530 + (knobSize + knobSpacing) * 2, knobY2, knobSize, knobSize); + osc2LevelKnob.setBounds(530 + (knobSize + knobSpacing) * 3, knobY2, knobSize, knobSize); + phaseOffsetKnob.setBounds(530 + (knobSize + knobSpacing) * 4, knobY2, knobSize, knobSize); + + filterLabel.setBounds(1152, sectionY, 498, labelH); + filterTypeBox.setBounds(1162, sectionY + labelH + 4, 200, comboH); + int knobYF = sectionY + labelH + comboH + 8; + filterCutoffKnob.setBounds(1162, knobYF, knobSize, knobSize); + filterResKnob.setBounds(1162 + knobSize + knobSpacing, knobYF, knobSize, knobSize); + filterEnvAmtKnob.setBounds(1162 + (knobSize + knobSpacing) * 2, knobYF, knobSize, knobSize); + keyTrackKnob.setBounds(1162 + (knobSize + knobSpacing) * 3, knobYF, knobSize, knobSize); + + envLabel.setBounds(75, 352, 498, labelH); + int knobYA = 352 + labelH + 6; + envAttackKnob.setBounds(85, knobYA, knobSize, knobSize); + envDecayKnob.setBounds(85 + knobSize + knobSpacing, knobYA, knobSize, knobSize); + envSustainKnob.setBounds(85 + (knobSize + knobSpacing) * 2, knobYA, knobSize, knobSize); + envReleaseKnob.setBounds(85 + (knobSize + knobSpacing) * 3, knobYA, knobSize, knobSize); + + fEnvLabel.setBounds(585, 352, 498, labelH); + int knobYFE = 352 + labelH + 6; + fEnvAttackKnob.setBounds(595, knobYFE, knobSize, knobSize); + fEnvDecayKnob.setBounds(595 + knobSize + knobSpacing, knobYFE, knobSize, knobSize); + fEnvSustainKnob.setBounds(595 + (knobSize + knobSpacing) * 2, knobYFE, knobSize, knobSize); + fEnvReleaseKnob.setBounds(595 + (knobSize + knobSpacing) * 3, knobYFE, knobSize, knobSize); + + globalLabel.setBounds(1095, 352, 490, labelH); + int knobYM = 352 + labelH + 6; + panKnob.setBounds(1105, knobYM, knobSize, knobSize); + driveKnob.setBounds(1105 + knobSize + knobSpacing, knobYM, knobSize, knobSize); + masterKnob.setBounds(1105 + (knobSize + knobSpacing) * 2, knobYM, knobSize, knobSize); + pbRangeKnob.setBounds(1105 + (knobSize + knobSpacing) * 3, knobYM, knobSize, knobSize); + + // --- FX SECTION --- + int fxY = 496; + int fxKnobSize = 100; + int fxLabelH = 16; + int fxKnobY = fxY + 54; // sub-section y(476) + titlePad(6) + titleH(14) + bottomPad(6) + 4 + + fxLabel.setBounds(10, fxY + 8, 1640, fxLabelH); + + // Distortion sub-section (X=15, Y=476, W=395, H=140) + distTypeBox.setBounds(22, fxKnobY + 2, 140, 36); + distAmountKnob.setBounds(170, fxKnobY, fxKnobSize, fxKnobSize); + distMixKnob.setBounds(280, fxKnobY, fxKnobSize, fxKnobSize); + + // Compressor sub-section (X=420, Y=476, W=660, H=140) + compThresholdKnob.setBounds(428, fxKnobY, fxKnobSize, fxKnobSize); + compRatioKnob.setBounds(538, fxKnobY, fxKnobSize, fxKnobSize); + compAttackKnob.setBounds(648, fxKnobY, fxKnobSize, fxKnobSize); + compReleaseKnob.setBounds(758, fxKnobY, fxKnobSize, fxKnobSize); + compMakeupKnob.setBounds(868, fxKnobY, fxKnobSize, fxKnobSize); + compMixKnob.setBounds(978, fxKnobY, fxKnobSize, fxKnobSize); + + // Auto-Pan sub-section (X=1090, Y=476, W=240, H=140) + autoPanRateKnob.setBounds(1098, fxKnobY, fxKnobSize, fxKnobSize); + autoPanDepthKnob.setBounds(1098 + fxKnobSize + 8, fxKnobY, fxKnobSize, fxKnobSize); + + // --- LFO SECTION --- + int lfoY = 670; + int lfoKnobSize = 80; + int lfoKnobY = 690 + 4 + 14 + 6; // subSectionY + titlePad + titleH + bottomPad + + lfoLabel.setBounds(10, lfoY, 1080, fxLabelH); + fx2Label.setBounds(1100, lfoY, 540, fxLabelH); + + // LFO 1 sub-section (X=15, Y=642, W=530, H=110) + lfo1RateKnob.setBounds(22, lfoKnobY, lfoKnobSize, lfoKnobSize); + lfo1DepthKnob.setBounds(132, lfoKnobY, lfoKnobSize, lfoKnobSize); + lfo1ShapeBox.setBounds(242, lfoKnobY + 2, 130, 36); + lfo1DestBox.setBounds(382, lfoKnobY + 2, 155, 36); + + // LFO 2 sub-section (X=560, Y=642, W=530, H=110) + lfo2RateKnob.setBounds(568, lfoKnobY, lfoKnobSize, lfoKnobSize); + lfo2DepthKnob.setBounds(678, lfoKnobY, lfoKnobSize, lfoKnobSize); + lfo2ShapeBox.setBounds(788, lfoKnobY + 2, 130, 36); + lfo2DestBox.setBounds(928, lfoKnobY + 2, 155, 36); + + // --- FX2 SECTION --- + int fx2KnobY = lfoKnobY; + + // Delay sub-section (X=1100, Y=690, W=265, H=130) + // Row 1: TIME, FBACK, MIX + delayTimeKnob.setBounds(1110, fx2KnobY, lfoKnobSize, lfoKnobSize); + delayFbKnob.setBounds(1198, fx2KnobY, lfoKnobSize, lfoKnobSize); + delayMixKnob.setBounds(1286, fx2KnobY, lfoKnobSize, lfoKnobSize); + // Row 2: PINGPONG combo, SPREAD knob + int delayRow2Y = fx2KnobY + lfoKnobSize + 4; + delayPingPongBox.setBounds(1110, delayRow2Y, 100, 36); + delaySpreadKnob.setBounds(1220, delayRow2Y, lfoKnobSize, lfoKnobSize); + + // Reverb sub-section (X=1375, Y=690, W=265, H=130) + reverbSizeKnob.setBounds(1383, fx2KnobY, lfoKnobSize, lfoKnobSize); + reverbDampKnob.setBounds(1468, fx2KnobY, lfoKnobSize, lfoKnobSize); + reverbMixKnob.setBounds(1553, fx2KnobY, lfoKnobSize, lfoKnobSize); + + // --- PRESET + SCALE --- + presetButton.setBounds(20, 8, 120, 28); + scaleLabel.setBounds(1520, 10, 42, 20); + uiScaleBox.setBounds(1566, 8, 80, 24); + + // Visualizer area + int vizY = 900; + int vizH = 128; + vuMeter.setBounds(14, vizY, 40, vizH); + waveformDisplay.setBounds(60, vizY, 780, vizH); + spectrumAnalyzer.setBounds(850, vizY, 794, vizH); + + // Piano roll + pianoRoll.setBounds(10, 1038, 1640, 136); +} + +// ===== PianoRollComponent ===== + +int PianoRollComponent::midiNoteForKey(int index) const { + // Map key index to MIDI note, skipping black keys in the index + // White keys: C D E F G A B (indices 0,2,4,5,7,9,11 in chromatic scale) + static const int whiteToChromatic[] = {0, 2, 4, 5, 7, 9, 11}; + int octave = index / 7; + int noteInOctave = index % 7; + return firstMidiNote + octave * 12 + whiteToChromatic[noteInOctave]; +} + +bool PianoRollComponent::isBlackKey(int midiNote) const { + int noteInOctave = midiNote % 12; + return noteInOctave == 1 || noteInOctave == 3 || noteInOctave == 6 || + noteInOctave == 8 || noteInOctave == 10; +} + +int PianoRollComponent::keyAtPosition(juce::Point pos) const { + int pbRight = pitchBendWidth; + int keysX = pbRight + 8; + int keysWidth = getWidth() - keysX - 6; + int numWhiteKeys = (numKeys * 7) / 12 + 1; + int localWhiteKeyWidth = keysWidth / numWhiteKeys; + + // Check black keys first (they're on top) + for (int i = 0; i < numKeys; ++i) { + int note = firstMidiNote + i; + if (!isBlackKey(note)) continue; + + int chroma = note % 12; + int octave = (note - firstMidiNote) / 12; + static const int blackToLeftWhite[] = { 0,0,1,1,2,3,3,4,4,5,5,6 }; + int leftWhite = blackToLeftWhite[chroma]; + int whiteIndexBefore = octave * 7 + leftWhite; + + float bx = static_cast(keysX + (whiteIndexBefore + 1) * localWhiteKeyWidth) - static_cast(blackKeyWidth) * 0.5f; + auto blackRect = juce::Rectangle(static_cast(bx), 0, blackKeyWidth, blackKeyHeight); + if (blackRect.contains(pos)) + return note; + } + + // Check white keys + for (int i = 0; i < numKeys; ++i) { + int note = firstMidiNote + i; + if (isBlackKey(note)) continue; + + int chroma = note % 12; + int octave = (note - firstMidiNote) / 12; + int whiteIndex = octave * 7; + if (chroma == 2) whiteIndex += 1; + else if (chroma == 4) whiteIndex += 2; + else if (chroma == 5) whiteIndex += 3; + else if (chroma == 7) whiteIndex += 4; + else if (chroma == 9) whiteIndex += 5; + else if (chroma == 11) whiteIndex += 6; + + auto whiteRect = juce::Rectangle(keysX + whiteIndex * localWhiteKeyWidth, 0, localWhiteKeyWidth, whiteKeyHeight); + if (whiteRect.contains(pos)) + return note; + } + + return -1; +} + +void PianoRollComponent::triggerNote(int note, bool on) { + if (on) + processor.noteOn(note, 0.8f); + else + processor.noteOff(note); +} + +void PianoRollComponent::paint(juce::Graphics& g) { + int pbRight = pitchBendWidth; + int keysX = pbRight + 8; + int keysWidth = getWidth() - keysX - 6; + int numWhiteKeys = (numKeys * 7) / 12 + 1; + whiteKeyWidth = keysWidth / numWhiteKeys; + + // Draw pitch bend strip background + g.setColour(juce::Colour(0xff222222)); + g.fillRect(10, 0, pbRight - 10, whiteKeyHeight); + + // Right edge border to separate from keys + g.setColour(juce::Colour(0xff444444)); + g.drawVerticalLine(pbRight - 8, 0.0f, static_cast(whiteKeyHeight)); + + // Active bend area indicator (shaded region from center to current position) + int pbCenter = whiteKeyHeight / 2; + float pbNorm = static_cast(currentPitchBend - 64) / 64.0f; // -1..+1 + int pbY = pbCenter - static_cast(pbNorm * static_cast(pbCenter - 8)); + + g.setColour(juce::Colour(0x40ccaa44)); + if (pbY < pbCenter) + g.fillRect(14, pbY, 18, pbCenter - pbY); + else + g.fillRect(14, pbCenter, 18, pbY - pbCenter); + + // Pitch bend center line + g.setColour(juce::Colour(0xffccaa44)); + g.drawHorizontalLine(pbCenter, 12.0f, static_cast(pbRight - 14)); + + // Pitch bend indicator dot + g.setColour(juce::Colour(0xffccaa44)); + g.fillEllipse(14, pbY - 5, 18, 10); + + // PB labels + g.setColour(juce::Colour(0xff888888)); + g.setFont(9.0f); + g.drawText("+", 14, 2, 18, 12, juce::Justification::centred); + g.drawText("-", 14, whiteKeyHeight - 14, 18, 12, juce::Justification::centred); + + // Draw white keys first + for (int i = 0; i < numKeys; ++i) { + int note = firstMidiNote + i; + if (isBlackKey(note)) continue; + + int chroma = note % 12; + int octave = (note - firstMidiNote) / 12; + int whiteIndex = octave * 7; + if (chroma == 2) whiteIndex += 1; + else if (chroma == 4) whiteIndex += 2; + else if (chroma == 5) whiteIndex += 3; + else if (chroma == 7) whiteIndex += 4; + else if (chroma == 9) whiteIndex += 5; + else if (chroma == 11) whiteIndex += 6; + + int kx = keysX + whiteIndex * whiteKeyWidth; + auto rect = juce::Rectangle(kx, 0, whiteKeyWidth - 1, whiteKeyHeight); + + bool pressed = processor.isNoteActive(note); + if (pressed) + g.setColour(juce::Colour(0xffccaa44)); + else + g.setColour(juce::Colour(0xffdddddd)); + g.fillRect(rect); + + g.setColour(juce::Colour(0xff555555)); + g.drawRect(rect, 1); + + // Draw note name on C keys + if (chroma == 0) { + g.setColour(juce::Colour(0xff666666)); + g.setFont(9.0f); + g.drawText("C" + juce::String(octave + 3), kx + 2, whiteKeyHeight - 14, whiteKeyWidth - 4, 12, + juce::Justification::centred); + } + } + + // Draw black keys on top + for (int i = 0; i < numKeys; ++i) { + int note = firstMidiNote + i; + if (!isBlackKey(note)) continue; + + int chroma = note % 12; + int octave = (note - firstMidiNote) / 12; + // chroma → white key index to the LEFT of this black key + static const int blackToLeftWhite[] = { 0,0,1,1,2,3,3,4,4,5,5,6 }; + int leftWhite = blackToLeftWhite[chroma]; + int whiteIndexBefore = octave * 7 + leftWhite; + + float bx = static_cast(keysX + (whiteIndexBefore + 1) * whiteKeyWidth) - static_cast(blackKeyWidth) * 0.5f; + auto rect = juce::Rectangle(static_cast(bx), 0, blackKeyWidth, blackKeyHeight); + + bool pressed = processor.isNoteActive(note); + if (pressed) + g.setColour(juce::Colour(0xffccaa44)); + else + g.setColour(juce::Colour(0xff333333)); + g.fillRect(rect); + + g.setColour(juce::Colour(0xff555555)); + g.drawRect(rect, 1); + } +} + +void PianoRollComponent::mouseDown(const juce::MouseEvent& e) { + auto pos = e.getPosition(); + + // Pitch bend strip + if (pos.x < pitchBendWidth) { + float pbNorm = 1.0f - static_cast(pos.y) / static_cast(whiteKeyHeight); + pbNorm = std::clamp(pbNorm, -1.0f, 1.0f); + currentPitchBend = 64 + static_cast(pbNorm * 64.0f); + processor.pitchBendValue.store(pbNorm); + return; + } + + int note = keyAtPosition(pos); + if (note >= 0) { + if (lastTriggeredNote >= 0 && lastTriggeredNote != note) + triggerNote(lastTriggeredNote, false); + lastTriggeredNote = note; + triggerNote(note, true); + repaint(); + } +} + +void PianoRollComponent::mouseUp(const juce::MouseEvent& e) { + // Reset pitch bend to center on release + if (e.getPosition().x < pitchBendWidth || lastTriggeredNote < 0) { + if (e.getPosition().x < pitchBendWidth) { + currentPitchBend = 64; + processor.pitchBendValue.store(0.0f); + repaint(); + } + if (lastTriggeredNote < 0) return; + } + + if (lastTriggeredNote >= 0) { + triggerNote(lastTriggeredNote, false); + lastTriggeredNote = -1; + repaint(); + } +} + +void PianoRollComponent::mouseDrag(const juce::MouseEvent& e) { + auto pos = e.getPosition(); + + // Pitch bend drag + if (pos.x < pitchBendWidth) { + float pbNorm = 1.0f - static_cast(pos.y) / static_cast(whiteKeyHeight); + pbNorm = std::clamp(pbNorm, -1.0f, 1.0f); + currentPitchBend = 64 + static_cast(pbNorm * 64.0f); + processor.pitchBendValue.store(pbNorm); + repaint(); + return; + } + + int note = keyAtPosition(pos); + if (note >= 0 && note != lastTriggeredNote) { + if (lastTriggeredNote >= 0) + triggerNote(lastTriggeredNote, false); + lastTriggeredNote = note; + triggerNote(note, true); + repaint(); + } +} + +ChromaFlockEditor::ChromaFlockEditor(ChromaFlockProcessor& p) + : AudioProcessorEditor(&p), content(p) { + addAndMakeVisible(content); + content.onScaleChanged = [this](float s) { setUIScale(s); }; + setSize(baseWidth, baseHeight); + setResizable(false, false); +} + +ChromaFlockEditor::~ChromaFlockEditor() {} + +void ChromaFlockEditor::paint(juce::Graphics& g) { + g.fillAll(juce::Colour(0xff1a1a1a)); +} + +void ChromaFlockEditor::resized() { + content.setBounds(0, 0, baseWidth, baseHeight); + content.setTransform(juce::AffineTransform::scale(currentScale, currentScale)); +} + +void ChromaFlockEditor::setUIScale(float newScale) { + currentScale = newScale; + setSize(static_cast(baseWidth * newScale), static_cast(baseHeight * newScale)); +} diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h new file mode 100644 index 0000000..7e7ed28 --- /dev/null +++ b/Source/PluginEditor.h @@ -0,0 +1,205 @@ +#pragma once +#include +#include +#include +#include "PluginProcessor.h" +#include + +class KnobLookAndFeel : public juce::LookAndFeel_V4 { +public: + void drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, + float sliderPos, float rotaryStartAngle, + float rotaryEndAngle, juce::Slider& slider) override; +}; + +class ComboBoxLookAndFeel : public juce::LookAndFeel_V4 { +public: + void drawComboBox(juce::Graphics& g, int width, int height, + bool isButtonDown, int buttonX, int buttonY, + int buttonW, int buttonH, juce::ComboBox& box) override; + void drawPopupMenuItem(juce::Graphics& g, const juce::Rectangle& area, + bool isSeparator, bool isActive, bool isHighlighted, + bool isTicked, bool hasSubMenu, const juce::String& text, + const juce::String& shortcutKeyText, + const juce::Drawable* icon, const juce::Colour* textColour) override; +}; + +class VuMeter : public juce::Component, public juce::Timer { +public: + explicit VuMeter(ChromaFlockProcessor& p) : processor(p) { startTimerHz(30); } + void paint(juce::Graphics& g) override; + void timerCallback() override { repaint(); } +private: + ChromaFlockProcessor& processor; +}; + +class WaveformDisplay : public juce::Component, public juce::Timer { +public: + explicit WaveformDisplay(ChromaFlockProcessor& p) : processor(p) { startTimerHz(30); } + void paint(juce::Graphics& g) override; + void timerCallback() override { repaint(); } +private: + ChromaFlockProcessor& processor; +}; + +class SpectrumAnalyzer : public juce::Component, public juce::Timer { +public: + explicit SpectrumAnalyzer(ChromaFlockProcessor& p) : processor(p) { + fft = std::make_unique(ChromaFlockProcessor::fftOrder); + startTimerHz(30); + } + void paint(juce::Graphics& g) override; + void timerCallback() override { repaint(); } +private: + ChromaFlockProcessor& processor; + std::unique_ptr fft; +}; + +class PianoRollComponent : public juce::Component, public juce::Timer { +public: + explicit PianoRollComponent(ChromaFlockProcessor& p) : processor(p) { startTimerHz(30); } + void paint(juce::Graphics& g) override; + void timerCallback() override { repaint(); } + void mouseDown(const juce::MouseEvent& e) override; + void mouseUp(const juce::MouseEvent& e) override; + void mouseDrag(const juce::MouseEvent& e) override; + +private: + ChromaFlockProcessor& processor; + + static constexpr int firstMidiNote = 48; // C3 + static constexpr int numKeys = 49; // C3..C7 + + int whiteKeyWidth = 54; + int whiteKeyHeight = 130; + int blackKeyWidth = 35; + int blackKeyHeight = 80; + int pitchBendWidth = 40; + + int midiNoteForKey(int index) const; + bool isBlackKey(int midiNote) const; + int keyAtPosition(juce::Point pos) const; + void triggerNote(int note, bool on); + + int lastTriggeredNote = -1; + int currentPitchBend = 64; // 0..127, 64 = center +}; + +class MainContentComponent : public juce::Component { +public: + explicit MainContentComponent(ChromaFlockProcessor& p); + void paint(juce::Graphics& g) override; + void resized() override; + + std::function onScaleChanged; + +private: + ChromaFlockProcessor& processorRef; + KnobLookAndFeel knobLaf; + ComboBoxLookAndFeel comboLaf; + juce::Image bgImage; + bool bgImageLoaded = false; + juce::Image logoImage; + bool logoLoaded = false; + + VuMeter vuMeter; + WaveformDisplay waveformDisplay; + SpectrumAnalyzer spectrumAnalyzer; + PianoRollComponent pianoRoll; + + juce::Label osc1Label, osc2Label, filterLabel, envLabel, fEnvLabel, globalLabel; + juce::Label fxLabel, lfoLabel, fx2Label; + juce::Label scaleLabel; + + juce::ComboBox osc1WaveBox; + juce::Slider osc1OctKnob, osc1SemiKnob, osc1FineKnob, osc1LevelKnob; + + juce::ComboBox osc2WaveBox; + juce::Slider osc2OctKnob, osc2SemiKnob, osc2FineKnob, osc2LevelKnob, phaseOffsetKnob; + + juce::ComboBox filterTypeBox; + juce::Slider filterCutoffKnob, filterResKnob, filterEnvAmtKnob, keyTrackKnob; + + juce::Slider envAttackKnob, envDecayKnob, envSustainKnob, envReleaseKnob; + juce::Slider fEnvAttackKnob, fEnvDecayKnob, fEnvSustainKnob, fEnvReleaseKnob; + + juce::Slider panKnob, driveKnob, masterKnob, pbRangeKnob; + + // FX controls + juce::ComboBox distTypeBox; + juce::Slider distAmountKnob, distMixKnob; + juce::Slider compThresholdKnob, compRatioKnob, compAttackKnob, compReleaseKnob, compMakeupKnob, compMixKnob; + juce::Slider autoPanRateKnob, autoPanDepthKnob; + + // LFO controls + juce::Slider lfo1RateKnob, lfo1DepthKnob; + juce::ComboBox lfo1ShapeBox, lfo1DestBox; + juce::Slider lfo2RateKnob, lfo2DepthKnob; + juce::ComboBox lfo2ShapeBox, lfo2DestBox; + + // FX2 controls + juce::Slider delayTimeKnob, delayFbKnob, delayMixKnob; + juce::ComboBox delayPingPongBox; + juce::Slider delaySpreadKnob; + juce::Slider reverbSizeKnob, reverbDampKnob, reverbMixKnob; + + // Preset menu + juce::TextButton presetButton{"PRESET"}; + + juce::ComboBox uiScaleBox; + + using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment; + using ComboBoxAttachment = juce::AudioProcessorValueTreeState::ComboBoxAttachment; + + std::unique_ptr osc1OctAttach, osc1SemiAttach, osc1FineAttach, osc1LevelAttach; + std::unique_ptr osc2OctAttach, osc2SemiAttach, osc2FineAttach, osc2LevelAttach, phaseOffsetAttach; + std::unique_ptr osc1WaveAttach, osc2WaveAttach, filterTypeAttach; + std::unique_ptr filterCutoffAttach, filterResAttach, filterEnvAmtAttach, keyTrackAttach; + std::unique_ptr envAttackAttach, envDecayAttach, envSustainAttach, envReleaseAttach; + std::unique_ptr fEnvAttackAttach, fEnvDecayAttach, fEnvSustainAttach, fEnvReleaseAttach; + std::unique_ptr panAttach, driveAttach, masterAttach, pbRangeAttach; + + // FX attachments + std::unique_ptr distTypeAttach; + std::unique_ptr distAmountAttach, distMixAttach; + std::unique_ptr compThresholdAttach, compRatioAttach, compAttackAttach; + std::unique_ptr compReleaseAttach, compMakeupAttach, compMixAttach; + std::unique_ptr autoPanRateAttach, autoPanDepthAttach; + + // LFO attachments + std::unique_ptr lfo1RateAttach, lfo1DepthAttach; + std::unique_ptr lfo1ShapeAttach, lfo1DestAttach; + std::unique_ptr lfo2RateAttach, lfo2DepthAttach; + std::unique_ptr lfo2ShapeAttach, lfo2DestAttach; + + // FX2 attachments + std::unique_ptr delayTimeAttach, delayFbAttach, delayMixAttach; + std::unique_ptr delayPingPongAttach; + std::unique_ptr delaySpreadAttach; + std::unique_ptr reverbSizeAttach, reverbDampAttach, reverbMixAttach; + + void setupLabel(juce::Label& label, const juce::String& text); + void setupKnob(juce::Slider& knob, const juce::String& name); + void setupCombo(juce::ComboBox& combo); + void showPresetMenu(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainContentComponent) +}; + +class ChromaFlockEditor : public juce::AudioProcessorEditor { +public: + ChromaFlockEditor(ChromaFlockProcessor& p); + ~ChromaFlockEditor() override; + + void paint(juce::Graphics&) override; + void resized() override; + void setUIScale(float newScale); + +private: + MainContentComponent content; + float currentScale = 1.0f; + static constexpr int baseWidth = 1660; + static constexpr int baseHeight = 1182; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ChromaFlockEditor) +}; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp new file mode 100644 index 0000000..2e5330b --- /dev/null +++ b/Source/PluginProcessor.cpp @@ -0,0 +1,387 @@ +#include "PluginProcessor.h" +#include "PluginEditor.h" + +ChromaFlockProcessor::ChromaFlockProcessor() + : AudioProcessor(BusesProperties() + .withOutput("Output", juce::AudioChannelSet::stereo(), true)), + apvts(*this, nullptr, "Parameters", createParameterLayout()) { + synth.addSound(new SubtractiveSound()); + for (int i = 0; i < maxVoices; ++i) + synth.addVoice(new SubtractiveVoice()); +} + +ChromaFlockProcessor::~ChromaFlockProcessor() {} + +juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::createParameterLayout() { + juce::AudioProcessorValueTreeState::ParameterLayout layout; + + auto zeroOne = juce::NormalisableRange(0.0f, 1.0f, 0.01f); + + // OSC 1 + layout.add(std::make_unique( + juce::ParameterID{"osc1Wave", 1}, "OSC1 Wave", + juce::StringArray{"Sine", "Saw", "Square", "Triangle", "Noise"}, 1)); + layout.add(std::make_unique( + juce::ParameterID{"osc1Oct", 1}, "OSC1 Octave", + juce::NormalisableRange(-3.0f, 3.0f, 1.0f), 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"osc1Semi", 1}, "OSC1 Semi", + juce::NormalisableRange(-12.0f, 12.0f, 1.0f), 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"osc1Fine", 1}, "OSC1 Fine", + juce::NormalisableRange(-100.0f, 100.0f, 1.0f), 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"osc1Level", 1}, "OSC1 Level", zeroOne, 0.7f)); + + // OSC 2 + layout.add(std::make_unique( + juce::ParameterID{"osc2Wave", 1}, "OSC2 Wave", + juce::StringArray{"Sine", "Saw", "Square", "Triangle", "Noise"}, 2)); + layout.add(std::make_unique( + juce::ParameterID{"osc2Oct", 1}, "OSC2 Octave", + juce::NormalisableRange(-3.0f, 3.0f, 1.0f), -1.0f)); + layout.add(std::make_unique( + juce::ParameterID{"osc2Semi", 1}, "OSC2 Semi", + juce::NormalisableRange(-12.0f, 12.0f, 1.0f), 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"osc2Fine", 1}, "OSC2 Fine", + juce::NormalisableRange(-100.0f, 100.0f, 1.0f), 7.0f)); + layout.add(std::make_unique( + juce::ParameterID{"osc2Level", 1}, "OSC2 Level", zeroOne, 0.7f)); + layout.add(std::make_unique( + juce::ParameterID{"phaseOffset", 1}, "Phase Offset", zeroOne, 0.5f)); + + // FILTER + layout.add(std::make_unique( + juce::ParameterID{"filterType", 1}, "Filter Type", + juce::StringArray{"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch"}, 0)); + layout.add(std::make_unique( + juce::ParameterID{"filterCutoff", 1}, "Filter Cutoff", + juce::NormalisableRange(25.0f, 1200.0f, 1.0f, 0.25f), 800.0f)); + layout.add(std::make_unique( + juce::ParameterID{"filterRes", 1}, "Filter Resonance", zeroOne, 0.7f)); + layout.add(std::make_unique( + juce::ParameterID{"filterEnvAmt", 1}, "Filter Env Amount", zeroOne, 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"keyTrack", 1}, "Key Tracking", zeroOne, 0.5f)); + + // FILTER ENVELOPE + layout.add(std::make_unique( + juce::ParameterID{"fEnvAttack", 1}, "Filter Attack", + juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.3f), 0.01f)); + layout.add(std::make_unique( + juce::ParameterID{"fEnvDecay", 1}, "Filter Decay", + juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.3f), 0.3f)); + layout.add(std::make_unique( + juce::ParameterID{"fEnvSustain", 1}, "Filter Sustain", zeroOne, 0.5f)); + layout.add(std::make_unique( + juce::ParameterID{"fEnvRelease", 1}, "Filter Release", + juce::NormalisableRange(0.001f, 10.0f, 0.001f, 0.3f), 0.5f)); + + // AMP ENVELOPE + layout.add(std::make_unique( + juce::ParameterID{"envAttack", 1}, "Attack", + juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.3f), 0.01f)); + layout.add(std::make_unique( + juce::ParameterID{"envDecay", 1}, "Decay", + juce::NormalisableRange(0.001f, 5.0f, 0.001f, 0.3f), 0.3f)); + layout.add(std::make_unique( + juce::ParameterID{"envSustain", 1}, "Sustain", zeroOne, 0.7f)); + layout.add(std::make_unique( + juce::ParameterID{"envRelease", 1}, "Release", + juce::NormalisableRange(0.001f, 10.0f, 0.001f, 0.3f), 0.5f)); + + // GLOBAL + layout.add(std::make_unique( + juce::ParameterID{"pan", 1}, "Pan", + juce::NormalisableRange(-1.0f, 1.0f, 0.01f), 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"drive", 1}, "Drive", + juce::NormalisableRange(1.0f, 5.0f, 0.01f, 0.5f), 1.5f)); + layout.add(std::make_unique( + juce::ParameterID{"masterLevel", 1}, "Master Level", zeroOne, 0.8f)); + + // PITCH BEND + layout.add(std::make_unique( + juce::ParameterID{"pitchBendRange", 1}, "Pitch Bend Range", + juce::NormalisableRange(1.0f, 12.0f, 1.0f), 2.0f)); + + // DISTORTION + layout.add(std::make_unique( + juce::ParameterID{"distType", 1}, "Dist Type", + juce::StringArray{"Soft Clip", "Hard Clip", "Foldback", "Overdrive"}, 0)); + layout.add(std::make_unique( + juce::ParameterID{"distAmount", 1}, "Dist Amount", zeroOne, 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"distMix", 1}, "Dist Mix", zeroOne, 0.0f)); + + // COMPRESSOR + layout.add(std::make_unique( + juce::ParameterID{"compThreshold", 1}, "Comp Threshold", + juce::NormalisableRange(-60.0f, 0.0f, 0.1f), -20.0f)); + layout.add(std::make_unique( + juce::ParameterID{"compRatio", 1}, "Comp Ratio", + juce::NormalisableRange(1.0f, 20.0f, 0.1f), 4.0f)); + layout.add(std::make_unique( + juce::ParameterID{"compAttack", 1}, "Comp Attack", + juce::NormalisableRange(0.1f, 100.0f, 0.1f), 10.0f)); + layout.add(std::make_unique( + juce::ParameterID{"compRelease", 1}, "Comp Release", + juce::NormalisableRange(10.0f, 1000.0f, 1.0f), 100.0f)); + layout.add(std::make_unique( + juce::ParameterID{"compMakeup", 1}, "Comp Makeup", + juce::NormalisableRange(0.0f, 24.0f, 0.1f), 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"compMix", 1}, "Comp Mix", zeroOne, 1.0f)); + + // AUTO-PAN + layout.add(std::make_unique( + juce::ParameterID{"autoPanRate", 1}, "Pan Rate", + juce::NormalisableRange(0.05f, 20.0f, 0.01f, 0.4f), 2.0f)); + layout.add(std::make_unique( + juce::ParameterID{"autoPanDepth", 1}, "Pan Depth", zeroOne, 0.0f)); + + // LFO 1 + layout.add(std::make_unique( + juce::ParameterID{"lfo1Rate", 1}, "LFO1 Rate", + juce::NormalisableRange(0.05f, 20.0f, 0.01f, 0.4f), 2.0f)); + layout.add(std::make_unique( + juce::ParameterID{"lfo1Depth", 1}, "LFO1 Depth", zeroOne, 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"lfo1Shape", 1}, "LFO1 Shape", + juce::StringArray{"Sine", "Triangle", "Saw", "Square"}, 0)); + layout.add(std::make_unique( + juce::ParameterID{"lfo1Dest", 1}, "LFO1 Dest", + juce::StringArray{"Filter", "OSC1", "OSC2", "Both OSC"}, 0)); + + // LFO 2 + layout.add(std::make_unique( + juce::ParameterID{"lfo2Rate", 1}, "LFO2 Rate", + juce::NormalisableRange(0.05f, 20.0f, 0.01f, 0.4f), 2.0f)); + layout.add(std::make_unique( + juce::ParameterID{"lfo2Depth", 1}, "LFO2 Depth", zeroOne, 0.0f)); + layout.add(std::make_unique( + juce::ParameterID{"lfo2Shape", 1}, "LFO2 Shape", + juce::StringArray{"Sine", "Triangle", "Saw", "Square"}, 0)); + layout.add(std::make_unique( + juce::ParameterID{"lfo2Dest", 1}, "LFO2 Dest", + juce::StringArray{"Filter", "OSC1", "OSC2", "Both OSC"}, 0)); + + // DELAY + layout.add(std::make_unique( + juce::ParameterID{"delayTime", 1}, "Delay Time", + juce::NormalisableRange(10.0f, 1000.0f, 1.0f, 0.3f), 200.0f)); + layout.add(std::make_unique( + juce::ParameterID{"delayFeedback", 1}, "Delay Feedback", zeroOne, 0.3f)); + layout.add(std::make_unique( + juce::ParameterID{"delayMix", 1}, "Delay Mix", zeroOne, 0.25f)); + layout.add(std::make_unique( + juce::ParameterID{"delayPingPong", 1}, "Delay PingPong", + juce::StringArray{"Off", "On"}, 0)); + layout.add(std::make_unique( + juce::ParameterID{"delaySpread", 1}, "Delay Spread", + juce::NormalisableRange(0.25f, 0.75f, 0.01f), 0.5f)); + + // REVERB + layout.add(std::make_unique( + juce::ParameterID{"reverbSize", 1}, "Reverb Size", zeroOne, 0.5f)); + layout.add(std::make_unique( + juce::ParameterID{"reverbDamping", 1}, "Reverb Damping", zeroOne, 0.5f)); + layout.add(std::make_unique( + juce::ParameterID{"reverbMix", 1}, "Reverb Mix", zeroOne, 0.2f)); + + return layout; +} + +void ChromaFlockProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) { + currentSampleRate = sampleRate; + synth.setCurrentPlaybackSampleRate(sampleRate); + distortion.prepare(sampleRate); + compressor.prepare(sampleRate); + delay.prepare(sampleRate); + reverbFX.prepare(sampleRate); + autoPanPhase = 0.0f; + updateVoiceParameters(); +} + +void ChromaFlockProcessor::releaseResources() {} + +void ChromaFlockProcessor::updateVoiceParameters() { + auto getParam = [&](const juce::String& id) -> float { + auto* p = apvts.getRawParameterValue(id); + return p != nullptr ? p->load() : 0.0f; + }; + + for (int i = 0; i < synth.getNumVoices(); ++i) { + if (auto* voice = dynamic_cast(synth.getVoice(i))) { + float pbRange = getParam("pitchBendRange"); + voice->setPitchBend(getPitchBend() * pbRange); + voice->setParameters( + static_cast(static_cast(getParam("osc1Wave"))), + static_cast(static_cast(getParam("osc2Wave"))), + getParam("osc1Oct"), + getParam("osc2Oct"), + getParam("osc1Semi"), + getParam("osc2Semi"), + getParam("osc1Fine"), + getParam("osc2Fine"), + getParam("osc1Level"), + getParam("osc2Level"), + getParam("phaseOffset"), + getParam("filterCutoff"), + getParam("filterRes"), + static_cast(static_cast(getParam("filterType"))), + getParam("filterEnvAmt"), + getParam("keyTrack"), + getParam("envAttack"), + getParam("envDecay"), + getParam("envSustain"), + getParam("envRelease"), + getParam("fEnvAttack"), + getParam("fEnvDecay"), + getParam("fEnvSustain"), + getParam("fEnvRelease"), + getParam("pan"), + getParam("drive"), + getParam("masterLevel"), + getParam("lfo1Rate"), + getParam("lfo1Depth"), + static_cast(getParam("lfo1Shape")), + static_cast(getParam("lfo1Dest")), + getParam("lfo2Rate"), + getParam("lfo2Depth"), + static_cast(getParam("lfo2Shape")), + static_cast(getParam("lfo2Dest")) + ); + } + } +} + +void ChromaFlockProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) { + juce::ScopedNoDenormals noDenormals; + + buffer.clear(); + updateVoiceParameters(); + + for (const auto metadata : midiMessages) + if (metadata.getMessage().isNoteOn()) + activeNotes[metadata.getMessage().getNoteNumber()].store(true, std::memory_order_relaxed); + else if (metadata.getMessage().isNoteOff()) + activeNotes[metadata.getMessage().getNoteNumber()].store(false, std::memory_order_relaxed); + + synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples()); + + // Read FX params + auto getParam = [&](const juce::String& id) -> float { + auto* p = apvts.getRawParameterValue(id); + return p != nullptr ? p->load() : 0.0f; + }; + + distortion.setParameters( + static_cast(static_cast(getParam("distType"))), + getParam("distAmount"), + getParam("distMix")); + + compressor.setParameters( + getParam("compThreshold"), + getParam("compRatio"), + getParam("compAttack"), + getParam("compRelease"), + getParam("compMakeup"), + getParam("compMix")); + + float panRate = getParam("autoPanRate"); + float panDepth = getParam("autoPanDepth"); + + delay.setParameters(getParam("delayTime"), getParam("delayFeedback"), getParam("delayMix"), + static_cast(getParam("delayPingPong")) == 1, getParam("delaySpread")); + reverbFX.setParameters(getParam("reverbSize"), getParam("reverbDamping"), 0.8f, getParam("reverbMix")); + + // FX processing: distortion → compression → auto-pan + int numSamples = buffer.getNumSamples(); + float* leftData = buffer.getWritePointer(0); + float* rightData = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : leftData; + + float twoPi = 6.2831853f; + for (int i = 0; i < numSamples; ++i) { + float left = leftData[i]; + float right = rightData[i]; + + // Distortion (stereo) + left = distortion.process(left); + right = distortion.process(right); + + // Compression (stereo) + left = compressor.process(left); + right = compressor.process(right); + + // Auto-pan + if (panDepth > 0.001f) { + float panLfo = std::sin(autoPanPhase * twoPi); + float leftGain = 1.0f - panDepth * 0.5f * (1.0f + panLfo); + float rightGain = 1.0f - panDepth * 0.5f * (1.0f - panLfo); + left *= leftGain; + right *= rightGain; + autoPanPhase += panRate / static_cast(currentSampleRate); + if (autoPanPhase >= 1.0f) autoPanPhase -= 1.0f; + } + + // Delay + delay.process(left, right); + + leftData[i] = left; + rightData[i] = right; + } + + // Reverb (block-based) + reverbFX.process(leftData, rightData, numSamples); + + // Metering + float sumSquares = 0.0f; + float peak = 0.0f; + for (int i = 0; i < numSamples; ++i) { + float mix = (leftData[i] + rightData[i]) * 0.5f; + + int writePos = scopeWritePos.load(std::memory_order_relaxed); + scopeBuffer[writePos] = mix; + scopeWritePos.store((writePos + 1) % scopeBufferSize, std::memory_order_release); + + int fftPos = fftWritePos.load(std::memory_order_relaxed); + fftInput[fftPos] = mix; + fftWritePos.store((fftPos + 1) % fftSize, std::memory_order_release); + + float absMix = std::abs(mix); + if (absMix > peak) peak = absMix; + sumSquares += mix * mix; + } + + float rms = std::sqrt(sumSquares / static_cast(numSamples)); + float level = juce::jmax(rms, peak); + level = std::min(level * 5.0f, 1.0f); + + float prev = rmsLevel.load(std::memory_order_relaxed); + if (level > prev) + rmsLevel.store(level, std::memory_order_relaxed); + else + rmsLevel.store(prev * 0.95f, std::memory_order_relaxed); +} + +juce::AudioProcessorEditor* ChromaFlockProcessor::createEditor() { + return new ChromaFlockEditor(*this); +} + +void ChromaFlockProcessor::getStateInformation(juce::MemoryBlock& destData) { + auto state = apvts.copyState(); + std::unique_ptr xml(state.createXml()); + copyXmlToBinary(*xml, destData); +} + +void ChromaFlockProcessor::setStateInformation(const void* data, int sizeInBytes) { + std::unique_ptr xml(getXmlFromBinary(data, sizeInBytes)); + if (xml && xml->hasTagName(apvts.state.getType())) + apvts.replaceState(juce::ValueTree::fromXml(*xml)); +} + +juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { + return new ChromaFlockProcessor(); +} diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h new file mode 100644 index 0000000..c8c27de --- /dev/null +++ b/Source/PluginProcessor.h @@ -0,0 +1,93 @@ +#pragma once +#include +#include +#include +#include "DSP/Voice.h" +#include "DSP/Distortion.h" +#include "DSP/Compressor.h" +#include "DSP/Delay.h" +#include "DSP/Reverb.h" +#include "PresetManager.h" +#include +#include + +class ChromaFlockProcessor : public juce::AudioProcessor { +public: + ChromaFlockProcessor(); + ~ChromaFlockProcessor() override; + + void prepareToPlay(double sampleRate, int samplesPerBlock) override; + void releaseResources() override; + void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override; + + juce::AudioProcessorEditor* createEditor() override; + bool hasEditor() const override { return true; } + + const juce::String getName() const override { return "ChromaFlock"; } + bool acceptsMidi() const override { return true; } + bool producesMidi() const override { return false; } + bool isMidiEffect() const override { return false; } + double getTailLengthSeconds() const override { return 0.0; } + + int getNumPrograms() override { return 1; } + int getCurrentProgram() override { return 0; } + void setCurrentProgram(int) override {} + const juce::String getProgramName(int) override { return {}; } + void changeProgramName(int, const juce::String&) override {} + + void getStateInformation(juce::MemoryBlock& destData) override; + void setStateInformation(const void* data, int sizeInBytes) override; + + juce::AudioProcessorValueTreeState apvts; + PresetManager presetManager; + + static constexpr int scopeBufferSize = 1024; + std::array scopeBuffer{}; + std::atomic scopeWritePos{0}; + + static constexpr int fftOrder = 11; + static constexpr int fftSize = 1 << fftOrder; + std::array fftInput{}; + std::atomic fftWritePos{0}; + + float getRmsLevel() const { return rmsLevel.load(); } + + void noteOn(int midiNote, float velocity) { + if (midiNote >= 0 && midiNote < 128) + activeNotes[midiNote].store(true, std::memory_order_relaxed); + synth.noteOn(1, midiNote, velocity); + } + void noteOff(int midiNote) { + if (midiNote >= 0 && midiNote < 128) + activeNotes[midiNote].store(false, std::memory_order_relaxed); + synth.noteOff(1, midiNote, 0.0f, true); + } + bool isNoteActive(int midiNote) const { + return midiNote >= 0 && midiNote < 128 + && activeNotes[midiNote].load(std::memory_order_relaxed); + } + + std::atomic pitchBendValue{0.0f}; + float getPitchBend() const { return pitchBendValue.load(std::memory_order_relaxed); } + +private: + juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); + + void updateVoiceParameters(); + + juce::Synthesiser synth; + static constexpr int maxVoices = 16; + + Distortion distortion; + Compressor compressor; + Delay delay; + ReverbFX reverbFX; + float autoPanPhase = 0.0f; + + double currentSampleRate = 44100.0; + + std::atomic rmsLevel{0.0f}; + std::array, 128> activeNotes{}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ChromaFlockProcessor) +}; diff --git a/Source/PresetManager.h b/Source/PresetManager.h new file mode 100644 index 0000000..5255a94 --- /dev/null +++ b/Source/PresetManager.h @@ -0,0 +1,676 @@ +#pragma once +#include +#include +#include + +struct Preset { + juce::String name; + juce::String category; + std::map params; +}; + +class PresetManager { +public: + PresetManager() { buildPresets(); } + + const std::vector& getPresets() const { return presets; } + + std::vector getCategories() const { + std::vector cats; + for (auto& p : presets) { + bool found = false; + for (auto& c : cats) + if (c == p.category) { found = true; break; } + if (!found) cats.push_back(p.category); + } + return cats; + } + + std::vector getPresetsInCategory(const juce::String& cat) const { + std::vector result; + for (auto& p : presets) + if (p.category == cat) + result.push_back(p); + return result; + } + + void applyPreset(const juce::String& name, juce::AudioProcessorValueTreeState& apvts) { + for (auto& p : presets) { + if (p.name == name) { + for (auto& kv : p.params) { + if (auto* param = apvts.getParameter(kv.first)) + param->setValueNotifyingHost(param->convertTo0to1(kv.second)); + } + return; + } + } + } + +private: + std::vector presets; + + void add(const juce::String& name, const juce::String& category, + std::map params) { + presets.push_back({ name, category, params }); + } + + void buildPresets() { + // --- DEFAULT --- + add("Init", "Init", { + {"osc1Wave", 1}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 2}, {"osc2Oct", -1}, {"osc2Semi", 0}, {"osc2Fine", 7}, {"osc2Level", 0.7f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 800}, {"filterRes", 0.7f}, {"filterEnvAmt", 0}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.01f}, {"fEnvDecay", 0.3f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 0.5f}, + {"envAttack", 0.01f}, {"envDecay", 0.3f}, {"envSustain", 0.7f}, {"envRelease", 0.5f}, + {"pan", 0}, {"drive", 1.5f}, {"masterLevel", 0.8f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -20}, {"compRatio", 4}, {"compAttack", 10}, {"compRelease", 100}, {"compMakeup", 0}, {"compMix", 1}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 200}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.5f}, {"reverbDamping", 0.5f}, {"reverbMix", 0} + }); + + // --- BASS --- + add("Sub Bass", "Bass", { + {"osc1Wave", 0}, {"osc1Oct", -2}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.8f}, + {"osc2Wave", 0}, {"osc2Oct", -1}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.3f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 150}, {"filterRes", 0.2f}, {"filterEnvAmt", 0}, {"keyTrack", 0.7f}, + {"fEnvAttack", 0.01f}, {"fEnvDecay", 0.3f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 0.5f}, + {"envAttack", 0.005f}, {"envDecay", 0.4f}, {"envSustain", 0.7f}, {"envRelease", 0.3f}, + {"pan", 0}, {"drive", 1.2f}, {"masterLevel", 0.85f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -18}, {"compRatio", 6}, {"compAttack", 5}, {"compRelease", 80}, {"compMakeup", 4}, {"compMix", 0.8f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0.2f}, {"lfo1Depth", 0.15f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 200}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.5f}, {"reverbDamping", 0.5f}, {"reverbMix", 0} + }); + + add("Reese Bass", "Bass", { + {"osc1Wave", 1}, {"osc1Oct", -1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 1}, {"osc2Oct", -1}, {"osc2Semi", -7}, {"osc2Fine", 15}, {"osc2Level", 0.7f}, + {"phaseOffset", 0.3f}, + {"filterType", 0}, {"filterCutoff", 400}, {"filterRes", 0.4f}, {"filterEnvAmt", 0.1f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.01f}, {"fEnvDecay", 0.5f}, {"fEnvSustain", 0.4f}, {"fEnvRelease", 0.5f}, + {"envAttack", 0.005f}, {"envDecay", 0.3f}, {"envSustain", 0.8f}, {"envRelease", 0.4f}, + {"pan", 0}, {"drive", 2.0f}, {"masterLevel", 0.8f}, + {"distType", 0}, {"distAmount", 0.1f}, {"distMix", 0.3f}, + {"compThreshold", -15}, {"compRatio", 4}, {"compAttack", 8}, {"compRelease", 100}, {"compMakeup", 3}, {"compMix", 0.7f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0.3f}, {"lfo1Depth", 0.2f}, {"lfo1Shape", 0}, {"lfo1Dest", 3}, + {"lfo2Rate", 0.15f}, {"lfo2Depth", 0.1f}, {"lfo2Shape", 1}, {"lfo2Dest", 0}, + {"delayTime", 200}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.5f}, {"reverbDamping", 0.5f}, {"reverbMix", 0} + }); + + add("Acid Bass", "Bass", { + {"osc1Wave", 1}, {"osc1Oct", -1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.8f}, + {"osc2Wave", 0}, {"osc2Oct", -2}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 300}, {"filterRes", 0.85f}, {"filterEnvAmt", 0.7f}, {"keyTrack", 0.6f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.2f}, {"fEnvSustain", 0.1f}, {"fEnvRelease", 0.3f}, + {"envAttack", 0.001f}, {"envDecay", 0.3f}, {"envSustain", 0.4f}, {"envRelease", 0.2f}, + {"pan", 0}, {"drive", 1.8f}, {"masterLevel", 0.85f}, + {"distType", 0}, {"distAmount", 0.1f}, {"distMix", 0.2f}, + {"compThreshold", -20}, {"compRatio", 5}, {"compAttack", 5}, {"compRelease", 60}, {"compMakeup", 3}, {"compMix", 0.6f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 4.0f}, {"lfo1Depth", 0.3f}, {"lfo1Shape", 2}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 200}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.5f}, {"reverbDamping", 0.5f}, {"reverbMix", 0} + }); + + add("Pluck Bass", "Bass", { + {"osc1Wave", 1}, {"osc1Oct", -1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 2}, {"osc2Oct", -2}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.4f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 500}, {"filterRes", 0.3f}, {"filterEnvAmt", 0.5f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.15f}, {"fEnvSustain", 0.1f}, {"fEnvRelease", 0.2f}, + {"envAttack", 0.001f}, {"envDecay", 0.15f}, {"envSustain", 0.1f}, {"envRelease", 0.1f}, + {"pan", 0}, {"drive", 1.5f}, {"masterLevel", 0.8f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -15}, {"compRatio", 4}, {"compAttack", 3}, {"compRelease", 50}, {"compMakeup", 2}, {"compMix", 0.5f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 200}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.5f}, {"reverbDamping", 0.5f}, {"reverbMix", 0} + }); + + add("Moog Bass", "Bass", { + {"osc1Wave", 2}, {"osc1Oct", -1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.9f}, + {"osc2Wave", 2}, {"osc2Oct", -1}, {"osc2Semi", 0}, {"osc2Fine", 8}, {"osc2Level", 0.4f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 250}, {"filterRes", 0.6f}, {"filterEnvAmt", 0.5f}, {"keyTrack", 0.6f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.25f}, {"fEnvSustain", 0.2f}, {"fEnvRelease", 0.2f}, + {"envAttack", 0.001f}, {"envDecay", 0.3f}, {"envSustain", 0.7f}, {"envRelease", 0.15f}, + {"pan", 0}, {"drive", 1.6f}, {"masterLevel", 0.85f}, + {"distType", 0}, {"distAmount", 0.15f}, {"distMix", 0.2f}, + {"compThreshold", -16}, {"compRatio", 5}, {"compAttack", 3}, {"compRelease", 60}, {"compMakeup", 4}, {"compMix", 0.7f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 120}, {"delayFeedback", 0.15f}, {"delayMix", 0.1f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.2f}, {"reverbDamping", 0.7f}, {"reverbMix", 0.05f} + }); + + add("Gritty Bass", "Bass", { + {"osc1Wave", 1}, {"osc1Oct", -1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.8f}, + {"osc2Wave", 2}, {"osc2Oct", -2}, {"osc2Semi", 0}, {"osc2Fine", 12}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.4f}, + {"filterType", 0}, {"filterCutoff", 350}, {"filterRes", 0.5f}, {"filterEnvAmt", 0.3f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.2f}, {"fEnvSustain", 0.3f}, {"fEnvRelease", 0.15f}, + {"envAttack", 0.001f}, {"envDecay", 0.25f}, {"envSustain", 0.6f}, {"envRelease", 0.15f}, + {"pan", 0}, {"drive", 3.5f}, {"masterLevel", 0.75f}, + {"distType", 2}, {"distAmount", 0.4f}, {"distMix", 0.35f}, + {"compThreshold", -14}, {"compRatio", 6}, {"compAttack", 2}, {"compRelease", 50}, {"compMakeup", 5}, {"compMix", 0.8f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 80}, {"delayFeedback", 0.2f}, {"delayMix", 0.08f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.15f}, {"reverbDamping", 0.8f}, {"reverbMix", 0.05f} + }); + + add("FM Bass", "Bass", { + {"osc1Wave", 0}, {"osc1Oct", -1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.8f}, + {"osc2Wave", 0}, {"osc2Oct", 2}, {"osc2Semi", 7}, {"osc2Fine", 0}, {"osc2Level", 0.6f}, + {"phaseOffset", 0.3f}, + {"filterType", 0}, {"filterCutoff", 600}, {"filterRes", 0.2f}, {"filterEnvAmt", 0.4f}, {"keyTrack", 0.7f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.12f}, {"fEnvSustain", 0.0f}, {"fEnvRelease", 0.1f}, + {"envAttack", 0.001f}, {"envDecay", 0.2f}, {"envSustain", 0.5f}, {"envRelease", 0.1f}, + {"pan", 0}, {"drive", 1.3f}, {"masterLevel", 0.85f}, + {"distType", 0}, {"distAmount", 0.1f}, {"distMix", 0.15f}, + {"compThreshold", -16}, {"compRatio", 4}, {"compAttack", 2}, {"compRelease", 40}, {"compMakeup", 3}, {"compMix", 0.7f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 100}, {"delayFeedback", 0.1f}, {"delayMix", 0.05f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.3f}, {"reverbDamping", 0.6f}, {"reverbMix", 0.08f} + }); + + // --- LEAD --- + add("Sync Lead", "Lead", { + {"osc1Wave", 1}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 2}, {"osc2Oct", 0}, {"osc2Semi", 7}, {"osc2Fine", 0}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 800}, {"filterRes", 0.3f}, {"filterEnvAmt", 0.2f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.005f}, {"fEnvDecay", 0.3f}, {"fEnvSustain", 0.4f}, {"fEnvRelease", 0.3f}, + {"envAttack", 0.005f}, {"envDecay", 0.2f}, {"envSustain", 0.7f}, {"envRelease", 0.3f}, + {"pan", 0}, {"drive", 2.0f}, {"masterLevel", 0.75f}, + {"distType", 0}, {"distAmount", 0.2f}, {"distMix", 0.3f}, + {"compThreshold", -18}, {"compRatio", 3}, {"compAttack", 8}, {"compRelease", 80}, {"compMakeup", 3}, {"compMix", 0.6f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 5.0f}, {"lfo1Depth", 0.15f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 250}, {"delayFeedback", 0.25f}, {"delayMix", 0.15f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.4f}, {"reverbDamping", 0.5f}, {"reverbMix", 0.1f} + }); + + add("Square Lead", "Lead", { + {"osc1Wave", 2}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.6f}, + {"osc2Wave", 2}, {"osc2Oct", 0}, {"osc2Semi", 5}, {"osc2Fine", 10}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 900}, {"filterRes", 0.2f}, {"filterEnvAmt", 0.15f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.005f}, {"fEnvDecay", 0.4f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 0.3f}, + {"envAttack", 0.005f}, {"envDecay", 0.2f}, {"envSustain", 0.8f}, {"envRelease", 0.3f}, + {"pan", 0}, {"drive", 1.5f}, {"masterLevel", 0.75f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -15}, {"compRatio", 3}, {"compAttack", 10}, {"compRelease", 100}, {"compMakeup", 2}, {"compMix", 0.5f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 3.0f}, {"lfo1Depth", 0.1f}, {"lfo1Shape", 3}, {"lfo1Dest", 2}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 300}, {"delayFeedback", 0.3f}, {"delayMix", 0.2f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.35f}, {"reverbDamping", 0.5f}, {"reverbMix", 0.12f} + }); + + add("Soft Lead", "Lead", { + {"osc1Wave", 3}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 0}, {"osc2Oct", 0}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.4f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 700}, {"filterRes", 0.15f}, {"filterEnvAmt", 0.1f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.01f}, {"fEnvDecay", 0.4f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 0.5f}, + {"envAttack", 0.01f}, {"envDecay", 0.3f}, {"envSustain", 0.7f}, {"envRelease", 0.5f}, + {"pan", 0}, {"drive", 1.3f}, {"masterLevel", 0.8f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -20}, {"compRatio", 3}, {"compAttack", 10}, {"compRelease", 120}, {"compMakeup", 2}, {"compMix", 0.4f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0.5f}, {"lfo1Depth", 0.08f}, {"lfo1Shape", 0}, {"lfo1Dest", 1}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 350}, {"delayFeedback", 0.2f}, {"delayMix", 0.15f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.45f}, {"reverbDamping", 0.4f}, {"reverbMix", 0.15f} + }); + + add("Saw Lead", "Lead", { + {"osc1Wave", 1}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.8f}, + {"osc2Wave", 1}, {"osc2Oct", 0}, {"osc2Semi", 12}, {"osc2Fine", 0}, {"osc2Level", 0.3f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 850}, {"filterRes", 0.25f}, {"filterEnvAmt", 0.2f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.005f}, {"fEnvDecay", 0.3f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 0.3f}, + {"envAttack", 0.005f}, {"envDecay", 0.2f}, {"envSustain", 0.8f}, {"envRelease", 0.3f}, + {"pan", 0}, {"drive", 1.8f}, {"masterLevel", 0.75f}, + {"distType", 0}, {"distAmount", 0.15f}, {"distMix", 0.25f}, + {"compThreshold", -18}, {"compRatio", 3}, {"compAttack", 8}, {"compRelease", 100}, {"compMakeup", 3}, {"compMix", 0.5f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 6.0f}, {"lfo1Depth", 0.12f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.8f}, {"lfo2Depth", 0.1f}, {"lfo2Shape", 1}, {"lfo2Dest", 3}, + {"delayTime", 200}, {"delayFeedback", 0.2f}, {"delayMix", 0.12f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.3f}, {"reverbDamping", 0.5f}, {"reverbMix", 0.1f} + }); + + // --- PAD --- + add("Warm Pad", "Pad", { + {"osc1Wave", 1}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.5f}, + {"osc2Wave", 3}, {"osc2Oct", 0}, {"osc2Semi", 0}, {"osc2Fine", 12}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 400}, {"filterRes", 0.2f}, {"filterEnvAmt", 0.1f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.5f}, {"fEnvDecay", 1.0f}, {"fEnvSustain", 0.6f}, {"fEnvRelease", 2.0f}, + {"envAttack", 1.5f}, {"envDecay", 1.0f}, {"envSustain", 0.8f}, {"envRelease", 3.0f}, + {"pan", 0}, {"drive", 1.2f}, {"masterLevel", 0.7f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -20}, {"compRatio", 2}, {"compAttack", 20}, {"compRelease", 200}, {"compMakeup", 2}, {"compMix", 0.4f}, + {"autoPanRate", 0.3f}, {"autoPanDepth", 0.3f}, + {"lfo1Rate", 0.25f}, {"lfo1Depth", 0.15f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.1f}, {"lfo2Depth", 0.1f}, {"lfo2Shape", 1}, {"lfo2Dest", 3}, + {"delayTime", 400}, {"delayFeedback", 0.3f}, {"delayMix", 0.15f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.7f}, {"reverbDamping", 0.3f}, {"reverbMix", 0.3f} + }); + + add("Glass Pad", "Pad", { + {"osc1Wave", 0}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.6f}, + {"osc2Wave", 3}, {"osc2Oct", 0}, {"osc2Semi", 0}, {"osc2Fine", 7}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 600}, {"filterRes", 0.15f}, {"filterEnvAmt", 0.05f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.8f}, {"fEnvDecay", 1.5f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 2.5f}, + {"envAttack", 2.0f}, {"envDecay", 1.5f}, {"envSustain", 0.7f}, {"envRelease", 4.0f}, + {"pan", 0}, {"drive", 1.0f}, {"masterLevel", 0.7f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -25}, {"compRatio", 2}, {"compAttack", 30}, {"compRelease", 250}, {"compMakeup", 3}, {"compMix", 0.3f}, + {"autoPanRate", 0.4f}, {"autoPanDepth", 0.25f}, + {"lfo1Rate", 0.15f}, {"lfo1Depth", 0.2f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.08f}, {"lfo2Depth", 0.12f}, {"lfo2Shape", 0}, {"lfo2Dest", 1}, + {"delayTime", 500}, {"delayFeedback", 0.35f}, {"delayMix", 0.2f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.8f}, {"reverbDamping", 0.2f}, {"reverbMix", 0.35f} + }); + + add("Dark Pad", "Pad", { + {"osc1Wave", 2}, {"osc1Oct", -1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.5f}, + {"osc2Wave", 1}, {"osc2Oct", -1}, {"osc2Semi", 0}, {"osc2Fine", 15}, {"osc2Level", 0.4f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 250}, {"filterRes", 0.3f}, {"filterEnvAmt", 0.15f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.3f}, {"fEnvDecay", 0.8f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 1.5f}, + {"envAttack", 1.0f}, {"envDecay", 0.8f}, {"envSustain", 0.8f}, {"envRelease", 3.0f}, + {"pan", 0}, {"drive", 1.3f}, {"masterLevel", 0.7f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -18}, {"compRatio", 3}, {"compAttack", 15}, {"compRelease", 150}, {"compMakeup", 2}, {"compMix", 0.5f}, + {"autoPanRate", 0.2f}, {"autoPanDepth", 0.2f}, + {"lfo1Rate", 0.2f}, {"lfo1Depth", 0.25f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.35f}, {"lfo2Depth", 0.1f}, {"lfo2Shape", 2}, {"lfo2Dest", 3}, + {"delayTime", 600}, {"delayFeedback", 0.4f}, {"delayMix", 0.25f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.85f}, {"reverbDamping", 0.25f}, {"reverbMix", 0.4f} + }); + + add("Shimmer Pad", "Pad", { + {"osc1Wave", 0}, {"osc1Oct", 1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.4f}, + {"osc2Wave", 3}, {"osc2Oct", 0}, {"osc2Semi", 12}, {"osc2Fine", 0}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 500}, {"filterRes", 0.2f}, {"filterEnvAmt", 0.3f}, {"keyTrack", 0.6f}, + {"fEnvAttack", 1.0f}, {"fEnvDecay", 2.0f}, {"fEnvSustain", 0.7f}, {"fEnvRelease", 3.0f}, + {"envAttack", 2.5f}, {"envDecay", 2.0f}, {"envSustain", 0.9f}, {"envRelease", 5.0f}, + {"pan", 0}, {"drive", 1.0f}, {"masterLevel", 0.7f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -25}, {"compRatio", 2}, {"compAttack", 25}, {"compRelease", 200}, {"compMakeup", 3}, {"compMix", 0.3f}, + {"autoPanRate", 0.5f}, {"autoPanDepth", 0.4f}, + {"lfo1Rate", 0.12f}, {"lfo1Depth", 0.3f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.05f}, {"lfo2Depth", 0.15f}, {"lfo2Shape", 0}, {"lfo2Dest", 1}, + {"delayTime", 600}, {"delayFeedback", 0.5f}, {"delayMix", 0.3f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.9f}, {"reverbDamping", 0.15f}, {"reverbMix", 0.5f} + }); + + add("Evolving Pad", "Pad", { + {"osc1Wave", 3}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", -5}, {"osc1Level", 0.5f}, + {"osc2Wave", 0}, {"osc2Oct", 0}, {"osc2Semi", 7}, {"osc2Fine", 8}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.2f}, + {"filterType", 0}, {"filterCutoff", 350}, {"filterRes", 0.25f}, {"filterEnvAmt", 0.2f}, {"keyTrack", 0.4f}, + {"fEnvAttack", 1.5f}, {"fEnvDecay", 2.5f}, {"fEnvSustain", 0.6f}, {"fEnvRelease", 4.0f}, + {"envAttack", 3.0f}, {"envDecay", 2.0f}, {"envSustain", 0.85f}, {"envRelease", 5.0f}, + {"pan", 0}, {"drive", 1.1f}, {"masterLevel", 0.7f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -22}, {"compRatio", 2}, {"compAttack", 30}, {"compRelease", 300}, {"compMakeup", 2}, {"compMix", 0.3f}, + {"autoPanRate", 0.15f}, {"autoPanDepth", 0.5f}, + {"lfo1Rate", 0.08f}, {"lfo1Depth", 0.35f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.03f}, {"lfo2Depth", 0.2f}, {"lfo2Shape", 1}, {"lfo2Dest", 1}, + {"delayTime", 700}, {"delayFeedback", 0.45f}, {"delayMix", 0.25f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.9f}, {"reverbDamping", 0.1f}, {"reverbMix", 0.45f} + }); + + add("Choir Pad", "Pad", { + {"osc1Wave", 0}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.6f}, + {"osc2Wave", 3}, {"osc2Oct", 0}, {"osc2Semi", 12}, {"osc2Fine", -3}, {"osc2Level", 0.4f}, + {"phaseOffset", 0.1f}, + {"filterType", 0}, {"filterCutoff", 700}, {"filterRes", 0.1f}, {"filterEnvAmt", 0.05f}, {"keyTrack", 0.6f}, + {"fEnvAttack", 0.8f}, {"fEnvDecay", 1.2f}, {"fEnvSustain", 0.7f}, {"fEnvRelease", 2.0f}, + {"envAttack", 1.2f}, {"envDecay", 1.0f}, {"envSustain", 0.85f}, {"envRelease", 3.0f}, + {"pan", 0}, {"drive", 1.0f}, {"masterLevel", 0.75f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -22}, {"compRatio", 2}, {"compAttack", 25}, {"compRelease", 250}, {"compMakeup", 3}, {"compMix", 0.3f}, + {"autoPanRate", 0.2f}, {"autoPanDepth", 0.35f}, + {"lfo1Rate", 0.1f}, {"lfo1Depth", 0.2f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.06f}, {"lfo2Depth", 0.1f}, {"lfo2Shape", 0}, {"lfo2Dest", 1}, + {"delayTime", 550}, {"delayFeedback", 0.4f}, {"delayMix", 0.2f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.85f}, {"reverbDamping", 0.2f}, {"reverbMix", 0.4f} + }); + + // --- KEYS --- + add("Electric Piano", "Keys", { + {"osc1Wave", 0}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 0}, {"osc2Oct", 0}, {"osc2Semi", 12}, {"osc2Fine", 0}, {"osc2Level", 0.3f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 800}, {"filterRes", 0.1f}, {"filterEnvAmt", 0.1f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.5f}, {"fEnvSustain", 0.3f}, {"fEnvRelease", 0.5f}, + {"envAttack", 0.001f}, {"envDecay", 0.5f}, {"envSustain", 0.3f}, {"envRelease", 0.8f}, + {"pan", 0}, {"drive", 1.0f}, {"masterLevel", 0.8f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -15}, {"compRatio", 3}, {"compAttack", 5}, {"compRelease", 80}, {"compMakeup", 2}, {"compMix", 0.4f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 250}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.4f}, {"reverbDamping", 0.5f}, {"reverbMix", 0.15f} + }); + + add("Organ", "Keys", { + {"osc1Wave", 2}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.5f}, + {"osc2Wave", 2}, {"osc2Oct", 1}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.3f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 1000}, {"filterRes", 0.1f}, {"filterEnvAmt", 0}, {"keyTrack", 0.8f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.1f}, {"fEnvSustain", 0.9f}, {"fEnvRelease", 0.1f}, + {"envAttack", 0.001f}, {"envDecay", 0.1f}, {"envSustain", 0.9f}, {"envRelease", 0.1f}, + {"pan", 0}, {"drive", 1.2f}, {"masterLevel", 0.75f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -18}, {"compRatio", 3}, {"compAttack", 8}, {"compRelease", 80}, {"compMakeup", 2}, {"compMix", 0.5f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 6.0f}, {"lfo1Depth", 0.05f}, {"lfo1Shape", 3}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 150}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.3f}, {"reverbDamping", 0.6f}, {"reverbMix", 0.1f} + }); + + add("Clav", "Keys", { + {"osc1Wave", 1}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 2}, {"osc2Oct", 0}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.3f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 700}, {"filterRes", 0.4f}, {"filterEnvAmt", 0.6f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.1f}, {"fEnvSustain", 0.1f}, {"fEnvRelease", 0.15f}, + {"envAttack", 0.001f}, {"envDecay", 0.2f}, {"envSustain", 0.05f}, {"envRelease", 0.1f}, + {"pan", 0}, {"drive", 1.5f}, {"masterLevel", 0.8f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -15}, {"compRatio", 4}, {"compAttack", 5}, {"compRelease", 60}, {"compMakeup", 3}, {"compMix", 0.5f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 100}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.25f}, {"reverbDamping", 0.7f}, {"reverbMix", 0.08f} + }); + + add("Rhodes", "Keys", { + {"osc1Wave", 0}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.65f}, + {"osc2Wave", 0}, {"osc2Oct", 0}, {"osc2Semi", 12}, {"osc2Fine", 5}, {"osc2Level", 0.35f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 650}, {"filterRes", 0.15f}, {"filterEnvAmt", 0.15f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.6f}, {"fEnvSustain", 0.25f}, {"fEnvRelease", 0.7f}, + {"envAttack", 0.001f}, {"envDecay", 0.6f}, {"envSustain", 0.25f}, {"envRelease", 1.0f}, + {"pan", 0}, {"drive", 1.1f}, {"masterLevel", 0.8f}, + {"distType", 0}, {"distAmount", 0.05f}, {"distMix", 0.1f}, + {"compThreshold", -18}, {"compRatio", 3}, {"compAttack", 8}, {"compRelease", 100}, {"compMakeup", 2}, {"compMix", 0.4f}, + {"autoPanRate", 0.8f}, {"autoPanDepth", 0.15f}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 350}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.55f}, {"reverbDamping", 0.4f}, {"reverbMix", 0.25f} + }); + + add("Bell Keys", "Keys", { + {"osc1Wave", 0}, {"osc1Oct", 1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.6f}, + {"osc2Wave", 0}, {"osc2Oct", 1}, {"osc2Semi", 12}, {"osc2Fine", 3}, {"osc2Level", 0.4f}, + {"phaseOffset", 0.3f}, + {"filterType", 0}, {"filterCutoff", 900}, {"filterRes", 0.05f}, {"filterEnvAmt", 0.05f}, {"keyTrack", 0.7f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.8f}, {"fEnvSustain", 0.0f}, {"fEnvRelease", 1.5f}, + {"envAttack", 0.001f}, {"envDecay", 0.8f}, {"envSustain", 0.0f}, {"envRelease", 2.0f}, + {"pan", 0}, {"drive", 1.0f}, {"masterLevel", 0.75f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -20}, {"compRatio", 2}, {"compAttack", 15}, {"compRelease", 150}, {"compMakeup", 2}, {"compMix", 0.3f}, + {"autoPanRate", 0.3f}, {"autoPanDepth", 0.2f}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 400}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.75f}, {"reverbDamping", 0.2f}, {"reverbMix", 0.35f} + }); + + add("Toy Piano", "Keys", { + {"osc1Wave", 0}, {"osc1Oct", 1}, {"osc1Semi", 0}, {"osc1Fine", 12}, {"osc1Level", 0.5f}, + {"osc2Wave", 2}, {"osc2Oct", 2}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.3f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 1100}, {"filterRes", 0.05f}, {"filterEnvAmt", 0.1f}, {"keyTrack", 0.8f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.15f}, {"fEnvSustain", 0.0f}, {"fEnvRelease", 0.3f}, + {"envAttack", 0.001f}, {"envDecay", 0.15f}, {"envSustain", 0.0f}, {"envRelease", 0.3f}, + {"pan", 0}, {"drive", 1.0f}, {"masterLevel", 0.8f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -15}, {"compRatio", 3}, {"compAttack", 3}, {"compRelease", 40}, {"compMakeup", 2}, {"compMix", 0.3f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 180}, {"delayFeedback", 0}, {"delayMix", 0}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.35f}, {"reverbDamping", 0.5f}, {"reverbMix", 0.12f} + }); + + // --- FX --- + add("Wobble", "FX1", { + {"osc1Wave", 2}, {"osc1Oct", -1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 1}, {"osc2Oct", -1}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 400}, {"filterRes", 0.8f}, {"filterEnvAmt", 0.9f}, {"keyTrack", 0.3f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.3f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 0.3f}, + {"envAttack", 0.005f}, {"envDecay", 0.3f}, {"envSustain", 0.8f}, {"envRelease", 0.3f}, + {"pan", 0}, {"drive", 2.5f}, {"masterLevel", 0.7f}, + {"distType", 0}, {"distAmount", 0.3f}, {"distMix", 0.4f}, + {"compThreshold", -15}, {"compRatio", 5}, {"compAttack", 5}, {"compRelease", 80}, {"compMakeup", 4}, {"compMix", 0.7f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 4.0f}, {"lfo1Depth", 0.7f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 8.0f}, {"lfo2Depth", 0.3f}, {"lfo2Shape", 3}, {"lfo2Dest", 3}, + {"delayTime", 200}, {"delayFeedback", 0.3f}, {"delayMix", 0.15f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.5f}, {"reverbDamping", 0.4f}, {"reverbMix", 0.15f} + }); + + add("Distorted Lead", "FX1", { + {"osc1Wave", 1}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.8f}, + {"osc2Wave", 1}, {"osc2Oct", 0}, {"osc2Semi", 7}, {"osc2Fine", 0}, {"osc2Level", 0.6f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 600}, {"filterRes", 0.3f}, {"filterEnvAmt", 0.2f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.005f}, {"fEnvDecay", 0.3f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 0.3f}, + {"envAttack", 0.005f}, {"envDecay", 0.2f}, {"envSustain", 0.8f}, {"envRelease", 0.3f}, + {"pan", 0}, {"drive", 3.0f}, {"masterLevel", 0.65f}, + {"distType", 0}, {"distAmount", 0.5f}, {"distMix", 0.6f}, + {"compThreshold", -20}, {"compRatio", 4}, {"compAttack", 8}, {"compRelease", 100}, {"compMakeup", 6}, {"compMix", 0.8f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 5.0f}, {"lfo1Depth", 0.2f}, {"lfo1Shape", 2}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 150}, {"delayFeedback", 0.2f}, {"delayMix", 0.1f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.3f}, {"reverbDamping", 0.5f}, {"reverbMix", 0.1f} + }); + + add("Auto-Pan Spread", "FX1", { + {"osc1Wave", 0}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.6f}, + {"osc2Wave", 3}, {"osc2Oct", 0}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.4f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 800}, {"filterRes", 0.1f}, {"filterEnvAmt", 0}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.01f}, {"fEnvDecay", 0.3f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 0.5f}, + {"envAttack", 0.01f}, {"envDecay", 0.3f}, {"envSustain", 0.7f}, {"envRelease", 0.5f}, + {"pan", 0}, {"drive", 1.0f}, {"masterLevel", 0.8f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -20}, {"compRatio", 3}, {"compAttack", 10}, {"compRelease", 100}, {"compMakeup", 2}, {"compMix", 0.4f}, + {"autoPanRate", 2.0f}, {"autoPanDepth", 0.8f}, + {"lfo1Rate", 0.3f}, {"lfo1Depth", 0.2f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.15f}, {"lfo2Depth", 0.15f}, {"lfo2Shape", 1}, {"lfo2Dest", 3}, + {"delayTime", 300}, {"delayFeedback", 0.25f}, {"delayMix", 0.15f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.4f}, {"reverbDamping", 0.5f}, {"reverbMix", 0.15f} + }); + + add("Filtered Noise", "FX1", { + {"osc1Wave", 4}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 4}, {"osc2Oct", 0}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.5f}, + {"filterType", 2}, {"filterCutoff", 400}, {"filterRes", 0.6f}, {"filterEnvAmt", 0.8f}, {"keyTrack", 0}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.5f}, {"fEnvSustain", 0.3f}, {"fEnvRelease", 0.5f}, + {"envAttack", 0.01f}, {"envDecay", 0.5f}, {"envSustain", 0.5f}, {"envRelease", 1.0f}, + {"pan", 0}, {"drive", 1.5f}, {"masterLevel", 0.7f}, + {"distType", 1}, {"distAmount", 0.2f}, {"distMix", 0.3f}, + {"compThreshold", -18}, {"compRatio", 4}, {"compAttack", 5}, {"compRelease", 100}, {"compMakeup", 4}, {"compMix", 0.6f}, + {"autoPanRate", 0.5f}, {"autoPanDepth", 0.6f}, + {"lfo1Rate", 2.5f}, {"lfo1Depth", 0.5f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.4f}, {"lfo2Depth", 0.3f}, {"lfo2Shape", 2}, {"lfo2Dest", 3}, + {"delayTime", 250}, {"delayFeedback", 0.35f}, {"delayMix", 0.2f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.6f}, {"reverbDamping", 0.3f}, {"reverbMix", 0.25f} + }); + + add("Dub Delay", "FX2", { + {"osc1Wave", 1}, {"osc1Oct", -1}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 2}, {"osc2Oct", -1}, {"osc2Semi", 0}, {"osc2Fine", 5}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 500}, {"filterRes", 0.7f}, {"filterEnvAmt", 0.4f}, {"keyTrack", 0.3f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.3f}, {"fEnvSustain", 0.3f}, {"fEnvRelease", 0.3f}, + {"envAttack", 0.001f}, {"envDecay", 0.2f}, {"envSustain", 0.7f}, {"envRelease", 0.2f}, + {"pan", 0}, {"drive", 1.5f}, {"masterLevel", 0.7f}, + {"distType", 0}, {"distAmount", 0.1f}, {"distMix", 0.15f}, + {"compThreshold", -16}, {"compRatio", 4}, {"compAttack", 5}, {"compRelease", 80}, {"compMakeup", 3}, {"compMix", 0.5f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 0}, {"lfo1Depth", 0}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 500}, {"delayFeedback", 0.7f}, {"delayMix", 0.4f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.6f}, {"reverbDamping", 0.4f}, {"reverbMix", 0.3f} + }); + + add("Reverse Reverb", "FX2", { + {"osc1Wave", 3}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.6f}, + {"osc2Wave", 0}, {"osc2Oct", 1}, {"osc2Semi", 12}, {"osc2Fine", 0}, {"osc2Level", 0.4f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 600}, {"filterRes", 0.2f}, {"filterEnvAmt", 0.3f}, {"keyTrack", 0.5f}, + {"fEnvAttack", 0.5f}, {"fEnvDecay", 1.0f}, {"fEnvSustain", 0.5f}, {"fEnvRelease", 2.0f}, + {"envAttack", 0.3f}, {"envDecay", 0.8f}, {"envSustain", 0.6f}, {"envRelease", 1.5f}, + {"pan", 0}, {"drive", 1.2f}, {"masterLevel", 0.65f}, + {"distType", 0}, {"distAmount", 0}, {"distMix", 0}, + {"compThreshold", -20}, {"compRatio", 2}, {"compAttack", 20}, {"compRelease", 200}, {"compMakeup", 2}, {"compMix", 0.3f}, + {"autoPanRate", 0.3f}, {"autoPanDepth", 0.3f}, + {"lfo1Rate", 0.1f}, {"lfo1Depth", 0.3f}, {"lfo1Shape", 1}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 800}, {"delayFeedback", 0.6f}, {"delayMix", 0.5f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.95f}, {"reverbDamping", 0.1f}, {"reverbMix", 0.6f} + }); + + add("Laser Zap", "FX2", { + {"osc1Wave", 1}, {"osc1Oct", 2}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.8f}, + {"osc2Wave", 4}, {"osc2Oct", 0}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.3f}, + {"phaseOffset", 0.5f}, + {"filterType", 0}, {"filterCutoff", 200}, {"filterRes", 0.9f}, {"filterEnvAmt", 1.0f}, {"keyTrack", 0}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.08f}, {"fEnvSustain", 0.0f}, {"fEnvRelease", 0.05f}, + {"envAttack", 0.001f}, {"envDecay", 0.1f}, {"envSustain", 0.0f}, {"envRelease", 0.05f}, + {"pan", 0}, {"drive", 2.0f}, {"masterLevel", 0.7f}, + {"distType", 1}, {"distAmount", 0.3f}, {"distMix", 0.4f}, + {"compThreshold", -15}, {"compRatio", 5}, {"compAttack", 1}, {"compRelease", 30}, {"compMakeup", 5}, {"compMix", 0.7f}, + {"autoPanRate", 2}, {"autoPanDepth", 0}, + {"lfo1Rate", 12.0f}, {"lfo1Depth", 0.8f}, {"lfo1Shape", 2}, {"lfo1Dest", 0}, + {"lfo2Rate", 0}, {"lfo2Depth", 0}, {"lfo2Shape", 0}, {"lfo2Dest", 0}, + {"delayTime", 100}, {"delayFeedback", 0.4f}, {"delayMix", 0.25f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.3f}, {"reverbDamping", 0.6f}, {"reverbMix", 0.2f} + }); + + add("Space Drone", "FX2", { + {"osc1Wave", 0}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", -7}, {"osc1Level", 0.5f}, + {"osc2Wave", 3}, {"osc2Oct", 0}, {"osc2Semi", 7}, {"osc2Fine", 7}, {"osc2Level", 0.5f}, + {"phaseOffset", 0.1f}, + {"filterType", 0}, {"filterCutoff", 300}, {"filterRes", 0.5f}, {"filterEnvAmt", 0.2f}, {"keyTrack", 0.3f}, + {"fEnvAttack", 2.0f}, {"fEnvDecay", 3.0f}, {"fEnvSustain", 0.7f}, {"fEnvRelease", 5.0f}, + {"envAttack", 3.0f}, {"envDecay", 2.0f}, {"envSustain", 0.9f}, {"envRelease", 6.0f}, + {"pan", 0}, {"drive", 1.1f}, {"masterLevel", 0.6f}, + {"distType", 0}, {"distAmount", 0.05f}, {"distMix", 0.1f}, + {"compThreshold", -25}, {"compRatio", 2}, {"compAttack", 40}, {"compRelease", 500}, {"compMakeup", 3}, {"compMix", 0.3f}, + {"autoPanRate", 0.1f}, {"autoPanDepth", 0.6f}, + {"lfo1Rate", 0.05f}, {"lfo1Depth", 0.4f}, {"lfo1Shape", 0}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.02f}, {"lfo2Depth", 0.25f}, {"lfo2Shape", 1}, {"lfo2Dest", 3}, + {"delayTime", 900}, {"delayFeedback", 0.75f}, {"delayMix", 0.5f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.95f}, {"reverbDamping", 0.05f}, {"reverbMix", 0.7f} + }); + + add("Ring Mod", "FX2", { + {"osc1Wave", 1}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.7f}, + {"osc2Wave", 0}, {"osc2Oct", 3}, {"osc2Semi", 5}, {"osc2Fine", 0}, {"osc2Level", 0.6f}, + {"phaseOffset", 0.5f}, + {"filterType", 2}, {"filterCutoff", 800}, {"filterRes", 0.3f}, {"filterEnvAmt", 0.5f}, {"keyTrack", 0.4f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.3f}, {"fEnvSustain", 0.4f}, {"fEnvRelease", 0.3f}, + {"envAttack", 0.001f}, {"envDecay", 0.3f}, {"envSustain", 0.7f}, {"envRelease", 0.3f}, + {"pan", 0}, {"drive", 2.0f}, {"masterLevel", 0.65f}, + {"distType", 3}, {"distAmount", 0.3f}, {"distMix", 0.3f}, + {"compThreshold", -15}, {"compRatio", 4}, {"compAttack", 3}, {"compRelease", 50}, {"compMakeup", 4}, {"compMix", 0.6f}, + {"autoPanRate", 3.0f}, {"autoPanDepth", 0.5f}, + {"lfo1Rate", 7.0f}, {"lfo1Depth", 0.6f}, {"lfo1Shape", 3}, {"lfo1Dest", 0}, + {"lfo2Rate", 11.0f}, {"lfo2Depth", 0.4f}, {"lfo2Shape", 2}, {"lfo2Dest", 2}, + {"delayTime", 180}, {"delayFeedback", 0.3f}, {"delayMix", 0.2f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.5f}, {"reverbDamping", 0.5f}, {"reverbMix", 0.2f} + }); + + add("Glitch Stutter", "FX2", { + {"osc1Wave", 2}, {"osc1Oct", 0}, {"osc1Semi", 0}, {"osc1Fine", 0}, {"osc1Level", 0.8f}, + {"osc2Wave", 4}, {"osc2Oct", 0}, {"osc2Semi", 0}, {"osc2Fine", 0}, {"osc2Level", 0.4f}, + {"phaseOffset", 0.5f}, + {"filterType", 1}, {"filterCutoff", 600}, {"filterRes", 0.6f}, {"filterEnvAmt", 0.7f}, {"keyTrack", 0.2f}, + {"fEnvAttack", 0.001f}, {"fEnvDecay", 0.05f}, {"fEnvSustain", 0.0f}, {"fEnvRelease", 0.03f}, + {"envAttack", 0.001f}, {"envDecay", 0.05f}, {"envSustain", 0.0f}, {"envRelease", 0.03f}, + {"pan", 0}, {"drive", 3.0f}, {"masterLevel", 0.6f}, + {"distType", 1}, {"distAmount", 0.5f}, {"distMix", 0.5f}, + {"compThreshold", -12}, {"compRatio", 8}, {"compAttack", 1}, {"compRelease", 20}, {"compMakeup", 6}, {"compMix", 0.8f}, + {"autoPanRate", 8.0f}, {"autoPanDepth", 0.8f}, + {"lfo1Rate", 15.0f}, {"lfo1Depth", 1.0f}, {"lfo1Shape", 3}, {"lfo1Dest", 0}, + {"lfo2Rate", 0.1f}, {"lfo2Depth", 0.5f}, {"lfo2Shape", 3}, {"lfo2Dest", 3}, + {"delayTime", 60}, {"delayFeedback", 0.5f}, {"delayMix", 0.3f}, {"delayPingPong", 0}, {"delaySpread", 0.5f}, + + {"reverbSize", 0.4f}, {"reverbDamping", 0.3f}, {"reverbMix", 0.15f} + }); + } +}; diff --git a/cfnew.png b/cfnew.png new file mode 100644 index 0000000..ead2a7c Binary files /dev/null and b/cfnew.png differ diff --git a/chromaflockbg5.png b/chromaflockbg5.png new file mode 100644 index 0000000..886b044 Binary files /dev/null and b/chromaflockbg5.png differ