Is the new LogSmoothedValue really logarithmic?

#1

The complexity and the addition in getNextValue is that irritates me, it looks more like a logarithmic ramp, where only the difference of the current and the target value is done logarithmically. With such a class you can’t have a smooth (log) fade between hertz values, or multiplication coefficients.

A maybe it should be renamed in LogarithmicRamp.

a real logarithmic change should be pretty straightforward

in setTargetValue()

step = exp(( log(target) - log(currentValue) ) / countdown );

in getNextValue()

currentValue *= step

#2

As it stands LogSmoothedValue is a logarithmic smoothing of a “regular” value. What you are proposing is an even smoothing of a logarithmic value in log space. The naming of these things is a little cumbersome, and the best I can come up with is:

SmoothedLinearValue (= LinearSmoothedValue)
SmoothedLogValue (= your proposal)
LogSmoothedLinearValue (= LogSmoothedValue)

1 Like
#3

:face_with_monocle: confused

I would say what I am proposing is to smooth a linear regular value in a logarithmic space.
Practical example a Hertz value (which is not a logarithmic-unit), but you (should) change it in exponential way.
The same is with a multiplication coefficient for gain (not dB).

#4

Yes, as I said the language here is pretty awkward.

The Hz unit is not logarithmic, but the way you use Hz values is. For example you cannot smoothly transition all the way down to 0 Hz. I can see your argument and I’m struggling to come up with a set of names that encompasses the kind of smoothing you would want for frequencies, the existing linear smoothing, and a logarithmic smoothing between two “linear” values (say 0 and 1).

I’m open to suggestions and I’m not fully convinced by anything I’ve thought of so far.

1 Like
#5

Okay, maybe what I want should be named ExponentialSmoothedValue, but I am not convinced be the naming of the new juce class.

#6

OK, I’m going to go with something like this:

SmoothedValue<float>                                     // = LinearSmoothedValue<float>
SmoothedValue<float, ValueSmoothingTypes::Linear>        // = LinearSmoothedValue<float>
SmoothedValue<float, ValueSmoothingTypes::Logarithmic>   // = Your proposal
dsp::LogRampedValue<float>                               // = LogSmoothedValue<float>

with a

template <typename FloatType>
using LinearSmoothedValue = SmoothedValue <FloatType, ValueSmoothingTypes::Linear>;

for backwards compatibility.

#7

Wow so confusing, what did you do to my class @t0m :rofl:

@chkn I see your argument too but it’s important to be ultra precise here, so the documentation and the names of the classes will be clear, which is not the case right now with the last commit.

When I submit the class LogSmoothedValue to the JUCE team, the point was that I wanted to smooth volume changes in a logarithmic way, in the specific context of the Convolution IR changes, with two volumes being applied in opposite direction during the transition. It solved the issue of audible artefacts with this improvement during changes. Before, the volume was increasing too quickly for our ears, and so users perceived artefacts during the beginning of the volume ramp, unless the ramp length would be stupidly high. So that class smoothes the volume between two values, in a way the ramp is linear in the log scale. It could be used also to model what happens when a user moves a volume slider, which is usually mapped in dB in the UI.

That’s because our ears perceive linearly volume changes moving linearly in a log scale, such as the dB scale.

That class takes two arguments, first one being the middle value in dB (the slope of the ramp in the dB/log scale, the speed for increase/decrease of the ramp), and a boolean which allows us to revert the whole curve if wanted (slow start vs slow end). That middle value parameter is important to fix the amount of “log smoothing” you want, meaning how much you want the curve to be far from something looking like a straight line. That curve will be the same whatever the values we have in the class for the starting point and the end point.

Here is how the smoothed value in the linear case looks like:

Log case (mid value at default value -40 dB):

Log case (mid value at value -20 dB):

Log case (mid value at default value -40 dB + slow end):

But what you proposed is indeed different. What you want is related with a change in another well know log mapping case, handling of cutoff frequencies. Like for volume, our ears perceive linearly frequency changes moving linearly in a log scale, which could be a scale called “dBHz”, so we don’t want to have a ramp going linearly from say 20 Hz to 20000 Hz, because we would have only high frequencies over the whole range and it would sound like a very quick change to our ears at first, and then slower and slower. But the code in the multiplicative case in the new JUCE class is not behaving exactly like my LogSmoothedValue. We don’t have a middle value parameter there to fix the speed of the curve. Instead, that speed is depending on the start and end values of the ramp !

Let’s see how it looks like with a change between 1 and 2 like previously with the new class:

Now if the change is between 10 and 100, here is the result:

In comparison, the change between 10 and 100 with the log class (mid at -40 dB):

So let’s look at the big picture now. We have three classes now that are meant to smooth parameters in a discrete way, meaning that at some discrete locations in time, we decide to change the value of a given parameter, and then we smooth that change using a ramp of a given length in samples. And we wait for a new change, either in this ramp duration or after to change the ramp again, by choosing a new target value, and selecting the current value as a source.

Now, that linear change might be a problem, so we want a ramp moving logarithmically, meaning linearly in a log scale, at a given speed which is not fixed just because we decided to dismiss the linear ramp in a linear scale. That speed could be either depending on a fixed parameter (LogSmoothedValue), or depending on the context and specifically the source + target values (new multiplicative SmoothedValue class).

Why don’t we do ONE new multiplicative class which allows us to specify the speed in the first way or the another one, instead of two different classes ? So we would have everything in juce_audio_basics (and @t0m you could fix the modules definition so that the cpp files with the unit tests are included only in the unit tests project, like we did for the FFT class for example, it’s weird to mix different unit tests practice in the code and having a unit test all the time compiled in the base code). And all the three classes do the same thing, so they should have a similar name, using either the word “ramp” or “smoothed”, but not mixing them differently.

#8

By the way, here are the uses I have for this kind of classes:

  • Every single time I need to apply a gain somewhere, for example with a volume or a mix knob, I use one to prevent abrupt volume changes (either linear or log could work)
  • When there is somewhere a change in the audio graph (like switching between two algorithms or the two IRs in Convolution), I use two log ramps in opposite direction to do a fade-in for the new one and a fade out with the other one

For stuff like cutoff frequency change in a filter, usually I’m just smoothing the change by design using the TPT structure, and LinearSmoothedValues for coefficients in the structure which are not a cutoff frequency. If I want to do better, I use generally continuous smoothing techniques, like a low-pass filter, ballistics filters or slew limiters, so I don’t have to use ramps between a source and a destination target, I’m just smoothing the control voltage all the time.

But sometimes it could be wanted to smooth the cutoff frequency or say a LFO rate, even with the TPT structure or the extra smoothing in the signal path. Using a ramp for this could be handy indeed (with the suggestion of @chkn). In this case, sometimes I have a value between 0 and 1, corresponding to the values that my frequency knob has, I smooth that value linearly, and then I apply a mapping between this new value and the cutoff frequencies range.

What about adding new jmap functions in the JUCE base code to do log to lin and lin to log mapping ?

1 Like
#9

The main reason for my confusion is:

I was under the impression that the “Smoothed” classes are for smoothing out un-regular parameter changes which can come from the GUI or the host. This was the first use case for it.

So you can do this in a linear (addition) way (or as I suggested in a exponential way (multiplication))

But in you class

That curve will be the same whatever the values we have in the class for the starting point and the end point.

This is the problem, if you know have un-regular parameter changes (or block sizes), this will result a very edgy curve.

Because when resetting the target the curve will be adjusted too.

So your class works entirely different than the first “smooth” class in juce. Its a complete different thing.

What you are describing is a more like “fade” class. IMHO for that special cross-fading use-case I wouldn’t use the logarithmic approach anyway. Just an equal power crossfade would be perfect. (But that’s a different story)

1 Like
#10

OK I understand your point now, you want to be able to change the target value as much as you want even during the time the parameter is being smoothed, which can change abruptly the curve of the ramp, which is not what is supposed to happen using your implementation.

However, your statement is wrong for the previous LinearSmoothedValue class. If you do a change between say 1 and 2 in 0.02 sec, and then a change between the current value in the last half of the ramp and 8 in still 0.02 sec, the change is abrupt too and the curve is steeper than before in the second ramp right ? Resetting the target adjusts the curve too, doesn’t it ?

I think it’s very important to provide a clear difference between classes made for continuous smoothing and classes made for discrete smoothing. For me, LinearSmoothedValue is in this former case, since any change provides an abrupt slope change for our ramp, and my LogSmoothedValue is in this case too. If you want something that could be used continuously without any edgy curve, you might need something else like a slew limiter or low-pass filtering.

But your own proposal is interesting because it is between the two approaches, we have something like a LogSmoothedValue but made once for all the potential values, and then we are always moving on this curve made once depending on the current value.

#11

its a complex topic. and there are different aspects.

-> smoothing-out un-regular parameter changes because the host/plugin-format doesn’t report ramp-data (btw some formats already report ramp data VST3)

-> smoothing fades with a defined start and stop point

-> smoothing with exponential growing/shrinking

-> smoothing with a low pass filter

#12

This thread is indeed very confusing and so is the new class. I’m using my own log smoother since a long time and it of course works as chkn describes… by linearly interpolating over a defined time in log space and this can be done for frequency and gain values (where it leads to linearly interpolating dB values).

For other I use something I call a “RampedValue” - also in linear and log space where the rate of change is fixed, not the time.

This +100.

#13

@pflugshaupt Could you be more precise about the differences between what does your “log smoother” and your log space ramped value ?

#14

I didn’t do anything to your class (other than the changes we discussed outside of this topic), so I’m not sure what you’re querying…

The current naming is much clearer than just LogSmoothedValue.

Yes, and your class is being used for that specific context - the Convolution IR changes are smoothed as you suggest.

The SmoothedValue<float, ValueSmoothingTypes::Multiplicative> class does exactly what you expect here.

For example, if you want a smooth change between 1.0 (0 dB) and 3.16228 (10 dB) in 10 steps, getNextValue() returns

1.12202 (1 dB)
1.25893 (2 dB)
1.41254 (3 dB)
...

and if you want a smooth change between 17.7828 (25 dB) and 5623.41 (75 dB) in 10 steps, getNextValue() returns

31.6228 (30 dB)
56.2341 (35 dB)
100.000 (40 dB)
...

You can, of course, achieve the same using LogRampedValue, but you need to configure the gradient of the ramp for each interval. To ramp linearly between 0 dB and 10 dB like the example above you need to call setLogParameters (-9.85218f, true), and to ramp between 25 dB and 75 dB you need to call setLogParameters (-25.4752f, true). The calculation of the required mid-point attenuation in either case is not trivial and, clearly, not portable across different intervals.

Again, the SmoothedValue<float, ValueSmoothingTypes::Multiplicative> class does what you expect here.

Consider a change from 440 Hz (A4) to 880 Hz (A5) in 12 steps. Here getNextValue() returns

466.164
493.883
523.251
...

which corresponds to an equal temperament tuning across the octave. To do the same with LogRampedValue you need to set the midpoint attenuation to Decibels::gainToDecibels ((622.254 - 440.0) / 440.0) which is cumbersome and, like the other examples, not portable to other frequency ranges.

Because, as I’ve shown in the examples above, how you need to use LogRampedValue to perform common tasks is too cumbersome. It’s best separated out into it’s own thing.

The DSP module is actually the outlier here - the rest of the codebase has unit tests guarded by a JUCE_UNIT_TESTS macro in the source files, rather than the main module cpp file. However, for consistency I’ll put the include in the main module guard for the DSP module. Even before this change the unit test is not compiled in the base code unless we’re specifically running the JUCE unit tests, just like all the other unit tests.

I tried to use a naming scheme where all three could be accommodated, but LogRampedValue is sufficiently different to the other two that it didn’t fit well. Given that it’s both reasonably difficult to use and it’s mainly useful for doing things like smoothing algorithm changes, the DSP module feels like a sensible home for it.

1 Like
#15

Hello @t0m and thanks for your explanations ! But some of my comments were addressed to @chkn :wink:

About my first comment, I wasn’t understanding the need to provide two different implementations of the class I submitted, but now it makes more sense. I admit I underestimated the need for a class providing “smoothing” using the implementation @chkn submitted, because I haven’t had any need for that in the way I work in plug-ins yet. But indeed it can be very helpful when you work with the ramp size and the start/end values as you suggested. On the other side, my class has more specific uses, so it’s fine for me if it is in the DSP module, to be used with the Convolution class as well.

For the unit tests, I was talking about the consistency, so what you just did is perfectly fine for me too.

Now I’m still feeling that there is room for improvement in the names of the three classes, since they are all there for smoothing and using ramps. That’s because “smoothing” is the function, and “ramp” is a mean to get that function, which is shared from all the methods, with others existing like slew limiters or filters (what I called continuous smoothing approaches, in opposition with discrete smoothing approaches where the smoothing functionality has to be enabled manually with time + source/target information). But at least having them separated and with the different names is the best approach we could get for now I guess.

However, for me it’s very important to make everything clearer in the documentation, so it could be a good thing to mention some direct applications, and to provide examples in the JUCE example projects. Then I won’t complain anymore !

#16

The log smoother interpolates linearly in log space so the target is reached in a specified time while the ramp thing interpolates linearly in log space with a constant speed, therefore the duration depends on the distance that needs to be covered. With constant speed, the duration depends on the distance in log space.

#17

Isn’t it way less of a headache if you always use linear smoothing and just have the value you are smoothing in the appropriate space (lin, log, exp, whatever)?
For example an equal power crossfade for a dryWet mix:

wet = smoothedWet.getNextValue();
mixWet = sqrt(wet);
mixDry = sqrt(1 - wet);
mix = signalDry * mixDry + signalWet * mixWet;

I may be missing something, but this looks like an overcomplicated approach for a pretty simple thing. Just my two cents…

3 Likes
#18

Thanks, so we have people who use log ramps defined with start + end + curve slew + length, start + end + length and slew being depending on the other parameters, and start + end with slew+length depending on start+end :smile: And @gustav-scholda approach is perfectly fine too. I thought that it could be possible to find one way to make everybody happy, my mistake :smile:

But at least I’m perfectly fine with @t0m code, and for other uses people could still use their own code I guess. Very interesting topic anyway, and my apologies for the headaches !

#19

I think something like a ChangeLimiter would also be useful. Here is my simple implementation:

template<typename T>
class ChangeLimiter
{
public:
	ChangeLimiter();
	
	void setTarget(const T target, const bool force = false);

	T getNext();

	T setTargetAndGetNext(const T target, const bool force = false);

	void setMaxChange(const T maxChange);

private:
	T m_current, m_target, m_maxChange;


	JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ChangeLimiter)
};


template<typename T>
inline ChangeLimiter<T>::ChangeLimiter()
{
	m_current = static_cast<T>(0);
	m_target = static_cast<T>(0);
	m_maxChange = static_cast<T>(0);
}

template<typename T>
inline void ChangeLimiter<T>::setTarget(const T target, const bool force)
{
	m_target = target;

	if (force) { m_current = m_target; }
}

template<typename T>
inline T ChangeLimiter<T>::getNext()
{
	const T change = m_target - m_current;
	m_current += jlimit(-m_maxChange, m_maxChange, change);

	return m_current;
}

template<typename T>
inline T ChangeLimiter<T>::setTargetAndGetNext(const T target, const bool force)
{
	setTarget(target, force);
	return getNext();
}

template<typename T>
inline void ChangeLimiter<T>::setMaxChange(const T maxChange)
{
	m_maxChange = maxChange;
}