From cd2caf782102ae947476ea82d0092376f10a7ebb Mon Sep 17 00:00:00 2001 From: Armin Date: Thu, 13 Aug 2026 04:28:58 +0200 Subject: [PATCH] Add delay invert + flatten LEDs; fix flatten to narrow bounce width instead of shortening tail --- Source/DSP/Delay.h | 53 ++++++++++++++++++++++++++++---------- Source/PluginEditor.cpp | 19 ++++++++++++-- Source/PluginEditor.h | 3 ++- Source/PluginProcessor.cpp | 10 ++++++- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/Source/DSP/Delay.h b/Source/DSP/Delay.h index b2e8851..77b92b4 100644 --- a/Source/DSP/Delay.h +++ b/Source/DSP/Delay.h @@ -13,13 +13,20 @@ public: writePos = 0; } - void setParameters(float timeMs, float fb, float mx, bool pingpong = false) { + void setParameters(float timeMs, float fb, float mx, bool pingpong = false, + bool invert = false, bool flatten = false) { timeSamplesF = timeMs * 0.001f * static_cast(sampleRate); float bufMax = static_cast(bufferL.size()) - 1.0f; timeSamplesF = std::clamp(timeSamplesF, 1.0f, bufMax); feedback = std::clamp(fb, 0.0f, 0.95f); mix = std::clamp(mx, 0.0f, 1.0f); pingPong = pingpong; + pingSign = invert ? -1 : 1; + // Flatten reduces the bounce *width* by 60% (0.4 of full) without + // touching the echo tail — tail length is governed solely by feedback. + pingStrength = flatten ? 0.4f : 1.0f; + timeSamplesI = static_cast(std::round(timeSamplesF)); + if (timeSamplesI < 1) timeSamplesI = 1; } void process(float& left, float& right) { @@ -32,27 +39,40 @@ public: int i1 = (i0 + 1) % bufSize; float frac = readPosF - std::floor(readPosF); - float outL = bufferL[i0] * (1.0f - frac) + bufferL[i1] * frac; - float outR = bufferR[i0] * (1.0f - frac) + bufferR[i1] * frac; - if (pingPong) { - // Classic ping-pong: the (summed) input enters one line and each - // echo is bounced to the opposite channel on the next repeat, so the - // taps alternate L/R. Only the cross-coupled echo is fed into the - // opposite line (never the dry input) so the bounce is audible even - // for a mono source where L == R. + // Mono echo line. The tail length is set by feedback alone; the + // ping-pong bounce is applied as alternating L/R panning whose + // width is scaled by pingStrength, so flattening narrows the image + // without shortening the number of repeats. + float out = bufferL[i0] * (1.0f - frac) + bufferL[i1] * frac; + float in = 0.5f * (left + right); - bufferL[writePos] = in + outR * feedback; - bufferR[writePos] = outL * feedback; + bufferL[writePos] = in + out * feedback; + + pingCounter += 1; + if (pingCounter >= timeSamplesI) { + pingCounter -= timeSamplesI; + pingPhase = -pingPhase; + } + float s = pingStrength * static_cast(pingPhase * pingSign); + float lPan = 0.5f + 0.5f * s; + float rPan = 0.5f - 0.5f * s; + + float wet = out * mix; + left = left * (1.0f - mix) + wet * lPan; + right = right * (1.0f - mix) + wet * rPan; } else { + float outL = bufferL[i0] * (1.0f - frac) + bufferL[i1] * frac; + float outR = bufferR[i0] * (1.0f - frac) + bufferR[i1] * frac; + bufferL[writePos] = left + outL * feedback; bufferR[writePos] = right + outR * feedback; + + left = left * (1.0f - mix) + outL * mix; + right = right * (1.0f - mix) + outR * mix; } writePos = (writePos + 1) % bufSize; - - left = left * (1.0f - mix) + outL * mix; - right = right * (1.0f - mix) + outR * mix; } private: @@ -60,7 +80,12 @@ private: int writePos = 0; double sampleRate = 44100; float timeSamplesF = 8820.0f; + int timeSamplesI = 8820; float feedback = 0.3f; float mix = 0.25f; bool pingPong = false; + int pingSign = 1; + float pingStrength = 1.0f; + int pingCounter = 0; + int pingPhase = 1; }; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 95aace3..5c7f796 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -579,6 +579,8 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) arpEnabledLed(p.apvts, "arpEnabled", "ON", "OFF"), arpDirectionLed(p.apvts, "arpDirection", "ON", "OFF"), delayPingPongLed(p.apvts, "delayPingPong", "PINGPONG", "OFF"), + delayInvertLed(p.apvts, "delayInvert", "INVERT", "OFF"), + delayFlattenLed(p.apvts, "delayFlatten", "FLATTEN", "OFF"), reverbOnLed(p.apvts, "reverbOn", "ON", "OFF"), lfo1SyncLed(p.apvts, "lfo1Sync", "SYNC ON", "SYNC OFF"), lfo2SyncLed(p.apvts, "lfo2Sync", "SYNC ON", "SYNC OFF"), @@ -689,6 +691,8 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK"); setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX"); addAndMakeVisible(delayPingPongLed); + addAndMakeVisible(delayInvertLed); + addAndMakeVisible(delayFlattenLed); addAndMakeVisible(reverbOnLed); setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE"); setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP"); @@ -1312,13 +1316,24 @@ void MainContentComponent::resized() { // --- FX2 SECTION --- int fx2KnobY = lfoKnobY; - // Delay sub-section: TIME, FBACK, MIX, PING + sync combo + // Delay sub-section: TIME, FBACK, MIX knobs + ping-pong toggle column + sync combo. + // The ping-pong column (PINGPONG / INVERT / FLATTEN) is stacked on the right, + // just below/under the ping-pong LED, with ping-pong itself nudged up a bit. int dx = 962; int dGap = lfoKnobSize + 8; delayTimeKnob.setBounds(dx, fx2KnobY, lfoKnobSize, lfoKnobSize); delayFbKnob.setBounds(dx + dGap, fx2KnobY, lfoKnobSize, lfoKnobSize); delayMixKnob.setBounds(dx + dGap * 2, fx2KnobY, lfoKnobSize, lfoKnobSize); - delayPingPongLed.setBounds(dx + dGap * 3, fx2KnobY, lfoKnobSize, lfoKnobSize); + + int ppX = 1230; + int ppW = 104; + int ppH = 36; + int ppGap = 4; + int ppStep = ppH + ppGap; + delayPingPongLed.setBounds(ppX, fx2KnobY, ppW, ppH); + delayInvertLed.setBounds(ppX, fx2KnobY + ppStep, ppW, ppH); + delayFlattenLed.setBounds(ppX, fx2KnobY + ppStep * 2, ppW, ppH); + delaySyncLed.setBounds(dx, fx2KnobY + lfoKnobSize + 6, 250, 28); // Reverb sub-section: SIZE, DAMP, MIX (right edge aligned to 1650) diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index f4ea464..270c0fb 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -196,7 +196,8 @@ private: // Arpeggiator controls juce::Label arpEnabledLabel, arpPatternLabel, arpOctavesLabel, arpDirectionLabel, arpRateLabel; juce::ComboBox arpPatternBox, arpOctavesBox, arpRateBox; - LedToggle arpEnabledLed, arpDirectionLed, delayPingPongLed, reverbOnLed; + LedToggle arpEnabledLed, arpDirectionLed, delayPingPongLed, reverbOnLed, + delayInvertLed, delayFlattenLed; // Preset menu juce::TextButton presetButton{"PRESET"}; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 1465feb..81bd192 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -265,6 +265,12 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create layout.add(std::make_unique( juce::ParameterID{"delayPingPong", 1}, "Delay Ping-Pong", juce::StringArray{"Off", "On"}, 0)); + layout.add(std::make_unique( + juce::ParameterID{"delayInvert", 1}, "Delay Invert", + juce::StringArray{"Off", "On"}, 0)); + layout.add(std::make_unique( + juce::ParameterID{"delayFlatten", 1}, "Delay Flatten", + juce::StringArray{"Off", "On"}, 0)); // REVERB layout.add(std::make_unique( @@ -526,7 +532,9 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer& buffer, juce:: delayTimeMs = beatValues[bi] * 60000.0f / static_cast(currentBpm); } delay.setParameters(delayTimeMs, getParam("delayFeedback"), getParam("delayMix"), - getParam("delayPingPong") > 0.5f); + getParam("delayPingPong") > 0.5f, + getParam("delayInvert") > 0.5f, + getParam("delayFlatten") > 0.5f); 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"),