Exponential Parameter

Hello Everyone ! Beginner question but I can’t seem to understand how it works despite other post I’ve read. How am I suppose to make an exponential parameter ? DisplayBase only affect the display isn’t it ? Put simply with an example : Is there an elegant way to have a frequency knobs going from 20 to 10,000k with 218 in the middle, with param.getValue() returning the value the knobs display ?

Thanks all !

since the oscillator itself is exponential, you don’t want the offset to be exponential, too. Most of mine had an octave control, 0 through 10. That’s purely linear, I think like -5 to 5 in one volt increments?

A better example might be the VCV Fundamental VCO. It has a presumably linear offset, but displays exponential pitch in Hz, and accepts text input in that format.

Thanks for your answer, Thats one use case, but sometimes I just want more precision in little values (knob CCW) than greater value (knob CW) Another example is for reverberation, where 8 o’clock would be 0.1s, 12’ would be 1s and 4’ would be 10s

There is plenty of precision. if the param is a 32 bit float that’s a lot of precision. Do the exp in your module like everyone else. You are looking at this the wrong way.

Isn’t there a module which does Hz to CV?

The easiest way to do this @NOI (and this is what squinky was saying) is to have the parameter itself be a linear range of 0…1 and make both the interpretation in the dsp code and the param quanitity to/from string do your scaling.

So lets say you wanted something crazy like the real value followed a 5 * sin curve from 0 to pi/2 as you turned the knob. You do

configParam<MyCustomParamQuantity>(BLAH, 0, 1, 0, "My Name")

then in your dsp you do

   auto useValue = 5 * std::sin(params[BLAH].getValue() * PI / 2 )

and you get the scaling. The thing is the display in the tooltip and type are now wrong so you do something like

struct MyCustomParamQuantity : rack::ParamQuantity 
{
   std::string getDisplayValueString() override {
      auto scaleVal = 5.0 * std:sin(getValue() * PI / 2);
      return std::to_string(scaleVal); // or whatever formatting you want
   }
   void setDisplayValueString(const std::string &s) override 
   {
      auto sv = std::atof(s.c_str());
      auto v = std::asin(sv / 5 ) * 2 / Pi; // Do some bounds checking
      setValue(v);
   }
}

roughly (this is from memory but it is basically right I think).

Then the knob behaves however you want since you implement the three API points where you need to do 0…1 to (whatever crazy range you want)

5 Likes

Thanks, yes, that is a better explanation. But if you want a nice exponential pitch in Hz, that’s already built in, so there is less to do. I strongly advise looking at Fundamental VCO to see how it does this.

oh yeah if you just want hz use the built ins.

i thought op wanted something nuttier than that.

oh, I don’t remember. It was my shorthand way of trying to say that for some “curves” there are built-in formatters and you don’t have to write your own. But, as you say, it’s it’s not in there, then you do. Like you demonstrated.

That’s the first thing I’ve done, I played with it’s code (rather unsuccesfully) and I couldn’t wrap my head around what was happening, thus the question… thanks for your patience !

Thanks a lot ! I’ll try this

Have a nice day !

1 Like

the key thing is this part of VCO.cpp. Here you can clearly see the very simple way the linear parameter and CV are exponentiated. The code is using approximations and simd float_4, which might make the code slightly odd looking:

		// Get frequency
			float_4 pitch = freqParam + inputs[PITCH_INPUT].getPolyVoltageSimd<float_4>(c);
			float_4 freq;
			if (!linear) {
				pitch += inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * fmParam;
				freq = dsp::FREQ_C4 * dsp::approxExp2_taylor5(pitch + 30.f) / std::pow(2.f, 30.f);
			}
			else {
				freq = dsp::FREQ_C4 * dsp::approxExp2_taylor5(pitch + 30.f) / std::pow(2.f, 30.f);
				freq += dsp::FREQ_C4 * inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * fmParam;
			}
			freq = clamp(freq, 0.f, args.sampleRate / 2.f);
			oscillator.freq = freq;

so, basically: freq = two_to_the(pitchCV + pitchParameter)

2 Likes

My code is working but i’m curious,

I still not quite understand how it’s done in the VCO
He config his param like this :
configParam(FREQ_PARAM, -54.f, 54.f, 0.f, “Frequency”, " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4);
where dsp::FREQ_SEMITONE = 1.0594630943592953f is the DisplayBase
and dsp::FREQ_C4 = 261.6256f is the displayMultiplier

taken from the API :
The formula is displayValue=f(value)∗displayMultiplier+displayOffset where f(value) is displayBase^value for displayBase>0.
(despite the param going to -54, which I find strange but math seems to say it’s work)

so the equation for the display is
218 * (1.059^param) (for param between -54 and 54)

but the equation for the dsp is (without the cv involve)
freq = dsp::FREQ_C4 * dsp::approxExp2_taylor5(pitch + 30.f) / std::pow(2.f, 30.f);
simplified : 218 * (2 ^ (param + 30)) / (2 ^ 30)

What is happening here ?
Is this some kind of math trick for optimization ?
and on a side note, why is (2^30) not using a constant ?
or even the taylor approximation which is used just before
I’m a bit lost

That’s a form of representing pitch in semitones or midi notes or some such. In virtual instruments you often represent frequency as 440*2^((n-69)/12) where n is a linear scale and this choice maps n=69 (the a above middle c on a midi keyboard, or concert a) to 440, the current standard tuning for concert a, with uniform spacing per note (equal temperament)

Looks like fundamental is doing the same thing with different units

1 Like

Oh and of course voct is the same just with different factors. There v0 is middle c or 261.5 hz give or take (actually 440 * 2^-9/12) and scales as 1v per octave, where in this tuning octave means frequency doubling so voct to hz is 261.5 * 2^voct

1 Like