Stepped Rotary Slider

I currently have a rotary slider set up for a delay plugin. When the slider is dragged, I only want it to stop on certain values in the range. I am currently trying to use the snapValue() member function but I am not sure how to implement it. I currently have it set up as this:

mTimeLSlider.snapValue(mTimeLSlider.getValue(), Slider::DragMode::absoluteDrag);

Thank you for any help or direction in advance!

1 Like

Ideally don’t solve this in the slider but the parameter. Otherwise the user is able to set illegal values from the automation, control surfaces or the hosts generic parameter controls.

I found the easiest is to connect the slider using AudioProcessorValueTreeState and its SliderAttachment. That makes sure to keep the settings synchronised, including range, step size, snapToValue, skew and textToValue/valueToText.

When you create the AudioParameterFloat for the delay parameter, use the constructor that takes a NormalisableRange<float> as argument. So follow the docs to NormalisableRange to learn, what options for snapToValue you have.

The snapToValue is a callback, i.e. you don’t call the function, but you provide the logic, so the system can call it. Back then, when the Slider was written, you did that by overriding the snapToValue() method, but nowadays using lambdas is a much more convenient way.

Here an example, I’ll write it step by step, but you can probably write it more condensed:

NormalisableRange<float> range (0.1f, 
                                1.0f,
                                [](auto rangeStart, auto rangeEnd, auto valueToRemap)  // maps real value to 0..1
                                { return jmap (valueToRemap, rangeStart, rangeEnd); },
                                [](auto rangeStart, auto rangeEnd, auto valueToRemap)  // maps 0..1 values to real world
                                { return jmap (valueToRemap, rangeStart, rangeEnd, 0.0f, 1.0f); },
                                [](auto rangeStart, auto rangeEnd, auto valueToRemap)  // maps real world to legal real world
                                {
                                    return jlimit (rangeStart, rangeEnd, 
                                                   0.1f * roundToInt (valueToRemap * 10.0f));  // e.g. make sure, only one digit after comma
                                });
// creating the parameter:
std::make_unique<AudioParameterFloat>("delay", "Delay", range, 0.1f);

Hope that helps

1 Like

Hey Daniel, Thanks for getting back to me! This totally makes sense to control it at the parameter. I have been messing around with this tonight and the range I have attached limits the parameter to be 400 anytime the slider is in between 300 and 500. Is this good practice to do this in the range or would you suggest another way?`

NormalisableRange<float>  range (10.0f, 1000.0f,
							[](auto rangeStart, auto rangeEnd, auto valueToRemap)  // maps real value to 0..1
							{ return jmap(valueToRemap, rangeStart, rangeEnd); },
							[](auto rangeStart, auto rangeEnd, auto valueToRemap)  // maps 0..1 values to real world
							{ return jmap(valueToRemap, rangeStart, rangeEnd, 0.0f, 1.0f); },
							[](auto rangeStart, auto rangeEnd, auto valueToRemap)  // maps real world to legal real world
							{
								if (valueToRemap > 300 && valueToRemap < 500)
								{
									return jlimit(rangeStart, rangeEnd,
										400.0f);  // e.g. make sure, only one digit after comma
								}
								else {
									return jlimit(rangeStart, rangeEnd,
										1.0f * roundToInt(valueToRemap * 1.0f));  // e.g. make sure, only one digit after comma
								}
							});

Ultimately I want to adjust this time parameter based off the tempo which I can get in the processBlock. Since this is before the processBlock, how would I access a variable from there to change the intervals?

The example would work like that, yes… I am not sure about your use case, but that is up to you, technically it is correct.

If you want the time being controlled by the host tempo, then an option is to make it a relative value. For instance percent of quarter beats. If the user leaves this alone on 100% it is synced with the beat. 110% would be slightly faster for instance. That way your delay always does the expected thing, when the hosts tempo changes. And have a toggle button to switch sync on and off…
So it comes down to 3 parameters:

  • sync on/of
  • delay time ms (could be hidden, when sync is on)
  • relative time (could be hidden, when sync is off)

That would be my approach, but there are probably many ways to do it…

2 Likes

Thanks daniel, I totally understand the approach. Maybe my problem is a bit of a beginner problem in that case, because I am struggling with how to make a parameter a relative value from inside the processBlock. After reading a bunch of posts on the forum, it seems like it is bad practice to set values from the processBlock, but I believe I can only access the getPlayHead() from the processBlock. Currently I am using parameterChanged() to set the parameter and it works, but the slider doesn’t reflect the change and it feels like I’m doing it wrong.

	AudioPlayHead* const ph = getPlayHead();
	AudioPlayHead::CurrentPositionInfo result;

	if (ph != nullptr && ph->getCurrentPosition(result))
	{
		parameterChanged(paramTimeR, (60000/result.bpm));

	}

Yes, that would be wrong :wink:
It is indeed bad practice to set the parameter. You want to calculate a value, that doesn’t need to be a parameter. Instead it is calculated via the combination of two values:

// assume the values read from your parameters:
float delay;    // msecs
bool sync;      // sync to bpm on/off
float relative; // percent of bpm
float bpm = result.bpm;

auto time = delay;
if (sync && bpm > 0)
    time = 60000.0f / bpm;

// play with `time` msecs delay

Hope that makes it a bit clearer

1 Like

Yes, that does make it clearer, and after a couple tests, this totally works to set the delay time for all my processing. I cannot however reflect the change on my slider. After time has been calculated I would like that to update the position of my delay time slider to equal the synced time. Am I missing something that you previously explained, or am I going about this syncing the wrong way by trying to update the slider?

If I understand you correctly, you want the slider to set a time value, then have the process block re-compute that value, then change the slider to that new value? That is circular logic. If your processing changes the slider parameter, then the parameter will change, and the processing will compute a new value, which will change the parameter again, and so on (forever, potentially).

You can always have a slider that sets a relative value, and then have your UI query the processor component for the actual value during the timer callback in order to update a separate display component, but you can’t have your slider both be an input and an output of your processing. (At least not without some kind of fancy footwork to prevent the circular changes I mentioned.)

Unless I’m misunderstanding something about your design here?

1 Like

Or to show from a different angle:
The user specified time value and the calculated time value would contradict each other. Instead of letting one value correct each other, I would leave the not selected alone and hide it from the UI, e.g. like:

void parameterChanged (const String& paramID, float value) override
{
    if (paramID == "sync")
    {
        const auto syncOn = value > 0.5f;
        timeSlider.setVisible (syncOn == false);
        percentSlider.setVisible (syncOn);
    }
}

And otherwise always do the same thing for both sliders, e.g. in resized(), give them the identical bounds.

An alternative strategy is to swap the SliderAttachment for that slider.

In the processor, both parameters are visible, and IMHO it is a good thing not to change the meaning of a parameter depending of the sync button.

1 Like

Thank you both for helping me out with that. I was struggling to wrap my head around how to make a relative parameter and I appreciate you guys giving me a couple different explanations, it really made it finally click!

2 Likes

I’m curious. If all I’m looking for is the value produced in the snapToLegalValueFunc callback, can I simply keep the other two callbacks null?

Are there any unintended consequences of the code below?

NormalisableRange<float> range (0.1f, 
                            1.0f,
                            null,
                            null,
                            [](auto rangeStart, auto rangeEnd, auto valueToRemap)  // maps real world to legal real world
                            {
                                return jlimit (rangeStart, rangeEnd, 
                                               0.1f * roundToInt (valueToRemap * 10.0f));  // e.g. make sure, only one digit after comma
                            });