diff --git a/Source/DSP/Arpeggiator.h b/Source/DSP/Arpeggiator.h index 528ad1e..21478c2 100644 --- a/Source/DSP/Arpeggiator.h +++ b/Source/DSP/Arpeggiator.h @@ -210,37 +210,6 @@ private: [](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]); }; @@ -341,16 +310,17 @@ private: int up = anchor + 12; up = up > 127 ? 127 : up; float vel = heldNotes.empty() ? 0.9f : velocities[heldNotes.back()]; + NotePitch rest{restNote, vel}; order.clear(); if (pattern == C3C4A) { order = {{anchor, vel}, {anchor, vel}, {up, vel}, {anchor, vel}, - {up, vel}, {anchor, vel}, {anchor, vel}, {up, vel}}; + {up, vel}, {anchor, vel}, rest, {up, vel}}; } else if (pattern == C3C4B) { - order = {{up, vel}, {anchor, vel}, {anchor, vel}, {anchor, vel}, - {anchor, vel}, {anchor, vel}, {anchor, vel}, {anchor, vel}}; + order = {{up, vel}, {anchor, vel}, rest, {anchor, vel}, + {anchor, vel}, {anchor, vel}, rest, {anchor, vel}}; } else if (pattern == C3C4C) { - order = {{anchor, vel}, {anchor, vel}, {anchor, vel}, {up, vel}, - {anchor, vel}, {anchor, vel}, {anchor, vel}, {anchor, vel}}; + order = {{anchor, vel}, {anchor, vel}, rest, {up, vel}, + rest, {anchor, vel}, rest, {anchor, vel}}; } else if (pattern == C3C4D) { order = {{anchor, vel}, {anchor, vel}, {up, vel}, {anchor, vel}, {anchor, vel}, {up, vel}, {anchor, vel}, {anchor, vel}, diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 326e1c4..1abc22d 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -555,6 +555,7 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) arpEnabledLed(p.apvts, "arpEnabled", "ON", "OFF"), arpDirectionLed(p.apvts, "arpDirection", "ON", "OFF"), delayPingPongLed(p.apvts, "delayPingPong", "ON", "OFF"), + reverbOnLed(p.apvts, "reverbOn", "ON", "OFF"), lfo1SyncLed(p.apvts, "lfo1Sync", "SYNC ON", "SYNC OFF"), lfo2SyncLed(p.apvts, "lfo2Sync", "SYNC ON", "SYNC OFF"), delaySyncLed(p.apvts, "delaySync", "SYNC ON", "SYNC OFF") { @@ -664,6 +665,7 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK"); setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX"); addAndMakeVisible(delayPingPongLed); + addAndMakeVisible(reverbOnLed); setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE"); setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP"); setupParam(reverbMixKnob, reverbMixAttach, "reverbMix", "MIX"); @@ -771,7 +773,7 @@ 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", "C3/C4 1", "C3/C4 2", "C3/C4 3", "C3/C4 4", "C3/C4 5"}); + "Up & Down / Minor", "Stab 1", "Stab 2", "Stab 3", "Stab 4", "Stab 5"}); setupCB(arpOctavesBox, arpOctavesAttach, "arpOctaves", {"1", "2", "3"}); setupCB(arpRateBox, arpRateAttach, "arpRate", {"1/32", "1/16", "1/8", "1/4", "1/2", "1", "2"}); @@ -1047,19 +1049,7 @@ void MainContentComponent::paint(juce::Graphics& g) { g.setOpacity(1.0f); } - // Header section background - { - auto rect = juce::Rectangle(10.0f, 0.0f, 1640.0f, 80.0f); - auto glow = rect.expanded(6.0f); - g.setColour(juce::Colour(0xff222222).withAlpha(0.25f)); - g.fillRoundedRectangle(glow, 12.0f); - juce::ColourGradient fillGrad(juce::Colour(0xBB222222), rect.getCentreX(), rect.getY(), - juce::Colour(0xBB111111), rect.getCentreX(), rect.getBottom(), false); - g.setGradientFill(fillGrad); - g.fillRoundedRectangle(rect, 9.0f); - g.setColour(juce::Colour(0xff333333)); - g.drawRoundedRectangle(rect, 9.0f, 1.0f); - } + // Header section: no boxed background (border removed) // Load logo if (!logoLoaded) { @@ -1116,13 +1106,13 @@ void MainContentComponent::paint(juce::Graphics& g) { g.drawRoundedRectangle(rect, 9.0f, 1.0f); }; - drawSection(10, 80, 498, 200); - drawSection(518, 80, 624, 200); - drawSection(1152, 80, 498, 200); - drawSection(10, 290, 563, 160); - drawSection(583, 290, 502, 160); - drawSection(1095, 290, 555, 160); - drawSection(10, 884, 1640, 136); + drawSection(10, 88, 498, 200); + drawSection(518, 88, 624, 200); + drawSection(1152, 88, 498, 200); + drawSection(10, 298, 563, 160); + drawSection(583, 298, 502, 160); + drawSection(1095, 298, 555, 160); + drawSection(10, 892, 1640, 136); // FX sub-sections auto drawSubSection = [&](int x, int y, int w, int h, const juce::String& title, int titlePadY = 2) { @@ -1142,25 +1132,25 @@ void MainContentComponent::paint(juce::Graphics& g) { g.drawText(title, x + 6, y + titlePadY, w - 12, 14, juce::Justification::centred); }; - drawSubSection(10, 460, 510, 115, "DISTORTION", 6); - drawSubSection(528, 460, 468, 115, "COMPRESSION", 6); - drawSubSection(1004, 460, 268, 115, "PANNING", 6); - drawSubSection(1280, 460, 370, 115, "LIMITER", 6); + drawSubSection(10, 468, 510, 115, "DISTORTION", 6); + drawSubSection(528, 468, 468, 115, "COMPRESSION", 6); + drawSubSection(1004, 468, 268, 115, "PANNING", 6); + drawSubSection(1280, 468, 370, 115, "LIMITER", 6); // LFO sub-sections - drawSubSection(10, 586, 540, 150, "LFO 1", 4); - drawSubSection(560, 586, 530, 150, "LFO 2", 4); + drawSubSection(10, 594, 464, 150, "LFO 1", 4); + drawSubSection(480, 594, 464, 150, "LFO 2", 4); // FX2 sub-sections - drawSubSection(1015, 586, 375, 150, "DELAY", 4); - drawSubSection(1395, 586, 255, 150, "REVERB", 4); + drawSubSection(950, 594, 368, 150, "DELAY", 4); + drawSubSection(1324, 594, 326, 150, "REVERB", 4); // Arpeggiator section (replaces the removed spectrum analyzer) - drawSubSection(860, 746, 790, 128, "ARPEGGIATOR", 6); + drawSubSection(860, 754, 790, 128, "ARPEGGIATOR", 6); } void MainContentComponent::resized() { - auto sectionY = 84; + auto sectionY = 92; auto knobSize = 112; auto knobSpacing = 10; auto labelH = 18; @@ -1191,33 +1181,33 @@ void MainContentComponent::resized() { filterEnvAmtKnob.setBounds(1162 + (knobSize + knobSpacing) * 2, knobYF, knobSize, knobSize); keyTrackKnob.setBounds(1162 + (knobSize + knobSpacing) * 3, knobYF, knobSize, knobSize); - envLabel.setBounds(10, 294, 563, labelH); + envLabel.setBounds(10, 302, 563, labelH); { int rowW = 4 * knobSize + 3 * knobSpacing; int sx = 10 + (563 - rowW) / 2; - int knobYA = 304 + labelH + 6; + int knobYA = 312 + labelH + 6; envAttackKnob.setBounds(sx, knobYA, knobSize, knobSize); envDecayKnob.setBounds(sx + (knobSize + knobSpacing), knobYA, knobSize, knobSize); envSustainKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYA, knobSize, knobSize); envReleaseKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYA, knobSize, knobSize); } - fEnvLabel.setBounds(583, 294, 502, labelH); + fEnvLabel.setBounds(583, 302, 502, labelH); { int rowW = 4 * knobSize + 3 * knobSpacing; int sx = 583 + (502 - rowW) / 2; - int knobYFE = 304 + labelH + 6; + int knobYFE = 312 + labelH + 6; fEnvAttackKnob.setBounds(sx, knobYFE, knobSize, knobSize); fEnvDecayKnob.setBounds(sx + (knobSize + knobSpacing), knobYFE, knobSize, knobSize); fEnvSustainKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYFE, knobSize, knobSize); fEnvReleaseKnob.setBounds(sx + (knobSize + knobSpacing) * 3, knobYFE, knobSize, knobSize); } - globalLabel.setBounds(1095, 294, 555, labelH); + globalLabel.setBounds(1095, 302, 555, labelH); { int rowW = 4 * knobSize + 3 * knobSpacing; int sx = 1095 + (555 - rowW) / 2; - int knobYM = 304 + labelH + 6; + int knobYM = 312 + labelH + 6; panKnob.setBounds(sx, knobYM, knobSize, knobSize); driveKnob.setBounds(sx + (knobSize + knobSpacing), knobYM, knobSize, knobSize); masterKnob.setBounds(sx + (knobSize + knobSpacing) * 2, knobYM, knobSize, knobSize); @@ -1225,7 +1215,7 @@ void MainContentComponent::resized() { } // --- FX SECTION --- - int fxY = 432; + int fxY = 440; int fxKnobSize = 80; int fxLabelH = 16; int fxKnobY = fxY + 54; // sub-section y(476) + titlePad(6) + titleH(14) + bottomPad(6) + 4 @@ -1274,32 +1264,32 @@ void MainContentComponent::resized() { } // --- LFO SECTION --- - int lfoY = 566; + int lfoY = 574; int lfoKnobSize = 80; - int lfoKnobY = 586 + 4 + 14 + 6; // subSectionY + titlePad + titleH + bottomPad + int lfoKnobY = 594 + 4 + 14 + 6; // subSectionY + titlePad + titleH + bottomPad - lfoLabel.setBounds(10, lfoY, 1080, fxLabelH); - fx2Label.setBounds(1015, lfoY, 635, fxLabelH); + lfoLabel.setBounds(10, lfoY, 934, fxLabelH); + fx2Label.setBounds(950, lfoY, 700, fxLabelH); - // LFO 1 sub-section (X=15, Y=642, W=530, H=110) + // LFO 1 sub-section (X=10, Y=642, W=464, H=110) lfo1RateKnob.setBounds(22, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo1DepthKnob.setBounds(132, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo1ShapeBox.setBounds(242, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28); lfo1DestBox.setBounds(357, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28); lfo1SyncLed.setBounds(22, lfoKnobY + lfoKnobSize + 6, 200, 28); - // 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 + (lfoKnobSize - 28) / 2, 105, 28); - lfo2DestBox.setBounds(903, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28); - lfo2SyncLed.setBounds(568, lfoKnobY + lfoKnobSize + 6, 200, 28); + // LFO 2 sub-section (X=480, Y=642, W=464, H=110) + lfo2RateKnob.setBounds(492, lfoKnobY, lfoKnobSize, lfoKnobSize); + lfo2DepthKnob.setBounds(602, lfoKnobY, lfoKnobSize, lfoKnobSize); + lfo2ShapeBox.setBounds(712, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28); + lfo2DestBox.setBounds(827, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28); + lfo2SyncLed.setBounds(492, lfoKnobY + lfoKnobSize + 6, 200, 28); // --- FX2 SECTION --- int fx2KnobY = lfoKnobY; // Delay sub-section: TIME, FBACK, MIX, PING + sync combo - int dx = 1020; + int dx = 962; int dGap = lfoKnobSize + 8; delayTimeKnob.setBounds(dx, fx2KnobY, lfoKnobSize, lfoKnobSize); delayFbKnob.setBounds(dx + dGap, fx2KnobY, lfoKnobSize, lfoKnobSize); @@ -1309,10 +1299,12 @@ void MainContentComponent::resized() { // Reverb sub-section: SIZE, DAMP, MIX (right edge aligned to 1650) { - int rx = 1402; + int rx = 1336; reverbSizeKnob.setBounds(rx, fx2KnobY, lfoKnobSize, lfoKnobSize); reverbDampKnob.setBounds(rx + (lfoKnobSize + 4), fx2KnobY, lfoKnobSize, lfoKnobSize); reverbMixKnob.setBounds(rx + (lfoKnobSize + 4) * 2, fx2KnobY, lfoKnobSize, lfoKnobSize); + // On/off LED on the right, filling the remaining space before the box edge + reverbOnLed.setBounds(rx + (lfoKnobSize + 4) * 3, fx2KnobY, 1650 - (rx + (lfoKnobSize + 4) * 3), lfoKnobSize); } // --- PRESET + SCALE (vertically centered within top section, y: 0..128) --- @@ -1341,11 +1333,11 @@ void MainContentComponent::resized() { midiLabel.setBounds(1354, labelY2, 64, labelH2); midiLed.setBounds(1418, (headerH - 10) / 2, 10, 10); - scaleLabel.setBounds(1514, labelY2, 58, labelH2); - uiScaleBox.setBounds(1574, comboY2, 80, comboH2); + scaleLabel.setBounds(1499, labelY2, 58, labelH2); + uiScaleBox.setBounds(1559, comboY2, 80, comboH2); // Visualizer area - int vizY = 746; + int vizY = 754; int vizH = 128; vuMeter.setBounds(10, vizY, 40, vizH); waveformDisplay.setBounds(60, vizY, 790, vizH); @@ -1384,7 +1376,7 @@ void MainContentComponent::resized() { } // Piano roll - pianoRoll.setBounds(10, 884, 1640, 136); + pianoRoll.setBounds(10, 892, 1640, 136); } // ===== PianoRollComponent ===== diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 3a8b0d1..f4ea464 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -196,7 +196,7 @@ private: // Arpeggiator controls juce::Label arpEnabledLabel, arpPatternLabel, arpOctavesLabel, arpDirectionLabel, arpRateLabel; juce::ComboBox arpPatternBox, arpOctavesBox, arpRateBox; - LedToggle arpEnabledLed, arpDirectionLed, delayPingPongLed; + LedToggle arpEnabledLed, arpDirectionLed, delayPingPongLed, reverbOnLed; // Preset menu juce::TextButton presetButton{"PRESET"}; @@ -274,7 +274,7 @@ private: MainContentComponent content; float currentScale = 1.0f; static constexpr int baseWidth = 1660; - static constexpr int baseHeight = 1028; + static constexpr int baseHeight = 1036; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ChromaFlockEditor) }; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 7bb1eee..1465feb 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -273,6 +273,9 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create juce::ParameterID{"reverbDamping", 1}, "Reverb Damping", zeroOne, 0.5f)); layout.add(std::make_unique( juce::ParameterID{"reverbMix", 1}, "Reverb Mix", zeroOne, 0.2f)); + layout.add(std::make_unique( + juce::ParameterID{"reverbOn", 1}, "Reverb On", + juce::StringArray{"Off", "On"}, 1)); // ARPEGGIATOR layout.add(std::make_unique( @@ -284,7 +287,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create "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", - "C3/C4 1", "C3/C4 2", "C3/C4 3", "C3/C4 4", "C3/C4 5"}, 0)); + "Stab 1", "Stab 2", "Stab 3", "Stab 4", "Stab 5"}, 0)); layout.add(std::make_unique( juce::ParameterID{"arpOctaves", 1}, "Arp Octaves", juce::StringArray{"1", "2", "3"}, 1)); @@ -460,10 +463,12 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer& buffer, juce:: std::vector stopNotes; std::vector playNotes; arp.step(stopNotes, playNotes); + bool isRest = playNotes.empty(); for (int n : stopNotes) - synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, true); + synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, !isRest); for (const auto& np : playNotes) - synth.noteOn(1, juce::jlimit(0, 127, np.note + transpose), np.velocity); + if (np.note >= 0 && np.note <= 127) + synth.noteOn(1, juce::jlimit(0, 127, np.note + transpose), np.velocity); arpSampleCount = 0.0; } @@ -475,10 +480,12 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer& buffer, juce:: std::vector stopNotes; std::vector playNotes; arp.step(stopNotes, playNotes); + bool isRest = playNotes.empty(); for (int n : stopNotes) - synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, true); + synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, !isRest); for (const auto& np : playNotes) - synth.noteOn(1, juce::jlimit(0, 127, np.note + transpose), np.velocity); + if (np.note >= 0 && np.note <= 127) + synth.noteOn(1, juce::jlimit(0, 127, np.note + transpose), np.velocity); } } else { arpSampleCount = 0.0; @@ -520,7 +527,8 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer& buffer, juce:: } delay.setParameters(delayTimeMs, getParam("delayFeedback"), getParam("delayMix"), getParam("delayPingPong") > 0.5f); - reverbFX.setParameters(getParam("reverbSize"), getParam("reverbDamping"), 0.8f, getParam("reverbMix")); + float reverbMix = getParam("reverbOn") > 0.5f ? getParam("reverbMix") : 0.0f; + reverbFX.setParameters(getParam("reverbSize"), getParam("reverbDamping"), 0.8f, reverbMix); limiter.setParameters(getParam("limiterThreshold"), getParam("limiterCeiling"), getParam("limiterRelease")); diff --git a/bg.png b/bg.png index e236f0d..78ce806 100644 Binary files a/bg.png and b/bg.png differ