Modifying WaveShaper Distortion with Slider Parameter

Hello there. First post here, I’ll do my best to make it as clear as possible.

I’m using the Juce WaveShaper to add distortion to my Processor chain. The basic implementation was pretty straightforward. It works when I pass a simple lambda function to to the WaveShaper FunctionToUse parameter like this:

waveshaper.functionToUse = [](float x)
{
    return std::atan(x);
};

(this is in my PluginProcessor.ccp file)
Problem is, I want to make the distortion more aggressive when the user adjusts a slider. Modifying the x inside the lambda function is the best sounding way I’ve found to do this. However, this code

drive = *valueTree.getRawParameterValue(DRIVE_ID);

auto& waveshaper = processorChain.template get<2>();    
waveshaper.functionToUse = [](float x)
{
    return std::atan(x*drive);
};

Generates this error:

an enclosing-function local variable cannot be referenced in a lambda body unless it is in the capture list

But when I try to add anything to the capture list:

waveshaper.functionToUse = [&](float x)
{
    return std::atan(x*drive);
};

I get this error:

no suitable conversion function from “lambda float (float x)->float” to “float (*)(float)” exists

I’ve done a lot of research but can’t find anything online (general c++ discussions) that address this problem. Has anyone run into this before? It seems like a general c++ syntax issue, but this situation in JUCE is the only example I can find.

The Juce WaveShaper isn’t compatible with lambdas that have captures. However, since you are just changing the gain of the signal that goes into the processing, I think you could do that outside of the waveshaper itself, in your processBlock method.

I see. Is a coefficient inside the function the same thing as gain? I wanted to modify the curve of the function itself to be sharper. But I guess I’m confused on what “x” actually winds up being in this scenario.

Would modifying the gain in the processBlock as you suggested be equivalent to adding a dsp::Gain before the WaveShaper in my ProcessorChain and modifying that?

It’s the input signal.

The WaveShaper class is very simple, you can look at the code what it is doing and probably pretty easily make your own that allows parameters for the waveshaping instead of just providing a static simple function.

1 Like

iirc you can use a capturing lambda:

1 Like

That did it! Thanks!

Right, I forgot the function type is a template parameter in the WaveShaper, that also allows using a stateful function object with it, which can be more efficient than going through a std::function instance :

class MyWaveShaperFunc
{
public:
    float driveParam = 1.0f;
    float operator()(float x) const
    {
        return std::atan(driveParam * x);
    }
};

// then to instantiate the waveshaper :

dsp::WaveShaper<float, MyWaveShaperFunc> ws;
2 Likes

I got both of the solutions suggested here working (adjusting the gain of the input signal and modifying the coefficient in the WaveShaper function) and I’m going with the option of adjusting the gain. As mentioned I planned to adjust the WaveShaper function, but after recording/testing both version side-by-side I found that adjusting the input gain resulted in a much more consistent output volume (i.e. it didn’t get much louder as I increased gain, just more distorted) which works better for my purposes. Adjusting the coefficient inside the WaveShaper function on the other hand did make the output a lot louder. I also thought the gain adjustment method sounded a little better, but maybe that’s just in my head.

sorry for the basic c++ question here, but why does this work?

i see parallels to how ruby treats callable objects and i know that in ruby if something expects a Proc i can pass in an object that responds to .call and fool it - essentially what’s happening above. but it’s strange to see that idiom in c++.

also - is there a way to get outer variables enclosed in the above pattern? or is that where you’d just use a lamda?

Technically, the whole operatorXY functions in C++ are all simple member or free functions, so what happens inside the function bodies could also happen in any other usual function implementation and will lead to the same assembly generated as a regular function would.

The difference is a purely syntactical. If we added a second function to the example class above like

class MyWaveShaperFunc
{
public:
    float driveParam = 1.0f;

    float operator()(float x) const
    {
        return std::atan(driveParam * x);
    }

    float compute (float x) const    
    {
        return std::atan(driveParam * x);
    }
};

then both of these calls would lead to the very same result:

MyWaveShaperFunc m;

m.driveParam = 1.5f;

auto a = m (2.0f);
auto b = m.compute (2.0f);

jassert (a == b);

By adding a call operator we just add a specific function that is called in case the compiler sees a syntax like instance (arg) instead of the usual instance.function (arg). You can also overload the call operator, e.g. you could add a second version like for an example without any deeper sense bool operator() (int y) – just like you could add an overload to compute like bool compute (int y).

The only reason why using the call operator makes sense here, is that the classes that use it, e.g. the waveshaper itself when performing sample-wise processing or the dsp::AudioBlock::process function when performing block wise processing expect that the templated function object instance passed in can be invoked by a call operator. This makes sense, as it opens up the possibility to put in a raw C-Style function pointer as Function template type, which can be called by the call operator according to C standards. And now, as free functions, static member functions and lambdas without captures can be implicitly converted to function pointers, this is the default template argument choice for the Waveshaper Function template.

Does this answer your question?