From 7c444ad849c90d676919ea94c595ab27e1028f05 Mon Sep 17 00:00:00 2001 From: Armin Date: Thu, 13 Aug 2026 01:39:02 +0200 Subject: [PATCH] rework arpeggiator patterns --- AGENTS.md | 10 +++--- Source/DSP/Arpeggiator.h | 66 ++++++++++++++++++++++++++++++++++---- Source/PluginEditor.cpp | 4 +-- Source/PluginProcessor.cpp | 9 +++--- 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5ca6eb0..844e935 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,10 +118,10 @@ 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(17) | 0 (Up) | +| arpPattern | Arp Pattern | Choice(20) | 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) | +| arpRate | Arp Rate | Choice(7) 1/32..2 | 1 (1/16)| ## UI Architecture @@ -200,9 +200,9 @@ 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, 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`. +- 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). +- `arpRate` step divisions (beats): 1/32, 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 eceb83f..6d31edb 100644 --- a/Source/DSP/Arpeggiator.h +++ b/Source/DSP/Arpeggiator.h @@ -27,6 +27,9 @@ public: UpMajor, DownMajor, UpDownMinor, + C3C4A, + C3C4B, + C3C4C, numPatterns }; @@ -189,14 +192,42 @@ private: } std::vector buildOrder(const std::vector& pool) { - // 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; }); int n = static_cast(seq.size()); + // "Down" patterns (and any harmony pattern with Direction Down) always + // descend from each held note through the chord tones of the octave + // below it: root, fifth below, third below, then the next octave's + // root. Built from the held notes directly so nothing ever plays + // above them. + bool wantDownFigure = hasHarmony() + && (direction == DirectionDown || pattern == Down || pattern == DownMajor); + if (wantDownFigure) { + std::vector down; + int thirdBelow = 12 - harmonyThird(); + std::vector tops(heldNotes.begin(), heldNotes.end()); + std::sort(tops.begin(), tops.end(), [](int a, int b) { return a > b; }); + for (int root : tops) { + if (std::find(tops.begin(), tops.end(), root + 12) != tops.end()) + continue; + float vel = velocities[root]; + for (int k = 0; k < octaves; ++k) { + auto pushClamped = [&](int pitch) { + pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch); + down.push_back({pitch, vel}); + }; + pushClamped(root - 12 * k); + if (k < octaves - 1) { + pushClamped(root - 12 * k - 5); + pushClamped(root - 12 * k - thirdBelow); + } + } + } + return down; + } + std::vector order; auto push = [&](int i) { order.push_back(seq[i]); }; @@ -283,6 +314,26 @@ private: case AsPlayed: order = pool; break; + case C3C4A: + case C3C4B: + case C3C4C: { + int anchor = heldNotes.empty() ? 48 : heldNotes.back(); + int up = anchor + 12; + up = up > 127 ? 127 : up; + float vel = heldNotes.empty() ? 0.9f : velocities[heldNotes.back()]; + order.clear(); + if (pattern == C3C4A) { + order = {{anchor, vel}, {anchor, vel}, {up, vel}, {anchor, vel}, + {up, vel}, {anchor, vel}, {anchor, vel}, {up, vel}}; + } else if (pattern == C3C4B) { + order = {{up, vel}, {anchor, vel}, {anchor, vel}, {anchor, vel}, + {anchor, vel}, {anchor, vel}, {anchor, vel}, {anchor, vel}}; + } else { + order = {{anchor, vel}, {anchor, vel}, {anchor, vel}, {up, vel}, + {anchor, vel}, {anchor, vel}, {anchor, vel}, {anchor, vel}}; + } + break; + } case Chord: case Up: case UpMajor: @@ -291,10 +342,6 @@ private: 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); @@ -311,6 +358,11 @@ private: order.swap(expanded); } + if (direction == DirectionDown && pattern != Random && pattern != RandomOnce + && pattern != AsPlayed && pattern != C3C4A && pattern != C3C4B && pattern != C3C4C) + std::sort(order.begin(), order.end(), + [](const NotePitch& a, const NotePitch& b) { return a.note > b.note; }); + return order; } diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index c099ea9..4b16eab 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -702,10 +702,10 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) {"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"}); + "Up & Down / Minor", "C3/C4 1", "C3/C4 2", "C3/C4 3"}); 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"}); + setupCB(arpRateBox, arpRateAttach, "arpRate", {"1/32", "1/16", "1/8", "1/4", "1/2", "1", "2"}); setupCombo(uiScaleBox); uiScaleBox.addItem("100%", 1); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 609ad3b..3d92b7e 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -283,7 +283,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create 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)); + "Pinky Down", "Up / Major", "Down / Major", "Up & Down / Minor", + "C3/C4 1", "C3/C4 2", "C3/C4 3"}, 0)); layout.add(std::make_unique( juce::ParameterID{"arpOctaves", 1}, "Arp Octaves", juce::StringArray{"1", "2", "3"}, 1)); @@ -292,7 +293,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create juce::StringArray{"Up", "Down"}, 0)); layout.add(std::make_unique( juce::ParameterID{"arpRate", 1}, "Arp Rate", - juce::StringArray{"1/16", "1/8", "1/4", "1/2", "1", "2"}, 1)); + juce::StringArray{"1/32", "1/16", "1/8", "1/4", "1/2", "1", "2"}, 1)); return layout; } @@ -413,8 +414,8 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer& buffer, juce:: bool arpOn = getRaw("arpEnabled") > 0.5f; if (arpOn) { - static const double arpRateBeats[] = {0.25, 0.5, 1.0, 2.0, 4.0, 8.0}; - int rateIdx = juce::jlimit(0, 5, juce::roundToInt(getRaw("arpRate"))); + static const double arpRateBeats[] = {0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0}; + int rateIdx = juce::jlimit(0, 6, juce::roundToInt(getRaw("arpRate"))); arp.setParameters(true, juce::roundToInt(getRaw("arpPattern")), juce::roundToInt(getRaw("arpOctaves")) + 1,