Add ping-pong delay, fix preset save extension, align reverb/scale UI

This commit is contained in:
Armin 2026-07-16 00:49:09 +02:00
commit af87193643
5 changed files with 129 additions and 1110 deletions

View file

@ -13,12 +13,13 @@ public:
writePos = 0; writePos = 0;
} }
void setParameters(float timeMs, float fb, float mx) { void setParameters(float timeMs, float fb, float mx, bool pingpong = 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;
} }
void process(float& left, float& right) { void process(float& left, float& right) {
@ -34,8 +35,15 @@ public:
float outL = bufferL[i0] * (1.0f - frac) + bufferL[i1] * frac; float outL = bufferL[i0] * (1.0f - frac) + bufferL[i1] * frac;
float outR = bufferR[i0] * (1.0f - frac) + bufferR[i1] * frac; float outR = bufferR[i0] * (1.0f - frac) + bufferR[i1] * frac;
if (pingPong) {
// Classic ping-pong: each channel's echo bounces to the opposite
// channel on the next repeat, so the taps alternate L/R.
bufferL[writePos] = left + outR * feedback;
bufferR[writePos] = right + outL * feedback;
} else {
bufferL[writePos] = left + outL * feedback; bufferL[writePos] = left + outL * feedback;
bufferR[writePos] = right + outR * feedback; bufferR[writePos] = right + outR * feedback;
}
writePos = (writePos + 1) % bufSize; writePos = (writePos + 1) % bufSize;
@ -50,4 +58,5 @@ private:
float timeSamplesF = 8820.0f; float timeSamplesF = 8820.0f;
float feedback = 0.3f; float feedback = 0.3f;
float mix = 0.25f; float mix = 0.25f;
bool pingPong = false;
}; };

View file

@ -521,6 +521,7 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox); setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox);
setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK"); setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK");
setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX"); setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX");
setupParam(delayPingPongKnob, delayPingPongAttach, "delayPingPong", "PING");
setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE"); setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE");
setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP"); setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP");
setupParam(reverbMixKnob, reverbMixAttach, "reverbMix", "MIX"); setupParam(reverbMixKnob, reverbMixAttach, "reverbMix", "MIX");
@ -737,13 +738,19 @@ void MainContentComponent::savePresetFile() {
"*.cfl"); "*.cfl");
chooser->launchAsync(juce::FileBrowserComponent::saveMode chooser->launchAsync(juce::FileBrowserComponent::saveMode
| juce::FileBrowserComponent::canSelectFiles, | juce::FileBrowserComponent::canSelectFiles
| juce::FileBrowserComponent::warnAboutOverwriting,
[this, chooser](const juce::FileChooser& c) { [this, chooser](const juce::FileChooser& c) {
std::unique_ptr<juce::FileChooser> deleter(chooser); std::unique_ptr<juce::FileChooser> deleter(chooser);
auto file = c.getResult(); auto file = c.getResult();
if (file == juce::File()) if (file == juce::File())
return; return;
if (file.getFileExtension().toLowerCase() != ".cfl")
// Always force the .cfl extension (macOS native panel does not
// reliably append it) and lowercase the filename.
juce::String name = file.getFileNameWithoutExtension();
if (name.toLowerCase() != name)
file = file.getParentDirectory().getChildFile(name.toLowerCase());
file = file.withFileExtension(".cfl"); file = file.withFileExtension(".cfl");
auto state = processorRef.apvts.copyState(); auto state = processorRef.apvts.copyState();
@ -942,9 +949,9 @@ void MainContentComponent::paint(juce::Graphics& g) {
g.drawText(title, x + 6, y + titlePadY, w - 12, 14, juce::Justification::centred); g.drawText(title, x + 6, y + titlePadY, w - 12, 14, juce::Justification::centred);
}; };
drawSubSection(10, 508, 512, 140, "DISTORTION", 6); drawSubSection(10, 508, 510, 140, "DISTORTION", 6);
drawSubSection(528, 508, 470, 140, "COMPRESSOR", 6); drawSubSection(528, 508, 468, 140, "COMPRESSOR", 6);
drawSubSection(1004, 508, 270, 140, "AUTO-PAN", 6); drawSubSection(1004, 508, 268, 140, "AUTO-PAN", 6);
drawSubSection(1280, 508, 370, 140, "LIMITER", 6); drawSubSection(1280, 508, 370, 140, "LIMITER", 6);
// LFO sub-sections // LFO sub-sections
@ -952,8 +959,8 @@ void MainContentComponent::paint(juce::Graphics& g) {
drawSubSection(560, 658, 530, 150, "LFO 2", 4); drawSubSection(560, 658, 530, 150, "LFO 2", 4);
// FX2 sub-sections // FX2 sub-sections
drawSubSection(1100, 658, 265, 150, "DELAY", 4); drawSubSection(1015, 658, 375, 150, "DELAY", 4);
drawSubSection(1375, 658, 275, 150, "REVERB", 4); drawSubSection(1395, 658, 255, 150, "REVERB", 4);
} }
void MainContentComponent::resized() { void MainContentComponent::resized() {
@ -1076,39 +1083,40 @@ void MainContentComponent::resized() {
int lfoKnobY = 658 + 4 + 14 + 6; // subSectionY + titlePad + titleH + bottomPad int lfoKnobY = 658 + 4 + 14 + 6; // subSectionY + titlePad + titleH + bottomPad
lfoLabel.setBounds(10, lfoY, 1080, fxLabelH); lfoLabel.setBounds(10, lfoY, 1080, fxLabelH);
fx2Label.setBounds(1100, lfoY, 540, fxLabelH); fx2Label.setBounds(1015, lfoY, 635, fxLabelH);
// LFO 1 sub-section (X=15, Y=642, W=530, H=110) // LFO 1 sub-section (X=15, Y=642, W=530, H=110)
lfo1RateKnob.setBounds(22, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo1RateKnob.setBounds(22, lfoKnobY, lfoKnobSize, lfoKnobSize);
lfo1DepthKnob.setBounds(132, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo1DepthKnob.setBounds(132, lfoKnobY, lfoKnobSize, lfoKnobSize);
lfo1ShapeBox.setBounds(242, lfoKnobY + (lfoKnobSize - 28) / 2, 130, 28); lfo1ShapeBox.setBounds(242, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo1DestBox.setBounds(382, lfoKnobY + (lfoKnobSize - 28) / 2, 155, 28); lfo1DestBox.setBounds(357, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo1SyncBox.setBounds(22, lfoKnobY + lfoKnobSize + 6, 200, 28); lfo1SyncBox.setBounds(22, lfoKnobY + lfoKnobSize + 6, 200, 28);
// LFO 2 sub-section (X=560, Y=642, W=530, H=110) // LFO 2 sub-section (X=560, Y=642, W=530, H=110)
lfo2RateKnob.setBounds(568, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo2RateKnob.setBounds(568, lfoKnobY, lfoKnobSize, lfoKnobSize);
lfo2DepthKnob.setBounds(678, lfoKnobY, lfoKnobSize, lfoKnobSize); lfo2DepthKnob.setBounds(678, lfoKnobY, lfoKnobSize, lfoKnobSize);
lfo2ShapeBox.setBounds(788, lfoKnobY + (lfoKnobSize - 28) / 2, 130, 28); lfo2ShapeBox.setBounds(788, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo2DestBox.setBounds(928, lfoKnobY + (lfoKnobSize - 28) / 2, 155, 28); lfo2DestBox.setBounds(903, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo2SyncBox.setBounds(568, lfoKnobY + lfoKnobSize + 6, 200, 28); lfo2SyncBox.setBounds(568, lfoKnobY + lfoKnobSize + 6, 200, 28);
// --- FX2 SECTION --- // --- FX2 SECTION ---
int fx2KnobY = lfoKnobY; int fx2KnobY = lfoKnobY;
// Delay sub-section (X=1100, Y=690, W=265, H=130) // Delay sub-section: TIME, FBACK, MIX, PING + sync combo
// Row 1: TIME, FBACK, MIX int dx = 1020;
delayTimeKnob.setBounds(1110, fx2KnobY, lfoKnobSize, lfoKnobSize); int dGap = lfoKnobSize + 8;
delayFbKnob.setBounds(1198, fx2KnobY, lfoKnobSize, lfoKnobSize); delayTimeKnob.setBounds(dx, fx2KnobY, lfoKnobSize, lfoKnobSize);
delayMixKnob.setBounds(1286, fx2KnobY, lfoKnobSize, lfoKnobSize); delayFbKnob.setBounds(dx + dGap, fx2KnobY, lfoKnobSize, lfoKnobSize);
delaySyncBox.setBounds(1110, fx2KnobY + lfoKnobSize + 6, 240, 28); delayMixKnob.setBounds(dx + dGap * 2, fx2KnobY, lfoKnobSize, lfoKnobSize);
delayPingPongKnob.setBounds(dx + dGap * 3, fx2KnobY, lfoKnobSize, lfoKnobSize);
delaySyncBox.setBounds(dx, fx2KnobY + lfoKnobSize + 6, 250, 28);
// Reverb sub-section (X=1375, Y=690, W=265, H=130) // Reverb sub-section: SIZE, DAMP, MIX (right edge aligned to 1650)
{ {
int rowW = 3 * lfoKnobSize + 2 * 8; int rx = 1402;
int rx = 1650 - rowW;
reverbSizeKnob.setBounds(rx, fx2KnobY, lfoKnobSize, lfoKnobSize); reverbSizeKnob.setBounds(rx, fx2KnobY, lfoKnobSize, lfoKnobSize);
reverbDampKnob.setBounds(rx + (lfoKnobSize + 8), fx2KnobY, lfoKnobSize, lfoKnobSize); reverbDampKnob.setBounds(rx + (lfoKnobSize + 4), fx2KnobY, lfoKnobSize, lfoKnobSize);
reverbMixKnob.setBounds(rx + (lfoKnobSize + 8) * 2, fx2KnobY, lfoKnobSize, lfoKnobSize); reverbMixKnob.setBounds(rx + (lfoKnobSize + 4) * 2, fx2KnobY, lfoKnobSize, lfoKnobSize);
} }
// --- PRESET + SCALE (vertically centered within top section, y: 0..128) --- // --- PRESET + SCALE (vertically centered within top section, y: 0..128) ---
@ -1134,8 +1142,8 @@ void MainContentComponent::resized() {
semitoneTransposeLabel.setBounds(1214, labelY2, 32, labelH2); semitoneTransposeLabel.setBounds(1214, labelY2, 32, labelH2);
semitoneTransposeBox.setBounds(1246, comboY2, 74, comboH2); semitoneTransposeBox.setBounds(1246, comboY2, 74, comboH2);
scaleLabel.setBounds(1520, labelY2, 42, labelH2); scaleLabel.setBounds(1514, labelY2, 42, labelH2);
uiScaleBox.setBounds(1566, comboY2, 80, comboH2); uiScaleBox.setBounds(1560, comboY2, 80, comboH2);
// Visualizer area // Visualizer area
int vizY = 818; int vizY = 818;

View file

@ -160,7 +160,7 @@ private:
juce::ComboBox lfo2ShapeBox, lfo2DestBox, lfo2SyncBox; juce::ComboBox lfo2ShapeBox, lfo2DestBox, lfo2SyncBox;
// FX2 controls // FX2 controls
juce::Slider delayTimeKnob, delayFbKnob, delayMixKnob; juce::Slider delayTimeKnob, delayFbKnob, delayMixKnob, delayPingPongKnob;
juce::ComboBox delaySyncBox; juce::ComboBox delaySyncBox;
juce::Slider reverbSizeKnob, reverbDampKnob, reverbMixKnob; juce::Slider reverbSizeKnob, reverbDampKnob, reverbMixKnob;
@ -202,6 +202,7 @@ private:
// FX2 attachments // FX2 attachments
std::unique_ptr<SliderAttachment> delayTimeAttach, delayFbAttach, delayMixAttach, delayBeatAttach; std::unique_ptr<SliderAttachment> delayTimeAttach, delayFbAttach, delayMixAttach, delayBeatAttach;
std::unique_ptr<ComboBoxAttachment> delaySyncAttach; std::unique_ptr<ComboBoxAttachment> delaySyncAttach;
std::unique_ptr<SliderAttachment> delayPingPongAttach;
std::unique_ptr<SliderAttachment> reverbSizeAttach, reverbDampAttach, reverbMixAttach; std::unique_ptr<SliderAttachment> reverbSizeAttach, reverbDampAttach, reverbMixAttach;
void setupLabel(juce::Label& label, const juce::String& text); void setupLabel(juce::Label& label, const juce::String& text);

View file

@ -257,6 +257,9 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create
juce::NormalisableRange<float>(0.0f, static_cast<float>(numBeats - 1), 1.0f), 4.0f, juce::NormalisableRange<float>(0.0f, static_cast<float>(numBeats - 1), 1.0f), 4.0f,
juce::AudioParameterFloatAttributes() juce::AudioParameterFloatAttributes()
.withStringFromValueFunction(beatValueToText))); .withStringFromValueFunction(beatValueToText)));
layout.add(std::make_unique<juce::AudioParameterChoice>(
juce::ParameterID{"delayPingPong", 1}, "Delay Ping-Pong",
juce::StringArray{"Off", "On"}, 0));
// REVERB // REVERB
layout.add(std::make_unique<juce::AudioParameterFloat>( layout.add(std::make_unique<juce::AudioParameterFloat>(
@ -420,7 +423,8 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::
int bi = juce::jlimit(0, numBeats - 1, juce::roundToInt(getParam("delayBeat"))); int bi = juce::jlimit(0, numBeats - 1, juce::roundToInt(getParam("delayBeat")));
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);
reverbFX.setParameters(getParam("reverbSize"), getParam("reverbDamping"), 0.8f, getParam("reverbMix")); reverbFX.setParameters(getParam("reverbSize"), getParam("reverbDamping"), 0.8f, getParam("reverbMix"));
limiter.setParameters(getParam("limiterThreshold"), getParam("limiterCeiling"), limiter.setParameters(getParam("limiterThreshold"), getParam("limiterCeiling"),
getParam("limiterRelease")); getParam("limiterRelease"));

File diff suppressed because it is too large Load diff