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;
}
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);
float bufMax = static_cast<float>(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;
}
void process(float& left, float& right) {
@ -34,8 +35,15 @@ public:
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;
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;
bufferR[writePos] = right + outR * feedback;
}
writePos = (writePos + 1) % bufSize;
@ -50,4 +58,5 @@ private:
float timeSamplesF = 8820.0f;
float feedback = 0.3f;
float mix = 0.25f;
bool pingPong = false;
};

View file

@ -521,6 +521,7 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox);
setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK");
setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX");
setupParam(delayPingPongKnob, delayPingPongAttach, "delayPingPong", "PING");
setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE");
setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP");
setupParam(reverbMixKnob, reverbMixAttach, "reverbMix", "MIX");
@ -736,30 +737,36 @@ void MainContentComponent::savePresetFile() {
.getChildFile("chromaflock-" + currentPresetName().replaceCharacter(' ', '-') + ".cfl"),
"*.cfl");
chooser->launchAsync(juce::FileBrowserComponent::saveMode
| juce::FileBrowserComponent::canSelectFiles,
[this, chooser](const juce::FileChooser& c) {
std::unique_ptr<juce::FileChooser> deleter(chooser);
auto file = c.getResult();
if (file == juce::File())
return;
if (file.getFileExtension().toLowerCase() != ".cfl")
chooser->launchAsync(juce::FileBrowserComponent::saveMode
| juce::FileBrowserComponent::canSelectFiles
| juce::FileBrowserComponent::warnAboutOverwriting,
[this, chooser](const juce::FileChooser& c) {
std::unique_ptr<juce::FileChooser> deleter(chooser);
auto file = c.getResult();
if (file == juce::File())
return;
// 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");
auto state = processorRef.apvts.copyState();
std::unique_ptr<juce::XmlElement> presetXml(state.createXml());
auto state = processorRef.apvts.copyState();
std::unique_ptr<juce::XmlElement> presetXml(state.createXml());
std::unique_ptr<juce::XmlElement> root(new juce::XmlElement(kCflTag));
root->setAttribute("version", kCflVersion);
root->setAttribute("generator", "ChromaFlock");
root->addChildElement(presetXml.release());
std::unique_ptr<juce::XmlElement> root(new juce::XmlElement(kCflTag));
root->setAttribute("version", kCflVersion);
root->setAttribute("generator", "ChromaFlock");
root->addChildElement(presetXml.release());
if (!root->writeTo(file))
juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon,
"Save Failed", "Could not write preset file:\n" + file.getFullPathName(), this);
else
patchLCD.setText(file.getFileNameWithoutExtension());
});
if (!root->writeTo(file))
juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon,
"Save Failed", "Could not write preset file:\n" + file.getFullPathName(), this);
else
patchLCD.setText(file.getFileNameWithoutExtension());
});
}
void MainContentComponent::loadPresetFile() {
@ -942,9 +949,9 @@ void MainContentComponent::paint(juce::Graphics& g) {
g.drawText(title, x + 6, y + titlePadY, w - 12, 14, juce::Justification::centred);
};
drawSubSection(10, 508, 512, 140, "DISTORTION", 6);
drawSubSection(528, 508, 470, 140, "COMPRESSOR", 6);
drawSubSection(1004, 508, 270, 140, "AUTO-PAN", 6);
drawSubSection(10, 508, 510, 140, "DISTORTION", 6);
drawSubSection(528, 508, 468, 140, "COMPRESSOR", 6);
drawSubSection(1004, 508, 268, 140, "AUTO-PAN", 6);
drawSubSection(1280, 508, 370, 140, "LIMITER", 6);
// LFO sub-sections
@ -952,8 +959,8 @@ void MainContentComponent::paint(juce::Graphics& g) {
drawSubSection(560, 658, 530, 150, "LFO 2", 4);
// FX2 sub-sections
drawSubSection(1100, 658, 265, 150, "DELAY", 4);
drawSubSection(1375, 658, 275, 150, "REVERB", 4);
drawSubSection(1015, 658, 375, 150, "DELAY", 4);
drawSubSection(1395, 658, 255, 150, "REVERB", 4);
}
void MainContentComponent::resized() {
@ -1076,39 +1083,40 @@ void MainContentComponent::resized() {
int lfoKnobY = 658 + 4 + 14 + 6; // subSectionY + titlePad + titleH + bottomPad
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)
lfo1RateKnob.setBounds(22, lfoKnobY, lfoKnobSize, lfoKnobSize);
lfo1DepthKnob.setBounds(132, lfoKnobY, lfoKnobSize, lfoKnobSize);
lfo1ShapeBox.setBounds(242, lfoKnobY + (lfoKnobSize - 28) / 2, 130, 28);
lfo1DestBox.setBounds(382, lfoKnobY + (lfoKnobSize - 28) / 2, 155, 28);
lfo1ShapeBox.setBounds(242, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo1DestBox.setBounds(357, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo1SyncBox.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, 130, 28);
lfo2DestBox.setBounds(928, lfoKnobY + (lfoKnobSize - 28) / 2, 155, 28);
lfo2ShapeBox.setBounds(788, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo2DestBox.setBounds(903, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo2SyncBox.setBounds(568, lfoKnobY + lfoKnobSize + 6, 200, 28);
// --- FX2 SECTION ---
int fx2KnobY = lfoKnobY;
// Delay sub-section (X=1100, Y=690, W=265, H=130)
// Row 1: TIME, FBACK, MIX
delayTimeKnob.setBounds(1110, fx2KnobY, lfoKnobSize, lfoKnobSize);
delayFbKnob.setBounds(1198, fx2KnobY, lfoKnobSize, lfoKnobSize);
delayMixKnob.setBounds(1286, fx2KnobY, lfoKnobSize, lfoKnobSize);
delaySyncBox.setBounds(1110, fx2KnobY + lfoKnobSize + 6, 240, 28);
// Delay sub-section: TIME, FBACK, MIX, PING + sync combo
int dx = 1020;
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);
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 = 1650 - rowW;
int rx = 1402;
reverbSizeKnob.setBounds(rx, fx2KnobY, lfoKnobSize, lfoKnobSize);
reverbDampKnob.setBounds(rx + (lfoKnobSize + 8), fx2KnobY, lfoKnobSize, lfoKnobSize);
reverbMixKnob.setBounds(rx + (lfoKnobSize + 8) * 2, fx2KnobY, lfoKnobSize, lfoKnobSize);
reverbDampKnob.setBounds(rx + (lfoKnobSize + 4), fx2KnobY, lfoKnobSize, lfoKnobSize);
reverbMixKnob.setBounds(rx + (lfoKnobSize + 4) * 2, fx2KnobY, lfoKnobSize, lfoKnobSize);
}
// --- PRESET + SCALE (vertically centered within top section, y: 0..128) ---
@ -1134,8 +1142,8 @@ void MainContentComponent::resized() {
semitoneTransposeLabel.setBounds(1214, labelY2, 32, labelH2);
semitoneTransposeBox.setBounds(1246, comboY2, 74, comboH2);
scaleLabel.setBounds(1520, labelY2, 42, labelH2);
uiScaleBox.setBounds(1566, comboY2, 80, comboH2);
scaleLabel.setBounds(1514, labelY2, 42, labelH2);
uiScaleBox.setBounds(1560, comboY2, 80, comboH2);
// Visualizer area
int vizY = 818;

View file

@ -160,7 +160,7 @@ private:
juce::ComboBox lfo2ShapeBox, lfo2DestBox, lfo2SyncBox;
// FX2 controls
juce::Slider delayTimeKnob, delayFbKnob, delayMixKnob;
juce::Slider delayTimeKnob, delayFbKnob, delayMixKnob, delayPingPongKnob;
juce::ComboBox delaySyncBox;
juce::Slider reverbSizeKnob, reverbDampKnob, reverbMixKnob;
@ -202,6 +202,7 @@ private:
// FX2 attachments
std::unique_ptr<SliderAttachment> delayTimeAttach, delayFbAttach, delayMixAttach, delayBeatAttach;
std::unique_ptr<ComboBoxAttachment> delaySyncAttach;
std::unique_ptr<SliderAttachment> delayPingPongAttach;
std::unique_ptr<SliderAttachment> reverbSizeAttach, reverbDampAttach, reverbMixAttach;
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::AudioParameterFloatAttributes()
.withStringFromValueFunction(beatValueToText)));
layout.add(std::make_unique<juce::AudioParameterChoice>(
juce::ParameterID{"delayPingPong", 1}, "Delay Ping-Pong",
juce::StringArray{"Off", "On"}, 0));
// REVERB
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")));
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"));
limiter.setParameters(getParam("limiterThreshold"), getParam("limiterCeiling"),
getParam("limiterRelease"));

File diff suppressed because it is too large Load diff