mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 04:10:47 +02:00
init
This commit is contained in:
commit
dca008e860
21 changed files with 3746 additions and 0 deletions
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
263
AGENTS.md
Normal file
263
AGENTS.md
Normal file
|
|
@ -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 <juce_core/...>` but currently compiles without explicit include
|
||||
|
||||
**Filter** (`DSP/Filter.h`)
|
||||
- TPT (Topology-Preserving Transform) SVF, 2 cascaded biquad stages
|
||||
- `FilterType` enum: LowPass12, LowPass24, BandPass, HighPass, Notch
|
||||
- Coefficients computed from tan-based transform
|
||||
- `prepare(sampleRate)` resets internal state
|
||||
|
||||
**ADSREnvelope** (`DSP/Envelope.h`)
|
||||
- Linear ramp attack/decay/release
|
||||
- `ADSRStage` enum: Idle, Attack, Decay, Sustain, Release
|
||||
- `prepare(sampleRate)` resets state to Idle
|
||||
|
||||
**SubtractiveVoice** (`DSP/Voice.h`)
|
||||
- Inherits `juce::SynthesiserVoice`
|
||||
- Contains: osc1, osc2, filter, env (amp), filterEnv
|
||||
- `renderNextBlock()` processes sample-by-sample: osc → envelope → filter → drive → output
|
||||
- **KNOWN BUG**: Single `filter` instance processes both L and R sequentially — state gets contaminated between channels, causing crashes when filter env amount > 0. FIX: add `Filter filterR` member, process left through `filter`, right through `filterR`.
|
||||
|
||||
## Plugin Processor
|
||||
|
||||
**ChromaFlockProcessor** (`PluginProcessor.h/.cpp`)
|
||||
- `juce::AudioProcessorValueTreeState apvts` — all parameters
|
||||
- `juce::Synthesiser synth` — 16 voices (`maxVoices = 16`)
|
||||
- `prepareToPlay()`: sets sample rate, calls `updateVoiceParameters()`
|
||||
- `processBlock()`: clears buffer, calls `updateVoiceParameters()`, then `synth.renderNextBlock()`
|
||||
- State save/restore via XML binary copy
|
||||
|
||||
### Parameters (27 total)
|
||||
|
||||
| ID | Name | Range | Default |
|
||||
|---------------------|------------------|--------------------|---------|
|
||||
| osc1Wave | OSC1 Wave | Choice(5) | 1 (Saw) |
|
||||
| osc1Oct | OSC1 Octave | -3..3 (int) | 0 |
|
||||
| osc1Semi | OSC1 Semi | -12..12 (int) | 0 |
|
||||
| osc1Fine | OSC1 Fine | -100..100 | 0 |
|
||||
| osc1Level | OSC1 Level | 0..1 | 0.7 |
|
||||
| osc2Wave | OSC2 Wave | Choice(5) | 2 (Square) |
|
||||
| osc2Oct | OSC2 Octave | -3..3 (int) | -1 |
|
||||
| osc2Semi | OSC2 Semi | -12..12 (int) | 0 |
|
||||
| osc2Fine | OSC2 Fine | -100..100 | 7 |
|
||||
| osc2Level | OSC2 Level | 0..1 | 0.7 |
|
||||
| phaseOffset | Phase Offset | 0..1 | 0.5 |
|
||||
| filterType | Filter Type | Choice(5) | 0 (LP12)|
|
||||
| filterCutoff | Filter Cutoff | 20..20000 (log) | 8000 |
|
||||
| filterRes | Filter Res | 0..1 | 0.3 |
|
||||
| filterEnvAmt | Filter Env Amt | 0..1 | 0 |
|
||||
| keyTrack | Key Tracking | 0..1 | 0.5 |
|
||||
| fEnvAttack | Filter Attack | 0.001..5 (log) | 0.01 |
|
||||
| fEnvDecay | Filter Decay | 0.001..5 (log) | 0.3 |
|
||||
| fEnvSustain | Filter Sustain | 0..1 | 0.5 |
|
||||
| fEnvRelease | Filter Release | 0.001..10 (log) | 0.5 |
|
||||
| envAttack | Attack | 0.001..5 (log) | 0.01 |
|
||||
| envDecay | Decay | 0.001..5 (log) | 0.3 |
|
||||
| envSustain | Sustain | 0..1 | 0.7 |
|
||||
| envRelease | Release | 0.001..10 (log) | 0.5 |
|
||||
| pan | Pan | -1..1 | 0 |
|
||||
| drive | Drive | 1..5 (log) | 1.5 |
|
||||
| masterLevel | Master Level | 0..1 | 0.8 |
|
||||
|
||||
## UI Architecture
|
||||
|
||||
### Class Hierarchy
|
||||
```
|
||||
ChromaFlockEditor (juce::AudioProcessorEditor)
|
||||
└── MainContentComponent (juce::Component) — scaled via AffineTransform::scale()
|
||||
├── Section labels (gold, 13pt bold)
|
||||
├── Combo boxes (osc1/osc2 wave, filter type, UI scale)
|
||||
└── Knobs (112×112, RotaryVerticalDrag, custom LAF)
|
||||
```
|
||||
|
||||
### Scaling System
|
||||
- `MainContentComponent` is painted/laid out at `baseWidth × baseHeight`
|
||||
- `ChromaFlockEditor::resized()` applies `AffineTransform::scale(currentScale)` to content
|
||||
- `setUIScale()` resizes the editor window to `baseWidth*scale × baseHeight*scale`
|
||||
- Scale options: 100%, 125%, 150%, 200%
|
||||
|
||||
### Layout Dimensions (base coordinates, 1× scale)
|
||||
- **baseWidth**: 1660
|
||||
- **baseHeight**: 480 (needs to increase to ~620 for visualizers)
|
||||
- **Section Y origin**: 56px (below header line at y=48)
|
||||
- **Knob size**: 112×112
|
||||
- **Knob spacing**: 10px
|
||||
- **Label height**: 18px
|
||||
- **Combo box height**: 48px
|
||||
|
||||
### Section Layout (from paint/drawSection calls)
|
||||
| Section | X | Y | W | H |
|
||||
|------------|------|-----|------|------|
|
||||
| OSC 1 | 10 | 52 | 498 | 200 |
|
||||
| OSC 2 | 520 | 52 | 620 | 200 |
|
||||
| FILTER | 1152 | 52 | 498 | 200 |
|
||||
| AMP ENV | 10 | 262 | 498 | 150 |
|
||||
| FILTER ENV | 520 | 262 | 498 | 150 |
|
||||
| MASTER | 1030 | 262 | 376 | 150 |
|
||||
|
||||
### Look-and-Feel (KnobLookAndFeel)
|
||||
- Inherits `juce::LookAndFeel_V4`
|
||||
- `drawRotarySlider()` renders:
|
||||
- 3D-ish appearance: dark circle fill, colored outline, gold thumb dot, pointer line
|
||||
- Label drawn at top of bounds (`labelH = 12px` currently)
|
||||
- Colors:
|
||||
- Knob body: `0xff2a2a2a` fill, `0xff555555` outline
|
||||
- Thumb/pointer: `0xffccaa44` / `0xffeebb55`
|
||||
- Section labels: `0xffccaa44` (gold)
|
||||
- Knob labels: `0xff888888`, 10pt font
|
||||
- Text boxes: `0xffcccccc` text on `0xff1a1a1a` background
|
||||
|
||||
### ComboBox LAF (not yet implemented — currently uses default V4 look)
|
||||
- Needs custom 3D LAF similar to knobs (gradient, shadow, highlight)
|
||||
|
||||
### Header
|
||||
- Title: "CHROMAFLOCK" at (20, 8), 22pt bold gold
|
||||
- Subtitle: "SUBTRACTIVE SYNTHESIZER" at (20, 30), 10pt gray
|
||||
- Divider line at y=48, full width, gold
|
||||
|
||||
### UI Scale Controls
|
||||
- Scale label at (1520, 10), 42×20
|
||||
- Scale combo at (1566, 8), 80×24
|
||||
|
||||
## Planned / In-Progress Work
|
||||
|
||||
### Filter Crash Fix (HIGH PRIORITY)
|
||||
**Problem**: Single `Filter filter` in `SubtractiveVoice` is called for both L and R in sequence (lines 87-88 of Voice.h). The filter's internal state (`stage[4]`) is shared/corrupted between channels, causing instability when filter env amount > 0.
|
||||
|
||||
**Fix**: Add `Filter filterR` member. In `renderNextBlock()`:
|
||||
- Process left channel through `filter`
|
||||
- Process right channel through `filterR`
|
||||
- In `startNote()`, call `filter.prepare(sr)` AND `filterR.prepare(sr)`
|
||||
|
||||
### Scope Buffer for Visualizers (HIGH PRIORITY)
|
||||
**Need to add to `ChromaFlockProcessor`**:
|
||||
- `std::array<float, 1024> scopeBuffer` — ring buffer for waveform data
|
||||
- `std::atomic<int> scopeWritePos` — write position index
|
||||
- Write to buffer in `processBlock()` after `synth.renderNextBlock()`, interleaving L+R samples
|
||||
- Add getter method: `const float* getScopeData() const`
|
||||
- Add `juce::juce_dsp` to CMakeLists.txt for FFT
|
||||
|
||||
### Visualizer Components (HIGH PRIORITY)
|
||||
Two new classes to add to `PluginEditor.h`:
|
||||
|
||||
**WaveformDisplay** (juce::Component)
|
||||
- Reads from processor scope buffer
|
||||
- Draws waveform as line graph on dark background
|
||||
- Timer callback at 30fps
|
||||
- Positioned at bottom-left of UI (~190px tall, ~830px wide)
|
||||
|
||||
**SpectrumDisplay** (juce::Component)
|
||||
- Uses `juce::dsp::FFT` to compute spectrum from scope buffer
|
||||
- Draws vertical bars with HSV color gradient (cool→warm based on frequency)
|
||||
- Timer callback at 30fps
|
||||
- Positioned at bottom-right of UI (~190px tall, ~830px wide)
|
||||
|
||||
### Height Expansion
|
||||
- `baseHeight` must increase from 480 to ~620
|
||||
- Bottom sections (AMP ENV, FILTER ENV, MASTER) at y=262 currently end at y=412
|
||||
- Visualizers fill the remaining space (y=412 to y=612)
|
||||
- Editor window resized accordingly
|
||||
|
||||
### ComboBox 3D LAF (MEDIUM PRIORITY)
|
||||
Custom look-and-feel for combo boxes matching the 3D knob aesthetic.
|
||||
|
||||
## Common Pitfalls & Gotchas
|
||||
|
||||
1. **ComboBox items**: Must manually call `addItemList()` BEFORE creating `ComboBoxAttachment` — the attachment alone does NOT populate items.
|
||||
|
||||
2. **AffineTransform::scale**: Use `AffineTransform::scale(float, float)`, NOT `scaled()` — the method name in JUCE is `AffineTransform::scale`.
|
||||
|
||||
3. **Filter state sharing**: Never share a single `Filter` instance between L/R or between voices — each needs its own filter state.
|
||||
|
||||
4. **JUCE modules**: If you use `juce::dsp::FFT` or any `juce_dsp` functionality, you must link `juce::juce_dsp` in CMakeLists.txt AND add `#include <juce_dsp/juce_dsp.h>`.
|
||||
|
||||
5. **Denormals**: `processBlock()` wraps with `juce::ScopedNoDenormals` — always keep this.
|
||||
|
||||
6. **Voice parameter update**: `updateVoiceParameters()` is called every `processBlock()` — this reads all 27 params and pushes to all 16 voices. This is intentional for simplicity but is CPU-expensive.
|
||||
|
||||
7. **Oscillator pan**: The oscillator's `process()` adds directly to output (`*leftOut += ...`), so it's additive between the two oscs in each voice.
|
||||
|
||||
8. **Drive math**: `std::tanh(left * drive) / std::tanh(drive)` — when `drive = 1.0`, division by `tanh(1.0) ≈ 0.762` normalizes. `drive` is clamped to min 1.001 in `setParameters()`.
|
||||
|
||||
## Theme Colors Reference
|
||||
|
||||
| Element | Color | Usage |
|
||||
|------------------|-------------|------------------------------|
|
||||
| Background | `0xff1a1a1a`| Main background |
|
||||
| Knob body | `0xff2a2a2a`| Circle fill |
|
||||
| Knob outline | `0xff555555`| Circle stroke |
|
||||
| Gold accent | `0xffccaa44`| Title, section labels, thumb |
|
||||
| Pointer gold | `0xffeebb55`| Knob pointer line |
|
||||
| Text light | `0xffcccccc`| Parameter values, knob labels|
|
||||
| Text dim | `0xff888888`| Subtitle, knob label text |
|
||||
| Section border | `0xff333333`| Section box outlines |
|
||||
| Text box bg | `0xff1a1a1a`| Slider text boxes |
|
||||
| Popup highlight | `0xffccaa44`| ComboBox dropdown highlight |
|
||||
| Popup text | `0xff1a1a1a`| ComboBox dropdown text on highlight |
|
||||
|
||||
## Testing / Verification
|
||||
|
||||
- Build with `cmake --build build --config Release -j8`
|
||||
- Load VST3 in any DAW (Ableton, Logic, Reaper, etc.)
|
||||
- Test: play notes, sweep filter cutoff, turn up ENV AMT knob (crash test for filter fix)
|
||||
- Test: UI scale switching (100% → 200% → back)
|
||||
- Test: state save/load (preset recall)
|
||||
61
CMakeLists.txt
Normal file
61
CMakeLists.txt
Normal file
|
|
@ -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
|
||||
)
|
||||
60
Makefile
Normal file
60
Makefile
Normal file
|
|
@ -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"
|
||||
209
README.md
Normal file
209
README.md
Normal file
|
|
@ -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
|
||||
47
Source/DSP/Compressor.h
Normal file
47
Source/DSP/Compressor.h
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#pragma once
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
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<float>(sampleRate)));
|
||||
releaseCoeff = std::exp(-1.0f / (releaseMs * 0.001f * static_cast<float>(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;
|
||||
};
|
||||
76
Source/DSP/Delay.h
Normal file
76
Source/DSP/Delay.h
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
class Delay {
|
||||
public:
|
||||
void prepare(double sr) {
|
||||
sampleRate = sr;
|
||||
int maxSamples = static_cast<int>(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<int>(timeMs * 0.001f * sampleRate);
|
||||
int bufMax = static_cast<int>(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<int>(bufferL.size());
|
||||
|
||||
if (pingpong) {
|
||||
int timeR = std::max(1, static_cast<int>(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<int>(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<float> 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;
|
||||
};
|
||||
71
Source/DSP/Distortion.h
Normal file
71
Source/DSP/Distortion.h
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
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;
|
||||
};
|
||||
87
Source/DSP/Envelope.h
Normal file
87
Source/DSP/Envelope.h
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#pragma once
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
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<float>(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<float>(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<float>(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;
|
||||
};
|
||||
110
Source/DSP/Filter.h
Normal file
110
Source/DSP/Filter.h
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#pragma once
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
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<float>(sampleRate * 0.49));
|
||||
resonance = std::clamp(res, 0.0f, 1.0f);
|
||||
filterType = type;
|
||||
|
||||
float g = std::tan(static_cast<float>(pi) * cutoff / static_cast<float>(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;
|
||||
};
|
||||
54
Source/DSP/LFO.h
Normal file
54
Source/DSP/LFO.h
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#pragma once
|
||||
#include <cmath>
|
||||
|
||||
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<float>(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;
|
||||
};
|
||||
80
Source/DSP/Oscillator.h
Normal file
80
Source/DSP/Oscillator.h
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#pragma once
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
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<float>(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;
|
||||
};
|
||||
25
Source/DSP/Reverb.h
Normal file
25
Source/DSP/Reverb.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
#include <juce_audio_basics/juce_audio_basics.h>
|
||||
|
||||
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;
|
||||
};
|
||||
231
Source/DSP/Voice.h
Normal file
231
Source/DSP/Voice.h
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
#pragma once
|
||||
#include "Oscillator.h"
|
||||
#include "Filter.h"
|
||||
#include "Envelope.h"
|
||||
#include "LFO.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
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<SubtractiveSound*>(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<float>(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<float>(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<float>& 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<float>(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<LFODest>(l1Dest);
|
||||
lfo1.setParameters(l1Rate, l1Depth, static_cast<LFOShape>(l1Shape), lfo1Dest);
|
||||
|
||||
lfo2Depth = l2Depth;
|
||||
lfo2Dest = static_cast<LFODest>(l2Dest);
|
||||
lfo2.setParameters(l2Rate, l2Depth, static_cast<LFOShape>(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;
|
||||
};
|
||||
968
Source/PluginEditor.cpp
Normal file
968
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,968 @@
|
|||
#include "PluginEditor.h"
|
||||
#include <BinaryData.h>
|
||||
|
||||
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<int>(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<int>(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<int>& 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<float>(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<float>(i) / static_cast<float>(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<float, ChromaFlockProcessor::fftSize * 2> 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<float>(i) / static_cast<float>(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<float>(numBars);
|
||||
int maxBin = fftSize / 4;
|
||||
|
||||
for (int bar = 0; bar < numBars; ++bar) {
|
||||
float t = static_cast<float>(bar) / static_cast<float>(numBars);
|
||||
float tNext = static_cast<float>(bar + 1) / static_cast<float>(numBars);
|
||||
int binStart = static_cast<int>(std::pow(t, 2.0f) * static_cast<float>(maxBin));
|
||||
int binEnd = static_cast<int>(std::pow(tNext, 2.0f) * static_cast<float>(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<float>(count) : 0.0f;
|
||||
mag = mag / static_cast<float>(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<float>(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<SliderAttachment>& attach,
|
||||
const juce::String& paramId, const juce::String& name) {
|
||||
setupKnob(knob, name);
|
||||
attach = std::make_unique<SliderAttachment>(processorRef.apvts, paramId, knob);
|
||||
};
|
||||
|
||||
auto setupCB = [&](juce::ComboBox& box, std::unique_ptr<ComboBoxAttachment>& attach,
|
||||
const juce::String& paramId, const juce::StringArray& items) {
|
||||
setupCombo(box);
|
||||
box.addItemList(items, 1);
|
||||
attach = std::make_unique<ComboBoxAttachment>(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<int>(presets.size()); ++i)
|
||||
++idx;
|
||||
if (idx < static_cast<int>(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<int>(logoH * 805.0 / 119.0);
|
||||
int logoX = (getWidth() - logoW) / 2;
|
||||
auto logoArea = juce::Rectangle<int>(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<float>(static_cast<float>(x), static_cast<float>(y),
|
||||
static_cast<float>(w), static_cast<float>(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<float>(static_cast<float>(x), static_cast<float>(y),
|
||||
static_cast<float>(w), static_cast<float>(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<int> 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<float>(keysX + (whiteIndexBefore + 1) * localWhiteKeyWidth) - static_cast<float>(blackKeyWidth) * 0.5f;
|
||||
auto blackRect = juce::Rectangle<int>(static_cast<int>(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<int>(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<float>(whiteKeyHeight));
|
||||
|
||||
// Active bend area indicator (shaded region from center to current position)
|
||||
int pbCenter = whiteKeyHeight / 2;
|
||||
float pbNorm = static_cast<float>(currentPitchBend - 64) / 64.0f; // -1..+1
|
||||
int pbY = pbCenter - static_cast<int>(pbNorm * static_cast<float>(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<float>(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<int>(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<float>(keysX + (whiteIndexBefore + 1) * whiteKeyWidth) - static_cast<float>(blackKeyWidth) * 0.5f;
|
||||
auto rect = juce::Rectangle<int>(static_cast<int>(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<float>(pos.y) / static_cast<float>(whiteKeyHeight);
|
||||
pbNorm = std::clamp(pbNorm, -1.0f, 1.0f);
|
||||
currentPitchBend = 64 + static_cast<int>(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<float>(pos.y) / static_cast<float>(whiteKeyHeight);
|
||||
pbNorm = std::clamp(pbNorm, -1.0f, 1.0f);
|
||||
currentPitchBend = 64 + static_cast<int>(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<int>(baseWidth * newScale), static_cast<int>(baseHeight * newScale));
|
||||
}
|
||||
205
Source/PluginEditor.h
Normal file
205
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <juce_gui_basics/juce_gui_basics.h>
|
||||
#include <juce_dsp/juce_dsp.h>
|
||||
#include "PluginProcessor.h"
|
||||
#include <vector>
|
||||
|
||||
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<int>& 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<juce::dsp::FFT>(ChromaFlockProcessor::fftOrder);
|
||||
startTimerHz(30);
|
||||
}
|
||||
void paint(juce::Graphics& g) override;
|
||||
void timerCallback() override { repaint(); }
|
||||
private:
|
||||
ChromaFlockProcessor& processor;
|
||||
std::unique_ptr<juce::dsp::FFT> 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<int> 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<void(float)> 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<SliderAttachment> osc1OctAttach, osc1SemiAttach, osc1FineAttach, osc1LevelAttach;
|
||||
std::unique_ptr<SliderAttachment> osc2OctAttach, osc2SemiAttach, osc2FineAttach, osc2LevelAttach, phaseOffsetAttach;
|
||||
std::unique_ptr<ComboBoxAttachment> osc1WaveAttach, osc2WaveAttach, filterTypeAttach;
|
||||
std::unique_ptr<SliderAttachment> filterCutoffAttach, filterResAttach, filterEnvAmtAttach, keyTrackAttach;
|
||||
std::unique_ptr<SliderAttachment> envAttackAttach, envDecayAttach, envSustainAttach, envReleaseAttach;
|
||||
std::unique_ptr<SliderAttachment> fEnvAttackAttach, fEnvDecayAttach, fEnvSustainAttach, fEnvReleaseAttach;
|
||||
std::unique_ptr<SliderAttachment> panAttach, driveAttach, masterAttach, pbRangeAttach;
|
||||
|
||||
// FX attachments
|
||||
std::unique_ptr<ComboBoxAttachment> distTypeAttach;
|
||||
std::unique_ptr<SliderAttachment> distAmountAttach, distMixAttach;
|
||||
std::unique_ptr<SliderAttachment> compThresholdAttach, compRatioAttach, compAttackAttach;
|
||||
std::unique_ptr<SliderAttachment> compReleaseAttach, compMakeupAttach, compMixAttach;
|
||||
std::unique_ptr<SliderAttachment> autoPanRateAttach, autoPanDepthAttach;
|
||||
|
||||
// LFO attachments
|
||||
std::unique_ptr<SliderAttachment> lfo1RateAttach, lfo1DepthAttach;
|
||||
std::unique_ptr<ComboBoxAttachment> lfo1ShapeAttach, lfo1DestAttach;
|
||||
std::unique_ptr<SliderAttachment> lfo2RateAttach, lfo2DepthAttach;
|
||||
std::unique_ptr<ComboBoxAttachment> lfo2ShapeAttach, lfo2DestAttach;
|
||||
|
||||
// FX2 attachments
|
||||
std::unique_ptr<SliderAttachment> delayTimeAttach, delayFbAttach, delayMixAttach;
|
||||
std::unique_ptr<ComboBoxAttachment> delayPingPongAttach;
|
||||
std::unique_ptr<SliderAttachment> delaySpreadAttach;
|
||||
std::unique_ptr<SliderAttachment> 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)
|
||||
};
|
||||
387
Source/PluginProcessor.cpp
Normal file
387
Source/PluginProcessor.cpp
Normal file
|
|
@ -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<float>(0.0f, 1.0f, 0.01f);
|
||||
|
||||
// OSC 1
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"osc1Wave", 1}, "OSC1 Wave",
|
||||
juce::StringArray{"Sine", "Saw", "Square", "Triangle", "Noise"}, 1));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"osc1Oct", 1}, "OSC1 Octave",
|
||||
juce::NormalisableRange<float>(-3.0f, 3.0f, 1.0f), 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"osc1Semi", 1}, "OSC1 Semi",
|
||||
juce::NormalisableRange<float>(-12.0f, 12.0f, 1.0f), 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"osc1Fine", 1}, "OSC1 Fine",
|
||||
juce::NormalisableRange<float>(-100.0f, 100.0f, 1.0f), 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"osc1Level", 1}, "OSC1 Level", zeroOne, 0.7f));
|
||||
|
||||
// OSC 2
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"osc2Wave", 1}, "OSC2 Wave",
|
||||
juce::StringArray{"Sine", "Saw", "Square", "Triangle", "Noise"}, 2));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"osc2Oct", 1}, "OSC2 Octave",
|
||||
juce::NormalisableRange<float>(-3.0f, 3.0f, 1.0f), -1.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"osc2Semi", 1}, "OSC2 Semi",
|
||||
juce::NormalisableRange<float>(-12.0f, 12.0f, 1.0f), 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"osc2Fine", 1}, "OSC2 Fine",
|
||||
juce::NormalisableRange<float>(-100.0f, 100.0f, 1.0f), 7.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"osc2Level", 1}, "OSC2 Level", zeroOne, 0.7f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"phaseOffset", 1}, "Phase Offset", zeroOne, 0.5f));
|
||||
|
||||
// FILTER
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"filterType", 1}, "Filter Type",
|
||||
juce::StringArray{"LP 12dB", "LP 24dB", "Band Pass", "High Pass", "Notch"}, 0));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"filterCutoff", 1}, "Filter Cutoff",
|
||||
juce::NormalisableRange<float>(25.0f, 1200.0f, 1.0f, 0.25f), 800.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"filterRes", 1}, "Filter Resonance", zeroOne, 0.7f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"filterEnvAmt", 1}, "Filter Env Amount", zeroOne, 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"keyTrack", 1}, "Key Tracking", zeroOne, 0.5f));
|
||||
|
||||
// FILTER ENVELOPE
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"fEnvAttack", 1}, "Filter Attack",
|
||||
juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.3f), 0.01f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"fEnvDecay", 1}, "Filter Decay",
|
||||
juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.3f), 0.3f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"fEnvSustain", 1}, "Filter Sustain", zeroOne, 0.5f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"fEnvRelease", 1}, "Filter Release",
|
||||
juce::NormalisableRange<float>(0.001f, 10.0f, 0.001f, 0.3f), 0.5f));
|
||||
|
||||
// AMP ENVELOPE
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"envAttack", 1}, "Attack",
|
||||
juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.3f), 0.01f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"envDecay", 1}, "Decay",
|
||||
juce::NormalisableRange<float>(0.001f, 5.0f, 0.001f, 0.3f), 0.3f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"envSustain", 1}, "Sustain", zeroOne, 0.7f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"envRelease", 1}, "Release",
|
||||
juce::NormalisableRange<float>(0.001f, 10.0f, 0.001f, 0.3f), 0.5f));
|
||||
|
||||
// GLOBAL
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"pan", 1}, "Pan",
|
||||
juce::NormalisableRange<float>(-1.0f, 1.0f, 0.01f), 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"drive", 1}, "Drive",
|
||||
juce::NormalisableRange<float>(1.0f, 5.0f, 0.01f, 0.5f), 1.5f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"masterLevel", 1}, "Master Level", zeroOne, 0.8f));
|
||||
|
||||
// PITCH BEND
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"pitchBendRange", 1}, "Pitch Bend Range",
|
||||
juce::NormalisableRange<float>(1.0f, 12.0f, 1.0f), 2.0f));
|
||||
|
||||
// DISTORTION
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"distType", 1}, "Dist Type",
|
||||
juce::StringArray{"Soft Clip", "Hard Clip", "Foldback", "Overdrive"}, 0));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"distAmount", 1}, "Dist Amount", zeroOne, 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"distMix", 1}, "Dist Mix", zeroOne, 0.0f));
|
||||
|
||||
// COMPRESSOR
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"compThreshold", 1}, "Comp Threshold",
|
||||
juce::NormalisableRange<float>(-60.0f, 0.0f, 0.1f), -20.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"compRatio", 1}, "Comp Ratio",
|
||||
juce::NormalisableRange<float>(1.0f, 20.0f, 0.1f), 4.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"compAttack", 1}, "Comp Attack",
|
||||
juce::NormalisableRange<float>(0.1f, 100.0f, 0.1f), 10.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"compRelease", 1}, "Comp Release",
|
||||
juce::NormalisableRange<float>(10.0f, 1000.0f, 1.0f), 100.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"compMakeup", 1}, "Comp Makeup",
|
||||
juce::NormalisableRange<float>(0.0f, 24.0f, 0.1f), 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"compMix", 1}, "Comp Mix", zeroOne, 1.0f));
|
||||
|
||||
// AUTO-PAN
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"autoPanRate", 1}, "Pan Rate",
|
||||
juce::NormalisableRange<float>(0.05f, 20.0f, 0.01f, 0.4f), 2.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"autoPanDepth", 1}, "Pan Depth", zeroOne, 0.0f));
|
||||
|
||||
// LFO 1
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"lfo1Rate", 1}, "LFO1 Rate",
|
||||
juce::NormalisableRange<float>(0.05f, 20.0f, 0.01f, 0.4f), 2.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"lfo1Depth", 1}, "LFO1 Depth", zeroOne, 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"lfo1Shape", 1}, "LFO1 Shape",
|
||||
juce::StringArray{"Sine", "Triangle", "Saw", "Square"}, 0));
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"lfo1Dest", 1}, "LFO1 Dest",
|
||||
juce::StringArray{"Filter", "OSC1", "OSC2", "Both OSC"}, 0));
|
||||
|
||||
// LFO 2
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"lfo2Rate", 1}, "LFO2 Rate",
|
||||
juce::NormalisableRange<float>(0.05f, 20.0f, 0.01f, 0.4f), 2.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"lfo2Depth", 1}, "LFO2 Depth", zeroOne, 0.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"lfo2Shape", 1}, "LFO2 Shape",
|
||||
juce::StringArray{"Sine", "Triangle", "Saw", "Square"}, 0));
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"lfo2Dest", 1}, "LFO2 Dest",
|
||||
juce::StringArray{"Filter", "OSC1", "OSC2", "Both OSC"}, 0));
|
||||
|
||||
// DELAY
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"delayTime", 1}, "Delay Time",
|
||||
juce::NormalisableRange<float>(10.0f, 1000.0f, 1.0f, 0.3f), 200.0f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"delayFeedback", 1}, "Delay Feedback", zeroOne, 0.3f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"delayMix", 1}, "Delay Mix", zeroOne, 0.25f));
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"delayPingPong", 1}, "Delay PingPong",
|
||||
juce::StringArray{"Off", "On"}, 0));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"delaySpread", 1}, "Delay Spread",
|
||||
juce::NormalisableRange<float>(0.25f, 0.75f, 0.01f), 0.5f));
|
||||
|
||||
// REVERB
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"reverbSize", 1}, "Reverb Size", zeroOne, 0.5f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"reverbDamping", 1}, "Reverb Damping", zeroOne, 0.5f));
|
||||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
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<SubtractiveVoice*>(synth.getVoice(i))) {
|
||||
float pbRange = getParam("pitchBendRange");
|
||||
voice->setPitchBend(getPitchBend() * pbRange);
|
||||
voice->setParameters(
|
||||
static_cast<Waveform>(static_cast<int>(getParam("osc1Wave"))),
|
||||
static_cast<Waveform>(static_cast<int>(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<FilterType>(static_cast<int>(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<int>(getParam("lfo1Shape")),
|
||||
static_cast<int>(getParam("lfo1Dest")),
|
||||
getParam("lfo2Rate"),
|
||||
getParam("lfo2Depth"),
|
||||
static_cast<int>(getParam("lfo2Shape")),
|
||||
static_cast<int>(getParam("lfo2Dest"))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& 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<DistortionType>(static_cast<int>(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<int>(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<float>(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<float>(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<juce::XmlElement> xml(state.createXml());
|
||||
copyXmlToBinary(*xml, destData);
|
||||
}
|
||||
|
||||
void ChromaFlockProcessor::setStateInformation(const void* data, int sizeInBytes) {
|
||||
std::unique_ptr<juce::XmlElement> 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();
|
||||
}
|
||||
93
Source/PluginProcessor.h
Normal file
93
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <juce_audio_basics/juce_audio_basics.h>
|
||||
#include <juce_dsp/juce_dsp.h>
|
||||
#include "DSP/Voice.h"
|
||||
#include "DSP/Distortion.h"
|
||||
#include "DSP/Compressor.h"
|
||||
#include "DSP/Delay.h"
|
||||
#include "DSP/Reverb.h"
|
||||
#include "PresetManager.h"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
|
||||
class ChromaFlockProcessor : public juce::AudioProcessor {
|
||||
public:
|
||||
ChromaFlockProcessor();
|
||||
~ChromaFlockProcessor() override;
|
||||
|
||||
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() override;
|
||||
void processBlock(juce::AudioBuffer<float>&, 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<float, scopeBufferSize> scopeBuffer{};
|
||||
std::atomic<int> scopeWritePos{0};
|
||||
|
||||
static constexpr int fftOrder = 11;
|
||||
static constexpr int fftSize = 1 << fftOrder;
|
||||
std::array<float, fftSize> fftInput{};
|
||||
std::atomic<int> 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<float> 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<float> rmsLevel{0.0f};
|
||||
std::array<std::atomic<bool>, 128> activeNotes{};
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ChromaFlockProcessor)
|
||||
};
|
||||
676
Source/PresetManager.h
Normal file
676
Source/PresetManager.h
Normal file
|
|
@ -0,0 +1,676 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
struct Preset {
|
||||
juce::String name;
|
||||
juce::String category;
|
||||
std::map<juce::String, float> params;
|
||||
};
|
||||
|
||||
class PresetManager {
|
||||
public:
|
||||
PresetManager() { buildPresets(); }
|
||||
|
||||
const std::vector<Preset>& getPresets() const { return presets; }
|
||||
|
||||
std::vector<juce::String> getCategories() const {
|
||||
std::vector<juce::String> 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<Preset> getPresetsInCategory(const juce::String& cat) const {
|
||||
std::vector<Preset> 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<Preset> presets;
|
||||
|
||||
void add(const juce::String& name, const juce::String& category,
|
||||
std::map<juce::String, float> 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}
|
||||
});
|
||||
}
|
||||
};
|
||||
BIN
cfnew.png
Normal file
BIN
cfnew.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
BIN
chromaflockbg5.png
Normal file
BIN
chromaflockbg5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Loading…
Add table
Add a link
Reference in a new issue