Logarithmic Slider for Frequencies - IIR HPF

Hello Everyone!

I’ve got my IIR high pass filter working on my PluginProcessor side, but on the GUI / PluginEdtior side, the frequency knob shows 20.0 Hz to 20480.0 Hz on a linear scale, even though I am using “hpfHzSlider.setSkewFactorFromMidPoint(1024.0)” to try to make the scale logarithmic. I don’t understand why this wouldn’t work.

Here is my code, from my PluginEditor.cpp :
"
hpfHzSlider.setSliderStyle(Slider::SliderStyle::RotaryHorizontalVerticalDrag);
hpfHzSlider.setTextBoxStyle(Slider::NoTextBox, false, 62, 38);
hpfHzSlider.hideTextBox(true);
hpfHzSlider.setValue(20.0);
hpfHzSlider.setSkewFactorFromMidPoint(1024.0);
hpfHzSlider.setRange(20.0, 20480.0);
hpfHzSlider.setPopupDisplayEnabled(true, true, this);
hpfHzSlider.setTextValueSuffix(" Hz");
hpfHzSlider.addListener(this);
addAndMakeVisible(hpfHzSlider);
hpfHzSlideAttch = std::make_uniqueAudioProcessorValueTreeState::SliderAttachment(processor.apvts,“hpfHz”, hpfHzSlider);

"

Thanks for your help!
Andy

i can’t answer your question due to my low juce skill level, but i’d like to point out that a skew factor is not a logarithmic curve, but just a linear curve split into 2 linear curves that meet at some value. it can give the impression of unlinear curves but maybe for a parameter that goes through such a huge range of values it might be better to make it truly log

1 Like

Cool! Great! I’m still learning stuff too. How do I set the values in the GUI to a log scale?

Hi @baintonaj, to make your code more readable, please indent every line with 4 spaces, or surround the whole code with three backticks before and after, like this:

```
void your_code_here ()
{
}
``` 

The same is obtained by selecting the code in your post while editing, then pressing the button that has this symbol on it: </>

1 Like

I am not entirely sure why your code above is not working. However I want to propose another solution to you, as I see you tapping into the same trap I was in half a year ago:
When you attach your Slider to the AudioParameter, it will automatically use the range, skew, and initial value from it. Furthermore, they can start conflicting when your GUI control uses skew, but your AudioParam doesn’t. So in your AudioProcessorValueTreeState initializer list you have to use

std::make_unique<AudioParameterFloat>(
          "hpfHz", "Highpass Filter Cutoff",
          NormalisableRange<float>(HPF_MIN, HPF_MAX, 0.f, HPF_SKEW_FACTOR),
          HPF_INITIAL_VALUE),

In your editor.cpp you now simply have to do:

hpfHzSlideAttch.reset(new SliderAttachment(processor.apvts, "hpfHz", hpfHzSlider));
//don't need these anymore:
//hpfHzSlider.setValue(HPF_INITIAL_VALUE);
//hpfHzSlider.setSkewFactorFromMidPoint(HPF_MID);
//hpfHzSlider.setRange(HPF_MIN, HPF_MAX);

Now one problem here is that you cannot set the skew from a midpoint anymore, which is convenient. To get the skewvalues, I used the same method as you do now (although working), and the immediately called getSkewFactor() to write out the value.

Anyway I’m using similar ranges for my filters as you (80-18kHz) and a skew factor of

#define HPF_SKEW_FACTOR 0.25f

works very well for me.

I might be mistaken, but I think this is not true (in JUCE at least). The documentation of setSkewFactor() states that it “…will logarithmically spread the values across the length of the slider” and in juce_NormalisableRange.h line 139 the calculation Slider -> [0,1] goes

        return (static_cast<ValueType> (1) + std::pow (std::abs (distanceFromMiddle), skew)
                                           * (distanceFromMiddle < ValueType() ? static_cast<ValueType> (-1)
                                                                               : static_cast<ValueType> (1)))
               / static_cast<ValueType> (2);

so there is at least a powerfunction involved.

2 Likes

oh nice. didn’t expect that. still op’s question, why it doesn’t work in their code remains unanswered… i wish i could help but i didn’t experiment with unlinear sliders in juce a lot yet.

edit: just saw you wrote another reply. nice :slight_smile:

Instead of using setSkew you can achieve better mapping by using:
NormalisableRange::ValueRemapFunction

3 Likes

Google for skew: https://en.wikipedia.org/wiki/Skewness#Introduction

And here is a generic logarithmic range I use for my frequency sliders (uses the remap function @ttg mentioned):

Feel free to use it

2 Likes
static inline NormalisableRange<float> getNormalisableRangeExp(float min, float max)
{
    jassert(min > 0.0f);
    jassert(max > 0.0f);
    jassert(min < max);
    
    float logmin = std::log(min);
    float logmax = std::log(max);
    float logrange = (logmax - logmin);
    
    jassert(logrange > 0.0f);
    
    return NormalisableRange<float>(min, max,
                                    [logmin,logrange](float start, float end, float normalized)
                                    {
                                        normalized = std::max(0.0f, std::min(1.0f, normalized));
                                        float value = exp((normalized * logrange) + logmin);
                                        return std::max(start, std::min(end, value));
                                    },
                                    [logmin,logrange](float start, float end, float value)
                                    {
                                        value = std::max(start, std::min(end, value));
                                        float logvalue = std::log(value);
                                        return (logvalue - logmin) / logrange;
                                    },
                                    [](float start, float end, float value)
                                    {
                                        return std::max(start, std::min(end, value));
                                    });
}

class AudioParameterFloatExp : public AudioParameterFloat
{
public:
    AudioParameterFloatExp( const String    &parameterID,
                            const String    &parameterName,
                            float           minValue,
                            float           maxValue,
                            float           defaultValue) :
    AudioParameterFloat(parameterID, parameterName, getNormalisableRangeExp(minValue, maxValue), defaultValue)
    {
        
    }
};
```
1 Like

Careful with capturing by reference in that scope :slight_smile:
The local variables will be gone

True. Fixed now.

For frequency controls, where the pitch midpoint is sqrt(min * max), the corresponding skew factor is 1 / log2(1 + sqrt(max / min)), so I get the normalisable range with

NormalisableRange<float> frequencyRange(float min, float max, float interval)
{
    return { min, max, interval, 1.f / std::log2(1.f + std::sqrt(max / min)) };
}
1 Like

Cool! Where would I put this code in my program? I’m still new to the structure of everything. Thanks!

Gotcha. So with this, is 1 kHz in the GUI still on 1 hKz in the processor? Or does it change the distribution to fit the log curve?

Cool! Where would I put this in the program? This would be something I called to do the remaping?

The AudioParameterFloat takes a NormalisableRange, which is in charge for the remapping.

If you use the AudioProcessorValueTreeState::SliderAttachment, this range is automatically propagated to the slider, no adjustments necessary.

It gives also the benefit of consistent display and behaviour between host and your editor.

1 Like

1 kHz will be 1 kHz everywhere. What the skew changes is the relation with the internal 0…1 value stored by AudioParameterFloat. The usual (most “automatic”) arrangement is to have an AudioProcessorValueTreeState as a member of your AudioProcessor holding all your automatable parameters, then a Slider/Button/ComboBoxAttachment for each UI element that controls one of them, usually as members of your AudioProcessorEditor. See this tutorial. You create your parameters in your AudioProcessor constructor -that’s where you set a skew for any parameter that needs one. I place that helper function in the cpp of my AudioProcessor, as it’s the only place where I use it. For example:

// processor.h
struct MyAudioProcessor : public AudioProcessor
{
    AudioProcessorValueTreeState apvts;
    UndoManager undoManager; // if you use one
    // etc
}

// processor.cpp
NormalisableRange<float> frequencyRange(float min, float max, float interval)
{
    return { min, max, interval, 1.f / std::log2(1.f + std::sqrt(max / min)) };
}

MyAudioProcessor::MyAudioProcessor()
    : apvts(*this, &undoManager /* or nullptr */, "my_plugin", {
        std::make_unique<AudioParameterFloat>("some_frequency", "some frequency", frequencyRange(20.f, 20000.f, 0.1f), 1000.f)
        // some more parameters, comma-separated
      })
{
}

// editor.h
struct MyAudioProcessorEditor : public AudioProcessorEditor
{
    MyAudioProcessorEditor(MyAudioProcessor& processor) : p{ processor } {}
private:
    MyAudioProcessor& p;
    Slider freqSlider;
    AudioProcessorValueTreeState::SliderAttachment freqSliderAttachment{ p.apvts, "some_frequency", freqSlider };
    // etc
}
1 Like

Cool! I think I’ve got it. I understand apvts now, and that the skew just remaps the knob-value relationship. Thanks for your all’s explanation!