RobinSchmidt / RS-MET

Codebase for RS-MET products (Robin Schmidt's Music Engineering Tools)
Other
57 stars 6 forks source link

Built in smoother for parameters #66

Closed elanhickler closed 7 years ago

elanhickler commented 7 years ago

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!

elanhickler commented 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)

elanhickler commented 7 years ago

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.

RobinSchmidt commented 7 years ago

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:

https://sourcemaking.com/design_patterns/decorator

RobinSchmidt commented 7 years ago

not sure, if that's a good idea...

elanhickler commented 7 years ago

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!

:star: Initializing jura::Parameter *

Here's how I conveniently fill paramStrIds while at the same time constructing all the myparams objects (declared in PrettyScope) with a cool syntax:

paramStrIds is declared in PrettyScope image

Now I need to fill each ptr with a jura::MetaControlledParameter for each myparams:

ptr is declared in myparams as a jura::Parameter * image

here's the other usages of 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);
}

:star: Initializing functions for ScopeDisplayOpenGL::parameterChanged()

Before, in void ScopeDisplayOpenGL::parameterChanged(Parameter* p) we checked to see what parameter was changed by doing a name/string lookup. Now I have...

image

...inside 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());
}

Here's a look at 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.

image

:star: Initializing widgets

read comments carefully

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());
    }
}
elanhickler commented 7 years ago

:star: myparams Convenience functions explained

This stuff makes use of 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; }

here's some example usage of 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;

:star: How I did smoothing

here's what I did inside 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(); }

now to put it all together

  1. spawn a jura::Parameter * / AutomatableSlider * for ParameterSmoothing with the help of myparams constructor, and save inside paramStrIds image

  2. 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. image

  3. 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. image

  4. in our void PrettyScope::processBlock(double **inOutBuffer, int numChannels, int numSamples) you then slap this kinda stuff in: image

elanhickler commented 7 years ago

ok, just moved myparams to its own class, defined right above PrettyScope class.

elanhickler commented 7 years ago

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?

elanhickler commented 7 years ago

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.

elanhickler commented 7 years ago

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?

elanhickler commented 7 years ago

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"

elanhickler commented 7 years ago

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:

⭐️ ParamSmoother

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;
    }
}

⭐️ ParamManager

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;
};
elanhickler commented 7 years ago

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);
  }
elanhickler commented 7 years ago

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.

elanhickler commented 7 years ago

I need a sliderChangeCallback, so I can add parameters to my smoother manager only when the slider changes rather than when the modulation changes

elanhickler commented 7 years ago

ok, i finished implementing a smoother manager to my satisfaction, you no longer need to rush this. Implement it whenever.

elanhickler commented 7 years ago

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.

RobinSchmidt commented 7 years ago

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.

RobinSchmidt commented 7 years ago

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)

RobinSchmidt commented 7 years ago

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

RobinSchmidt commented 7 years ago

so if you smoothly change that nominal unmodulated reference value, it should actually work...unless i'm missing something

RobinSchmidt commented 7 years ago

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

elanhickler commented 7 years ago

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

RobinSchmidt commented 7 years ago

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)

RobinSchmidt commented 7 years ago

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

elanhickler commented 7 years ago

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.

elanhickler commented 7 years ago

Right now this happens.

  1. I move a slider.
  2. Parameter callback is called with input value of slider.
  3. Ignore that and do getValue() to get value of just slider without modulation.
  4. By the way, modulators call this callback when they update value, too. Ignore that and take modulated value into account before I set the value of the underlying object with getModulatedValue(),
  5. Finally, whenever I increment the smoothers, I do the "callCallbacks" function to update the underlying object, all the while modulation updates will also call the callback, thereby executing all my smoothing code, but i have if statements to bypass when smoothing is already happening.

I think just I need a separate callback for slider updates or something.

elanhickler commented 7 years ago

I need:

  1. move slider
  2. do slider changed callback
  3. capture slider value via slider change callback
  4. set smoother target value in slider change callback
  5. when modulation happens, that calls the value change callback setting the modulated value, there I capture modulated value in a separate variable. Maybe you should just have a modulation change callback... or maybe just implement a smoother so I don't have to do all this working around!
  6. increment smoother per sample
  7. smoother sets unmodulated value while be incremented
  8. FINALLY: send unmodulated value + modulated value to underlying object
elanhickler commented 7 years ago

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!

.h

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;
};

.cpp

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();
}
elanhickler commented 7 years ago

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;
}
elanhickler commented 7 years ago

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.

RobinSchmidt commented 7 years ago

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

elanhickler commented 7 years ago

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();
    }
};
RobinSchmidt commented 7 years ago

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

RobinSchmidt commented 7 years ago

also, your monosynth is coupled with the smoother. i'd prefer a design that decouples the monosynth from the smoother/manager

elanhickler commented 7 years ago

ok, maybe you have better ideas. I wouldn't worry about supporting arbitrary variables unless you see an easy way to do it.

RobinSchmidt commented 7 years ago

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

RobinSchmidt commented 7 years ago

....but: how to marry that with the mod-system? that adds another layer of complication

elanhickler commented 7 years ago

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.

elanhickler commented 7 years ago

oh, well you're talking about value change callbacks, nevermind. In my value change callbacks I do smoother.getSmoothedValue() essentially.

RobinSchmidt commented 7 years ago

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

RobinSchmidt commented 7 years ago

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

RobinSchmidt commented 7 years ago

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")

elanhickler commented 7 years ago

My smoother was fine with meta-parameter system... actually... I never tested it... not sure.

RobinSchmidt commented 7 years ago

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?

elanhickler commented 7 years ago

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.

RobinSchmidt commented 7 years ago

...i mean, if you set the smoother target in the slider callback, how would that be supposed to work?

RobinSchmidt commented 7 years ago

hmm...ok..i think, i need to check the call-stack in the debugger

elanhickler commented 7 years ago

image

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
}
RobinSchmidt commented 7 years ago

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?