Closed elanhickler closed 7 years ago
I wonder if you might want to have a place for a pointer for the smoothing amount value? To avoid making a loop for my "smoothing amount parameter setting slider" and putting all the parameters in its callback that I want to smooth in there.
Edit: Oh I guess the only difference there is where you put it in code. I'd still need to direct each parameter to a smoothing amount value somewhere.
Oh yeah, I want to allow for 0.0 smoothing to 1.0 smoothing (frozen)
Oh wait, would this contradict the design of keeping gui and audio separate? Nevermind if it is. I implemented smoothing for some parameters in PrettyScope already.
smoothing - yeah - this would also be nice to have. how does your system work? do you have to re-implement it everytime from scratch? can't you make that reusable and put it somewhere into your shared code section?
the problem i'm seeing with putting that functionality into the Parameter class is to bloat the class up and in some cases, one might not need that feature. of course, i could put it into a subclass SmoothedParameter of Parameter and add the functionality there. ...but then we may also want other features like meta-control, modulation, etc. currently my inheritance hierarchy looks like this:
Parameter < MetaControlledParameter < ModulatableParameter
i might do it like:
Parameter < SmoothedParameter < MetaControlledParameter < ModulatableParameter
...but then i couldn't have non-smoothed meta-controlled parameters. actually, it would be desirable to be able to mix and match these 3 additional functionalities smoothing, meta-control and modulation (and maybe more to come in the future) at will from client code side. i'm considering a redesign of the Parameter class using the "Decorator" design pattern:
not sure, if that's a good idea...
Right now my myparams
class holds the information for just about everything I need for conveniently defining parameters, defining callbacks, setting/getting values, and convenience functions to make the code less verbose.
Today, I just threw in a rosic::ExponentialSmoother
member in myparams
.
myparams
holds everything... well, because of this, there will be nullptrs that needs to be filled. So myparams
does not have the most solid C++ design, as it would be easy to get errors and crash. Certain things need to be done in a certain order and in certain functions/callbacks, etc. It's not too bad, but a random programmer would need working code examples and be careful not to move certain things around. Robin, I think you could possibly change your code base so this is less of a problem. Not sure, need to think more.
NOTE: For this explanation, not all code is represented. Some code is omitted so it's easier to focus on the relevant content. The explanation will be in multiple comments!
paramStrIds
while at the same time constructing all the myparams
objects (declared in PrettyScope
) with a cool syntax:paramStrIds is declared in PrettyScope
ptr
with a jura::MetaControlledParameter
for each myparams
:ptr
is declared in myparams
as a jura::Parameter *
paramStrIds
// register
void ScopeDisplayOpenGL::connectParameters()
{
for (auto & s : scope->paramStrIds)
s->ptr->registerParameterObserver(this);
}
// deregister
void ScopeDisplayOpenGL::disconnectParameters()
{
for (auto & s : scope->paramStrIds)
s->ptr->deRegisterParameterObserver(this);
}
void ScopeDisplayOpenGL::parameterChanged(Parameter* p)
we checked to see what parameter was changed by doing a name/string lookup. Now I have...ScopeDisplayOpenGL
so I can look it up with a jura::Parameter *
, which I assume is faster than string. I run the std::function
that I instantiated earlier inside initialzeParamLookup()
.void ScopeDisplayOpenGL::parameterChanged(Parameter* p)
{
/* I couldn't figure out where to put `initializeParamLookup()`
* where the openglOscilloscope is not nullptr. So I put it here. */
if (!initializeParamLookup_was_called)
initializeParamLookup();
paramlookup[p](p->getValue());
}
initilizeParamLookup()
. I set a default callback at the top of the function so we don't have to ask whether a parameter is found or not, because I'm not storing all jura::Parameter *
in paramlookup
, as only some parameters need a specific function for ScopeDisplayOpenGL
.void PrettyScopeWidgetSection::createWidgets()
{
/* We check to see what kind of myparam we are looking at.
* Is it a BUTTON? SLIDER? COMBOBOX?
* I have an enum for this: enum {BUTTON, SLIDER, COMBOBOX}; */
for (auto & param : prettyScope->paramStrIds)
{
switch (param->type)
{
case BUTTON:
param->button = new jura::AutomatableButton(param->text);
break;
case SLIDER:
param->slider = new jura::AutomatableSlider();
param->slider->setSliderName(param->text);
param->slider->setStringConversionFunction(param->stringConvertFunc);
break;
case COMBOBOX:
param->combobox = new jura::AutomatableComboBox();
param->combobox->registerComboBoxObserver(this);
break;
}
param->getRWidget()->assignParameter(param->ptr);
param->getRWidget()->setDescriptionField(infoField);
param->getRWidget()->setDescription("");
addWidget(param->getRWidget());
}
}
getValue()
setValue()
functions from jura::Parameter *
via boolean operators such as ==
, !=
, <
, etc, and most importantly, the equals =
operator:/* this is inside myparams class */
operator double() const { return ptr->getValue(); }
bool operator==(const double & rhs) const { return ptr->getValue() == rhs; }
bool operator!=(const double & rhs) const { return ptr->getValue() != rhs; }
bool operator<(const double & rhs) const { return ptr->getValue() < rhs; }
bool operator>(const double & rhs) const { return ptr->getValue() > rhs; }
myparams& operator =(double rhs) { ptr->setValue(rhs, true, true); return *this; }
myparams objects
for the get/set value stuff with operators://set default value in void PrettyScope::createParameters(), calls getValue
LineColorMode = 1.0;
// checks if FXMode button is on so we can write modified audio to output, calls getValue
if (FXMode)
//in mosueDrag callback to set ShiftX, calls setValue
scope->ShiftX = +dragValueX + init_ShiftX;
myparams
class (remember, it holds EVERYTHING!! HEEHEE!!). I added the smoother object and some smoother convenience functions./* this is inside myparams class */
rosic::ExponentialSmoother smoother;
void setSmootherTarget(double v) { smoother.setTargetValue(v); }
double getSmootherValue() { return smoother.getSample(); }
spawn a jura::Parameter *
/ AutomatableSlider *
for ParameterSmoothing
with the help of myparams
constructor, and save inside paramStrIds
impregnate an std::function
into ParameterSmoothing
's jura::Parameter *
via setValueChangeCallback()
, put all the myparams
babies in here that you want to smooth, and call their smoothers' setTimeConstantAndSampleRate
.
for each parameter you want to smooth, shove a little std::function into jura::Parameter *
. Remember, .ptr
refers to a jura::Parameter *
, a member of myparams
.
in our void PrettyScope::processBlock(double **inOutBuffer, int numChannels, int numSamples)
you then slap this kinda stuff in:
ok, just moved myparams to its own class, defined right above PrettyScope class.
Another thing is that we are smoothing unchanging values. Once the smoothing has reached the target value, we no longer need to continue updating the smoother. If you were to build something into your library, I bet you could account for this, such as: calculate the number of samples it takes to reach target value (such as .000001 delta of current - target), and once getSample()
has been called that many times, set smoother current value to target value.
...or more complex... have a smoother manager, and add/remove things from the manager's smoothing list when they do or do not need smoothing?
pasted over from the Upsampling/Oversampling for PhaseScope/PrettyScope thread
class SmootherManager
{
public:
SmootherManager() {}
juce::Array<smoothedParameter *> CurSmoothingList;
void add(smoothedParameter * sp)
{
CurSmoothingList.addIfNotAlreadyThere(sp);
}
void remove(smoothedParameter * sp)
{
CurSmoothingList.removeFirstMatchingValue(sp);
}
void doSmoothing()
{
for (auto & sp : CurSmoothingList)
sp->incValue();
}
};
the object adds itself to the manager, so you need to instantiate the manager and give it a pointer to that so it can add/remove itself.
add itself when value changed remove itself when value stops changing, or the difference is like... 0.000001. in that case, set the smoother's internal state to the target value, don't leave it at a difference obviously.
I'm trying to make a parameter smoother manager, but it seems like it is going to conflict with the modulation system, because
right now I have this
parTune.setCallback([this](double v) { jbMushroomCore.setPitchOffset(v + parOctave*12); });
but I need this:
parTune.setCallback([this](double v) { parTune.smoother.setTargetValue(v); });
but now the modulation system is going to call parTune.smoother.setTargetValue()
dunno what to do. How do I smooth the slider changes without smoothing the modulator?
It seems that you have to implement a smoother that works alongside the modulation system where changing the UI slider sets the new modulation offset value, that then becomes my "v"
actually, this is really needed right now. If you aren't going to do it, tell me where in code I can change RS-MET library locally. Here's my code so far if you want to copy and paste it in for yourself:
class ParamSmoother
{
public:
ParamSmoother()
{
gaussSmoother.setOrder(8);
gaussSmoother.setShapeParameter(1);
}
void setSampleRate(double v) { sampleRate = v; }
enum type { EXPONENTIAL, GAUSSIAN, LINEAR };
void setSmootherType(type v) { smootherType = v; }
void setSmoothingAmount(double v) { smoothingAmount = v; }
void inc();
void setInternalValue(double v);
void setTargetValue(double v);
double getValue() { return currentValue; }
protected:
RAPT::rsSmoothingFilter<double, double> gaussSmoother;
rosic::ExponentialSmoother expoSmoother;
double sampleRate;
int smootherType = 0;
double currentValue, targetValue;
double smoothingAmount = 0.0;
bool isPaused = false;
};
void ParamSmoother::inc()
{
if (isPaused)
return;
switch (smootherType)
{
case EXPONENTIAL:
currentValue = expoSmoother.getSample();
break;
case GAUSSIAN:
currentValue = gaussSmoother.getSample(targetValue);
break;
}
}
void ParamSmoother::setTargetValue(double v)
{
targetValue = v;
switch (smootherType)
{
case EXPONENTIAL:
expoSmoother.setTargetValue(v);
break;
case GAUSSIAN:
break;
}
}
void ParamSmoother::setInternalValue(double v)
{
currentValue = v;
switch (smootherType)
{
case EXPONENTIAL:
expoSmoother.setCurrentValue(v);
break;
case GAUSSIAN:
gaussSmoother.setTimeConstantAndSampleRate(0, sampleRate);
gaussSmoother.getSample(v);
gaussSmoother.setTimeConstantAndSampleRate(smoothingAmount, sampleRate);
break;
}
}
class ParamManager
{
public:
ParamManager() {}
ParamManager(vector<myparams*> ParamList) : ParamList(ParamList)
{
for (auto & p : ParamList)
if (p->shouldBeSmoothed)
paramsThatShouldBeSmoothed.push_back(p);
}
void setSampleRate(double v)
{
for (auto & p : paramsThatShouldBeSmoothed)
p->smoother.setSampleRate(v);
}
void addForSmoothing(myparams * sp)
{
paramsToSmooth.addIfNotAlreadyThere(sp);
}
void removeForSmoothing(myparams * sp)
{
paramsToSmooth.removeFirstMatchingValue(sp);
}
void doSmoothing()
{
for (auto & p : paramsToSmooth)
p->smoother.inc();
}
int getNumParamsToSmooth()
{
return paramsToSmooth.size();
}
protected:
vector<myparams*> ParamList;
juce::Array<myparams*> paramsToSmooth;
vector<myparams*> paramsThatShouldBeSmoothed;
};
I found ModulationTarget class with this:
void setUnmodulatedValue(double newValue)
{
unmodulatedValue = newValue;
}
So all that is needed is to call setUnmodulatedValue()
for slider changes based on the smoothed value or something.
Aha, this is inside ModulatableParameter... now, how to use it...
/** Overriden in order to also set up unmodulatedValue member inherited from ModulationTarget. */
virtual void setValue(double newValue, bool sendNotification, bool callCallbacks) override
{
MetaControlledParameter::setValue(newValue, sendNotification, callCallbacks);
ModulationTarget::setUnmodulatedValue(newValue);
}
If you don't quickly make a smoothing system, maybe you could quickly separate out the modulation and slider change callbacks so I can do:
parTune.setCallback([this](double v)
{
//ptr is jura::ModulatableParameter2
double modValue = parTune.ptr->getModulationValue();
double sliderValue = parTune.ptr->getUnmodulatedValue();
parTune.smoother.setTargetValue(sliderValue);
// in my per sample callback I do parTune.smoother.inc();
jbMushroomCore.setPitchOffset(parTune.smoother.getValue() + modValue);
});
Boom! Done.
I need a sliderChangeCallback, so I can add parameters to my smoother manager only when the slider changes rather than when the modulation changes
ok, i finished implementing a smoother manager to my satisfaction, you no longer need to rush this. Implement it whenever.
fuck. nope. Smoothing doesn't work because the modulated value is inexplicably tied to to the slider value.
If there's no modulator, my smoother setup works. If there's a modulator, then the modulation bias is based on the slider value or something. I need smoothing! Do something like give me some callbacks to work inside or some functions to retrieve the raw modulation value before it gets affected by the slider or something!!!!
...I guess i can release some free products with this smoothing issue.
is you smoother calling ModulatableParameter::setValue per sample with the smoothed (lowpassed) value? if, so - what kind of problem are you experiencing? actually, this call would set up the unmodulatedValue in the ModulationTarget class, which in turn would get modulated by the modulators. and if the unmodulated value changes smoothly, so should the modulated value as well. the modulated value should just smoothly follow the unmodulated one, if i'm not mistaken.
i could certainly add a getUnmodulatedValue function to ModulationTarget, if you need to inquire that value. but i would think, what you actually need is a setter - and such a setter is already there. ModulatableParameter::setValue calls ModulationTarget::setUnmodulatedValue (which you could also call directly, if deisred, but i think ModulatableParameter::setValue is appropriate)
Do something like give me some callbacks to work inside or some functions to retrieve the raw modulation value before it gets affected by the slider
actually, there is no such thing as a "raw modulation value" that "gets affected by the slider". is the other way around: there's a raw (unmodulated) value that is set up by the slider and that value gets affected by the modulators
so if you smoothly change that nominal unmodulated reference value, it should actually work...unless i'm missing something
ModulatableParameter::setValue calls ModulationTarget::setUnmodulatedValue (which you could also call directly, if deisred, but i think ModulatableParameter::setValue is appropriate)
wait - no - you should probably better call ModulationTarget::setUnmodulatedValue because ModulatableParameter::setValue also calls MetaControlledParameter::setValue - and that notifies the host about plugin automatable parameter changes. you probably don't want that to happen per sample
robin, just tell me what to do! Don't add features unless what I want to do is impossible. Ok, let me try what you are suggesting. ModulationTarget::setUnmodulatedValue
i don't know your smoothing system well enough to be exactly sure what to do. but my suggestion would be to call ModulationTarget::setUnmodulatedValue from your per-sample update function in your smoother. i think, that should work. like per sample, call
myParam->setUnmodulatedValue(mySmoothingLowpass.getSample(targetValue))
(just as conceptual pseudocode)
that sets the base value which in turn gets modulated. so, you would make this base value (or "bias" or whatever you want to call it) smoothly changing
ok, I can set the unmodulated value, sure, but moving the slider still sets the value too. So how do I stop that and only have the value be set by the smoother?
Also, setting the unmodulated value doesn't do anything (as far as I can tell) unless there's a modulator involved.
Right now this happens.
I think just I need a separate callback for slider updates or something.
I need:
My smoother manager code solid, all you would have to do is copy/paste that into handling the underlying slider value. You could even use less code that this!
class SmootherManager
{
friend class myparams;
public:
SmootherManager() {}
// add available parameters that need smoothing
void addParameters(vector<myparams*> list);
// sets samplerate for all smoothers
void setSampleRate(double v);
// call this per sample to do smothing
void doSmoothing();
// call this in block AFTER smoothing to remove "finished" smoothers
void cleanup();
// call is in block to know if smoothing should take place per sample
int getNumParamsToSmooth();
// convenience function to set smoothing amount for all smoothers
void setGlobalSmoothingAmount(double v);
protected:
void addForSmoothing(myparams * sp);
void removeForSmoothing(myparams * sp);
vector<myparams*> ParamList;
juce::Array<myparams*> paramsToSmooth;
vector<myparams*> paramsThatShouldBeSmoothed;
};
void ParamManager::addParameters(vector<myparams*> list)
{
ParamList.insert(ParamList.end(), list.begin(), list.end());
int i = 0; // robin doesn't need this
for (auto & p : ParamList)
{
p->id = i++; // robin doesn't need this
p->managerPtr = this;
// robin doesn't need this
// this is to be more efficient for setSampleRate and setGlobalSmoothingAmount
// we have a master list of parameters and from that list we get
// another list just for those parameters that want smoothing
if (p->shouldBeSmoothed)
paramsThatShouldBeSmoothed.push_back(p);
}
}
void ParamManager::setSampleRate(double v)
{
for (auto & p : paramsThatShouldBeSmoothed)
p->smoother.setSampleRate(v);
}
void ParamManager::setGlobalSmoothingAmount(double v)
{
for (auto & p : paramsThatShouldBeSmoothed)
p->smoother.setSmoothingAmount(v);
}
// robin, you could have the underlying parameter
// add itself and keep track of if it was added
// rather than the manager. I did this this way because
// it's cleaner code for dealing with the lack
// of proper callbacks.
void ParamManager::addForSmoothing(myparams * p)
{
if (p->isAddedToSmootherManager)
return;
paramsToSmooth.add(p);
p->isAddedToSmootherManager = true;
}
// again, you can have the underlying parameter
// do this stuff.
void ParamManager::removeForSmoothing(myparams * p)
{
paramsToSmooth.removeFirstMatchingValue(p);
p->isAddedToSmootherManager = false;
}
// not sure how else to do it with
// the lack of proper callbacks
void ParamManager::doSmoothing()
{
for (auto & p : paramsToSmooth)
{
p->smoother.inc();
p->ptr->callValueChangeCallbacks();
}
}
void ParamManager::cleanup()
{
for (auto & p : paramsToSmooth)
if (!p->smoother.needsSmoothing())
removeForSmoothing(p);
}
int ParamManager::getNumParamsToSmooth()
{
return paramsToSmooth.size();
}
Here I'm pulling some code for the actual smoother class just so you see how I make sure we don't smooth parameters that no longer need smoothing.
// every sample we check to see if we are close enough to target
// if so, call end smoothing so that needs_smoothing becomes false
void ParamSmoother::inc()
{
if (!needs_smoothing || isPaused)
return;
currentValue = expoSmoother.getSample();
if (fabs(currentValue - targetValue) <= 1.e-6)
endSmoothing();
}
// when we set a new target, set needs_smoothing to true
// you could also check to see if smoothing speed is
// negligible and just call endSmoothing if so and
// keep needs_smoothing false
void ParamSmoother::setTargetValue(double v)
{
targetValue = v;
expoSmoother.setTargetValue(v);
needs_smoothing = true;
}
void ParamSmoother::endSmoothing()
{
currentValue = targetValue;
expoSmoother.setInternalValue(targetValue);
needs_smoothing = false;
}
void ParamSmoother::getCurrentValue()
{
return currentValue;
}
Robin, I just realized I need a smoother manager for my synth for general purposes of smoothing things like pitch and just random stuff.
If you create a built in smoother for parameters, I'm guessing I can't also use it to smooth arbitrary variables in my synth. So I should keep my smoother manager? I could repurpose it just for arbitrary variables not having to do with parameters.
oh - for arbitrary variables. that's an interesting design challenge. how would you approach that? i guess, it would have to based on some member-function pointer where the member function to be called must adhere to a given signature, such as: void setSomething(double something);
....or something. in a similar way, the valueChangeCallback of the Parameter class works. unless you want to force your dsp-classes with smoothable parameters to be subclasses of some framework'ish baseclass
To make things uncomplicated, and also allow the smoother manager to do more than just parameters, the class that needs a manager has a smoother manager point AND smoother objects
what about something like this
class SmootherManager
{
public:
Array<Smoother *> smoothersCurrentlyActive;
void addForSmoothing(Smoother * smoother)
{
if (!smoother->wasAlreadyAdded)
{
smoothersCurrentlyActive.push(smoother);
smoother->wasAlreadyAdded = true; // set to false before removing smoother
// manager removes smoother when it's done smoothing
}
}
};
class Monosynth
{
public:
SmootherManager * smootherManagr;
Smoother pitchSmoother;
void frequencyChanged()
{
smootherManager->addForSmoothing(pitchSmoother);
}
updateFrequency()
{
}
void processPerSample()
{
if (pitchSmoother.valueHasChanged())
updateFrequency();
}
};
do you want for every smoothable parameter a paramaterChanged and updateParameter function? ....and a big series if if(thisSmoother.valueHasChanged)...? i think, this will lead to a proliferation of boilerplate code real quick
also, your monosynth is coupled with the smoother. i'd prefer a design that decouples the monosynth from the smoother/manager
ok, maybe you have better ideas. I wouldn't worry about supporting arbitrary variables unless you see an easy way to do it.
i would probably attempt something based on a callback-object, like in Parameter. a smoother gets a target value (and current value) and a member-function-callback-object (which contains the target-object and the member function to call). ...and then the smoother manager would call some per sample update-function on each of its smoothers. the target class, i.e. the smoothee, would not have to know about the existence of the smoother class at all. it could be, for example, a filter - and the smoother would then just call setCutoff on it, per sample, via the callback-object. from the perspective of the filter, it would just look like any other call to setCutoff
....but: how to marry that with the mod-system? that adds another layer of complication
That seems like the easy part. Your mod system has the "unmodified value", that's the one you smooth/update. You know my smoother manager works with your mod system, it only doesn't work for child modules because I didn't provide them with a smoother manager pointer, I'd need to rewrite some code for that.
oh, well you're talking about value change callbacks, nevermind. In my value change callbacks I do smoother.getSmoothedValue()
essentially.
hmm...well, ok, yes - you could give the smoother the ModulatableParameter as target "smothee" object with the setUnmodulatedValue as target callback. that should probably work
did your smoother actually work well together with the meta-system? such that, for example, when 2 parameters were assigned to the same meta and you moved one, both were smoothed? where did you set up your target-value? in the slider callback? because i think, in this case, it should not work out with the meta-system. i think, to make it all work, i'll need to change my inheritance hierarchy to something like:
Parameter < Modulated < Smoothed < MetaControlled
...but where would then later a Poly-Parameter go? maybe between Modulated and Smoothed....but then we would always have the data-overhead of mod and poly, even for parameters where just smoothing and meta-control is needed
because when a meta changes, it should set the target of the smoother (so "smoothed" must be inside "meta"). the smoother in turn should change the unmodulatedValue of the modulated parameter (so "modulated" must be inside "smoothed")
My smoother was fine with meta-parameter system... actually... I never tested it... not sure.
really? that would sort of surprise me. can you try to assign some arbitrary parameter and DC to the same meta, move the "arbitrary" slider and see if you get smoothed dc?
works fine. I still get clicks due to having your modulation stuff in my smoothing code. But i think we can safely assume it is it working as intended.
...i mean, if you set the smoother target in the slider callback, how would that be supposed to work?
hmm...ok..i think, i need to check the call-stack in the debugger
not sure what you are using this for, but you can store a bool "isAddedToWhatever" inside the object that you are adding to an array.
addObjectToArray(object * obj)
{
if (obj->wasAdded)
return;
obj->wasAdded = true;
myArray.add(object); // does not require a search
}
removeObjectFromArray(object * obj)
{
obj->wasAdded = false;
myArray.remove(object); // this may require a search
}
i'm using this for general purpose adding of an object to a std::vector, if it's not already inside the vector. "general purpose" here means that it's not feasible in general to store a "wasAdded" flag inside the object. you could do this thing with special purpose arrays - but it would imply that the to-be-stored objects are subclasses of some baseclass that has a "wasAdded" flag. so you would have a coupling between the array container class and the element class
...also what would you do, if the object is possibly element of several arrays at the same time?
Could you put in a smoother for parameters?
The only non-obvious thing I would have to do is do some increment of the smoother in the processblock sample loop, easy. Would be pretty easy for you to implement as well.
Oh, and I'd have another place in code where I set the smoothing amount per parameter.
you have getValue, you have setValue. Add getSmoothedValue(), add incSmoothedValue(), or whatever.
I'm building my own system for smoothing for PrettyScope, already did it for Spiral Generator. Just annoying, waste of space in code!