To my amazement I actually managed to get my first C++ code to compile without errors and the thing even does something. Simply ramping up a voltage with cosine, linear and exponential interpolation. Is it ok to put some code up here (~120 lines) with some questions for comment?
I donāt see why not.
Ok then, here we go. Code below.
Questions:
- I donāt understand the pulseGenerator.
first there is
endPulseGen.trigger(1e-3f);
then
endPulse = endPulseGen.process(1/args.sampleTime);
outputs[END_OUTPUT].setVoltage(endPulse ? 10.f : 0.f);
this works, but
endPulse = endPulseGen.process(args.sampleTime);
does not work, it works as a gate. Voltages goes up but does not come down?
-
How to keep the state of things? Three states is doable, more would be complex.
-
Do I have to set variables and output voltages every cycle? For example in the last āelseā they are there to get the voltage to 0 when a cable is unplugged.
-
Thereās a lot of (nested) āifā making it complex to keep track of things. Is there a way out?
-
Creating more ramps (8) would be a matter of adding them in a loop and turn some of the variables in arrays?
-
Is the interpolation function a candidate for simd? Just for learning and maybe for an other plugin that needs more uhmpf.
-
Is there a way to put more than one cable on an triggered input? For example for looping the end signal to start?
-
A lead on how to create a knob that starts at 0, has a mid position value of 1 and a end value of 10 would be welcome.
#include "plugin.hpp"
#include <cmath>
struct Ramp : Module {
enum ParamIds {
VFROM_PARAM,
VTO_PARAM,
TIME_PARAM,
INTERP_PARAM,
NUM_PARAMS
};
enum InputIds {
START_INPUT,
STOP_INPUT,
NUM_INPUTS
};
enum OutputIds {
END_OUTPUT,
VOUT_OUTPUT,
NUM_OUTPUTS
};
enum LightIds {
NUM_LIGHTS
};
Ramp() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
configParam(VFROM_PARAM, 0.f, 10.f, 0.f, "Voltage from");
configParam(VTO_PARAM, 0.f, 10.f, 0.f, "Voltage to");
configParam(TIME_PARAM, 0.f, 1200.f, 0.f, "time", "s");
configParam(INTERP_PARAM, 0.f, 10.f, 0.f, "interpolate");
}
/** Interpolates over gs, ge and rescales to ts, te.
gc: global current
//gs: global start omitted as gs is always 0
ge: global end
ts: target start
te: target end
im: interpolation method,
im = 0 - cosine interpolation
0> im <1 - exponential (ease in)
im = 1 - linear
1> im <10 - exponential (ease out)
im = 10 - step
*/
inline float interpolate(float gc, float ge, float ts, float te, float im) {
float v;
if (im == 10.f) {(gc == ge) ? v = te : v = ts;} // step
else {
float x = gc / ge;
if (im != 0) {
(im == 1.f) ? v = ts + (te - ts) * x : v = ts + (te - ts) * pow(x, im);
}
else { // cosine
float f = (1 - cos(x * M_PI)) * 0.5;
v = ts * (1 - f) + te * f;
}
}
return v;
}
float gc = 0;
bool running = false;
bool finished = false;
bool stopped = true;
bool endPulse = false;
dsp::SchmittTrigger startTrigger;
dsp::SchmittTrigger stopTrigger;
dsp::PulseGenerator endPulseGen;
void process(const ProcessArgs& args) override {
if (inputs[START_INPUT].isConnected() && outputs[VOUT_OUTPUT].isConnected() == true) {
if (running == true) {
gc += args.sampleTime;
if (gc < params[TIME_PARAM].getValue()) {
outputs[VOUT_OUTPUT].setVoltage(
interpolate(
gc,
params[TIME_PARAM].getValue(),
params[VFROM_PARAM].getValue(),
params[VTO_PARAM].getValue(),
params[INTERP_PARAM].getValue()
)
);
}
else {
running = false;
finished = true;
stopped = false;
endPulseGen.trigger(1e-3f);
}
}
else if (finished == true) {
endPulse = endPulseGen.process(1/args.sampleTime);
outputs[END_OUTPUT].setVoltage(endPulse ? 10.f : 0.f);
outputs[VOUT_OUTPUT].setVoltage(params[VTO_PARAM].getValue());
}
if (startTrigger.process(inputs[START_INPUT].getVoltage()) == true) {
gc = 0;
running = true;
finished = false;
stopped = false;
}
if (inputs[STOP_INPUT].isConnected() && stopTrigger.process(inputs[STOP_INPUT].getVoltage()) == true) {
gc = 0;
running = false;
finished = false;
stopped = true;
outputs[VOUT_OUTPUT].setVoltage(0.f);
}
}
else {
outputs[VOUT_OUTPUT].setVoltage(0.f);
gc = 0;
}
}
};
/* --- snip silver screw and black knob from hardware department --- */
-
Pass args.sampletime to the pulse generator not 1 / sampletime. sampletime = 1/samplerate.
-
To keep states you need variables. There is no other way arround it. But maybe you can eliminate the stoped variable and use !running intead. The ! operator makes true estatements false and false true.
-
I also donāt like nested if. Sometimes to keep it less tabbed I do things like:
if (inputnotconected || !running) { return; }
-
Yes, 8 arrays is what you need. You could use objects if it becomes too complex to keep track of everything. But the array thing usually is better for performance.
-
I donāt know about simd.
-
Inputs can only have one cable connected to it.
-
For the knob what I do is have a parameter from 0 to 1. And a array{0,1,10} and use linearinterpolation(array, param*arraylen).
Input cables only except 1 connection unless poly where it can have up to 16 channels on one cable, any poly merger could sum mono cables.
Calling sin every process call will make your code 10x slower than it needs to be. Rack has a sin aproximation built in.
You could tidy a bit by assigning the interpolate variables out of scope.
float time = params[TIME_PARAM].getValue(),
voltFrom = params[VFROM_PARAM].getValue(),
voltTo = params[VTO_PARAM].getValue(),
interp = params[INTERP_PARAM].getValue();
outputs[VOUT_OUTPUT].setVoltage(interpolate(gc, time, voltFrom, voltTo, interp);
Not sure if there is another way around not using nested statements.
@lomasmodules Iāll look into the 1/sampleTime again tomorrow and investigate the ! operator. 8 arrays are working now after many crashes and vague errors lerp the knob is what Iāll try.
@Coirt Ah, polyphony, thatās why it sometimes works and then in an other module not. Would it be useful to add polyphony to a ramp? Regarding tidying up, somehow I always end up with āblack pagesā of code
@Squinky The cos and pow and thinking about other blending functions like logistic-sigmoid is why I looked at simd. Iāll look into the approixmated one. Currently it does .75us with all 8 ones running on an 8 year old AMD athlon under win10.
Thank yāall, Cheers
Well if you wanted combine inputs you could use polyphony to sum, but there might be some conflict if it was full poly. Probably just better to have it full poly rather than 2 channel poly. If it was full poly then the extra in port would probably be a must from a UX stand point, unless someone would read a manual they may not realise that function exists on the module.
It is useful to have poly but with regards to params you canāt have different settings across all channels from controlled by 1 knob, no choice but to be the setting across all poly channels.
Regarding inputs, decided to keep it monophonic and with just one input.
The matter with 1/sampleTime was a bug in my logic. I connected the end signal to the stop input, then the pulse generator never times out. Manually pulling the signal down on stop works fine.
Where can I find the faster cos approximation?
pow
has been implemented so far in dsp/approx.hpp
the dsp/approx.hpp seems only do pow(2,x), not pow(x,y)?
Edit: moved the code to https://github.com/Moaneschien/4rack Will have to do some work on the knobs now.
Thanks,
There is a simd cos approximation in /include/simd/sse_mathfun.h
I donāt know if there is a single float version.
Iāve always used an interpolating lookup for all fancy functions. Itās not as fast as some of the approximations, but it does work for any function, and is always fast enough. If you remember your high school math itās easy to do.
just adding using simd::float_4;
made it ~30% faster as well on ideling and all interpolations. Couldnāt see no difference wit simd::cos
, then again I only have the engine cpu meter to observe,
Thanks, Iāll investigate more,
Ingo
Cpu meters are good enough for gross things. If the trig function isnāt a critical path, then you donāt need to worry.
the difference youāll notice is when youāre running multiple parallel operations. executing simd::cos(1.0f)
essentially runs four parallel instances of cos(1.0f)
. if youāre doing math on four in parallel, youāll see a much better savings:
float_4 v;
v[0] = 1.0f;
v[1] = 0.9f;
v[2] = 0.8f;
v[3] = 0.7f;
v = simd::cos(v);
note that accessing the individual float
values is still a bit expensive, but if you load up float_4
values and do the math with float_4
as much as possible, youāll see a significant gain: especially when executing 4 paths in parallel.
if you have the Synthetic FX (non-free), you can see this in action by using Curds or Whey, where up to 64 Biquad operations are occurring for every step (16x polyphony and a customized Biquad making 4 parallel operations).
it makes quite an improvement if done correctly.
Itās not just simd. I replaced the scalar trig functions in the AS mixer with scalar lookup tables and it was 8 times faster! Every plugin Iāve improved was significantly slowed down by trig functions. Befaco Even VCO, the old Fundamental VCO-1. sin and cos are just really slow, at the resolution mandated by IEEE math spec.
Insightfull,
Thanks.
Just been poking in your code & docs a bit. Only calculating every 4 samples is a nice one Iāll try.