add arpeggiator, rename presets, move spectrum analyzer into waveform

This commit is contained in:
Armin 2026-08-13 00:38:10 +02:00
commit 3331b4cc16
9 changed files with 1126 additions and 395 deletions

104
AGENTS.md
View file

@ -86,7 +86,7 @@ Osc1 + Osc2 → Amplitude Envelope × Velocity → Filter (cutoff modulated by F
- `processBlock()`: clears buffer, calls `updateVoiceParameters()`, then `synth.renderNextBlock()`
- State save/restore via XML binary copy
### Parameters (27 total)
### Parameters (32 total)
| ID | Name | Range | Default |
|---------------------|------------------|--------------------|---------|
@ -117,6 +117,11 @@ Osc1 + Osc2 → Amplitude Envelope × Velocity → Filter (cutoff modulated by F
| pan | Pan | -1..1 | 0 |
| drive | Drive | 1..5 (log) | 1.5 |
| masterLevel | Master Level | 0..1 | 0.8 |
| arpEnabled | Arp On | Choice(2) Off/On | 0 (Off) |
| arpPattern | Arp Pattern | Choice(7) | 0 (Up) |
| arpOctaves | Arp Octaves | Choice(3) 1/2/3 | 1 (2) |
| arpDirection | Arp Direction | Choice(2) Up/Down | 0 (Up) |
| arpRate | Arp Rate | Choice(6) 1/16..2 | 1 (1/8) |
## UI Architecture
@ -137,22 +142,31 @@ ChromaFlockEditor (juce::AudioProcessorEditor)
### 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
- **baseHeight**: 1028
- **Header**: y 0..80 (logo + preset buttons + transpose + scale)
- **Knob size**: 112×112 (main rows), 80×80 (FX/LFO rows)
- **Visualizer row**: y=746, h=128 (VU meter, waveform w/ spectrum overlay, arpeggiator)
- **Piano roll**: y=884, h=136
### Section Layout (from paint/drawSection calls)
| Section | X | Y | W | H |
|------------|------|-----|------|------|
| OSC 1 | 10 | 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 |
| Section | X | Y | W | H |
|--------------|------|-----|------|------|
| OSC 1 | 10 | 80 | 498 | 200 |
| OSC 2 | 518 | 80 | 624 | 200 |
| FILTER | 1152 | 80 | 498 | 200 |
| AMP ENV | 10 | 290 | 563 | 160 |
| FILTER ENV | 583 | 290 | 502 | 160 |
| MASTER | 1095 | 290| 555 | 160 |
| DISTORTION | 10 | 460 | 510 | 115 |
| COMPRESSION | 528 | 460 | 468 | 115 |
| PANNING | 1004 | 460 | 268 | 115 |
| LIMITER | 1280 | 460 | 370 | 115 |
| LFO 1 | 10 | 586 | 540 | 150 |
| LFO 2 | 560 | 586 | 530 | 150 |
| DELAY | 1015 | 586 | 375 | 150 |
| REVERB | 1395 | 586 | 255 | 150 |
| ARPEGGIATOR | 860 | 746 | 790 | 128 |
| PIANO ROLL | 10 | 884 | 1640 | 136 |
### Look-and-Feel (KnobLookAndFeel)
- Inherits `juce::LookAndFeel_V4`
@ -166,8 +180,9 @@ ChromaFlockEditor (juce::AudioProcessorEditor)
- 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)
### ComboBox LAF (implemented — `ComboBoxLookAndFeel`)
- Custom 3D LAF (gradient spotlight, drop shadow, gold arrow, highlighted popup items)
- `drawPopupMenuItem` highlights the selected entry persistently and inverts text color on hover
### Header
- Title: "CHROMAFLOCK" at (20, 8), 22pt bold gold
@ -178,47 +193,28 @@ ChromaFlockEditor (juce::AudioProcessorEditor)
- Scale label at (1520, 10), 42×20
- Scale combo at (1566, 8), 80×24
## Planned / In-Progress Work
## Implemented Features
### 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.
### Filter Crash Fix
`SubtractiveVoice` now has separate `filter` (L) and `filterR` (R) instances; `startNote()` prepares both. Resolves the state-sharing crash when filter env amount > 0.
**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)`
### Arpeggiator
- New class `DSP/Arpeggiator.h` — tempo-synced, pure note-logic (no audio). Tracks held notes (press order for the As Played pattern), builds a pitch pool from held notes × octave range (13, clamped to MIDI range), and sequences it per pattern.
- Patterns: Up, Down, UpDown, DownUp, Random, As Played, Chord.
- `arpDirection` (Up/Down) controls whether octave copies extend above or below the root; sequential patterns always order by ascending pitch.
- `arpRate` step divisions (beats): 1/16, 1/8, 1/4, 1/2, 1, 2 — tick period = `rateBeats * 60 / bpm`.
- Routing (`PluginProcessor.cpp::processBlock`): when `arpEnabled`, MIDI note events feed the arp instead of the synth; the arp drives `synth.noteOn/noteOff` (with transpose) on each tick via `arpSampleCount` accumulation. Piano-roll `noteOn/noteOff` route through the same logic. When disabled, `arp.reset()` stops any sounding arp note and notes play normally.
- UI: bottom-right section at (860, 746) titled ARPEGGIATOR with 5 combos (ON, PATTERN, OCTAVES, DIR, RATE).
### 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
### Visualizers
- `WaveformDisplay` (bottom-left, 60×746, 790×128) draws the scope ring buffer waveform AND a semi-transparent `juce::dsp::FFT` spectrum overlay behind it (30 fps). Requires `juce::juce_dsp` linked and `#include <juce_dsp/juce_dsp.h>`.
- Standalone `SpectrumAnalyzer` component was removed.
### 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.
### Preset Level Normalization (`Tools/LevelNormalizer.cpp`)
- Standalone JUCE console tool that renders every preset offline (C4, 8s, 44.1 kHz) and derives a new `masterLevel` for consistent loudness: peak normalized to 0.80, RMS-normalized to 0.15, final value = `min(gainPeak, gainRms) * current`, clamped to 0..1.
- Build target: `cmake --build build --target level_normalizer`; run `./build/level_normalizer --apply` to rewrite the `masterLevel` entries in `Source/PresetManager.h` (reports a count mismatch and aborts if preset order/count ever drifts).
- Applied 2026: all 136 presets re-balanced; `masterLevel` now ranges 0.325..1.0. Sustained/washy presets (heavy reverb/delay) get the largest cuts; quiet transient presets clamp at 1.0.
- Caveat: noise-based presets render with slight run-to-run variance (`juce::Random` reseeds per process), so re-running can shift borderline values by a few percent.
## Common Pitfalls & Gotchas