fix pitch feedback loop; tune analyzer UI and dry/wet reflection

This commit is contained in:
Armin 2026-08-10 15:36:11 +02:00
commit 2359bfd579
5 changed files with 57 additions and 33 deletions

View file

@ -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 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 dL = diffL2.process (diffL1.process (fl, diffG), diffG);
const float fbR = fr + fbGain * 0.5f * (1.0f - gMax) * shiftR.process (lastWetR, ratio); const float dR = diffR2.process (diffR1.process (fr, diffG), diffG);
const float dL = diffL2.process (diffL1.process (fbL, diffG), diffG);
const float dR = diffR2.process (diffR1.process (fbR, diffG), diffG);
float wetL = 0.0f; float wetL = 0.0f;
float wetR = 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); 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); wetL = dcL.process (wetL);
wetR = dcR.process (wetR); wetR = dcR.process (wetR);

View file

@ -68,7 +68,10 @@ void Knob::resized()
const int valueH = juce::roundToInt (16.0f * s); const int valueH = juce::roundToInt (16.0f * s);
const int maxKnobSize = juce::roundToInt (84.0f * s); const int maxKnobSize = juce::roundToInt (84.0f * s);
bounds.removeFromTop (juce::roundToInt (15.0f * s));
auto nameArea = bounds.removeFromTop (nameH); auto nameArea = bounds.removeFromTop (nameH);
bounds.removeFromBottom (juce::roundToInt (10.0f * s));
auto valueArea = bounds.removeFromBottom (valueH); auto valueArea = bounds.removeFromBottom (valueH);
nameLabel.setBounds (nameArea); nameLabel.setBounds (nameArea);
valueLabel.setBounds (valueArea); valueLabel.setBounds (valueArea);

View file

@ -66,7 +66,7 @@ namespace
#define HORIZONT_BUILD_TIMESTAMP "unknown" #define HORIZONT_BUILD_TIMESTAMP "unknown"
#endif #endif
juce::String text ("HORIZONT v" + juce::String (HORIZONT_VERSION)); juce::String text ("HORIZONT");
text << " · git " << HORIZONT_GIT_REVISION; text << " · git " << HORIZONT_GIT_REVISION;
text << " · build " << HORIZONT_BUILD_TIMESTAMP; text << " · build " << HORIZONT_BUILD_TIMESTAMP;
return text; return text;
@ -248,7 +248,7 @@ void HorizontAudioProcessorEditor::resized()
const int subtitleMarginV = juce::roundToInt (2.0f * s); const int subtitleMarginV = juce::roundToInt (2.0f * s);
const int rowsMarginH = juce::roundToInt (16.0f * s); const int rowsMarginH = juce::roundToInt (16.0f * s);
const int rowsMarginV = juce::roundToInt (4.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); footerArea = area.removeFromBottom (footerH);
footerLabel.setBounds (footerArea.reduced (rowsMarginH, 0)); footerLabel.setBounds (footerArea.reduced (rowsMarginH, 0));
@ -263,7 +263,7 @@ void HorizontAudioProcessorEditor::resized()
scaleBox.setBounds (scaleArea); scaleBox.setBounds (scaleArea);
presetBox.setBounds (comboArea); 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); titleLabel.setBounds (titleArea);
subtitleLabel.setBounds (header.reduced (subtitleMarginH, subtitleMarginV)); subtitleLabel.setBounds (header.reduced (subtitleMarginH, subtitleMarginV));
@ -271,13 +271,18 @@ void HorizontAudioProcessorEditor::resized()
subtitleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s)))); subtitleLabel.setFont (juce::Font (juce::FontOptions (juce::roundToInt (11.0f * s))));
auto modules = area.reduced (rowsMarginH, rowsMarginV); auto modules = area.reduced (rowsMarginH, rowsMarginV);
modules.removeFromBottom (juce::roundToInt (15.0f * s));
const int panelH = (modules.getHeight() - 2 * gap) / 3; const int panelH = (modules.getHeight() - 2 * gap) / 3;
auto displayPanel = modules.removeFromTop (panelH); auto displayPanel = modules.removeFromTop (panelH);
display.setBounds (displayPanel); display.setBounds (displayPanel);
knobRow1Area = modules.removeFromTop (panelH + gap); modules.removeFromTop (gap);
knobRow2Area = modules.removeFromTop (panelH + gap);
knobRow1Area = modules.removeFromTop (panelH);
modules.removeFromTop (gap);
knobRow2Area = modules.removeFromTop (panelH);
layoutKnobRow (knobRow1Area, 0, 5); layoutKnobRow (knobRow1Area, 0, 5);
layoutKnobRow (knobRow2Area, 5, 4); layoutKnobRow (knobRow2Area, 5, 4);
@ -317,8 +322,8 @@ void HorizontAudioProcessorEditor::paint (juce::Graphics& g)
g.setGradientFill (shadowGrad); g.setGradientFill (shadowGrad);
g.fillRoundedRectangle (shRect, 13.0f); g.fillRoundedRectangle (shRect, 13.0f);
g.setGradientFill (juce::ColourGradient (juce::Colour (0xFF24323A), { 0.0f, r.getY() }, g.setGradientFill (juce::ColourGradient (juce::Colour (0x0024323A), { 0.0f, r.getY() },
juce::Colour (0xFF0B1216), { 0.0f, r.getBottom() }, juce::Colour (0x000B1216), { 0.0f, r.getBottom() },
false)); false));
g.fillRoundedRectangle (r, 12.0f); g.fillRoundedRectangle (r, 12.0f);
@ -334,7 +339,7 @@ void HorizontAudioProcessorEditor::paint (juce::Graphics& g)
g.saveState(); g.saveState();
g.reduceClipRegion (r.toNearestInt().reduced (1)); 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 }, juce::Colour (0x00FFFFFF), { 0.0f, r.getY() + r.getHeight() * 0.45f },
false); false);
g.setGradientFill (refl); g.setGradientFill (refl);

View file

@ -120,6 +120,8 @@ void HorizontAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
const bool haveLeft = buffer.getNumChannels() > 0 && inputChannels > 0; const bool haveLeft = buffer.getNumChannels() > 0 && inputChannels > 0;
const bool haveRight = buffer.getNumChannels() > 1 && inputChannels > 1; const bool haveRight = buffer.getNumChannels() > 1 && inputChannels > 1;
const bool canCaptureWet = wetScratch.size() >= (size_t) numSamples; 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) for (int s = 0; s < numSamples; ++s)
{ {
@ -136,8 +138,8 @@ void HorizontAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
if (canCaptureWet) if (canCaptureWet)
{ {
wetScratch[(size_t) s] = engine.getLastWetL(); wetScratch[(size_t) s] = engine.getLastWetL() * wetVal;
dryScratch[(size_t) s] = inL; dryScratch[(size_t) s] = inL * dryGain;
} }
} }

View file

@ -10,7 +10,7 @@ namespace
const juce::Colour wetFillCol (0xE63CE0C8); const juce::Colour wetFillCol (0xE63CE0C8);
const juce::Colour wetPeakCol (0x883CE0C8); const juce::Colour wetPeakCol (0x883CE0C8);
const juce::Colour dryLineCol (0xCCF0A03C); const juce::Colour dryLineCol (0xCCF0A03C);
const juce::Colour dryFillCol (0x22F0A03C); const juce::Colour dryFillCol (0x66F0A03C);
const juce::Colour dryPeakCol (0x55E89B3A); const juce::Colour dryPeakCol (0x55E89B3A);
juce::String formatFreq (float hz) juce::String formatFreq (float hz)
@ -89,6 +89,19 @@ void SpectrumDisplay::timerCallback()
computeLevels (wetBuf.getReadPointer (0), got, level); computeLevels (wetBuf.getReadPointer (0), got, level);
computeLevels (dryBuf.getReadPointer (0), fftSize, dryLevel); 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) for (int i = 0; i < numBins; ++i)
{ {
const int lo = jmax (0, i - 1); const int lo = jmax (0, i - 1);
@ -144,7 +157,7 @@ void SpectrumDisplay::paint (juce::Graphics& g)
g.fillRoundedRectangle (bounds, 12.0f * s); g.fillRoundedRectangle (bounds, 12.0f * s);
g.setColour (juce::Colour (0x3A3CE0C8)); 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.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)); 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.saveState();
g.reduceClipRegion (bounds.toNearestInt().reduced (juce::roundToInt (1.0f * s))); 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 }, juce::Colour (0x00FFFFFF), { 0.0f, bounds.getY() + bounds.getHeight() * 0.45f },
false); false);
g.setGradientFill (refl); g.setGradientFill (refl);
@ -227,7 +240,7 @@ void SpectrumDisplay::paint (juce::Graphics& g)
g.fillPath (wetFill); g.fillPath (wetFill);
auto dryGrad = juce::ColourGradient (dryFillCol, { 0.0f, graph.getY() }, auto dryGrad = juce::ColourGradient (dryFillCol, { 0.0f, graph.getY() },
juce::Colour (0x00FFFFFF), { 0.0f, graph.getBottom() }, juce::Colour (0x12F0A03C), { 0.0f, graph.getBottom() },
false); false);
g.setGradientFill (dryGrad); g.setGradientFill (dryGrad);
g.fillPath (dryFill); g.fillPath (dryFill);
@ -235,6 +248,11 @@ void SpectrumDisplay::paint (juce::Graphics& g)
g.setColour (wetPeakCol); g.setColour (wetPeakCol);
g.strokePath (makeCurve (graph, peak), juce::PathStrokeType (1.0f * s)); 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.setColour (dryLineCol);
g.strokePath (dry, juce::PathStrokeType (1.2f * s, g.strokePath (dry, juce::PathStrokeType (1.2f * s,
juce::PathStrokeType::JointStyle::curved, juce::PathStrokeType::JointStyle::curved,
@ -242,30 +260,20 @@ void SpectrumDisplay::paint (juce::Graphics& g)
g.setColour (dryPeakCol); g.setColour (dryPeakCol);
g.strokePath (makeCurve (graph, dryPeak), juce::PathStrokeType (0.8f * s)); 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)); const float legendY = graph.getY() + 24.0f * s;
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 legendX = graph.getRight() - 2.0f * s; const float legendX = graph.getRight() - 2.0f * s;
const auto legendRow = juce::Rectangle<float> (legendX - 84.0f * s, legendY - 9.0f * s, 84.0f * s, 12.0f * s); const auto legendRow = juce::Rectangle<float> (legendX - 84.0f * s, legendY - 9.0f * s, 84.0f * s, 12.0f * s);
g.setFont (juce::Font (juce::FontOptions (9.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.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.setColour (juce::Colour (0x88FFFFFF));
g.drawText ("WET", legendRow.withX (legendRow.getX() + 14.0f * s).withWidth (28.0f * s), g.drawText ("WET", legendRow.withX (legendRow.getX() + 14.0f * s).withWidth (28.0f * s),
juce::Justification::left, false); 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.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.setColour (juce::Colour (0x88FFFFFF));
g.drawText ("DRY", legendRow.withX (legendRow.getX() + 56.0f * s).withWidth (28.0f * s), g.drawText ("DRY", legendRow.withX (legendRow.getX() + 56.0f * s).withWidth (28.0f * s),