Add delay invert + flatten LEDs; fix flatten to narrow bounce width instead of shortening tail

This commit is contained in:
Armin 2026-08-13 04:28:58 +02:00
commit cd2caf7821
4 changed files with 67 additions and 18 deletions

View file

@ -13,13 +13,20 @@ public:
writePos = 0; 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<float>(sampleRate); timeSamplesF = timeMs * 0.001f * static_cast<float>(sampleRate);
float bufMax = static_cast<float>(bufferL.size()) - 1.0f; float bufMax = static_cast<float>(bufferL.size()) - 1.0f;
timeSamplesF = std::clamp(timeSamplesF, 1.0f, bufMax); timeSamplesF = std::clamp(timeSamplesF, 1.0f, bufMax);
feedback = std::clamp(fb, 0.0f, 0.95f); feedback = std::clamp(fb, 0.0f, 0.95f);
mix = std::clamp(mx, 0.0f, 1.0f); mix = std::clamp(mx, 0.0f, 1.0f);
pingPong = pingpong; 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<int>(std::round(timeSamplesF));
if (timeSamplesI < 1) timeSamplesI = 1;
} }
void process(float& left, float& right) { void process(float& left, float& right) {
@ -32,27 +39,40 @@ public:
int i1 = (i0 + 1) % bufSize; int i1 = (i0 + 1) % bufSize;
float frac = readPosF - std::floor(readPosF); 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) { if (pingPong) {
// Classic ping-pong: the (summed) input enters one line and each // Mono echo line. The tail length is set by feedback alone; the
// echo is bounced to the opposite channel on the next repeat, so the // ping-pong bounce is applied as alternating L/R panning whose
// taps alternate L/R. Only the cross-coupled echo is fed into the // width is scaled by pingStrength, so flattening narrows the image
// opposite line (never the dry input) so the bounce is audible even // without shortening the number of repeats.
// for a mono source where L == R. float out = bufferL[i0] * (1.0f - frac) + bufferL[i1] * frac;
float in = 0.5f * (left + right); float in = 0.5f * (left + right);
bufferL[writePos] = in + outR * feedback; bufferL[writePos] = in + out * feedback;
bufferR[writePos] = outL * feedback;
pingCounter += 1;
if (pingCounter >= timeSamplesI) {
pingCounter -= timeSamplesI;
pingPhase = -pingPhase;
}
float s = pingStrength * static_cast<float>(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 { } 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; bufferL[writePos] = left + outL * feedback;
bufferR[writePos] = right + outR * feedback; bufferR[writePos] = right + outR * feedback;
left = left * (1.0f - mix) + outL * mix;
right = right * (1.0f - mix) + outR * mix;
} }
writePos = (writePos + 1) % bufSize; writePos = (writePos + 1) % bufSize;
left = left * (1.0f - mix) + outL * mix;
right = right * (1.0f - mix) + outR * mix;
} }
private: private:
@ -60,7 +80,12 @@ private:
int writePos = 0; int writePos = 0;
double sampleRate = 44100; double sampleRate = 44100;
float timeSamplesF = 8820.0f; float timeSamplesF = 8820.0f;
int timeSamplesI = 8820;
float feedback = 0.3f; float feedback = 0.3f;
float mix = 0.25f; float mix = 0.25f;
bool pingPong = false; bool pingPong = false;
int pingSign = 1;
float pingStrength = 1.0f;
int pingCounter = 0;
int pingPhase = 1;
}; };

View file

@ -579,6 +579,8 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
arpEnabledLed(p.apvts, "arpEnabled", "ON", "OFF"), arpEnabledLed(p.apvts, "arpEnabled", "ON", "OFF"),
arpDirectionLed(p.apvts, "arpDirection", "ON", "OFF"), arpDirectionLed(p.apvts, "arpDirection", "ON", "OFF"),
delayPingPongLed(p.apvts, "delayPingPong", "PINGPONG", "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"), reverbOnLed(p.apvts, "reverbOn", "ON", "OFF"),
lfo1SyncLed(p.apvts, "lfo1Sync", "SYNC ON", "SYNC OFF"), lfo1SyncLed(p.apvts, "lfo1Sync", "SYNC ON", "SYNC OFF"),
lfo2SyncLed(p.apvts, "lfo2Sync", "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(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK");
setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX"); setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX");
addAndMakeVisible(delayPingPongLed); addAndMakeVisible(delayPingPongLed);
addAndMakeVisible(delayInvertLed);
addAndMakeVisible(delayFlattenLed);
addAndMakeVisible(reverbOnLed); addAndMakeVisible(reverbOnLed);
setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE"); setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE");
setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP"); setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP");
@ -1312,13 +1316,24 @@ void MainContentComponent::resized() {
// --- FX2 SECTION --- // --- FX2 SECTION ---
int fx2KnobY = lfoKnobY; 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 dx = 962;
int dGap = lfoKnobSize + 8; int dGap = lfoKnobSize + 8;
delayTimeKnob.setBounds(dx, fx2KnobY, lfoKnobSize, lfoKnobSize); delayTimeKnob.setBounds(dx, fx2KnobY, lfoKnobSize, lfoKnobSize);
delayFbKnob.setBounds(dx + dGap, fx2KnobY, lfoKnobSize, lfoKnobSize); delayFbKnob.setBounds(dx + dGap, fx2KnobY, lfoKnobSize, lfoKnobSize);
delayMixKnob.setBounds(dx + dGap * 2, 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); delaySyncLed.setBounds(dx, fx2KnobY + lfoKnobSize + 6, 250, 28);
// Reverb sub-section: SIZE, DAMP, MIX (right edge aligned to 1650) // Reverb sub-section: SIZE, DAMP, MIX (right edge aligned to 1650)

View file

@ -196,7 +196,8 @@ private:
// Arpeggiator controls // Arpeggiator controls
juce::Label arpEnabledLabel, arpPatternLabel, arpOctavesLabel, arpDirectionLabel, arpRateLabel; juce::Label arpEnabledLabel, arpPatternLabel, arpOctavesLabel, arpDirectionLabel, arpRateLabel;
juce::ComboBox arpPatternBox, arpOctavesBox, arpRateBox; juce::ComboBox arpPatternBox, arpOctavesBox, arpRateBox;
LedToggle arpEnabledLed, arpDirectionLed, delayPingPongLed, reverbOnLed; LedToggle arpEnabledLed, arpDirectionLed, delayPingPongLed, reverbOnLed,
delayInvertLed, delayFlattenLed;
// Preset menu // Preset menu
juce::TextButton presetButton{"PRESET"}; juce::TextButton presetButton{"PRESET"};

View file

@ -265,6 +265,12 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create
layout.add(std::make_unique<juce::AudioParameterChoice>( layout.add(std::make_unique<juce::AudioParameterChoice>(
juce::ParameterID{"delayPingPong", 1}, "Delay Ping-Pong", juce::ParameterID{"delayPingPong", 1}, "Delay Ping-Pong",
juce::StringArray{"Off", "On"}, 0)); juce::StringArray{"Off", "On"}, 0));
layout.add(std::make_unique<juce::AudioParameterChoice>(
juce::ParameterID{"delayInvert", 1}, "Delay Invert",
juce::StringArray{"Off", "On"}, 0));
layout.add(std::make_unique<juce::AudioParameterChoice>(
juce::ParameterID{"delayFlatten", 1}, "Delay Flatten",
juce::StringArray{"Off", "On"}, 0));
// REVERB // REVERB
layout.add(std::make_unique<juce::AudioParameterFloat>( layout.add(std::make_unique<juce::AudioParameterFloat>(
@ -526,7 +532,9 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::
delayTimeMs = beatValues[bi] * 60000.0f / static_cast<float>(currentBpm); delayTimeMs = beatValues[bi] * 60000.0f / static_cast<float>(currentBpm);
} }
delay.setParameters(delayTimeMs, getParam("delayFeedback"), getParam("delayMix"), 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; float reverbMix = getParam("reverbOn") > 0.5f ? getParam("reverbMix") : 0.0f;
reverbFX.setParameters(getParam("reverbSize"), getParam("reverbDamping"), 0.8f, reverbMix); reverbFX.setParameters(getParam("reverbSize"), getParam("reverbDamping"), 0.8f, reverbMix);
limiter.setParameters(getParam("limiterThreshold"), getParam("limiterCeiling"), limiter.setParameters(getParam("limiterThreshold"), getParam("limiterCeiling"),