- **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`.
`SubtractiveVoice` now has separate `filter` (L) and `filterR` (R) instances; `startNote()` prepares both. Resolves the state-sharing crash when filter env amount > 0.
- 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 (1–3, clamped to MIDI range), and sequences it per pattern.
- Patterns: Up, Down, UpDown, DownUp, Random, As Played, Chord, Up&Down X, Down&Up X, Random Once, Octave Up, Octave Down, Pinky Up, Pinky Down. X variants are endpoint-exclusive bounces; Random Once shuffles per cycle (no repeat until full pass); Octave Up/Down play each root followed by its ±12 copies spanning the selected OCTAVES count; Pinky Up/Down alternate extremes low/high. Up arpeggiate a minor third + fifth (root, +3, +7) and the Up major variant a major third + fifth (root, +4, +7); UpDown/DownUp arpeggiate major (root, +4, +7) plus an UpDown minor variant (root, +3, +7), each as a separate step. Dropdown labels: "Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Up / Major", "Down / Major", "Up & Down / Minor". "Down" patterns always descend from each held note through the chord tones below it (root, fifth below, third below, next-octave root — e.g. C3, G2, E2, C2 for major), never playing above the held note; any harmony pattern does the same when DIR is set to Down. C3/C4 1/2/3 anchor to the most recently held key and play fixed 8-step loops alternating it and its octave above (3-3-4-3-4-3-3-4, 4-3-3-3-3-3-3-3, 3-3-3-4-3-3-3-3).
-`arpDirection` (Up/Down) controls whether octave copies extend above or below the root AND sorts sequential patterns descending so Direction Down actually steps downward; Random and As Played ignore the inversion. Harmony patterns with Down direction descend through chord tones below each root (see above).
- 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).
-`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.
- 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.
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()`.