From 2359bfd57960d1ae9338c142e772a7ab5a33a995 Mon Sep 17 00:00:00 2001 From: Armin Date: Mon, 10 Aug 2026 15:36:11 +0200 Subject: [PATCH] fix pitch feedback loop; tune analyzer UI and dry/wet reflection --- Source/HorizontEngine.cpp | 18 ++++++++++------ Source/Knob.cpp | 3 +++ Source/PluginEditor.cpp | 21 +++++++++++-------- Source/PluginProcessor.cpp | 6 ++++-- Source/SpectrumDisplay.cpp | 42 +++++++++++++++++++++++--------------- 5 files changed, 57 insertions(+), 33 deletions(-) diff --git a/Source/HorizontEngine.cpp b/Source/HorizontEngine.cpp index 9b789dc..da055df 100644 --- a/Source/HorizontEngine.cpp +++ b/Source/HorizontEngine.cpp @@ -182,13 +182,11 @@ void HorizontEngine::processSample (float inL, float inR, float& outL, float& ou const float loopInvRt = invRt * (1.0f - 0.7f * fbGain); - const float gMax = std::exp (-6.907755f * baseLen[0] * sizeCur * loopInvRt); + const float shiftOutL = shiftL.process (lastWetL, ratio); + const float shiftOutR = shiftR.process (lastWetR, ratio); - const float fbL = fl + fbGain * 0.5f * (1.0f - gMax) * shiftL.process (lastWetL, ratio); - const float fbR = fr + fbGain * 0.5f * (1.0f - gMax) * shiftR.process (lastWetR, ratio); - - const float dL = diffL2.process (diffL1.process (fbL, diffG), diffG); - const float dR = diffR2.process (diffR1.process (fbR, diffG), diffG); + const float dL = diffL2.process (diffL1.process (fl, diffG), diffG); + const float dR = diffR2.process (diffR1.process (fr, diffG), diffG); float wetL = 0.0f; float wetR = 0.0f; @@ -204,6 +202,14 @@ void HorizontEngine::processSample (float inL, float inR, float& outL, float& ou wetR += 0.25f * combR[(size_t) i].process (dR, lenR, dampC, gainR); } + // recirculate the shifted wet outside the comb bank: loop gain is exactly + // fbGain (< 1) so it cannot self-oscillate; gated off at 0 semitones + if (std::fabs (ratio - 1.0f) > 1e-4f) + { + wetL += fbGain * shiftOutL; + wetR += fbGain * shiftOutR; + } + wetL = dcL.process (wetL); wetR = dcR.process (wetR); diff --git a/Source/Knob.cpp b/Source/Knob.cpp index a1d2970..3c1ab3e 100644 --- a/Source/Knob.cpp +++ b/Source/Knob.cpp @@ -68,7 +68,10 @@ void Knob::resized() const int valueH = juce::roundToInt (16.0f * s); const int maxKnobSize = juce::roundToInt (84.0f * s); + bounds.removeFromTop (juce::roundToInt (15.0f * s)); + auto nameArea = bounds.removeFromTop (nameH); + bounds.removeFromBottom (juce::roundToInt (10.0f * s)); auto valueArea = bounds.removeFromBottom (valueH); nameLabel.setBounds (nameArea); valueLabel.setBounds (valueArea); diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 7fc7371..562b49a 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -66,7 +66,7 @@ namespace #define HORIZONT_BUILD_TIMESTAMP "unknown" #endif - juce::String text ("HORIZONT v" + juce::String (HORIZONT_VERSION)); + juce::String text ("HORIZONT"); text << " · git " << HORIZONT_GIT_REVISION; text << " · build " << HORIZONT_BUILD_TIMESTAMP; return text; @@ -248,7 +248,7 @@ void HorizontAudioProcessorEditor::resized() const int subtitleMarginV = juce::roundToInt (2.0f * s); const int rowsMarginH = juce::roundToInt (16.0f * s); const int rowsMarginV = juce::roundToInt (4.0f * s); - const int gap = juce::roundToInt (10.0f * s); + const int gap = juce::roundToInt (20.0f * s); footerArea = area.removeFromBottom (footerH); footerLabel.setBounds (footerArea.reduced (rowsMarginH, 0)); @@ -263,7 +263,7 @@ void HorizontAudioProcessorEditor::resized() scaleBox.setBounds (scaleArea); presetBox.setBounds (comboArea); - auto titleArea = header.removeFromTop (titleH).reduced (titleMarginH, titleMarginV); + auto titleArea = header.removeFromTop (titleH).reduced (titleMarginH, titleMarginV).translated (0, juce::roundToInt (10.0f * s)); titleLabel.setBounds (titleArea); subtitleLabel.setBounds (header.reduced (subtitleMarginH, subtitleMarginV)); @@ -271,13 +271,18 @@ void HorizontAudioProcessorEditor::resized() subtitleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s)))); auto modules = area.reduced (rowsMarginH, rowsMarginV); + modules.removeFromBottom (juce::roundToInt (15.0f * s)); const int panelH = (modules.getHeight() - 2 * gap) / 3; auto displayPanel = modules.removeFromTop (panelH); display.setBounds (displayPanel); - knobRow1Area = modules.removeFromTop (panelH + gap); - knobRow2Area = modules.removeFromTop (panelH + gap); + modules.removeFromTop (gap); + + knobRow1Area = modules.removeFromTop (panelH); + modules.removeFromTop (gap); + + knobRow2Area = modules.removeFromTop (panelH); layoutKnobRow (knobRow1Area, 0, 5); layoutKnobRow (knobRow2Area, 5, 4); @@ -317,8 +322,8 @@ void HorizontAudioProcessorEditor::paint (juce::Graphics& g) g.setGradientFill (shadowGrad); g.fillRoundedRectangle (shRect, 13.0f); - g.setGradientFill (juce::ColourGradient (juce::Colour (0xFF24323A), { 0.0f, r.getY() }, - juce::Colour (0xFF0B1216), { 0.0f, r.getBottom() }, + g.setGradientFill (juce::ColourGradient (juce::Colour (0x0024323A), { 0.0f, r.getY() }, + juce::Colour (0x000B1216), { 0.0f, r.getBottom() }, false)); g.fillRoundedRectangle (r, 12.0f); @@ -334,7 +339,7 @@ void HorizontAudioProcessorEditor::paint (juce::Graphics& g) g.saveState(); g.reduceClipRegion (r.toNearestInt().reduced (1)); - auto refl = juce::ColourGradient (juce::Colour (0x28FFFFFF), { 0.0f, r.getY() }, + auto refl = juce::ColourGradient (juce::Colour (0x00FFFFFF), { 0.0f, r.getY() }, juce::Colour (0x00FFFFFF), { 0.0f, r.getY() + r.getHeight() * 0.45f }, false); g.setGradientFill (refl); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 22c97a8..32937ee 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -120,6 +120,8 @@ void HorizontAudioProcessor::processBlock (juce::AudioBuffer& buffer, juc const bool haveLeft = buffer.getNumChannels() > 0 && inputChannels > 0; const bool haveRight = buffer.getNumChannels() > 1 && inputChannels > 1; const bool canCaptureWet = wetScratch.size() >= (size_t) numSamples; + const float wetVal = dryWetParam->load(); + const float dryGain = 1.0f - wetVal; for (int s = 0; s < numSamples; ++s) { @@ -136,8 +138,8 @@ void HorizontAudioProcessor::processBlock (juce::AudioBuffer& buffer, juc if (canCaptureWet) { - wetScratch[(size_t) s] = engine.getLastWetL(); - dryScratch[(size_t) s] = inL; + wetScratch[(size_t) s] = engine.getLastWetL() * wetVal; + dryScratch[(size_t) s] = inL * dryGain; } } diff --git a/Source/SpectrumDisplay.cpp b/Source/SpectrumDisplay.cpp index bce5032..456ff55 100644 --- a/Source/SpectrumDisplay.cpp +++ b/Source/SpectrumDisplay.cpp @@ -10,7 +10,7 @@ namespace const juce::Colour wetFillCol (0xE63CE0C8); const juce::Colour wetPeakCol (0x883CE0C8); const juce::Colour dryLineCol (0xCCF0A03C); - const juce::Colour dryFillCol (0x22F0A03C); + const juce::Colour dryFillCol (0x66F0A03C); const juce::Colour dryPeakCol (0x55E89B3A); juce::String formatFreq (float hz) @@ -89,6 +89,19 @@ void SpectrumDisplay::timerCallback() computeLevels (wetBuf.getReadPointer (0), got, level); computeLevels (dryBuf.getReadPointer (0), fftSize, dryLevel); + float wetPeakDb = -200.0f; + float dryPeakDb = -200.0f; + for (int i = 0; i < numBins; ++i) + { + wetPeakDb = jmax (wetPeakDb, level[(size_t) i]); + dryPeakDb = jmax (dryPeakDb, dryLevel[(size_t) i]); + } + + const float dryBoostDb = juce::jlimit (0.0f, 30.0f, wetPeakDb - dryPeakDb - 6.0f); + if (dryBoostDb > 0.0f) + for (int i = 0; i < numBins; ++i) + dryLevel[(size_t) i] += dryBoostDb; + for (int i = 0; i < numBins; ++i) { const int lo = jmax (0, i - 1); @@ -144,7 +157,7 @@ void SpectrumDisplay::paint (juce::Graphics& g) g.fillRoundedRectangle (bounds, 12.0f * s); g.setColour (juce::Colour (0x3A3CE0C8)); - g.drawRoundedRectangle (bounds.expanded (1.0f * s), 13.0f * s, 1.0f * s); + g.drawRoundedRectangle (bounds.expanded (1.0f * s), 16.0f * s, 1.0f * s); g.setColour (juce::Colour (0x38FFFFFF)); g.drawHorizontalLine (juce::roundToInt (bounds.getY() + 1.0f * s), juce::roundToInt (bounds.getX() + 3.0f * s), juce::roundToInt (bounds.getRight() - 3.0f * s)); @@ -155,7 +168,7 @@ void SpectrumDisplay::paint (juce::Graphics& g) g.saveState(); g.reduceClipRegion (bounds.toNearestInt().reduced (juce::roundToInt (1.0f * s))); - auto refl = juce::ColourGradient (juce::Colour (0x28FFFFFF), { 0.0f, bounds.getY() }, + auto refl = juce::ColourGradient (juce::Colour (0x12FFFFFF), { 0.0f, bounds.getY() }, juce::Colour (0x00FFFFFF), { 0.0f, bounds.getY() + bounds.getHeight() * 0.45f }, false); g.setGradientFill (refl); @@ -227,7 +240,7 @@ void SpectrumDisplay::paint (juce::Graphics& g) g.fillPath (wetFill); auto dryGrad = juce::ColourGradient (dryFillCol, { 0.0f, graph.getY() }, - juce::Colour (0x00FFFFFF), { 0.0f, graph.getBottom() }, + juce::Colour (0x12F0A03C), { 0.0f, graph.getBottom() }, false); g.setGradientFill (dryGrad); g.fillPath (dryFill); @@ -235,6 +248,11 @@ void SpectrumDisplay::paint (juce::Graphics& g) g.setColour (wetPeakCol); g.strokePath (makeCurve (graph, peak), juce::PathStrokeType (1.0f * s)); + g.setColour (wetLineCol); + g.strokePath (curve, juce::PathStrokeType (1.4f * s, + juce::PathStrokeType::JointStyle::curved, + juce::PathStrokeType::EndCapStyle::rounded)); + g.setColour (dryLineCol); g.strokePath (dry, juce::PathStrokeType (1.2f * s, juce::PathStrokeType::JointStyle::curved, @@ -242,30 +260,20 @@ void SpectrumDisplay::paint (juce::Graphics& g) g.setColour (dryPeakCol); g.strokePath (makeCurve (graph, dryPeak), juce::PathStrokeType (0.8f * s)); - - g.setColour (wetLineCol); - g.strokePath (curve, juce::PathStrokeType (1.4f * s, - juce::PathStrokeType::JointStyle::curved, - juce::PathStrokeType::EndCapStyle::rounded)); } - g.setColour (juce::Colour (0x66FFFFFF)); - g.setFont (juce::Font (juce::FontOptions (10.0f * s, juce::Font::bold))); - g.drawText ("REVERB SPECTRUM", graph.withTop (graph.getY() - 2.0f * s).withBottom (graph.getY() + 14.0f * s), - juce::Justification::bottomLeft, false); - - const float legendY = graph.getY() + 4.0f * s; + const float legendY = graph.getY() + 24.0f * s; const float legendX = graph.getRight() - 2.0f * s; const auto legendRow = juce::Rectangle (legendX - 84.0f * s, legendY - 9.0f * s, 84.0f * s, 12.0f * s); g.setFont (juce::Font (juce::FontOptions (9.0f * s))); - g.setColour (wetPeakCol); + g.setColour (wetLineCol); g.drawHorizontalLine (juce::roundToInt (legendRow.getY() + 8.0f * s), juce::roundToInt (legendRow.getX()), juce::roundToInt (legendRow.getX() + 11.0f * s)); g.setColour (juce::Colour (0x88FFFFFF)); g.drawText ("WET", legendRow.withX (legendRow.getX() + 14.0f * s).withWidth (28.0f * s), juce::Justification::left, false); - g.setColour (dryPeakCol); + g.setColour (dryLineCol); g.drawHorizontalLine (juce::roundToInt (legendRow.getY() + 8.0f * s), juce::roundToInt (legendRow.getX() + 42.0f * s), juce::roundToInt (legendRow.getX() + 53.0f * s)); g.setColour (juce::Colour (0x88FFFFFF)); g.drawText ("DRY", legendRow.withX (legendRow.getX() + 56.0f * s).withWidth (28.0f * s),