From 56fc80c405a39f9a79aeeadc12cffe49550823ed Mon Sep 17 00:00:00 2001 From: Armin Date: Thu, 13 Aug 2026 01:15:17 +0200 Subject: [PATCH] re-work appegiator patterns --- AGENTS.md | 6 +- Source/DSP/Arpeggiator.h | 114 ++++++++++++++++++++++++++++++++++++- Source/PluginEditor.cpp | 5 +- Source/PluginProcessor.cpp | 5 +- 4 files changed, 122 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ec7252d..5ca6eb0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,7 +118,7 @@ Osc1 + Osc2 → Amplitude Envelope × Velocity → Filter (cutoff modulated by F | 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) | +| arpPattern | Arp Pattern | Choice(17) | 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) | @@ -200,8 +200,8 @@ ChromaFlockEditor (juce::AudioProcessorEditor) ### 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 (1–3, 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. +- 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/Down arpeggiate a minor third + fifth (root, +3, +7) and Up/Down major variants 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". +- `arpDirection` (Up/Down) controls whether octave copies extend above or below the root AND inverts the traversal for sequential patterns (Up/Down/UpDown/DownUp) so Direction Down actually steps descending; Random and As Played ignore the inversion. - `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). diff --git a/Source/DSP/Arpeggiator.h b/Source/DSP/Arpeggiator.h index 8984169..eceb83f 100644 --- a/Source/DSP/Arpeggiator.h +++ b/Source/DSP/Arpeggiator.h @@ -17,6 +17,16 @@ public: Random, AsPlayed, Chord, + UpDownX, + DownUpX, + RandomOnce, + OctaveUp, + OctaveDown, + PinkyUp, + PinkyDown, + UpMajor, + DownMajor, + UpDownMinor, numPatterns }; @@ -137,6 +147,19 @@ public: } private: + // Harmony patterns arpeggiate a third + fifth after each root: minor + // (root, +3, +7) or major (root, +4, +7). Each is a separate step. + bool hasHarmony() const { + return pattern == Up || pattern == Down || pattern == UpDown || pattern == DownUp + || pattern == UpMajor || pattern == DownMajor || pattern == UpDownMinor; + } + + int harmonyThird() const { + switch (pattern) { + case Up: case Down: case UpDownMinor: return 3; + default: return 4; + } + } void buildPool(std::vector& pool) const { bool used[128] = {}; auto addPitch = [&](int pitch, float velocity) { @@ -166,8 +189,9 @@ private: } std::vector buildOrder(const std::vector& pool) { - // Sequential patterns play by ascending pitch regardless of the - // octave direction; As Played keeps the key-press order. + // Sequential patterns play by ascending pitch; Direction Down inverts + // the traversal so the arpeggio descends. As Played keeps the + // key-press order; Random is direction-agnostic. std::vector seq(pool); std::sort(seq.begin(), seq.end(), [](const NotePitch& a, const NotePitch& b) { return a.note < b.note; }); @@ -178,17 +202,43 @@ private: switch (pattern) { case Down: + case DownMajor: for (int i = n - 1; i >= 0; --i) push(i); break; case UpDown: + case UpDownMinor: + for (int i = 0; i < n; ++i) push(i); + for (int i = n - 2; i >= 0; --i) push(i); + break; + case DownUp: + for (int i = n - 1; i >= 0; --i) push(i); + for (int i = 1; i < n; ++i) push(i); + break; + case UpDownX: for (int i = 0; i < n; ++i) push(i); for (int i = n - 2; i >= 1; --i) push(i); break; - case DownUp: + case DownUpX: for (int i = n - 1; i >= 0; --i) push(i); for (int i = 1; i < n - 1; ++i) push(i); break; case Random: + case RandomOnce: { + if (pattern == RandomOnce) { + std::vector keys; + for (const auto& np : seq) keys.push_back(np.note); + if (keys != randomOncePool || stepIndex % n == 0) { + randomOncePool = keys; + randomOnceOrder = seq; + for (int i = n - 1; i > 0; --i) { + int j = static_cast(randomState % static_cast(i + 1)); + std::swap(randomOnceOrder[i], randomOnceOrder[j]); + randomState = randomState * 1664525u + 1013904223u; + } + } + order = randomOnceOrder; + break; + } order = seq; for (int i = n - 1; i > 0; --i) { int j = static_cast(randomState % static_cast(i + 1)); @@ -196,15 +246,71 @@ private: randomState = randomState * 1664525u + 1013904223u; } break; + } + case OctaveUp: + case OctaveDown: + for (const auto& np : seq) { + bool isRoot = true; + for (const auto& other : seq) { + if (other.note == np.note - 12) { isRoot = false; break; } + } + if (!isRoot) continue; + int step = (pattern == OctaveUp ? 12 : -12); + for (int k = 0; k < octaves; ++k) { + int pitch = np.note + step * k; + pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch); + auto it = std::find_if(seq.begin(), seq.end(), + [&](const NotePitch& o) { return o.note == pitch; }); + if (it != seq.end()) + order.push_back(*it); + else + order.push_back({pitch, np.velocity}); + } + } + break; + case PinkyUp: + case PinkyDown: + for (int hi = n - 1, lo = 0; hi >= lo; --hi, ++lo) { + if (pattern == PinkyUp) { + push(hi); + if (lo < hi) push(lo); + } else { + push(lo); + if (lo < hi) push(hi); + } + } + break; case AsPlayed: order = pool; break; case Chord: case Up: + case UpMajor: default: for (int i = 0; i < n; ++i) push(i); break; } + + if (direction == DirectionDown && pattern != Random && pattern != RandomOnce + && pattern != AsPlayed) + std::reverse(order.begin(), order.end()); + + if (hasHarmony()) { + std::vector expanded; + expanded.reserve(order.size() * 3); + int third = harmonyThird(); + for (const auto& np : order) { + expanded.push_back(np); + auto pushClamped = [&](int pitch) { + pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch); + expanded.push_back({pitch, np.velocity}); + }; + pushClamped(np.note + third); + pushClamped(np.note + 7); + } + order.swap(expanded); + } + return order; } @@ -222,4 +328,6 @@ private: bool needsImmediateStep = false; uint32_t randomState = 0x12345678u; + std::vector randomOncePool; + std::vector randomOnceOrder; }; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 092d1c0..c099ea9 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -699,7 +699,10 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) setupCB(arpEnabledBox, arpEnabledAttach, "arpEnabled", {"Off", "On"}); setupCB(arpPatternBox, arpPatternAttach, "arpPattern", - {"Up", "Down", "UpDown", "DownUp", "Random", "As Played", "Chord"}); + {"Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Random", + "As Played", "Chord", "Up & Down X", "Down & Up X", "Random Once", "Octave Up", + "Octave Down", "Pinky Up", "Pinky Down", "Up / Major", "Down / Major", + "Up & Down / Minor"}); setupCB(arpOctavesBox, arpOctavesAttach, "arpOctaves", {"1", "2", "3"}); setupCB(arpDirectionBox, arpDirectionAttach, "arpDirection", {"Up", "Down"}); setupCB(arpRateBox, arpRateAttach, "arpRate", {"1/16", "1/8", "1/4", "1/2", "1", "2"}); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 0c639d8..609ad3b 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -280,7 +280,10 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create juce::StringArray{"Off", "On"}, 0)); layout.add(std::make_unique( juce::ParameterID{"arpPattern", 1}, "Arp Pattern", - juce::StringArray{"Up", "Down", "UpDown", "DownUp", "Random", "As Played", "Chord"}, 0)); + juce::StringArray{"Up / Minor", "Down / Minor", "Up & Down / Major", + "Down & Up / Major", "Random", "As Played", "Chord", "Up & Down X", + "Down & Up X", "Random Once", "Octave Up", "Octave Down", "Pinky Up", + "Pinky Down", "Up / Major", "Down / Major", "Up & Down / Minor"}, 0)); layout.add(std::make_unique( juce::ParameterID{"arpOctaves", 1}, "Arp Octaves", juce::StringArray{"1", "2", "3"}, 1));